Skip to content
This repository was archived by the owner on May 15, 2025. It is now read-only.

Commit 06ae37e

Browse files
committed
Implementation of a trust-region solver
1 parent d6e4633 commit 06ae37e

File tree

4 files changed

+182
-4
lines changed

4 files changed

+182
-4
lines changed

src/SimpleNonlinearSolve.jl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ include("raphson.jl")
2121
include("ad.jl")
2222
include("broyden.jl")
2323
include("klement.jl")
24+
include("trustRegion.jl")
2425

2526
import SnoopPrecompile
2627

@@ -30,6 +31,10 @@ SnoopPrecompile.@precompile_all_calls begin for T in (Float32, Float64)
3031
solve(prob_no_brack, alg(), tol = T(1e-2))
3132
end
3233

34+
for alg in (TrustRegion(1.0),)
35+
solve(prob_no_brack, alg, tol = T(1e-2))
36+
end
37+
3338
#=
3439
for alg in (SimpleNewtonRaphson,)
3540
for u0 in ([1., 1.], StaticArraysCore.SA[1.0, 1.0])
@@ -47,6 +52,7 @@ SnoopPrecompile.@precompile_all_calls begin for T in (Float32, Float64)
4752
end end
4853

4954
# DiffEq styled algorithms
50-
export Bisection, Broyden, Falsi, Klement, SimpleNewtonRaphson
55+
export Bisection, Broyden, Falsi, Klement, SimpleNewtonRaphson,
56+
TrustRegion
5157

5258
end # module

