Skip to content

Commit affc2ec

Browse files
Add Kantorovich homotopy continuation
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
1 parent 247a8bf commit affc2ec

10 files changed

Lines changed: 549 additions & 56 deletions

File tree

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "NonlinearSolve"
22
uuid = "8913a72c-1f9b-4ce2-8d82-65094dcecaec"
3-
version = "4.21.0"
3+
version = "4.22.0"
44
authors = ["SciML"]
55

66
[deps]
@@ -98,7 +98,7 @@ NLSolvers = "0.5"
9898
NLsolve = "4.5"
9999
NaNMath = "1"
100100
NonlinearProblemLibrary = "0.1.2"
101-
NonlinearSolveBase = "2.31"
101+
NonlinearSolveBase = "2.36"
102102
NonlinearSolveFirstOrder = "2"
103103
NonlinearSolveQuasiNewton = "1.12"
104104
NonlinearSolveSpectralMethods = "1.6"

docs/src/native/solvers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ LimitedMemoryBroyden
5454

5555
```@docs
5656
HomotopySweep
57+
KantorovichHomotopy
5758
ArcLengthContinuation
5859
HomotopyPolyAlgorithm
5960
```

lib/NonlinearSolveBase/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "NonlinearSolveBase"
22
uuid = "be0214bd-f91f-a760-ac4e-3421ce2b2da0"
3-
version = "2.35.0"
3+
version = "2.36.0"
44
authors = ["Avik Pal <avikpal@mit.edu> and contributors"]
55

66
[deps]

lib/NonlinearSolveBase/src/NonlinearSolveBase.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ include("timer_outputs.jl")
6565
include("tracing.jl")
6666
include("wrappers.jl")
6767
include("polyalg.jl")
68+
include("kantorovich_homotopy.jl")
6869
include("homotopy_sweep.jl")
6970
include("arclength.jl")
7071
include("homotopy_polyalg.jl")
@@ -118,7 +119,7 @@ export DescentResult, SteepestDescent, NewtonDescent, DampedNewtonDescent, Dogle
118119

119120
export NonlinearSolvePolyAlgorithm
120121

121-
export HomotopySweep, ArcLengthContinuation, HomotopyPolyAlgorithm
122+
export HomotopySweep, KantorovichHomotopy, ArcLengthContinuation, HomotopyPolyAlgorithm
122123

123124
export NonlinearVerbosity
124125

lib/NonlinearSolveBase/src/homotopy_polyalg.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ function _rescale_step_caps(stage::HomotopySweep, scale)
145145
msf = stage.max_step_factor
146146
return @set stage.max_step_factor = min(msf * scale, oneunit(msf))
147147
end
148+
function _rescale_step_caps(stage::KantorovichHomotopy, scale)
149+
if stage.nsteps !== nothing
150+
stage = @set stage.nsteps = max(1, ceil(Int, stage.nsteps / scale))
151+
end
152+
msf = stage.max_step_factor
153+
return @set stage.max_step_factor = min(msf * scale, oneunit(msf))
154+
end
148155

