diff --git a/Project.toml b/Project.toml index de4f4830..4cca0536 100644 --- a/Project.toml +++ b/Project.toml @@ -12,6 +12,7 @@ NumericalIntegration = "e7bfaba1-d571-5449-8927-abc22e82249b" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" PCHIPInterpolation = "afe20452-48d1-4729-9a8b-50fb251f06cd" RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" ResumableFunctions = "c5292f4c-5179-55e1-98c5-05642aab7184" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" ToeplitzMatrices = "c751599d-da0a-543b-9d20-d0a503d91d24" @@ -24,6 +25,7 @@ OrdinaryDiffEq = "6.31" NumericalIntegration = "0.3.3" PCHIPInterpolation = "0.1.7" RecipesBase = "1" +RecursiveArrayTools = "2" ResumableFunctions = "0.6.3" StaticArrays = "0.12, 1" ToeplitzMatrices = "0.7, 0.8" diff --git a/docs/src/inverse.md b/docs/src/inverse.md index 07300120..b5d14cfb 100644 --- a/docs/src/inverse.md +++ b/docs/src/inverse.md @@ -5,6 +5,7 @@ CurrentModule = Fronts # Inverse problems ```@docs -inverse -sorptivity(::AbstractVector, ::AbstractVector) +InverseProblem +diffusivity(::InverseProblem) +sorptivity(::InverseProblem) ``` diff --git a/docs/src/solution.md b/docs/src/solution.md index 3b727d06..2c4d26a0 100644 --- a/docs/src/solution.md +++ b/docs/src/solution.md @@ -11,6 +11,5 @@ Solution flux d_dϕ rb -sorptivity(::Solution) sorptivity(::Solution, _) ``` diff --git a/src/Fronts.jl b/src/Fronts.jl index 94fa970c..8e36dfc7 100644 --- a/src/Fronts.jl +++ b/src/Fronts.jl @@ -23,6 +23,7 @@ using LinearAlgebra: Diagonal using ArgCheck: @argcheck using StaticArrays: @SVector, @SMatrix +using RecursiveArrayTools: ArrayPartition using PCHIPInterpolation: Interpolator, integrate import NumericalIntegration using RecipesBase diff --git a/src/ParamEstim.jl b/src/ParamEstim.jl index de7d5cf2..a71e8cb6 100644 --- a/src/ParamEstim.jl +++ b/src/ParamEstim.jl @@ -1,11 +1,13 @@ module ParamEstim import ..Fronts -using ..Fronts: Problem, Solution, solve, SolvingError, sorptivity +using ..Fronts: InverseProblem, Problem, Solution, solve, SolvingError, sorptivity using LsqFit: curve_fit """ + RSSCostFunction{fit_D0}(func, prob::InverseProblem; catch_errors, D0tol, ϕi_hint]) + RSSCostFunction{fit_D0}(func, ϕ, data[, weights; catch_errors, D0tol, ϕi_hint]) Residual sum of squares cost function for parameter estimation. @@ -22,8 +24,9 @@ retrieved by calling the `candidate` function. `Fronts.Problem`. If func returns a `Problem`, it is solved with `trysolve`. `func` is also allowed to return `nothing` to signal that no solution could be found for the parameter values, which will imply an infinite cost (see also the `catch_errors` keyword argument). +- `prob::InverseProblem`: inverse problem. See [`InverseProblem`](@ref). - `ϕ`: vector of values of the Boltzmann variable. See [`Fronts.ϕ`](@ref). -- `data`: data to fit. Must be a vector of the same length as `ϕ`. +- `θ`: data to fit. Must be a vector of the same length as `ϕ`. - `weights`: optional weights for the data. If given, must be a vector of the same length as `data`. # Keyword arguments @@ -52,24 +55,28 @@ If you need to know more than just the cost, call the `candidate` function inste See also: [`candidate`](@ref), [`Fronts.Solution`](@ref), [`Fronts.Problem`](@ref), [`trysolve`](@ref) """ -struct RSSCostFunction{fit_D0, _Tfunc, _Tϕ, _Tdata, _Tweights, _Tcatch_errors, _Tϕi_hint, _TD0tol} +struct RSSCostFunction{fit_D0, _Tfunc, _Tprob, _Tcatch_errors, _TD0tol, _Tϕi_hint, _Tsorptivity} _func::_Tfunc - _ϕ::_Tϕ - _data::_Tdata - _weights::_Tweights + _prob::_Tprob _catch_errors::_Tcatch_errors _D0tol::_TD0tol _ϕi_hint::_Tϕi_hint + _sorptivity::_Tsorptivity - function RSSCostFunction{true}(func, ϕ, data, weights=nothing; ϕi_hint=nothing, D0tol=1e-3, catch_errors=(SolvingError,)) - new{true,typeof(func),typeof(ϕ),typeof(data),typeof(weights),typeof(catch_errors),typeof(ϕi_hint),typeof(D0tol)}(func, ϕ, data, weights, catch_errors, D0tol, ϕi_hint) + function RSSCostFunction{true}(func, prob::InverseProblem; catch_errors=(SolvingError,), D0tol=1e-3, ϕi_hint=nothing) + S = isnothing(ϕi_hint) ? sorptivity(prob) : nothing + new{true,typeof(func),typeof(prob),typeof(catch_errors),typeof(D0tol),typeof(ϕi_hint),typeof(S)}(func, prob, catch_errors, D0tol, ϕi_hint, S) end - function RSSCostFunction{false}(func, ϕ, data, weights=nothing; catch_errors=(SolvingError,)) - new{false,typeof(func),typeof(ϕ),typeof(data),typeof(weights),typeof(catch_errors),Nothing,Nothing}(func, ϕ, data, weights, catch_errors, nothing) + function RSSCostFunction{false}(func, prob::InverseProblem; catch_errors=(SolvingError,)) + new{false,typeof(func),typeof(prob),typeof(catch_errors),Nothing,Nothing,Nothing}(func, prob, catch_errors, nothing) end end +function RSSCostFunction{fit_D0}(func, ϕ, θ, weights=nothing; kwargs...) where {fit_D0} + return RSSCostFunction{fit_D0}(func, InverseProblem(ϕ, θ, weights); kwargs...) +end + (cf::RSSCostFunction)(arg) = candidate(cf, arg).cost """ @@ -149,10 +156,10 @@ candidate(::RSSCostFunction{true}, ::Nothing) = _Candidate(nothing, NaN, Inf) candidate(::RSSCostFunction{false}, ::Nothing) = _Candidate(nothing, 1, Inf) function candidate(cf::RSSCostFunction{false}, sol::Solution) - if !isnothing(cf._weights) - return _Candidate(sol, 1, sum(cf._weights.*(sol.(cf._ϕ) .- cf._data).^2)) + if !isnothing(cf._prob._weights) + return _Candidate(sol, 1, sum(cf._prob._weights.*(sol.(cf._prob._ϕ) .- cf._prob._θ).^2)) else - return _Candidate(sol, 1, sum((sol.(cf._ϕ) .- cf._data).^2)) + return _Candidate(sol, 1, sum((sol.(cf._prob._ϕ) .- cf._prob._θ).^2)) end end @@ -162,13 +169,13 @@ function candidate(cf::RSSCostFunction{true}, sol::Solution) if !isnothing(cf._ϕi_hint) D0_hint = (cf._ϕi_hint/sol.ϕi)^2 else - D0_hint = (sorptivity(cf._ϕ, cf._data, i=sol.i, b=sol.b)/sorptivity(sol))^2 + D0_hint = (cf._sorptivity/sorptivity(sol))^2 end scaling = curve_fit(scaled!, - cf._ϕ, - cf._data, - (!isnothing(cf._weights) ? (cf._weights,) : ())..., + cf._prob._ϕ, + cf._prob._θ, + (!isnothing(cf._prob._weights) ? (cf._prob._weights,) : ())..., [D0_hint], inplace=true, lower=[0.0], diff --git a/src/inverse.jl b/src/inverse.jl index e968baeb..601ccd6b 100644 --- a/src/inverse.jl +++ b/src/inverse.jl @@ -1,4 +1,42 @@ """ + InverseProblem(ϕ, θ[, weights; i, b, ϕb]) + +Problem type for inverse functions and parameter estimation with experimental data. + +# Arguments +- `ϕ::AbstractVector`: values of the Boltzmann variable. See [`ϕ`](@ref). +- `θ::AbstractVector`: observed solution values at each point in `ϕ`. +- `weights`: optional weights for the data. + +# Keyword arguments +- `i`: initial value, if known. +- `b`: boundary value, if known. +- `ϕb=0`: value of `ϕ` at the boundary. + +# See also +[`diffusivity`](@ref), [`sorptivity`](@ref), `Fronts.ParamEstim` +""" +struct InverseProblem{_Tϕ,_Tθ,_Tweights,_Ti,_Tb,_Tϕb} + _ϕ::_Tϕ + _θ::_Tθ + _weights::_Tweights + _i::_Ti + _b::_Tb + _ϕb::_Tϕb + function InverseProblem(ϕ::AbstractVector, θ::AbstractVector, weights=nothing; i=nothing, b=nothing, ϕb=zero(eltype(ϕ))) + @argcheck length(ϕ) ≥ 2 + @argcheck all(ϕ1 ≤ ϕ2 for (ϕ1, ϕ2) in zip(ϕ[begin:end-1], ϕ[begin+1:end])) "ϕ must be monotonically increasing" + @argcheck length(ϕ) == length(θ) DimensionMismatch + !isnothing(weights) && @argcheck length(weights) == length(ϕ) DimensionMismatch + @argcheck zero(ϕb) ≤ ϕb ≤ ϕ[begin] + + new{typeof(ϕ),typeof(θ),typeof(weights),typeof(i),typeof(b),typeof(ϕb)}(ϕ, θ, weights, i, b, ϕb) + end +end + +""" + inverse(prob::InverseProblem) -> Function + inverse(ϕ, θ) -> Function Extract a diffusivity function `D` from a solution to a semi-infinite one-dimensional nonlinear diffusion problem, @@ -9,6 +47,7 @@ Interpolates the given solution with a PCHIP monotonic spline and uses the Bruce Due to the method used for interpolation, `D` will be continuous but will have discontinuous derivatives. # Arguments +- `prob::InverseProblem`: inverse problem. See [`InverseProblem`](@ref). - `ϕ::AbstractVector`: values of the Boltzmann variable. See [`ϕ`](@ref). - `θ::AbstractVector`: solution values at each point in `ϕ`. @@ -19,9 +58,12 @@ Capillarity, 2023, vol. 6, no. 2, p. 31-40. BRUCE, R. R.; KLUTE, A. The measurement of soil moisture diffusivity. Soil Science Society of America Journal, 1956, vol. 20, no. 4, p. 458-462. """ -function inverse(ϕ::AbstractVector, θ::AbstractVector) - @argcheck length(ϕ) ≥ 2 - @argcheck length(ϕ) == length(θ) DimensionMismatch +function inverse(prob::InverseProblem) + @argcheck isnothing(prob._weights) "not implemented for weighted data" + + ϕ = !isnothing(prob._b) ? ArrayPartition(prob._ϕb, prob._ϕ) : prob._ϕ + θ = !isnothing(prob._b) ? ArrayPartition(prob._b, prob._θ) : prob._θ + i = !isnothing(prob._i) ? prob._i : prob._θ[end] indices = sortperm(θ) ϕ = ϕ[indices] @@ -30,20 +72,23 @@ function inverse(ϕ::AbstractVector, θ::AbstractVector) indices = unique(i -> θ[i], eachindex(θ)) ϕ = ϕ[indices] θ = θ[indices] - - θi = θ[argmax(ϕ)] + ϕ = Interpolator(θ, ϕ) - let ϕ=ϕ, θi=θi + let ϕ=ϕ, i=i function D(θ) dϕ_dθ = derivative(ϕ, θ) - ∫ϕdθ = integrate(ϕ, θi, θ) + ∫ϕdθ = integrate(ϕ, i, θ) return -(dϕ_dθ*∫ϕdθ)/2 end end end +inverse(ϕ::AbstractVector, θ::AbstractVector) = inverse(InverseProblem(ϕ, θ)) + """ + sorptivity(::InverseProblem) + sorptivity(ϕ, θ) Calculate the sorptivity of a solution to a semi-infinite one-dimensional nonlinear diffusion problem, @@ -52,6 +97,7 @@ where the solution is given as a set of discrete points. Uses numerical integration. # Arguments +- `prob::InverseProblem`: inverse problem. See [`InverseProblem`](@ref). - `ϕ::AbstractVector`: values of the Boltzmann variable. See [`ϕ`](@ref). - `θ::AbstractVector`: solution values at each point in `ϕ`. @@ -64,17 +110,14 @@ Uses numerical integration. PHILIP, J. R. The theory of infiltration: 4. Sorptivity and algebraic infiltration equations. Soil Science, 1957, vol. 83, no. 5, p. 345-357. """ -function sorptivity(ϕ::AbstractVector, θ::AbstractVector; i=nothing, b=nothing, ϕb=0) - @argcheck length(ϕ) ≥ 2 - @argcheck length(ϕ) == length(θ) DimensionMismatch - @argcheck zero(ϕb) ≤ ϕb ≤ ϕ[begin] +function sorptivity(prob::InverseProblem) + @argcheck isnothing(prob._weights) "not implemented for weighted data" - if isnothing(i) - i = θ[end] - end - - ϕ = [ϕb; ϕ] - θ = [!isnothing(b) ? b : θ[begin]; θ] + ϕ = ArrayPartition(prob._ϕb, prob._ϕ) + θ = ArrayPartition(!isnothing(prob._b) ? prob._b : prob._θ[begin], prob._θ) + i = !isnothing(prob._i) ? prob._i : prob._θ[end] return NumericalIntegration.integrate(ϕ, θ .- i) end + +sorptivity(ϕ::AbstractVector, θ::AbstractVector) = sorptivity(InverseProblem(ϕ, θ))