Skip to content

Commit 611477c

Browse files
committed
Add Limited Memory Broyden
1 parent ee5e5b8 commit 611477c

File tree

8 files changed

+403
-18
lines changed

8 files changed

+403
-18
lines changed

.github/workflows/CI.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ jobs:
1616
strategy:
1717
matrix:
1818
group:
19-
- All
19+
- Core
20+
- 23TestProblems
2021
version:
2122
- '1'
2223
steps:

src/NonlinearSolve.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ include("dfsane.jl")
7676
include("pseudotransient.jl")
7777
include("broyden.jl")
7878
include("klement.jl")
79+
include("lbroyden.jl")
7980
include("jacobian.jl")
8081
include("ad.jl")
8182
include("default.jl")
@@ -106,7 +107,7 @@ end
106107
export RadiusUpdateSchemes
107108

108109
export NewtonRaphson, TrustRegion, LevenbergMarquardt, DFSane, GaussNewton, PseudoTransient,
109-
GeneralBroyden, GeneralKlement
110+
GeneralBroyden, GeneralKlement, LimitedMemoryBroyden
110111
export LeastSquaresOptimJL, FastLevenbergMarquardtJL
111112
export RobustMultiNewton, FastShortcutNonlinearPolyalg
112113

src/broyden.jl

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# Sadly `Broyden` is taken up by SimpleNonlinearSolve.jl
22
"""
3-
GeneralBroyden(max_resets, linesearch)
4-
GeneralBroyden(; max_resets = 3, linesearch = LineSearch())
3+
GeneralBroyden(; max_resets = 3, linesearch = LineSearch(), reset_tolerance = nothing)
54
65
An implementation of `Broyden` with reseting and line search.
76
87
## Arguments
98
109
- `max_resets`: the maximum number of resets to perform. Defaults to `3`.
10+
- `reset_tolerance`: the tolerance for the reset check. Defaults to
11+
`sqrt(eps(eltype(u)))`.
1112
- `linesearch`: the line search algorithm to use. Defaults to [`LineSearch()`](@ref),
1213
which means that no line search is performed. Algorithms from `LineSearches.jl` can be
1314
used here directly, and they will be converted to the correct `LineSearch`. It is
@@ -16,12 +17,14 @@ An implementation of `Broyden` with reseting and line search.
1617
"""
1718
@concrete struct GeneralBroyden <: AbstractNewtonAlgorithm{false, Nothing}
1819
max_resets::Int
20+
reset_tolerance
1921
linesearch
2022
end
2123

22-
function GeneralBroyden(; max_resets = 3, linesearch = LineSearch())
24+
function GeneralBroyden(; max_resets = 3, linesearch = LineSearch(),
25+
reset_tolerance = nothing)
2326
linesearch = linesearch isa LineSearch ? linesearch : LineSearch(; method = linesearch)
24-
return GeneralBroyden(max_resets, linesearch)
27+
return GeneralBroyden(max_resets, reset_tolerance, linesearch)
2528
end
2629

