@@ -380,6 +380,141 @@ function _effort_wants_shrink(nit::Int, budget::Int)
380380 return nit >= 0 && nit <= budget && 4 * nit >= 3 * budget
381381end
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λ = 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 * dλ
462+ dλ = 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 * dλ
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 = dλ / 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(
396531end
397532
398533function 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
410546# fallback cold at `λspan[1]`.
411547function _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 dλ = 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λ = 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 * dλ
645- dλ = 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- dλ = dλ / 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+ dλ = 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