src/trustRegion.jl

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
```julia
3+
TrustRegion(max_trust_radius::Number; chunk_size = Val{0}(),
4+
autodiff = Val{true}(), diff_type = Val{:forward})
5+
```
6+
7+
A low-overhead implementation of a
8+
[trust-region](https://optimization.mccormick.northwestern.edu/index.php/Trust-region_methods)
9+
solver
10+
11+
12+
### Keyword Arguments
13+
- `max_trust_radius`: the maximum radius of the trust region. The step size in the algorithm
14+
will change dynamically. However, it will never be greater than the `max_trust_radius`.
15+
16+
### Keyword Arguments
17+
18+
- `chunk_size`: the chunk size used by the internal ForwardDiff.jl automatic differentiation
19+
system. This allows for multiple derivative columns to be computed simultaneously,
20+
improving performance. Defaults to `0`, which is equivalent to using ForwardDiff.jl's
21+
default chunk size mechanism. For more details, see the documentation for
22+
[ForwardDiff.jl](https://juliadiff.org/ForwardDiff.jl/stable/).
23+
- `autodiff`: whether to use forward-mode automatic differentiation for the Jacobian.
24+
Note that this argument is ignored if an analytical Jacobian is passed; as that will be
25+
used instead. Defaults to `Val{true}`, which means ForwardDiff.jl is used by default.
26+
If `Val{false}`, then FiniteDiff.jl is used for finite differencing.
27+
- `diff_type`: the type of finite differencing used if `autodiff = false`. Defaults to
28+
`Val{:forward}` for forward finite differences. For more details on the choices, see the
29+
[FiniteDiff.jl](https://github.com/JuliaDiff/FiniteDiff.jl) documentation.
30+
"""
31+
struct TrustRegion{CS, AD, FDT} <: AbstractNewtonAlgorithm{CS, AD, FDT}
32+
max_trust_radius::Number
33+
function TrustRegion(max_turst_radius::Number; chunk_size = Val{0}(),
34+
autodiff = Val{true}(),
35+
diff_type = Val{:forward})
36+
new{SciMLBase._unwrap_val(chunk_size), SciMLBase._unwrap_val(autodiff),
37+
SciMLBase._unwrap_val(diff_type)}(max_trust_radius)
38+
end
39+
end
40+
41+
function SciMLBase.solve(prob::NonlinearProblem,
42+
alg::TrustRegion, args...; abstol = nothing,
43+
reltol = nothing,
44+
maxiters = 1000, kwargs...)
45+
f = Base.Fix2(prob.f, prob.p)
46+
x = float(prob.u0)
47+
T = typeof(x)
48+
Δₘₐₓ = float(alg.max_trust_radius) # The maximum trust region radius.
49+
Δ = Δₘₐₓ / 5 # Initial trust region radius.
50+
η₁ = 0.1 # Threshold for taking a step.
51+
η₂ = 0.25 # Threshold for shrinking the trust region.
52+
η₃ = 0.75 # Threshold for expanding the trust region.
53+
t₁ = 0.25 # Factor to shrink the trust region with.
54+
t₂ = 2.0 # Factor to expand the trust region with.
55+
56+
if SciMLBase.isinplace(prob)
57+
error("TrustRegion currently only supports out-of-place nonlinear problems")
58+
end
59+
60+
atol = abstol !== nothing ? abstol :
61+
real(oneunit(eltype(T))) * (eps(real(one(eltype(T)))))^(4 // 5)
62+
rtol = reltol !== nothing ? reltol : eps(real(one(eltype(T))))^(4 // 5)
63+
64+
if alg_autodiff(alg)
65+
F, ∇f = value_derivative(f, x)
66+
elseif x isa AbstractArray
67+
F = f(x)
68+
∇f = FiniteDiff.finite_difference_jacobian(f, x, diff_type(alg), eltype(x), F)
69+
else
70+
F = f(x)
71+
∇f = FiniteDiff.finite_difference_derivative(f, x, diff_type(alg), eltype(x), F)
72+
end
73+
74+
fₖ = 0.5 * norm(F)^2
75+
H = ∇f * ∇f
76+
g = ∇f * F
77+
78+
for k in 1:maxiters
79+
# Solve the trust region subproblem.
80+
δ = dogleg_method(H, g, Δ)
81+
xₖ₊₁ = x + δ
82+
Fₖ₊₁ = f(xₖ₊₁)
83+
fₖ₊₁ = 0.5 * norm(Fₖ₊₁)^2
84+
85+
# Compute the ratio of the actual to predicted reduction.
86+
model = -' * g + 0.5 * δ' * H * δ)
87+
r = (fₖ - fₖ₊₁) / model
88+
89+
# Update the trust region radius.
90+
if r < η₂
91+
Δ *= t₁
92+
if r > η₁
93+
if isapprox(x̂, x, atol = atol, rtol = rtol)
94+
return SciMLBase.build_solution(prob, alg, x, F;
95+
retcode = ReturnCode.Success)
96+
end
97+
98+
x = xₖ₊₁
99+
F = Fₖ₊₁
100+
if alg_autodiff(alg)
101+
F, ∇f = value_derivative(f, x)
102+
elseif x isa AbstractArray
103+
∇f = FiniteDiff.finite_difference_jacobian(f, x, diff_type(alg), eltype(x),
104+
F)
105+
else
106+
∇f = FiniteDiff.finite_difference_derivative(f, x, diff_type(alg),
107+
eltype(x),
108+
F)
109+
end
110+
111+
iszero(F) &&
112+
return SciMLBase.build_solution(prob, alg, x, F;
113+
retcode = ReturnCode.Success)
114+
# Update the trust region radius.
115+
if r > η₃ && norm(δ) Δ
116+
Δ = min(t₂ * Δ, Δₘₐₓ)
117+
end
118+
fₖ =
119+
H = ∇f * ∇f
120+
g = ∇f * F
121+
end
122+
end
123+
124+
return SciMLBase.build_solution(prob, alg, x, F; retcode = ReturnCode.MaxIters)
125+
end

src/utils.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,28 @@ function init_J(x)
4343
end
4444
return J
4545
end
46+
47+
function dogleg_method(H, g, Δ)
48+
# Compute the Newton step.
49+
δN = -H \ g
50+
# Test if the full step is within the trust region.
51+
if norm(δN) Δ
52+
return δN
53+
end
54+
55+
# Calcualte Cauchy point, optimum along the steepest descent direction.
56+
δsd = -g
57+
norm_δsd = norm(δsd)
58+
if norm_δsd Δ
59+
return δsd .* Δ / norm_δsd
60+
end
61+
62+
# Find the intersection point on the boundary.
63+
δN_δsd = δN - δsd
64+
dot_δN_δsd = dot(δN_δsd, δN_δsd)
65+
dot_δsd_δN_δsd = dot(δsd, δN_δsd)
66+
dot_δsd = dot(δsd, δsd)
67+
fact = dot_δsd_δN_δsd^2 - dot_δN_δsd * (dot_δsd - Δ^2)
68+
tau = (-dot_δsd_δN_δsd + sqrt(fact)) / dot_δN_δsd
69+
return δsd + tau * δN_δsd
70+
end

test/basictests.jl

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,24 @@ sol = benchmark_scalar(sf, csu0)
4646
@test sol.u * sol.u - 2 < 1e-9
4747
@test (@ballocated benchmark_scalar(sf, csu0)) == 0
4848

49+
# SimpleNewtonRaphsonTrustRegion
50+
function benchmark_scalar(f, u0)
51+
probN = NonlinearProblem{false}(f, u0)
52+
sol = (solve(probN, SimpleNewtonRaphsonTrustRegion(1.0)))
53+
end
54+
55+
sol = benchmark_scalar(sf, csu0)
56+
@test sol.retcode === ReturnCode.Success
57+
@test sol.u * sol.u - 2 < 1e-9
58+
4959
# AD Tests
5060
using ForwardDiff
5161

5262
# Immutable
5363
f, u0 = (u, p) -> u .* u .- p, @SVector[1.0, 1.0]
5464

55-
for alg in [SimpleNewtonRaphson(), Broyden(), Klement()]
65+
for alg in [SimpleNewtonRaphson(), Broyden(), Klement(),
66+
SimpleNewtonRaphsonTrustRegion(10.0)]
5667
g = function (p)
5768
probN = NonlinearProblem{false}(f, csu0, p)
5869
sol = solve(probN, alg, tol = 1e-9)
@@ -67,7 +78,8 @@ end
6778

6879
# Scalar
6980
f, u0 = (u, p) -> u * u - p, 1.0
70-
for alg in [SimpleNewtonRaphson(), Broyden(), Klement()]
81+
for alg in [SimpleNewtonRaphson(), Broyden(), Klement(),
82+
SimpleNewtonRaphsonTrustRegion(10.0)]
7183
g = function (p)
7284
probN = NonlinearProblem{false}(f, oftype(p, u0), p)
7385
sol = solve(probN, alg)
@@ -108,7 +120,8 @@ for alg in [Bisection(), Falsi()]
108120
@test ForwardDiff.jacobian(g, p) ForwardDiff.jacobian(t, p)
109121
end
110122

111-
for alg in [SimpleNewtonRaphson(), Broyden(), Klement()]
123+
for alg in [SimpleNewtonRaphson(), Broyden(), Klement(),
124+
SimpleNewtonRaphsonTrustRegion(1.0)]
112125
global g, p
113126
g = function (p)
114127
probN = NonlinearProblem{false}(f, 0.5, p)
@@ -128,6 +141,11 @@ probN = NonlinearProblem(f, u0)
128141
@test solve(probN, SimpleNewtonRaphson(; autodiff = false)).u[end] sqrt(2.0)
129142
@test solve(probN, SimpleNewtonRaphson(; autodiff = false)).u[end] sqrt(2.0)
130143

144+
@test solve(probN, SimpleNewtonRaphsonTrustRegion(1.0)).u[end] sqrt(2.0)
145+
@test solve(probN, SimpleNewtonRaphsonTrustRegion(1.0); immutable = false).u[end] sqrt(2.0)
146+
@test solve(probN, SimpleNewtonRaphsonTrustRegion(1.0; autodiff = false)).u[end] sqrt(2.0)
147+
@test solve(probN, SimpleNewtonRaphsonTrustRegion(1.0; autodiff = false)).u[end] sqrt(2.0)
148+
131149
@test solve(probN, Broyden()).u[end] sqrt(2.0)
132150
@test solve(probN, Broyden(); immutable = false).u[end] sqrt(2.0)
133151

@@ -144,6 +162,10 @@ for u0 in [1.0, [1, 1.0]]
144162
@test solve(probN, SimpleNewtonRaphson()).u sol
145163
@test solve(probN, SimpleNewtonRaphson(; autodiff = false)).u sol
146164

165+
@test solve(probN, SimpleNewtonRaphsonTrustRegion(1.0)).u sol
166+
@test solve(probN, SimpleNewtonRaphsonTrustRegion(1.0)).u sol
167+
@test solve(probN, SimpleNewtonRaphsonTrustRegion(1.0; autodiff = false)).u sol
168+
147169
@test solve(probN, Broyden()).u sol
148170

149171
@test solve(probN, Klement()).u sol

0 commit comments

Comments
 (0)