Skip to content

Commit 1804133

Browse files
Merge pull request #332 from SciML/qqy/max_min_sol
Add maxsol and minsol
2 parents 0a71586 + 055e4f6 commit 1804133

File tree

8 files changed

+151
-2
lines changed

8 files changed

+151
-2
lines changed

docs/pages.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +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", "tutorials/solve_nlls_bvp.md"],
5+
"Tutorials" => Any[
6+
"tutorials/continuation.md", "tutorials/solve_nlls_bvp.md", "tutorials/extremum.md"],
67
"Basics" => Any["basics/bvp_problem.md", "basics/bvp_functions.md",
78
"basics/solve.md", "basics/autodiff.md", "basics/error_control.md"],
89
"Solver Summaries and Recommendations" => Any[

docs/src/tutorials/extremum.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Solve BVP with Extremum Boundary Conditions
2+
3+
In many physical systems, boundary conditions are not always defined at fixed points such as intitial or terminal ends of the domain. Instead, we may encounter scenarios where constraints are imposed on the maximum or minimum values that the solution must attain somewhere within the domain. In such cases, we can use the `maxsol` and `minsol` functions provided by BoundaryValueDiffEq.jl to specify such extremum boundary conditions.
4+
5+
Let's walk through this functionality with an intuitive example. We still revisit the simple pendulum example here, but this time, suppose we need to impose the maximum and minimum value to our boundary conditions, specified as:
6+
7+
```math
8+
\max{u}=ub\\
9+
\min{u}=lb
10+
```
11+
12+
where `lb=-4.8161991710010925` and `ub=5.0496477654230745`. So the states must conform that the maximum value of the state should be `lb` while the minimum value of the state should be `ub`. To solve such problems, we can simply use the `maxsol` and `minsol` functions when defining the boundary value problem in BoundaryValueDiffEq.jl.
13+
14+
```@example inequality
15+
using BoundaryValueDiffEq, Plots
16+
tspan = (0.0, pi / 2)
17+
function simplependulum!(du, u, p, t)
18+
θ = u[1]
19+
dθ = u[2]
20+
du[1] = dθ
21+
du[2] = -9.81 * sin(θ)
22+
end
23+
function bc!(residual, u, p, t)
24+
residual[1] = maxsol(u, (0.0, pi / 2)) - 5.0496477654230745
25+
residual[2] = minsol(u, (0.0, pi / 2)) + 4.8161991710010925
26+
end
27+
prob = BVProblem(simplependulum!, bc!, [pi / 2, pi / 2], tspan)
28+
sol = solve(prob, MIRK4(), dt = 0.05)
29+
plot(sol)
30+
```

lib/BoundaryValueDiffEqCore/src/default_nlsolve.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,18 @@ function __FastShortcutBVPCompatibleNonlinearPolyalg(
3636
return NonlinearSolvePolyAlgorithm(algs)
3737
end
3838

39+
function __FastShortcutNonlinearPolyalg(::Type{T} = Float64; concrete_jac = nothing,
40+
linsolve = nothing, autodiff = nothing) where {T}
41+
if T <: Complex
42+
algs = (NewtonRaphson(; concrete_jac, linsolve, autodiff),)
43+
else
44+
algs = (NewtonRaphson(; concrete_jac, linsolve, autodiff),
45+
NewtonRaphson(; concrete_jac, linsolve, linesearch = BackTracking(), autodiff),
46+
TrustRegion(; concrete_jac, linsolve, autodiff))
47+
end
48+
return NonlinearSolvePolyAlgorithm(algs)
49+
end
50+
3951
@inline __concrete_nonlinearsolve_algorithm(prob, alg) = alg
4052
@inline function __concrete_nonlinearsolve_algorithm(prob, ::Nothing)
4153
if prob isa NonlinearLeastSquaresProblem

lib/BoundaryValueDiffEqCore/src/solution_utils.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ Base.eltype(e::EvalSol) = eltype(e.u)
2929
function Base.show(io::IO, m::MIME"text/plain", e::EvalSol)
3030
(print(io, "t: "); show(io, m, e.t); println(io); print(io, "u: "); show(io, m, e.u))
3131
end
32+
33+
Base.maximum(sol::EvalSol) = maximum(Iterators.flatten(sol.u))
34+
Base.minimum(sol::EvalSol) = minimum(Iterators.flatten(sol.u))

lib/BoundaryValueDiffEqMIRK/src/BoundaryValueDiffEqMIRK.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ using BoundaryValueDiffEqCore: AbstractBoundaryValueDiffEqAlgorithm,
2121
DefectControl, GlobalErrorControl, SequentialErrorControl,
2222
HybridErrorControl, HOErrorControl, __use_both_error_control,
2323
__default_coloring_algorithm, DiffCacheNeeded,
24-
NoDiffCacheNeeded, __split_kwargs
24+
NoDiffCacheNeeded, __split_kwargs,
25+
__FastShortcutNonlinearPolyalg
2526

2627
using ConcreteStructs: @concrete
2728
using DiffEqBase: DiffEqBase
@@ -160,5 +161,6 @@ end
160161

161162
export MIRK2, MIRK3, MIRK4, MIRK5, MIRK6, MIRK6I
162163
export BVPJacobianAlgorithm
164+
export maxsol, minsol
163165

164166
end

lib/BoundaryValueDiffEqMIRK/src/interpolation.jl

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,86 @@ function (s::EvalSol{C})(tval::Number) where {C <: MIRKCache}
158158
return z
159159
end
160160

161+
# Interpolate intermidiate solution at multiple points
162+
function (s::EvalSol{C})(tvals::AbstractArray{<:Number}) where {C <: MIRKCache}
163+
(; t, u, cache) = s
164+
(; alg, stage, k_discrete, mesh_dt) = cache
165+
# Quick handle for the case where tval is at the boundary
166+
zvals = [zero(last(u)) for _ in tvals]
167+
for (i, tval) in enumerate(tvals)
168+
(tval == t[1]) && return first(u)
169+
(tval == t[end]) && return last(u)
170+
ii = interval(t, tval)
171+
dt = mesh_dt[ii]
172+
τ = (tval - t[ii]) / dt
173+
w, _ = evalsol_interp_weights(τ, alg)
174+
K = __needs_diffcache(alg.jac_alg) ? @view(k_discrete[ii].du[:, 1:stage]) :
175+
@view(k_discrete[ii][:, 1:stage])
176+
__maybe_matmul!(zvals[i], K, @view(w[1:stage]))
177+
zvals[i] .= zvals[i] .* dt .+ u[ii]
178+
end
179+
return zvals
180+
end
181+
182+
# Intermidiate derivative solution for evaluating boundry conditions
183+
function (s::EvalSol{C})(tval::Number, ::Type{Val{1}}) where {C <: MIRKCache}
184+
(; t, u, cache) = s
185+
(; alg, stage, k_discrete, mesh_dt) = cache
186+
z′ = zeros(typeof(tval), 2)
187+
ii = interval(t, tval)
188+
dt = mesh_dt[ii]
189+
τ = (tval - t[ii]) / dt
190+
_, w′ = interp_weights(τ, alg)
191+
__maybe_matmul!(z′, @view(k_discrete[ii].du[:, 1:stage]), @view(w′[1:stage]))
192+
return z′
193+
end
194+
195+
"""
196+
Construct n root-finding problems and solve them to find the critical points with continuous derivative polynomials
197+
"""
198+
function __construct_then_solve_root_problem(
199+
sol::EvalSol{C}, tspan::Tuple) where {C <: MIRKCache}
200+
(; alg) = sol.cache
201+
n = first(size(sol))
202+
nlprobs = Vector{SciMLBase.NonlinearProblem}(undef, n)
203+
nlsols = Vector{SciMLBase.NonlinearSolution}(undef, length(nlprobs))
204+
nlsolve_alg = __FastShortcutNonlinearPolyalg(eltype(sol.cache))
205+
for i in 1:n
206+
f = @closure (t, p) -> sol(t, Val{1})[i]
207+
nlprob = NonlinearProblem(f, sol.cache.prob.u0[i], tspan)
208+
nlsols[i] = solve(nlprob, nlsolve_alg)
209+
end
210+
return nlsols
211+
end
212+
213+
# It turns out the critical points can't cover all possible maximum/minimum values
214+
# especially when the solution are monotonic, we still need to compare the extremes with
215+
# value at critical points to find the maximum/minimum
216+
217+
"""
218+
maxsol(sol::EvalSol, tspan::Tuple)
219+
220+
Find the maximum of the solution over the time span `tspan`.
221+
"""
222+
function maxsol(sol::EvalSol{C}, tspan::Tuple) where {C <: MIRKCache}
223+
nlsols = __construct_then_solve_root_problem(sol, tspan)
224+
tvals = map(nlsol -> (SciMLBase.successful_retcode(nlsol); return nlsol.u), nlsols)
225+
u = sol(tvals)
226+
return max(maximum(sol), maximum(Iterators.flatten(u)))
227+
end
228+
229+
"""
230+
minsol(sol::EvalSol, tspan::Tuple)
231+
232+
Find the minimum of the solution over the time span `tspan`.
233+
"""
234+
function minsol(sol::EvalSol{C}, tspan::Tuple) where {C <: MIRKCache}
235+
nlsols = __construct_then_solve_root_problem(sol, tspan)
236+
tvals = map(nlsol -> (SciMLBase.successful_retcode(nlsol); return nlsol.u), nlsols)
237+
u = sol(tvals)
238+
return min(minimum(sol), minimum(Iterators.flatten(u)))
239+
end
240+
161241
@inline function evalsol_interp_weights::T, ::MIRK2) where {T}
162242
w = [0, τ * (1 - τ / 2), τ^2 / 2]
163243

lib/BoundaryValueDiffEqMIRK/test/mirk_basic_tests.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,22 @@ end
401401
@test SciMLBase.successful_retcode(sol)
402402
end
403403
end
404+
405+
@testitem "Test maxsol and minsol" setup=[MIRKConvergenceTests] begin
406+
tspan = (0.0, pi / 2)
407+
function simplependulum!(du, u, p, t)
408+
θ = u[1]
409+
= u[2]
410+
du[1] =
411+
du[2] = -9.81 * sin(θ)
412+
end
413+
function bc!(residual, u, p, t)
414+
residual[1] = maxsol(u, (0.0, pi / 2)) - 5.0496477654230745
415+
residual[2] = minsol(u, (0.0, pi / 2)) + 4.8161991710010925
416+
end
417+
prob = BVProblem(simplependulum!, bc!, [pi / 2, pi / 2], tspan)
418+
for order in (4, 6)
419+
sol = solve(prob, mirk_solver(Val(order)), dt = 0.05)
420+
@test SciMLBase.successful_retcode(sol)
421+
end
422+
end

src/BoundaryValueDiffEq.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,6 @@ export BVPM2, BVPSOL, COLNEW # From ODEInterface.jl
3232

3333
export BVPJacobianAlgorithm
3434

35+
export maxsol, minsol
36+
3537
end

0 commit comments

Comments
 (0)