Skip to content

Commit 19f6283

Browse files
Merge pull request #340 from SciML/qqy/fit_parameters
Add unknown parameters estimation
2 parents 0ea842a + 57cfca1 commit 19f6283

12 files changed

Lines changed: 383 additions & 72 deletions

File tree

docs/pages.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
pages = ["index.md",
44
"Getting Started with BVP solving in Julia" => "tutorials/getting_started.md",
5-
"Tutorials" => Any["tutorials/continuation.md",
6-
"tutorials/solve_nlls_bvp.md", "tutorials/extremum.md"],
5+
"Tutorials" => Any["tutorials/continuation.md", "tutorials/solve_nlls_bvp.md",
6+
"tutorials/unknown_parameters.md", "tutorials/extremum.md"],
77
"Basics" => Any["basics/bvp_problem.md", "basics/bvp_functions.md",
88
"basics/solve.md", "basics/autodiff.md", "basics/error_control.md"],
99
"Solver Summaries and Recommendations" => Any[
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Estimate Unknown Parameters in BVP
2+
3+
When there are unknown parameters in boundary value problems, we can estimate the unknown parameters by solving the BVP, it is quite useful in practical applications in dynamical optimizations and inverse problems. This approach allows us to incorporate both the governing differential equations and boundary conditions to infer parameters that may not be directly measurable.
4+
5+
Let's walk through this functionality with an intuitive example. In the following tutorial, we use the [Mathieu equation](https://en.wikipedia.org/wiki/Mathieu_wavelet) which is a second-order differential equation:
6+
7+
```math
8+
y''+(\lambda-2q\cos(2x))y=0
9+
```
10+
11+
where $\lambda$ is the unknown parameter we wish to estimate, `q` is a known real-valued parameter, with boundary conditions when `q=5`:
12+
13+
```math
14+
y'(0)=0,\ y'(\pi)=0
15+
```
16+
17+
The second-order BVP can be transformed into a first-order system of BVP:
18+
19+
```math
20+
\begin{cases}
21+
y_1'=y_2\\
22+
y_2'=-(\lambda-2q\cos(2x))y_1
23+
\end{cases}
24+
```
25+
26+
with boundary conditions of
27+
28+
```math
29+
y_2(0)=0,\ y_2(\pi)=0
30+
```
31+
32+
It is worthnoting that in this system, while we have two differetial equations, it isn't enough to estimate the unknown parameters and gurantee a unique numerical solution with only two given boundary conditions. While under the hood, the parameters are estimated simultanously with the numerical solution, it makes the boundary value problem an underconstrained BVP if the number of constraints are equal to the number of states, which may result in more than one solution. So we should provide additional constraint $y(0)=1$ from the original equation to make sure unique numerical solution and the estimated parameters are we actually wanted.
33+
34+
With BoundaryValueDiffEq.jl, it's easy to solve boundary value problems with unknown parameters, we can just specify `fit_parameters=true` when constructing the BVP and provide the guess of the unknown parameters in `prob.p`, for example, to estimate the unknown parameters in the above BVP system:
35+
36+
```@example unknown
37+
using BoundaryValueDiffEq, Plots
38+
tspan = (0.0, pi)
39+
function f!(du, u, p, t)
40+
du[1] = u[2]
41+
du[2] = -(p[1] - 10 * cos(2 * t)) * u[1]
42+
end
43+
function bca!(res, u, p)
44+
res[1] = u[2]
45+
res[2] = u[1] - 1.0
46+
end
47+
function bcb!(res, u, p)
48+
res[1] = u[2]
49+
end
50+
guess(p, t) = [cos(4t); -4sin(4t)]
51+
bvp = TwoPointBVProblem(f!, (bca!, bcb!), guess, tspan, [15.0],
52+
bcresid_prototype = (zeros(2), zeros(1)), fit_parameters = true)
53+
sol = solve(bvp, MIRK4(), dt = 0.05)
54+
plot(sol)
55+
```
56+
57+
after solving the boundary value problem, the estimated unknown parameters can be accessed with
58+
59+
```@example unknown
60+
sol.prob.p
61+
```

lib/BoundaryValueDiffEqCore/src/utils.jl

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -262,38 +262,57 @@ function __extract_problem_details(prob, u0::AbstractVectorOfArray; kwargs...)
262262
_u0 = first(u0.u)
263263
return Val(true), eltype(_u0), length(_u0), (length(u0.u) - 1), _u0
264264
end
265-
function __extract_problem_details(
266-
prob, u0::AbstractArray; dt = 0.0, check_positive_dt::Bool = false)
265+
function __extract_problem_details(prob, u0::AbstractArray; dt = 0.0,
266+
check_positive_dt::Bool = false, fit_parameters::Bool = false)
267267
# Problem does not have Initial Guess
268268
check_positive_dt && dt 0 && throw(ArgumentError("dt must be positive"))
269269
t₀, t₁ = prob.tspan
270+
if fit_parameters
271+
prob.p isa SciMLBase.NullParameters &&
272+
throw(ArgumentError("`fit_parameters` is true but `prob.p` is not set."))
273+
new_u = vcat(u0, prob.p)
274+
return Val(false), eltype(new_u), length(new_u), Int(cld(t₁ - t₀, dt)), new_u
275+
end
270276
return Val(false), eltype(u0), length(u0), Int(cld(t₁ - t₀, dt)), prob.u0
271277
end
272-
function __extract_problem_details(
273-
prob, f::F; dt = 0.0, check_positive_dt::Bool = false) where {F <: Function}
278+
function __extract_problem_details(prob, f::F; dt = 0.0, check_positive_dt::Bool = false,
279+
fit_parameters::Bool = false) where {F <: Function}
274280
# Problem passes in a initial guess function
275281
check_positive_dt && dt 0 && throw(ArgumentError("dt must be positive"))
276-
u0 = __initial_guess(f, prob.p, prob.tspan[1])
282+
283+
u0 = __initial_guess(f, prob.p, prob.tspan[1]; fit_parameters = fit_parameters)
277284
t₀, t₁ = prob.tspan
278285
return Val(true), eltype(u0), length(u0), Int(cld(t₁ - t₀, dt)), u0
279286
end
280287

281-
function __extract_problem_details(
282-
prob, u0::SciMLBase.ODESolution; dt = 0.0, check_positive_dt::Bool = false)
288+
function __extract_problem_details(prob, u0::SciMLBase.ODESolution; dt = 0.0,
289+
check_positive_dt::Bool = false, fit_parameters::Bool = false)
283290
# Problem passes in a initial guess function
284291
_u0 = first(u0.u)
285292
_t = u0.t
293+
if fit_parameters
294+
prob.p isa SciMLBase.NullParameters &&
295+
throw(ArgumentError("`fit_parameters` is true but `prob.p` is not set."))
296+
new_u = vcat(_u0, prob.p)
297+
return Val(false), eltype(new_u), length(new_u), Int(cld(t₁ - t₀, dt)), new_u
298+
end
286299
return Val(true), eltype(_u0), length(_u0), (length(_t) - 1), _u0
287300
end
288301

289-
function __initial_guess(f::F, p::P, t::T) where {F, P, T}
302+
function __initial_guess(f::F, p::P, t::T; fit_parameters = false) where {F, P, T}
290303
if hasmethod(f, Tuple{P, T})
304+
p isa SciMLBase.NullParameters &&
305+
throw(ArgumentError("`fit_parameters` is true but `prob.p` is not set."))
306+
fit_parameters && return vcat(f(p, t), p)
291307
return f(p, t)
292308
elseif hasmethod(f, Tuple{T})
293309
Base.depwarn("initial guess function must take 2 inputs `(p, t)` instead of just \
294310
`t`. The single argument version has been deprecated and will be \
295311
removed in the next major release of SciMLBase.",
296312
:__initial_guess)
313+
p isa SciMLBase.NullParameters &&
314+
throw(ArgumentError("`fit_parameters` is true but `prob.p` is not set."))
315+
fit_parameters && return vcat(f(t), p)
297316
return f(t)
298317
else
299318
throw(ArgumentError("`initial_guess` must be a function of the form `f(p, t)`"))
@@ -535,7 +554,7 @@ end
535554
return VectorOfArray([vec(__initial_guess(u₀, p, t)) for t in mesh])
536555
end
537556
@inline function __initial_guess_on_mesh(
538-
prob::SecondOrderBVProblem, u₀::AbstractArray, Nig, p, alias_u0::Bool)
557+
prob::SecondOrderBVProblem, u₀::AbstractArray, Nig, p)
539558
return VectorOfArray([copy(vec(u₀)) for _ in 1:(2 * (Nig + 1))])
540559
end
541560

lib/BoundaryValueDiffEqFIRK/src/firk.jl

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
@concrete struct FIRKCacheNested{iip, T, diffcache} <: AbstractBoundaryValueDiffEqCache
1+
@concrete struct FIRKCacheNested{iip, T, diffcache, fit_parameters} <:
2+
AbstractBoundaryValueDiffEqCache
23
order::Int # The order of FIRK method
34
stage::Int # The state of FIRK method
45
M::Int # The number of equations
@@ -31,7 +32,8 @@ end
3132

3233
Base.eltype(::FIRKCacheNested{iip, T}) where {iip, T} = T
3334

34-
@concrete struct FIRKCacheExpand{iip, T, diffcache} <: AbstractBoundaryValueDiffEqCache
35+
@concrete struct FIRKCacheExpand{iip, T, diffcache, fit_parameters} <:
36+
AbstractBoundaryValueDiffEqCache
3537
order::Int # The order of FIRK method
3638
stage::Int # The state of FIRK method
3739
M::Int # The number of equations
@@ -103,9 +105,14 @@ function init_nested(
103105
error("Algorithm doesn't support adaptivity. Please choose a higher order algorithm.")
104106
end
105107
diffcache = __cache_trait(alg.jac_alg)
108+
fit_parameters = haskey(prob.kwargs, :fit_parameters)
106109

107110
t₀, t₁ = prob.tspan
108-
ig, T, M, Nig, X = __extract_problem_details(prob; dt, check_positive_dt = true)
111+
ig, T,
112+
M,
113+
Nig,
114+
X = __extract_problem_details(
115+
prob; dt, check_positive_dt = true, fit_parameters = fit_parameters)
109116
mesh = __extract_mesh(prob.u0, t₀, t₁, Nig)
110117
mesh_dt = diff(mesh)
111118

@@ -116,7 +123,7 @@ function init_nested(
116123
fᵢ₂_cache = vec(zero(X))
117124

118125
# Don't flatten this here, since we need to expand it later if needed
119-
y₀ = __initial_guess_on_mesh(prob.u0, mesh, prob.p)
126+
y₀ = __initial_guess_on_mesh(X, mesh, prob.p)
120127

121128
y = __alloc.(copy.(y₀.u))
122129
TU, ITU = constructRK(alg, T)
@@ -143,7 +150,17 @@ function init_nested(
143150
bcresid_prototype = __vec(bcresid_prototype)
144151
f,
145152
bc = if X isa AbstractVector
146-
prob.f, prob.f.bc
153+
if fit_parameters == true
154+
l_parameters = length(prob.p)
155+
vecf! = function (du, u, p, t)
156+
prob.f(du, u, @view(u[(end - l_parameters + 1):end]), t)
157+
du[(end - l_parameters + 1):end] .= 0
158+
end
159+
vecbc! = prob.f.bc
160+
vecf!, vecbc!
161+
else
162+
prob.f, prob.f.bc
163+
end
147164
elseif iip
148165
vecf! = @closure (du, u, p, t) -> __vec_f!(du, u, p, t, prob.f, size(X))
149166
vecbc! = if !(prob.problem_type isa TwoPointBVProblem)
@@ -169,7 +186,8 @@ function init_nested(
169186

170187
prob_ = !(prob.u0 isa AbstractArray) ? remake(prob; u0 = X) : prob
171188

172-
K0 = __K0_on_u0(prob.u0, stage) # Somewhat arbitrary initialization of K
189+
# Somewhat arbitrary initialization of K
190+
K0 = __K0_on_u0(prob, stage; fit_parameters = fit_parameters)
173191

174192
nestprob_p = zeros(T, M + 2)
175193

@@ -181,7 +199,7 @@ function init_nested(
181199
(K, p) -> FIRK_nlsolve(K, p, f, TU, prob.p), K0, nestprob_p)
182200
end
183201

184-
return FIRKCacheNested{iip, T, typeof(diffcache)}(
202+
return FIRKCacheNested{iip, T, typeof(diffcache), fit_parameters}(
185203
alg_order(alg), stage, M, size(X), f, bc, prob_, prob.problem_type,
186204
prob.p, alg, TU, ITU, bcresid_prototype, mesh, mesh_dt, k_discrete,
187205
y, y₀, residual, fᵢ_cache, fᵢ₂_cache, defect, nestprob, resid₁_size,
@@ -197,11 +215,16 @@ function init_expanded(
197215
error("Algorithm $(alg) doesn't support adaptivity. Please choose a higher order algorithm.")
198216
end
199217
diffcache = __cache_trait(alg.jac_alg)
218+
fit_parameters = haskey(prob.kwargs, :fit_parameters)
200219

201220
iip = isinplace(prob)
202221

203222
t₀, t₁ = prob.tspan
204-
ig, T, M, Nig, X = __extract_problem_details(prob; dt, check_positive_dt = true)
223+
ig, T,
224+
M,
225+
Nig,
226+
X = __extract_problem_details(
227+
prob; dt, check_positive_dt = true, fit_parameters = fit_parameters)
205228
mesh = __extract_mesh(prob.u0, t₀, t₁, Nig)
206229
mesh_dt = diff(mesh)
207230

@@ -215,7 +238,7 @@ function init_expanded(
215238
fᵢ₂_cache = vec(zero(X))
216239

217240
# Don't flatten this here, since we need to expand it later if needed
218-
_y₀ = __initial_guess_on_mesh(prob.u0, mesh, prob.p)
241+
_y₀ = __initial_guess_on_mesh(X, mesh, prob.p)
219242
y₀ = extend_y(_y₀, Nig + 1, stage)
220243
y = __alloc.(copy.(y₀.u)) # Runtime dispatch
221244

@@ -240,7 +263,17 @@ function init_expanded(
240263
bcresid_prototype = __vec(bcresid_prototype)
241264
f,
242265
bc = if X isa AbstractVector
243-
prob.f, prob.f.bc
266+
if fit_parameters == true
267+
l_parameters = length(prob.p)
268+
vecf! = function (du, u, p, t)
269+
prob.f(du, u, @view(u[(end - l_parameters + 1):end]), t)
270+
du[(end - l_parameters + 1):end] .= 0
271+
end
272+
vecbc! = prob.f.bc
273+
vecf!, vecbc!
274+
else
275+
prob.f, prob.f.bc
276+
end
244277
elseif iip
245278
vecf! = @closure (du, u, p, t) -> __vec_f!(du, u, p, t, prob.f, size(X))
246279
vecbc! = if !(prob.problem_type isa TwoPointBVProblem)
@@ -266,7 +299,7 @@ function init_expanded(
266299

267300
prob_ = !(prob.u0 isa AbstractArray) ? remake(prob; u0 = X) : prob
268301

269-
return FIRKCacheExpand{iip, T, typeof(diffcache)}(
302+
return FIRKCacheExpand{iip, T, typeof(diffcache), fit_parameters}(
270303
alg_order(alg), stage, M, size(X), f, bc, prob_, prob.problem_type,
271304
prob.p, alg, TU, ITU, bcresid_prototype, mesh, mesh_dt, k_discrete,
272305
y, y₀, residual, fᵢ_cache, fᵢ₂_cache, defect, resid₁_size,
@@ -299,9 +332,12 @@ function __expand_cache!(cache::FIRKCacheNested)
299332
return cache
300333
end
301334

302-
function SciMLBase.solve!(cache::FIRKCacheExpand{iip, T}) where {iip, T}
335+
function SciMLBase.solve!(cache::FIRKCacheExpand{
336+
iip, T, diffcache, fit_parameters}) where {iip, T, diffcache, fit_parameters}
303337
(abstol, adaptive, _), kwargs = __split_kwargs(; cache.kwargs...)
304338
info::ReturnCode.T = ReturnCode.Success
339+
prob = cache.prob
340+
length_u = cache.in_size
305341

306342
# We do the first iteration outside the loop to preserve type-stability of the
307343
# `original` field of the solution
@@ -314,19 +350,28 @@ function SciMLBase.solve!(cache::FIRKCacheExpand{iip, T}) where {iip, T}
314350
end
315351
end
316352

317-
u = shrink_y(
318-
[reshape(y, cache.in_size) for y in cache.y₀], length(cache.mesh), cache.stage)
353+
# Parameter estimation, put the estimated parameters to sol.prob.p
354+
if fit_parameters
355+
length_u = cache.M - length(prob.p)
356+
prob = remake(prob; p = first(cache.y₀)[(length_u + 1):end])
357+
map(x -> resize!(x, length_u), cache.y₀)
358+
resize!(cache.fᵢ₂_cache, length_u)
359+
end
360+
361+
u = shrink_y([reshape(y, length_u) for y in cache.y₀], length(cache.mesh), cache.stage)
319362

320363
interpolation = __build_interpolation(cache, u)
321364

322365
odesol = DiffEqBase.build_solution(
323-
cache.prob, cache.alg, cache.mesh, u; interp = interpolation, retcode = info)
324-
return __build_solution(cache.prob, odesol, sol_nlprob)
366+
prob, cache.alg, cache.mesh, u; interp = interpolation, retcode = info)
367+
return __build_solution(prob, odesol, sol_nlprob)
325368
end
326369

327-
function SciMLBase.solve!(cache::FIRKCacheNested{iip, T}) where {iip, T}
370+
function SciMLBase.solve!(cache::FIRKCacheNested{
371+
iip, T, diffcache, fit_parameters}) where {iip, T, diffcache, fit_parameters}
328372
(abstol, adaptive, _), kwargs = __split_kwargs(; cache.kwargs...)
329373
info::ReturnCode.T = ReturnCode.Success
374+
prob = cache.prob
330375

331376
# We do the first iteration outside the loop to preserve type-stability of the
332377
# `original` field of the solution
@@ -339,13 +384,21 @@ function SciMLBase.solve!(cache::FIRKCacheNested{iip, T}) where {iip, T}
339384
end
340385
end
341386

387+
# Parameter estimation, put the estimated parameters to sol.prob.p
388+
if fit_parameters
389+
length_u = cache.M - length(prob.p)
390+
prob = remake(prob; p = first(cache.y₀)[(length_u + 1):end])
391+
map(x -> resize!(x, length_u), cache.y₀)
392+
resize!(cache.fᵢ₂_cache, length_u)
393+
end
394+
342395
u = recursivecopy(cache.y₀)
343396

344397
interpolation = __build_interpolation(cache, u.u)
345398

346399
odesol = DiffEqBase.build_solution(
347-
cache.prob, cache.alg, cache.mesh, u.u; interp = interpolation, retcode = info)
348-
return __build_solution(cache.prob, odesol, sol_nlprob)
400+
prob, cache.alg, cache.mesh, u.u; interp = interpolation, retcode = info)
401+
return __build_solution(prob, odesol, sol_nlprob)
349402
end
350403

351404
function __perform_firk_iteration(

0 commit comments

Comments
 (0)