|
| 1 | +""" |
| 2 | + DFSane(; σ_min::Real = 1e-10, σ_max::Real = 1e10, σ_1::Real = 1.0, |
| 3 | + M::Int = 10, γ::Real = 1e-4, τ_min::Real = 0.1, τ_max::Real = 0.5, |
| 4 | + n_exp::Int = 2, η_strategy::Function = (fn_1, n, x_n, f_n) -> fn_1 / n^2, |
| 5 | + max_inner_iterations::Int = 1000) |
| 6 | +
|
| 7 | +A low-overhead and allocation-free implementation of the df-sane method for solving large-scale nonlinear |
| 8 | +systems of equations. For in depth information about all the parameters and the algorithm, |
| 9 | +see the paper: [W LaCruz, JM Martinez, and M Raydan (2006), Spectral residual mathod without |
| 10 | +gradient information for solving large-scale nonlinear systems of equations, Mathematics of |
| 11 | +Computation, 75, 1429-1448.](https://www.researchgate.net/publication/220576479_Spectral_Residual_Method_without_Gradient_Information_for_Solving_Large-Scale_Nonlinear_Systems_of_Equations) |
| 12 | +
|
| 13 | +See also the implementation in [SimpleNonlinearSolve.jl](https://github.com/SciML/SimpleNonlinearSolve.jl/blob/main/src/dfsane.jl) |
| 14 | +
|
| 15 | +### Keyword Arguments |
| 16 | +
|
| 17 | +- `σ_min`: the minimum value of the spectral coefficient `σₙ` which is related to the step |
| 18 | + size in the algorithm. Defaults to `1e-10`. |
| 19 | +- `σ_max`: the maximum value of the spectral coefficient `σₙ` which is related to the step |
| 20 | + size in the algorithm. Defaults to `1e10`. |
| 21 | +- `σ_1`: the initial value of the spectral coefficient `σₙ` which is related to the step |
| 22 | + size in the algorithm.. Defaults to `1.0`. |
| 23 | +- `M`: The monotonicity of the algorithm is determined by a this positive integer. |
| 24 | + A value of 1 for `M` would result in strict monotonicity in the decrease of the L2-norm |
| 25 | + of the function `f`. However, higher values allow for more flexibility in this reduction. |
| 26 | + Despite this, the algorithm still ensures global convergence through the use of a |
| 27 | + non-monotone line-search algorithm that adheres to the Grippo-Lampariello-Lucidi |
| 28 | + condition. Values in the range of 5 to 20 are usually sufficient, but some cases may call |
| 29 | + for a higher value of `M`. The default setting is 10. |
| 30 | +- `γ`: a parameter that influences if a proposed step will be accepted. Higher value of `γ` |
| 31 | + will make the algorithm more restrictive in accepting steps. Defaults to `1e-4`. |
| 32 | +- `τ_min`: if a step is rejected the new step size will get multiplied by factor, and this |
| 33 | + parameter is the minimum value of that factor. Defaults to `0.1`. |
| 34 | +- `τ_max`: if a step is rejected the new step size will get multiplied by factor, and this |
| 35 | + parameter is the maximum value of that factor. Defaults to `0.5`. |
| 36 | +- `n_exp`: the exponent of the loss, i.e. ``f_n=||F(x_n)||^{n_exp}``. The paper uses |
| 37 | + `n_exp ∈ {1,2}`. Defaults to `2`. |
| 38 | +- `η_strategy`: function to determine the parameter `η`, which enables growth |
| 39 | + of ``||f_n||^2``. Called as ``η = η_strategy(fn_1, n, x_n, f_n)`` with `fn_1` initialized as |
| 40 | + ``fn_1=||f(x_1)||^{n_exp}``, `n` is the iteration number, `x_n` is the current `x`-value and |
| 41 | + `f_n` the current residual. Should satisfy ``η > 0`` and ``∑ₖ ηₖ < ∞``. Defaults to |
| 42 | + ``fn_1 / n^2``. |
| 43 | +- `max_inner_iterations`: the maximum number of iterations allowed for the inner loop of the |
| 44 | + algorithm. Defaults to `1000`. |
| 45 | +""" |
| 46 | + |
| 47 | +struct DFSane{T, F} <: AbstractNonlinearSolveAlgorithm |
| 48 | + σ_min::T |
| 49 | + σ_max::T |
| 50 | + σ_1::T |
| 51 | + M::Int |
| 52 | + γ::T |
| 53 | + τ_min::T |
| 54 | + τ_max::T |
| 55 | + n_exp::Int |
| 56 | + η_strategy::F |
| 57 | + max_inner_iterations::Int |
| 58 | +end |
| 59 | + |
| 60 | +function DFSane(; σ_min = 1e-10, |
| 61 | + σ_max = 1e+10, |
| 62 | + σ_1 = 1.0, |
| 63 | + M = 10, |
| 64 | + γ = 1e-4, |
| 65 | + τ_min = 0.1, |
| 66 | + τ_max = 0.5, |
| 67 | + n_exp = 2, |
| 68 | + η_strategy = (fn_1, n, x_n, f_n) -> fn_1 / n^2, |
| 69 | + max_inner_iterations = 1000) |
| 70 | + return DFSane{typeof(σ_min), typeof(η_strategy)}(σ_min, |
| 71 | + σ_max, |
| 72 | + σ_1, |
| 73 | + M, |
| 74 | + γ, |
| 75 | + τ_min, |
| 76 | + τ_max, |
| 77 | + n_exp, |
| 78 | + η_strategy, |
| 79 | + max_inner_iterations) |
| 80 | +end |
| 81 | +mutable struct DFSaneCache{iip, algType, uType, resType, T, pType, |
| 82 | + INType, |
| 83 | + tolType, |
| 84 | + probType} |
| 85 | + f::Function |
| 86 | + alg::algType |
| 87 | + uₙ::uType |
| 88 | + uₙ₋₁::uType |
| 89 | + fuₙ::resType |
| 90 | + fuₙ₋₁::resType |
| 91 | + 𝒹::uType |
| 92 | + ℋ::Vector{T} |
| 93 | + f₍ₙₒᵣₘ₎ₙ₋₁::T |
| 94 | + f₍ₙₒᵣₘ₎₀::T |
| 95 | + M::Int |
| 96 | + σₙ::T |
| 97 | + σₘᵢₙ::T |
| 98 | + σₘₐₓ::T |
| 99 | + α₁::T |
| 100 | + γ::T |
| 101 | + τₘᵢₙ::T |
| 102 | + τₘₐₓ::T |
| 103 | + nₑₓₚ::Int |
| 104 | + p::pType |
| 105 | + force_stop::Bool |
| 106 | + maxiters::Int |
| 107 | + internalnorm::INType |
| 108 | + retcode::SciMLBase.ReturnCode.T |
| 109 | + abstol::tolType |
| 110 | + prob::probType |
| 111 | + stats::NLStats |
| 112 | + function DFSaneCache{iip}(f::Function, alg::algType, uₙ::uType, uₙ₋₁::uType, |
| 113 | + fuₙ::resType, fuₙ₋₁::resType, 𝒹::uType, ℋ::Vector{T}, |
| 114 | + f₍ₙₒᵣₘ₎ₙ₋₁::T, f₍ₙₒᵣₘ₎₀::T, M::Int, σₙ::T, σₘᵢₙ::T, σₘₐₓ::T, |
| 115 | + α₁::T, γ::T, τₘᵢₙ::T, τₘₐₓ::T, nₑₓₚ::Int, p::pType, |
| 116 | + force_stop::Bool, maxiters::Int, internalnorm::INType, |
| 117 | + retcode::SciMLBase.ReturnCode.T, abstol::tolType, |
| 118 | + prob::probType, |
| 119 | + stats::NLStats) where {iip, algType, uType, |
| 120 | + resType, T, pType, INType, |
| 121 | + tolType, |
| 122 | + probType |
| 123 | + } |
| 124 | + new{iip, algType, uType, resType, T, pType, INType, tolType, |
| 125 | + probType |
| 126 | + }(f, alg, uₙ, uₙ₋₁, fuₙ, fuₙ₋₁, 𝒹, ℋ, f₍ₙₒᵣₘ₎ₙ₋₁, f₍ₙₒᵣₘ₎₀, M, σₙ, |
| 127 | + σₘᵢₙ, σₘₐₓ, α₁, γ, τₘᵢₙ, |
| 128 | + τₘₐₓ, nₑₓₚ, p, force_stop, maxiters, internalnorm, |
| 129 | + retcode, |
| 130 | + abstol, prob, stats) |
| 131 | + end |
| 132 | +end |
| 133 | + |
| 134 | +function SciMLBase.__init(prob::NonlinearProblem{uType, iip}, alg::DFSane, |
| 135 | + args...; |
| 136 | + alias_u0 = false, |
| 137 | + maxiters = 1000, |
| 138 | + abstol = 1e-6, |
| 139 | + internalnorm = DEFAULT_NORM, |
| 140 | + kwargs...) where {uType, iip} |
| 141 | + if alias_u0 |
| 142 | + uₙ = prob.u0 |
| 143 | + else |
| 144 | + uₙ = deepcopy(prob.u0) |
| 145 | + end |
| 146 | + |
| 147 | + p = prob.p |
| 148 | + T = eltype(uₙ) |
| 149 | + σₘᵢₙ, σₘₐₓ, γ, τₘᵢₙ, τₘₐₓ = T(alg.σ_min), T(alg.σ_max), T(alg.γ), T(alg.τ_min), |
| 150 | + T(alg.τ_max) |
| 151 | + α₁ = one(T) |
| 152 | + γ = T(alg.γ) |
| 153 | + f₍ₙₒᵣₘ₎ₙ₋₁ = α₁ |
| 154 | + σₙ = T(alg.σ_1) |
| 155 | + M = alg.M |
| 156 | + nₑₓₚ = alg.n_exp |
| 157 | + 𝒹, uₙ₋₁, fuₙ, fuₙ₋₁ = copy(uₙ), copy(uₙ), copy(uₙ), copy(uₙ) |
| 158 | + |
| 159 | + if iip |
| 160 | + f = (dx, x) -> prob.f(dx, x, p) |
| 161 | + f(fuₙ₋₁, uₙ₋₁) |
| 162 | + else |
| 163 | + f = (x) -> prob.f(x, p) |
| 164 | + fuₙ₋₁ = f(uₙ₋₁) |
| 165 | + end |
| 166 | + |
| 167 | + f₍ₙₒᵣₘ₎ₙ₋₁ = norm(fuₙ₋₁)^nₑₓₚ |
| 168 | + f₍ₙₒᵣₘ₎₀ = f₍ₙₒᵣₘ₎ₙ₋₁ |
| 169 | + |
| 170 | + ℋ = fill(f₍ₙₒᵣₘ₎ₙ₋₁, M) |
| 171 | + return DFSaneCache{iip}(f, alg, uₙ, uₙ₋₁, fuₙ, fuₙ₋₁, 𝒹, ℋ, f₍ₙₒᵣₘ₎ₙ₋₁, f₍ₙₒᵣₘ₎₀, |
| 172 | + M, σₙ, σₘᵢₙ, σₘₐₓ, α₁, γ, τₘᵢₙ, |
| 173 | + τₘₐₓ, nₑₓₚ, p, false, maxiters, |
| 174 | + internalnorm, ReturnCode.Default, abstol, prob, |
| 175 | + NLStats(1, 0, 0, 0, 0)) |
| 176 | +end |
| 177 | + |
| 178 | +function perform_step!(cache::DFSaneCache{true}) |
| 179 | + @unpack f, alg, f₍ₙₒᵣₘ₎ₙ₋₁, f₍ₙₒᵣₘ₎₀, |
| 180 | + σₙ, σₘᵢₙ, σₘₐₓ, α₁, γ, τₘᵢₙ, τₘₐₓ, nₑₓₚ, M = cache |
| 181 | + |
| 182 | + T = eltype(cache.uₙ) |
| 183 | + n = cache.stats.nsteps |
| 184 | + |
| 185 | + # Spectral parameter range check |
| 186 | + σₙ = sign(σₙ) * clamp(abs(σₙ), σₘᵢₙ, σₘₐₓ) |
| 187 | + |
| 188 | + # Line search direction |
| 189 | + @. cache.𝒹 = -σₙ * cache.fuₙ₋₁ |
| 190 | + |
| 191 | + η = alg.η_strategy(f₍ₙₒᵣₘ₎₀, n, cache.uₙ₋₁, cache.fuₙ₋₁) |
| 192 | + |
| 193 | + f̄ = maximum(cache.ℋ) |
| 194 | + α₊ = α₁ |
| 195 | + α₋ = α₁ |
| 196 | + @. cache.uₙ = cache.uₙ₋₁ + α₊ * cache.𝒹 |
| 197 | + |
| 198 | + f(cache.fuₙ, cache.uₙ) |
| 199 | + f₍ₙₒᵣₘ₎ₙ = norm(cache.fuₙ)^nₑₓₚ |
| 200 | + for _ in 1:(cache.alg.max_inner_iterations) |
| 201 | + 𝒸 = f̄ + η - γ * α₊^2 * f₍ₙₒᵣₘ₎ₙ₋₁ |
| 202 | + |
| 203 | + f₍ₙₒᵣₘ₎ₙ ≤ 𝒸 && break |
| 204 | + |
| 205 | + α₊ = clamp(α₊^2 * f₍ₙₒᵣₘ₎ₙ₋₁ / |
| 206 | + (f₍ₙₒᵣₘ₎ₙ + (T(2) * α₊ - T(1)) * f₍ₙₒᵣₘ₎ₙ₋₁), |
| 207 | + τₘᵢₙ * α₊, |
| 208 | + τₘₐₓ * α₊) |
| 209 | + @. cache.uₙ = cache.uₙ₋₁ - α₋ * cache.𝒹 |
| 210 | + |
| 211 | + f(cache.fuₙ, cache.uₙ) |
| 212 | + f₍ₙₒᵣₘ₎ₙ = norm(cache.fuₙ)^nₑₓₚ |
| 213 | + |
| 214 | + f₍ₙₒᵣₘ₎ₙ .≤ 𝒸 && break |
| 215 | + |
| 216 | + α₋ = clamp(α₋^2 * f₍ₙₒᵣₘ₎ₙ₋₁ / (f₍ₙₒᵣₘ₎ₙ + (T(2) * α₋ - T(1)) * f₍ₙₒᵣₘ₎ₙ₋₁), |
| 217 | + τₘᵢₙ * α₋, |
| 218 | + τₘₐₓ * α₋) |
| 219 | + |
| 220 | + @. cache.uₙ = cache.uₙ₋₁ + α₊ * cache.𝒹 |
| 221 | + f(cache.fuₙ, cache.uₙ) |
| 222 | + f₍ₙₒᵣₘ₎ₙ = norm(cache.fuₙ)^nₑₓₚ |
| 223 | + end |
| 224 | + |
| 225 | + if cache.internalnorm(cache.fuₙ) < cache.abstol |
| 226 | + cache.force_stop = true |
| 227 | + end |
| 228 | + |
| 229 | + # Update spectral parameter |
| 230 | + @. cache.uₙ₋₁ = cache.uₙ - cache.uₙ₋₁ |
| 231 | + @. cache.fuₙ₋₁ = cache.fuₙ - cache.fuₙ₋₁ |
| 232 | + |
| 233 | + α₊ = sum(abs2, cache.uₙ₋₁) |
| 234 | + @. cache.uₙ₋₁ = cache.uₙ₋₁ * cache.fuₙ₋₁ |
| 235 | + α₋ = sum(cache.uₙ₋₁) |
| 236 | + cache.σₙ = α₊ / α₋ |
| 237 | + |
| 238 | + # Spectral parameter bounds check |
| 239 | + if abs(cache.σₙ) > σₘₐₓ || abs(cache.σₙ) < σₘᵢₙ |
| 240 | + test_norm = sqrt(sum(abs2, cache.fuₙ₋₁)) |
| 241 | + cache.σₙ = clamp(1.0 / test_norm, 1, 1e5) |
| 242 | + end |
| 243 | + |
| 244 | + # Take step |
| 245 | + @. cache.uₙ₋₁ = cache.uₙ |
| 246 | + @. cache.fuₙ₋₁ = cache.fuₙ |
| 247 | + cache.f₍ₙₒᵣₘ₎ₙ₋₁ = f₍ₙₒᵣₘ₎ₙ |
| 248 | + |
| 249 | + # Update history |
| 250 | + cache.ℋ[n % M + 1] = f₍ₙₒᵣₘ₎ₙ |
| 251 | + cache.stats.nf += 1 |
| 252 | + return nothing |
| 253 | +end |
| 254 | + |
| 255 | +function perform_step!(cache::DFSaneCache{false}) |
| 256 | + @unpack f, alg, f₍ₙₒᵣₘ₎ₙ₋₁, f₍ₙₒᵣₘ₎₀, |
| 257 | + σₙ, σₘᵢₙ, σₘₐₓ, α₁, γ, τₘᵢₙ, τₘₐₓ, nₑₓₚ, M = cache |
| 258 | + |
| 259 | + T = eltype(cache.uₙ) |
| 260 | + n = cache.stats.nsteps |
| 261 | + |
| 262 | + # Spectral parameter range check |
| 263 | + σₙ = sign(σₙ) * clamp(abs(σₙ), σₘᵢₙ, σₘₐₓ) |
| 264 | + |
| 265 | + # Line search direction |
| 266 | + cache.𝒹 = -σₙ * cache.fuₙ₋₁ |
| 267 | + |
| 268 | + η = alg.η_strategy(f₍ₙₒᵣₘ₎₀, n, cache.uₙ₋₁, cache.fuₙ₋₁) |
| 269 | + |
| 270 | + f̄ = maximum(cache.ℋ) |
| 271 | + α₊ = α₁ |
| 272 | + α₋ = α₁ |
| 273 | + cache.uₙ = cache.uₙ₋₁ + α₊ * cache.𝒹 |
| 274 | + |
| 275 | + cache.fuₙ = f(cache.uₙ) |
| 276 | + f₍ₙₒᵣₘ₎ₙ = norm(cache.fuₙ)^nₑₓₚ |
| 277 | + for _ in 1:(cache.alg.max_inner_iterations) |
| 278 | + 𝒸 = f̄ + η - γ * α₊^2 * f₍ₙₒᵣₘ₎ₙ₋₁ |
| 279 | + |
| 280 | + f₍ₙₒᵣₘ₎ₙ ≤ 𝒸 && break |
| 281 | + |
| 282 | + α₊ = clamp(α₊^2 * f₍ₙₒᵣₘ₎ₙ₋₁ / |
| 283 | + (f₍ₙₒᵣₘ₎ₙ + (T(2) * α₊ - T(1)) * f₍ₙₒᵣₘ₎ₙ₋₁), |
| 284 | + τₘᵢₙ * α₊, |
| 285 | + τₘₐₓ * α₊) |
| 286 | + cache.uₙ = @. cache.uₙ₋₁ - α₋ * cache.𝒹 |
| 287 | + |
| 288 | + cache.fuₙ = f(cache.uₙ) |
| 289 | + f₍ₙₒᵣₘ₎ₙ = norm(cache.fuₙ)^nₑₓₚ |
| 290 | + |
| 291 | + f₍ₙₒᵣₘ₎ₙ .≤ 𝒸 && break |
| 292 | + |
| 293 | + α₋ = clamp(α₋^2 * f₍ₙₒᵣₘ₎ₙ₋₁ / (f₍ₙₒᵣₘ₎ₙ + (T(2) * α₋ - T(1)) * f₍ₙₒᵣₘ₎ₙ₋₁), |
| 294 | + τₘᵢₙ * α₋, |
| 295 | + τₘₐₓ * α₋) |
| 296 | + |
| 297 | + cache.uₙ = @. cache.uₙ₋₁ + α₊ * cache.𝒹 |
| 298 | + cache.fuₙ = f(cache.uₙ) |
| 299 | + f₍ₙₒᵣₘ₎ₙ = norm(cache.fuₙ)^nₑₓₚ |
| 300 | + end |
| 301 | + |
| 302 | + if cache.internalnorm(cache.fuₙ) < cache.abstol |
| 303 | + cache.force_stop = true |
| 304 | + end |
| 305 | + |
| 306 | + # Update spectral parameter |
| 307 | + cache.uₙ₋₁ = @. cache.uₙ - cache.uₙ₋₁ |
| 308 | + cache.fuₙ₋₁ = @. cache.fuₙ - cache.fuₙ₋₁ |
| 309 | + |
| 310 | + α₊ = sum(abs2, cache.uₙ₋₁) |
| 311 | + cache.uₙ₋₁ = @. cache.uₙ₋₁ * cache.fuₙ₋₁ |
| 312 | + α₋ = sum(cache.uₙ₋₁) |
| 313 | + cache.σₙ = α₊ / α₋ |
| 314 | + |
| 315 | + # Spectral parameter bounds check |
| 316 | + if abs(cache.σₙ) > σₘₐₓ || abs(cache.σₙ) < σₘᵢₙ |
| 317 | + test_norm = sqrt(sum(abs2, cache.fuₙ₋₁)) |
| 318 | + cache.σₙ = clamp(1.0 / test_norm, 1, 1e5) |
| 319 | + end |
| 320 | + |
| 321 | + # Take step |
| 322 | + cache.uₙ₋₁ = cache.uₙ |
| 323 | + cache.fuₙ₋₁ = cache.fuₙ |
| 324 | + cache.f₍ₙₒᵣₘ₎ₙ₋₁ = f₍ₙₒᵣₘ₎ₙ |
| 325 | + |
| 326 | + # Update history |
| 327 | + cache.ℋ[n % M + 1] = f₍ₙₒᵣₘ₎ₙ |
| 328 | + cache.stats.nf += 1 |
| 329 | + return nothing |
| 330 | +end |
| 331 | + |
| 332 | +function SciMLBase.solve!(cache::DFSaneCache) |
| 333 | + while !cache.force_stop && cache.stats.nsteps < cache.maxiters |
| 334 | + cache.stats.nsteps += 1 |
| 335 | + perform_step!(cache) |
| 336 | + end |
| 337 | + |
| 338 | + if cache.stats.nsteps == cache.maxiters |
| 339 | + cache.retcode = ReturnCode.MaxIters |
| 340 | + else |
| 341 | + cache.retcode = ReturnCode.Success |
| 342 | + end |
| 343 | + |
| 344 | + SciMLBase.build_solution(cache.prob, cache.alg, cache.uₙ, cache.fuₙ; |
| 345 | + retcode = cache.retcode, stats = cache.stats) |
| 346 | +end |
| 347 | + |
| 348 | +function SciMLBase.reinit!(cache::DFSaneCache{iip}, u0 = cache.uₙ; p = cache.p, |
| 349 | + abstol = cache.abstol, maxiters = cache.maxiters) where {iip} |
| 350 | + cache.p = p |
| 351 | + if iip |
| 352 | + recursivecopy!(cache.uₙ, u0) |
| 353 | + recursivecopy!(cache.uₙ₋₁, u0) |
| 354 | + cache.f = (dx, x) -> cache.prob.f(dx, x, p) |
| 355 | + cache.f(cache.fuₙ, cache.uₙ) |
| 356 | + cache.f(cache.fuₙ₋₁, cache.uₙ) |
| 357 | + else |
| 358 | + cache.uₙ = u0 |
| 359 | + cache.uₙ₋₁ = u0 |
| 360 | + cache.f = (x) -> cache.prob.f(x, p) |
| 361 | + cache.fuₙ = cache.f(cache.uₙ) |
| 362 | + cache.fuₙ₋₁ = cache.f(cache.uₙ) |
| 363 | + end |
| 364 | + |
| 365 | + cache.f₍ₙₒᵣₘ₎ₙ₋₁ = norm(cache.fuₙ₋₁)^cache.nₑₓₚ |
| 366 | + cache.f₍ₙₒᵣₘ₎₀ = cache.f₍ₙₒᵣₘ₎ₙ₋₁ |
| 367 | + fill!(cache.ℋ, cache.f₍ₙₒᵣₘ₎ₙ₋₁) |
| 368 | + |
| 369 | + T = eltype(cache.uₙ) |
| 370 | + cache.σₙ = T(cache.alg.σ_1) |
| 371 | + |
| 372 | + cache.abstol = abstol |
| 373 | + cache.maxiters = maxiters |
| 374 | + cache.stats.nf = 1 |
| 375 | + cache.stats.nsteps = 1 |
| 376 | + cache.force_stop = false |
| 377 | + cache.retcode = ReturnCode.Default |
| 378 | + return cache |
| 379 | +end |
0 commit comments