149156
function CommonSolve.solve(
150157
prob::SciMLBase.HomotopyProblem{uType, iip}, alg::HomotopyPolyAlgorithm,
@@ -186,7 +193,7 @@ function CommonSolve.solve(
186193
# through to the cold full-range attempt so the handoff never costs
187194
# robustness relative to the plain staged behavior.
188195
end
189-
sol = if stage isa HomotopySweep
196+
sol = if stage isa Union{HomotopySweep, KantorovichHomotopy}
190197
csol, λ_last = _homotopy_sweep_solve(prob, stage, args...; kwargs...)
191198
if λ_last !== nothing
192199
# The sweep dies where the path turns hard (a fold), and a fallback

lib/NonlinearSolveBase/src/homotopy_sweep.jl

Lines changed: 173 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,141 @@ function _effort_wants_shrink(nit::Int, budget::Int)
380380
return nit >= 0 && nit <= budget && 4 * nit >= 3 * budget
381381
end
382382

383+
function _homotopy_corrector!(cache, ::HomotopySweep, ::Type{T}) where {T}
384+
sol = CommonSolve.solve!(cache)
385+
return sol, zero(T), zero(T), false, false
386+
end
387+
388+
function _homotopy_corrector!(
389+
cache::NonlinearSolvePolyAlgorithmCache, ::KantorovichHomotopy, ::Type{T}
390+
) where {T}
391+
sol = CommonSolve.solve!(cache)
392+
return sol, zero(T), zero(T), false, false
393+
end
394+
395+
function _homotopy_corrector!(
396+
cache, alg::KantorovichHomotopy, ::Type{T}
397+
) where {T}
398+
# No-init algorithms do not expose intermediate residuals or a step interface.
399+
if !hasfield(typeof(cache), :fu) || cache.retcode == ReturnCode.InitialFailure
400+
sol = CommonSolve.solve!(cache)
401+
return sol, zero(T), zero(T), false, false
402+
end
403+
404+
residualnormprev = zero(L2_NORM(get_fu(cache)))
405+
observations = 0
406+
first_Θ = zero(T)
407+
rejected_Θ = zero(T)
408+
has_Θ = false
409+
contraction_rejected = false
410+
Θreject = T(alg.Θreject)
411+
412+
while not_terminated(cache)
413+
CommonSolve.step!(cache)
414+
residualnorm = L2_NORM(get_fu(cache))
415+
if observations > 0
416+
Θ = iszero(residualnormprev) ? zero(T) : T(residualnorm / residualnormprev)
417+
if !has_Θ
418+
first_Θ = Θ
419+
has_Θ = true
420+
end
421+
if !contraction_rejected && !<= Θreject)
422+
rejected_Θ = Θ
423+
contraction_rejected = true
424+
end
425+
end
426+
residualnormprev = residualnorm
427+
observations += 1
428+
end
429+
430+
sol = CommonSolve.solve!(cache)
431+
return sol, first_Θ, rejected_Θ, has_Θ, contraction_rejected
432+
end
433+
434+
435+
function _kantorovich_step_factor(
436+
alg::KantorovichHomotopy, Θ, has_Θ::Bool, ::Type{T}
437+
) where {T}
438+
has_Θ && (!isfinite(Θ) || Θ < 0) && return T(alg.qmin)
439+
Θeff = has_Θ ? max(T(Θ), T(alg.Θmin)) : T(alg.Θmin)
440+
g(x) = sqrt(one(T) + 4 * x) - one(T)
441+
q = T(alg.γ) * (g(T(alg.Θbar)) / g(Θeff))^(inv(T(alg.predictor_order)))
442+
return clamp(q, T(alg.qmin), T(alg.qmax))
443+
end
444+
445+
function _accepted_step_size(
446+
alg::HomotopySweep, dλ, streak::Int, θ, nit::Int, budget::Int,
447+
min_dλ, max_dλ, expand_factor, ::Type{T}, first_Θ, has_Θ
448+
) where {T}
449+
alg.adaptive || return dλ, streak
450+
if _effort_wants_shrink(nit, budget)
451+
abs(dλ) / 2 >= min_dλ && (dλ =/ 2)
452+
return dλ, 0
453+
end
454+
455+
streak += 1
456+
corrector_cheap = nit >= 0 && nit <= 2
457+
if streak >= alg.expand_threshold &&
458+
=== nothing || θ <= T(alg.expand_quality) || corrector_cheap)
459+
growth = _effort_growth_factor(nit, budget, expand_factor)
460+
if growth > 1
461+
grown = growth *
462+
= abs(grown) > abs(max_dλ) ? max_dλ : grown
463+
streak = 0
464+
end
465+
end
466+
return dλ, streak
467+
end
468+
469+
function _accepted_step_size(
470+
alg::KantorovichHomotopy, dλ, streak::Int, θ, nit::Int, budget::Int,
471+
min_dλ, max_dλ, expand_factor, ::Type{T}, first_Θ, has_Θ
472+
) where {T}
473+
q = _kantorovich_step_factor(alg, first_Θ, has_Θ, T)
474+
corrector_cheap = nit >= 0 && nit <= 2
475+
if q > 1 && !=== nothing || θ <= T(alg.expand_quality) || corrector_cheap)
476+
q = one(T)
477+
end
478+
proposed = q *
479+
if abs(proposed) > abs(max_dλ)
480+
proposed = max_dλ
481+
elseif abs(proposed) < min_dλ
482+
proposed = sign(dλ) * min_dλ
483+
end
484+
return proposed, 0
485+
end
486+
487+
function _rejected_step_size(
488+
alg::HomotopySweep, dλ, attempted_dλ, min_dλ,
489+
first_Θ, rejected_Θ, has_Θ, contraction_rejected, inner_success,
490+
::Type{T}
491+
) where {T}
492+
alg.adaptive || return dλ, false
493+
proposed =/ 2
494+
return proposed, abs(proposed) >= min_dλ
495+
end
496+
497+
function _rejected_step_size(
498+
alg::KantorovichHomotopy, dλ, attempted_dλ, min_dλ,
499+
first_Θ, rejected_Θ, has_Θ, contraction_rejected, inner_success,
500+
::Type{T}
501+
) where {T}
502+
q = if inner_success && contraction_rejected && isfinite(rejected_Θ) && rejected_Θ >= 0
503+
_kantorovich_step_factor(alg, rejected_Θ, true, T)
504+
else
505+
T(alg.qmin)
506+
end
507+
proposed = q * attempted_dλ
508+
return proposed, abs(proposed) >= min_dλ && !iszero(proposed)
509+
end
510+
511+
_strict_contraction_rejection(::HomotopySweep, contraction_rejected) = false
512+
function _strict_contraction_rejection(
513+
alg::KantorovichHomotopy, contraction_rejected
514+
)
515+
return alg.strict && contraction_rejected
516+
end
517+
383518
# Full-fidelity standalone solve at a fixed λ, with the user's original kwargs (full
384519
# iteration budget, full tolerances) outside the tracking cache. Used only where the
385520
# tracking cap / loose tracking tolerance are active but must not bind: the λspan[1]
@@ -396,7 +531,8 @@ function _sweep_exempt_solve(
396531
end
397532

398533
function CommonSolve.solve(
399-
prob::SciMLBase.HomotopyProblem, alg::HomotopySweep, args...; kwargs...
534+
prob::SciMLBase.HomotopyProblem,
535+
alg::Union{HomotopySweep, KantorovichHomotopy}, args...; kwargs...
400536
)
401537
sol, _ = _homotopy_sweep_solve(prob, alg, args...; kwargs...)
402538
return sol
@@ -410,7 +546,7 @@ end
410546
# fallback cold at `λspan[1]`.
411547
function _homotopy_sweep_solve(
412548
prob::SciMLBase.HomotopyProblem{uType, iip},
413-
alg::HomotopySweep, args...; kwargs...
549+
alg::Union{HomotopySweep, KantorovichHomotopy}, args...; kwargs...
414550
) where {uType, iip}
415551
λ0, λ1 = prob.λspan
416552
λ = float(λ0)
@@ -420,7 +556,7 @@ function _homotopy_sweep_solve(
420556
= alg.nsteps === nothing ? λT(alg.initial_step_factor) * span : span / alg.nsteps
421557
min_dλ = alg.min_dλ === nothing ? sqrt(eps(λT)) : λT(alg.min_dλ)
422558
max_dλ = λT(alg.max_step_factor) * span # carries span's sign, like dλ
423-
expand_factor = λT(alg.expand_factor)
559+
expand_factor = alg isa HomotopySweep ? λT(alg.expand_factor) : one(λT)
424560
abs(dλ) > abs(max_dλ) && (dλ = max_dλ)
425561
u = copy(prob.u0)
426562
budget, cap_active = _tracking_budget(alg.tracking_maxiters, prob.kwargs, kwargs)
@@ -553,7 +689,8 @@ function _homotopy_sweep_solve(
553689
# won the previous solve (the anchor's full-ladder run discovers the winner)
554690
# instead of re-failing the cheaper ladder members on every warm-started step.
555691
reinit_retaining!(cache, guess)
556-
last_sol = CommonSolve.solve!(cache)
692+
last_sol, first_Θ, rejected_Θ, has_Θ, contraction_rejected =
693+
_homotopy_corrector!(cache, alg, λT)
557694

558695
if next_λ == λend
559696
if cap_active && !SciMLBase.successful_retcode(last_sol)
@@ -571,6 +708,10 @@ function _homotopy_sweep_solve(
571708
last_sol = _sweep_exempt_solve(
572709
prob, alg.inner, retry_guess, next_λ, args...; kwargs...
573710
)
711+
first_Θ = zero(λT)
712+
rejected_Θ = zero(λT)
713+
has_Θ = false
714+
contraction_rejected = false
574715
elseif tol_active && SciMLBase.successful_retcode(last_sol)
575716
# The landing is exempt from the loose tracking tolerance: the loose
576717
# cache solve above did the bulk of the convergence, now re-polish at
@@ -583,7 +724,9 @@ function _homotopy_sweep_solve(
583724
end
584725
end
585726

586-
if SciMLBase.successful_retcode(last_sol)
727+
inner_success = SciMLBase.successful_retcode(last_sol)
728+
strict_rejection = _strict_contraction_rejection(alg, contraction_rejected)
729+
if inner_success && !strict_rejection
587730
# The secant prediction error θ (relative to the recent solution movement)
588731
# is a cheap local error estimate: it grows with the path's curvature times
589732
# dλ², so a large θ means the path is turning and a stale tangent would
@@ -615,52 +758,32 @@ function _homotopy_sweep_solve(
615758
λ_prev = λ
616759
λ = next_λ
617760
λ == λend && break
618-
if alg.adaptive
619-
nit = last_sol.stats === nothing ? -1 : Int(last_sol.stats.nsteps)
620-
if _effort_wants_shrink(nit, budget)
621-
# Proactive shrink on a straining success (see
622-
# `_effort_wants_shrink`); the floor guard keeps the increment
623-
# from dropping below what bisection itself may reach.
624-
abs(dλ) / 2 >= min_dλ && (dλ =/ 2)
625-
streak = 0
626-
else
627-
streak += 1
628-
# Growth requires a streak of successes (the classic heuristic)
629-
# plus evidence the corrector has headroom: either a small relative
630-
# prediction error (the quality gate) or a corrector that converged
631-
# almost immediately. The iteration count covers paths that flatten
632-
# exponentially, where θ stays at a constant mediocre value while
633-
# the absolute corrections — and hence the corrector work — become
634-
# negligible. The growth factor itself is scaled by the corrector's
635-
# effort (see `_effort_growth_factor`), so the gates veto growth
636-
# and the effort bands size it. The streak is NOT reset on a vetoed
637-
# or held expansion, so growth resumes on the first step whose
638-
# evidence recovers.
639-
corrector_cheap = nit >= 0 && nit <= 2
640-
if streak >= alg.expand_threshold &&
641-
=== nothing || θ <= λT(alg.expand_quality) || corrector_cheap)
642-
g = _effort_growth_factor(nit, budget, expand_factor)
643-
if g > 1
644-
grown = g *
645-
= abs(grown) > abs(max_dλ) ? max_dλ : grown
646-
streak = 0
647-
end
648-
end
649-
end
650-
end
651-
elseif alg.adaptive && abs(dλ) / 2 >= min_dλ
652-
=/ 2 # bisect; retry from the same λ (do not advance)
653-
streak = 0
654-
# a rejected step is evidence against the tangent: bisection retries (and
655-
# the steps right after) warm-start constantly until quality re-accumulates
656-
trust = 0
761+
nit = last_sol.stats === nothing ? -1 : Int(last_sol.stats.nsteps)
762+
dλ, streak = _accepted_step_size(
763+
alg, dλ, streak, θ, nit, budget, min_dλ, max_dλ,
764+
expand_factor, λT, first_Θ, has_Θ
765+
)
657766
else
658-
# on failure: u is the last converged iterate (λ<λ1); resid is from the failed step (advisory)
659-
return build_solution_less_specialize(
660-
prob, alg, u, last_sol.resid;
661-
retcode = last_sol.retcode, original = last_sol,
662-
store_original = alg.store_original
663-
), λ
767+
attempted_dλ = next_λ - λ
768+
proposed_dλ, can_retry = _rejected_step_size(
769+
alg, dλ, attempted_dλ, min_dλ, first_Θ, rejected_Θ,
770+
has_Θ, contraction_rejected, inner_success, λT
771+
)
772+
if can_retry
773+
= proposed_dλ
774+
streak = 0
775+
# a rejected step is evidence against the tangent: bisection retries (and
776+
# the steps right after) warm-start constantly until quality re-accumulates
777+
trust = 0
778+
else
779+
# on failure: u is the last converged iterate (λ<λ1); resid is from the failed step (advisory)
780+
retcode = strict_rejection ? ReturnCode.ConvergenceFailure : last_sol.retcode
781+
return build_solution_less_specialize(
782+
prob, alg, u, last_sol.resid;
783+
retcode, original = last_sol,
784+
store_original = alg.store_original
785+
), λ
786+
end
664787
end
665788
end
666789

0 commit comments

Comments
 (0)