Skip to content

Commit

Permalink
Add InverseProblem type
Browse files Browse the repository at this point in the history
  • Loading branch information
gerlero committed Oct 17, 2023
1 parent 8923dfb commit e5b86ee
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 37 deletions.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions docs/src/inverse.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CurrentModule = Fronts
# Inverse problems

```@docs
inverse
sorptivity(::AbstractVector, ::AbstractVector)
InverseProblem
diffusivity(::InverseProblem)
sorptivity(::InverseProblem)
```
1 change: 0 additions & 1 deletion docs/src/solution.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,5 @@ Solution
flux
d_dϕ
rb
sorptivity(::Solution)
sorptivity(::Solution, _)
```
1 change: 1 addition & 0 deletions src/Fronts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 24 additions & 17 deletions src/ParamEstim.jl
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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

"""
Expand Down Expand Up @@ -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))

Check warning on line 160 in src/ParamEstim.jl

View check run for this annotation

Codecov / codecov/patch

src/ParamEstim.jl#L160

Added line #L160 was not covered by tests
else
return _Candidate(sol, 1, sum((sol.(cf._ϕ) .- cf._data).^2))
return _Candidate(sol, 1, sum((sol.(cf._prob._ϕ) .- cf._prob.).^2))
end
end

Expand All @@ -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],
Expand Down
77 changes: 60 additions & 17 deletions src/inverse.jl
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 `ϕ`.
Expand All @@ -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]
Expand All @@ -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,
Expand All @@ -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 `ϕ`.
Expand All @@ -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(ϕ, θ))

0 comments on commit e5b86ee

Please sign in to comment.