diff --git a/README.md b/README.md index df100c0d..dbf4df79 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ corresponding to `(u,t)` pairs. - `ConstantInterpolation(u,t)` - A piecewise constant interpolation. + - `SmoothedConstantInterpolation(u,t)` - An integral preserving continuously differentiable approximation of constant interpolation. - `LinearInterpolation(u,t)` - A linear interpolation. - `QuadraticInterpolation(u,t)` - A quadratic interpolation. - `LagrangeInterpolation(u,t,n)` - A Lagrange interpolation of order `n`. diff --git a/docs/src/index.md b/docs/src/index.md index 1b19d50f..5ce66a6d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -18,6 +18,7 @@ corresponding to `(u,t)` pairs. - `ConstantInterpolation(u,t)` - A piecewise constant interpolation. + - `SmoothedConstantInterpolation(u,t)` - An integral preserving continuously differentiable approximation of constant interpolation. - `LinearInterpolation(u,t)` - A linear interpolation. - `QuadraticInterpolation(u,t)` - A quadratic interpolation. - `LagrangeInterpolation(u,t,n)` - A Lagrange interpolation of order `n`. diff --git a/docs/src/manual.md b/docs/src/manual.md index 3818a9e3..e599fad9 100644 --- a/docs/src/manual.md +++ b/docs/src/manual.md @@ -6,6 +6,7 @@ QuadraticInterpolation LagrangeInterpolation AkimaInterpolation ConstantInterpolation +SmoothedConstantInterpolation QuadraticSpline CubicSpline BSplineInterpolation diff --git a/docs/src/methods.md b/docs/src/methods.md index 648131eb..9a7b54ff 100644 --- a/docs/src/methods.md +++ b/docs/src/methods.md @@ -77,6 +77,24 @@ A = ConstantInterpolation(u, t, dir = :right) plot(A) ``` +## Smoothed Constant Interpolation + +This function is much like the constant interpolation above, but the transition +between consecutive values is smoothed out so that the function is continuously +differentiable. The smoothing is done in such a way that the integral of this function +is never much off from the same integral of constant interpolation without smoothing (because of the symmetry of the smoothing sections). +The maximum smoothing distance in the `t` direction from the data points can be set +with the keyword argument `d_max`. + +```@example tutorial +A = ConstantInterpolation(u, t) +plot(A) +A = SmoothedConstantInterpolation(u, t; d_max = 10) +plot!(A) +``` + +Note that `u[end]` is ignored. + ## Quadratic Spline This is the quadratic spline. It is a continuously differentiable interpolation diff --git a/src/DataInterpolations.jl b/src/DataInterpolations.jl index 393f1160..1da0f240 100644 --- a/src/DataInterpolations.jl +++ b/src/DataInterpolations.jl @@ -86,9 +86,10 @@ function Base.showerror(io::IO, e::IntegralNotInvertibleError) end export LinearInterpolation, QuadraticInterpolation, LagrangeInterpolation, - AkimaInterpolation, ConstantInterpolation, QuadraticSpline, CubicSpline, - BSplineInterpolation, BSplineApprox, CubicHermiteSpline, PCHIPInterpolation, - QuinticHermiteSpline, LinearInterpolationIntInv, ConstantInterpolationIntInv + AkimaInterpolation, ConstantInterpolation, SmoothedConstantInterpolation, + QuadraticSpline, CubicSpline, BSplineInterpolation, BSplineApprox, + CubicHermiteSpline, PCHIPInterpolation, QuinticHermiteSpline, + LinearInterpolationIntInv, ConstantInterpolationIntInv # added for RegularizationSmooth, JJS 11/27/21 ### Regularization data smoothing and interpolation diff --git a/src/derivatives.jl b/src/derivatives.jl index 7d6dea24..b4922c9d 100644 --- a/src/derivatives.jl +++ b/src/derivatives.jl @@ -19,6 +19,24 @@ function _derivative(A::LinearInterpolation, t::Number, iguess) slope end +function _derivative(A::SmoothedConstantInterpolation{<:AbstractVector}, t::Number, iguess) + idx = get_idx(A, t, iguess) + d_lower, d_upper, c_lower, c_upper = get_parameters(A, idx) + + # Fix extrapolation behavior as constant for now + if t <= first(A.t) || t >= last(A.t) + return zero(c_upper / oneunit(t)) + end + + if (t - A.t[idx]) < d_lower + -2c_lower * ((t - A.t[idx]) / d_lower - 1) / d_lower + elseif (A.t[idx + 1] - t) < d_upper + 2c_upper * (1 - (A.t[idx + 1] - t) / d_upper) / d_upper + else + zero(c_upper / oneunit(t)) + end +end + function _derivative(A::QuadraticInterpolation, t::Number, iguess) idx = get_idx(A, t, iguess) Δt = t - A.t[idx] diff --git a/src/integrals.jl b/src/integrals.jl index 2bdb6408..4f1487e6 100644 --- a/src/integrals.jl +++ b/src/integrals.jl @@ -56,6 +56,38 @@ function _integral( end end +function _integral(A::SmoothedConstantInterpolation{<:AbstractVector}, + idx::Number, t1::Number, t2::Number) + d_lower, d_upper, c_lower, c_upper = get_parameters(A, idx) + + bound_lower = A.t[idx] + d_lower + bound_upper = A.t[idx + 1] - d_upper + + out = A.u[idx] * (t2 - t1) + + # Fix extrapolation behavior as constant for now + if t1 <= first(A.t) + t1 = first(A.t) + elseif t2 >= last(A.t) + t2 = last(A.t) + end + + if t1 < bound_lower + t2_ = min(t2, bound_lower) + out -= c_lower * d_lower * + (((t2_ - A.t[idx]) / d_lower - 1)^3 - ((t1 - A.t[idx]) / d_lower - 1)^3) / 3 + end + + if t2 > bound_upper + t1_ = max(t1, bound_upper) + out += c_upper * d_upper * + ((1 - (A.t[idx + 1] - t2) / d_upper)^3 - + (1 - (A.t[idx + 1] - t1_) / d_upper)^3) / 3 + end + + out +end + function _integral(A::QuadraticInterpolation{<:AbstractVector{<:Number}}, idx::Number, t1::Number, t2::Number) α, β = get_parameters(A, idx) diff --git a/src/interpolation_caches.jl b/src/interpolation_caches.jl index 850ac74b..66b11aa2 100644 --- a/src/interpolation_caches.jl +++ b/src/interpolation_caches.jl @@ -285,6 +285,61 @@ function ConstantInterpolation( ConstantInterpolation(u, t, I, dir, extrapolate, cache_parameters, assume_linear_t) end +""" + SmoothedConstantInterpolation(u, t; d_max = Inf, extrapolate = false, + cache_parameters = false, assume_linear_t = 1e-2) + +It is a method for interpolating constantly with forward fill, with smoothing around the +value transitions to make the curve continuously differentiable while the integral never +drifts far from the integral of constant interpolation. In this interpolation type u[end] is ignored. + +## Arguments + + - `u`: data points. + - `t`: time points. + +## Keyword Arguments + + - `d_max`: the maximum distance in `t` from the data points the smoothing is allowed to reach. + - `extrapolate`: boolean value to allow extrapolation. Defaults to `false`. + - `cache_parameters`: precompute parameters at initialization for faster interpolation computations. Note: if activated, `u` and `t` should not be modified. Defaults to `false`. + - `assume_linear_t`: boolean value to specify a faster index lookup behavior for + evenly-distributed abscissae. Alternatively, a numerical threshold may be specified + for a test based on the normalized standard deviation of the difference with respect + to the straight line (see [`looks_linear`](@ref)). Defaults to 1e-2. +""" +struct SmoothedConstantInterpolation{uType, tType, IType, dType, cType, dmaxType, T, N} <: + AbstractInterpolation{T, N} + u::uType + t::tType + I::IType + p::SmoothedConstantParameterCache{dType, cType} + d_max::dmaxType + extrapolate::Bool + iguesser::Guesser{tType} + cache_parameters::Bool + linear_lookup::Bool + function SmoothedConstantInterpolation( + u, t, I, p, d_max, extrapolate, cache_parameters, assume_linear_t) + linear_lookup = seems_linear(assume_linear_t, t) + N = get_output_dim(u) + new{typeof(u), typeof(t), typeof(I), typeof(p.d), + typeof(p.c), typeof(d_max), eltype(u), N}( + u, t, I, p, d_max, extrapolate, Guesser(t), cache_parameters, linear_lookup) + end +end + +function SmoothedConstantInterpolation(u, t; d_max = Inf, extrapolate = false, + cache_parameters = false, assume_linear_t = 1e-2) + u, t = munge_data(u, t) + p = SmoothedConstantParameterCache(u, t, cache_parameters, d_max) + A = SmoothedConstantInterpolation( + u, t, nothing, p, d_max, extrapolate, cache_parameters, assume_linear_t) + I = cumulative_integral(A, cache_parameters) + SmoothedConstantInterpolation( + u, t, I, p, d_max, extrapolate, cache_parameters, assume_linear_t) +end + """ QuadraticSpline(u, t; extrapolate = false, cache_parameters = false) diff --git a/src/interpolation_methods.jl b/src/interpolation_methods.jl index 54129ae6..1870ea28 100644 --- a/src/interpolation_methods.jl +++ b/src/interpolation_methods.jl @@ -116,7 +116,7 @@ function _interpolate(A::AkimaInterpolation{<:AbstractVector}, t::Number, iguess @evalpoly wj A.u[idx] A.b[idx] A.c[idx] A.d[idx] end -# ConstantInterpolation Interpolation +# Constant Interpolation function _interpolate(A::ConstantInterpolation{<:AbstractVector}, t::Number, iguess) if A.dir === :left # :left means that value to the left is used for interpolation @@ -139,6 +139,29 @@ function _interpolate(A::ConstantInterpolation{<:AbstractMatrix}, t::Number, igu A.u[:, idx] end +# Smoothed constant Interpolation +function _interpolate(A::SmoothedConstantInterpolation{<:AbstractVector}, t::Number, iguess) + idx = get_idx(A, t, iguess) + d_lower, d_upper, c_lower, c_upper = get_parameters(A, idx) + + # Fix extrapolation behavior as constant for now + if t <= first(A.t) + return first(A.u) + elseif t >= last(A.t) + return A.u[end - 1] + end + + out = A.u[idx] + + if (t - A.t[idx]) < d_lower + out -= c_lower * ((t - A.t[idx]) / d_lower - 1)^2 + elseif (A.t[idx + 1] - t) < d_upper + out += c_upper * (1 - (A.t[idx + 1] - t) / d_upper)^2 + end + + out +end + # QuadraticSpline Interpolation function _interpolate(A::QuadraticSpline{<:AbstractVector}, t::Number, iguess) idx = get_idx(A, t, iguess) diff --git a/src/interpolation_utils.jl b/src/interpolation_utils.jl index 2356942c..a3b719a7 100644 --- a/src/interpolation_utils.jl +++ b/src/interpolation_utils.jl @@ -207,6 +207,21 @@ function get_parameters(A::LinearInterpolation, idx) end end +function get_parameters(A::SmoothedConstantInterpolation, idx) + if A.cache_parameters + d_lower = A.p.d[idx] + d_upper = A.p.d[idx + 1] + c_lower = A.p.c[idx] + c_upper = A.p.c[idx + 1] + d_lower, d_upper, c_lower, c_upper + else + d_lower, c_lower = smoothed_linear_interpolation_parameters(A.u, A.t, A.d_max, idx) + d_upper, c_upper = smoothed_linear_interpolation_parameters( + A.u, A.t, A.d_max, idx + 1) + d_lower, d_upper, c_lower, c_upper + end +end + function get_parameters(A::QuadraticInterpolation, idx) if A.cache_parameters A.p.α[idx], A.p.β[idx] diff --git a/src/parameter_caches.jl b/src/parameter_caches.jl index 737f736b..912f497e 100644 --- a/src/parameter_caches.jl +++ b/src/parameter_caches.jl @@ -32,6 +32,31 @@ function linear_interpolation_parameters(u::AbstractArray{T, N}, t, idx) where { return slope end +struct SmoothedConstantParameterCache{dType, cType} + d::dType + c::cType +end + +function SmoothedConstantParameterCache(u, t, cache_parameters, d_max) + if cache_parameters + parameters = smoothed_linear_interpolation_parameters.( + Ref(u), Ref(t), d_max, eachindex(t)) + d, c = collect.(eachrow(stack(collect.(parameters)))) + SmoothedConstantParameterCache(d, c) + else + SmoothedConstantParameterCache(eltype(t)[], eltype(u)[]) + end +end + +function smoothed_linear_interpolation_parameters(u, t, d_max, idx) + # TODO: Add support for making periodic extrapolation smooth + if isone(idx) || (idx == length(t)) + zero(one(eltype(t))) / 2, zero(one(eltype(u)) / 2) + else + min(t[idx] - t[idx - 1], t[idx + 1] - t[idx], 2d_max) / 2, (u[idx] - u[idx - 1]) / 2 + end +end + struct QuadraticParameterCache{pType} α::pType β::pType diff --git a/test/derivative_tests.jl b/test/derivative_tests.jl index 37595a4f..15731fd2 100644 --- a/test/derivative_tests.jl +++ b/test/derivative_tests.jl @@ -27,7 +27,7 @@ function test_derivatives(method; args = [], kwargs = [], name::String) # Interpolation time points for _t in t[2:(end - 1)] if func isa BSplineInterpolation || func isa BSplineApprox || - func isa CubicHermiteSpline + func isa CubicHermiteSpline || func isa SmoothedConstantInterpolation fdiff = forward_fdm(5, 1; geom = true)(func, _t) fdiff2 = forward_fdm(5, 1; geom = true)(t -> derivative(func, t), _t) else @@ -140,6 +140,13 @@ end @test all(derivative.(Ref(A), t2 .+ 0.1) .== 0.0) end +@testset "SmoothedConstantInterpolation" begin + u = [5.5, 2.7, 5.1, 3.0] + t = [2.5, 5.6, 6.3, 8.9] + test_derivatives(SmoothedConstantInterpolation; args = [u, t], + name = "Smoothed constant interpolation") +end + @testset "Quadratic Spline" begin u = [0.0, 1.0, 3.0] t = [-1.0, 0.0, 1.0] diff --git a/test/integral_tests.jl b/test/integral_tests.jl index 3d59a6c0..126a2496 100644 --- a/test/integral_tests.jl +++ b/test/integral_tests.jl @@ -65,6 +65,20 @@ end name = "Linear Interpolation (Vector) with random points") end +@testset "SmoothedConstantInterpolation" begin + u = [1.0, 4.0, 9.0, 16.0] + t = [1.0, 2.0, 3.0, 4.0] + test_integral( + SmoothedConstantInterpolation; args = [u, t], name = "Smoothed constant interpolation") + + A_constant = ConstantInterpolation(u, t) + I_ref = DataInterpolations.integral(A_constant, first(t), last(t)) + I_smoothed = [DataInterpolations.integral( + SmoothedConstantInterpolation(u, t; d_max), first(t), last(t)) + for d_max in 0.0:0.1:1.0] + @test all(I_smoothed .≈ I_ref) +end + @testset "QuadraticInterpolation" begin u = [1.0, 4.0, 9.0, 16.0] t = [1.0, 2.0, 3.0, 4.0] diff --git a/test/interpolation_tests.jl b/test/interpolation_tests.jl index 65b48113..1e8bfabb 100644 --- a/test/interpolation_tests.jl +++ b/test/interpolation_tests.jl @@ -501,6 +501,19 @@ end @test A(-Inf) == first(u) end +@testset "Smoothed constant Interpolation" begin + test_interpolation_type(SmoothedConstantInterpolation) + u = [0.0, 2.0, 1.0, 3.0] + t = [1.2, 2.5, 5.7, 8.7] + d_max = 0.5 + A = SmoothedConstantInterpolation(u, t; d_max) + test_cached_index(A) + + @test A(1.9) == u[1] + @test A(3.1) == u[2] + @test A(2.5) ≈ (u[1] + u[2]) / 2 +end + @testset "QuadraticSpline Interpolation" begin test_interpolation_type(QuadraticSpline) diff --git a/test/parameter_tests.jl b/test/parameter_tests.jl index 3afe2a17..3030ec21 100644 --- a/test/parameter_tests.jl +++ b/test/parameter_tests.jl @@ -7,6 +7,14 @@ using DataInterpolations @test A.p.slope ≈ [4.0, -2.0, 1.0, 0.0] end +@testset "Smoothed constant Interpolation" begin + u = [1.0, 5.0, 3.0, 4.0, 4.0] + t = collect(1:5) + A = SmoothedConstantInterpolation(u, t; cache_parameters = true) + A.p.d ≈ [0.0, 0.5, 0.5, 0.5, 0.0] + A.p.c ≈ [0.0, 2.0, -1.0, 0.5, 0.0] +end + @testset "Quadratic Interpolation" begin u = [1.0, 5.0, 3.0, 4.0, 4.0] t = collect(1:5)