2730
@concrete mutable struct GeneralBroydenCache{iip} <: AbstractNonlinearSolveCache{iip}
@@ -43,6 +46,8 @@ end
4346
internalnorm
4447
retcode::ReturnCode.T
4548
abstol
49+
reset_tolerance
50+
reset_check
4651
prob
4752
stats::NLStats
4853
lscache
@@ -57,9 +62,13 @@ function SciMLBase.__init(prob::NonlinearProblem{uType, iip}, alg::GeneralBroyde
5762
u = alias_u0 ? u0 : deepcopy(u0)
5863
fu = evaluate_f(prob, u)
5964
J⁻¹ = __init_identity_jacobian(u, fu)
65+
reset_tolerance = alg.reset_tolerance === nothing ? sqrt(eps(eltype(u))) :
66+
alg.reset_tolerance
67+
reset_check = x -> abs(x) reset_tolerance
6068
return GeneralBroydenCache{iip}(f, alg, u, _mutable_zero(u), fu, zero(fu),
6169
zero(fu), p, J⁻¹, zero(_vec(fu)'), _mutable_zero(u), false, 0, alg.max_resets,
62-
maxiters, internalnorm, ReturnCode.Default, abstol, prob, NLStats(1, 0, 0, 0, 0),
70+
maxiters, internalnorm, ReturnCode.Default, abstol, reset_tolerance,
71+
reset_check, prob, NLStats(1, 0, 0, 0, 0),
6372
init_linesearch_cache(alg.linesearch, f, u, p, fu, Val(iip)))
6473
end
6574

@@ -79,8 +88,13 @@ function perform_step!(cache::GeneralBroydenCache{true})
7988

8089
# Update the inverse jacobian
8190
dfu .= fu2 .- fu
82-
if cache.resets < cache.max_resets &&
83-
(all(x -> abs(x) 1e-12, du) || all(x -> abs(x) 1e-12, dfu))
91+
92+
if all(cache.reset_check, du) || all(cache.reset_check, dfu)
93+
if cache.resets cache.max_resets
94+
cache.retcode = ReturnCode.Unstable
95+
cache.force_stop = true
96+
return nothing
97+
end
8498
fill!(J⁻¹, 0)
8599
J⁻¹[diagind(J⁻¹)] .= T(1)
86100
cache.resets += 1
@@ -111,8 +125,12 @@ function perform_step!(cache::GeneralBroydenCache{false})
111125

112126
# Update the inverse jacobian
113127
cache.dfu = cache.fu2 .- cache.fu
114-
if cache.resets < cache.max_resets &&
115-
(all(x -> abs(x) 1e-12, cache.du) || all(x -> abs(x) 1e-12, cache.dfu))
128+
if all(cache.reset_check, cache.du) || all(cache.reset_check, cache.dfu)
129+
if cache.resets cache.max_resets
130+
cache.retcode = ReturnCode.Unstable
131+
cache.force_stop = true
132+
return nothing
133+
end
116134
cache.J⁻¹ = __init_identity_jacobian(cache.u, cache.fu)
117135
cache.resets += 1
118136
else
@@ -141,6 +159,7 @@ function SciMLBase.reinit!(cache::GeneralBroydenCache{iip}, u0 = cache.u; p = ca
141159
cache.maxiters = maxiters
142160
cache.stats.nf = 1
143161
cache.stats.nsteps = 1
162+
cache.resets = 0
144163
cache.force_stop = false
145164
cache.retcode = ReturnCode.Default
146165
return cache

src/lbroyden.jl

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""
2+
LimitedMemoryBroyden(; max_resets::Int = 3, linesearch = LineSearch(),
3+
threshold::Int = 10, reset_tolerance = nothing)
4+
5+
An implementation of `LimitedMemoryBroyden` with reseting and line search.
6+
7+
## Arguments
8+
9+
- `max_resets`: the maximum number of resets to perform. Defaults to `3`.
10+
- `reset_tolerance`: the tolerance for the reset check. Defaults to
11+
`sqrt(eps(eltype(u)))`.
12+
- `threshold`: the number of vectors to store in the low rank approximation. Defaults
13+
to `10`.
14+
- `linesearch`: the line search algorithm to use. Defaults to [`LineSearch()`](@ref),
15+
which means that no line search is performed. Algorithms from `LineSearches.jl` can be
16+
used here directly, and they will be converted to the correct `LineSearch`. It is
17+
recommended to use [LiFukushimaLineSearchCache](@ref) -- a derivative free linesearch
18+
specifically designed for Broyden's method.
19+
"""
20+
@concrete struct LimitedMemoryBroyden <: AbstractNewtonAlgorithm{false, Nothing}
21+
max_resets::Int
22+
threshold::Int
23+
linesearch
24+
reset_tolerance
25+
end
26+
27+
function LimitedMemoryBroyden(; max_resets::Int = 3, linesearch = LineSearch(),
28+
threshold::Int = 10, reset_tolerance = nothing)
29+
linesearch = linesearch isa LineSearch ? linesearch : LineSearch(; method = linesearch)
30+
return LimitedMemoryBroyden(max_resets, threshold, linesearch, reset_tolerance)
31+
end
32+
33+
@concrete mutable struct LimitedMemoryBroydenCache{iip} <: AbstractNonlinearSolveCache{iip}
34+
f
35+
alg
36+
u
37+
du
38+
fu
39+
fu2
40+
dfu
41+
p
42+
U
43+
Vᵀ
44+
Ux
45+
xᵀVᵀ
46+
u_cache
47+
vᵀ_cache
48+
force_stop::Bool
49+
resets::Int
50+
iterations_since_reset::Int
51+
max_resets::Int
52+
maxiters::Int
53+
internalnorm
54+
retcode::ReturnCode.T
55+
abstol
56+
reset_tolerance
57+
reset_check
58+
prob
59+
stats::NLStats
60+
lscache
61+
end
62+
63+
get_fu(cache::LimitedMemoryBroydenCache) = cache.fu
64+
65+
function SciMLBase.__init(prob::NonlinearProblem{uType, iip}, alg::LimitedMemoryBroyden,
66+
args...; alias_u0 = false, maxiters = 1000, abstol = 1e-6, internalnorm = DEFAULT_NORM,
67+
kwargs...) where {uType, iip}
68+
@unpack f, u0, p = prob
69+
u = alias_u0 ? u0 : deepcopy(u0)
70+
if u isa Number
71+
# If u is a number then we simply use Broyden
72+
return SciMLBase.__init(prob,
73+
GeneralBroyden(; alg.max_resets, alg.reset_tolerance,
74+
alg.linesearch), args...; alias_u0, maxiters, abstol, internalnorm, kwargs...)
75+
end
76+
fu = evaluate_f(prob, u)
77+
threshold = min(alg.threshold, maxiters)
78+
U, Vᵀ = __init_low_rank_jacobian(u, fu, threshold)
79+
du = -fu
80+
reset_tolerance = alg.reset_tolerance === nothing ? sqrt(eps(eltype(u))) :
81+
alg.reset_tolerance
82+
reset_check = x -> abs(x) reset_tolerance
83+
return LimitedMemoryBroydenCache{iip}(f, alg, u, du, fu, zero(fu),
84+
zero(fu), p, U, Vᵀ, similar(u, threshold), similar(u, 1, threshold),
85+
zero(u), zero(u), false, 0, 0, alg.max_resets, maxiters, internalnorm,
86+
ReturnCode.Default, abstol, reset_tolerance, reset_check, prob,
87+
NLStats(1, 0, 0, 0, 0),
88+
init_linesearch_cache(alg.linesearch, f, u, p, fu, Val(iip)))
89+
end
90+
91+
function perform_step!(cache::LimitedMemoryBroydenCache{true})
92+
@unpack f, p, du, u = cache
93+
T = eltype(u)
94+
95+
α = perform_linesearch!(cache.lscache, u, du)
96+
axpy!(α, du, u)
97+
f(cache.fu2, u, p)
98+
99+
cache.internalnorm(cache.fu2) < cache.abstol && (cache.force_stop = true)
100+
cache.stats.nf += 1
101+
102+
cache.force_stop && return nothing
103+
104+
# Update the Inverse Jacobian Approximation
105+
cache.dfu .= cache.fu2 .- cache.fu
106+
107+
# Only try to reset if we have enough iterations since last reset
108+
if cache.iterations_since_reset > size(cache.U, 1) &&
109+
(all(cache.reset_check, du) || all(cache.reset_check, cache.dfu))
110+
if cache.resets cache.max_resets
111+
cache.retcode = ReturnCode.Unstable
112+
cache.force_stop = true
113+
return nothing
114+
end
115+
cache.iterations_since_reset = 0
116+
cache.resets += 1
117+
cache.du .= -cache.fu
118+
else
119+
idx = min(cache.iterations_since_reset, size(cache.U, 1))
120+
U_part = selectdim(cache.U, 1, 1:idx)
121+
Vᵀ_part = selectdim(cache.Vᵀ, 2, 1:idx)
122+
123+
__lbroyden_matvec!(_vec(cache.vᵀ_cache), cache.Ux, U_part, Vᵀ_part, _vec(cache.du))
124+
__lbroyden_rmatvec!(_vec(cache.u_cache), cache.xᵀVᵀ, U_part, Vᵀ_part,
125+
_vec(cache.dfu))
126+
cache.u_cache .= (du .- cache.u_cache) ./
127+
(dot(cache.vᵀ_cache, cache.dfu) .+ T(1e-5))
128+
129+
idx = mod1(cache.iterations_since_reset + 1, size(cache.U, 1))
130+
selectdim(cache.U, 1, idx) .= _vec(cache.u_cache)
131+
selectdim(cache.Vᵀ, 2, idx) .= _vec(cache.vᵀ_cache)
132+
133+
idx = min(cache.iterations_since_reset + 1, size(cache.U, 1))
134+
U_part = selectdim(cache.U, 1, 1:idx)
135+
Vᵀ_part = selectdim(cache.Vᵀ, 2, 1:idx)
136+
__lbroyden_matvec!(_vec(cache.du), cache.Ux, U_part, Vᵀ_part, _vec(cache.fu2))
137+
cache.du .*= -1
138+
cache.iterations_since_reset += 1
139+
end
140+
141+
cache.fu .= cache.fu2
142+
143+
return nothing
144+
end
145+
146+
function perform_step!(cache::LimitedMemoryBroydenCache{false})
147+
@unpack f, p = cache
148+
T = eltype(cache.u)
149+
150+
α = perform_linesearch!(cache.lscache, cache.u, cache.du)
151+
cache.u = cache.u .+ α * cache.du
152+
cache.fu2 = f(cache.u, p)
153+
154+
cache.internalnorm(cache.fu2) < cache.abstol && (cache.force_stop = true)
155+
cache.stats.nf += 1
156+
157+
cache.force_stop && return nothing
158+
159+
# Update the Inverse Jacobian Approximation
160+
cache.dfu .= cache.fu2 .- cache.fu
161+
162+
# Only try to reset if we have enough iterations since last reset
163+
if cache.iterations_since_reset > size(cache.U, 1) &&
164+
(all(cache.reset_check, cache.du) || all(cache.reset_check, cache.dfu))
165+
if cache.resets cache.max_resets
166+
cache.retcode = ReturnCode.Unstable
167+
cache.force_stop = true
168+
return nothing
169+
end
170+
cache.iterations_since_reset = 0
171+
cache.resets += 1
172+
cache.du = -cache.fu
173+
else
174+
idx = min(cache.iterations_since_reset, size(cache.U, 1))
175+
U_part = selectdim(cache.U, 1, 1:idx)
176+
Vᵀ_part = selectdim(cache.Vᵀ, 2, 1:idx)
177+
178+
cache.vᵀ_cache = _restructure(cache.vᵀ_cache,
179+
__lbroyden_matvec(U_part, Vᵀ_part, _vec(cache.du)))
180+
cache.u_cache = _restructure(cache.u_cache,
181+
__lbroyden_rmatvec(U_part, Vᵀ_part, _vec(cache.dfu)))
182+
cache.u_cache = (cache.du .- cache.u_cache) ./
183+
(dot(cache.vᵀ_cache, cache.dfu) .+ T(1e-5))
184+
185+
idx = mod1(cache.iterations_since_reset + 1, size(cache.U, 1))
186+
selectdim(cache.U, 1, idx) .= _vec(cache.u_cache)
187+
selectdim(cache.Vᵀ, 2, idx) .= _vec(cache.vᵀ_cache)
188+
189+
idx = min(cache.iterations_since_reset + 1, size(cache.U, 1))
190+
U_part = selectdim(cache.U, 1, 1:idx)
191+
Vᵀ_part = selectdim(cache.Vᵀ, 2, 1:idx)
192+
cache.du = _restructure(cache.du,
193+
-__lbroyden_matvec(U_part, Vᵀ_part, _vec(cache.fu2)))
194+
cache.iterations_since_reset += 1
195+
end
196+
197+
cache.fu = cache.fu2
198+
199+
return nothing
200+
end
201+
202+
function SciMLBase.reinit!(cache::LimitedMemoryBroydenCache{iip}, u0 = cache.u; p = cache.p,
203+
abstol = cache.abstol, maxiters = cache.maxiters) where {iip}
204+
cache.p = p
205+
if iip
206+
recursivecopy!(cache.u, u0)
207+
cache.f(cache.fu, cache.u, p)
208+
else
209+
# don't have alias_u0 but cache.u is never mutated for OOP problems so it doesn't matter
210+
cache.u = u0
211+
cache.fu = cache.f(cache.u, p)
212+
end
213+
cache.abstol = abstol
214+
cache.maxiters = maxiters
215+
cache.stats.nf = 1
216+
cache.stats.nsteps = 1
217+
cache.resets = 0
218+
cache.iterations_since_reset = 0
219+
cache.force_stop = false
220+
cache.retcode = ReturnCode.Default
221+
return cache
222+
end
223+
224+
@views function __lbroyden_matvec!(y::AbstractVector, Ux::AbstractVector,
225+
U::AbstractMatrix, Vᵀ::AbstractMatrix, x::AbstractVector)
226+
# Computes Vᵀ × U × x
227+
η = size(U, 1)
228+
if η == 0
229+
y .= x
230+
return nothing
231+
end
232+
mul!(Ux[1:η], U, x)
233+
mul!(y, Vᵀ[:, 1:η], Ux[1:η])
234+
return nothing
235+
end
236+
237+
@views function __lbroyden_matvec(U::AbstractMatrix, Vᵀ::AbstractMatrix, x::AbstractVector)
238+
# Computes Vᵀ × U × x
239+
size(U, 1) == 0 && return x
240+
return Vᵀ * (U * x)
241+
end
242+
243+
@views function __lbroyden_rmatvec!(y::AbstractVector, xᵀVᵀ::AbstractMatrix,
244+
U::AbstractMatrix, Vᵀ::AbstractMatrix, x::AbstractVector)
245+
# Computes xᵀ × Vᵀ × U
246+
η = size(U, 1)
247+
if η == 0
248+
y .= x
249+
return nothing
250+
end
251+
mul!(xᵀVᵀ[:, 1:η], x', Vᵀ)
252+
mul!(y', xᵀVᵀ[:, 1:η], U)
253+
return nothing
254+
end
255+
256+
@views function __lbroyden_rmatvec(U::AbstractMatrix, Vᵀ::AbstractMatrix, x::AbstractVector)
257+
# Computes xᵀ × Vᵀ × U
258+
size(U, 1) == 0 && return x
259+
return (x' * Vᵀ) * U
260+
end

0 commit comments

Comments
 (0)