From 104ad4cf99481cedfce0b7bb3e0f99bfd840fd72 Mon Sep 17 00:00:00 2001 From: "Ralph A. Smith" Date: Thu, 2 Aug 2018 22:03:12 -0400 Subject: [PATCH 01/29] add 0.7-compatible showarg methods --- src/io.jl | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 2 +- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/io.jl b/src/io.jl index 232489e7..5d8c2958 100644 --- a/src/io.jl +++ b/src/io.jl @@ -1,3 +1,5 @@ +# after this, functionality was incorporated into Base +if VERSION < v"0.7.0-DEV.1790" using ShowItLikeYouBuildIt Base.summary(A::AbstractInterpolation) = summary_build(A) @@ -53,6 +55,78 @@ function ShowItLikeYouBuildIt.showarg(io::IO, A::FilledExtrapolation{T,N,TI,IT,G print(io, ", ", A.fillvalue, ')') end +else + +function Base.showarg(io::IO, A::BSplineInterpolation{T,N,TW,ST,GT}, toplevel) where {T,N,TW,ST,GT} + print(io, "interpolate(") + Base.showarg(io, A.coefs, false) + print(io, ", ") + _showtypeparam(io, ST) + print(io, ", ") + _showtypeparam(io, GT) + if toplevel + print(io, ") with element type ",T) + else + print(io, ')') + end +end + +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, ", ") + Base.showarg(io, A.coefs, false) + print(io, ", ") + _showtypeparam(io, ST) + if toplevel + print(io, ") with element type ",T) + else + print(io, ')') + end +end + +_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) + Base.showarg(io, A, false) + i < N && print(io, ',') + end + N == 1 && print(io, ',') + print(io, ')') +end + +function Base.showarg(io::IO, A::ScaledInterpolation{T}, toplevel) where {T} + print(io, "scale(") + Base.showarg(io, A.itp, false) + print(io, ", ", A.ranges, ')') + if toplevel + print(io, " with element type ",T) + end +end + +function Base.showarg(io::IO, A::Extrapolation{T,N,TI,IT,GT,ET}, toplevel) where {T,N,TI,IT,GT,ET} + print(io, "extrapolate(") + Base.showarg(io, A.itp, false) + print(io, ", ") + _showtypeparam(io, ET) + print(io, ')') + if toplevel + print(io, " with element type ",T) + end +end + +function Base.showarg(io::IO, A::FilledExtrapolation{T,N,TI,IT,GT}, toplevel) where {T,N,TI,IT,GT} + print(io, "extrapolate(") + Base.showarg(io, A.itp, false) + print(io, ", ", A.fillvalue, ')') + if toplevel + print(io, " with element type ",T) + end +end + +end + _showtypeparam(io, ::Type{T}) where {T} = print(io, T.name.name, "()") _showtypeparam(io, ::Type{Quadratic{T}}) where {T} = diff --git a/test/runtests.jl b/test/runtests.jl index 3f3859f8..9c747f6b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -27,7 +27,7 @@ include("typing.jl") include("issues/runtests.jl") -# include("io.jl") +include("io.jl") include("convenience-constructors.jl") include("readme-examples.jl") From b117e5f40ee72e719bfa932870bcc3243d8c0478 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sat, 11 Aug 2018 23:36:46 +0100 Subject: [PATCH 02/29] Remove Compat dependency and don't export gradient, gradient!, hessian, hessian! --- REQUIRE | 4 +-- src/Interpolations.jl | 21 ++---------- src/b-splines/prefiltering.jl | 7 +--- test/b-splines/constant.jl | 2 +- test/b-splines/cubic.jl | 15 ++++----- test/b-splines/function-call-syntax.jl | 3 +- test/b-splines/linear.jl | 2 +- test/b-splines/mixed.jl | 2 +- test/b-splines/multivalued.jl | 1 - test/b-splines/non1.jl | 3 +- test/b-splines/quadratic.jl | 2 +- test/convenience-constructors.jl | 2 +- test/extrapolation/function-call-syntax.jl | 2 +- test/extrapolation/non1.jl | 2 +- test/extrapolation/runtests.jl | 4 +-- test/extrapolation/type-stability.jl | 2 +- test/gradient.jl | 38 +++++++++++----------- test/grid.jl | 2 +- test/gridded/function-call-syntax.jl | 3 +- test/gridded/gridded.jl | 3 +- test/gridded/mixed.jl | 3 +- test/io.jl | 2 +- test/issues/runtests.jl | 2 +- test/linear.jl | 2 +- test/nointerp.jl | 2 +- test/on-grid.jl | 2 +- test/readme-examples.jl | 2 +- test/runtests.jl | 1 - test/scaling/dimspecs.jl | 4 +-- test/scaling/function-call-syntax.jl | 3 +- test/scaling/nointerp.jl | 7 ++-- test/scaling/scaling.jl | 6 ++-- test/scaling/withextrap.jl | 3 +- test/type-instantiation.jl | 2 +- test/typing.jl | 6 ++-- 35 files changed, 66 insertions(+), 101 deletions(-) diff --git a/REQUIRE b/REQUIRE index 91a313b1..994bef36 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,8 +1,6 @@ -julia 0.6 +julia 0.7 -ShowItLikeYouBuildIt WoodburyMatrices 0.1.5 Ratios AxisAlgorithms 0.3.0 OffsetArrays -Compat 0.59 diff --git a/src/Interpolations.jl b/src/Interpolations.jl index 5cb222bf..04c7b08e 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, @@ -37,21 +29,12 @@ export # extrapolation/extrapolation.jl # scaling/scaling.jl -using Compat -using Compat.LinearAlgebra, Compat.SparseArrays +using LinearAlgebra, SparseArrays using WoodburyMatrices, Ratios, AxisAlgorithms, OffsetArrays -import Base: convert, size, getindex, promote_rule, +import Base: convert, size, axes, getindex, promote_rule, ndims, eltype, checkbounds -@static if VERSION < v"0.7.0-DEV.3449" - import Base: gradient -else - import LinearAlgebra: gradient -end - -import Compat: axes - abstract type Flag end abstract type InterpolationType <: Flag end struct NoInterp <: InterpolationType end diff --git a/src/b-splines/prefiltering.jl b/src/b-splines/prefiltering.jl index b485f496..1dc58419 100644 --- a/src/b-splines/prefiltering.jl +++ b/src/b-splines/prefiltering.jl @@ -20,12 +20,7 @@ end 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)) -end +ct!(coefs, indscp, A, indsA) = copyto!(coefs, CartesianIndices(indscp), A, CartesianIndices(indsA)) 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}} diff --git a/test/b-splines/constant.jl b/test/b-splines/constant.jl index 6daecbb9..4a673a75 100644 --- a/test/b-splines/constant.jl +++ b/test/b-splines/constant.jl @@ -1,7 +1,7 @@ module ConstantTests using Interpolations -using Compat.Test +using Test # Instantiation N1 = 10 diff --git a/test/b-splines/cubic.jl b/test/b-splines/cubic.jl index e2f2b086..436417b6 100644 --- a/test/b-splines/cubic.jl +++ b/test/b-splines/cubic.jl @@ -1,6 +1,6 @@ module CubicTests -using Compat.Test +using Test using Interpolations for (constructor, copier) in ((interpolate, identity), (interpolate!, copy)) @@ -55,8 +55,7 @@ end module CubicGradientTests -using Interpolations, Compat.Test, Compat.LinearAlgebra -using Compat: range +using Interpolations, Test, LinearAlgebra ix = 1:15 f(x) = cos((x-1)*2pi/(length(ix)-1)) @@ -71,16 +70,16 @@ for (constructor, copier) in ((interpolate, identity), (interpolate!, copy)) 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))))) + @test ≈(g(x),(Interpolations.gradient(itp,x))[1],atol=cbrt(cbrt(eps(g(x))))) 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()) +@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 ≈((gradient(itp_flat_c,0.5))[1],0,atol=eps()) -@test ≈((gradient(itp_flat_c,ix[end] + 0.5))[1],0,atol=eps()) +@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 index 5f553b7f..153d9b18 100644 --- a/test/b-splines/function-call-syntax.jl +++ b/test/b-splines/function-call-syntax.jl @@ -1,7 +1,6 @@ module ExtrapFunctionCallSyntax -using Compat.Test, Interpolations, DualNumbers -using Compat: range +using Test, Interpolations, DualNumbers # Test if b-spline interpolation by function syntax yields identical results f(x) = sin((x-3)*2pi/9 - 1) diff --git a/test/b-splines/linear.jl b/test/b-splines/linear.jl index f1e1b5ec..e0d32838 100644 --- a/test/b-splines/linear.jl +++ b/test/b-splines/linear.jl @@ -1,7 +1,7 @@ module LinearTests using Interpolations -using Compat.Test +using Test xmax = 10 g1(x) = sin((x-3)*2pi/(xmax-1)-1) diff --git a/test/b-splines/mixed.jl b/test/b-splines/mixed.jl index 2e9a3325..cfd312f9 100644 --- a/test/b-splines/mixed.jl +++ b/test/b-splines/mixed.jl @@ -1,6 +1,6 @@ module MixedTests -using Interpolations, Compat, Compat.Test, Compat.SharedArrays, Compat.Random +using Interpolations, Test, SharedArrays, Random N = 10 diff --git a/test/b-splines/multivalued.jl b/test/b-splines/multivalued.jl index 55400472..04bdac5b 100644 --- a/test/b-splines/multivalued.jl +++ b/test/b-splines/multivalued.jl @@ -3,7 +3,6 @@ module NonNumeric # Test interpolation with a multi-valued type using Interpolations -using Compat import Base: +, -, *, / diff --git a/test/b-splines/non1.jl b/test/b-splines/non1.jl index 410bc6c9..901a77ed 100644 --- a/test/b-splines/non1.jl +++ b/test/b-splines/non1.jl @@ -1,7 +1,6 @@ module Non1Tests -using Interpolations, OffsetArrays, AxisAlgorithms, Compat.Test -using Compat: axes +using Interpolations, OffsetArrays, AxisAlgorithms, Test # 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) diff --git a/test/b-splines/quadratic.jl b/test/b-splines/quadratic.jl index 70214bf3..15954181 100644 --- a/test/b-splines/quadratic.jl +++ b/test/b-splines/quadratic.jl @@ -1,6 +1,6 @@ module QuadraticTests -using Interpolations, Compat.Test +using Interpolations, Test for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy)) f(x) = sin((x-3)*2pi/9 - 1) diff --git a/test/convenience-constructors.jl b/test/convenience-constructors.jl index b7774c60..84fc850d 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 diff --git a/test/extrapolation/function-call-syntax.jl b/test/extrapolation/function-call-syntax.jl index 58810c39..9e3257f6 100644 --- a/test/extrapolation/function-call-syntax.jl +++ b/test/extrapolation/function-call-syntax.jl @@ -1,6 +1,6 @@ module ExtrapFunctionCallSyntax -using Compat.Test, Interpolations, DualNumbers +using Test, Interpolations, DualNumbers # Test if extrapolation by function syntax yields identical results f(x) = sin((x-3)*2pi/9 - 1) diff --git a/test/extrapolation/non1.jl b/test/extrapolation/non1.jl index 79fe7968..7d86d294 100644 --- a/test/extrapolation/non1.jl +++ b/test/extrapolation/non1.jl @@ -1,6 +1,6 @@ module ExtrapNon1 -using Compat.Test, Interpolations, OffsetArrays +using Test, Interpolations, OffsetArrays f(x) = sin((x-3)*2pi/9 - 1) xinds = -3:6 diff --git a/test/extrapolation/runtests.jl b/test/extrapolation/runtests.jl index 6526bf0d..eee37ba1 100644 --- a/test/extrapolation/runtests.jl +++ b/test/extrapolation/runtests.jl @@ -1,8 +1,8 @@ module ExtrapTests -using Compat.Test, DualNumbers +using DualNumbers using Interpolations - +using Test f(x) = sin((x-3)*2pi/9 - 1) xmax = 10 diff --git a/test/extrapolation/type-stability.jl b/test/extrapolation/type-stability.jl index 3345dd99..5ae7e8ee 100644 --- a/test/extrapolation/type-stability.jl +++ b/test/extrapolation/type-stability.jl @@ -1,6 +1,6 @@ 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) diff --git a/test/gradient.jl b/test/gradient.jl index a22d2b8c..219144e8 100644 --- a/test/gradient.jl +++ b/test/gradient.jl @@ -1,6 +1,6 @@ module GradientTests -using Compat, Compat.Test, Interpolations, DualNumbers, Compat.LinearAlgebra +using Test, Interpolations, DualNumbers, LinearAlgebra nx = 10 f1(x) = sin((x-3)*2pi/(nx-1) - 1) @@ -13,8 +13,8 @@ itp1 = interpolate(Float64[f1(x) for x in 1:nx-1], g = Array{Float64}(undef, 1) for x in 1:nx - @test gradient(itp1, x)[1] == 0 - @test gradient!(g, itp1, x)[1] == 0 + @test Interpolations.gradient(itp1, x)[1] == 0 + @test Interpolations.gradient!(g, itp1, x)[1] == 0 @test g[1] == 0 end @@ -25,14 +25,14 @@ 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),(Interpolations.gradient(itp,x))[1],atol=abs(0.1 * g1(x))) + @test ≈(g1(x),(Interpolations.gradient!(g,itp,x))[1],atol=abs(0.1 * 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(itp, x)[1] + gtmp = Interpolations.gradient(itp, x)[1] xd = dual(x, 1) @test epsilon(itp[xd]) ≈ gtmp end @@ -44,8 +44,8 @@ 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),(Interpolations.gradient(itp_grid,x))[1],atol=abs(0.5 * g1(x))) + @test ≈(g1(x),(Interpolations.gradient!(g,itp_grid,x))[1],atol=abs(0.5 * g1(x))) @test ≈(g1(x),g[1],atol=abs(0.5 * g1(x))) end @@ -53,14 +53,14 @@ end 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),(Interpolations.gradient(itp1,x))[1],atol=abs(0.05 * g1(x))) + @test ≈(g1(x),(Interpolations.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] + gtmp = Interpolations.gradient(itp1, x)[1] xd = dual(x, 1) @test epsilon(itp1[xd]) ≈ gtmp end @@ -79,7 +79,7 @@ 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) +@test (Interpolations.gradient(iq,x))[1] ≈ dqfunc(x) # 2d (biquadratic) p = [(x-1.75)^2 for x = 1:7] @@ -87,14 +87,14 @@ 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) +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 = gradient(iq, 4, 3) +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) @@ -102,7 +102,7 @@ g = gradient(iq, 4, 3) 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) +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) @@ -119,19 +119,19 @@ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell) y = rand()*(nx-2)+1.5 xd = dual(x, 1) yd = dual(y, 1) - gtmp = gradient(itp_a, x, y) + 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 = gradient(itp_b, x, y) + 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 = gradient(itp_c, ix, y) + gtmp = Interpolations.gradient(itp_c, ix, y) @test length(gtmp) == 1 @test epsilon(itp_c[ix,yd]) ≈ gtmp[1] - gtmp = gradient(itp_d, x, iy) + gtmp = Interpolations.gradient(itp_d, x, iy) @test length(gtmp) == 1 @test epsilon(itp_d[xd,iy]) ≈ gtmp[1] end diff --git a/test/grid.jl b/test/grid.jl index 22af0969..5160efce 100644 --- a/test/grid.jl +++ b/test/grid.jl @@ -1,6 +1,6 @@ module GridTests -using Interpolations, Compat.Test +using Interpolations, Test # On-grid values A = randn(4,10) diff --git a/test/gridded/function-call-syntax.jl b/test/gridded/function-call-syntax.jl index 5cb36b1f..4d62b394 100644 --- a/test/gridded/function-call-syntax.jl +++ b/test/gridded/function-call-syntax.jl @@ -1,7 +1,6 @@ module GriddedFunctionCallSyntax -using Interpolations, Compat.Test -using Compat: range +using Interpolations, Test for D in (Constant, Linear), G in (OnCell, OnGrid) ## 1D diff --git a/test/gridded/gridded.jl b/test/gridded/gridded.jl index 8d1d4c05..0a6826a2 100644 --- a/test/gridded/gridded.jl +++ b/test/gridded/gridded.jl @@ -1,7 +1,6 @@ module LinearTests -using Interpolations, Compat.Test -using Compat: range +using Interpolations, Test for D in (Constant, Linear), G in (OnCell, OnGrid) ## 1D diff --git a/test/gridded/mixed.jl b/test/gridded/mixed.jl index c24047c0..4556c710 100644 --- a/test/gridded/mixed.jl +++ b/test/gridded/mixed.jl @@ -1,7 +1,6 @@ module MixedTests -using Interpolations, Compat.Test -using Compat: range +using Interpolations, Test 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)))) diff --git a/test/io.jl b/test/io.jl index 66fba08b..cbd3f6b7 100644 --- a/test/io.jl +++ b/test/io.jl @@ -1,7 +1,7 @@ module IOTests -using Compat.Test using Interpolations +using Test SPACE = " " diff --git a/test/issues/runtests.jl b/test/issues/runtests.jl index 7d7857fa..db21f9d1 100644 --- a/test/issues/runtests.jl +++ b/test/issues/runtests.jl @@ -1,6 +1,6 @@ module Issue34 -using Interpolations, Compat.Test +using Interpolations, Test A = rand(1:20, 100, 100) 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..e6e02476 100644 --- a/test/nointerp.jl +++ b/test/nointerp.jl @@ -1,6 +1,6 @@ module NoInterpTests println("Testing NoInterp...") -using Interpolations, Compat.Test +using Interpolations, Test a = reshape(1:12, 3, 4) ai = interpolate(a, NoInterp(), OnGrid()) 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..74a41156 100644 --- a/test/readme-examples.jl +++ b/test/readme-examples.jl @@ -1,6 +1,6 @@ module ReadmeExampleTests # verify examples from README.md run -using Interpolations, Compat.Test +using Interpolations, Test ## Bsplines a = randn(5) diff --git a/test/runtests.jl b/test/runtests.jl index 9c747f6b..c9c1798a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,5 @@ module RunTests -using Compat.Test using Interpolations diff --git a/test/scaling/dimspecs.jl b/test/scaling/dimspecs.jl index 2e8ab9b6..54808572 100644 --- a/test/scaling/dimspecs.jl +++ b/test/scaling/dimspecs.jl @@ -1,6 +1,6 @@ module ScalingDimspecTests -using Interpolations, DualNumbers, Compat.Test, Compat.LinearAlgebra +using Interpolations, DualNumbers, Test, LinearAlgebra xs = -pi:(2pi/10):pi-2pi/10 ys = -2:.1:2 @@ -12,7 +12,7 @@ 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))) - g = gradient(sitp, x, y) + g = Interpolations.gradient(sitp, x, y) fx = epsilon(sitp[dual(x,1), dual(y,0)]) fy = epsilon(sitp[dual(x,0), dual(y,1)]) diff --git a/test/scaling/function-call-syntax.jl b/test/scaling/function-call-syntax.jl index c533c41f..dae55f0e 100644 --- a/test/scaling/function-call-syntax.jl +++ b/test/scaling/function-call-syntax.jl @@ -1,7 +1,6 @@ module ScalingFunctionCallTests -using Interpolations, Compat, Compat.Test -using Compat: range +using Interpolations, Test # Model linear interpolation of y = -3 + .5x by interpolating y=x # and then scaling to the new x range diff --git a/test/scaling/nointerp.jl b/test/scaling/nointerp.jl index db3e2cef..6d0c2091 100644 --- a/test/scaling/nointerp.jl +++ b/test/scaling/nointerp.jl @@ -1,7 +1,6 @@ module ScalingNoInterpTests -using Interpolations, Compat.Test, Compat.LinearAlgebra, Compat.Random -using Compat: range +using Interpolations, Test, LinearAlgebra, Random xs = -pi:2pi/10:pi f1(x) = sin(x) @@ -20,7 +19,7 @@ for (ix,x0) in enumerate(xs[1:end-1]), y0 in ys @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!` @@ -34,6 +33,6 @@ itpb = interpolate(zb, (NoInterp(), BSpline(Linear())), OnGrid()) 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) +@test Interpolations.gradient(sitpa, 3.0, 3) == Interpolations.gradient(sitpb, 3, 3.0) end diff --git a/test/scaling/scaling.jl b/test/scaling/scaling.jl index ed4488fc..1f1dc44c 100644 --- a/test/scaling/scaling.jl +++ b/test/scaling/scaling.jl @@ -1,7 +1,7 @@ module ScalingTests -using Interpolations, Compat -using Compat.Test, Compat.LinearAlgebra +using Interpolations +using Test, LinearAlgebra # Model linear interpolation of y = -3 + .5x by interpolating y=x # and then scaling to the new x range @@ -39,7 +39,7 @@ itp = interpolate(ys, BSpline(Linear()), OnGrid()) sitp = @inferred scale(itp, xs) for x in -pi:.1:pi - g = @inferred(gradient(sitp, x))[1] + g = @inferred(Interpolations.gradient(sitp, x))[1] @test ≈(cos(x),g,atol=0.05) end diff --git a/test/scaling/withextrap.jl b/test/scaling/withextrap.jl index 86a78ec6..f0df91fa 100644 --- a/test/scaling/withextrap.jl +++ b/test/scaling/withextrap.jl @@ -1,7 +1,6 @@ module ScalingWithExtrapTests -using Interpolations, Compat.Test -using Compat: range +using Interpolations, Test xs = range(-5, stop=5, length=10) ys = map(sin, xs) 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..39c5343b 100644 --- a/test/typing.jl +++ b/test/typing.jl @@ -1,6 +1,6 @@ module TypingTests -using Interpolations, Compat.Test, Compat.LinearAlgebra +using Interpolations, Test, LinearAlgebra nx = 10 f(x) = convert(Float32, x^3/(nx-1)) @@ -22,10 +22,10 @@ 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))) + @test ≈([g(x)], Interpolations.gradient(itp,x),atol=abs(0.1 * g(x))) end -@test typeof(gradient(itp, 3.5f0)[1]) == Float32 +@test typeof(Interpolations.gradient(itp, 3.5f0)[1]) == Float32 # Rational element types R = Rational{Int}[x^2//10 for x in 1:10] From c2fb19021b86ea4d45af0c03c1e833d6be28fb98 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 13 Aug 2018 23:58:40 +0000 Subject: [PATCH 03/29] Add parentaxes, it, and gt fields to BSplineInterpolation Storing parentaxes will allow us to do padding via OffsetArrays, which has the major advantage of keeping the indices of the padded array in-register with those of the original array. This should simplify a lot of code. --- src/b-splines/b-splines.jl | 41 +++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/b-splines/b-splines.jl b/src/b-splines/b-splines.jl index 645e8cf5..7ebc8940 100644 --- a/src/b-splines/b-splines.jl +++ b/src/b-splines/b-splines.jl @@ -14,23 +14,27 @@ BSpline(::D) where {D<:Degree} = BSpline{D}() bsplinetype(::Type{BSpline{D}}) where {D<:Degree} = D -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},GT<:DimSpec{GridType},Axs<:Tuple{Vararg{AbstractUnitRange,N}}} <: AbstractInterpolation{T,N,IT,GT} coefs::TCoefs + parentaxes::Axs + it::IT + gt::GT 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, gt::GT, axs) where {N,Tel,TWeights<:Real,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} + # 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)))") - c = zero(TWeights) - for _ in 2:N - c *= c - end + isconcretetype(IT) || err_concrete(IT) + isconcretetype(typeof(A)) || warn_concrete(A) + + # 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,GT,typeof(axs)}(A, fix_axis.(axs), it, gt) end # Utilities for working either with scalars or tuples/tuple-types @@ -68,13 +72,17 @@ end @inline axes(itp::BSplineInterpolation{T,N,TCoefs,IT,GT,pad}) where {T,N,TCoefs,IT,GT,pad} = indices_removepad.(axes(itp.coefs), pad) +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)) 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 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) + Apad = prefilter(TWeights, TC, A, it, gt) + BSplineInterpolation(TWeights, Apad, it, gt, axes(A)) end function interpolate(A::AbstractArray, it::IT, gt::GT) where {IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} interpolate(tweight(A), tcoef(A), A, it, gt) @@ -91,7 +99,12 @@ 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!(::Type{TWeights}, A, it::IT, gt::GT) where {TWeights,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} + # Set the bounds of the interpolant inward, if necessary + axsA = axes(A) + axspad = padded_axes(axsA, it) + BSplineInterpolation(TWeights, prefilter!(TWeights, A, it, gt), it, gt, fix_axis.(padinset.(axsA, axspad))) +end function interpolate!(A::AbstractArray, it::IT, gt::GT) where {IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} interpolate!(tweight(A), A, it, gt) end From 2f893eba3ee8417226f11fe01dc757e14c634e83 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Tue, 14 Aug 2018 01:12:32 +0000 Subject: [PATCH 04/29] Implement the new scheme for BSpline (and partial for NoInterp) This makes several major changes: - it eliminates the generated functions and switches emphasis to the value domain rather than the type domain - it uses OffsetArrays to handle the padding. When using interpolate!, the axes may be narrowed to account for the boundary conditions. - now, the axes of the output correspond to the region where we can guarantee that we reconstruct the original array values - switches to the bounds-checking framework in base (may not be working yet) --- REQUIRE | 1 + src/Interpolations.jl | 190 +++++++++++++++--- src/b-splines/b-splines.jl | 89 ++++----- src/b-splines/constant.jl | 50 +---- src/b-splines/cubic.jl | 192 +++++------------- src/b-splines/indexing.jl | 357 +++++++++++++++++++++------------- src/b-splines/linear.jl | 86 +------- src/b-splines/prefiltering.jl | 87 ++++----- src/b-splines/quadratic.jl | 171 ++++------------ src/filter1d.jl | 2 + src/io.jl | 152 +++++---------- src/nointerp/nointerp.jl | 41 ++-- src/utils.jl | 18 ++ test/b-splines/constant.jl | 49 +++-- test/b-splines/cubic.jl | 65 +++++-- test/b-splines/linear.jl | 30 ++- test/b-splines/mixed.jl | 44 +++-- test/b-splines/multivalued.jl | 39 ++-- test/b-splines/non1.jl | 59 ++++-- test/b-splines/quadratic.jl | 56 ++++-- test/b-splines/runtests.jl | 2 +- test/gradient.jl | 50 ++--- test/io.jl | 84 ++++---- test/nointerp.jl | 14 +- test/readme-examples.jl | 32 +-- test/runtests.jl | 20 +- test/typing.jl | 9 +- 27 files changed, 969 insertions(+), 1020 deletions(-) diff --git a/REQUIRE b/REQUIRE index 994bef36..056c8db9 100644 --- a/REQUIRE +++ b/REQUIRE @@ -4,3 +4,4 @@ WoodburyMatrices 0.1.5 Ratios AxisAlgorithms 0.3.0 OffsetArrays +StaticArrays diff --git a/src/Interpolations.jl b/src/Interpolations.jl index 04c7b08e..6d78f1ee 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -30,10 +30,10 @@ export # scaling/scaling.jl using LinearAlgebra, SparseArrays -using WoodburyMatrices, Ratios, AxisAlgorithms, OffsetArrays +using StaticArrays, WoodburyMatrices, Ratios, AxisAlgorithms, OffsetArrays -import Base: convert, size, axes, getindex, promote_rule, - ndims, eltype, checkbounds +using Base: @propagate_inbounds +import Base: convert, size, axes, promote_rule, ndims, eltype, checkbounds abstract type Flag end abstract type InterpolationType <: Flag end @@ -48,57 +48,183 @@ abstract type AbstractInterpolation{T,N,IT<:DimSpec{InterpolationType},GT<:DimSp 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 +""" + BoundaryCondition + +An abstract type with one of the following values (see the help for each for details): + +- `Throw()` +- `Flat()` +- `Line()` +- `Free()` +- `Periodic()` +- `Reflect()` +- `InPlace()` +- `InPlaceQ()` +""" +abstract type BoundaryCondition <: Flag end +struct Throw <: BoundaryCondition end +struct Flat <: BoundaryCondition end +struct Line <: BoundaryCondition end +struct Free <: BoundaryCondition end +struct Periodic <: BoundaryCondition end +struct Reflect <: BoundaryCondition end +struct InPlace <: BoundaryCondition 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 <: BoundaryCondition 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) +Base.IndexStyle(::Type{<:AbstractInterpolation}) = IndexCartesian() + +size(itp::AbstractInterpolation) = map(length, itp.parentaxes) +size(exp::AbstractExtrapolation) = size(exp.itp) +axes(itp::AbstractInterpolation) = itp.parentaxes +axes(exp::AbstractExtrapolation) = axes(exp.itp) + +bounds(itp::AbstractInterpolation) = map(twotuple, lbounds(itp), ubounds(itp)) +twotuple(r::AbstractUnitRange) = (first(r), last(r)) +twotuple(x, y) = (x, y) +bounds(itp::AbstractInterpolation, d) = bounds(itp)[d] +lbounds(itp::AbstractInterpolation) = _lbounds(itp.parentaxes, itpflag(itp), gridflag(itp)) +ubounds(itp::AbstractInterpolation) = _ubounds(itp.parentaxes, itpflag(itp), gridflag(itp)) +_lbounds(axs::NTuple{N,Any}, itp, gt) where N = ntuple(d->lbound(axs[d], iextract(itp,d), iextract(gt,d)), Val(N)) +_ubounds(axs::NTuple{N,Any}, itp, gt) where N = ntuple(d->ubound(axs[d], iextract(itp,d), iextract(gt,d)), Val(N)) + itptype(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = IT itptype(::Type{ITP}) where {ITP<:AbstractInterpolation} = itptype(supertype(ITP)) -itptype(itp::AbstractInterpolation ) = itptype(typeof(itp)) +itptype(itp::AbstractInterpolation) = itptype(typeof(itp)) +itpflag(itp::AbstractInterpolation) = itp.it gridtype(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = GT gridtype(::Type{ITP}) where {ITP<:AbstractInterpolation} = gridtype(supertype(ITP)) gridtype(itp::AbstractInterpolation) = gridtype(typeof(itp)) +gridflag(itp::AbstractInterpolation) = itp.gt ndims(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = 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{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 + +""" + 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. -@inline function Base._getindex(::IndexCartesian, A::AbstractInterpolation{T,N}, I::Vararg{Int,N}) where {T,N} # ambiguity resolution - @inbounds r = getindex(A, I...) - r +# 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}) + gradient(itp, to_indices(itp, x)) end -@inline function Base._getindex(::IndexCartesian, A::AbstractInterpolation{T,N}, I::Vararg{Real,N}) where {T,N} - @inbounds r = getindex(A, I...) - r +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->gradient(itp, y), Iterators.product(x...)) +end +function hessian(itp::AbstractInterpolation, x::Vararg{ExpandedIndexTypes}) + map(y->hessian(itp, y), Iterators.product(x...)) +end + +# 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...) +@inline function getindex(itp::AbstractInterpolation{T,1}, i::Integer, j::Integer) where T + @boundscheck (j == 1 || Base.throw_boundserror(itp, (i, j))) + @inbounds itp(i) +end + +# deprecate getindex for other numeric indices +@deprecate getindex(itp::AbstractInterpolation{T,N}, i::Vararg{Number,N}) where {T,N} itp(i...) include("nointerp/nointerp.jl") include("b-splines/b-splines.jl") -include("gridded/gridded.jl") -include("extrapolation/extrapolation.jl") -include("scaling/scaling.jl") +# include("gridded/gridded.jl") +# include("extrapolation/extrapolation.jl") +# include("scaling/scaling.jl") include("utils.jl") include("io.jl") include("convenience-constructors.jl") diff --git a/src/b-splines/b-splines.jl b/src/b-splines/b-splines.jl index 7ebc8940..f3f8eda0 100644 --- a/src/b-splines/b-splines.jl +++ b/src/b-splines/b-splines.jl @@ -13,6 +13,10 @@ struct BSpline{D<:Degree} <: InterpolationType end BSpline(::D) where {D<:Degree} = BSpline{D}() bsplinetype(::Type{BSpline{D}}) where {D<:Degree} = D +bsplinetype(::BS) where {BS<:BSpline} = bsplinetype(BS) + +degree(::BSpline{D}) where D<:Degree = D() +degree(::NoInterp) = NoInterp() struct BSplineInterpolation{T,N,TCoefs<:AbstractArray,IT<:DimSpec{BSpline},GT<:DimSpec{GridType},Axs<:Tuple{Vararg{AbstractUnitRange,N}}} <: AbstractInterpolation{T,N,IT,GT} coefs::TCoefs @@ -37,53 +41,44 @@ function BSplineInterpolation(::Type{TWeights}, A::AbstractArray{Tel,N}, it::IT, BSplineInterpolation{T,N,typeof(A),IT,GT,typeof(axs)}(A, fix_axis.(axs), it, gt) 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 -end +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) -@inline axes(itp::BSplineInterpolation{T,N,TCoefs,IT,GT,pad}) where {T,N,TCoefs,IT,GT,pad} = - indices_removepad.(axes(itp.coefs), pad) +# The unpadded defaults +lbound(ax, ::BSpline, ::OnCell) = first(ax) - 0.5 +ubound(ax, ::BSpline, ::OnCell) = last(ax) + 0.5 +lbound(ax, ::BSpline, ::OnGrid) = first(ax) +ubound(ax, ::BSpline, ::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)) -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 + +count_interp_dims(::Type{BSI}, n) where BSI<:BSplineInterpolation = count_interp_dims(itptype(BSI), n) function interpolate(::Type{TWeights}, ::Type{TC}, A, it::IT, gt::GT) where {TWeights,TC,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} Apad = prefilter(TWeights, TC, A, it, gt) BSplineInterpolation(TWeights, Apad, it, gt, axes(A)) end + +""" + itp = interpolate(A, interpmode, gridstyle) + +Interpolate an array `A` in the mode determined by `interpmode` and `gridstyle`. +`interpmode` may be one of + +- `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, gt::GT) where {IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} interpolate(tweight(A), tcoef(A), A, it, gt) end @@ -109,24 +104,7 @@ function interpolate!(A::AbstractArray, it::IT, gt::GT) where {IT<:DimSpec{BSpli interpolate!(tweight(A), A, it, gt) 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)) -end +lut!(dl, d, du) = lu!(Tridiagonal(dl, d, du), Val(false)) include("constant.jl") include("linear.jl") @@ -137,4 +115,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..b416ef74 100644 --- a/src/b-splines/constant.jl +++ b/src/b-splines/constant.jl @@ -6,48 +6,16 @@ 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]))) +function base_rem(::Constant, bounds, x) + xm = roundbounds(x, bounds) + δx = x - xm + fast_trunc(Int, xm), δx 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 +expand_index(::Constant, xi::Number, ax::AbstractUnitRange, δx) = (xi,) -""" -`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) -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) = (oneunit(δx),) +gradient_weights(::Constant, δx) = (zero(δx),) +hessian_weights(::Constant, δx) = (zero(δx),) -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..1084b531 100644 --- a/src/b-splines/cubic.jl +++ b/src/b-splines/cubic.jl @@ -24,145 +24,57 @@ 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 -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`. - -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 base_rem(::Cubic, bounds, x) + xf = floorbounds(x, bounds) + xf -= ifelse(xf > bounds[2]-1, oneunit(xf), zero(xf)) + δx = x - xf + fast_trunc(Int, xf), δx 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 +expand_index(::Cubic{BC}, xi::Number, ax::AbstractUnitRange, δx) where BC = (xi-1, xi, xi+1, xi+2) +expand_index(::Cubic{Periodic}, xi::Number, ax::AbstractUnitRange, δx) = + (modrange(xi-1, ax), modrange(xi, ax), modrange(xi+1, ax), modrange(xi+2, ax)) + +# expand_coefs(::Type{BSpline{Cubic{BC}}}, δx) = cvcoefs(δx) +# expand_coefs(::Type{BSpline{Cubic{BC}}}, dref, d, δx) = ifelse(d==dref, cgcoefs(δx), cvcoefs(δx)) +# function expand_coefs(::Type{BSpline{Cubic{BC}}}, dref1, dref2, d, δx) +# if dref1 == dref2 +# d == dref1 ? chcoefs(δx) : cvcoefs(δx) +# else +# d == dref1 | d == dref2 ? cgcoefs(δx) : cvcoefs(δx) +# end +# 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 -""" -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 - - 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 +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 `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) +hessian_weights(::Cubic, δx) = (1-δx, 3*δx-2, 3*(1-δx)-2, δx) -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}}) = 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 +83,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 +97,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 +123,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 +152,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 +177,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 +201,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}, ::GridType) where {T,TC} + dl, d, du = inner_system_diags(T,n,degree) specs = WoodburyMatrices.sparse_factors(T, n, (1, n, du[1]), @@ -308,8 +220,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}, ::GridType) 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..60805914 100644 --- a/src/b-splines/indexing.jl +++ b/src/b-splines/indexing.jl @@ -1,181 +1,258 @@ -using Base.Cartesian +### Primary evaluation (indexing) entry points -import Base.getindex +@inline function (itp::BSplineInterpolation{T,N})(x::Vararg{Number,N}) where {T,N} + @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x)) + expand_value(itp, x) +end +@inline function (itp::BSplineInterpolation{T,1})(x::Integer, y::Integer) where T + @boundscheck (y == 1 || Base.throw_boundserror(itp, (x,y))) + expand_value(itp, (x,)) +end -Base.IndexStyle(::Type{<:AbstractInterpolation}) = IndexCartesian() +@inline function gradient(itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N} + @boundscheck checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x) + expand_gradient(itp, x) +end +@inline function gradient!(dest, itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N} + @boundscheck checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x) + expand_gradient!(dest, 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) + expand_hessian(itp, x) +end -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]...) +checkbounds(::Type{Bool}, itp::AbstractInterpolation, x::Vararg{Number,N}) where N = + checklubounds(lbounds(itp), ubounds(itp), x) -coefficients(::Type{IT}, N) where {IT} = Expr(:block, Expr[coefficients(iextract(IT, d), N, d) for d = 1:N]...) +# 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 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...) +""" + val = expand_value(itp, x) + +Interpolate `itp` at `x`. +""" +function expand_value(itp::AbstractInterpolation, x::Tuple) + coefs = coefficients(itp) + degree = interpdegree(itp) + ixs, rxs = expand_indices_resid(degree, bounds(itp), x) + cxs = expand_weights(value_weights, degree, rxs) + expand(coefs, cxs, ixs) 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...) + +""" + g = expand_gradient(itp, x) + +Calculate the interpolated gradient of `itp` at `x`. +""" +function expand_gradient(itp::AbstractInterpolation, x::Tuple) + coefs = coefficients(itp) + degree = interpdegree(itp) + ixs, rxs = expand_indices_resid(degree, bounds(itp), x) + cxs = expand_weights(value_weights, degree, rxs) + gxs = expand_weights(gradient_weights, degree, rxs) + expand(coefs, (cxs, gxs), ixs) 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 +function expand_gradient!(dest, itp::AbstractInterpolation, x::Tuple) + coefs = coefficients(itp) + degree = interpdegree(itp) + ixs, rxs = expand_indices_resid(degree, bounds(itp), x) + cxs = expand_weights(value_weights, degree, rxs) + gxs = expand_weights(gradient_weights, degree, rxs) + expand!(dest, coefs, (cxs, gxs), ixs) 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) +""" + H = expand_hessian(itp, x) + +Calculate the interpolated hessian of `itp` at `x`. +""" +function expand_hessian(itp::AbstractInterpolation, x::Tuple) + coefs = coefficients(itp) + degree = interpdegree(itp) + ixs, rxs = expand_indices_resid(degree, axes(coefs), x) + cxs = expand_weights(value_weights, degree, rxs) + gxs = expand_weights(gradient_weights, degree, rxs) + hxs = expand_weights(hessian_weights, degree, rxs) + expand(coefs, (cxs, gxs, hxs), ixs) +end - # Calculate the indices of all coefficients that will be used - # and define fx = x - xi in each dimension - $(define_indices(IT, N, Pad)) +""" + Weights{N} - # Calculate coefficient weights based on fx - $(coefficients(IT, N)) +A type alias for an `N`-dimensional tuple of weights or indexes for interpolation. - # Generate the indexing expression - @inbounds ret = $(index_gen(IT, N)) - ret - end -end +# Example -@generated function getindex(itp::BSplineInterpolation{T,N}, xs::Number...) where {T,N} - getindex_impl(itp) -end +If you are performing linear interpolation (degree 1) in three dimensions at the point +`x = [5.1, 8.2, 4.3]`, then the floating-point residuals are `[0.1, 0.2, 0.3]`. +For value interpoations, the corresponding `Weights` would be -function (itp::BSplineInterpolation{T,N,TCoefs,IT,GT,pad})(args...) where {T,N,TCoefs,IT,GT,pad} - # support function calls - itp[args...] -end + ((0.9,0.1), (0.8,0.2), (0.7,0.3)) # (dim1, dim2, dim3) -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) +Note each "inner" tuple, for value interpolation, sums to 1. For gradient Weights, +each inner tuple would be `(-1, 1)`, and for hessian Weights each would be `(0, 0)`. - # Calculate the indices of all coefficients that will be used - # and define fx = x - xi in each dimension - $(define_indices(IT, N, Pad)) +The same structure can be used for the integer indexes at which each coefficient is evaluated. +For the example above (with `x = [5.1, 8.2, 4.3]`), the indexes would be - $gradient_exprs + ((5,6), (8,9), (4,5)) - g - end -end +corresponding to the integer pairs that bracket each coordinate. -# 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) +When performing mixed interpolation (e.g., `Linear` along dimension 1 and `Cubic` along dimension 2), +the inner tuples may not all be of the same length. +""" +const Weights{N} = NTuple{N,Tuple{Vararg{<:Number}}} -@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} - reduce(_promote_mul, eltype(TCoefs), argtypes) -end +""" + Indexes{N} -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} - _promote_mul(eltype(TCoefs), I) +The same as [`Weights`](@ref) for the integer-values used as evaluation locations for the +coefficients array. +""" +const Indexes{N} = NTuple{N,Tuple{Vararg{<:Integer}}} + +""" + val = expand(coefs, vweights::Weights, ixs::Indexes) + g = expand(coefs, (vweights, gweights), ixs) + H = expand(coefs, (vweights, gweights, hweights), ixs) + +Calculate the value, gradient, or hessian of a separable AbstractInterpolation object. +This function works recursively, processing one index at a time and calling itself as + + ret = expand(coefs, , ixs, iexpanded...) + +(The `iexpanded` form is not intended to be called directly by users.) For example, +for two-dimensional linear interpolation at a point `x = [5.1, 8.2]` the corresponding +top-level call would be + + # weights ixs + expand(coefs, ((0.9,0.1), (0.8,0.2)), ((5,6), (8,9)) + +After one round of recursion this becomes + + # weights ixs iexpanded + 0.9*expand(coefs, ((0.8,0.2),), ((8,9),), 5) + + 0.1*expand(coefs, ((0.8,0.2),), ((8,9),), 6) + +(The first dimension has been processed.) After another round, this becomes + + # wts ixs iexpanded + 0.9*(0.8*expand(coefs, (), (), 5, 8) + 0.2*expand(coefs, (), (), 5, 9)) + + 0.1*(0.8*expand(coefs, (), (), 6, 8) + 0.2*expand(coefs, (), (), 6, 9)) + +Now that the weights and `ixs` are empty and all indices are in `iexpanded`, +it finally resolves to + + 0.9*(0.8*coefs[5, 8] + 0.2*coefs[5, 9]) + + 0.1*(0.8*coefs[6, 8] + 0.2*coefs[6, 9]) + +which is the expression for bilinear interpolation at the given `x`. + +For calculating the components of the gradient and hessian, individual dimensions of +`gweights` and/or `hweights` will be substituted into the appropriate slot. For example, +in three dimensions + + g[1] = expand(coefs, (gweights[1], vweights[2], vweights[3]), ixs) + g[2] = expand(coefs, (vweights[1], gweights[2], vweights[3]), ixs) + g[3] = expand(coefs, (vweights[1], vweights[2], gweights[3]), ixs) +""" +function expand(coefs::AbstractArray, vweights::Weights, ixs::Indexes, iexpanded::Vararg{Integer,M}) where {M} + w1, wrest = vweights[1], Base.tail(vweights) + ix1, ixrest = ixs[1], Base.tail(ixs) + _expand1(coefs, w1, ix1, wrest, ixrest, iexpanded) +end +function expand(coefs::AbstractArray{T,N}, vweights::Tuple{}, ixs::Tuple{}, iexpanded::Vararg{Integer,N}) where {T,N} + @inbounds coefs[iexpanded...] # @inbounds is safe because we checked in the original call 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) +# _expand1 handles the expansion of a single dimension weight list (of length L) +@inline _expand1(coefs, w1, ix1, wrest, ixrest, iexpanded) = + w1[1] * expand(coefs, wrest, ixrest, iexpanded..., ix1[1]) + + _expand1(coefs, Base.tail(w1), Base.tail(ix1), wrest, ixrest, iexpanded) +@inline _expand1(coefs, w1::Tuple{Number}, ix1::Tuple{Integer}, wrest, ixrest, iexpanded) = + w1[1] * expand(coefs, wrest, ixrest, iexpanded..., ix1[1]) + +# Expansion of the gradient +function expand(coefs, (vweights, gweights)::Tuple{Weights{N},Weights{N}}, ixs::Indexes{N}) where N + # We swap in one gradient dimension per call to expand + SVector(ntuple(d->expand(coefs, substitute(vweights, d, gweights), ixs), Val(N))) +end +function expand!(dest, coefs, (vweights, gweights)::Tuple{Weights{N},Weights{N}}, ixs::Indexes{N}) where N + # We swap in one gradient dimension per call to expand + for d = 1:N + dest[d] = expand(coefs, substitute(vweights, d, gweights), ixs) + end + dest 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...))) +function expand(coefs, (vweights, gweights, hweights)::NTuple{3,Weights{N}}, ixs::Indexes{N}) where N + error("not yet implemented") 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 +function expand_indices_resid(degree, bounds, x) + ixbase, δxs = splitpaired(_base_rem(degree, bounds, x)) + expand_indices(degree, ixbase, bounds, δxs), δxs 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)))) - end - end - hessian_exprs = Expr(:block, exs...) +@inline _base_rem(degree::Union{Degree,NoInterp}, bounds, x) = + (base_rem(degree, bounds[1], x[1]), _base_rem(degree, Base.tail(bounds), Base.tail(x))...) +@inline _base_rem(degree::Union{Degree,NoInterp}, ::Tuple{}, ::Tuple{}) = () +@inline _base_rem(degree::Tuple{Vararg{Union{Degree,NoInterp},N}}, bounds::Tuple{Vararg{Any,N}}, x::Tuple{Vararg{Number,N}}) where N = + (base_rem(degree[1], bounds[1], x[1]), _base_rem(Base.tail(degree), Base.tail(bounds), Base.tail(x))...) +@inline _base_rem(::Tuple{}, ::Tuple{}, ::Tuple{}) = () - 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) +expand_weights(f, degree::Union{Degree,NoInterp}, ixs) = + (f(degree, ixs[1]), expand_weights(f, degree, Base.tail(ixs))...) +expand_weights(f, degree::Union{Degree,NoInterp}, ::Tuple{}) = () - $(define_indices(IT, N, Pad)) +expand_weights(f, degree::Tuple{Vararg{Union{Degree,NoInterp},N}}, ixs::NTuple{N,Number}) where N = + f.(degree, ixs) - $hessian_exprs +expand_indices(degree::Union{Degree,NoInterp}, ixs, axs, δxs) = + (expand_index(degree, ixs[1], axs[1], δxs[1]), expand_indices(degree, Base.tail(ixs), Base.tail(axs), Base.tail(δxs))...) +expand_indices(degree::Union{Degree,NoInterp}, ::Tuple{}, ::Tuple{}, ::Tuple{}) = () - H - end -end +expand_indices(degree::Tuple{Vararg{Union{Degree,NoInterp},N}}, ixs::NTuple{N,Number}, axs::NTuple{N,Tuple{Real,Real}}, δxs::NTuple{N,Number}) where N = + expand_index.(degree, ixs, axs, δxs) + +expand_index(degree, ixs, bounds::Tuple{Real,Real}, δxs) = expand_index(degree, ixs, axfrombounds(bounds), δxs) + +checklubounds(ls, us, xs) = _checklubounds(true, ls, us, xs) +_checklubounds(tf::Bool, ls, us, xs) = _checklubounds(tf & (ls[1] <= xs[1] <= us[1]), + Base.tail(ls), Base.tail(us), Base.tail(xs)) +_checklubounds(tf::Bool, ::Tuple{}, ::Tuple{}, ::Tuple{}) = tf -@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) + +# 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} + reduce(_promote_mul, eltype(TCoefs), argtypes) 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...))) +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} + _promote_mul(eltype(TCoefs), I) 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...)) - end +# This handles round-towards-the-middle for points on half-integer edges +roundbounds(x, bounds::Tuple{Integer,Integer}) = round(x) +roundbounds(x, (l, u)) = ifelse(x == l, ceil(l), ifelse(x == u, floor(u), round(x))) + +floorbounds(x, bounds::Tuple{Integer,Integer}) = floor(x) +function floorbounds(x, (l, u)) + ceill = ceil(l) + ifelse(l <= x <= ceill, ceill, floor(x)) end -hessian1(itp::AbstractInterpolation{T,1}, x) where {T} = hessian(itp, x)[1,1] +axfrombounds((l, u)::Tuple{Integer,Integer}) = UnitRange(l, u) +axfrombounds((l, u)) = UnitRange(ceil(Int, l), floor(Int, u)) diff --git a/src/b-splines/linear.jl b/src/b-splines/linear.jl index 0680c057..3a573eba 100644 --- a/src/b-splines/linear.jl +++ b/src/b-splines/linear.jl @@ -21,83 +21,17 @@ 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 +function base_rem(::Linear, bounds, x) + xf = floorbounds(x, bounds) + xf -= ifelse(xf >= floor(bounds[2]), oneunit(xf), zero(xf)) + δx = x - xf + fast_trunc(Int, xf), δx 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 +expand_index(::Linear, xi::Number, ax::AbstractUnitRange, δx) = (xi, xi+(δx>0)) - 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`. -""" -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 -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) - -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 1dc58419..f0f1871d 100644 --- a/src/b-splines/prefiltering.jl +++ b/src/b-splines/prefiltering.jl @@ -1,87 +1,71 @@ -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) +# 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 + ct!(coefs, indscp, A, indsA) = copyto!(coefs, CartesianIndices(indscp), A, CartesianIndices(indsA)) -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) +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}}}}, + gt::DimSpec{GridType} + ) where {TWeights,TC} + ret = copy_with_padding(TC, A, it) + prefilter!(TWeights, ret, it, gt) 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, gt::GridType + ) 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), gt) + 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}}}, gt::DimSpec{GridType} + ) 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), iextract(gt, dim)) 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 @@ -90,6 +74,9 @@ end prefiltering_system(::Any, ::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..86cbb3ce 100644 --- a/src/b-splines/quadratic.jl +++ b/src/b-splines/quadratic.jl @@ -23,129 +23,38 @@ 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 +function base_rem(::Quadratic, bounds, x) + xm = roundbounds(x, bounds) + δx = x - xm + fast_trunc(Int, xm), δx 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 -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 - cm_d = p(fx_d) - c_d = q(fx_d) - cp_d = p(1-fx_d) +value_weights(::Quadratic, δx) = ( + sqr(δx - SimpleRatio(1,2))/2, + SimpleRatio(3,4) - sqr(δx), + sqr(δx + SimpleRatio(1,2))/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 +gradient_weights(::Quadratic, δx) = ( + δx - SimpleRatio(1,2), + -2 * δx, + δx + SimpleRatio(1,2)) -""" -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{BC}, xi::Number, ax::AbstractUnitRange, δx) where BC = (xi-1, xi, xi+1) +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 +67,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}, ::OnCell) where {T,TC,BC<:Union{Flat,Reflect}} + 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 +90,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 +101,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}, ::OnGrid) where {T,TC,BC<:Union{Flat,Reflect}} + dl,d,du = inner_system_diags(T,n,degree) d[1] = d[end] = -1 du[1] = dl[end] = 0 @@ -212,8 +121,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}, ::GridType) where {T,TC} + dl,d,du = inner_system_diags(T,n,degree) d[1] = d[end] = 1 du[1] = dl[end] = -2 @@ -232,8 +141,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}, ::GridType) where {T,TC} + dl,d,du = inner_system_diags(T,n,degree) d[1] = d[end] = 1 du[1] = dl[end] = -3 @@ -254,8 +163,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}, ::GridType) 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/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/io.jl b/src/io.jl index 5d8c2958..39b38a50 100644 --- a/src/io.jl +++ b/src/io.jl @@ -1,62 +1,4 @@ # after this, functionality was incorporated into Base -if VERSION < v"0.7.0-DEV.1790" -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} - print(io, "interpolate(") - showarg(io, A.coefs) - print(io, ", ") - _showtypeparam(io, ST) - print(io, ", ") - _showtypeparam(io, GT) - print(io, ')') -end - -function ShowItLikeYouBuildIt.showarg(io::IO, A::GriddedInterpolation{T,N,TC,ST,K}) where {T,N,TC,ST,K} - print(io, "interpolate(") - _showknots(io, A.knots) - print(io, ", ") - showarg(io, A.coefs) - print(io, ", ") - _showtypeparam(io, ST) - print(io, ')') -end - -_showknots(io, A) = showarg(io, A) -function _showknots(io, tup::NTuple{N,Any}) where N - print(io, '(') - for (i, A) in enumerate(tup) - showarg(io, A) - i < N && print(io, ',') - end - N == 1 && print(io, ',') - print(io, ')') -end - -function ShowItLikeYouBuildIt.showarg(io::IO, A::ScaledInterpolation) - print(io, "scale(") - showarg(io, A.itp) - print(io, ", ", A.ranges, ')') -end - -function ShowItLikeYouBuildIt.showarg(io::IO, A::Extrapolation{T,N,TI,IT,GT,ET}) where {T,N,TI,IT,GT,ET} - print(io, "extrapolate(") - showarg(io, A.itp) - print(io, ", ") - _showtypeparam(io, ET) - print(io, ')') -end - -function ShowItLikeYouBuildIt.showarg(io::IO, A::FilledExtrapolation{T,N,TI,IT,GT}) where {T,N,TI,IT,GT} - print(io, "extrapolate(") - showarg(io, A.itp) - print(io, ", ", A.fillvalue, ')') -end - -else - function Base.showarg(io::IO, A::BSplineInterpolation{T,N,TW,ST,GT}, toplevel) where {T,N,TW,ST,GT} print(io, "interpolate(") Base.showarg(io, A.coefs, false) @@ -71,19 +13,19 @@ function Base.showarg(io::IO, A::BSplineInterpolation{T,N,TW,ST,GT}, toplevel) w end end -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, ", ") - Base.showarg(io, A.coefs, false) - print(io, ", ") - _showtypeparam(io, ST) - if toplevel - print(io, ") with element type ",T) - else - print(io, ')') - end -end +# 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, ", ") +# Base.showarg(io, A.coefs, false) +# print(io, ", ") +# _showtypeparam(io, ST) +# if toplevel +# print(io, ") with element type ",T) +# else +# print(io, ')') +# end +# end _showknots(io, A) = Base.showarg(io, A, false) function _showknots(io, tup::NTuple{N,Any}) where N @@ -96,36 +38,34 @@ function _showknots(io, tup::NTuple{N,Any}) where N print(io, ')') end -function Base.showarg(io::IO, A::ScaledInterpolation{T}, toplevel) where {T} - print(io, "scale(") - Base.showarg(io, A.itp, false) - print(io, ", ", A.ranges, ')') - if toplevel - print(io, " with element type ",T) - end -end - -function Base.showarg(io::IO, A::Extrapolation{T,N,TI,IT,GT,ET}, toplevel) where {T,N,TI,IT,GT,ET} - print(io, "extrapolate(") - Base.showarg(io, A.itp, false) - print(io, ", ") - _showtypeparam(io, ET) - print(io, ')') - if toplevel - print(io, " with element type ",T) - end -end - -function Base.showarg(io::IO, A::FilledExtrapolation{T,N,TI,IT,GT}, toplevel) where {T,N,TI,IT,GT} - print(io, "extrapolate(") - Base.showarg(io, A.itp, false) - print(io, ", ", A.fillvalue, ')') - if toplevel - print(io, " with element type ",T) - end -end - -end +# function Base.showarg(io::IO, A::ScaledInterpolation{T}, toplevel) where {T} +# print(io, "scale(") +# Base.showarg(io, A.itp, false) +# print(io, ", ", A.ranges, ')') +# if toplevel +# print(io, " with element type ",T) +# end +# end + +# function Base.showarg(io::IO, A::Extrapolation{T,N,TI,IT,GT,ET}, toplevel) where {T,N,TI,IT,GT,ET} +# print(io, "extrapolate(") +# Base.showarg(io, A.itp, false) +# print(io, ", ") +# _showtypeparam(io, ET) +# print(io, ')') +# if toplevel +# print(io, " with element type ",T) +# end +# end + +# function Base.showarg(io::IO, A::FilledExtrapolation{T,N,TI,IT,GT}, toplevel) where {T,N,TI,IT,GT} +# print(io, "extrapolate(") +# Base.showarg(io, A.itp, false) +# print(io, ", ", A.fillvalue, ')') +# if toplevel +# print(io, " with element type ",T) +# end +# end _showtypeparam(io, ::Type{T}) where {T} = print(io, T.name.name, "()") @@ -140,11 +80,11 @@ function _showtypeparam(io, ::Type{BSpline{T}}) where T print(io, ')') end -function _showtypeparam(io, ::Type{Gridded{T}}) where T - print(io, "Gridded(") - _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, '(') diff --git a/src/nointerp/nointerp.jl b/src/nointerp/nointerp.jl index 17490cd8..b9ca09f3 100644 --- a/src/nointerp/nointerp.jl +++ b/src/nointerp/nointerp.jl @@ -2,37 +2,22 @@ function interpolate(A::AbstractArray, ::NoInterp, gt::GT) where {GT<:DimSpec{Gr interpolate(Int, eltype(A), A, NoInterp(), gt) 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 +prefilter(::Type{TWeights}, ::Type{TC}, A::AbstractArray, ::NoInterp, ::GridType) where {TWeights, TC} = A -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 +lbound(ax, ::NoInterp, ::GridType) = first(ax) +ubound(ax, ::NoInterp, ::GridType) = last(ax) -padding(::Type{NoInterp}) = Val{0}() +base_rem(::NoInterp, bounds, x::Number) = Int(x), 0 -# 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 +expand_index(::NoInterp, xi::Number, ax::AbstractUnitRange, δx) = (xi,) + +value_weights(::NoInterp, δx) = (oneunit(δx),) +gradient_weights(::NoInterp, δx) = (zero(δx),) +hessian_weights(::NoInterp, δx) = (zero(δx),) -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/utils.jl b/src/utils.jl index c740b1bd..13c025aa 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -2,3 +2,21 @@ @inline cub(x) = x*x*x modrange(x, r::AbstractUnitRange) = mod(x-first(r), length(r)) + first(r) + +split_flag(f::Flag) = f, f +split_flag(t::Tuple) = t[1], Base.tail(t) + +splitpaired(prs) = first.(prs), last.(prs) + +fast_trunc(::Type{Int}, x) = unsafe_trunc(Int, x) +fast_trunc(::Type{Int}, x::Rational) = x.num ÷ x.den + +iextract(f::Flag, d) = f +iextract(t::Tuple, d) = t[d] + +function substitute(default::NTuple{N,Any}, d::Integer, subst::NTuple{N,Any}) where N + ntuple(i->ifelse(i==d, subst[i], default[i]), Val(N)) +end +function substitute(default::NTuple{N,Any}, d::Integer, val) where N + ntuple(i->ifelse(i==d, val, default[i]), Val(N)) +end diff --git a/test/b-splines/constant.jl b/test/b-splines/constant.jl index 4a673a75..467f88d5 100644 --- a/test/b-splines/constant.jl +++ b/test/b-splines/constant.jl @@ -9,6 +9,9 @@ A1 = rand(Float64, N1) * 100 A2 = rand(Float64, N1, N1) * 100 A3 = rand(Float64, N1, N1, N1) * 100 +getindexib(itp, i...) = @inbounds itp[i...] +callib(itp, i...) = @inbounds itp(i...) + 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())) @@ -20,44 +23,40 @@ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy)) @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)] + for (itp, A) in ((itp1c, A1), (itp1g, A1), (itp2c, A2), (itp2g, A2), (itp3c, A3), (itp3g, A3)) + for i in eachindex(itp) + @test A[i] == itp[i] == itp[Tuple(i)...] == itp(i) == itp(float.(Tuple(i))...) + end + @test @inferred(axes(itp)) == axes(A) + @test @inferred(size(itp)) == size(A) + # @test @inferred(axes(itp)) == (contructor == interpolate ? (1:N1) : (2:N1-1)) + # @test @inferred(size(itp)) == (contructor == interpolate ? N1 : N1-2) 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)] + for itp in (itp1c, itp1g) + for i = 1:N1 + @test itp[i,1] == A1[i] # used in the AbstractArray display infrastructure + @test_throws BoundsError itp[i,2] + @test_broken getindexib(itp, i, 2) == A1[i] + @test_broken callib(itp, i, 2) == A1[i] + end end - @test @inferred(size(itp3c)) == size(A3) - @test @inferred(size(itp3g)) == size(A3) - # Evaluation between data points + # Evaluation between data points (tests constancy) for i in 2:N1-1 - @test A1[i] == itp1c[i+.3] == itp1g[i+.3] == itp1c[i-.3] == itp1g[i-.3] + @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] + @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] + @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] + @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 436417b6..f952eafc 100644 --- a/test/b-splines/cubic.jl +++ b/test/b-splines/cubic.jl @@ -18,35 +18,57 @@ for (constructor, copier) in ((interpolate, identity), (interpolate!, copy)) 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) + isfullsize = constructor == interpolate || BC==Periodic + if isfullsize + @test @inferred(size(itp1)) == size(A) + @test @inferred(axes(itp1)) == axes(A) + else + @test @inferred(size(itp1)) == (xmax-2,) + @test @inferred(axes(itp1)) == (2:xmax-1,) + end @test_throws ArgumentError parent(itp1) + # Test that within the axes, we reconstruct exactly + for i in eachindex(itp1) + @test A[i] ≈ itp1[i] == itp1[Tuple(i)...] == itp1(i) ≈ itp1(float.(Tuple(i))...) + end # 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))) + @test f(x) ≈ itp1(x) atol=abs(0.1 * f(x)) 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] + isfullsize ? @test(itp1[1] ≈ A[1]) : @test_throws BoundsError itp1[1] + isfullsize ? @test(itp1(1.0) ≈ A[1]) : @test_throws BoundsError itp1(1.0) + isfullsize ? itp1(1.2) : @test_throws BoundsError itp1(1.2) + itp1(1.6) + itp1(9.4) + isfullsize ? itp1(9.8) : @test_throws BoundsError itp1(9.8) + isfullsize ? @test(itp1(10.0) ≈ A[10]) : @test_throws BoundsError itp1(10.0) + isfullsize ? @test(itp1[10] ≈ A[10]) : @test_throws BoundsError itp1[10] else - itp1[0.5] - itp1[0.6] - itp1[10.4] - itp1[10.5] + isfullsize ? itp1(0.5) : @test_throws BoundsError itp1(0.5) + isfullsize ? itp1(0.6) : @test_throws BoundsError itp1(0.6) + itp1(1.6) + itp1(9.4) + isfullsize ? itp1(10.4) : @test_throws BoundsError itp1(10.4) + isfullsize ? itp1(10.5) : @test_throws BoundsError itp1(10.5) end end + isfullsize = constructor == interpolate || BC==Periodic itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC())), GT())) - @test @inferred(size(itp2)) == size(A2) + if isfullsize + @test @inferred(size(itp2)) == size(A2) + @test @inferred(axes(itp2)) == axes(A2) + else + @test @inferred(size(itp2)) == (xmax2-2,ymax2-2) + @test @inferred(axes(itp2)) == (2:xmax2-1,2:ymax2-1) + end 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))) + @test f2(x,y) ≈ itp2(x,y) atol=abs(0.1 * f2(x,y)) end end end @@ -58,8 +80,9 @@ module CubicGradientTests using Interpolations, Test, LinearAlgebra ix = 1:15 -f(x) = cos((x-1)*2pi/(length(ix)-1)) -g(x) = -2pi/14 * sin((x-1)*2pi/(length(ix)-1)) +k = length(ix) - 1 +f(x) = cos((x-1)*2pi/k) +g(x) = -2pi/k * sin((x-1)*2pi/k) A = map(f, ix) @@ -70,16 +93,16 @@ for (constructor, copier) in ((interpolate, identity), (interpolate!, copy)) 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.gradient(itp,x))[1],atol=cbrt(cbrt(eps(g(x))))) + @test g(x) ≈ Interpolations.gradient1(itp,x) atol=cbrt(cbrt(eps(g(x)))) end end end 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()) +@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()) +@test_broken Interpolations.gradient(itp_flat_c,0.5)[1] ≈ 0 atol=eps() +@test_broken Interpolations.gradient(itp_flat_c,ix[end] + 0.5)[1] ≈ 0 atol=eps() end diff --git a/test/b-splines/linear.jl b/test/b-splines/linear.jl index e0d32838..ec83d6b7 100644 --- a/test/b-splines/linear.jl +++ b/test/b-splines/linear.jl @@ -16,35 +16,45 @@ 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())) + itp1g = @inferred(constructor(copier(A1), BSpline(Linear()), OnGrid())) + itp2c = @inferred(constructor(copier(A2), BSpline(Linear()), OnCell())) + itp2g = @inferred(constructor(copier(A2), BSpline(Linear()), OnGrid())) @test parent(itp1c) === itp1c.coefs + for (itp, A) in ((itp1c, A1), (itp1g, A1), (itp2c, A2), (itp2g, A2)) + for i in eachindex(itp) + @test A[i] == itp[i] == itp[Tuple(i)...] == itp(i) == itp(float.(Tuple(i))...) + end + @test @inferred(axes(itp)) == axes(A) + @test @inferred(size(itp)) == size(A) + end + # Just interpolation for x in 1:.2:xmax - @test ≈(f(x),itp1c[x],atol=abs(0.1 * f(x))) + @test f(x) ≈ itp1c(x) atol=abs(0.1 * f(x)) 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 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))) + for itp2 in (itp2c, itp2g) + 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 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] +@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 cfd312f9..8308d23f 100644 --- a/test/b-splines/mixed.jl +++ b/test/b-splines/mixed.jl @@ -9,20 +9,32 @@ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy)) 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) + 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) - 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 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 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] + @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 @@ -46,8 +58,18 @@ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copyshared)) @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) + 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]))) @@ -56,8 +78,8 @@ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copyshared)) 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] + @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 diff --git a/test/b-splines/multivalued.jl b/test/b-splines/multivalued.jl index 04bdac5b..7f527058 100644 --- a/test/b-splines/multivalued.jl +++ b/test/b-splines/multivalued.jl @@ -2,9 +2,9 @@ module NonNumeric # Test interpolation with a multi-valued type -using Interpolations +using Interpolations, Test -import Base: +, -, *, / +import Base: +, -, *, /, ≈ struct MyPair{T} first::T @@ -19,25 +19,32 @@ end (/)(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)} +≈(p1::MyPair, p2::MyPair) = (p1.first ≈ p2.first) & (p1.second ≈ p2.second) +# 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)) +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()), OnGrid()) -itp[3.2] +@test itp(3.2) ≈ MyPair(A0[5],A0[6]) itp = interpolate(A, BSpline(Linear()), OnGrid()) -itp[3.2] -itp = interpolate(A, BSpline(Quadratic(Flat())), OnGrid()) -itp[3.2] +@test itp(3.2) ≈ 0.8*MyPair(A0[5],A0[6]) + 0.2*MyPair(A0[7],A0[8]) +it, gt = BSpline(Quadratic(Flat())), OnGrid() +itp = interpolate(A, it, gt) +@test itp(3.2) ≈ MyPair(interpolate(a1, it, gt)(3.2), interpolate(a2, it, gt)(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] +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, gt) in ((BSpline(Constant()), OnGrid()), + (BSpline(Linear()), OnGrid()), + (BSpline(Quadratic(Flat())), OnGrid())) + itp = interpolate(A, it, gt) + @test itp(3.2,1.8) ≈ MyPair(interpolate(a1, it, gt)(3.2,1.8), interpolate(a2, it, gt)(3.2,1.8)) +end end diff --git a/test/b-splines/non1.jl b/test/b-splines/non1.jl index 901a77ed..eba7a669 100644 --- a/test/b-splines/non1.jl +++ b/test/b-splines/non1.jl @@ -27,47 +27,70 @@ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy)) # test that we reproduce the values at on-grid points for x = inds - @test itp1[x] ≈ f1(x) + @test itp1[x] == 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] + @test itp2[i,j] == itp2(i,j) ≈ A2[i,j] end 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) + isfullsize = constructor == interpolate || BC==Periodic + if isfullsize + @test @inferred(size(itp1)) == size(A1) + @test @inferred(axes(itp1)) == axes(A1) + else + @test @inferred(size(itp1)) == (length(Base.Slice(first(inds)+1:last(inds)-1)),) + @test @inferred(axes(itp1)) == (Base.Slice(first(inds)+1:last(inds)-1),) + end - # 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 x in eachindex(itp1) + @test itp1[x] == 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] + if isfullsize + @test @inferred(size(itp2)) == size(A2) + @test @inferred(axes(itp2)) == axes(A2) + else + @test @inferred(size(itp2)) == (29, 8) + @test @inferred(axes(itp2)) == (Base.Slice(-1:27), Base.Slice(1:8)) + end + for x in eachindex(itp2) + @test itp2[x] == itp2(x) ≈ A2[x] 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) + isfullsize = constructor == interpolate || BC==Periodic + if isfullsize + @test @inferred(size(itp1)) == size(A1) + @test @inferred(axes(itp1)) == axes(A1) + else + @test @inferred(size(itp1)) == (length(Base.Slice(first(inds)+1:last(inds)-1)),) + @test @inferred(axes(itp1)) == (Base.Slice(first(inds)+1:last(inds)-1),) + end # 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 x = eachindex(itp1) + @test itp1[x] == itp1(x) ≈ f1(x) 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] + if isfullsize + @test @inferred(size(itp2)) == size(A2) + @test @inferred(axes(itp2)) == axes(A2) + else + @test @inferred(size(itp2)) == (29, 8) + @test @inferred(axes(itp2)) == (Base.Slice(-1:27), Base.Slice(1:8)) + end + for x in eachindex(itp2) + @test_skip itp2[x] == itp2(x) ≈ A2[x] end end end @@ -86,7 +109,7 @@ let 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] + @test itp2[i,j] == itp2(i,j) ≈ A2[i,j] end end diff --git a/test/b-splines/quadratic.jl b/test/b-splines/quadratic.jl index 15954181..90cc4b0a 100644 --- a/test/b-splines/quadratic.jl +++ b/test/b-splines/quadratic.jl @@ -8,27 +8,42 @@ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy)) 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) + isfullsize = constructor == interpolate || BC==Periodic + if isfullsize + @test @inferred(size(itp1)) == size(A) + @test @inferred(axes(itp1)) == axes(A) + else + @test @inferred(size(itp1)) == (xmax-2,) + @test @inferred(axes(itp1)) == (2:xmax-1,) + end @test_throws ArgumentError parent(itp1) + # Test that within the axes, we reconstruct exactly + for i in eachindex(itp1) + @test A[i] ≈ itp1[i] == itp1[Tuple(i)...] == itp1(i) ≈ itp1(float.(Tuple(i))...) + end # 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))) + @test f(x) ≈ itp1(x) atol=abs(0.1 * f(x)) 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] + isfullsize ? @test(itp1[1] ≈ A[1]) : @test_throws BoundsError itp1[1] + isfullsize ? @test(itp1(1.0) ≈ A[1]) : @test_throws BoundsError itp1(1.0) + isfullsize ? itp1(1.2) : @test_throws BoundsError itp1(1.2) + itp1(1.6) + itp1(9.4) + isfullsize ? itp1(9.8) : @test_throws BoundsError itp1(9.8) + isfullsize ? @test(itp1(10.0) ≈ A[10]) : @test_throws BoundsError itp1(10.0) + isfullsize ? @test(itp1[10] ≈ A[10]) : @test_throws BoundsError itp1[10] else - itp1[0.5] - itp1[0.6] - itp1[10.4] - itp1[10.5] + isfullsize ? itp1(0.5) : @test_throws BoundsError itp1(0.5) + isfullsize ? itp1(0.6) : @test_throws BoundsError itp1(0.6) + itp1(1.6) + itp1(9.4) + isfullsize ? itp1(10.4) : @test_throws BoundsError itp1(10.4) + isfullsize ? itp1(10.5) : @test_throws BoundsError itp1(10.5) end end @@ -39,10 +54,17 @@ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy)) # 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) + isfullsize = constructor == interpolate || BC==Periodic + if isfullsize + @test @inferred(size(itp2)) == size(A) + @test @inferred(axes(itp2)) == axes(A) + else + @test @inferred(size(itp2)) == (xmax-2,ymax-2) + @test @inferred(axes(itp2)) == (2:xmax-1,2:ymax-1) + end 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 f(x,y) ≈ itp2(x,y) atol=abs(0.1 * f(x,y)) end end end @@ -52,16 +74,18 @@ let xmax = 10 A = Float64[f(x) for x in 1:xmax] itp1 = interpolate!(copy(A), BSpline(Quadratic(InPlace())), OnCell()) + @test axes(itp1) == axes(A) for i = 1:xmax - @test itp1[i] ≈ A[i] + @test itp1(i) ≈ A[i] 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()) + @test axes(itp2) == axes(A) for j = 1:ymax, i = 1:xmax - @test itp2[i,j] ≈ A[i,j] + @test itp2(i,j) ≈ A[i,j] end end diff --git a/test/b-splines/runtests.jl b/test/b-splines/runtests.jl index 981a6c9e..ba269baf 100644 --- a/test/b-splines/runtests.jl +++ b/test/b-splines/runtests.jl @@ -7,6 +7,6 @@ include("cubic.jl") include("mixed.jl") include("multivalued.jl") include("non1.jl") -include("function-call-syntax.jl") +# include("function-call-syntax.jl") end diff --git a/test/gradient.jl b/test/gradient.jl index 219144e8..237938a9 100644 --- a/test/gradient.jl +++ b/test/gradient.jl @@ -7,7 +7,7 @@ f1(x) = sin((x-3)*2pi/(nx-1) - 1) g1(x) = 2pi/(nx-1) * cos((x-3)*2pi/(nx-1) - 1) # Gradient of Constant should always be 0 -itp1 = interpolate(Float64[f1(x) for x in 1:nx-1], +itp1 = interpolate(Float64[f1(x) for x in 1:nx], BSpline(Constant()), OnGrid()) g = Array{Float64}(undef, 1) @@ -19,11 +19,11 @@ for x in 1:nx 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], +itp1 = interpolate(Float64[f1(x) for x in 1:nx], BSpline(Linear()), OnGrid()) -itp2 = interpolate((1:nx-1,), Float64[f1(x) for x in 1:nx-1], - Gridded(Linear())) -for itp in (itp1, itp2) +# 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),(Interpolations.gradient(itp,x))[1],atol=abs(0.1 * g1(x))) @test ≈(g1(x),(Interpolations.gradient!(g,itp,x))[1],atol=abs(0.1 * g1(x))) @@ -34,20 +34,20 @@ for itp in (itp1, itp2) x = rand()*(nx-2)+1.5 gtmp = Interpolations.gradient(itp, x)[1] xd = dual(x, 1) - @test epsilon(itp[xd]) ≈ gtmp + @test epsilon(itp(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),(Interpolations.gradient(itp_grid,x))[1],atol=abs(0.5 * g1(x))) - @test ≈(g1(x),(Interpolations.gradient!(g,itp_grid,x))[1],atol=abs(0.5 * g1(x))) - @test ≈(g1(x),g[1],atol=abs(0.5 * g1(x))) -end +# 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),(Interpolations.gradient(itp_grid,x))[1],atol=abs(0.5 * g1(x))) +# @test ≈(g1(x),(Interpolations.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], @@ -62,7 +62,7 @@ for i = 1:10 x = rand()*(nx-2)+1.5 gtmp = Interpolations.gradient(itp1, x)[1] xd = dual(x, 1) - @test epsilon(itp1[xd]) ≈ gtmp + @test epsilon(itp1(xd)) ≈ gtmp end # For a quadratic function and quadratic interpolation, we expect an @@ -78,7 +78,7 @@ y = qfunc(xg) iq = interpolate(y, BSpline(Quadratic(Free())), OnCell()) x = 1.8 -@test iq[x] ≈ qfunc(x) +@test iq(x) ≈ qfunc(x) @test (Interpolations.gradient(iq,x))[1] ≈ dqfunc(x) # 2d (biquadratic) @@ -121,19 +121,19 @@ for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell) 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] + @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] + @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] + @test_broken length(gtmp) == 1 + @test_broken epsilon(itp_c(ix,yd)) ≈ gtmp[1] gtmp = Interpolations.gradient(itp_d, x, iy) - @test length(gtmp) == 1 - @test epsilon(itp_d[xd,iy]) ≈ gtmp[1] + @test_broken length(gtmp) == 1 + @test epsilon(itp_d(xd,iy)) ≈ gtmp[1] end end diff --git a/test/io.jl b/test/io.jl index cbd3f6b7..1b46a73e 100644 --- a/test/io.jl +++ b/test/io.jl @@ -18,7 +18,7 @@ SPACE = " " @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" + @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()), OnGrid()) @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, (BSpline(Linear()), NoInterp()), OnGrid()) with element type Float64" @@ -27,47 +27,47 @@ SPACE = " " @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 +# @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 end # Module diff --git a/test/nointerp.jl b/test/nointerp.jl index e6e02476..7077cfec 100644 --- a/test/nointerp.jl +++ b/test/nointerp.jl @@ -7,13 +7,13 @@ 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] +@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] +# 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/readme-examples.jl b/test/readme-examples.jl index 74a41156..43a98f7d 100644 --- a/test/readme-examples.jl +++ b/test/readme-examples.jl @@ -26,13 +26,13 @@ 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) +# ## 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 @@ -40,14 +40,14 @@ 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] +# 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 c9c1798a..71b955c6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,22 +1,26 @@ module RunTests -using Interpolations +using Test +using StaticArrays, WoodburyMatrices +ambs = detect_ambiguities(StaticArrays, WoodburyMatrices, Base, Core) +using Interpolations +@test isempty(setdiff(detect_ambiguities(Interpolations, Base, Core), ambs)) -# extrapolation tests -include("extrapolation/runtests.jl") +# # extrapolation tests +# include("extrapolation/runtests.jl") # b-spline interpolation tests include("b-splines/runtests.jl") include("nointerp.jl") -# scaling tests -include("scaling/runtests.jl") +# # scaling tests +# include("scaling/runtests.jl") # # test gradient evaluation include("gradient.jl") -# gridded interpolation tests -include("gridded/runtests.jl") +# # gridded interpolation tests +# include("gridded/runtests.jl") # test interpolation with specific types include("typing.jl") @@ -27,7 +31,7 @@ include("typing.jl") include("issues/runtests.jl") include("io.jl") -include("convenience-constructors.jl") +# include("convenience-constructors.jl") include("readme-examples.jl") end diff --git a/test/typing.jl b/test/typing.jl index 39c5343b..7efdd4bd 100644 --- a/test/typing.jl +++ b/test/typing.jl @@ -16,10 +16,10 @@ itp = interpolate(A, BSpline(Quadratic(Flat())), OnCell()) # )) for x in 3.1:.2:4.3 - @test ≈(float(f(x)),float(itp[x]),atol=abs(0.1 * f(x))) + @test ≈(float(f(x)),float(itp(x)),atol=abs(0.1 * f(x))) end -@test typeof(itp[3.5f0]) == Float32 +@test typeof(itp(3.5f0)) == Float32 for x in 3.1:.2:4.3 @test ≈([g(x)], Interpolations.gradient(itp,x),atol=abs(0.1 * g(x))) @@ -30,9 +30,8 @@ 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(itp[11//10]) == Rational{Int} -@test itp[11//10] == (11//10)^2//10 +@test typeof(itp(11//10)) == Rational{Int} +@test itp(11//10) == (11//10)^2//10 end From 4f93dd5889363b694610e40089e7b9de13db912b Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sun, 19 Aug 2018 04:53:25 -0500 Subject: [PATCH 05/29] Switch to testsets --- src/rewrite.jl | 15 ++ test/InterpolationTestUtils.jl | 120 ++++++++++++++++ test/b-splines/constant.jl | 98 ++++++------- test/b-splines/cubic.jl | 159 +++++++++------------ test/b-splines/linear.jl | 105 +++++++------- test/b-splines/mixed.jl | 144 ++++++++++--------- test/b-splines/multivalued.jl | 71 ++++------ test/b-splines/non1.jl | 178 ++++++++++-------------- test/b-splines/quadratic.jl | 134 +++++++----------- test/b-splines/runtests.jl | 19 ++- test/gradient.jl | 244 ++++++++++++++++----------------- test/io.jl | 105 +++++++------- test/issues/runtests.jl | 22 +-- test/nointerp.jl | 31 ++--- test/readme-examples.jl | 97 ++++++------- test/runtests.jl | 49 +++---- test/typing.jl | 48 ++++--- 17 files changed, 814 insertions(+), 825 deletions(-) create mode 100644 src/rewrite.jl create mode 100644 test/InterpolationTestUtils.jl 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/test/InterpolationTestUtils.jl b/test/InterpolationTestUtils.jl new file mode 100644 index 00000000..6e7888ee --- /dev/null +++ b/test/InterpolationTestUtils.jl @@ -0,0 +1,120 @@ +module InterpolationTestUtils + +using Test, Interpolations +using Interpolations: degree, itpflag, bounds, lbounds, ubounds +using Interpolations: substitute + +export check_axes, check_inbounds_values, check_oob, can_eval_near_boundaries +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) + for i in eachindex(itp) + @test A[i] ≈ itp[i] == itp[Tuple(i)...] ≈ itp(i) ≈ itp(float.(Tuple(i))...) + end + 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] + @test getindexib(itp, i, 2) ≈ A[i] + @test callib(itp, i, 2) ≈ A[i] + 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 = lbounds(itp), 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 + +# 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/b-splines/constant.jl b/test/b-splines/constant.jl index 467f88d5..0850c841 100644 --- a/test/b-splines/constant.jl +++ b/test/b-splines/constant.jl @@ -1,62 +1,48 @@ -module ConstantTests - -using Interpolations -using Test - -# Instantiation -N1 = 10 -A1 = rand(Float64, N1) * 100 -A2 = rand(Float64, N1, N1) * 100 -A3 = rand(Float64, N1, N1, N1) * 100 - -getindexib(itp, i...) = @inbounds itp[i...] -callib(itp, i...) = @inbounds itp(i...) - -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 +@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! + 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 + @test Interpolations.lbounds(itp1c) == (0.5,) + @test Interpolations.lbounds(itp1g) == (1,) + @test Interpolations.ubounds(itp1c) == (N1 + 0.5,) + @test Interpolations.ubounds(itp1g) == (N1,) + + # Evaluation on provided data points + for (itp, A) in ((itp1c, A1), (itp1g, A1), (itp2c, A2), (itp2g, A2), (itp3c, A3), (itp3g, A3)) + check_axes(itp, A, isinplace) + check_inbounds_values(itp, A) + check_oob(itp) + can_eval_near_boundaries(itp) + end - # Evaluation on provided data points - for (itp, A) in ((itp1c, A1), (itp1g, A1), (itp2c, A2), (itp2g, A2), (itp3c, A3), (itp3g, A3)) - for i in eachindex(itp) - @test A[i] == itp[i] == itp[Tuple(i)...] == itp(i) == itp(float.(Tuple(i))...) + # Evaluation between data points (tests constancy) + for i in 2:N1-1 + @test A1[i] == itp1c(i+.3) == itp1g(i+.3) == itp1c(i-.3) == itp1g(i-.3) end - @test @inferred(axes(itp)) == axes(A) - @test @inferred(size(itp)) == size(A) - # @test @inferred(axes(itp)) == (contructor == interpolate ? (1:N1) : (2:N1-1)) - # @test @inferred(size(itp)) == (contructor == interpolate ? N1 : N1-2) - end - for itp in (itp1c, itp1g) - for i = 1:N1 - @test itp[i,1] == A1[i] # used in the AbstractArray display infrastructure - @test_throws BoundsError itp[i,2] - @test_broken getindexib(itp, i, 2) == A1[i] - @test_broken callib(itp, i, 2) == A1[i] + # 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 - end - # Evaluation between data points (tests constancy) - 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) + # Edge behavior + @test A1[1] == itp1c(.7) + @test A1[N1] == itp1c(N1+.3) 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 f952eafc..8f8e9a50 100644 --- a/test/b-splines/cubic.jl +++ b/test/b-splines/cubic.jl @@ -1,108 +1,79 @@ -module CubicTests - -using 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())) - isfullsize = constructor == interpolate || BC==Periodic - if isfullsize - @test @inferred(size(itp1)) == size(A) - @test @inferred(axes(itp1)) == axes(A) - else - @test @inferred(size(itp1)) == (xmax-2,) - @test @inferred(axes(itp1)) == (2:xmax-1,) - end - @test_throws ArgumentError parent(itp1) - # Test that within the axes, we reconstruct exactly - for i in eachindex(itp1) - @test A[i] ≈ itp1[i] == itp1[Tuple(i)...] == itp1(i) ≈ itp1(float.(Tuple(i))...) +@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)) + global gitp, gA + itp1 = @inferred(constructor(copier(A), BSpline(Cubic(BC())), GT())) + gitp = itp1 + gA = A + ax1 = axes(itp1)[1] + @test Interpolations.lbounds(itp1) == (first(ax1) - 0.5,) + @test Interpolations.ubounds(itp1) == (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) + InterpolationTestUtils.failstore[] = nothing + + # 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 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 - - # test that we can evaluate close to, and at, boundaries - if GT == OnGrid - isfullsize ? @test(itp1[1] ≈ A[1]) : @test_throws BoundsError itp1[1] - isfullsize ? @test(itp1(1.0) ≈ A[1]) : @test_throws BoundsError itp1(1.0) - isfullsize ? itp1(1.2) : @test_throws BoundsError itp1(1.2) - itp1(1.6) - itp1(9.4) - isfullsize ? itp1(9.8) : @test_throws BoundsError itp1(9.8) - isfullsize ? @test(itp1(10.0) ≈ A[10]) : @test_throws BoundsError itp1(10.0) - isfullsize ? @test(itp1[10] ≈ A[10]) : @test_throws BoundsError itp1[10] - else - isfullsize ? itp1(0.5) : @test_throws BoundsError itp1(0.5) - isfullsize ? itp1(0.6) : @test_throws BoundsError itp1(0.6) - itp1(1.6) - itp1(9.4) - isfullsize ? itp1(10.4) : @test_throws BoundsError itp1(10.4) - isfullsize ? itp1(10.5) : @test_throws BoundsError itp1(10.5) + itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC())), GT())) + global gitp, gA + gitp = itp2 + gA = A2 + @test_throws ArgumentError parent(itp2) + check_axes(itp2, A2, isinplace) + check_inbounds_values(itp2, A2) + check_oob(itp2) + can_eval_near_boundaries(itp2) + InterpolationTestUtils.failstore[] = nothing + + 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 - - isfullsize = constructor == interpolate || BC==Periodic - itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC())), GT())) - if isfullsize - @test @inferred(size(itp2)) == size(A2) - @test @inferred(axes(itp2)) == axes(A2) - else - @test @inferred(size(itp2)) == (xmax2-2,ymax2-2) - @test @inferred(axes(itp2)) == (2:xmax2-1,2:ymax2-1) - end - - 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 - -module CubicGradientTests -using Interpolations, Test, LinearAlgebra + 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 -k = length(ix) - 1 -f(x) = cos((x-1)*2pi/k) -g(x) = -2pi/k * sin((x-1)*2pi/k) + 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) ≈ Interpolations.gradient1(itp,x) 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 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_broken Interpolations.gradient(itp_flat_c,0.5)[1] ≈ 0 atol=eps() -@test_broken Interpolations.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/linear.jl b/test/b-splines/linear.jl index ec83d6b7..0845e5b3 100644 --- a/test/b-splines/linear.jl +++ b/test/b-splines/linear.jl @@ -1,60 +1,59 @@ -module LinearTests - -using Interpolations -using 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())) - itp1g = @inferred(constructor(copier(A1), BSpline(Linear()), OnGrid())) - itp2c = @inferred(constructor(copier(A2), BSpline(Linear()), OnCell())) - itp2g = @inferred(constructor(copier(A2), BSpline(Linear()), OnGrid())) - - @test parent(itp1c) === itp1c.coefs - - for (itp, A) in ((itp1c, A1), (itp1g, A1), (itp2c, A2), (itp2g, A2)) - for i in eachindex(itp) - @test A[i] == itp[i] == itp[Tuple(i)...] == itp(i) == itp(float.(Tuple(i))...) +@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! + itp1c = @inferred(constructor(copier(A1), BSpline(Linear()), OnCell())) + itp1g = @inferred(constructor(copier(A1), BSpline(Linear()), OnGrid())) + itp2c = @inferred(constructor(copier(A2), BSpline(Linear()), OnCell())) + itp2g = @inferred(constructor(copier(A2), BSpline(Linear()), OnGrid())) + + @test parent(itp1c) === itp1c.coefs + @test Interpolations.lbounds(itp1c) == (0.5,) + @test Interpolations.lbounds(itp1g) == (1,) + @test Interpolations.ubounds(itp1c) == (xmax + 0.5,) + @test Interpolations.ubounds(itp1g) == (xmax,) + + for (itp, A) in ((itp1c, A1), (itp1g, A1), (itp2c, A2), (itp2g, A2)) + check_axes(itp, A, isinplace) + check_inbounds_values(itp, A) + check_oob(itp) + can_eval_near_boundaries(itp) end - @test @inferred(axes(itp)) == axes(A) - @test @inferred(size(itp)) == size(A) - end - # Just interpolation - for x in 1:.2:xmax - @test f(x) ≈ itp1c(x) atol=abs(0.1 * f(x)) - end + # Just interpolation + for x in 1:.2:xmax + @test f(x) ≈ itp1c(x) atol=abs(0.1 * f(x)) + 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 - for itp2 in (itp2c, itp2g) - 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))) + # 2D + for itp2 in (itp2c, itp2g) + 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 - 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) + # 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} + 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) end diff --git a/test/b-splines/mixed.jl b/test/b-splines/mixed.jl index 8308d23f..7733928f 100644 --- a/test/b-splines/mixed.jl +++ b/test/b-splines/mixed.jl @@ -1,87 +1,83 @@ -module MixedTests +@testset "Mixed" begin + N = 10 -using Interpolations, Test, SharedArrays, 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())), BSpline(Linear())), GT())) + 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())) - 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) - - 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 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 - 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]))) + 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())), BSpline(Linear())), GT())) + 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 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 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] + end end end end - -end diff --git a/test/b-splines/multivalued.jl b/test/b-splines/multivalued.jl index 7f527058..ec41853e 100644 --- a/test/b-splines/multivalued.jl +++ b/test/b-splines/multivalued.jl @@ -1,50 +1,25 @@ -module NonNumeric - -# Test interpolation with a multi-valued type - -using Interpolations, Test - -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) -# 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 -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()), OnGrid()) -@test itp(3.2) ≈ MyPair(A0[5],A0[6]) -itp = interpolate(A, BSpline(Linear()), OnGrid()) -@test itp(3.2) ≈ 0.8*MyPair(A0[5],A0[6]) + 0.2*MyPair(A0[7],A0[8]) -it, gt = BSpline(Quadratic(Flat())), OnGrid() -itp = interpolate(A, it, gt) -@test itp(3.2) ≈ MyPair(interpolate(a1, it, gt)(3.2), interpolate(a2, it, gt)(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, gt) in ((BSpline(Constant()), OnGrid()), - (BSpline(Linear()), OnGrid()), - (BSpline(Quadratic(Flat())), OnGrid())) +@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()), OnGrid()) + @test itp(3.2) ≈ MyPair(A0[5],A0[6]) + itp = interpolate(A, BSpline(Linear()), OnGrid()) + @test itp(3.2) ≈ 0.8*MyPair(A0[5],A0[6]) + 0.2*MyPair(A0[7],A0[8]) + it, gt = BSpline(Quadratic(Flat())), OnGrid() itp = interpolate(A, it, gt) - @test itp(3.2,1.8) ≈ MyPair(interpolate(a1, it, gt)(3.2,1.8), interpolate(a2, it, gt)(3.2,1.8)) -end - + @test itp(3.2) ≈ MyPair(interpolate(a1, it, gt)(3.2), interpolate(a2, it, gt)(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, gt) in ((BSpline(Constant()), OnGrid()), + (BSpline(Linear()), OnGrid()), + (BSpline(Quadratic(Flat())), OnGrid())) + itp = interpolate(A, it, gt) + @test itp(3.2,1.8) ≈ MyPair(interpolate(a1, it, gt)(3.2,1.8), interpolate(a2, it, gt)(3.2,1.8)) + end end diff --git a/test/b-splines/non1.jl b/test/b-splines/non1.jl index eba7a669..50c5f368 100644 --- a/test/b-splines/non1.jl +++ b/test/b-splines/non1.jl @@ -1,116 +1,86 @@ -module Non1Tests - -using Interpolations, OffsetArrays, AxisAlgorithms, Test - -# 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] == 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] == itp2(i,j) ≈ A2[i,j] - end - end - - for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell) - itp1 = @inferred(constructor(copier(A1), BSpline(Quadratic(BC())), GT())) - isfullsize = constructor == interpolate || BC==Periodic - if isfullsize - @test @inferred(size(itp1)) == size(A1) - @test @inferred(axes(itp1)) == axes(A1) - else - @test @inferred(size(itp1)) == (length(Base.Slice(first(inds)+1:last(inds)-1)),) - @test @inferred(axes(itp1)) == (Base.Slice(first(inds)+1:last(inds)-1),) - end - - for x in eachindex(itp1) - @test itp1[x] == itp1(x) ≈ f1(x) - end - - itp2 = @inferred(constructor(copier(A2), BSpline(Quadratic(BC())), GT())) - if isfullsize - @test @inferred(size(itp2)) == size(A2) - @test @inferred(axes(itp2)) == axes(A2) - else - @test @inferred(size(itp2)) == (29, 8) - @test @inferred(axes(itp2)) == (Base.Slice(-1:27), Base.Slice(1:8)) - end - for x in eachindex(itp2) - @test itp2[x] == itp2(x) ≈ A2[x] +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), GT in (OnGrid, OnCell) - itp1 = @inferred(constructor(copier(A1), BSpline(Cubic(BC())), GT())) - isfullsize = constructor == interpolate || BC==Periodic - if isfullsize - @test @inferred(size(itp1)) == size(A1) - @test @inferred(axes(itp1)) == axes(A1) - else - @test @inferred(size(itp1)) == (length(Base.Slice(first(inds)+1:last(inds)-1)),) - @test @inferred(axes(itp1)) == (Base.Slice(first(inds)+1:last(inds)-1),) + 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 GT in (OnGrid, OnCell), O in (Constant, Linear) + itp1 = @inferred(constructor(copier(A1), BSpline(O()), 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(O()), GT())) + check_axes(itp2, A2, isinplace) + check_inbounds_values(itp2, A2) + check_oob(itp2) + can_eval_near_boundaries(itp2) end - # test that we reproduce the values at on-grid points - for x = eachindex(itp1) - @test itp1[x] == 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())) - if isfullsize - @test @inferred(size(itp2)) == size(A2) - @test @inferred(axes(itp2)) == axes(A2) - else - @test @inferred(size(itp2)) == (29, 8) - @test @inferred(axes(itp2)) == (Base.Slice(-1:27), Base.Slice(1:8)) + 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 - for x in eachindex(itp2) - @test_skip itp2[x] == itp2(x) ≈ A2[x] - 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] == 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 90cc4b0a..d4cf3015 100644 --- a/test/b-splines/quadratic.jl +++ b/test/b-splines/quadratic.jl @@ -1,92 +1,58 @@ -module QuadraticTests - -using Interpolations, 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())) - isfullsize = constructor == interpolate || BC==Periodic - if isfullsize - @test @inferred(size(itp1)) == size(A) - @test @inferred(axes(itp1)) == axes(A) - else - @test @inferred(size(itp1)) == (xmax-2,) - @test @inferred(axes(itp1)) == (2:xmax-1,) - end - @test_throws ArgumentError parent(itp1) - # Test that within the axes, we reconstruct exactly - for i in eachindex(itp1) - @test A[i] ≈ itp1[i] == itp1[Tuple(i)...] == itp1(i) ≈ itp1(float.(Tuple(i))...) +@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) == (first(ax1) - 0.5,) + @test Interpolations.ubounds(itp1) == (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 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 - - # test that we can evaluate close to, and at, boundaries - if GT == OnGrid - isfullsize ? @test(itp1[1] ≈ A[1]) : @test_throws BoundsError itp1[1] - isfullsize ? @test(itp1(1.0) ≈ A[1]) : @test_throws BoundsError itp1(1.0) - isfullsize ? itp1(1.2) : @test_throws BoundsError itp1(1.2) - itp1(1.6) - itp1(9.4) - isfullsize ? itp1(9.8) : @test_throws BoundsError itp1(9.8) - isfullsize ? @test(itp1(10.0) ≈ A[10]) : @test_throws BoundsError itp1(10.0) - isfullsize ? @test(itp1[10] ≈ A[10]) : @test_throws BoundsError itp1[10] - else - isfullsize ? itp1(0.5) : @test_throws BoundsError itp1(0.5) - isfullsize ? itp1(0.6) : @test_throws BoundsError itp1(0.6) - itp1(1.6) - itp1(9.4) - isfullsize ? itp1(10.4) : @test_throws BoundsError itp1(10.4) - isfullsize ? itp1(10.5) : @test_throws BoundsError 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())) - isfullsize = constructor == interpolate || BC==Periodic - if isfullsize - @test @inferred(size(itp2)) == size(A) - @test @inferred(axes(itp2)) == axes(A) - else - @test @inferred(size(itp2)) == (xmax-2,ymax-2) - @test @inferred(axes(itp2)) == (2:xmax-1,2:ymax-1) - end - - 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()) - @test axes(itp1) == axes(A) - for i = 1:xmax - @test itp1(i) ≈ A[i] - 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()) - @test axes(itp2) == axes(A) - for j = 1:ymax, i = 1:xmax - @test itp2(i,j) ≈ A[i,j] + # 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 end - -end diff --git a/test/b-splines/runtests.jl b/test/b-splines/runtests.jl index ba269baf..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/gradient.jl b/test/gradient.jl index 237938a9..7f5fce9e 100644 --- a/test/gradient.jl +++ b/test/gradient.jl @@ -1,140 +1,138 @@ -module GradientTests - using Test, Interpolations, DualNumbers, LinearAlgebra -nx = 10 -f1(x) = sin((x-3)*2pi/(nx-1) - 1) -g1(x) = 2pi/(nx-1) * cos((x-3)*2pi/(nx-1) - 1) +@testset "Gradients" begin + nx = 10 + f1(x) = sin((x-3)*2pi/(nx-1) - 1) + g1(x) = 2pi/(nx-1) * cos((x-3)*2pi/(nx-1) - 1) -# Gradient of Constant should always be 0 -itp1 = interpolate(Float64[f1(x) for x in 1:nx], - BSpline(Constant()), OnGrid()) + # Gradient of Constant should always be 0 + itp1 = interpolate(Float64[f1(x) for x in 1:nx], + BSpline(Constant()), OnGrid()) -g = Array{Float64}(undef, 1) + g = Array{Float64}(undef, 1) -for x in 1:nx - @test Interpolations.gradient(itp1, x)[1] == 0 - @test Interpolations.gradient!(g, itp1, x)[1] == 0 - @test g[1] == 0 -end + for x in 1:nx + @test Interpolations.gradient(itp1, x)[1] == 0 + @test Interpolations.gradient!(g, itp1, x)[1] == 0 + @test g[1] == 0 + end + + # 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()), 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),(Interpolations.gradient(itp,x))[1],atol=abs(0.1 * g1(x))) + @test ≈(g1(x),(Interpolations.gradient!(g,itp,x))[1],atol=abs(0.1 * 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 = Interpolations.gradient(itp, x)[1] + xd = dual(x, 1) + @test epsilon(itp(xd)) ≈ gtmp + end + end -# 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()), 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),(Interpolations.gradient(itp,x))[1],atol=abs(0.1 * g1(x))) - @test ≈(g1(x),(Interpolations.gradient!(g,itp,x))[1],atol=abs(0.1 * g1(x))) + # 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),(Interpolations.gradient(itp_grid,x))[1],atol=abs(0.5 * g1(x))) + # @test ≈(g1(x),(Interpolations.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),(Interpolations.gradient(itp1,x))[1],atol=abs(0.05 * g1(x))) + @test ≈(g1(x),(Interpolations.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 = Interpolations.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),(Interpolations.gradient(itp_grid,x))[1],atol=abs(0.5 * g1(x))) -# @test ≈(g1(x),(Interpolations.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),(Interpolations.gradient(itp1,x))[1],atol=abs(0.05 * g1(x))) - @test ≈(g1(x),(Interpolations.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 = Interpolations.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 (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 -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 = 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_broken length(gtmp) == 1 - @test_broken epsilon(itp_c(ix,yd)) ≈ gtmp[1] - gtmp = Interpolations.gradient(itp_d, x, iy) - @test_broken 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 + 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 + 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_broken length(gtmp) == 1 + @test_broken epsilon(itp_c(ix,yd)) ≈ gtmp[1] + gtmp = Interpolations.gradient(itp_d, x, iy) + @test_broken length(gtmp) == 1 + @test epsilon(itp_d(xd,iy)) ≈ gtmp[1] + end end -end end diff --git a/test/io.jl b/test/io.jl index 1b46a73e..39e69e84 100644 --- a/test/io.jl +++ b/test/io.jl @@ -1,73 +1,70 @@ -module IOTests - using Interpolations using Test -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" +@testset "IO" begin + SPACE = " " - itp = interpolate(A, BSpline(Constant()), OnGrid()) - @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Constant()), OnGrid()) with element type Float64" + @testset "BSpline" begin + A = rand(8,20) - 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(Constant()), OnCell()) + @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Constant()), OnCell()) 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(Constant()), OnGrid()) + @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Constant()), OnGrid()) 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(A, BSpline(Linear()), OnGrid()) + @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Linear()), 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 + 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" -# @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" + itp = interpolate(A, (BSpline(Linear()), NoInterp()), OnGrid()) + @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, (BSpline(Linear()), NoInterp()), OnGrid()) 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!(copy(A), BSpline(Quadratic(InPlace())), OnCell()) + @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Quadratic(InPlace())), OnCell()) with element type Float64" + end -# 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 "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" -# @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" + # 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" -# 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 + # 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 "Extrapolation" begin -# A = rand(8,20) + # @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" -# 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" + # 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 -# etpf = extrapolate(itpg, NaN) -# @test summary(etpf) == "8×20 extrapolate(interpolate(::Array{Float64,2}, BSpline(Linear()), OnGrid()), NaN) 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 end # Module diff --git a/test/issues/runtests.jl b/test/issues/runtests.jl index db21f9d1..30d1a52c 100644 --- a/test/issues/runtests.jl +++ b/test/issues/runtests.jl @@ -1,14 +1,14 @@ -module Issue34 - using Interpolations, Test -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 - +@testset "Issues" begin + @testset "issue 34" begin + 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 end diff --git a/test/nointerp.jl b/test/nointerp.jl index 7077cfec..77ba22ea 100644 --- a/test/nointerp.jl +++ b/test/nointerp.jl @@ -1,19 +1,16 @@ -module NoInterpTests -println("Testing NoInterp...") -using Interpolations, 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(), OnGrid()) + @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/readme-examples.jl b/test/readme-examples.jl index 43a98f7d..93f735b2 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, 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] +@testset "Readme Examples" begin + + ## 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] end diff --git a/test/runtests.jl b/test/runtests.jl index 71b955c6..7cf32dfb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,39 +1,42 @@ -module RunTests +if !isdefined(Main, :InterpolationTestUtils) + include("InterpolationTestUtils.jl") + @eval using Main.InterpolationTestUtils +end -using Test +using Test, SharedArrays, Random using StaticArrays, WoodburyMatrices ambs = detect_ambiguities(StaticArrays, WoodburyMatrices, Base, Core) using Interpolations @test isempty(setdiff(detect_ambiguities(Interpolations, Base, Core), ambs)) -# # extrapolation tests -# include("extrapolation/runtests.jl") -# b-spline interpolation tests -include("b-splines/runtests.jl") -include("nointerp.jl") +@testset "Interpolations" begin -# # scaling tests -# include("scaling/runtests.jl") + # b-spline interpolation tests + include("b-splines/runtests.jl") + 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") -# 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") + # Tests copied from Grid.jl's old test suite + # include("grid.jl") -include("io.jl") -# include("convenience-constructors.jl") -include("readme-examples.jl") + include("issues/runtests.jl") -end + include("io.jl") + # include("convenience-constructors.jl") + include("readme-examples.jl") -nothing +end diff --git a/test/typing.jl b/test/typing.jl index 7efdd4bd..c26c431d 100644 --- a/test/typing.jl +++ b/test/typing.jl @@ -1,37 +1,35 @@ -module TypingTests - using Interpolations, Test, LinearAlgebra -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] +@testset "Typing" begin + nx = 10 + f(x) = convert(Float32, x^3/(nx-1)) + g(x) = convert(Float32, 3x^2/(nx-1)) -itp = interpolate(A, BSpline(Quadratic(Flat())), OnCell()) + A = Float32[f(x) for x in 1:nx] -# 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), -# )) + itp = interpolate(A, BSpline(Quadratic(Flat())), OnCell()) -for x in 3.1:.2:4.3 - @test ≈(float(f(x)),float(itp(x)),atol=abs(0.1 * f(x))) -end + # 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), + # )) -@test typeof(itp(3.5f0)) == Float32 + 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 ≈([g(x)], Interpolations.gradient(itp,x),atol=abs(0.1 * g(x))) -end + @test typeof(itp(3.5f0)) == Float32 -@test typeof(Interpolations.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()) + @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 From 1fcce032dca59998559d462a3cae9d9855a870b3 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sun, 19 Aug 2018 06:58:27 -0500 Subject: [PATCH 06/29] Fix gradient for NoInterp --- src/b-splines/indexing.jl | 14 ++++++++++---- src/nointerp/nointerp.jl | 4 ++-- src/utils.jl | 4 ++++ test/gradient.jl | 13 ++++++++++--- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/b-splines/indexing.jl b/src/b-splines/indexing.jl index 60805914..2a5148a7 100644 --- a/src/b-splines/indexing.jl +++ b/src/b-splines/indexing.jl @@ -174,6 +174,9 @@ function expand(coefs::AbstractArray{T,N}, vweights::Tuple{}, ixs::Tuple{}, iexp @inbounds coefs[iexpanded...] # @inbounds is safe because we checked in the original call end +const HasNoInterp{N} = NTuple{N,Tuple{Vararg{<:Union{Number,NoInterp}}}} +expand(coefs::AbstractArray, vweights::HasNoInterp, ixs::Indexes, iexpanded::Vararg{Integer,M}) where {M} = NoInterp() + # _expand1 handles the expansion of a single dimension weight list (of length L) @inline _expand1(coefs, w1, ix1, wrest, ixrest, iexpanded) = w1[1] * expand(coefs, wrest, ixrest, iexpanded..., ix1[1]) + @@ -182,14 +185,17 @@ end w1[1] * expand(coefs, wrest, ixrest, iexpanded..., ix1[1]) # Expansion of the gradient -function expand(coefs, (vweights, gweights)::Tuple{Weights{N},Weights{N}}, ixs::Indexes{N}) where N +function expand(coefs, (vweights, gweights)::Tuple{HasNoInterp{N},HasNoInterp{N}}, ixs::Indexes{N}) where N # We swap in one gradient dimension per call to expand - SVector(ntuple(d->expand(coefs, substitute(vweights, d, gweights), ixs), Val(N))) + SVector(skip_nointerp(ntuple(d->expand(coefs, substitute(vweights, d, gweights), ixs), Val(N))...)) end -function expand!(dest, coefs, (vweights, gweights)::Tuple{Weights{N},Weights{N}}, ixs::Indexes{N}) where N +function expand!(dest, coefs, (vweights, gweights)::Tuple{HasNoInterp{N},HasNoInterp{N}}, ixs::Indexes{N}) where N # We swap in one gradient dimension per call to expand + i = 0 for d = 1:N - dest[d] = expand(coefs, substitute(vweights, d, gweights), ixs) + w = substitute(vweights, d, gweights) + w isa Weights || continue # must have a NoInterp in it + dest[i+=1] = expand(coefs, w, ixs) end dest end diff --git a/src/nointerp/nointerp.jl b/src/nointerp/nointerp.jl index b9ca09f3..085f96b1 100644 --- a/src/nointerp/nointerp.jl +++ b/src/nointerp/nointerp.jl @@ -17,7 +17,7 @@ base_rem(::NoInterp, bounds, x::Number) = Int(x), 0 expand_index(::NoInterp, xi::Number, ax::AbstractUnitRange, δx) = (xi,) value_weights(::NoInterp, δx) = (oneunit(δx),) -gradient_weights(::NoInterp, δx) = (zero(δx),) -hessian_weights(::NoInterp, δx) = (zero(δx),) +gradient_weights(::NoInterp, δx) = (NoInterp(),) +hessian_weights(::NoInterp, δx) = (NoInterp(),) padded_axis(ax::AbstractUnitRange, ::NoInterp) = ax diff --git a/src/utils.jl b/src/utils.jl index 13c025aa..d7263944 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -20,3 +20,7 @@ end function substitute(default::NTuple{N,Any}, d::Integer, val) where N ntuple(i->ifelse(i==d, val, default[i]), Val(N)) end + +@inline skip_nointerp(x, rest...) = (x, skip_nointerp(rest...)...) +@inline skip_nointerp(::NoInterp, rest...) = skip_nointerp(rest...) +skip_nointerp() = () diff --git a/test/gradient.jl b/test/gradient.jl index 7f5fce9e..816929d4 100644 --- a/test/gradient.jl +++ b/test/gradient.jl @@ -106,6 +106,7 @@ using Test, Interpolations, DualNumbers, LinearAlgebra @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())), BSpline(Linear())), GT()) @@ -127,11 +128,17 @@ using Test, Interpolations, DualNumbers, LinearAlgebra @test epsilon(itp_b(x,yd)) ≈ gtmp[2] ix, iy = round(Int, x), round(Int, y) gtmp = Interpolations.gradient(itp_c, ix, y) - @test_broken length(gtmp) == 1 - @test_broken epsilon(itp_c(ix,yd)) ≈ gtmp[1] + @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_broken length(gtmp) == 1 + @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 From 7fca31eaafb7d127e0858215bdb13d1cd86053a7 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sun, 19 Aug 2018 16:53:18 -0500 Subject: [PATCH 07/29] Use different tests depending on whether check-bounds=yes --- test/InterpolationTestUtils.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/InterpolationTestUtils.jl b/test/InterpolationTestUtils.jl index 6e7888ee..239ad9cd 100644 --- a/test/InterpolationTestUtils.jl +++ b/test/InterpolationTestUtils.jl @@ -53,12 +53,18 @@ function check_inbounds_values(itp, A) 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] - @test getindexib(itp, i, 2) ≈ A[i] - @test callib(itp, i, 2) ≈ A[i] + 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 From 7522e4dec9d0a3080dd178690e069655a30b1f82 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 20 Aug 2018 07:26:21 -0500 Subject: [PATCH 08/29] Update Travis script --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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())' From eda9f104eee7f5f08192b5af3d5279bcc7f7bb22 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 20 Aug 2018 07:34:45 -0500 Subject: [PATCH 09/29] Implement and test hessian computation --- src/Interpolations.jl | 8 ++-- src/b-splines/indexing.jl | 77 +++++++++++++++++++++++++++++++--- src/utils.jl | 6 +++ test/InterpolationTestUtils.jl | 34 +++++++++++++-- test/gradient.jl | 52 ++++++++++++++++------- test/hessian.jl | 44 +++++++++++++++++++ test/runtests.jl | 2 + 7 files changed, 196 insertions(+), 27 deletions(-) create mode 100644 test/hessian.jl diff --git a/src/Interpolations.jl b/src/Interpolations.jl index 6d78f1ee..31edb963 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -187,16 +187,16 @@ Base.to_index(::AbstractInterpolation, x::Number) = x # itp(to_indices(itp, x)) # end function gradient(itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes}) - gradient(itp, to_indices(itp, x)) + gradient(itp, to_indices(itp, x)...) end function gradient!(dest, itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes}) - gradient!(dest, itp, to_indices(itp, x)) + gradient!(dest, itp, to_indices(itp, x)...) end function hessian(itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes}) - hessian(itp, to_indices(itp, x)) + hessian(itp, to_indices(itp, x)...) end function hessian!(dest, itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes}) - hessian!(dest, itp, to_indices(itp, x)) + hessian!(dest, itp, to_indices(itp, x)...) end # @inline function (itp::AbstractInterpolation)(x::Vararg{ExpandedIndexTypes}) diff --git a/src/b-splines/indexing.jl b/src/b-splines/indexing.jl index 2a5148a7..d27eb885 100644 --- a/src/b-splines/indexing.jl +++ b/src/b-splines/indexing.jl @@ -17,10 +17,15 @@ end @boundscheck checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x) expand_gradient!(dest, 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) expand_hessian(itp, x) end +@inline function hessian!(dest, itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N} + @boundscheck checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x) + expand_hessian(itp, x) +end checkbounds(::Type{Bool}, itp::AbstractInterpolation, x::Vararg{Number,N}) where N = checklubounds(lbounds(itp), ubounds(itp), x) @@ -77,7 +82,7 @@ Calculate the interpolated hessian of `itp` at `x`. function expand_hessian(itp::AbstractInterpolation, x::Tuple) coefs = coefficients(itp) degree = interpdegree(itp) - ixs, rxs = expand_indices_resid(degree, axes(coefs), x) + ixs, rxs = expand_indices_resid(degree, bounds(itp), x) cxs = expand_weights(value_weights, degree, rxs) gxs = expand_weights(gradient_weights, degree, rxs) hxs = expand_weights(hessian_weights, degree, rxs) @@ -194,14 +199,76 @@ function expand!(dest, coefs, (vweights, gweights)::Tuple{HasNoInterp{N},HasNoIn i = 0 for d = 1:N w = substitute(vweights, d, gweights) - w isa Weights || continue # must have a NoInterp in it + w isa Weights || continue # if this isn't true, it must have a NoInterp in it dest[i+=1] = expand(coefs, w, ixs) end dest end -function expand(coefs, (vweights, gweights, hweights)::NTuple{3,Weights{N}}, ixs::Indexes{N}) where N - error("not yet implemented") +# Expansion of the hessian +# To handle the immutability of SMatrix we build static methods that visit just the entries we need, +# which due to symmetry is just the upper triangular part +ntuple_sym(f, ::Val{0}) = () +ntuple_sym(f, ::Val{1}) = (f(1,1),) +ntuple_sym(f, ::Val{2}) = (f(1,1), f(1,2), f(2,2)) +ntuple_sym(f, ::Val{3}) = (f(1,1), f(1,2), f(2,2), f(1,3), f(2,3), f(3,3)) +ntuple_sym(f, ::Val{4}) = (f(1,1), f(1,2), f(2,2), f(1,3), f(2,3), f(3,3), f(1,4), f(2,4), f(3,4), f(4,4)) +@inline function ntuple_sym(f, ::Val{N}) where N + (ntuple_sym(f, Val(N-1))..., ntuple(i->f(i,N), Val(N))...) +end + +sym2dense(t::Tuple{}) = t +sym2dense(t::NTuple{1,T}) where T = t +sym2dense(t::NTuple{3,T}) where T = (t[1], t[2], t[2], t[3]) +sym2dense(t::NTuple{6,T}) where T = (t[1], t[2], t[4], t[2], t[3], t[5], t[4], t[5], t[6]) +sym2dense(t::NTuple{10,T}) where T = (t[1], t[2], t[4], t[7], t[2], t[3], t[5], t[8], t[4], t[5], t[6], t[9], t[7], t[8], t[9], t[10]) +function sym2dense(t::NTuple{L,T}) where {L,T} + # Warning: non-inferrable unless we make this @generated. + # Above 4 dims one might anyway prefer an Array, and use hessian! + N = ceil(Int, sqrt(2*L)) + @assert (N*(N+1))÷2 == L + a = Vector{T}(undef, N*N) + idx = 0 + for j = 1:N, i=1:N + iu, ju = ifelse(i>=j, (j, i), (i, j)) # index in the upper triangular + k = (ju*(ju+1))÷2 + iu + a[idx+=1] = t[k] + end + tuple(a...) +end + +squarematrix(t::NTuple{1,T}) where T = SMatrix{1,1,T}(t) +squarematrix(t::NTuple{4,T}) where T = SMatrix{2,2,T}(t) +squarematrix(t::NTuple{9,T}) where T = SMatrix{3,3,T}(t) +squarematrix(t::NTuple{16,T}) where T = SMatrix{4,4,T}(t) +function squarematrix(t::NTuple{L,T}) where {L,T} + # Warning: non-inferrable unless we make this @generated. + # Above 4 dims one might anyway prefer an Array, and use hessian! + N = floor(Int, sqrt(L)) + @assert N*N == L + SMatrix{N,N,T}(t) +end + +cumweights(w) = _cumweights(0, w...) +_cumweights(c, w1, w...) = (c+1, _cumweights(c+1, w...)...) +_cumweights(c, ::NoInterp, w...) = (c, _cumweights(c, w...)...) +_cumweights(c) = () + +function expand(coefs, (vweights, gweights, hweights)::NTuple{3,HasNoInterp{N}}, ixs::Indexes{N}) where N + coefs = ntuple_sym((i,j)->expand(coefs, substitute(vweights, i, j, gweights, hweights), ixs), Val(N)) + squarematrix(sym2dense(skip_nointerp(coefs...))) +end + +function expand!(dest, coefs, (vweights, gweights, hweights)::NTuple{3,HasNoInterp{N}}, ixs::Indexes{N}) where N + # The Hessian is nominally N × N, but if there are K NoInterp dims then it's N-K × N-K + indlookup = cumweights(hweights) # for d in 1:N, indlookup[d] returns the appropriate index in 1:N-K + for d2 = 1:N, d1 = 1:d2 + w = substitute(vweights, d1, d2, gweights, hweights) + w isa Weights || continue # if this isn't true, it must have a NoInterp in it + i, j = indlookup[d1], indlookup[d2] + dest[i, j] = dest[j, i] = expand(coefs, w, ixs) + end + dest end function expand_indices_resid(degree, bounds, x) @@ -255,7 +322,7 @@ roundbounds(x, bounds::Tuple{Integer,Integer}) = round(x) roundbounds(x, (l, u)) = ifelse(x == l, ceil(l), ifelse(x == u, floor(u), round(x))) floorbounds(x, bounds::Tuple{Integer,Integer}) = floor(x) -function floorbounds(x, (l, u)) +function floorbounds(x, (l, u)::Tuple{Real,Real}) ceill = ceil(l) ifelse(l <= x <= ceill, ceill, floor(x)) end diff --git a/src/utils.jl b/src/utils.jl index d7263944..8c59fa2c 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -14,6 +14,7 @@ fast_trunc(::Type{Int}, x::Rational) = x.num ÷ x.den iextract(f::Flag, d) = f iextract(t::Tuple, d) = t[d] +# Substitution for gradient components function substitute(default::NTuple{N,Any}, d::Integer, subst::NTuple{N,Any}) where N ntuple(i->ifelse(i==d, subst[i], default[i]), Val(N)) end @@ -21,6 +22,11 @@ function substitute(default::NTuple{N,Any}, d::Integer, val) where N ntuple(i->ifelse(i==d, val, default[i]), Val(N)) end +# Substitution for hessian components +function substitute(default::NTuple{N,Any}, d1::Integer, d2::Integer, subst1::NTuple{N,Any}, subst2::NTuple{N,Any}) where N + ntuple(i->ifelse(i==d1==d2, subst2[i], ifelse(i==d1, subst1[i], ifelse(i==d2, subst1[i], default[i]))), Val(N)) +end + @inline skip_nointerp(x, rest...) = (x, skip_nointerp(rest...)...) @inline skip_nointerp(::NoInterp, rest...) = skip_nointerp(rest...) skip_nointerp() = () diff --git a/test/InterpolationTestUtils.jl b/test/InterpolationTestUtils.jl index 239ad9cd..45c612a4 100644 --- a/test/InterpolationTestUtils.jl +++ b/test/InterpolationTestUtils.jl @@ -1,10 +1,11 @@ module InterpolationTestUtils -using Test, Interpolations +using Test, Interpolations, ForwardDiff, StaticArrays using Interpolations: degree, itpflag, bounds, lbounds, ubounds using Interpolations: substitute -export check_axes, check_inbounds_values, check_oob, can_eval_near_boundaries +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 @@ -105,7 +106,34 @@ function can_eval_near_boundaries(itp::AbstractInterpolation) end end -# Used for multi-valued tests +# 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) + 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) + 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} diff --git a/test/gradient.jl b/test/gradient.jl index 816929d4..122249bb 100644 --- a/test/gradient.jl +++ b/test/gradient.jl @@ -3,18 +3,40 @@ using Test, Interpolations, DualNumbers, LinearAlgebra @testset "Gradients" begin nx = 10 f1(x) = sin((x-3)*2pi/(nx-1) - 1) - g1(x) = 2pi/(nx-1) * cos((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()), OnGrid()) + for x in InterpolationTestUtils.thirds(axes(A)) + @test all(iszero, Interpolations.gradient(itp, x...)) + @test all(iszero, Interpolations.gradient!(g, itp, x...)) + end - # Gradient of Constant should always be 0 - itp1 = interpolate(Float64[f1(x) for x in 1:nx], - BSpline(Constant()), OnGrid()) + for GT in (OnGrid, OnCell) + itp = interpolate(A, BSpline(Linear()), GT()) + check_gradient(itp, g) + I = first(eachindex(itp)) + @test Interpolations.gradient(itp, I) == Interpolations.gradient(itp, Tuple(I)...) + end - g = Array{Float64}(undef, 1) + 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 x in 1:nx - @test Interpolations.gradient(itp1, x)[1] == 0 - @test Interpolations.gradient!(g, itp1, x)[1] == 0 - @test g[1] == 0 + 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 # Since Linear is OnGrid in the domain, check the gradients between grid points @@ -24,9 +46,9 @@ using Test, Interpolations, DualNumbers, LinearAlgebra # Gridded(Linear())) for itp in (itp1, )#itp2) for x in 2.5:nx-1.5 - @test ≈(g1(x),(Interpolations.gradient(itp,x))[1],atol=abs(0.1 * g1(x))) - @test ≈(g1(x),(Interpolations.gradient!(g,itp,x))[1],atol=abs(0.1 * g1(x))) - @test ≈(g1(x),g[1],atol=abs(0.1 * g1(x))) + @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 @@ -52,9 +74,9 @@ using Test, Interpolations, DualNumbers, LinearAlgebra itp1 = interpolate(Float64[f1(x) for x in 1:nx-1], BSpline(Quadratic(Periodic())), OnCell()) for x in 2:nx-1 - @test ≈(g1(x),(Interpolations.gradient(itp1,x))[1],atol=abs(0.05 * g1(x))) - @test ≈(g1(x),(Interpolations.gradient!(g,itp1,x))[1],atol=abs(0.05 * g1(x))) - @test ≈(g1(x),g[1],atol=abs(0.1 * g1(x))) + @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 diff --git a/test/hessian.jl b/test/hessian.jl new file mode 100644 index 00000000..34f617b6 --- /dev/null +++ b/test/hessian.jl @@ -0,0 +1,44 @@ +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 GT in (OnGrid, OnCell) + for itp in (interpolate(A, BSpline(Constant()), GT()), + interpolate(A, BSpline(Linear()), GT())) + 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 + 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 + + # TODO: mixed interpolation (see gradient.jl) +end diff --git a/test/runtests.jl b/test/runtests.jl index 7cf32dfb..d281d73c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,6 +23,8 @@ using Interpolations # # test gradient evaluation include("gradient.jl") + # # test hessian evaluation + include("hessian.jl") # # gridded interpolation tests # include("gridded/runtests.jl") From 60862fe96b0808066628db3f3d74fd23fdf75158 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 20 Aug 2018 10:30:48 -0500 Subject: [PATCH 10/29] Don't pretend to be more generic than is possible --- src/Interpolations.jl | 12 ++---------- src/b-splines/b-splines.jl | 10 ++++++++++ src/b-splines/indexing.jl | 8 ++++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Interpolations.jl b/src/Interpolations.jl index 31edb963..239218e8 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -77,28 +77,20 @@ const Natural = Line Base.IndexStyle(::Type{<:AbstractInterpolation}) = IndexCartesian() -size(itp::AbstractInterpolation) = map(length, itp.parentaxes) size(exp::AbstractExtrapolation) = size(exp.itp) -axes(itp::AbstractInterpolation) = itp.parentaxes axes(exp::AbstractExtrapolation) = axes(exp.itp) -bounds(itp::AbstractInterpolation) = map(twotuple, lbounds(itp), ubounds(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] -lbounds(itp::AbstractInterpolation) = _lbounds(itp.parentaxes, itpflag(itp), gridflag(itp)) -ubounds(itp::AbstractInterpolation) = _ubounds(itp.parentaxes, itpflag(itp), gridflag(itp)) -_lbounds(axs::NTuple{N,Any}, itp, gt) where N = ntuple(d->lbound(axs[d], iextract(itp,d), iextract(gt,d)), Val(N)) -_ubounds(axs::NTuple{N,Any}, itp, gt) where N = ntuple(d->ubound(axs[d], iextract(itp,d), iextract(gt,d)), Val(N)) itptype(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = IT itptype(::Type{ITP}) where {ITP<:AbstractInterpolation} = itptype(supertype(ITP)) itptype(itp::AbstractInterpolation) = itptype(typeof(itp)) -itpflag(itp::AbstractInterpolation) = itp.it gridtype(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = GT gridtype(::Type{ITP}) where {ITP<:AbstractInterpolation} = gridtype(supertype(ITP)) gridtype(itp::AbstractInterpolation) = gridtype(typeof(itp)) -gridflag(itp::AbstractInterpolation) = itp.gt ndims(::Type{AbstractInterpolation{T,N,IT,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = N ndims(::Type{ITP}) where {ITP<:AbstractInterpolation} = ndims(supertype(ITP)) ndims(itp::AbstractInterpolation) = ndims(typeof(itp)) @@ -184,7 +176,7 @@ 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)) +# itp(to_indices(itp, x)...) # end function gradient(itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes}) gradient(itp, to_indices(itp, x)...) diff --git a/src/b-splines/b-splines.jl b/src/b-splines/b-splines.jl index f3f8eda0..2ba79fbb 100644 --- a/src/b-splines/b-splines.jl +++ b/src/b-splines/b-splines.jl @@ -45,6 +45,16 @@ 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 +gridflag(itp::BSplineInterpolation) = itp.gt + +size(itp::BSplineInterpolation) = map(length, itp.parentaxes) +axes(itp::BSplineInterpolation) = itp.parentaxes + +lbounds(itp::BSplineInterpolation) = _lbounds(itp.parentaxes, itpflag(itp), gridflag(itp)) +ubounds(itp::BSplineInterpolation) = _ubounds(itp.parentaxes, itpflag(itp), gridflag(itp)) +_lbounds(axs::NTuple{N,Any}, itp, gt) where N = ntuple(d->lbound(axs[d], iextract(itp,d), iextract(gt,d)), Val(N)) +_ubounds(axs::NTuple{N,Any}, itp, gt) where N = ntuple(d->ubound(axs[d], iextract(itp,d), iextract(gt,d)), Val(N)) # The unpadded defaults lbound(ax, ::BSpline, ::OnCell) = first(ax) - 0.5 diff --git a/src/b-splines/indexing.jl b/src/b-splines/indexing.jl index d27eb885..704ead5c 100644 --- a/src/b-splines/indexing.jl +++ b/src/b-splines/indexing.jl @@ -4,10 +4,10 @@ @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x)) expand_value(itp, x) end -@inline function (itp::BSplineInterpolation{T,1})(x::Integer, y::Integer) where T - @boundscheck (y == 1 || Base.throw_boundserror(itp, (x,y))) - expand_value(itp, (x,)) -end +# @inline function (itp::BSplineInterpolation{T,1})(x::Integer, y::Integer) where T +# @boundscheck (y == 1 || Base.throw_boundserror(itp, (x,y))) +# expand_value(itp, (x,)) +# 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) From ed84771db57e0118a38b6525a92d01aa79905a61 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Mon, 20 Aug 2018 10:31:35 -0500 Subject: [PATCH 11/29] Implement extrapolation --- src/Interpolations.jl | 6 +- src/b-splines/b-splines.jl | 6 +- src/b-splines/cubic.jl | 8 +- src/b-splines/indexing.jl | 68 ++++---- src/b-splines/linear.jl | 4 +- src/b-splines/quadratic.jl | 6 +- src/extrapolation/extrap_prep.jl | 91 ---------- src/extrapolation/extrap_prep_gradient.jl | 54 ------ src/extrapolation/extrapolation.jl | 129 ++++++++------ src/extrapolation/filled.jl | 45 +++-- src/extrapolation/flat.jl | 42 ----- src/extrapolation/indexing.jl | 0 src/extrapolation/linear.jl | 26 --- src/extrapolation/periodic.jl | 27 --- src/extrapolation/reflect.jl | 26 --- src/extrapolation/throw.jl | 14 -- src/utils.jl | 29 +++- test/b-splines/cubic.jl | 12 +- test/b-splines/linear.jl | 2 + test/b-splines/quadratic.jl | 4 +- test/extrapolation/runtests.jl | 200 +++++++++++----------- test/extrapolation/type-stability.jl | 12 +- test/runtests.jl | 4 +- 23 files changed, 293 insertions(+), 522 deletions(-) delete mode 100644 src/extrapolation/extrap_prep.jl delete mode 100644 src/extrapolation/extrap_prep_gradient.jl delete mode 100644 src/extrapolation/flat.jl delete mode 100644 src/extrapolation/indexing.jl delete mode 100644 src/extrapolation/linear.jl delete mode 100644 src/extrapolation/periodic.jl delete mode 100644 src/extrapolation/reflect.jl delete mode 100644 src/extrapolation/throw.jl diff --git a/src/Interpolations.jl b/src/Interpolations.jl index 239218e8..b5722160 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -204,9 +204,9 @@ end # 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...) -@inline function getindex(itp::AbstractInterpolation{T,1}, i::Integer, j::Integer) where T +@propagate_inbounds function getindex(itp::AbstractInterpolation{T,1}, i::Integer, j::Integer) where T @boundscheck (j == 1 || Base.throw_boundserror(itp, (i, j))) - @inbounds itp(i) + itp(i) end # deprecate getindex for other numeric indices @@ -215,7 +215,7 @@ end include("nointerp/nointerp.jl") include("b-splines/b-splines.jl") # include("gridded/gridded.jl") -# include("extrapolation/extrapolation.jl") +include("extrapolation/extrapolation.jl") # include("scaling/scaling.jl") include("utils.jl") include("io.jl") diff --git a/src/b-splines/b-splines.jl b/src/b-splines/b-splines.jl index 2ba79fbb..87e59e22 100644 --- a/src/b-splines/b-splines.jl +++ b/src/b-splines/b-splines.jl @@ -53,8 +53,10 @@ axes(itp::BSplineInterpolation) = itp.parentaxes lbounds(itp::BSplineInterpolation) = _lbounds(itp.parentaxes, itpflag(itp), gridflag(itp)) ubounds(itp::BSplineInterpolation) = _ubounds(itp.parentaxes, itpflag(itp), gridflag(itp)) -_lbounds(axs::NTuple{N,Any}, itp, gt) where N = ntuple(d->lbound(axs[d], iextract(itp,d), iextract(gt,d)), Val(N)) -_ubounds(axs::NTuple{N,Any}, itp, gt) where N = ntuple(d->ubound(axs[d], iextract(itp,d), iextract(gt,d)), Val(N)) +_lbounds(axs, itp, gt) = (lbound(axs[1], getfirst(itp), getfirst(gt)), _lbounds(Base.tail(axs), getrest(itp), getrest(gt))...) +_ubounds(axs, itp, gt) = (ubound(axs[1], getfirst(itp), getfirst(gt)), _ubounds(Base.tail(axs), getrest(itp), getrest(gt))...) +_lbounds(::Tuple{}, itp, gt) = () +_ubounds(::Tuple{}, itp, gt) = () # The unpadded defaults lbound(ax, ::BSpline, ::OnCell) = first(ax) - 0.5 diff --git a/src/b-splines/cubic.jl b/src/b-splines/cubic.jl index 1084b531..cd8a3bce 100644 --- a/src/b-splines/cubic.jl +++ b/src/b-splines/cubic.jl @@ -26,7 +26,7 @@ Cubic function base_rem(::Cubic, bounds, x) xf = floorbounds(x, bounds) - xf -= ifelse(xf > bounds[2]-1, oneunit(xf), zero(xf)) + xf -= ifelse(xf > last(bounds)-1, oneunit(xf), zero(xf)) δx = x - xf fast_trunc(Int, xf), δx end @@ -71,9 +71,9 @@ hessian_weights(::Cubic, δx) = (1-δx, 3*δx-2, 3*(1-δx)-2, δx) padded_axis(ax::AbstractUnitRange, ::BSpline{<:Cubic}) = first(ax)-1:last(ax)+1 padded_axis(ax::AbstractUnitRange, ::BSpline{Cubic{Periodic}}) = 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 +# # 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 diff --git a/src/b-splines/indexing.jl b/src/b-splines/indexing.jl index 704ead5c..796465b8 100644 --- a/src/b-splines/indexing.jl +++ b/src/b-splines/indexing.jl @@ -4,10 +4,12 @@ @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x)) expand_value(itp, x) end -# @inline function (itp::BSplineInterpolation{T,1})(x::Integer, y::Integer) where T -# @boundscheck (y == 1 || Base.throw_boundserror(itp, (x,y))) -# expand_value(itp, (x,)) -# 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 gradient(itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N} @boundscheck checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x) @@ -46,7 +48,7 @@ Interpolate `itp` at `x`. function expand_value(itp::AbstractInterpolation, x::Tuple) coefs = coefficients(itp) degree = interpdegree(itp) - ixs, rxs = expand_indices_resid(degree, bounds(itp), x) + ixs, rxs = splitgrouped(expand_indices_resid(degree, axes(itp), x)) cxs = expand_weights(value_weights, degree, rxs) expand(coefs, cxs, ixs) end @@ -59,7 +61,7 @@ Calculate the interpolated gradient of `itp` at `x`. function expand_gradient(itp::AbstractInterpolation, x::Tuple) coefs = coefficients(itp) degree = interpdegree(itp) - ixs, rxs = expand_indices_resid(degree, bounds(itp), x) + ixs, rxs = splitgrouped(expand_indices_resid(degree, axes(itp), x)) cxs = expand_weights(value_weights, degree, rxs) gxs = expand_weights(gradient_weights, degree, rxs) expand(coefs, (cxs, gxs), ixs) @@ -68,7 +70,7 @@ end function expand_gradient!(dest, itp::AbstractInterpolation, x::Tuple) coefs = coefficients(itp) degree = interpdegree(itp) - ixs, rxs = expand_indices_resid(degree, bounds(itp), x) + ixs, rxs = splitgrouped(expand_indices_resid(degree, axes(itp), x)) cxs = expand_weights(value_weights, degree, rxs) gxs = expand_weights(gradient_weights, degree, rxs) expand!(dest, coefs, (cxs, gxs), ixs) @@ -82,7 +84,7 @@ Calculate the interpolated hessian of `itp` at `x`. function expand_hessian(itp::AbstractInterpolation, x::Tuple) coefs = coefficients(itp) degree = interpdegree(itp) - ixs, rxs = expand_indices_resid(degree, bounds(itp), x) + ixs, rxs = splitgrouped(expand_indices_resid(degree, axes(itp), x)) cxs = expand_weights(value_weights, degree, rxs) gxs = expand_weights(gradient_weights, degree, rxs) hxs = expand_weights(hessian_weights, degree, rxs) @@ -271,17 +273,16 @@ function expand!(dest, coefs, (vweights, gweights, hweights)::NTuple{3,HasNoInte dest end -function expand_indices_resid(degree, bounds, x) - ixbase, δxs = splitpaired(_base_rem(degree, bounds, x)) - expand_indices(degree, ixbase, bounds, δxs), δxs +function expand_indices_resid(degree, axs, x) + item = expand_index_resid(getfirst(degree), axs[1], x[1]) + (item, expand_indices_resid(getrest(degree), Base.tail(axs), Base.tail(x))...) end +expand_indices_resid(degree, ::Tuple{}, ::Tuple{}) = () -@inline _base_rem(degree::Union{Degree,NoInterp}, bounds, x) = - (base_rem(degree, bounds[1], x[1]), _base_rem(degree, Base.tail(bounds), Base.tail(x))...) -@inline _base_rem(degree::Union{Degree,NoInterp}, ::Tuple{}, ::Tuple{}) = () -@inline _base_rem(degree::Tuple{Vararg{Union{Degree,NoInterp},N}}, bounds::Tuple{Vararg{Any,N}}, x::Tuple{Vararg{Number,N}}) where N = - (base_rem(degree[1], bounds[1], x[1]), _base_rem(Base.tail(degree), Base.tail(bounds), Base.tail(x))...) -@inline _base_rem(::Tuple{}, ::Tuple{}, ::Tuple{}) = () +function expand_index_resid(degree, ax, x::Number) + ix, δx = base_rem(degree, ax, x) + expand_index(degree, ix, ax, δx), δx +end expand_weights(f, degree::Union{Degree,NoInterp}, ixs) = (f(degree, ixs[1]), expand_weights(f, degree, Base.tail(ixs))...) @@ -290,14 +291,14 @@ expand_weights(f, degree::Union{Degree,NoInterp}, ::Tuple{}) = () expand_weights(f, degree::Tuple{Vararg{Union{Degree,NoInterp},N}}, ixs::NTuple{N,Number}) where N = f.(degree, ixs) -expand_indices(degree::Union{Degree,NoInterp}, ixs, axs, δxs) = - (expand_index(degree, ixs[1], axs[1], δxs[1]), expand_indices(degree, Base.tail(ixs), Base.tail(axs), Base.tail(δxs))...) -expand_indices(degree::Union{Degree,NoInterp}, ::Tuple{}, ::Tuple{}, ::Tuple{}) = () +# expand_indices(degree::Union{Degree,NoInterp}, ixs, axs, δxs) = +# (expand_index(degree, ixs[1], axs[1], δxs[1]), expand_indices(degree, Base.tail(ixs), Base.tail(axs), Base.tail(δxs))...) +# expand_indices(degree::Union{Degree,NoInterp}, ::Tuple{}, ::Tuple{}, ::Tuple{}) = () -expand_indices(degree::Tuple{Vararg{Union{Degree,NoInterp},N}}, ixs::NTuple{N,Number}, axs::NTuple{N,Tuple{Real,Real}}, δxs::NTuple{N,Number}) where N = - expand_index.(degree, ixs, axs, δxs) +# expand_indices(degree::Tuple{Vararg{Union{Degree,NoInterp},N}}, ixs::NTuple{N,Number}, axs::NTuple{N,Tuple{Real,Real}}, δxs::NTuple{N,Number}) where N = +# expand_index.(degree, ixs, axs, δxs) -expand_index(degree, ixs, bounds::Tuple{Real,Real}, δxs) = expand_index(degree, ixs, axfrombounds(bounds), δxs) +# expand_index(degree, ixs, bounds::Tuple{Real,Real}, δxs) = expand_index(degree, ixs, axfrombounds(bounds), δxs) checklubounds(ls, us, xs) = _checklubounds(true, ls, us, xs) _checklubounds(tf::Bool, ls, us, xs) = _checklubounds(tf & (ls[1] <= xs[1] <= us[1]), @@ -318,14 +319,19 @@ function getindex_return_type(::Type{BSplineInterpolation{T,N,TCoefs,IT,GT,Pad}} end # This handles round-towards-the-middle for points on half-integer edges -roundbounds(x, bounds::Tuple{Integer,Integer}) = round(x) -roundbounds(x, (l, u)) = ifelse(x == l, ceil(l), ifelse(x == u, floor(u), round(x))) +roundbounds(x::Integer, bounds) = x +function roundbounds(x, bounds) + 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, bounds::Tuple{Integer,Integer}) = floor(x) -function floorbounds(x, (l, u)::Tuple{Real,Real}) - ceill = ceil(l) - ifelse(l <= x <= ceill, ceill, floor(x)) +floorbounds(x::Integer, ax) = x +function floorbounds(x, ax) + l = first(ax) + h = half(x) + ifelse(x < l, floor(x+h), floor(x+zero(h))) end -axfrombounds((l, u)::Tuple{Integer,Integer}) = UnitRange(l, u) -axfrombounds((l, u)) = UnitRange(ceil(Int, l), floor(Int, u)) +half(x) = oneunit(x)/2 diff --git a/src/b-splines/linear.jl b/src/b-splines/linear.jl index 3a573eba..0ae10c6f 100644 --- a/src/b-splines/linear.jl +++ b/src/b-splines/linear.jl @@ -23,12 +23,12 @@ Linear function base_rem(::Linear, bounds, x) xf = floorbounds(x, bounds) - xf -= ifelse(xf >= floor(bounds[2]), oneunit(xf), zero(xf)) + xf -= ifelse(xf >= last(bounds), oneunit(xf), zero(xf)) δx = x - xf fast_trunc(Int, xf), δx end -expand_index(::Linear, xi::Number, ax::AbstractUnitRange, δx) = (xi, xi+(δx>0)) +expand_index(::Linear, xi::Number, ax::AbstractUnitRange, δx) = (xi, xi + ((δx!=0) | (xi < last(ax)))) value_weights(::Linear, δx) = (1-δx, δx) gradient_weights(::Linear, δx) = (-oneunit(δx), oneunit(δx)) diff --git a/src/b-splines/quadratic.jl b/src/b-splines/quadratic.jl index 86cbb3ce..48e0d480 100644 --- a/src/b-splines/quadratic.jl +++ b/src/b-splines/quadratic.jl @@ -50,9 +50,9 @@ expand_index(::Quadratic{BC}, xi::Number, ax::AbstractUnitRange, δx) where BC<: 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 -# 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 +# # 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, ::Quadratic) where {T} du = fill(convert(T, SimpleRatio(1,8)), n-1) 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..6ec5d124 100644 --- a/src/extrapolation/extrapolation.jl +++ b/src/extrapolation/extrapolation.jl @@ -1,5 +1,6 @@ -mutable struct Extrapolation{T,N,ITPT,IT,GT,ET} <: AbstractExtrapolation{T,N,ITPT,IT,GT} +struct Extrapolation{T,N,ITPT,IT,GT,ET} <: AbstractExtrapolation{T,N,ITPT,IT,GT} itp::ITPT + et::ET end Base.parent(A::Extrapolation) = A.itp @@ -10,6 +11,9 @@ Base.parent(A::Extrapolation) = A.itp # different directions. const ExtrapDimSpec = Union{Flag,Tuple{Vararg{Union{Flag,NTuple{2,Flag}}}}} +etptype(::Extrapolation{T,N,ITPT,IT,GT,ET}) where {T,N,ITPT,IT,GT,ET} = ET +etpflag(etp::Extrapolation{T,N,ITPT,IT,GT,ET}) where {T,N,ITPT,IT,GT,ET} = etp.et + """ `extrapolate(itp, scheme)` adds extrapolation behavior to an interpolation object, according to the provided scheme. @@ -25,71 +29,100 @@ You can also combine schemes in tuples. For example, the scheme `(Linear(), Flat 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. """ -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,GT}, et::ET) where {T,N,IT,GT,ET<:ExtrapDimSpec} = + Extrapolation{T,N,typeof(itp),IT,GT,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, x, xs, Tuple(gradient(itp, xs...)), itp(xs...)) +end -include("extrap_prep.jl") -include("extrap_prep_gradient.jl") +checkbounds(::Bool, ::AbstractExtrapolation, I...) = true -""" -`getindex_impl(::Type{E<:Extrapolation}, xs...)` +# 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) -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 +# 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,Linear},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,Linear}}, (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(::Linear, 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{Linear,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,Linear}, 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..18c47207 100644 --- a/src/extrapolation/filled.jl +++ b/src/extrapolation/filled.jl @@ -1,5 +1,3 @@ -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} itp::ITP fillvalue::FT @@ -11,37 +9,34 @@ function FilledExtrapolation(itp::AbstractInterpolation{T,N,IT,GT}, fillvalue) w end Base.parent(A::FilledExtrapolation) = A.itp +etpflag(A::FilledExtrapolation) = A.fillvalue """ -`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}) +@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...) + convert(Tret, expand_value(itp, x)) 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 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,GT,FT}, x) where {T,N,ITP,IT,GT,FT} = fv +# expand_etp_gradientE(fv::FT, etp::FilledExtrapolation{T,N,ITP,IT,GT,FT}, x) where {T,N,ITP,IT,GT,FT} = +# zero(SVector{N,FT}) +# expand_etp_hessianE(fv::FT, etp::FilledExtrapolation{T,N,ITP,IT,GT,FT}, x) where {T,N,ITP,IT,GT,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/utils.jl b/src/utils.jl index 8c59fa2c..5f8328e8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -2,18 +2,33 @@ @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 split_flag(f::Flag) = f, f split_flag(t::Tuple) = t[1], Base.tail(t) -splitpaired(prs) = first.(prs), last.(prs) +getfirst(f::Flag) = f +getfirst(t::Tuple) = t[1] +getrest(f::Flag) = f +getrest(t::Tuple) = Base.tail(t) -fast_trunc(::Type{Int}, x) = unsafe_trunc(Int, x) -fast_trunc(::Type{Int}, x::Rational) = x.num ÷ x.den +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 + # Substitution for gradient components function substitute(default::NTuple{N,Any}, d::Integer, subst::NTuple{N,Any}) where N ntuple(i->ifelse(i==d, subst[i], default[i]), Val(N)) @@ -30,3 +45,11 @@ end @inline skip_nointerp(x, rest...) = (x, skip_nointerp(rest...)...) @inline skip_nointerp(::NoInterp, rest...) = skip_nointerp(rest...) skip_nointerp() = () + +@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 diff --git a/test/b-splines/cubic.jl b/test/b-splines/cubic.jl index 8f8e9a50..417c57d7 100644 --- a/test/b-splines/cubic.jl +++ b/test/b-splines/cubic.jl @@ -14,19 +14,15 @@ for BC in (Line, Flat, Free, Periodic), GT in (OnGrid, OnCell) for (A, f) in ((A0, f0), (A1, f1)) - global gitp, gA itp1 = @inferred(constructor(copier(A), BSpline(Cubic(BC())), GT())) - gitp = itp1 - gA = A ax1 = axes(itp1)[1] - @test Interpolations.lbounds(itp1) == (first(ax1) - 0.5,) - @test Interpolations.ubounds(itp1) == (last(ax1) + 0.5,) + @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) - InterpolationTestUtils.failstore[] = nothing # test that inner region is close to data for x in 3.1:.2:8.1 @@ -35,15 +31,11 @@ end itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC())), GT())) - global gitp, gA - gitp = itp2 - gA = A2 @test_throws ArgumentError parent(itp2) check_axes(itp2, A2, isinplace) check_inbounds_values(itp2, A2) check_oob(itp2) can_eval_near_boundaries(itp2) - InterpolationTestUtils.failstore[] = nothing 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)) diff --git a/test/b-splines/linear.jl b/test/b-splines/linear.jl index 0845e5b3..9d3cc9ce 100644 --- a/test/b-splines/linear.jl +++ b/test/b-splines/linear.jl @@ -28,6 +28,8 @@ 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 diff --git a/test/b-splines/quadratic.jl b/test/b-splines/quadratic.jl index d4cf3015..d3ab9ffb 100644 --- a/test/b-splines/quadratic.jl +++ b/test/b-splines/quadratic.jl @@ -7,8 +7,8 @@ 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) == (first(ax1) - 0.5,) - @test Interpolations.ubounds(itp1) == (last(ax1) + 0.5,) + @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) diff --git a/test/extrapolation/runtests.jl b/test/extrapolation/runtests.jl index eee37ba1..1aa9eea6 100644 --- a/test/extrapolation/runtests.jl +++ b/test/extrapolation/runtests.jl @@ -1,106 +1,104 @@ -module ExtrapTests - using DualNumbers using Interpolations using Test -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] +@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()), 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(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 + + # check all extrapolations work with vectorized indexing + for E in [0,Flat(),Linear(),Periodic(),Reflect()] + @test_broken (@inferred(extrapolate(interpolate([0,0],BSpline(Linear()),OnGrid()),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()), 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) + + 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 5ae7e8ee..6461fe38 100644 --- a/test/extrapolation/type-stability.jl +++ b/test/extrapolation/type-stability.jl @@ -16,14 +16,14 @@ schemes = ( ) 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 @@ -47,7 +47,7 @@ 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] @@ -55,8 +55,8 @@ Af = Float64.(A) for B in (A, Af) itpg2 = interpolate(B, BSpline(Linear()), OnGrid()) 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/runtests.jl b/test/runtests.jl index d281d73c..28ab0ea6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,8 +15,8 @@ using Interpolations # b-spline interpolation tests include("b-splines/runtests.jl") include("nointerp.jl") - # # extrapolation tests - # include("extrapolation/runtests.jl") + # extrapolation tests + include("extrapolation/runtests.jl") # # scaling tests # include("scaling/runtests.jl") From aa5fd2915e9921eeaad37fedae554e8b9926ccdd Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Wed, 22 Aug 2018 10:29:05 -0500 Subject: [PATCH 12/29] Add ForwardDiff to test/REQUIRE --- test/REQUIRE | 1 + 1 file changed, 1 insertion(+) 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 From 53a7bc0861daebf9a438fb506c95d301e23b5fa9 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Wed, 22 Aug 2018 12:46:11 -0500 Subject: [PATCH 13/29] Disable the boundscheck-removal tests These seem flaky and depend on what other code is doing. For now they are not worth it. --- test/InterpolationTestUtils.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/InterpolationTestUtils.jl b/test/InterpolationTestUtils.jl index 45c612a4..d06ebdc8 100644 --- a/test/InterpolationTestUtils.jl +++ b/test/InterpolationTestUtils.jl @@ -59,13 +59,13 @@ function check_inbounds_values(itp, A) 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 + # 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 From aaf9ca0fb0f8e9c445cbb079b025bc41c9883393 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Wed, 22 Aug 2018 12:46:46 -0500 Subject: [PATCH 14/29] Re-enable scaling with the exception of iteration --- src/Interpolations.jl | 2 +- src/b-splines/b-splines.jl | 8 +- src/extrapolation/extrapolation.jl | 1 + src/extrapolation/filled.jl | 1 + src/scaling/scaling.jl | 499 ++++++++++++++--------------- test/runtests.jl | 4 +- test/scaling/dimspecs.jl | 30 +- test/scaling/nointerp.jl | 53 ++- test/scaling/runtests.jl | 2 +- test/scaling/scaling.jl | 163 +++++----- test/scaling/withextrap.jl | 56 ++-- 11 files changed, 401 insertions(+), 418 deletions(-) diff --git a/src/Interpolations.jl b/src/Interpolations.jl index b5722160..976bf8ec 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -216,7 +216,7 @@ include("nointerp/nointerp.jl") include("b-splines/b-splines.jl") # include("gridded/gridded.jl") include("extrapolation/extrapolation.jl") -# include("scaling/scaling.jl") +include("scaling/scaling.jl") include("utils.jl") include("io.jl") include("convenience-constructors.jl") diff --git a/src/b-splines/b-splines.jl b/src/b-splines/b-splines.jl index 87e59e22..ae54a59e 100644 --- a/src/b-splines/b-splines.jl +++ b/src/b-splines/b-splines.jl @@ -59,10 +59,10 @@ _lbounds(::Tuple{}, itp, gt) = () _ubounds(::Tuple{}, itp, gt) = () # The unpadded defaults -lbound(ax, ::BSpline, ::OnCell) = first(ax) - 0.5 -ubound(ax, ::BSpline, ::OnCell) = last(ax) + 0.5 -lbound(ax, ::BSpline, ::OnGrid) = first(ax) -ubound(ax, ::BSpline, ::OnGrid) = last(ax) +lbound(ax::AbstractUnitRange, ::BSpline, ::OnCell) = first(ax) - 0.5 +ubound(ax::AbstractUnitRange, ::BSpline, ::OnCell) = last(ax) + 0.5 +lbound(ax::AbstractUnitRange, ::BSpline, ::OnGrid) = first(ax) +ubound(ax::AbstractUnitRange, ::BSpline, ::OnGrid) = last(ax) fix_axis(r::Base.OneTo) = r fix_axis(r::Base.Slice) = r diff --git a/src/extrapolation/extrapolation.jl b/src/extrapolation/extrapolation.jl index 6ec5d124..d2ab35c4 100644 --- a/src/extrapolation/extrapolation.jl +++ b/src/extrapolation/extrapolation.jl @@ -4,6 +4,7 @@ struct Extrapolation{T,N,ITPT,IT,GT,ET} <: AbstractExtrapolation{T,N,ITPT,IT,GT} 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 diff --git a/src/extrapolation/filled.jl b/src/extrapolation/filled.jl index 18c47207..bc875afc 100644 --- a/src/extrapolation/filled.jl +++ b/src/extrapolation/filled.jl @@ -10,6 +10,7 @@ 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. diff --git a/src/scaling/scaling.jl b/src/scaling/scaling.jl index 7153f673..47031002 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} 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,45 +17,30 @@ 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,GT}, ranges::Vararg{AbstractRange,N}) where {T,N,IT,GT} + check_ranges(itpflag(itp), axes(itp), ranges) + ScaledInterpolation{T,N,typeof(itp),IT,GT,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 -getindex(sitp::ScaledInterpolation{T,1}, x::Number, y::Int) where {T} = y == 1 ? sitp[x] : throw(BoundsError()) +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")) -function (sitp::ScaledInterpolation{T,N,ITPT,IT})(args...) where {T,N,ITPT,IT<:DimSpec} - sitp[args...] -end +size(sitp::ScaledInterpolation) = size(sitp.itp) +axes(sitp::ScaledInterpolation) = axes(sitp.itp) + +lbounds(sitp::ScaledInterpolation) = _lbounds(sitp.ranges, itpflag(sitp.itp), gridflag(sitp.itp)) +ubounds(sitp::ScaledInterpolation) = _ubounds(sitp.ranges, itpflag(sitp.itp), gridflag(sitp.itp)) -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]) +lbound(ax, ::BSpline, ::OnCell) = first(ax) - boundstep(ax) +ubound(ax, ::BSpline, ::OnCell) = last(ax) + boundstep(ax) +lbound(ax, ::BSpline, ::OnGrid) = first(ax) +ubound(ax, ::BSpline, ::OnGrid) = last(ax) boundstep(r::StepRange) = r.step / 2 boundstep(r::UnitRange) = 1//2 @@ -77,46 +51,78 @@ 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 +function (sitp::ScaledInterpolation{T,N})(xs::Vararg{Number,N}) where {T,N} + xl = coordslookup(itpflag(sitp.itp), sitp.ranges, xs) + sitp.itp(xl...) +end + +(sitp::ScaledInterpolation{T,1}, x::Number, y::Int) where {T} = y == 1 ? sitp(x) : Base.throw_boundserror(sitp, (x, y)) + +@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(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 + +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{}) = () + + +# # @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 +# end rescale_gradient(r::StepRange, g) = g / r.step @@ -129,181 +135,166 @@ Implements the chain rule dy/dx = dy/du * du/dx for use when calculating gradien """ 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) -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 - -function index_gen1(::Union{Type{NoInterp}, Type{BSpline{Constant}}}) - quote - value = iter.itp_tail[1] - end -end - -function index_gen1(::Type{BSpline{Linear}}) - quote - p = iter.itp_tail - value = c_1*p[1] + cp_1*p[2] - end -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] - 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) - end -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 - -@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 - -ssize(sitp::ScaledInterpolation{T,N}) where {T,N} = map(r->round(Int, last(r)-first(r)+1), sitp.ranges)::NTuple{N,Int} +# ### 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) +# end + +# function index_gen1(::Union{Type{NoInterp}, Type{BSpline{Constant}}}) +# quote +# value = iter.itp_tail[1] +# end +# end + +# function index_gen1(::Type{BSpline{Linear}}) +# quote +# p = iter.itp_tail +# value = c_1*p[1] + cp_1*p[2] +# end +# 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] +# 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) +# end +# 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 + +# @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 + +# 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/test/runtests.jl b/test/runtests.jl index 28ab0ea6..a3642bd2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,8 +18,8 @@ using Interpolations # extrapolation tests include("extrapolation/runtests.jl") - # # scaling tests - # include("scaling/runtests.jl") + # scaling tests + include("scaling/runtests.jl") # # test gradient evaluation include("gradient.jl") diff --git a/test/scaling/dimspecs.jl b/test/scaling/dimspecs.jl index 54808572..d2fb2c32 100644 --- a/test/scaling/dimspecs.jl +++ b/test/scaling/dimspecs.jl @@ -1,23 +1,21 @@ -module ScalingDimspecTests - using Interpolations, DualNumbers, Test, LinearAlgebra -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())), BSpline(Linear())), OnGrid()) -sitp = scale(itp, xs, ys) +@testset "ScalingDimspecTests" begin + xs = -pi:(2pi/10):pi-2pi/10 + ys = -2:.1:2 + f(x,y) = sin(x) * y^2 -for (ix,x) in enumerate(xs), (iy,y) in enumerate(ys) - @test ≈(sitp[x,y],f(x,y),atol=sqrt(eps(1.0))) + itp = interpolate(Float64[f(x,y) for x in xs, y in ys], (BSpline(Quadratic(Periodic())), BSpline(Linear())), OnGrid()) + sitp = scale(itp, xs, ys) - g = Interpolations.gradient(sitp, x, y) - fx = epsilon(sitp[dual(x,1), dual(y,0)]) - fy = epsilon(sitp[dual(x,0), dual(y,1)]) + for (ix,x) in enumerate(xs), (iy,y) in enumerate(ys) + @test ≈(sitp(x,y),f(x,y),atol=sqrt(eps(1.0))) - @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/nointerp.jl b/test/scaling/nointerp.jl index 6d0c2091..9c89cbe7 100644 --- a/test/scaling/nointerp.jl +++ b/test/scaling/nointerp.jl @@ -1,38 +1,37 @@ -module ScalingNoInterpTests - using Interpolations, Test, LinearAlgebra, Random -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 +@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 -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())), NoInterp()), OnGrid()) -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(Interpolations.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()), OnGrid()) + itpb = interpolate(zb, (NoInterp(), BSpline(Linear())), OnGrid()) -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) + 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..253581a9 100644 --- a/test/scaling/runtests.jl +++ b/test/scaling/runtests.jl @@ -2,4 +2,4 @@ include("scaling.jl") include("dimspecs.jl") include("nointerp.jl") include("withextrap.jl") -include("function-call-syntax.jl") +# include("function-call-syntax.jl") diff --git a/test/scaling/scaling.jl b/test/scaling/scaling.jl index 1f1dc44c..c1dad97f 100644 --- a/test/scaling/scaling.jl +++ b/test/scaling/scaling.jl @@ -1,101 +1,94 @@ -module ScalingTests - using Interpolations using Test, LinearAlgebra -# 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()) +@testset "Scaling" begin + # Model linear interpolation of y = -3 + .5x by interpolating y=x + # and then scaling to the new x range -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 + itp = interpolate(1:1.0:10, BSpline(Linear()), OnGrid()) -# Verify that it works in >1D, with different types of ranges + sitp = @inferred(scale(itp, -3:.5:1.5)) + @test typeof(sitp) <: Interpolations.ScaledInterpolation + @test parent(sitp) === itp -gauss(phi, mu, sigma) = exp(-(phi-mu)^2 / (2sigma)^2) -testfunction(x,y) = gauss(x, 0.5, 4) * gauss(y, -.5, 2) + for (x,y) in zip(-3:.05:1.5, 1:.1:10) + @test sitp(x) ≈ y + end -xs = -5:.5:5 -ys = -4:.2:4 -zs = Float64[testfunction(x,y) for x in xs, y in ys] + # Verify that it works in >1D, with different types of ranges -itp2 = interpolate(zs, BSpline(Quadratic(Flat())), OnGrid()) -sitp2 = @inferred scale(itp2, xs, ys) + gauss(phi, mu, sigma) = exp(-(phi-mu)^2 / (2sigma)^2) + testfunction(x,y) = gauss(x, 0.5, 4) * gauss(y, -.5, 2) -for x in xs, y in ys - @test testfunction(x,y) ≈ sitp2[x,y] -end + xs = -5:.5:5 + ys = -4:.2:4 + zs = Float64[testfunction(x,y) for x in xs, y in ys] -# Test gradients of scaled grids -xs = -pi:.1:pi -ys = map(sin, xs) -itp = interpolate(ys, BSpline(Linear()), OnGrid()) -sitp = @inferred scale(itp, xs) + itp2 = interpolate(zs, BSpline(Quadratic(Flat())), OnGrid()) + sitp2 = @inferred scale(itp2, xs, ys) -for x in -pi:.1:pi - g = @inferred(Interpolations.gradient(sitp, x))[1] - @test ≈(cos(x),g,atol=0.05) -end + for x in xs, y in ys + @test testfunction(x,y) ≈ sitp2(x,y) + end -# 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 + # Test gradients of scaled grids + xs = -pi:.1:pi + ys = map(sin, xs) + itp = interpolate(ys, BSpline(Linear()), OnGrid()) + sitp = @inferred scale(itp, xs) -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] + for x in -pi:.1:pi + g = @inferred(Interpolations.gradient(sitp, x))[1] + @test ≈(cos(x),g,atol=0.05) end - dest -end -rfoo = Array{Float64}(undef, Interpolations.ssize(sitp)) -rbar = similar(rfoo) -foo!(rfoo, sitp) -bar!(rbar, sitp) -@test rfoo ≈ rbar + + # Verify that return types are reasonable + @inferred(sitp2(-3.4, 1.2)) + @inferred(sitp2(-3, 1)) + @inferred(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(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)) + + # 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)}} + + # 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 end diff --git a/test/scaling/withextrap.jl b/test/scaling/withextrap.jl index f0df91fa..35fd6f7d 100644 --- a/test/scaling/withextrap.jl +++ b/test/scaling/withextrap.jl @@ -1,39 +1,39 @@ -module ScalingWithExtrapTests - using Interpolations, Test -xs = range(-5, stop=5, length=10) -ys = map(sin, xs) +@testset "ScalingWithExtrapTests" begin -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 + xs = range(-5, stop=5, length=10) + ys = map(sin, xs) -function run_tests(sut::Interpolations.AbstractInterpolation{T,N,IT,OnCell}, itp) where {T,N,IT} - halfcell = (xs[2] - xs[1]) / 2 + 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 - 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,OnCell}, itp) where {T,N,IT} + halfcell = (xs[2] - xs[1]) / 2 + axs1 = axes(itp)[1] + for x in (5 + halfcell, 5 + 1.1halfcell, 15.8, Inf) + @test sut(-x) == itp(.5) + @test sut(x) == itp(last(axs1)+0.5) + 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 From 9f7bf1688907255b13109163afee2387b74efe55 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sat, 25 Aug 2018 12:25:35 -0500 Subject: [PATCH 15/29] Put the GridType into the BoundaryCondition (fixes #228) --- src/Interpolations.jl | 65 +++++++++++++--------- src/b-splines/b-splines.jl | 86 ++++++++++++++++++++---------- src/b-splines/cubic.jl | 29 +++++----- src/b-splines/linear.jl | 2 +- src/b-splines/prefiltering.jl | 15 +++--- src/b-splines/quadratic.jl | 29 +++++----- src/deprecations.jl | 63 ++++++++++++++++++++++ src/extrapolation/extrapolation.jl | 28 +++++----- src/extrapolation/filled.jl | 14 ++--- src/gridded/gridded.jl | 6 +-- src/nointerp/nointerp.jl | 12 +++-- src/scaling/scaling.jl | 12 ++--- test/scaling/withextrap.jl | 10 ++-- 13 files changed, 241 insertions(+), 130 deletions(-) create mode 100644 src/deprecations.jl diff --git a/src/Interpolations.jl b/src/Interpolations.jl index 976bf8ec..334180ff 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -44,37 +44,52 @@ 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 +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()` -- `Flat()` -- `Line()` -- `Free()` -- `Periodic()` -- `Reflect()` -- `InPlace()` -- `InPlaceQ()` +- `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 -struct Throw <: BoundaryCondition end -struct Flat <: BoundaryCondition end -struct Line <: BoundaryCondition end -struct Free <: BoundaryCondition end -struct Periodic <: BoundaryCondition end -struct Reflect <: BoundaryCondition end -struct InPlace <: BoundaryCondition 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 <: BoundaryCondition end +struct InPlaceQ{GT<:Union{GridType,Nothing}} <: BoundaryCondition gt::GT end const Natural = Line +(::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) @@ -85,16 +100,16 @@ 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,GT}}) where {T,N,IT<:DimSpec{InterpolationType},GT<:DimSpec{GridType}} = IT +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 +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)) @@ -209,9 +224,6 @@ import Base: getindex itp(i) end -# deprecate getindex for other numeric indices -@deprecate getindex(itp::AbstractInterpolation{T,N}, i::Vararg{Number,N}) where {T,N} itp(i...) - include("nointerp/nointerp.jl") include("b-splines/b-splines.jl") # include("gridded/gridded.jl") @@ -220,5 +232,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 ae54a59e..9cb15b8a 100644 --- a/src/b-splines/b-splines.jl +++ b/src/b-splines/b-splines.jl @@ -8,29 +8,50 @@ 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(::BSpline{D}) where D<:Degree = D() +degree(mode::BSpline) = mode.degree degree(::NoInterp) = NoInterp() -struct BSplineInterpolation{T,N,TCoefs<:AbstractArray,IT<:DimSpec{BSpline},GT<:DimSpec{GridType},Axs<:Tuple{Vararg{AbstractUnitRange,N}}} <: AbstractInterpolation{T,N,IT,GT} +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},Axs<:Tuple{Vararg{AbstractUnitRange,N}}} <: AbstractInterpolation{T,N,IT} coefs::TCoefs parentaxes::Axs it::IT - gt::GT end -function BSplineInterpolation(::Type{TWeights}, A::AbstractArray{Tel,N}, it::IT, gt::GT, axs) where {N,Tel,TWeights<:Real,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} +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") 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) @@ -38,31 +59,38 @@ function BSplineInterpolation(::Type{TWeights}, A::AbstractArray{Tel,N}, it::IT, else T = typeof(zero(TWeights) * first(A)) end - BSplineInterpolation{T,N,typeof(A),IT,GT,typeof(axs)}(A, fix_axis.(axs), it, gt) + BSplineInterpolation{T,N,typeof(A),IT,typeof(axs)}(A, fix_axis.(axs), it) end +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 -gridflag(itp::BSplineInterpolation) = itp.gt size(itp::BSplineInterpolation) = map(length, itp.parentaxes) axes(itp::BSplineInterpolation) = itp.parentaxes -lbounds(itp::BSplineInterpolation) = _lbounds(itp.parentaxes, itpflag(itp), gridflag(itp)) -ubounds(itp::BSplineInterpolation) = _ubounds(itp.parentaxes, itpflag(itp), gridflag(itp)) -_lbounds(axs, itp, gt) = (lbound(axs[1], getfirst(itp), getfirst(gt)), _lbounds(Base.tail(axs), getrest(itp), getrest(gt))...) -_ubounds(axs, itp, gt) = (ubound(axs[1], getfirst(itp), getfirst(gt)), _ubounds(Base.tail(axs), getrest(itp), getrest(gt))...) -_lbounds(::Tuple{}, itp, gt) = () -_ubounds(::Tuple{}, itp, gt) = () - -# The unpadded defaults -lbound(ax::AbstractUnitRange, ::BSpline, ::OnCell) = first(ax) - 0.5 -ubound(ax::AbstractUnitRange, ::BSpline, ::OnCell) = last(ax) + 0.5 -lbound(ax::AbstractUnitRange, ::BSpline, ::OnGrid) = first(ax) -ubound(ax::AbstractUnitRange, ::BSpline, ::OnGrid) = last(ax) +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::AbstractUnitRange, bs::BSpline) = lbound(ax, degree(bs)) +lbound(ax::AbstractUnitRange, deg::Degree) = first(ax) +lbound(ax::AbstractUnitRange, deg::DegreeBC) = lbound(ax, deg, deg.bc.gt) +ubound(ax::AbstractUnitRange, bs::BSpline) = ubound(ax, degree(bs)) +ubound(ax::AbstractUnitRange, deg::Degree) = last(ax) +ubound(ax::AbstractUnitRange, 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 @@ -71,9 +99,9 @@ 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, gt::GT) where {TWeights,TC,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} - Apad = prefilter(TWeights, TC, A, it, gt) - BSplineInterpolation(TWeights, Apad, it, gt, axes(A)) +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 """ @@ -91,8 +119,8 @@ It may also be a tuple of such values, if you want to use different interpolatio `gridstyle` should be one of `OnGrid()` or `OnCell()`. """ -function interpolate(A::AbstractArray, it::IT, gt::GT) where {IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} - interpolate(tweight(A), tcoef(A), A, it, gt) +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 @@ -106,14 +134,14 @@ tcoef(A::AbstractArray{Float32}) = Float32 tcoef(A::AbstractArray{Rational{Int}}) = Rational{Int} tcoef(A::AbstractArray{T}) where {T<:Integer} = typeof(float(zero(T))) -function interpolate!(::Type{TWeights}, A, it::IT, gt::GT) where {TWeights,IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} +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, gt), it, gt, fix_axis.(padinset.(axsA, axspad))) + BSplineInterpolation(TWeights, prefilter!(TWeights, A, it), it, fix_axis.(padinset.(axsA, axspad))) end -function interpolate!(A::AbstractArray, it::IT, gt::GT) where {IT<:DimSpec{BSpline},GT<:DimSpec{GridType}} - interpolate!(tweight(A), A, it, gt) +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)) diff --git a/src/b-splines/cubic.jl b/src/b-splines/cubic.jl index cd8a3bce..fca814b8 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 @@ -32,7 +35,7 @@ function base_rem(::Cubic, bounds, x) end expand_index(::Cubic{BC}, xi::Number, ax::AbstractUnitRange, δx) where BC = (xi-1, xi, xi+1, xi+2) -expand_index(::Cubic{Periodic}, xi::Number, ax::AbstractUnitRange, δx) = +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)) # expand_coefs(::Type{BSpline{Cubic{BC}}}, δx) = cvcoefs(δx) @@ -45,7 +48,7 @@ expand_index(::Cubic{Periodic}, xi::Number, ax::AbstractUnitRange, δx) = # end # end -function value_weights(::Cubic, δx) +function value_weights(::BSpline{<:Cubic}, δx) x3, xcomp3 = cub(δx), cub(1-δx) (SimpleRatio(1,6) * xcomp3, SimpleRatio(2,3) - sqr(δx) + SimpleRatio(1,2)*x3, @@ -53,7 +56,7 @@ function value_weights(::Cubic, δx) SimpleRatio(1,6) * x3) end -function gradient_weights(::Cubic, δx) +function gradient_weights(::BSpline{<:Cubic}, δx) x2, xcomp2 = sqr(δx), sqr(1-δx) (-SimpleRatio(1,2) * xcomp2, -2*δx + SimpleRatio(3,2)*x2, @@ -61,7 +64,7 @@ function gradient_weights(::Cubic, δx) SimpleRatio(1,2) * x2) end -hessian_weights(::Cubic, δx) = (1-δx, 3*δx-2, 3*(1-δx)-2, δx) +hessian_weights(::BSpline{<:Cubic}, δx) = (1-δx, 3*δx-2, 3*(1-δx)-2, δx) # ------------ # @@ -69,7 +72,7 @@ hessian_weights(::Cubic, δx) = (1-δx, 3*δx-2, 3*(1-δx)-2, δx) # ------------ # padded_axis(ax::AbstractUnitRange, ::BSpline{<:Cubic}) = first(ax)-1:last(ax)+1 -padded_axis(ax::AbstractUnitRange, ::BSpline{Cubic{Periodic}}) = ax +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 @@ -97,7 +100,7 @@ Applying this condition yields -cm + cp = 0 """ function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, - degree::Cubic{Flat}, ::OnGrid) where {T,TC} + 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) @@ -123,7 +126,7 @@ 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, - degree::Cubic{Flat}, ::OnCell) where {T,TC} + 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 @@ -152,7 +155,7 @@ 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, - degree::Cubic{Line}, ::OnCell) where {T,TC} + 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 @@ -177,7 +180,7 @@ condition gives: 1 cm -2 c + 1 cp = 0 """ function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, - degree::Cubic{Line}, ::OnGrid) where {T,TC} + 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 @@ -201,7 +204,7 @@ as periodic, yielding where `N` is the number of data points. """ function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, - degree::Cubic{Periodic}, ::GridType) where {T,TC} + degree::Cubic{<:Periodic}) where {T,TC} dl, d, du = inner_system_diags(T,n,degree) specs = WoodburyMatrices.sparse_factors(T, n, @@ -220,7 +223,7 @@ 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, - degree::Cubic{Free}, ::GridType) where {T,TC} + degree::Cubic{<:Free}) where {T,TC} dl, d, du = inner_system_diags(T,n,degree) specs = WoodburyMatrices.sparse_factors(T, n, diff --git a/src/b-splines/linear.jl b/src/b-splines/linear.jl index 0ae10c6f..2c85bd26 100644 --- a/src/b-splines/linear.jl +++ b/src/b-splines/linear.jl @@ -1,4 +1,4 @@ -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 diff --git a/src/b-splines/prefiltering.jl b/src/b-splines/prefiltering.jl index f0f1871d..cd6ad9e0 100644 --- a/src/b-splines/prefiltering.jl +++ b/src/b-splines/prefiltering.jl @@ -34,28 +34,27 @@ prefilter!(::Type{TWeights}, A::AbstractArray, ::BSpline{D}, ::GridType) where { function prefilter( ::Type{TWeights}, ::Type{TC}, A::AbstractArray, - it::Union{BSpline,Tuple{Vararg{Union{BSpline,NoInterp}}}}, - gt::DimSpec{GridType} + it::Union{BSpline,Tuple{Vararg{Union{BSpline,NoInterp}}}} ) where {TWeights,TC} ret = copy_with_padding(TC, A, it) - prefilter!(TWeights, ret, it, gt) + prefilter!(TWeights, ret, it) end function prefilter!( - ::Type{TWeights}, ret::TCoefs, it::BSpline, gt::GridType + ::Type{TWeights}, ret::TCoefs, it::BSpline ) where {TWeights,TCoefs<:AbstractArray} local buf, shape, retrs sz = size(ret) first = true for dim in 1:ndims(ret) - M, b = prefiltering_system(TWeights, eltype(TCoefs), sz[dim], degree(it), gt) + 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, its::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) @@ -63,7 +62,7 @@ function prefilter!( for dim in 1:ndims(ret) it = iextract(its, dim) if it != NoInterp - M, b = prefiltering_system(TWeights, eltype(TCoefs), sz[dim], degree(it), iextract(gt, dim)) + M, b = prefiltering_system(TWeights, eltype(TCoefs), sz[dim], degree(it)) if M != nothing A_ldiv_B_md!(popwrapper(ret), M, popwrapper(ret), dim, b) end @@ -72,7 +71,7 @@ function prefilter!( 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 diff --git a/src/b-splines/quadratic.jl b/src/b-splines/quadratic.jl index 48e0d480..3d431b21 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 @@ -29,20 +32,20 @@ function base_rem(::Quadratic, bounds, x) fast_trunc(Int, xm), δx end -value_weights(::Quadratic, δx) = ( +value_weights(::BSpline{<:Quadratic}, δx) = ( sqr(δx - SimpleRatio(1,2))/2, SimpleRatio(3,4) - sqr(δx), sqr(δx + SimpleRatio(1,2))/2) -gradient_weights(::Quadratic, δx) = ( +gradient_weights(::BSpline{<:Quadratic}, δx) = ( δx - SimpleRatio(1,2), -2 * δx, δx + SimpleRatio(1,2)) -hessian_weights(::Quadratic, δx) = (oneunit(δx), -2*oneunit(δx), oneunit(δx)) +hessian_weights(::BSpline{<:Quadratic}, δx) = (oneunit(δx), -2*oneunit(δx), oneunit(δx)) expand_index(::Quadratic{BC}, xi::Number, ax::AbstractUnitRange, δx) where BC = (xi-1, xi, xi+1) -expand_index(::Quadratic{Periodic}, xi::Number, ax::AbstractUnitRange, δx) = +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))) @@ -67,21 +70,21 @@ end -cm + c = 0 """ -function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, degree::Quadratic{BC}, ::OnCell) where {T,TC,BC<:Union{Flat,Reflect}} +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, degree::Quadratic{InPlace}, ::OnCell) where {T,TC} +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, degree::Quadratic{InPlaceQ}, ::OnCell) where {T,TC} +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) @@ -101,7 +104,7 @@ end -cm + cp = 0 """ -function prefiltering_system(::Type{T}, ::Type{TC}, n::Int, degree::Quadratic{BC}, ::OnGrid) where {T,TC,BC<:Union{Flat,Reflect}} +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 @@ -121,7 +124,7 @@ 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, degree::Quadratic{Line}, ::GridType) where {T,TC} +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 @@ -141,7 +144,7 @@ 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, degree::Quadratic{Free}, ::GridType) where {T,TC} +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 @@ -163,7 +166,7 @@ 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, degree::Quadratic{Periodic}, ::GridType) where {T,TC} +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, diff --git a/src/deprecations.jl b/src/deprecations.jl new file mode 100644 index 00000000..f751ddb4 --- /dev/null +++ b/src/deprecations.jl @@ -0,0 +1,63 @@ +# deprecate getindex for non-integer numeric indices +@deprecate getindex(itp::AbstractInterpolation{T,N}, i::Vararg{Number,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/extrapolation.jl b/src/extrapolation/extrapolation.jl index d2ab35c4..d0c896b8 100644 --- a/src/extrapolation/extrapolation.jl +++ b/src/extrapolation/extrapolation.jl @@ -1,4 +1,4 @@ -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 @@ -10,10 +10,10 @@ itpflag(etp::Extrapolation) = itpflag(etp.itp) # 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,GT,ET}) where {T,N,ITPT,IT,GT,ET} = ET -etpflag(etp::Extrapolation{T,N,ITPT,IT,GT,ET}) where {T,N,ITPT,IT,GT,ET} = etp.et +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. @@ -22,16 +22,16 @@ 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::ET) where {T,N,IT,GT,ET<:ExtrapDimSpec} = - Extrapolation{T,N,typeof(itp),IT,GT,ET}(itp, et) +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) @@ -71,10 +71,10 @@ function inbounds_index((flagl,flagu)::Tuple{Nothing,Throw}, (l,u), x, etp, xN) x end -function inbounds_index((flagl,flagu)::Tuple{Union{Flat,Linear},Flag}, (l,u), x, etp, xN) +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,Linear}}, (l,u), x, etp, xN) +function inbounds_index((flagl,flagu)::Tuple{Nothing,Union{Flat,Line}}, (l,u), x, etp, xN) minp(x,u) end @@ -112,15 +112,15 @@ end extrapolate_value(::Any, ::Tuple{}, ::Tuple{}, ::Tuple{}, val) = val extrapolate_axis(::Flag, x, xs, g, val) = val -extrapolate_axis(::Linear, x, xs, g, val) = val + (x-xs)*g +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{Linear,Flag}, x, xs, g, 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,Linear}, x, xs, g, val) = +extrapolate_axis((flagl,flagu)::Tuple{Nothing, Line}, x, xs, g, val) = ifelse(x > xs, val + (x-xs)*g, val) extrapolate_gradient(::Flat, x, xs, g) = ifelse(x==xs, g, zero(g)) diff --git a/src/extrapolation/filled.jl b/src/extrapolation/filled.jl index bc875afc..e580533f 100644 --- a/src/extrapolation/filled.jl +++ b/src/extrapolation/filled.jl @@ -1,11 +1,11 @@ -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 @@ -15,7 +15,7 @@ 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::AbstractInterpolation{T,N,IT,GT}, fillvalue) where {T,N,IT,GT} = FilledExtrapolation(itp, fillvalue) +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) @@ -36,8 +36,8 @@ end expand_index_resid_etp(deg, fillvalue, (l, u), x, etp::FilledExtrapolation, xN) = (l <= x <= u || Base.throw_boundserror(etp, xN)) -# expand_etp_valueE(fv::FT, etp::FilledExtrapolation{T,N,ITP,IT,GT,FT}, x) where {T,N,ITP,IT,GT,FT} = fv -# expand_etp_gradientE(fv::FT, etp::FilledExtrapolation{T,N,ITP,IT,GT,FT}, x) where {T,N,ITP,IT,GT,FT} = +# 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,GT,FT}, x) where {T,N,ITP,IT,GT,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/gridded/gridded.jl b/src/gridded/gridded.jl index cf9a3138..b4ffb799 100644 --- a/src/gridded/gridded.jl +++ b/src/gridded/gridded.jl @@ -9,11 +9,11 @@ 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,OnGrid} knots::K coefs::Array{TCoefs,N} 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)))") @@ -33,7 +33,7 @@ function GriddedInterpolation(::Type{TWeights}, knots::NTuple{N,GridIndex}, A::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 diff --git a/src/nointerp/nointerp.jl b/src/nointerp/nointerp.jl index 085f96b1..93a10a8f 100644 --- a/src/nointerp/nointerp.jl +++ b/src/nointerp/nointerp.jl @@ -1,5 +1,5 @@ -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 # How many non-NoInterp dimensions are there? @@ -7,10 +7,12 @@ count_interp_dims(::Type{NoInterp}) = 0 interpdegree(::NoInterp) = NoInterp() -prefilter(::Type{TWeights}, ::Type{TC}, A::AbstractArray, ::NoInterp, ::GridType) where {TWeights, TC} = A +iscomplete(::NoInterp) = true -lbound(ax, ::NoInterp, ::GridType) = first(ax) -ubound(ax, ::NoInterp, ::GridType) = last(ax) +prefilter(::Type{TWeights}, ::Type{TC}, A::AbstractArray, ::NoInterp) where {TWeights, TC} = A + +lbound(ax, ::NoInterp) = first(ax) +ubound(ax, ::NoInterp) = last(ax) base_rem(::NoInterp, bounds, x::Number) = Int(x), 0 diff --git a/src/scaling/scaling.jl b/src/scaling/scaling.jl index 47031002..4ecf4550 100644 --- a/src/scaling/scaling.jl +++ b/src/scaling/scaling.jl @@ -2,7 +2,7 @@ export ScaledInterpolation, eachvalue 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 @@ -17,9 +17,9 @@ 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::Vararg{AbstractRange,N}) where {T,N,IT,GT} +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,GT,typeof(ranges)}(itp, ranges) + ScaledInterpolation{T,N,typeof(itp),IT,typeof(ranges)}(itp, ranges) end function check_ranges(flags, axs, ranges) @@ -75,7 +75,7 @@ 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)) @@ -98,9 +98,9 @@ rescale_gradient_components(flags, ::Tuple{}, ::Tuple{}) = () # # @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(sitp::ScaledInterpolation{T,N,ITPT,IT}, xs::Real...) where {T,N,ITPT,IT<:DimSpec{InterpolationType}<: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(sitp::ScaledInterpolation{T,N,ITPT,IT}, xs...) where {T,N,ITPT,IT<:DimSpec{InterpolationType}<: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)")) diff --git a/test/scaling/withextrap.jl b/test/scaling/withextrap.jl index 35fd6f7d..ea2d2aee 100644 --- a/test/scaling/withextrap.jl +++ b/test/scaling/withextrap.jl @@ -5,7 +5,7 @@ using Interpolations, Test 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} + 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 @@ -13,12 +13,12 @@ using Interpolations, Test @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} + function run_tests(sut::Interpolations.AbstractInterpolation{T,N,IT}, itp) where {T,N,IT} halfcell = (xs[2] - xs[1]) / 2 - axs1 = axes(itp)[1] + itps, axs = Interpolations.itpinfo(itp) for x in (5 + halfcell, 5 + 1.1halfcell, 15.8, Inf) - @test sut(-x) == itp(.5) - @test sut(x) == itp(last(axs1)+0.5) + @test sut(-x) == itp(Interpolations.lbound(axs[1], itps[1])) + @test sut(x) == itp(Interpolations.ubound(axs[1], itps[1])) end end From adf55718719e372b5d56831d8d96b6c7e239bb70 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sat, 25 Aug 2018 15:12:54 -0500 Subject: [PATCH 16/29] Fix test depwarns from putting the gridtype into the boundarycondition --- test/b-splines/constant.jl | 29 ++++++++-------------- test/b-splines/cubic.jl | 10 ++++---- test/b-splines/linear.jl | 32 ++++++++++--------------- test/b-splines/mixed.jl | 8 +++---- test/b-splines/multivalued.jl | 20 ++++++++-------- test/b-splines/non1.jl | 18 +++++++------- test/b-splines/quadratic.jl | 8 +++---- test/extrapolation/non1.jl | 6 ++--- test/extrapolation/runtests.jl | 28 ++++++++-------------- test/extrapolation/type-stability.jl | 8 +++---- test/gradient.jl | 36 +++++++++++++--------------- test/hessian.jl | 33 ++++++++++++------------- test/issues/runtests.jl | 2 +- test/nointerp.jl | 2 +- test/readme-examples.jl | 10 ++++---- test/scaling/dimspecs.jl | 2 +- test/scaling/nointerp.jl | 6 ++--- test/scaling/scaling.jl | 8 +++---- test/scaling/withextrap.jl | 2 +- test/typing.jl | 4 ++-- 20 files changed, 124 insertions(+), 148 deletions(-) diff --git a/test/b-splines/constant.jl b/test/b-splines/constant.jl index 0850c841..504be388 100644 --- a/test/b-splines/constant.jl +++ b/test/b-splines/constant.jl @@ -7,21 +7,16 @@ for (constructor, copier) in ((interpolate, x->x), (interpolate!, copy)) isinplace = constructor == interpolate! - 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())) + itp1 = @inferred(constructor(copier(A1), BSpline(Constant()))) + itp2 = @inferred(constructor(copier(A2), BSpline(Constant()))) + itp3 = @inferred(constructor(copier(A3), BSpline(Constant()))) - @test parent(itp1c) === itp1c.coefs - @test Interpolations.lbounds(itp1c) == (0.5,) - @test Interpolations.lbounds(itp1g) == (1,) - @test Interpolations.ubounds(itp1c) == (N1 + 0.5,) - @test Interpolations.ubounds(itp1g) == (N1,) + @test parent(itp1) === itp1.coefs + @test Interpolations.lbounds(itp1) == (1,) + @test Interpolations.ubounds(itp1) == (N1,) # Evaluation on provided data points - for (itp, A) in ((itp1c, A1), (itp1g, A1), (itp2c, A2), (itp2g, A2), (itp3c, A3), (itp3g, A3)) + for (itp, A) in ((itp1, A1), (itp2, A2), (itp3, A3)) check_axes(itp, A, isinplace) check_inbounds_values(itp, A) check_oob(itp) @@ -30,19 +25,15 @@ # Evaluation between data points (tests constancy) for i in 2:N1-1 - @test A1[i] == itp1c(i+.3) == itp1g(i+.3) == itp1c(i-.3) == itp1g(i-.3) + @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] == itp2c(i+.4,j-.3) == itp2g(i+.4,j-.3) + @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] == itp3c(i+.4,j-.3,k+.1) == itp3g(i+.4,j-.3,k+.2) + @test A3[i,j,k] == itp3(i+.4,j-.3,k+.1) == itp3(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 417c57d7..2ab6ff36 100644 --- a/test/b-splines/cubic.jl +++ b/test/b-splines/cubic.jl @@ -14,7 +14,7 @@ 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())) + 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,)) @@ -30,7 +30,7 @@ end end - itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC())), GT())) + itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC(GT()))))) @test_throws ArgumentError parent(itp2) check_axes(itp2, A2, isinplace) check_inbounds_values(itp2, A2) @@ -54,18 +54,18 @@ for BC in (Line, Flat, Free, Periodic), GT in (OnGrid,OnCell) - itp = constructor(copier(A), BSpline(Cubic(BC())), GT()) + 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 - itp_flat_g = interpolate(A, BSpline(Cubic(Flat())), OnGrid()) + 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()) + 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/linear.jl b/test/b-splines/linear.jl index 9d3cc9ce..35e21523 100644 --- a/test/b-splines/linear.jl +++ b/test/b-splines/linear.jl @@ -12,18 +12,14 @@ for (constructor, copier) in ((interpolate, identity), (interpolate!, copy)) isinplace = constructor == interpolate! - itp1c = @inferred(constructor(copier(A1), BSpline(Linear()), OnCell())) - itp1g = @inferred(constructor(copier(A1), BSpline(Linear()), OnGrid())) - itp2c = @inferred(constructor(copier(A2), BSpline(Linear()), OnCell())) - itp2g = @inferred(constructor(copier(A2), BSpline(Linear()), OnGrid())) - - @test parent(itp1c) === itp1c.coefs - @test Interpolations.lbounds(itp1c) == (0.5,) - @test Interpolations.lbounds(itp1g) == (1,) - @test Interpolations.ubounds(itp1c) == (xmax + 0.5,) - @test Interpolations.ubounds(itp1g) == (xmax,) - - for (itp, A) in ((itp1c, A1), (itp1g, A1), (itp2c, A2), (itp2g, A2)) + 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) @@ -34,19 +30,17 @@ # Just interpolation for x in 1:.2:xmax - @test f(x) ≈ itp1c(x) atol=abs(0.1 * f(x)) + @test f(x) ≈ itp1(x) atol=abs(0.1 * f(x)) end # 2D - for itp2 in (itp2c, itp2g) - 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 + 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()), OnGrid())) + 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} @@ -55,7 +49,7 @@ # Issue #183 x = rand(3,3,3) - itp = interpolate(x, BSpline(Linear()), OnGrid()) + 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 7733928f..aea46fe0 100644 --- a/test/b-splines/mixed.jl +++ b/test/b-splines/mixed.jl @@ -4,8 +4,8 @@ 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())) + 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) @@ -49,8 +49,8 @@ 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())), BSpline(Linear())), GT())) + 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) diff --git a/test/b-splines/multivalued.jl b/test/b-splines/multivalued.jl index ec41853e..9182c39a 100644 --- a/test/b-splines/multivalued.jl +++ b/test/b-splines/multivalued.jl @@ -4,22 +4,22 @@ A = reinterpret(MyPair{Float64}, A0) a1, a2 = A0[1:2:end], A0[2:2:end] @test length(A) == 10 - itp = interpolate(A, BSpline(Constant()), OnGrid()) + itp = interpolate(A, BSpline(Constant())) @test itp(3.2) ≈ MyPair(A0[5],A0[6]) - itp = interpolate(A, BSpline(Linear()), OnGrid()) + itp = interpolate(A, BSpline(Linear())) @test itp(3.2) ≈ 0.8*MyPair(A0[5],A0[6]) + 0.2*MyPair(A0[7],A0[8]) - it, gt = BSpline(Quadratic(Flat())), OnGrid() - itp = interpolate(A, it, gt) - @test itp(3.2) ≈ MyPair(interpolate(a1, it, gt)(3.2), interpolate(a2, it, gt)(3.2)) + 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, gt) in ((BSpline(Constant()), OnGrid()), - (BSpline(Linear()), OnGrid()), - (BSpline(Quadratic(Flat())), OnGrid())) - itp = interpolate(A, it, gt) - @test itp(3.2,1.8) ≈ MyPair(interpolate(a1, it, gt)(3.2,1.8), interpolate(a2, it, gt)(3.2,1.8)) + 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 50c5f368..82be4c29 100644 --- a/test/b-splines/non1.jl +++ b/test/b-splines/non1.jl @@ -21,14 +21,14 @@ using AxisAlgorithms, OffsetArrays 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())) + 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()), GT())) + itp2 = @inferred(constructor(copier(A2), BSpline(O()))) check_axes(itp2, A2, isinplace) check_inbounds_values(itp2, A2) check_oob(itp2) @@ -36,13 +36,13 @@ using AxisAlgorithms, OffsetArrays end for BC in (Flat,Line,Free,Periodic,Reflect,Natural), GT in (OnGrid, OnCell) - itp1 = @inferred(constructor(copier(A1), BSpline(Quadratic(BC())), GT())) + 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())) + itp2 = @inferred(constructor(copier(A2), BSpline(Quadratic(BC(GT()))))) check_axes(itp2, A2, isinplace) check_inbounds_values(itp2, A2) check_oob(itp2) @@ -50,13 +50,13 @@ using AxisAlgorithms, OffsetArrays end for BC in (Flat,Line,Free,Periodic), GT in (OnGrid, OnCell) - itp1 = @inferred(constructor(copier(A1), BSpline(Cubic(BC())), GT())) + 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())) + itp2 = @inferred(constructor(copier(A2), BSpline(Cubic(BC(GT()))))) check_axes(itp2, A2, isinplace) check_inbounds_values(itp2, A2) check_oob(itp2) @@ -68,7 +68,7 @@ using AxisAlgorithms, OffsetArrays 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()) + itp1 = interpolate!(copy(A), BSpline(Quadratic(InPlace(OnCell())))) check_axes(itp1, A) check_inbounds_values(itp1, A) check_oob(itp1) @@ -77,7 +77,7 @@ using AxisAlgorithms, OffsetArrays 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()) + itp2 = interpolate!(copy(A2), BSpline(Quadratic(InPlace(OnCell())))) check_axes(itp2, A2) check_inbounds_values(itp2, A2) check_oob(itp2) diff --git a/test/b-splines/quadratic.jl b/test/b-splines/quadratic.jl index d3ab9ffb..582c3c61 100644 --- a/test/b-splines/quadratic.jl +++ b/test/b-splines/quadratic.jl @@ -5,7 +5,7 @@ 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())) + 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,)) @@ -27,7 +27,7 @@ # 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())) + itp2 = @inferred(constructor(copier(A), BSpline(Quadratic(BC(GT()))))) check_axes(itp2, A, isinplace) check_inbounds_values(itp2, A) check_oob(itp2) @@ -44,14 +44,14 @@ 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()) + 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()) + itp2 = interpolate!(copy(A), BSpline(Quadratic(InPlace(OnCell())))) @test axes(itp2) == axes(A) check_inbounds_values(itp2, A) end diff --git a/test/extrapolation/non1.jl b/test/extrapolation/non1.jl index 7d86d294..ef7959f0 100644 --- a/test/extrapolation/non1.jl +++ b/test/extrapolation/non1.jl @@ -5,11 +5,11 @@ 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 1aa9eea6..3295add1 100644 --- a/test/extrapolation/runtests.jl +++ b/test/extrapolation/runtests.jl @@ -8,7 +8,7 @@ using Test xmax = 10 A = Float64[f(x) for x in 1:xmax] - itpg = interpolate(A, BSpline(Linear()), OnGrid()) + itpg = interpolate(A, BSpline(Linear())) etpg = extrapolate(itpg, Flat()) @test typeof(etpg) <: AbstractExtrapolation @@ -34,7 +34,7 @@ using Test x = @inferred(etpf(dual(-2.5,1))) @test isa(x, Dual) - etpl = extrapolate(itpg, Linear()) + 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) @@ -46,36 +46,28 @@ using Test 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())) + 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, ((Linear(), Flat()), Flat())) + 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, Linear()) + 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()), OnGrid()), 0.0im) + 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 - 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 @@ -87,15 +79,15 @@ using Test # @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_broken (@inferred(extrapolate(interpolate([0,0],BSpline(Linear()),OnGrid()),E))([1.2, 1.8, 3.1])) == [0,0,0] + for E in [0,Flat(),Line(),Periodic(),Reflect()] + @test_broken (@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()), OnGrid()); # itps = scale(itp, 1:10, 1:4) - # itpe = extrapolate(itps, (Linear(), Interpolations.Throw())) + # itpe = extrapolate(itps, (Line(), Interpolations.Throw())) # @test itpe(10.1, 1) ≈ 10.1 # @test_throws BoundsError itpe(9.9, 0) diff --git a/test/extrapolation/type-stability.jl b/test/extrapolation/type-stability.jl index 6461fe38..57b3d59d 100644 --- a/test/extrapolation/type-stability.jl +++ b/test/extrapolation/type-stability.jl @@ -6,11 +6,11 @@ using Test, Interpolations, DualNumbers 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 ) @@ -30,7 +30,7 @@ end 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 ( @@ -53,7 +53,7 @@ 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(etp(dual(1.5,1), dual(1.5,1)))) == typeof(@inferred(etp(dual(6.5,1), dual(3.5,1)))) diff --git a/test/gradient.jl b/test/gradient.jl index 122249bb..544cf871 100644 --- a/test/gradient.jl +++ b/test/gradient.jl @@ -11,28 +11,26 @@ using Test, Interpolations, DualNumbers, LinearAlgebra for (A, g) in ((A1, g1), (A2, g2)) # Gradient of Constant should always be 0 - itp = interpolate(A, BSpline(Constant()), OnGrid()) + itp = interpolate(A, BSpline(Constant())) for x in InterpolationTestUtils.thirds(axes(A)) @test all(iszero, Interpolations.gradient(itp, x...)) @test all(iszero, Interpolations.gradient!(g, itp, x...)) end - for GT in (OnGrid, OnCell) - itp = interpolate(A, BSpline(Linear()), GT()) - check_gradient(itp, g) - I = first(eachindex(itp)) - @test Interpolations.gradient(itp, I) == Interpolations.gradient(itp, Tuple(I)...) - 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()) + 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()) + 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)...) @@ -41,7 +39,7 @@ using Test, Interpolations, DualNumbers, LinearAlgebra # 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()), OnGrid()) + BSpline(Linear())) # itp2 = interpolate((1:nx-1,), Float64[f1(x) for x in 1:nx-1], # Gridded(Linear())) for itp in (itp1, )#itp2) @@ -72,7 +70,7 @@ using Test, Interpolations, DualNumbers, LinearAlgebra # 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()) + 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))) @@ -97,7 +95,7 @@ using Test, Interpolations, DualNumbers, LinearAlgebra xg = Float64[1:5;] y = qfunc(xg) - iq = interpolate(y, BSpline(Quadratic(Free())), OnCell()) + iq = interpolate(y, BSpline(Quadratic(Free(OnCell())))) x = 1.8 @test iq(x) ≈ qfunc(x) @test (Interpolations.gradient(iq,x))[1] ≈ dqfunc(x) @@ -105,14 +103,14 @@ using Test, Interpolations, DualNumbers, LinearAlgebra # 2d (biquadratic) p = [(x-1.75)^2 for x = 1:7] A = p*p' - iq = interpolate(A, BSpline(Quadratic(Free())), OnCell()) + 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()) + 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) @@ -120,7 +118,7 @@ using Test, Interpolations, DualNumbers, LinearAlgebra @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()) + 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) @@ -130,10 +128,10 @@ using Test, Interpolations, DualNumbers, LinearAlgebra 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())), BSpline(Linear())), GT()) - itp_c = interpolate(A2, (NoInterp(), BSpline(Quadratic(BC()))), GT()) - itp_d = interpolate(A2, (BSpline(Quadratic(BC())), NoInterp()), GT()) + 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 diff --git a/test/hessian.jl b/test/hessian.jl index 34f617b6..2af7574d 100644 --- a/test/hessian.jl +++ b/test/hessian.jl @@ -10,35 +10,36 @@ using Test, Interpolations, LinearAlgebra h2 = Array{Float64}(undef, 2, 2) for (A, h) in ((A1, h1), (A2, h2)) - for GT in (OnGrid, OnCell) - for itp in (interpolate(A, BSpline(Constant()), GT()), - interpolate(A, BSpline(Linear()), GT())) - 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 + 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()) + 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()) + itp = interpolate(A, BSpline(Cubic(BC(GT())))) check_hessian(itp, h) end end - # TODO: mixed interpolation (see gradient.jl) + 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/issues/runtests.jl b/test/issues/runtests.jl index 30d1a52c..31b17e88 100644 --- a/test/issues/runtests.jl +++ b/test/issues/runtests.jl @@ -5,7 +5,7 @@ using Interpolations, Test A = rand(1:20, 100, 100) # In #34, this incantation throws - itp = interpolate(A, BSpline(Quadratic(Flat())), OnCell()) + 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] diff --git a/test/nointerp.jl b/test/nointerp.jl index 77ba22ea..76b7843d 100644 --- a/test/nointerp.jl +++ b/test/nointerp.jl @@ -1,6 +1,6 @@ @testset "NoInterp" begin a = reshape(1:12, 3, 4) - ai = interpolate(a, NoInterp(), OnGrid()) + ai = interpolate(a, NoInterp()) @test eltype(ai) == Int check_axes(ai, a) check_inbounds_values(ai, a) diff --git a/test/readme-examples.jl b/test/readme-examples.jl index 93f735b2..0e6fe84d 100644 --- a/test/readme-examples.jl +++ b/test/readme-examples.jl @@ -8,21 +8,21 @@ using Interpolations, Test A = randn(5, 5) # Nearest-neighbor interpolation - itp = interpolate(a, BSpline(Constant()), OnCell()) + itp = interpolate(a, BSpline(Constant())) v = itp(5.4) # returns a[5] @test v ≈ 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]) @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()) + 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] @test v ≈ (0.35*A[3,5] + 0.65*A[4,5]) @@ -40,7 +40,7 @@ using Interpolations, Test 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) # @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) diff --git a/test/scaling/dimspecs.jl b/test/scaling/dimspecs.jl index d2fb2c32..009771fa 100644 --- a/test/scaling/dimspecs.jl +++ b/test/scaling/dimspecs.jl @@ -5,7 +5,7 @@ using Interpolations, DualNumbers, Test, LinearAlgebra 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())), BSpline(Linear())), OnGrid()) + itp = interpolate(Float64[f(x,y) for x in xs, y in ys], (BSpline(Quadratic(Periodic(OnGrid()))), BSpline(Linear()))) sitp = scale(itp, xs, ys) for (ix,x) in enumerate(xs), (iy,y) in enumerate(ys) diff --git a/test/scaling/nointerp.jl b/test/scaling/nointerp.jl index 9c89cbe7..2128990b 100644 --- a/test/scaling/nointerp.jl +++ b/test/scaling/nointerp.jl @@ -10,7 +10,7 @@ using Interpolations, Test, LinearAlgebra, Random A = hcat(map(f1, xs), map(f2, xs), map(f3, xs)) - itp = interpolate(A, (BSpline(Quadratic(Periodic())), NoInterp()), OnGrid()) + itp = interpolate(A, (BSpline(Quadratic(Periodic(OnGrid()))), NoInterp())) sitp = scale(itp, xs, ys) for (ix,x0) in enumerate(xs[1:end-1]), y0 in ys @@ -26,8 +26,8 @@ using Interpolations, Test, LinearAlgebra, Random 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) diff --git a/test/scaling/scaling.jl b/test/scaling/scaling.jl index c1dad97f..87a90080 100644 --- a/test/scaling/scaling.jl +++ b/test/scaling/scaling.jl @@ -5,7 +5,7 @@ using Test, LinearAlgebra # 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()) + itp = interpolate(1:1.0:10, BSpline(Linear())) sitp = @inferred(scale(itp, -3:.5:1.5)) @test typeof(sitp) <: Interpolations.ScaledInterpolation @@ -24,7 +24,7 @@ using Test, LinearAlgebra ys = -4:.2:4 zs = Float64[testfunction(x,y) for x in xs, y in ys] - itp2 = interpolate(zs, BSpline(Quadratic(Flat())), OnGrid()) + itp2 = interpolate(zs, BSpline(Quadratic(Flat(OnGrid())))) sitp2 = @inferred scale(itp2, xs, ys) for x in xs, y in ys @@ -34,7 +34,7 @@ using Test, LinearAlgebra # Test gradients of scaled grids xs = -pi:.1:pi ys = map(sin, xs) - itp = interpolate(ys, BSpline(Linear()), OnGrid()) + itp = interpolate(ys, BSpline(Linear())) sitp = @inferred scale(itp, xs) for x in -pi:.1:pi @@ -47,7 +47,7 @@ using Test, LinearAlgebra @inferred(sitp2(-3, 1)) @inferred(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) + 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 diff --git a/test/scaling/withextrap.jl b/test/scaling/withextrap.jl index ea2d2aee..77b880bd 100644 --- a/test/scaling/withextrap.jl +++ b/test/scaling/withextrap.jl @@ -23,7 +23,7 @@ using Interpolations, Test end for GT in (OnGrid, OnCell) - itp = interpolate(ys, BSpline(Quadratic(Flat())), GT()) + itp = interpolate(ys, BSpline(Quadratic(Flat(GT())))) # Test extrapolating, then scaling eitp = extrapolate(itp, Flat()) diff --git a/test/typing.jl b/test/typing.jl index c26c431d..9cf02eaa 100644 --- a/test/typing.jl +++ b/test/typing.jl @@ -7,7 +7,7 @@ using Interpolations, Test, LinearAlgebra 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), @@ -28,7 +28,7 @@ using Interpolations, Test, LinearAlgebra # Rational element types R = Rational{Int}[x^2//10 for x in 1:10] - itp = interpolate(R, BSpline(Quadratic(Free())), OnCell()) + itp = interpolate(R, BSpline(Quadratic(Free(OnCell())))) @test typeof(itp(11//10)) == Rational{Int} @test itp(11//10) == (11//10)^2//10 From 54552ad6e122d13e420ab58307da677571db7283 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Fri, 31 Aug 2018 04:49:09 -0500 Subject: [PATCH 17/29] Introduce and implement WeightedIndex --- src/Interpolations.jl | 74 ++++++ src/b-splines/b-splines.jl | 12 +- src/b-splines/constant.jl | 14 +- src/b-splines/cubic.jl | 26 +- src/b-splines/indexing.jl | 452 +++++++++++++-------------------- src/b-splines/linear.jl | 15 +- src/b-splines/quadratic.jl | 15 +- src/extrapolation/filled.jl | 3 +- src/nointerp/nointerp.jl | 10 +- src/scaling/scaling.jl | 14 +- src/utils.jl | 26 +- test/InterpolationTestUtils.jl | 14 +- test/gradient.jl | 8 +- test/runtests.jl | 4 +- 14 files changed, 333 insertions(+), 354 deletions(-) diff --git a/src/Interpolations.jl b/src/Interpolations.jl index 334180ff..10c89099 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -127,6 +127,80 @@ count_interp_dims(it::Type{IT}, n) where IT<:Tuple{Vararg{InterpolationType,N}} _count_interp_dims(c + count_interp_dims(IT1), args...) _count_interp_dims(c) = c + +""" + wi = WeightedIndex(indexes, weights) + +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 + +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 + + + """ w = value_weights(degree, δx) diff --git a/src/b-splines/b-splines.jl b/src/b-splines/b-splines.jl index 9cb15b8a..0a6b2cbc 100644 --- a/src/b-splines/b-splines.jl +++ b/src/b-splines/b-splines.jl @@ -80,12 +80,12 @@ _ubounds(axs, itp) = (ubound(axs[1], getfirst(itp)), _ubounds(Base.tail(axs), ge _lbounds(::Tuple{}, itp) = () _ubounds(::Tuple{}, itp) = () -lbound(ax::AbstractUnitRange, bs::BSpline) = lbound(ax, degree(bs)) -lbound(ax::AbstractUnitRange, deg::Degree) = first(ax) -lbound(ax::AbstractUnitRange, deg::DegreeBC) = lbound(ax, deg, deg.bc.gt) -ubound(ax::AbstractUnitRange, bs::BSpline) = ubound(ax, degree(bs)) -ubound(ax::AbstractUnitRange, deg::Degree) = last(ax) -ubound(ax::AbstractUnitRange, deg::DegreeBC) = ubound(ax, deg, deg.bc.gt) +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 diff --git a/src/b-splines/constant.jl b/src/b-splines/constant.jl index b416ef74..037dbbcd 100644 --- a/src/b-splines/constant.jl +++ b/src/b-splines/constant.jl @@ -2,20 +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 -function base_rem(::Constant, bounds, x) - xm = roundbounds(x, bounds) +function positions(::Constant, ax, x) # discontinuity occurs at half-integer locations + xm = roundbounds(x, ax) δx = x - xm fast_trunc(Int, xm), δx end -expand_index(::Constant, xi::Number, ax::AbstractUnitRange, δx) = (xi,) - -value_weights(::Constant, δx) = (oneunit(δx),) -gradient_weights(::Constant, δx) = (zero(δx),) -hessian_weights(::Constant, δx) = (zero(δx),) +value_weights(::Constant, δx) = (1,) +gradient_weights(::Constant, δx) = (0,) +hessian_weights(::Constant, δx) = (0,) padded_axis(ax::AbstractUnitRange, ::BSpline{Constant}) = ax diff --git a/src/b-splines/cubic.jl b/src/b-splines/cubic.jl index fca814b8..c6f152c2 100644 --- a/src/b-splines/cubic.jl +++ b/src/b-splines/cubic.jl @@ -27,28 +27,18 @@ When we derive boundary conditions we will use derivatives `y_0'(x)` and """ Cubic -function base_rem(::Cubic, bounds, x) - xf = floorbounds(x, bounds) - xf -= ifelse(xf > last(bounds)-1, oneunit(xf), zero(xf)) +function positions(deg::Cubic, ax, x) + xf = floorbounds(x, ax) + xf -= ifelse(xf > last(ax)-1, oneunit(xf), zero(xf)) δx = x - xf - fast_trunc(Int, xf), δx + expand_index(deg, fast_trunc(Int, xf), ax, δx), δx end -expand_index(::Cubic{BC}, xi::Number, ax::AbstractUnitRange, δx) where BC = (xi-1, xi, xi+1, xi+2) +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)) -# expand_coefs(::Type{BSpline{Cubic{BC}}}, δx) = cvcoefs(δx) -# expand_coefs(::Type{BSpline{Cubic{BC}}}, dref, d, δx) = ifelse(d==dref, cgcoefs(δx), cvcoefs(δx)) -# function expand_coefs(::Type{BSpline{Cubic{BC}}}, dref1, dref2, d, δx) -# if dref1 == dref2 -# d == dref1 ? chcoefs(δx) : cvcoefs(δx) -# else -# d == dref1 | d == dref2 ? cgcoefs(δx) : cvcoefs(δx) -# end -# end - -function value_weights(::BSpline{<:Cubic}, δx) +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, @@ -56,7 +46,7 @@ function value_weights(::BSpline{<:Cubic}, δx) SimpleRatio(1,6) * x3) end -function gradient_weights(::BSpline{<:Cubic}, δx) +function gradient_weights(::Cubic, δx) x2, xcomp2 = sqr(δx), sqr(1-δx) (-SimpleRatio(1,2) * xcomp2, -2*δx + SimpleRatio(3,2)*x2, @@ -64,7 +54,7 @@ function gradient_weights(::BSpline{<:Cubic}, δx) SimpleRatio(1,2) * x2) end -hessian_weights(::BSpline{<:Cubic}, δx) = (1-δx, 3*δx-2, 3*(1-δx)-2, δx) +hessian_weights(::Cubic, δx) = (1-δx, 3*δx-2, 3*(1-δx)-2, δx) # ------------ # diff --git a/src/b-splines/indexing.jl b/src/b-splines/indexing.jl index 796465b8..e338c1a9 100644 --- a/src/b-splines/indexing.jl +++ b/src/b-splines/indexing.jl @@ -1,8 +1,50 @@ -### Primary evaluation (indexing) entry points +### 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) +end + +# 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_getindex(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 + +# version for WeightedAdjIndex +_interp_getindex(A, i::Int, weights::NTuple{K,Number}, rest, I::Vararg{Int,M}) where {M,K} = + weights[1] * interp_getindex(A, rest, I..., i) + _interp_getindex(A, i+1, Base.tail(weights), rest, I...) +_interp_getindex(A, i::Int, weights::Tuple{Number}, rest, I::Vararg{Int,M}) where M = + weights[1] * interp_getindex(A, rest, I..., i) +_interp_getindex(A, i::Int, weights::Tuple{}, rest, I::Vararg{Int,M}) where M = + error("exhausted weights, this should never happen") # helps inference + +# version for WeightedArbIndex +_interp_getindex(A, indexes::NTuple{K,Int}, weights::NTuple{K,Number}, rest, I::Vararg{Int,M}) where {M,K} = + weights[1] * interp_getindex(A, rest, I..., indexes[1]) + _interp_getindex(A, Base.tail(indexes), Base.tail(weights), rest, I...) +_interp_getindex(A, indexes::Tuple{Int}, weights::Tuple{Number}, rest, I::Vararg{Int,M}) where M = + weights[1] * interp_getindex(A, rest, I..., indexes[1]) +_interp_getindex(A, indexes::Tuple{}, weights::Tuple{}, rest, I::Vararg{Int,M}) where M = + error("exhausted weights and indexes, this should never happen") + + +### Primary evaluation entry points (itp(x...), gradient(itp, x...), and hessian(itp, x...)) + +itpinfo(itp) = (tcollect(itpflag, itp), axes(itp)) @inline function (itp::BSplineInterpolation{T,N})(x::Vararg{Number,N}) where {T,N} @boundscheck (checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x)) - expand_value(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) @@ -13,25 +55,30 @@ 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) - expand_gradient(itp, x) + wis = weightedindexes((value_weights, gradient_weights), itpinfo(itp)..., x) + SVector(map(inds->itp.coefs[inds...], wis)) end -@inline function gradient!(dest, itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N} - @boundscheck checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x) - expand_gradient!(dest, itp, x) +@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) - expand_hessian(itp, x) + wis = weightedindexes((value_weights, gradient_weights, hessian_weights), itpinfo(itp)..., x) + symmatrix(map(inds->itp.coefs[inds...], wis)) end -@inline function hessian!(dest, itp::BSplineInterpolation{T,N}, x::Vararg{Number,N}) where {T,N} - @boundscheck checkbounds(Bool, itp, x...) || Base.throw_boundserror(itp, x) - expand_hessian(itp, x) +@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{Number,N}) where N = checklubounds(lbounds(itp), ubounds(itp), x) +checklubounds(ls, us, xs) = _checklubounds(true, ls, us, xs) +_checklubounds(tf::Bool, ls, us, xs) = _checklubounds(tf & (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)...) @@ -40,281 +87,113 @@ end itp.(Iterators.product(x...)) end -""" - val = expand_value(itp, x) - -Interpolate `itp` at `x`. -""" -function expand_value(itp::AbstractInterpolation, x::Tuple) - coefs = coefficients(itp) - degree = interpdegree(itp) - ixs, rxs = splitgrouped(expand_indices_resid(degree, axes(itp), x)) - cxs = expand_weights(value_weights, degree, rxs) - expand(coefs, cxs, ixs) -end - -""" - g = expand_gradient(itp, x) - -Calculate the interpolated gradient of `itp` at `x`. -""" -function expand_gradient(itp::AbstractInterpolation, x::Tuple) - coefs = coefficients(itp) - degree = interpdegree(itp) - ixs, rxs = splitgrouped(expand_indices_resid(degree, axes(itp), x)) - cxs = expand_weights(value_weights, degree, rxs) - gxs = expand_weights(gradient_weights, degree, rxs) - expand(coefs, (cxs, gxs), ixs) -end - -function expand_gradient!(dest, itp::AbstractInterpolation, x::Tuple) - coefs = coefficients(itp) - degree = interpdegree(itp) - ixs, rxs = splitgrouped(expand_indices_resid(degree, axes(itp), x)) - cxs = expand_weights(value_weights, degree, rxs) - gxs = expand_weights(gradient_weights, degree, rxs) - expand!(dest, coefs, (cxs, gxs), ixs) -end - -""" - H = expand_hessian(itp, x) - -Calculate the interpolated hessian of `itp` at `x`. -""" -function expand_hessian(itp::AbstractInterpolation, x::Tuple) - coefs = coefficients(itp) - degree = interpdegree(itp) - ixs, rxs = splitgrouped(expand_indices_resid(degree, axes(itp), x)) - cxs = expand_weights(value_weights, degree, rxs) - gxs = expand_weights(gradient_weights, degree, rxs) - hxs = expand_weights(hessian_weights, degree, rxs) - expand(coefs, (cxs, gxs, hxs), ixs) -end - -""" - Weights{N} - -A type alias for an `N`-dimensional tuple of weights or indexes for interpolation. - -# Example - -If you are performing linear interpolation (degree 1) in three dimensions at the point -`x = [5.1, 8.2, 4.3]`, then the floating-point residuals are `[0.1, 0.2, 0.3]`. -For value interpoations, the corresponding `Weights` would be - - ((0.9,0.1), (0.8,0.2), (0.7,0.3)) # (dim1, dim2, dim3) - -Note each "inner" tuple, for value interpolation, sums to 1. For gradient Weights, -each inner tuple would be `(-1, 1)`, and for hessian Weights each would be `(0, 0)`. - -The same structure can be used for the integer indexes at which each coefficient is evaluated. -For the example above (with `x = [5.1, 8.2, 4.3]`), the indexes would be - - ((5,6), (8,9), (4,5)) - -corresponding to the integer pairs that bracket each coordinate. - -When performing mixed interpolation (e.g., `Linear` along dimension 1 and `Cubic` along dimension 2), -the inner tuples may not all be of the same length. -""" -const Weights{N} = NTuple{N,Tuple{Vararg{<:Number}}} - -""" - Indexes{N} - -The same as [`Weights`](@ref) for the integer-values used as evaluation locations for the -coefficients array. -""" -const Indexes{N} = NTuple{N,Tuple{Vararg{<:Integer}}} - -""" - val = expand(coefs, vweights::Weights, ixs::Indexes) - g = expand(coefs, (vweights, gweights), ixs) - H = expand(coefs, (vweights, gweights, hweights), ixs) - -Calculate the value, gradient, or hessian of a separable AbstractInterpolation object. -This function works recursively, processing one index at a time and calling itself as - - ret = expand(coefs, , ixs, iexpanded...) - -(The `iexpanded` form is not intended to be called directly by users.) For example, -for two-dimensional linear interpolation at a point `x = [5.1, 8.2]` the corresponding -top-level call would be - - # weights ixs - expand(coefs, ((0.9,0.1), (0.8,0.2)), ((5,6), (8,9)) - -After one round of recursion this becomes - - # weights ixs iexpanded - 0.9*expand(coefs, ((0.8,0.2),), ((8,9),), 5) + - 0.1*expand(coefs, ((0.8,0.2),), ((8,9),), 6) - -(The first dimension has been processed.) After another round, this becomes - - # wts ixs iexpanded - 0.9*(0.8*expand(coefs, (), (), 5, 8) + 0.2*expand(coefs, (), (), 5, 9)) + - 0.1*(0.8*expand(coefs, (), (), 6, 8) + 0.2*expand(coefs, (), (), 6, 9)) - -Now that the weights and `ixs` are empty and all indices are in `iexpanded`, -it finally resolves to - - 0.9*(0.8*coefs[5, 8] + 0.2*coefs[5, 9]) + - 0.1*(0.8*coefs[6, 8] + 0.2*coefs[6, 9]) - -which is the expression for bilinear interpolation at the given `x`. - -For calculating the components of the gradient and hessian, individual dimensions of -`gweights` and/or `hweights` will be substituted into the appropriate slot. For example, -in three dimensions - - g[1] = expand(coefs, (gweights[1], vweights[2], vweights[3]), ixs) - g[2] = expand(coefs, (vweights[1], gweights[2], vweights[3]), ixs) - g[3] = expand(coefs, (vweights[1], vweights[2], gweights[3]), ixs) -""" -function expand(coefs::AbstractArray, vweights::Weights, ixs::Indexes, iexpanded::Vararg{Integer,M}) where {M} - w1, wrest = vweights[1], Base.tail(vweights) - ix1, ixrest = ixs[1], Base.tail(ixs) - _expand1(coefs, w1, ix1, wrest, ixrest, iexpanded) -end -function expand(coefs::AbstractArray{T,N}, vweights::Tuple{}, ixs::Tuple{}, iexpanded::Vararg{Integer,N}) where {T,N} - @inbounds coefs[iexpanded...] # @inbounds is safe because we checked in the original call -end - -const HasNoInterp{N} = NTuple{N,Tuple{Vararg{<:Union{Number,NoInterp}}}} -expand(coefs::AbstractArray, vweights::HasNoInterp, ixs::Indexes, iexpanded::Vararg{Integer,M}) where {M} = NoInterp() - -# _expand1 handles the expansion of a single dimension weight list (of length L) -@inline _expand1(coefs, w1, ix1, wrest, ixrest, iexpanded) = - w1[1] * expand(coefs, wrest, ixrest, iexpanded..., ix1[1]) + - _expand1(coefs, Base.tail(w1), Base.tail(ix1), wrest, ixrest, iexpanded) -@inline _expand1(coefs, w1::Tuple{Number}, ix1::Tuple{Integer}, wrest, ixrest, iexpanded) = - w1[1] * expand(coefs, wrest, ixrest, iexpanded..., ix1[1]) - -# Expansion of the gradient -function expand(coefs, (vweights, gweights)::Tuple{HasNoInterp{N},HasNoInterp{N}}, ixs::Indexes{N}) where N - # We swap in one gradient dimension per call to expand - SVector(skip_nointerp(ntuple(d->expand(coefs, substitute(vweights, d, gweights), ixs), Val(N))...)) -end -function expand!(dest, coefs, (vweights, gweights)::Tuple{HasNoInterp{N},HasNoInterp{N}}, ixs::Indexes{N}) where N - # We swap in one gradient dimension per call to expand - i = 0 - for d = 1:N - w = substitute(vweights, d, gweights) - w isa Weights || continue # if this isn't true, it must have a NoInterp in it - dest[i+=1] = expand(coefs, w, ixs) - end - dest -end - -# Expansion of the hessian -# To handle the immutability of SMatrix we build static methods that visit just the entries we need, -# which due to symmetry is just the upper triangular part -ntuple_sym(f, ::Val{0}) = () -ntuple_sym(f, ::Val{1}) = (f(1,1),) -ntuple_sym(f, ::Val{2}) = (f(1,1), f(1,2), f(2,2)) -ntuple_sym(f, ::Val{3}) = (f(1,1), f(1,2), f(2,2), f(1,3), f(2,3), f(3,3)) -ntuple_sym(f, ::Val{4}) = (f(1,1), f(1,2), f(2,2), f(1,3), f(2,3), f(3,3), f(1,4), f(2,4), f(3,4), f(4,4)) -@inline function ntuple_sym(f, ::Val{N}) where N - (ntuple_sym(f, Val(N-1))..., ntuple(i->f(i,N), Val(N))...) -end - -sym2dense(t::Tuple{}) = t -sym2dense(t::NTuple{1,T}) where T = t -sym2dense(t::NTuple{3,T}) where T = (t[1], t[2], t[2], t[3]) -sym2dense(t::NTuple{6,T}) where T = (t[1], t[2], t[4], t[2], t[3], t[5], t[4], t[5], t[6]) -sym2dense(t::NTuple{10,T}) where T = (t[1], t[2], t[4], t[7], t[2], t[3], t[5], t[8], t[4], t[5], t[6], t[9], t[7], t[8], t[9], t[10]) -function sym2dense(t::NTuple{L,T}) where {L,T} - # Warning: non-inferrable unless we make this @generated. - # Above 4 dims one might anyway prefer an Array, and use hessian! - N = ceil(Int, sqrt(2*L)) - @assert (N*(N+1))÷2 == L - a = Vector{T}(undef, N*N) - idx = 0 - for j = 1:N, i=1:N - iu, ju = ifelse(i>=j, (j, i), (i, j)) # index in the upper triangular - k = (ju*(ju+1))÷2 + iu - a[idx+=1] = t[k] - end - tuple(a...) -end - -squarematrix(t::NTuple{1,T}) where T = SMatrix{1,1,T}(t) -squarematrix(t::NTuple{4,T}) where T = SMatrix{2,2,T}(t) -squarematrix(t::NTuple{9,T}) where T = SMatrix{3,3,T}(t) -squarematrix(t::NTuple{16,T}) where T = SMatrix{4,4,T}(t) -function squarematrix(t::NTuple{L,T}) where {L,T} - # Warning: non-inferrable unless we make this @generated. - # Above 4 dims one might anyway prefer an Array, and use hessian! - N = floor(Int, sqrt(L)) - @assert N*N == L - SMatrix{N,N,T}(t) -end - -cumweights(w) = _cumweights(0, w...) -_cumweights(c, w1, w...) = (c+1, _cumweights(c+1, w...)...) -_cumweights(c, ::NoInterp, w...) = (c, _cumweights(c, w...)...) -_cumweights(c) = () - -function expand(coefs, (vweights, gweights, hweights)::NTuple{3,HasNoInterp{N}}, ixs::Indexes{N}) where N - coefs = ntuple_sym((i,j)->expand(coefs, substitute(vweights, i, j, gweights, hweights), ixs), Val(N)) - squarematrix(sym2dense(skip_nointerp(coefs...))) -end - -function expand!(dest, coefs, (vweights, gweights, hweights)::NTuple{3,HasNoInterp{N}}, ixs::Indexes{N}) where N - # The Hessian is nominally N × N, but if there are K NoInterp dims then it's N-K × N-K - indlookup = cumweights(hweights) # for d in 1:N, indlookup[d] returns the appropriate index in 1:N-K - for d2 = 1:N, d1 = 1:d2 - w = substitute(vweights, d1, d2, gweights, hweights) - w isa Weights || continue # if this isn't true, it must have a NoInterp in it - i, j = indlookup[d1], indlookup[d2] - dest[i, j] = dest[j, i] = expand(coefs, w, ixs) - end - dest -end -function expand_indices_resid(degree, axs, x) - item = expand_index_resid(getfirst(degree), axs[1], x[1]) - (item, expand_indices_resid(getrest(degree), Base.tail(axs), Base.tail(x))...) +function weightedindexes(fs::F, itpflags::NTuple{N,Flag}, axs::NTuple{N,AbstractUnitRange}, xs::NTuple{N,Number}) where {F,N} + parts = map((flag, ax, x)->weightedindex_parts(fs, flag, ax, x), itpflags, axs, xs) + weightedindexes(parts...) +end + +weightedindexes(i::Vararg{Int,N}) where N = i # the all-NoInterp case + +const PositionCoefs{P,C} = NamedTuple{(:position,:coefs),Tuple{P,C}} +const ValueParts{P,W} = PositionCoefs{P,Tuple{W}} +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) + +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] + +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 + +# 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 + +# 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 weightedindex_parts(fs::F, deg::Degree, ax, x) where F + pos, δx = positions(deg, ax, x) + (position=pos, coefs=fmap(fs, deg, δx)) end -expand_indices_resid(degree, ::Tuple{}, ::Tuple{}) = () - -function expand_index_resid(degree, ax, x::Number) - ix, δx = base_rem(degree, ax, x) - expand_index(degree, ix, ax, δx), δx -end - -expand_weights(f, degree::Union{Degree,NoInterp}, ixs) = - (f(degree, ixs[1]), expand_weights(f, degree, Base.tail(ixs))...) -expand_weights(f, degree::Union{Degree,NoInterp}, ::Tuple{}) = () - -expand_weights(f, degree::Tuple{Vararg{Union{Degree,NoInterp},N}}, ixs::NTuple{N,Number}) where N = - f.(degree, ixs) - -# expand_indices(degree::Union{Degree,NoInterp}, ixs, axs, δxs) = -# (expand_index(degree, ixs[1], axs[1], δxs[1]), expand_indices(degree, Base.tail(ixs), Base.tail(axs), Base.tail(δxs))...) -# expand_indices(degree::Union{Degree,NoInterp}, ::Tuple{}, ::Tuple{}, ::Tuple{}) = () - -# expand_indices(degree::Tuple{Vararg{Union{Degree,NoInterp},N}}, ixs::NTuple{N,Number}, axs::NTuple{N,Tuple{Real,Real}}, δxs::NTuple{N,Number}) where N = -# expand_index.(degree, ixs, axs, δxs) - -# expand_index(degree, ixs, bounds::Tuple{Real,Real}, δxs) = expand_index(degree, ixs, axfrombounds(bounds), δxs) - -checklubounds(ls, us, xs) = _checklubounds(true, ls, us, xs) -_checklubounds(tf::Bool, ls, us, xs) = _checklubounds(tf & (ls[1] <= xs[1] <= us[1]), - Base.tail(ls), Base.tail(us), Base.tail(xs)) -_checklubounds(tf::Bool, ::Tuple{}, ::Tuple{}, ::Tuple{}) = tf # 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 @@ -335,3 +214,28 @@ function floorbounds(x, ax) 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 + 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 diff --git a/src/b-splines/linear.jl b/src/b-splines/linear.jl index 2c85bd26..4587ad4a 100644 --- a/src/b-splines/linear.jl +++ b/src/b-splines/linear.jl @@ -1,7 +1,7 @@ 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,15 +21,14 @@ a piecewise linear function connecting each pair of neighboring data points. """ Linear -function base_rem(::Linear, bounds, x) - xf = floorbounds(x, bounds) - xf -= ifelse(xf >= last(bounds), oneunit(xf), zero(xf)) - δx = x - xf - fast_trunc(Int, xf), δx +function positions(::Linear, ax, 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 -expand_index(::Linear, xi::Number, ax::AbstractUnitRange, δx) = (xi, xi + ((δx!=0) | (xi < last(ax)))) - value_weights(::Linear, δx) = (1-δx, δx) gradient_weights(::Linear, δx) = (-oneunit(δx), oneunit(δx)) hessian_weights(::Linear, δx) = (zero(δx), zero(δx)) diff --git a/src/b-splines/quadratic.jl b/src/b-splines/quadratic.jl index 3d431b21..47bc60cd 100644 --- a/src/b-splines/quadratic.jl +++ b/src/b-splines/quadratic.jl @@ -26,25 +26,26 @@ When we derive boundary conditions we will use derivatives `y_1'(x-1)` and """ Quadratic -function base_rem(::Quadratic, bounds, x) - xm = roundbounds(x, bounds) +function positions(deg::Quadratic, ax, x) + xm = roundbounds(x, ax) δx = x - xm - fast_trunc(Int, xm), δx + expand_index(deg, fast_trunc(Int, xm), ax, δx), δx end -value_weights(::BSpline{<:Quadratic}, δx) = ( +value_weights(::Quadratic, δx) = ( sqr(δx - SimpleRatio(1,2))/2, SimpleRatio(3,4) - sqr(δx), sqr(δx + SimpleRatio(1,2))/2) -gradient_weights(::BSpline{<:Quadratic}, δx) = ( +gradient_weights(::Quadratic, δx) = ( δx - SimpleRatio(1,2), -2 * δx, δx + SimpleRatio(1,2)) -hessian_weights(::BSpline{<:Quadratic}, δx) = (oneunit(δx), -2*oneunit(δx), oneunit(δx)) +hessian_weights(::Quadratic, δx) = (oneunit(δx), -2*oneunit(δx), oneunit(δx)) -expand_index(::Quadratic{BC}, xi::Number, ax::AbstractUnitRange, δx) where BC = (xi-1, xi, xi+1) +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} = diff --git a/src/extrapolation/filled.jl b/src/extrapolation/filled.jl index e580533f..3686d7b0 100644 --- a/src/extrapolation/filled.jl +++ b/src/extrapolation/filled.jl @@ -21,7 +21,8 @@ extrapolate(itp::AbstractInterpolation{T,N,IT}, fillvalue) where {T,N,IT} = Fill itp = parent(etp) Tret = typeof(prod(x) * zero(T)) if checkbounds(Bool, itp, x...) - convert(Tret, expand_value(itp, x)) + wis = weightedindexes((value_weights,), itpinfo(itp)..., x) + convert(Tret, itp.coefs[wis...]) else convert(Tret, etp.fillvalue) end diff --git a/src/nointerp/nointerp.jl b/src/nointerp/nointerp.jl index 93a10a8f..c46d0191 100644 --- a/src/nointerp/nointerp.jl +++ b/src/nointerp/nointerp.jl @@ -14,12 +14,12 @@ prefilter(::Type{TWeights}, ::Type{TC}, A::AbstractArray, ::NoInterp) where {TWe lbound(ax, ::NoInterp) = first(ax) ubound(ax, ::NoInterp) = last(ax) -base_rem(::NoInterp, bounds, x::Number) = Int(x), 0 +weightedindex_parts(fs, ::NoInterp, ax, x::Number) = Int(x) -expand_index(::NoInterp, xi::Number, ax::AbstractUnitRange, δx) = (xi,) +# positions(::NoInterp, ax, x) = (Int(x),), 0 -value_weights(::NoInterp, δx) = (oneunit(δx),) -gradient_weights(::NoInterp, δx) = (NoInterp(),) -hessian_weights(::NoInterp, δx) = (NoInterp(),) +# value_weights(::NoInterp, δx) = (oneunit(δx),) +# gradient_weights(::NoInterp, δx) = (NoInterp(),) +# hessian_weights(::NoInterp, δx) = (NoInterp(),) padded_axis(ax::AbstractUnitRange, ::NoInterp) = ax diff --git a/src/scaling/scaling.jl b/src/scaling/scaling.jl index 4ecf4550..472ab16d 100644 --- a/src/scaling/scaling.jl +++ b/src/scaling/scaling.jl @@ -34,17 +34,17 @@ check_range(::Any, ax, r) = length(ax) == length(r) || throw(ArgumentError("The size(sitp::ScaledInterpolation) = size(sitp.itp) axes(sitp::ScaledInterpolation) = axes(sitp.itp) -lbounds(sitp::ScaledInterpolation) = _lbounds(sitp.ranges, itpflag(sitp.itp), gridflag(sitp.itp)) -ubounds(sitp::ScaledInterpolation) = _ubounds(sitp.ranges, itpflag(sitp.itp), gridflag(sitp.itp)) - -lbound(ax, ::BSpline, ::OnCell) = first(ax) - boundstep(ax) -ubound(ax, ::BSpline, ::OnCell) = last(ax) + boundstep(ax) -lbound(ax, ::BSpline, ::OnGrid) = first(ax) -ubound(ax, ::BSpline, ::OnGrid) = last(ax) +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 +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) + """ Returns *half* the width of one step of the range. diff --git a/src/utils.jl b/src/utils.jl index 5f8328e8..7f6ce00b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -4,6 +4,10 @@ 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) @@ -12,6 +16,10 @@ 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...) @@ -29,18 +37,12 @@ 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 -# Substitution for gradient components -function substitute(default::NTuple{N,Any}, d::Integer, subst::NTuple{N,Any}) where N - ntuple(i->ifelse(i==d, subst[i], default[i]), Val(N)) -end -function substitute(default::NTuple{N,Any}, d::Integer, val) where N - ntuple(i->ifelse(i==d, val, default[i]), Val(N)) -end - -# Substitution for hessian components -function substitute(default::NTuple{N,Any}, d1::Integer, d2::Integer, subst1::NTuple{N,Any}, subst2::NTuple{N,Any}) where N - ntuple(i->ifelse(i==d1==d2, subst2[i], ifelse(i==d1, subst1[i], ifelse(i==d2, subst1[i], default[i]))), Val(N)) -end +# 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...) diff --git a/test/InterpolationTestUtils.jl b/test/InterpolationTestUtils.jl index d06ebdc8..8232cd3d 100644 --- a/test/InterpolationTestUtils.jl +++ b/test/InterpolationTestUtils.jl @@ -2,7 +2,6 @@ module InterpolationTestUtils using Test, Interpolations, ForwardDiff, StaticArrays using Interpolations: degree, itpflag, bounds, lbounds, ubounds -using Interpolations: substitute export check_axes, check_inbounds_values, check_oob, can_eval_near_boundaries, check_gradient, check_hessian @@ -51,6 +50,8 @@ function check_axes(itp, A, isinplace=false) 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 @@ -91,7 +92,7 @@ function can_eval_near_boundaries(itp::AbstractInterpolation{T,1}) where T end function can_eval_near_boundaries(itp::AbstractInterpolation) - l, u = lbounds(itp), ubounds(itp) + 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 @@ -106,6 +107,11 @@ function can_eval_near_boundaries(itp::AbstractInterpolation) 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...)...) @@ -117,6 +123,8 @@ 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 @@ -127,6 +135,8 @@ 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 diff --git a/test/gradient.jl b/test/gradient.jl index 544cf871..c9911851 100644 --- a/test/gradient.jl +++ b/test/gradient.jl @@ -13,8 +13,8 @@ using Test, Interpolations, DualNumbers, LinearAlgebra # Gradient of Constant should always be 0 itp = interpolate(A, BSpline(Constant())) for x in InterpolationTestUtils.thirds(axes(A)) - @test all(iszero, Interpolations.gradient(itp, x...)) - @test all(iszero, Interpolations.gradient!(g, itp, x...)) + @test all(iszero, @inferred(Interpolations.gradient(itp, x...))) + @test all(iszero, @inferred(Interpolations.gradient!(g, itp, x...))) end itp = interpolate(A, BSpline(Linear())) @@ -25,8 +25,8 @@ using Test, Interpolations, DualNumbers, LinearAlgebra 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)...) + 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) diff --git a/test/runtests.jl b/test/runtests.jl index a3642bd2..92a7a6b4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,9 +21,9 @@ using Interpolations # scaling tests include("scaling/runtests.jl") - # # test gradient evaluation + # test gradient evaluation include("gradient.jl") - # # test hessian evaluation + # test hessian evaluation include("hessian.jl") # # gridded interpolation tests From 32f4a1fbca21306d6385dec08e80e5444c6e0a46 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sat, 8 Sep 2018 12:29:21 -0500 Subject: [PATCH 18/29] IO fixes --- src/io.jl | 34 +--------------------------------- test/io.jl | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 45 deletions(-) diff --git a/src/io.jl b/src/io.jl index 39b38a50..164ff71e 100644 --- a/src/io.jl +++ b/src/io.jl @@ -1,11 +1,8 @@ -# after this, functionality was incorporated into Base function Base.showarg(io::IO, A::BSplineInterpolation{T,N,TW,ST,GT}, toplevel) where {T,N,TW,ST,GT} print(io, "interpolate(") Base.showarg(io, A.coefs, false) print(io, ", ") - _showtypeparam(io, ST) - print(io, ", ") - _showtypeparam(io, GT) + show(io, itpflag(A)) if toplevel print(io, ") with element type ",T) else @@ -66,32 +63,3 @@ end # print(io, " with element type ",T) # end # 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, ", ") - end - print(io, ')') -end diff --git a/test/io.jl b/test/io.jl index 39e69e84..0e196b51 100644 --- a/test/io.jl +++ b/test/io.jl @@ -7,23 +7,23 @@ using Test @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())) + @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Constant())) 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(Constant())) + @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, BSpline(Constant())) 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(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(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()), OnGrid()) - @test summary(itp) == "8×20 interpolate(::Array{Float64,2}, (BSpline(Linear()), NoInterp()), OnGrid()) 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" + 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 From e6c8a54ec97398fcc673b8ae7a722451e7da410b Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sat, 8 Sep 2018 13:43:10 -0500 Subject: [PATCH 19/29] Fix boundserror in README example --- test/readme-examples.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/readme-examples.jl b/test/readme-examples.jl index 0e6fe84d..913f2f14 100644 --- a/test/readme-examples.jl +++ b/test/readme-examples.jl @@ -4,7 +4,7 @@ using Interpolations, Test @testset "Readme Examples" begin ## Bsplines - a = randn(5) + a = randn(6) A = randn(5, 5) # Nearest-neighbor interpolation From 42fa67137706b2ca7c1af8b737ea65c198da4ea8 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sun, 9 Sep 2018 05:17:40 -0500 Subject: [PATCH 20/29] Re-implement fast iteration for ScaledInterpolation --- src/scaling/scaling.jl | 248 ++++++++++++++-------------------------- test/readme-examples.jl | 22 ++-- test/scaling/scaling.jl | 66 ++++------- 3 files changed, 123 insertions(+), 213 deletions(-) diff --git a/src/scaling/scaling.jl b/src/scaling/scaling.jl index 472ab16d..42c4cb89 100644 --- a/src/scaling/scaling.jl +++ b/src/scaling/scaling.jl @@ -31,30 +31,38 @@ 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")) +# With regards to size and [], ScaledInterpolation behaves like the underlying interpolation object size(sitp::ScaledInterpolation) = size(sitp.itp) axes(sitp::ScaledInterpolation) = axes(sitp.itp) +@propagate_inbounds function Base.getindex(sitp::ScaledInterpolation{T,N}, i::Vararg{Int,N}) where {T,N} + sitp.itp[i...] +end + 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 - -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) - """ 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}) + sitp(to_indices(sitp, x)...) +end (sitp::ScaledInterpolation{T,1}, x::Number, y::Int) where {T} = y == 1 ? sitp(x) : Base.throw_boundserror(sitp, (x, y)) @@ -134,167 +142,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 -# ### 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) -# end +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 -# function index_gen1(::Union{Type{NoInterp}, Type{BSpline{Constant}}}) -# quote -# value = iter.itp_tail[1] -# end -# 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(::Type{BSpline{Linear}}) -# quote -# p = iter.itp_tail -# value = c_1*p[1] + cp_1*p[2] -# end -# end +struct ScaledIterState{N,V} + cistate::CartesianIndex{N} + ibreak::Int + cached_evaluations::NTuple{N,V} +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] -# 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 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_gen_tail(::Type{BSpline{Linear}}, ::Type{IT}, N) where IT -# [index_gen(BS1, IT, N, i) for i = 0:1] -# end +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 + (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 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) -# end -# 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") -# @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 +# 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} +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/test/readme-examples.jl b/test/readme-examples.jl index 913f2f14..efafa774 100644 --- a/test/readme-examples.jl +++ b/test/readme-examples.jl @@ -18,7 +18,7 @@ using Interpolations, Test @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 + # 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 @@ -27,13 +27,13 @@ using Interpolations, Test @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) + ## 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 @@ -41,9 +41,9 @@ using Interpolations, Test 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) + 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) diff --git a/test/scaling/scaling.jl b/test/scaling/scaling.jl index 87a90080..ace48ec6 100644 --- a/test/scaling/scaling.jl +++ b/test/scaling/scaling.jl @@ -50,45 +50,29 @@ using Test, LinearAlgebra 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)) - - # 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)}} - - # 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 - + # 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 + 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 end From 69dc2c5bd232d3243b002e172d752a88595838b0 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Fri, 31 Aug 2018 04:49:39 -0500 Subject: [PATCH 21/29] Re-implement GriddedInterpolation --- src/Interpolations.jl | 4 +- src/b-splines/indexing.jl | 20 +++-- src/b-splines/linear.jl | 2 +- src/deprecations.jl | 1 + src/gridded/constant.jl | 8 ++ src/gridded/gridded.jl | 66 +++++++------- src/gridded/indexing.jl | 185 ++++++++++++-------------------------- test/gridded/gridded.jl | 68 +++++++------- test/gridded/mixed.jl | 15 ++-- test/gridded/runtests.jl | 2 +- 10 files changed, 153 insertions(+), 218 deletions(-) diff --git a/src/Interpolations.jl b/src/Interpolations.jl index 10c89099..df345739 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -33,7 +33,7 @@ using LinearAlgebra, SparseArrays using StaticArrays, WoodburyMatrices, Ratios, AxisAlgorithms, OffsetArrays using Base: @propagate_inbounds -import Base: convert, size, axes, promote_rule, ndims, eltype, checkbounds +import Base: convert, size, axes, promote_rule, ndims, eltype, checkbounds, axes1 abstract type Flag end abstract type InterpolationType <: Flag end @@ -300,7 +300,7 @@ end include("nointerp/nointerp.jl") include("b-splines/b-splines.jl") -# include("gridded/gridded.jl") +include("gridded/gridded.jl") include("extrapolation/extrapolation.jl") include("scaling/scaling.jl") include("utils.jl") diff --git a/src/b-splines/indexing.jl b/src/b-splines/indexing.jl index e338c1a9..9df16b2b 100644 --- a/src/b-splines/indexing.jl +++ b/src/b-splines/indexing.jl @@ -88,8 +88,8 @@ end end -function weightedindexes(fs::F, itpflags::NTuple{N,Flag}, axs::NTuple{N,AbstractUnitRange}, xs::NTuple{N,Number}) where {F,N} - parts = map((flag, ax, x)->weightedindex_parts(fs, flag, ax, x), itpflags, axs, xs) +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 @@ -179,7 +179,7 @@ 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 weightedindex_parts(fs::F, deg::Degree, ax, x) where F +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 @@ -198,16 +198,22 @@ function getindex_return_type(::Type{BSplineInterpolation{T,N,TCoefs,IT,Axs}}, : end # This handles round-towards-the-middle for points on half-integer edges -roundbounds(x::Integer, bounds) = x -function roundbounds(x, bounds) +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) = x -function floorbounds(x, ax) +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))) diff --git a/src/b-splines/linear.jl b/src/b-splines/linear.jl index 4587ad4a..eb46b6fb 100644 --- a/src/b-splines/linear.jl +++ b/src/b-splines/linear.jl @@ -21,7 +21,7 @@ a piecewise linear function connecting each pair of neighboring data points. """ Linear -function positions(::Linear, ax, x) +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) diff --git a/src/deprecations.jl b/src/deprecations.jl index f751ddb4..658f1afb 100644 --- a/src/deprecations.jl +++ b/src/deprecations.jl @@ -1,5 +1,6 @@ # 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 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 b4ffb799..b6c63b7b 100644 --- a/src/gridded/gridded.jl +++ b/src/gridded/gridded.jl @@ -1,33 +1,22 @@ export Gridded -struct Gridded{D<:Degree} <: InterpolationType end -Gridded(::D) where {D<:Degree} = Gridded{D}() - -griddedtype(::Type{Gridded{D}}) where {D<:Degree} = D +struct Gridded{D<:Degree} <: InterpolationType + degree::D +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{AbstractVector}}} <: 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::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 @@ -36,39 +25,44 @@ function GriddedInterpolation(::Type{TWeights}, knots::NTuple{N,GridIndex}, A::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..580cc923 100644 --- a/src/gridded/indexing.jl +++ b/src/gridded/indexing.jl @@ -1,154 +1,79 @@ -using Base.Cartesian - -import Base.getindex - -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...) +# 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 +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=fmap(fs, deg, δx)) 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)) - - $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) + [coefs[i...] for i in Iterators.product(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/test/gridded/gridded.jl b/test/gridded/gridded.jl index 0a6826a2..05fd2452 100644 --- a/test/gridded/gridded.jl +++ b/test/gridded/gridded.jl @@ -2,73 +2,75 @@ module LinearTests using Interpolations, Test -for D in (Constant, Linear), G in (OnCell, OnGrid) +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) ## 1D a = rand(5) - knots = (collect(range(1, stop=length(a), length=length(a))),) + knots = (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] + @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(getindex(itp, knots...)) - @test itp[knots...] ≈ a + @inferred(itp(knots...)) + @test itp(knots...) ≈ a # compare scalar indexing and vector indexing - x = knots[1] .+ 0.1 - v = itp[x] + x = front(knots[1] .+ 0.1) + v = itp(x) for i = 1:length(x) - @test v[i] ≈ itp[x[i]] + @test v[i] ≈ itp(x[i]) end - # check the fallback vector indexing x = [2.3,2.2] # non-increasing order - v = itp[x] + v = itp(x) for i = 1:length(x) - @test v[i] ≈ itp[x[i]] + @test v[i] ≈ itp(x[i]) end # compare against BSpline - itpb = @inferred(interpolate(a, BSpline(D()), G())) + itpb = @inferred(interpolate(a, BSpline(D()))) for x in range(1.1, stop=4.9, length=101) - @test itp[x] ≈ itpb[x] + @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)))) + 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(getindex(itp, 2, 2)) - @inferred(getindex(itp, CartesianIndex((2,2)))) + @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] + @test itp(i,j) ≈ A[i,j] + @test itp(CartesianIndex((i,j))) ≈ A[i,j] end - @test itp[knots...] ≈ A - @inferred(getindex(itp, knots...)) + @test itp(knots...) ≈ A + @inferred(itp(knots...)) # compare scalar indexing and vector indexing - x, y = knots[1] .+ 0.1, knots[2] .+ 0.6 - v = itp[x,y] + 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]] + @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] + v = itp(x,y) for j = 1:length(y), i = 1:length(x) - @test v[i,j] ≈ itp[x[i],y[j]] + @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] + 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] + @test itp(4,1.2) ≈ A[2,6] end end diff --git a/test/gridded/mixed.jl b/test/gridded/mixed.jl index 4556c710..379c836d 100644 --- a/test/gridded/mixed.jl +++ b/test/gridded/mixed.jl @@ -3,21 +3,20 @@ module MixedTests using Interpolations, Test 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)))) +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(getindex(itp, 2, 2)) -@inferred(getindex(itp, CartesianIndex((2,2)))) +@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] + @test itp(i,j) ≈ A[i,j] + @test itp(CartesianIndex(i,j)) ≈ A[i,j] end -@test itp[knots...] ≈ A -@inferred(getindex(itp, knots...)) +@inferred(itp(knots...)) ≈ A 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 itp(4,1.2) ≈ A[2,6] @test_throws ErrorException interpolate(knots, A, (Gridded(Linear()),NoInterp())) end diff --git a/test/gridded/runtests.jl b/test/gridded/runtests.jl index 4491d388..fd6e179d 100644 --- a/test/gridded/runtests.jl +++ b/test/gridded/runtests.jl @@ -2,6 +2,6 @@ module GriddedTests include("gridded.jl") include("mixed.jl") -include("function-call-syntax.jl") +# include("function-call-syntax.jl") end From 63f1b795efd3ec045f5805816891fff7b93dafbf Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sun, 9 Sep 2018 14:51:00 -0500 Subject: [PATCH 22/29] Re-enable commented-out tests and fix issues --- src/Interpolations.jl | 63 ++++++++- src/b-splines/indexing.jl | 55 ++------ src/extrapolation/extrapolation.jl | 24 +++- src/extrapolation/filled.jl | 10 ++ src/gridded/gridded.jl | 6 + src/gridded/indexing.jl | 21 ++- src/io.jl | 80 ++++++------ src/scaling/scaling.jl | 41 ++---- src/utils.jl | 39 ++++++ test/b-splines/function-call-syntax.jl | 24 ---- test/core.jl | 15 +++ test/extrapolation/function-call-syntax.jl | 104 --------------- test/extrapolation/runtests.jl | 34 ++--- test/gradient.jl | 25 ++-- test/grid.jl | 54 -------- test/gridded/function-call-syntax.jl | 74 ----------- test/gridded/gridded.jl | 141 +++++++++++---------- test/gridded/mixed.jl | 33 +++-- test/gridded/runtests.jl | 1 - test/io.jl | 82 ++++++------ test/issues/runtests.jl | 121 +++++++++++++++++- test/readme-examples.jl | 12 +- test/runtests.jl | 8 +- test/scaling/function-call-syntax.jl | 117 ----------------- test/scaling/runtests.jl | 1 - 25 files changed, 519 insertions(+), 666 deletions(-) delete mode 100644 test/b-splines/function-call-syntax.jl create mode 100644 test/core.jl delete mode 100644 test/extrapolation/function-call-syntax.jl delete mode 100644 test/grid.jl delete mode 100644 test/gridded/function-call-syntax.jl delete mode 100644 test/scaling/function-call-syntax.jl diff --git a/src/Interpolations.jl b/src/Interpolations.jl index df345739..cd769b47 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -20,6 +20,7 @@ export Natural, InPlace, InPlaceQ, + Throw, LinearInterpolation, CubicSplineInterpolation @@ -199,7 +200,50 @@ 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) +end +# 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_getindex(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_getindex(A, i::Int, weights::NTuple{K,Number}, rest, I::Vararg{Int,M}) where {M,K} = + weights[1] * interp_getindex(A, rest, I..., i) + _interp_getindex(A, i+1, Base.tail(weights), rest, I...) +@inline _interp_getindex(A, i::Int, weights::Tuple{Number}, rest, I::Vararg{Int,M}) where M = + weights[1] * interp_getindex(A, rest, I..., i) +_interp_getindex(A, i::Int, weights::Tuple{}, rest, I::Vararg{Int,M}) where M = + error("exhausted weights, this should never happen") # helps inference + +# version for WeightedArbIndex +@inline _interp_getindex(A, indexes::NTuple{K,Int}, weights::NTuple{K,Number}, rest, I::Vararg{Int,M}) where {M,K} = + weights[1] * interp_getindex(A, rest, I..., indexes[1]) + _interp_getindex(A, Base.tail(indexes), Base.tail(weights), rest, I...) +@inline _interp_getindex(A, indexes::Tuple{Int}, weights::Tuple{Number}, rest, I::Vararg{Int,M}) where M = + weights[1] * interp_getindex(A, rest, I..., indexes[1]) +_interp_getindex(A, indexes::Tuple{}, weights::Tuple{}, rest, I::Vararg{Int,M}) where M = + error("exhausted weights and indexes, this should never happen") """ w = value_weights(degree, δx) @@ -268,7 +312,9 @@ Base.to_index(::AbstractInterpolation, x::Number) = x # itp(to_indices(itp, x)...) # end function gradient(itp::AbstractInterpolation, x::Vararg{UnexpandedIndexTypes}) - gradient(itp, to_indices(itp, x)...) + 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)...) @@ -283,12 +329,15 @@ end # @inline function (itp::AbstractInterpolation)(x::Vararg{ExpandedIndexTypes}) # itp.(Iterators.product(x...)) # end -function gradient(itp::AbstractInterpolation, x::Vararg{ExpandedIndexTypes}) - map(y->gradient(itp, y), Iterators.product(x...)) -end -function hessian(itp::AbstractInterpolation, x::Vararg{ExpandedIndexTypes}) - map(y->hessian(itp, y), 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 diff --git a/src/b-splines/indexing.jl b/src/b-splines/indexing.jl index 9df16b2b..b9cf7985 100644 --- a/src/b-splines/indexing.jl +++ b/src/b-splines/indexing.jl @@ -1,42 +1,3 @@ -### 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) -end - -# 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_getindex(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 - -# version for WeightedAdjIndex -_interp_getindex(A, i::Int, weights::NTuple{K,Number}, rest, I::Vararg{Int,M}) where {M,K} = - weights[1] * interp_getindex(A, rest, I..., i) + _interp_getindex(A, i+1, Base.tail(weights), rest, I...) -_interp_getindex(A, i::Int, weights::Tuple{Number}, rest, I::Vararg{Int,M}) where M = - weights[1] * interp_getindex(A, rest, I..., i) -_interp_getindex(A, i::Int, weights::Tuple{}, rest, I::Vararg{Int,M}) where M = - error("exhausted weights, this should never happen") # helps inference - -# version for WeightedArbIndex -_interp_getindex(A, indexes::NTuple{K,Int}, weights::NTuple{K,Number}, rest, I::Vararg{Int,M}) where {M,K} = - weights[1] * interp_getindex(A, rest, I..., indexes[1]) + _interp_getindex(A, Base.tail(indexes), Base.tail(weights), rest, I...) -_interp_getindex(A, indexes::Tuple{Int}, weights::Tuple{Number}, rest, I::Vararg{Int,M}) where M = - weights[1] * interp_getindex(A, rest, I..., indexes[1]) -_interp_getindex(A, indexes::Tuple{}, weights::Tuple{}, rest, I::Vararg{Int,M}) where M = - error("exhausted weights and indexes, this should never happen") - - ### Primary evaluation entry points (itp(x...), gradient(itp, x...), and hessian(itp, x...)) itpinfo(itp) = (tcollect(itpflag, itp), axes(itp)) @@ -52,6 +13,14 @@ end @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) @@ -71,12 +40,14 @@ end dest .= hessian(itp, x...) end -checkbounds(::Type{Bool}, itp::AbstractInterpolation, x::Vararg{Number,N}) where N = +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) = _checklubounds(tf & (ls[1] <= xs[1] <= us[1]), - Base.tail(ls), Base.tail(us), Base.tail(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 diff --git a/src/extrapolation/extrapolation.jl b/src/extrapolation/extrapolation.jl index d0c896b8..1adf2a01 100644 --- a/src/extrapolation/extrapolation.jl +++ b/src/extrapolation/extrapolation.jl @@ -39,7 +39,29 @@ count_interp_dims(::Type{<:Extrapolation{T,N,ITPT}}, n) where {T,N,ITPT} = count itp = parent(etp) eflag = etpflag(etp) xs = inbounds_position(eflag, bounds(itp), x, etp, x) - extrapolate_value(eflag, x, xs, Tuple(gradient(itp, xs...)), itp(xs...)) + 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 + +@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 checkbounds(::Bool, ::AbstractExtrapolation, I...) = true diff --git a/src/extrapolation/filled.jl b/src/extrapolation/filled.jl index 3686d7b0..a49dff46 100644 --- a/src/extrapolation/filled.jl +++ b/src/extrapolation/filled.jl @@ -33,6 +33,16 @@ end @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 expand_index_resid_etp(deg, fillvalue, (l, u), x, etp::FilledExtrapolation, xN) = (l <= x <= u || Base.throw_boundserror(etp, xN)) diff --git a/src/gridded/gridded.jl b/src/gridded/gridded.jl index b6c63b7b..df82246c 100644 --- a/src/gridded/gridded.jl +++ b/src/gridded/gridded.jl @@ -4,6 +4,12 @@ struct Gridded{D<:Degree} <: InterpolationType degree::D end +function Base.show(io::IO, g::Gridded) + print(io, "Gridded(") + show(io, degree(g)) + print(io, ')') +end + const GridIndex{T} = Union{AbstractVector{T}, Tuple} struct GriddedInterpolation{T,N,TCoefs,IT<:DimSpec{Gridded},K<:Tuple{Vararg{AbstractVector}}} <: AbstractInterpolation{T,N,IT} diff --git a/src/gridded/indexing.jl b/src/gridded/indexing.jl index 580cc923..ff8606d2 100644 --- a/src/gridded/indexing.jl +++ b/src/gridded/indexing.jl @@ -8,6 +8,15 @@ end itp(to_indices(itp, x)...) end +@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) weightedindex_parts(fs::F, itpflag::Gridded, ax, x) where F = @@ -45,15 +54,23 @@ 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=fmap(fs, deg, δx)) + (position=iclamp, coefs=rescale_gridded(fs, fmap(fs, deg, δx), u-l)) end +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 + @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) - [coefs[i...] for i in Iterators.product(wis...)] + ret = [coefs[i...] for i in Iterators.product(wis...)] + reshape(ret, shape(wis...)) end function dimension_wis(f::F, itps, knots, xs) where F diff --git a/src/io.jl b/src/io.jl index 164ff71e..da0bdffb 100644 --- a/src/io.jl +++ b/src/io.jl @@ -1,4 +1,4 @@ -function Base.showarg(io::IO, A::BSplineInterpolation{T,N,TW,ST,GT}, toplevel) 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(") Base.showarg(io, A.coefs, false) print(io, ", ") @@ -10,19 +10,19 @@ function Base.showarg(io::IO, A::BSplineInterpolation{T,N,TW,ST,GT}, toplevel) w end end -# 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, ", ") -# Base.showarg(io, A.coefs, false) -# print(io, ", ") -# _showtypeparam(io, ST) -# if toplevel -# print(io, ") with element type ",T) -# else -# print(io, ')') -# end -# end +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, ", ") + Base.showarg(io, A.coefs, false) + print(io, ", ") + show(io, itpflag(A)) + if toplevel + print(io, ") with element type ",T) + else + print(io, ')') + end +end _showknots(io, A) = Base.showarg(io, A, false) function _showknots(io, tup::NTuple{N,Any}) where N @@ -35,31 +35,31 @@ function _showknots(io, tup::NTuple{N,Any}) where N print(io, ')') end -# function Base.showarg(io::IO, A::ScaledInterpolation{T}, toplevel) where {T} -# print(io, "scale(") -# Base.showarg(io, A.itp, false) -# print(io, ", ", A.ranges, ')') -# if toplevel -# print(io, " with element type ",T) -# end -# end +function Base.showarg(io::IO, A::ScaledInterpolation{T}, toplevel) where {T} + print(io, "scale(") + Base.showarg(io, A.itp, false) + print(io, ", ", A.ranges, ')') + if toplevel + print(io, " with element type ",T) + end +end -# function Base.showarg(io::IO, A::Extrapolation{T,N,TI,IT,GT,ET}, toplevel) where {T,N,TI,IT,GT,ET} -# print(io, "extrapolate(") -# Base.showarg(io, A.itp, false) -# print(io, ", ") -# _showtypeparam(io, ET) -# print(io, ')') -# if toplevel -# print(io, " with element type ",T) -# end -# end +function Base.showarg(io::IO, A::Extrapolation{T,N,TI,IT,ET}, toplevel) where {T,N,TI,IT,ET} + print(io, "extrapolate(") + Base.showarg(io, A.itp, false) + print(io, ", ") + show(io, etpflag(A)) + print(io, ')') + if toplevel + print(io, " with element type ",T) + end +end -# function Base.showarg(io::IO, A::FilledExtrapolation{T,N,TI,IT,GT}, toplevel) where {T,N,TI,IT,GT} -# print(io, "extrapolate(") -# Base.showarg(io, A.itp, false) -# print(io, ", ", A.fillvalue, ')') -# if toplevel -# print(io, " with element type ",T) -# end -# end +function Base.showarg(io::IO, A::FilledExtrapolation{T,N,TI,IT}, toplevel) where {T,N,TI,IT} + print(io, "extrapolate(") + Base.showarg(io, A.itp, false) + print(io, ", ", A.fillvalue, ')') + if toplevel + print(io, " with element type ",T) + end +end diff --git a/src/scaling/scaling.jl b/src/scaling/scaling.jl index 42c4cb89..65c9acf9 100644 --- a/src/scaling/scaling.jl +++ b/src/scaling/scaling.jl @@ -35,6 +35,8 @@ check_range(::Any, ax, r) = length(ax) == length(r) || throw(ArgumentError("The size(sitp::ScaledInterpolation) = size(sitp.itp) axes(sitp::ScaledInterpolation) = axes(sitp.itp) +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 @@ -61,11 +63,18 @@ function (sitp::ScaledInterpolation{T,N})(xs::Vararg{Number,N}) where {T,N} sitp.itp(xl...) end @inline function (sitp::ScaledInterpolation)(x::Vararg{UnexpandedIndexTypes}) - sitp(to_indices(sitp, x)...) + 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))...) @@ -103,36 +112,6 @@ function rescale_gradient_components(flags, ranges, g) end rescale_gradient_components(flags, ::Tuple{}, ::Tuple{}) = () - -# # @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}, xs::Real...) where {T,N,ITPT,IT<:DimSpec{InterpolationType}<:DimSpec{GridType}} = -# gradient!(Array{T}(undef, count_interp_dims(IT,N)), sitp, xs...) -# gradient(sitp::ScaledInterpolation{T,N,ITPT,IT}, xs...) where {T,N,ITPT,IT<:DimSpec{InterpolationType}<: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 -# end - - rescale_gradient(r::StepRange, g) = g / r.step rescale_gradient(r::UnitRange, g) = g diff --git a/src/utils.jl b/src/utils.jl index 7f6ce00b..f0b025a6 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -48,6 +48,11 @@ substitute_ruled(default::Tuple{}, ruler::NTuple{N,Any}, subst) where N = () @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 @@ -55,3 +60,37 @@ 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/b-splines/function-call-syntax.jl b/test/b-splines/function-call-syntax.jl deleted file mode 100644 index 153d9b18..00000000 --- a/test/b-splines/function-call-syntax.jl +++ /dev/null @@ -1,24 +0,0 @@ -module ExtrapFunctionCallSyntax - -using Test, Interpolations, DualNumbers - -# 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/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 9e3257f6..00000000 --- a/test/extrapolation/function-call-syntax.jl +++ /dev/null @@ -1,104 +0,0 @@ -module ExtrapFunctionCallSyntax - -using 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/runtests.jl b/test/extrapolation/runtests.jl index 3295add1..e103c2f6 100644 --- a/test/extrapolation/runtests.jl +++ b/test/extrapolation/runtests.jl @@ -68,28 +68,28 @@ using Test @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 + # 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_broken (@inferred(extrapolate(interpolate([0,0],BSpline(Linear())),E))([1.2, 1.8, 3.1])) == [0,0,0] + @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()), OnGrid()); - # itps = scale(itp, 1:10, 1:4) - # itpe = extrapolate(itps, (Line(), Interpolations.Throw())) - # @test itpe(10.1, 1) ≈ 10.1 - # @test_throws BoundsError itpe(9.9, 0) + # 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") diff --git a/test/gradient.jl b/test/gradient.jl index c9911851..e91ec703 100644 --- a/test/gradient.jl +++ b/test/gradient.jl @@ -40,9 +40,9 @@ using Test, Interpolations, DualNumbers, LinearAlgebra # 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) + 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))) @@ -51,6 +51,7 @@ using Test, Interpolations, DualNumbers, LinearAlgebra 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 @@ -58,15 +59,15 @@ using Test, Interpolations, DualNumbers, LinearAlgebra 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),(Interpolations.gradient(itp_grid,x))[1],atol=abs(0.5 * g1(x))) - # @test ≈(g1(x),(Interpolations.gradient!(g,itp_grid,x))[1],atol=abs(0.5 * g1(x))) - # @test ≈(g1(x),g[1],atol=abs(0.5 * g1(x))) - # end + 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 ≈(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 Quadratic is OnCell in the domain, check gradients at grid points itp1 = interpolate(Float64[f1(x) for x in 1:nx-1], diff --git a/test/grid.jl b/test/grid.jl deleted file mode 100644 index 5160efce..00000000 --- a/test/grid.jl +++ /dev/null @@ -1,54 +0,0 @@ -module GridTests - -using Interpolations, 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 4d62b394..00000000 --- a/test/gridded/function-call-syntax.jl +++ /dev/null @@ -1,74 +0,0 @@ -module GriddedFunctionCallSyntax - -using Interpolations, Test - -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 05fd2452..19c74902 100644 --- a/test/gridded/gridded.jl +++ b/test/gridded/gridded.jl @@ -1,76 +1,79 @@ -module LinearTests - using Interpolations, Test -front(r::AbstractUnitRange) = first(r):last(r)-1 -front(r::AbstractRange) = range(first(r), step=step(r), length=length(r)-1) +@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) - ## 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 + 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 = (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 + 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 379c836d..789720f2 100644 --- a/test/gridded/mixed.jl +++ b/test/gridded/mixed.jl @@ -1,22 +1,21 @@ -module MixedTests - using Interpolations, Test -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 +@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(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 fd6e179d..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/io.jl b/test/io.jl index 0e196b51..6027c13e 100644 --- a/test/io.jl +++ b/test/io.jl @@ -26,45 +26,45 @@ using Test @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 + @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 31b17e88..2fe7ba5b 100644 --- a/test/issues/runtests.jl +++ b/test/issues/runtests.jl @@ -1,4 +1,4 @@ -using Interpolations, Test +using Interpolations, Test, ForwardDiff @testset "Issues" begin @testset "issue 34" begin @@ -11,4 +11,123 @@ using Interpolations, Test @test itp[i,j] ≈ A[i,j] end 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/readme-examples.jl b/test/readme-examples.jl index efafa774..872ae77e 100644 --- a/test/readme-examples.jl +++ b/test/readme-examples.jl @@ -30,7 +30,7 @@ using Interpolations, Test ## Scaled Bsplines 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) @test sitp(3.) ≈ log(3.) # exactly log(3.) @test sitp(3.5) ≈ log(3.5) atol=.1 # approximately log(3.5) @@ -45,10 +45,10 @@ using Interpolations, Test @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] + ## 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 92a7a6b4..bf49f225 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -11,6 +11,7 @@ using Interpolations @test isempty(setdiff(detect_ambiguities(Interpolations, Base, Core), ambs)) @testset "Interpolations" begin + include("core.jl") # b-spline interpolation tests include("b-splines/runtests.jl") @@ -26,15 +27,12 @@ using Interpolations # test hessian evaluation include("hessian.jl") - # # gridded interpolation tests - # include("gridded/runtests.jl") + # gridded interpolation tests + include("gridded/runtests.jl") # test interpolation with specific types include("typing.jl") - # Tests copied from Grid.jl's old test suite - # include("grid.jl") - include("issues/runtests.jl") include("io.jl") diff --git a/test/scaling/function-call-syntax.jl b/test/scaling/function-call-syntax.jl deleted file mode 100644 index dae55f0e..00000000 --- a/test/scaling/function-call-syntax.jl +++ /dev/null @@ -1,117 +0,0 @@ -module ScalingFunctionCallTests - -using Interpolations, Test - -# 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/runtests.jl b/test/scaling/runtests.jl index 253581a9..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") From f0be3f9bbe2920db6e7cc204395a041a8a80c091 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Tue, 11 Sep 2018 05:05:58 -0500 Subject: [PATCH 23/29] Fix inference problems by using a generated function --- src/Interpolations.jl | 82 ++++++++++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/src/Interpolations.jl b/src/Interpolations.jl index cd769b47..165df82b 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -214,37 +214,69 @@ Base.length(x::WeightedIndex) = 1 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) + interp_getindex(A, I, ntuple(d->0, Val(N))...) end -# 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_getindex(A, indexes(wi), weights(wi), Base.tail(J), I...) +# 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") + +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 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_getindex(A, i::Int, weights::NTuple{K,Number}, rest, I::Vararg{Int,M}) where {M,K} = - weights[1] * interp_getindex(A, rest, I..., i) + _interp_getindex(A, i+1, Base.tail(weights), rest, I...) -@inline _interp_getindex(A, i::Int, weights::Tuple{Number}, rest, I::Vararg{Int,M}) where M = - weights[1] * interp_getindex(A, rest, I..., i) -_interp_getindex(A, i::Int, weights::Tuple{}, rest, I::Vararg{Int,M}) where M = - error("exhausted weights, this should never happen") # helps inference - -# version for WeightedArbIndex -@inline _interp_getindex(A, indexes::NTuple{K,Int}, weights::NTuple{K,Number}, rest, I::Vararg{Int,M}) where {M,K} = - weights[1] * interp_getindex(A, rest, I..., indexes[1]) + _interp_getindex(A, Base.tail(indexes), Base.tail(weights), rest, I...) -@inline _interp_getindex(A, indexes::Tuple{Int}, weights::Tuple{Number}, rest, I::Vararg{Int,M}) where M = - weights[1] * interp_getindex(A, rest, I..., indexes[1]) -_interp_getindex(A, indexes::Tuple{}, weights::Tuple{}, rest, I::Vararg{Int,M}) where M = - error("exhausted weights and indexes, this should never happen") - """ w = value_weights(degree, δx) From 678b4a23ac0b13d951d1605ea146b16a1e770f68 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Tue, 11 Sep 2018 07:40:48 -0500 Subject: [PATCH 24/29] Update the benchmarks Also add an analysis & plotting function --- perf/benchmarks.jl | 44 +++++++++++++++------------- perf/compare_benchmarks.jl | 60 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 perf/compare_benchmarks.jl diff --git a/perf/benchmarks.jl b/perf/benchmarks.jl index 0cba81e9..74cde788 100644 --- a/perf/benchmarks.jl +++ b/perf/benchmarks.jl @@ -15,19 +15,19 @@ 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) = replace(str, "Interpolations."=>"") benchstr(::Type{T}) where {T<:Interpolations.GridType} = strip_prefix(string(T)) benchstr(::Type{Constant}) = "Constant()" @@ -49,16 +49,14 @@ 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) @@ -66,8 +64,8 @@ for A in (collect(Float64, 1:3), Ac = copy(A) idstr = string(ndims(A), "d_", benchstr(Quadratic{BC}), '_', benchstr(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) @@ -76,8 +74,8 @@ for A in (collect(Float64, 1:3), Ac = copy(A) idstr = string(ndims(A), "d_", benchstr(Quadratic{BC}), '_', benchstr(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) @@ -88,8 +86,8 @@ for A in (collect(Float64, 1:3), Ac = copy(A) idstr = string(ndims(A), "d_", benchstr(Cubic{BC}), '_', benchstr(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 From ededaabc2afcaf2ae0f7589223a879fe163ed06a Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Tue, 11 Sep 2018 07:41:14 -0500 Subject: [PATCH 25/29] Add some inline annotations for performance --- src/Interpolations.jl | 4 ++-- src/b-splines/indexing.jl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Interpolations.jl b/src/Interpolations.jl index 165df82b..3bae69ed 100644 --- a/src/Interpolations.jl +++ b/src/Interpolations.jl @@ -246,7 +246,7 @@ end # interp_getindex1(A, indexes::Tuple{}, weights::Tuple{}, rest, I::Vararg{Int,M}) where M = # error("exhausted the weights and indexes, this should never happen") -interp_getindex(A::AbstractArray{T,N}, J::Tuple{Int,Vararg{Any,K}}, I::Vararg{Int,N}) where {T,N,K} = +@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)) @@ -274,7 +274,7 @@ end $ex end end -interp_getindex(A::AbstractArray{T,N}, ::Tuple{}, I::Vararg{Int,N}) where {T,N} = # termination +@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 """ diff --git a/src/b-splines/indexing.jl b/src/b-splines/indexing.jl index b9cf7985..6348281c 100644 --- a/src/b-splines/indexing.jl +++ b/src/b-splines/indexing.jl @@ -59,7 +59,7 @@ end end -function weightedindexes(fs::F, itpflags::NTuple{N,Flag}, knots::NTuple{N,AbstractVector}, xs::NTuple{N,Number}) where {F,N} +@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 @@ -68,7 +68,7 @@ weightedindexes(i::Vararg{Int,N}) where N = i # the all-NoInterp case const PositionCoefs{P,C} = NamedTuple{(:position,:coefs),Tuple{P,C}} const ValueParts{P,W} = PositionCoefs{P,Tuple{W}} -weightedindexes(parts::Vararg{Union{Int,ValueParts},N}) where N = maybe_weightedindex.(positions.(parts), valuecoefs.(parts)) +@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) From c1124c1e8b0109070afeef978c0fdfd04178d32b Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Tue, 11 Sep 2018 09:21:21 -0500 Subject: [PATCH 26/29] Update benchmark naming with the new API This makes it backwards-incompatible but will be more consistent for the future. --- perf/benchmarks.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/perf/benchmarks.jl b/perf/benchmarks.jl index 74cde788..71e61814 100644 --- a/perf/benchmarks.jl +++ b/perf/benchmarks.jl @@ -27,15 +27,15 @@ function sumvalues_indices(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" @@ -62,7 +62,7 @@ for A in (collect(Float64, 1:3), 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())))) @@ -72,7 +72,7 @@ for A in (collect(Float64, 1:3), 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())))) @@ -84,7 +84,7 @@ 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())))) From 328d0c1553c5d578de64f8f6aff2f4c276b54eff Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Tue, 11 Sep 2018 10:08:13 -0500 Subject: [PATCH 27/29] Update the README and other docs to the new API --- README.md | 115 +- doc/Interpolations.jl.ipynb | 5974 +---------------------------------- doc/Math.md | 4 +- doc/Plotting examples.ipynb | 4 +- doc/devdocs.md | 123 - 5 files changed, 143 insertions(+), 6077 deletions(-) delete mode 100644 doc/devdocs.md 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/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", - "\n", - " \n", - " x\n", - " \n", - " \n", - " 0.0\n", - " 2.5\n", - " 5.0\n", - " 7.5\n", - " 10.0\n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - " -1.0\n", - " -0.5\n", - " 0.0\n", - " 0.5\n", - " 1.0\n", - " \n", - " \n", - " y\n", - " \n", - "\n", - "\n", - " \n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "\n" - ], - "text/html": [ - "\n", - "\n", - "\n", - " \n", - " x\n", - " \n", - " \n", - " -12.5\n", - " -10.0\n", - " -7.5\n", - " -5.0\n", - " -2.5\n", - " 0.0\n", - " 2.5\n", - " 5.0\n", - " 7.5\n", - " 10.0\n", - " 12.5\n", - " 15.0\n", - " 17.5\n", - " 20.0\n", - " 22.5\n", - " -10.0\n", - " -9.5\n", - " -9.0\n", - " -8.5\n", - " -8.0\n", - " -7.5\n", - " -7.0\n", - " -6.5\n", - " -6.0\n", - " -5.5\n", - " -5.0\n", - " -4.5\n", - " -4.0\n", - " -3.5\n", - " -3.0\n", - " -2.5\n", - " -2.0\n", - " -1.5\n", - " -1.0\n", - " -0.5\n", - " 0.0\n", - " 0.5\n", - " 1.0\n", - " 1.5\n", - " 2.0\n", - " 2.5\n", - " 3.0\n", - " 3.5\n", - " 4.0\n", - " 4.5\n", - " 5.0\n", - " 5.5\n", - " 6.0\n", - " 6.5\n", - " 7.0\n", - " 7.5\n", - " 8.0\n", - " 8.5\n", - " 9.0\n", - " 9.5\n", - " 10.0\n", - " 10.5\n", - " 11.0\n", - " 11.5\n", - " 12.0\n", - " 12.5\n", - " 13.0\n", - " 13.5\n", - " 14.0\n", - " 14.5\n", - " 15.0\n", - " 15.5\n", - " 16.0\n", - " 16.5\n", - " 17.0\n", - " 17.5\n", - " 18.0\n", - " 18.5\n", - " 19.0\n", - " 19.5\n", - " 20.0\n", - " -10\n", - " 0\n", - " 10\n", - " 20\n", - " -10\n", - " -9\n", - " -8\n", - " -7\n", - " -6\n", - " -5\n", - " -4\n", - " -3\n", - " -2\n", - " -1\n", - " 0\n", - " 1\n", - " 2\n", - " 3\n", - " 4\n", - " 5\n", - " 6\n", - " 7\n", - " 8\n", - " 9\n", - " 10\n", - " 11\n", - " 12\n", - " 13\n", - " 14\n", - " 15\n", - " 16\n", - " 17\n", - " 18\n", - " 19\n", - " 20\n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - " -3.5\n", - " -3.0\n", - " -2.5\n", - " -2.0\n", - " -1.5\n", - " -1.0\n", - " -0.5\n", - " 0.0\n", - " 0.5\n", - " 1.0\n", - " 1.5\n", - " 2.0\n", - " 2.5\n", - " 3.0\n", - " 3.5\n", - " -3.0\n", - " -2.9\n", - " -2.8\n", - " -2.7\n", - " -2.6\n", - " -2.5\n", - " -2.4\n", - " -2.3\n", - " -2.2\n", - " -2.1\n", - " -2.0\n", - " -1.9\n", - " -1.8\n", - " -1.7\n", - " -1.6\n", - " -1.5\n", - " -1.4\n", - " -1.3\n", - " -1.2\n", - " -1.1\n", - " -1.0\n", - " -0.9\n", - " -0.8\n", - " -0.7\n", - " -0.6\n", - " -0.5\n", - " -0.4\n", - " -0.3\n", - " -0.2\n", - " -0.1\n", - " 0.0\n", - " 0.1\n", - " 0.2\n", - " 0.3\n", - " 0.4\n", - " 0.5\n", - " 0.6\n", - " 0.7\n", - " 0.8\n", - " 0.9\n", - " 1.0\n", - " 1.1\n", - " 1.2\n", - " 1.3\n", - " 1.4\n", - " 1.5\n", - " 1.6\n", - " 1.7\n", - " 1.8\n", - " 1.9\n", - " 2.0\n", - " 2.1\n", - " 2.2\n", - " 2.3\n", - " 2.4\n", - " 2.5\n", - " 2.6\n", - " 2.7\n", - " 2.8\n", - " 2.9\n", - " 3.0\n", - " -4\n", - " -2\n", - " 0\n", - " 2\n", - " 4\n", - " -3.0\n", - " -2.8\n", - " -2.6\n", - " -2.4\n", - " -2.2\n", - " -2.0\n", - " -1.8\n", - " -1.6\n", - " -1.4\n", - " -1.2\n", - " -1.0\n", - " -0.8\n", - " -0.6\n", - " -0.4\n", - " -0.2\n", - " 0.0\n", - " 0.2\n", - " 0.4\n", - " 0.6\n", - " 0.8\n", - " 1.0\n", - " 1.2\n", - " 1.4\n", - " 1.6\n", - " 1.8\n", - " 2.0\n", - " 2.2\n", - " 2.4\n", - " 2.6\n", - " 2.8\n", - " 3.0\n", - " \n", - " \n", - " y\n", - " \n", - "\n", - "\n", - " \n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\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", - "\n", - " \n", - " x\n", - " \n", - " \n", - " 0\n", - " 1\n", - " 2\n", - " 3\n", - " 4\n", - " 5\n", - " \n", - " \n", - " \n", - " -10\n", - " 20\n", - " -30\n", - " 30\n", - " 10\n", - " 0\n", - " -20\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " Color\n", - " \n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - " 0\n", - " 2\n", - " 4\n", - " 6\n", - " 8\n", - " \n", - " \n", - " y\n", - " \n", - "\n", - "\n", - " \n", - " \n", - "\n", - "\n", - "\n" - ], - "text/html": [ - "\n", - "\n", - "\n", - " \n", - " x\n", - " \n", - " \n", - " -6\n", - " -5\n", - " -4\n", - " -3\n", - " -2\n", - " -1\n", - " 0\n", - " 1\n", - " 2\n", - " 3\n", - " 4\n", - " 5\n", - " 6\n", - " 7\n", - " 8\n", - " 9\n", - " 10\n", - " 11\n", - " -5.0\n", - " -4.8\n", - " -4.6\n", - " -4.4\n", - " -4.2\n", - " -4.0\n", - " -3.8\n", - " -3.6\n", - " -3.4\n", - " -3.2\n", - " -3.0\n", - " -2.8\n", - " -2.6\n", - " -2.4\n", - " -2.2\n", - " -2.0\n", - " -1.8\n", - " -1.6\n", - " -1.4\n", - " -1.2\n", - " -1.0\n", - " -0.8\n", - " -0.6\n", - " -0.4\n", - " -0.2\n", - " 0.0\n", - " 0.2\n", - " 0.4\n", - " 0.6\n", - " 0.8\n", - " 1.0\n", - " 1.2\n", - " 1.4\n", - " 1.6\n", - " 1.8\n", - " 2.0\n", - " 2.2\n", - " 2.4\n", - " 2.6\n", - " 2.8\n", - " 3.0\n", - " 3.2\n", - " 3.4\n", - " 3.6\n", - " 3.8\n", - " 4.0\n", - " 4.2\n", - " 4.4\n", - " 4.6\n", - " 4.8\n", - " 5.0\n", - " 5.2\n", - " 5.4\n", - " 5.6\n", - " 5.8\n", - " 6.0\n", - " 6.2\n", - " 6.4\n", - " 6.6\n", - " 6.8\n", - " 7.0\n", - " 7.2\n", - " 7.4\n", - " 7.6\n", - " 7.8\n", - " 8.0\n", - " 8.2\n", - " 8.4\n", - " 8.6\n", - " 8.8\n", - " 9.0\n", - " 9.2\n", - " 9.4\n", - " 9.6\n", - " 9.8\n", - " 10.0\n", - " -5\n", - " 0\n", - " 5\n", - " 10\n", - " -5.0\n", - " -4.5\n", - " -4.0\n", - " -3.5\n", - " -3.0\n", - " -2.5\n", - " -2.0\n", - " -1.5\n", - " -1.0\n", - " -0.5\n", - " 0.0\n", - " 0.5\n", - " 1.0\n", - " 1.5\n", - " 2.0\n", - " 2.5\n", - " 3.0\n", - " 3.5\n", - " 4.0\n", - " 4.5\n", - " 5.0\n", - " 5.5\n", - " 6.0\n", - " 6.5\n", - " 7.0\n", - " 7.5\n", - " 8.0\n", - " 8.5\n", - " 9.0\n", - " 9.5\n", - " 10.0\n", - " \n", - " \n", - " \n", - " -10\n", - " 20\n", - " -30\n", - " 30\n", - " 10\n", - " 0\n", - " -20\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " Color\n", - " \n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - " -10\n", - " -8\n", - " -6\n", - " -4\n", - " -2\n", - " 0\n", - " 2\n", - " 4\n", - " 6\n", - " 8\n", - " 10\n", - " 12\n", - " 14\n", - " 16\n", - " 18\n", - " -8.0\n", - " -7.5\n", - " -7.0\n", - " -6.5\n", - " -6.0\n", - " -5.5\n", - " -5.0\n", - " -4.5\n", - " -4.0\n", - " -3.5\n", - " -3.0\n", - " -2.5\n", - " -2.0\n", - " -1.5\n", - " -1.0\n", - " -0.5\n", - " 0.0\n", - " 0.5\n", - " 1.0\n", - " 1.5\n", - " 2.0\n", - " 2.5\n", - " 3.0\n", - " 3.5\n", - " 4.0\n", - " 4.5\n", - " 5.0\n", - " 5.5\n", - " 6.0\n", - " 6.5\n", - " 7.0\n", - " 7.5\n", - " 8.0\n", - " 8.5\n", - " 9.0\n", - " 9.5\n", - " 10.0\n", - " 10.5\n", - " 11.0\n", - " 11.5\n", - " 12.0\n", - " 12.5\n", - " 13.0\n", - " 13.5\n", - " 14.0\n", - " 14.5\n", - " 15.0\n", - " 15.5\n", - " 16.0\n", - " -10\n", - " 0\n", - " 10\n", - " 20\n", - " -8.0\n", - " -7.5\n", - " -7.0\n", - " -6.5\n", - " -6.0\n", - " -5.5\n", - " -5.0\n", - " -4.5\n", - " -4.0\n", - " -3.5\n", - " -3.0\n", - " -2.5\n", - " -2.0\n", - " -1.5\n", - " -1.0\n", - " -0.5\n", - " 0.0\n", - " 0.5\n", - " 1.0\n", - " 1.5\n", - " 2.0\n", - " 2.5\n", - " 3.0\n", - " 3.5\n", - " 4.0\n", - " 4.5\n", - " 5.0\n", - " 5.5\n", - " 6.0\n", - " 6.5\n", - " 7.0\n", - " 7.5\n", - " 8.0\n", - " 8.5\n", - " 9.0\n", - " 9.5\n", - " 10.0\n", - " 10.5\n", - " 11.0\n", - " 11.5\n", - " 12.0\n", - " 12.5\n", - " 13.0\n", - " 13.5\n", - " 14.0\n", - " 14.5\n", - " 15.0\n", - " 15.5\n", - " 16.0\n", - " \n", - " \n", - " y\n", - " \n", - "\n", - "\n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "Plot(...)" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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", - "image/svg+xml": [ - "\n", - "\n", - "\n", - " \n", - " x\n", - " \n", - " \n", - " 0\n", - " 1\n", - " 2\n", - " 3\n", - " 4\n", - " 5\n", - " \n", - " \n", - " \n", - " -10\n", - " 20\n", - " -30\n", - " 30\n", - " 10\n", - " 0\n", - " -20\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " Color\n", - " \n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - " 0\n", - " 2\n", - " 4\n", - " 6\n", - " 8\n", - " \n", - " \n", - " y\n", - " \n", - "\n", - "\n", - " \n", - " \n", - "\n", - "\n", - "\n" - ], - "text/html": [ - "\n", - "\n", - "\n", - " \n", - " x\n", - " \n", - " \n", - " -6\n", - " -5\n", - " -4\n", - " -3\n", - " -2\n", - " -1\n", - " 0\n", - " 1\n", - " 2\n", - " 3\n", - " 4\n", - " 5\n", - " 6\n", - " 7\n", - " 8\n", - " 9\n", - " 10\n", - " 11\n", - " -5.0\n", - " -4.8\n", - " -4.6\n", - " -4.4\n", - " -4.2\n", - " -4.0\n", - " -3.8\n", - " -3.6\n", - " -3.4\n", - " -3.2\n", - " -3.0\n", - " -2.8\n", - " -2.6\n", - " -2.4\n", - " -2.2\n", - " -2.0\n", - " -1.8\n", - " -1.6\n", - " -1.4\n", - " -1.2\n", - " -1.0\n", - " -0.8\n", - " -0.6\n", - " -0.4\n", - " -0.2\n", - " 0.0\n", - " 0.2\n", - " 0.4\n", - " 0.6\n", - " 0.8\n", - " 1.0\n", - " 1.2\n", - " 1.4\n", - " 1.6\n", - " 1.8\n", - " 2.0\n", - " 2.2\n", - " 2.4\n", - " 2.6\n", - " 2.8\n", - " 3.0\n", - " 3.2\n", - " 3.4\n", - " 3.6\n", - " 3.8\n", - " 4.0\n", - " 4.2\n", - " 4.4\n", - " 4.6\n", - " 4.8\n", - " 5.0\n", - " 5.2\n", - " 5.4\n", - " 5.6\n", - " 5.8\n", - " 6.0\n", - " 6.2\n", - " 6.4\n", - " 6.6\n", - " 6.8\n", - " 7.0\n", - " 7.2\n", - " 7.4\n", - " 7.6\n", - " 7.8\n", - " 8.0\n", - " 8.2\n", - " 8.4\n", - " 8.6\n", - " 8.8\n", - " 9.0\n", - " 9.2\n", - " 9.4\n", - " 9.6\n", - " 9.8\n", - " 10.0\n", - " -5\n", - " 0\n", - " 5\n", - " 10\n", - " -5.0\n", - " -4.5\n", - " -4.0\n", - " -3.5\n", - " -3.0\n", - " -2.5\n", - " -2.0\n", - " -1.5\n", - " -1.0\n", - " -0.5\n", - " 0.0\n", - " 0.5\n", - " 1.0\n", - " 1.5\n", - " 2.0\n", - " 2.5\n", - " 3.0\n", - " 3.5\n", - " 4.0\n", - " 4.5\n", - " 5.0\n", - " 5.5\n", - " 6.0\n", - " 6.5\n", - " 7.0\n", - " 7.5\n", - " 8.0\n", - " 8.5\n", - " 9.0\n", - " 9.5\n", - " 10.0\n", - " \n", - " \n", - " \n", - " -10\n", - " 20\n", - " -30\n", - " 30\n", - " 10\n", - " 0\n", - " -20\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " Color\n", - " \n", - " \n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - " -10\n", - " -8\n", - " -6\n", - " -4\n", - " -2\n", - " 0\n", - " 2\n", - " 4\n", - " 6\n", - " 8\n", - " 10\n", - " 12\n", - " 14\n", - " 16\n", - " 18\n", - " -8.0\n", - " -7.5\n", - " -7.0\n", - " -6.5\n", - " -6.0\n", - " -5.5\n", - " -5.0\n", - " -4.5\n", - " -4.0\n", - " -3.5\n", - " -3.0\n", - " -2.5\n", - " -2.0\n", - " -1.5\n", - " -1.0\n", - " -0.5\n", - " 0.0\n", - " 0.5\n", - " 1.0\n", - " 1.5\n", - " 2.0\n", - " 2.5\n", - " 3.0\n", - " 3.5\n", - " 4.0\n", - " 4.5\n", - " 5.0\n", - " 5.5\n", - " 6.0\n", - " 6.5\n", - " 7.0\n", - " 7.5\n", - " 8.0\n", - " 8.5\n", - " 9.0\n", - " 9.5\n", - " 10.0\n", - " 10.5\n", - " 11.0\n", - " 11.5\n", - " 12.0\n", - " 12.5\n", - " 13.0\n", - " 13.5\n", - " 14.0\n", - " 14.5\n", - " 15.0\n", - " 15.5\n", - " 16.0\n", - " -10\n", - " 0\n", - " 10\n", - " 20\n", - " -8.0\n", - " -7.5\n", - " -7.0\n", - " -6.5\n", - " -6.0\n", - " -5.5\n", - " -5.0\n", - " -4.5\n", - " -4.0\n", - " -3.5\n", - " -3.0\n", - " -2.5\n", - " -2.0\n", - " -1.5\n", - " -1.0\n", - " -0.5\n", - " 0.0\n", - " 0.5\n", - " 1.0\n", - " 1.5\n", - " 2.0\n", - " 2.5\n", - " 3.0\n", - " 3.5\n", - " 4.0\n", - " 4.5\n", - " 5.0\n", - " 5.5\n", - " 6.0\n", - " 6.5\n", - " 7.0\n", - " 7.5\n", - " 8.0\n", - " 8.5\n", - " 9.0\n", - " 9.5\n", - " 10.0\n", - " 10.5\n", - " 11.0\n", - " 11.5\n", - " 12.0\n", - " 12.5\n", - " 13.0\n", - " 13.5\n", - " 14.0\n", - " 14.5\n", - " 15.0\n", - " 15.5\n", - " 16.0\n", - " \n", - " \n", - " y\n", - " \n", - "\n", - "\n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\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. From 144dc00f10d67383c8a4aa3065807c52feb37871 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Tue, 11 Sep 2018 10:08:33 -0500 Subject: [PATCH 28/29] Fix convenience constructors and visual tests --- src/convenience-constructors.jl | 12 ++++++------ test/convenience-constructors.jl | 20 ++++++++++---------- test/runtests.jl | 2 +- test/visual.jl | 22 +++++++++++----------- 4 files changed, 28 insertions(+), 28 deletions(-) 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/test/convenience-constructors.jl b/test/convenience-constructors.jl index 84fc850d..70a30107 100644 --- a/test/convenience-constructors.jl +++ b/test/convenience-constructors.jl @@ -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/runtests.jl b/test/runtests.jl index bf49f225..9e861742 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -36,7 +36,7 @@ using Interpolations include("issues/runtests.jl") include("io.jl") - # include("convenience-constructors.jl") + include("convenience-constructors.jl") include("readme-examples.jl") 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 From 7089d9d8c0b690368173f8a1d00a079a03bc8164 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Tue, 18 Sep 2018 12:05:29 -0500 Subject: [PATCH 29/29] Ensure Travis stays awake during long test runs --- test/runtests.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 9e861742..c31533d9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,11 +10,14 @@ ambs = detect_ambiguities(StaticArrays, WoodburyMatrices, Base, Core) using Interpolations @test isempty(setdiff(detect_ambiguities(Interpolations, Base, Core), ambs)) +const isci = get(ENV, "CI", "") in ("true", "True") + @testset "Interpolations" begin include("core.jl") # b-spline interpolation tests include("b-splines/runtests.jl") + isci && println("finished b-spline") include("nointerp.jl") # extrapolation tests include("extrapolation/runtests.jl") @@ -24,8 +27,10 @@ using Interpolations # test gradient evaluation include("gradient.jl") + isci && println("finished gradient") # test hessian evaluation include("hessian.jl") + isci && println("finished hessian") # gridded interpolation tests include("gridded/runtests.jl")