From 2859af8dd9d85e3bc1be996a2cbbbd89b8c24eb4 Mon Sep 17 00:00:00 2001 From: Paul Brehmer Date: Fri, 26 Jun 2026 17:15:41 +0200 Subject: [PATCH 1/6] Add `hasconvered` and `shouldstop` kwargs to optimization routine --- .../optimization/peps_optimization.jl | 26 ++++++++++++++----- src/algorithms/select_algorithm.jl | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/algorithms/optimization/peps_optimization.jl b/src/algorithms/optimization/peps_optimization.jl index 5bf49e8bb..190d40ca2 100644 --- a/src/algorithms/optimization/peps_optimization.jl +++ b/src/algorithms/optimization/peps_optimization.jl @@ -109,7 +109,9 @@ The optimization parameters can be supplied via the keyword arguments or directl 4. Debug info including AD outputs * `reuse_env::Bool=$(Defaults.reuse_env)` : If `true`, the current optimization step is initialized on the previous environment, otherwise a random environment is used. * `symmetrization::Union{Nothing,SymmetrizationStyle}=nothing` : Accepts `nothing` or a `SymmetrizationStyle`, in which case the PEPS and PEPS gradient are symmetrized after each optimization iteration. -* `(finalize!)=OptimKit._finalize!` : Inserts a `finalize!` function call after each optimization step by utilizing the `finalize!` kwarg of `OptimKit.optimize`. The function maps `(peps, env), f, g = finalize!((peps, env), f, g, numiter)`. +* `hasconverged=OptimKit.DefaultHasConverged(optimizer_alg.tol)` : Function specifying the convergence criterion with signature `bool = hasconverged(state, cost, grad, gradnorm)`, see [OptimKit.optimize](https://github.com/Jutho/OptimKit.jl/blob/master/src/OptimKit.jl) for specifics. +* `shouldstop=OptimKit.DefaultShouldStop(optimizer_alg.maxiter)` : Function specifying the stopping criterion with signature `bool = shouldstop(state, cost, grad, numfg, iter, timespent)`, see [OptimKit.optimize](https://github.com/Jutho/OptimKit.jl/blob/master/src/OptimKit.jl) for specifics. +* `(finalize!)=OptimKit._finalize!` : Inserts a `finalize!` function call after each optimization step by utilizing the `finalize!` kwarg of `OptimKit.optimize`. The function maps `(state, env), f, g = finalize!((state, env), cost, grad, numiter)`. ### Boundary algorithm @@ -131,7 +133,7 @@ keyword arguments are: * `verbosity::Int` : Gradient output verbosity, ≤0 by default to disable too verbose printing. Should only be >0 for debug purposes. * `alg::Symbol=:$(Defaults.gradient_alg)` : Implicit gradient algorithm variant, can be one of the following: - `:FixedPointGradient` : Compute the gradient via fixed-point differentiation, see [`FixedPointGradient`](@ref) -* `solver_alg::`Union{Algorithm,NamedTuple}`: Solver algorithm for computing the implicit gradient; see [`FixedPointGradient`](@ref) for supported algorithms. +* `solver_alg::Union{Algorithm,NamedTuple}`: Solver algorithm for computing the implicit gradient; see [`FixedPointGradient`](@ref) for supported algorithms. ### Optimizer settings @@ -165,13 +167,24 @@ information `NamedTuple` which contains the following entries: * `times` : History of optimization step execution times. """ function fixedpoint( - operator, peps₀::InfinitePEPS, env₀; (finalize!) = OptimKit._finalize!, kwargs..., + operator, peps₀::InfinitePEPS, env₀; + (finalize!) = OptimKit._finalize!, + hasconverged = nothing, shouldstop = nothing, kwargs..., ) + # select OptimizationAlgorithm from kwargs alg = select_algorithm(fixedpoint, env₀; kwargs...) - return fixedpoint(operator, peps₀, env₀, alg; finalize!) + + # default stopping criteria initialized on alg parameters that have to be select first + isnothing(hasconverged) && (hasconverged = OptimKit.DefaultHasConverged(alg.optimizer_alg.tol)) + isnothing(shouldstop) && (shouldstop = OptimKit.DefaultShouldStop(alg.optimizer_alg.maxiter)) + + return fixedpoint(operator, peps₀, env₀, alg; finalize!, hasconverged, shouldstop) end function fixedpoint( - operator, peps₀::InfinitePEPS, env₀, alg::PEPSOptimize; (finalize!) = OptimKit._finalize!, + operator, peps₀::InfinitePEPS, env₀, alg::PEPSOptimize; + (finalize!) = OptimKit._finalize!, + hasconverged = OptimKit.DefaultHasConverged(alg.optimizer_alg.tol), + shouldstop = OptimKit.DefaultShouldStop(alg.optimizer_alg.maxiter), ) # validate inputs check_input(fixedpoint, peps₀, env₀, alg) @@ -197,7 +210,8 @@ function fixedpoint( # optimize operator cost function (peps_final, env_final), cost_final, ∂cost, numfg, convergence_history = optimize( (peps₀, env₀), alg.optimizer_alg; - retract, inner = real_inner, finalize!, (transport!) = (peps_transport!), + retract, inner = real_inner, (transport!) = (peps_transport!), + hasconverged, shouldstop, finalize!, ) do (peps, env) start_time = time_ns() E, gs = withgradient(peps) do ψ diff --git a/src/algorithms/select_algorithm.jl b/src/algorithms/select_algorithm.jl index 93445a6b5..f46c5cf22 100644 --- a/src/algorithms/select_algorithm.jl +++ b/src/algorithms/select_algorithm.jl @@ -23,7 +23,7 @@ function select_algorithm( tol = Defaults.optimizer_tol, # top-level tolerance verbosity = 3, # top-level verbosity boundary_alg = (;), gradient_alg = (;), optimizer_alg = (;), - symmetrization = nothing, kwargs..., + symmetrization = nothing, hasconverged = nothing, shouldstop = nothing, kwargs..., ) # adjust CTMRG tols and verbosity if boundary_alg isa NamedTuple From 8573a475a8f2160f9d2cfd4a5afb5cf73d3c0f56 Mon Sep 17 00:00:00 2001 From: Paul Brehmer Date: Mon, 29 Jun 2026 16:53:36 +0200 Subject: [PATCH 2/6] Add small test for `hasconverged` and `shouldstop` --- .../optimization/peps_optimization.jl | 4 +- test/examples/heisenberg.jl | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/algorithms/optimization/peps_optimization.jl b/src/algorithms/optimization/peps_optimization.jl index 190d40ca2..96bb2dc78 100644 --- a/src/algorithms/optimization/peps_optimization.jl +++ b/src/algorithms/optimization/peps_optimization.jl @@ -175,7 +175,7 @@ function fixedpoint( alg = select_algorithm(fixedpoint, env₀; kwargs...) # default stopping criteria initialized on alg parameters that have to be select first - isnothing(hasconverged) && (hasconverged = OptimKit.DefaultHasConverged(alg.optimizer_alg.tol)) + isnothing(hasconverged) && (hasconverged = OptimKit.DefaultHasConverged(alg.optimizer_alg.gradtol)) isnothing(shouldstop) && (shouldstop = OptimKit.DefaultShouldStop(alg.optimizer_alg.maxiter)) return fixedpoint(operator, peps₀, env₀, alg; finalize!, hasconverged, shouldstop) @@ -183,7 +183,7 @@ end function fixedpoint( operator, peps₀::InfinitePEPS, env₀, alg::PEPSOptimize; (finalize!) = OptimKit._finalize!, - hasconverged = OptimKit.DefaultHasConverged(alg.optimizer_alg.tol), + hasconverged = OptimKit.DefaultHasConverged(alg.optimizer_alg.gradtol), shouldstop = OptimKit.DefaultShouldStop(alg.optimizer_alg.maxiter), ) # validate inputs diff --git a/test/examples/heisenberg.jl b/test/examples/heisenberg.jl index 6cb797b2b..e937567fb 100644 --- a/test/examples/heisenberg.jl +++ b/test/examples/heisenberg.jl @@ -150,3 +150,51 @@ end @test e_site2 ≈ E_ref atol = 1.0e-2 @test all(@. ξ_h > 0 && ξ_v > 0) end + +@kwdef mutable struct ΔEnergyShouldStop{T <: Real} + E_last::T = 0.0 + tol::T = 1.0e-12 +end +function (es::ΔEnergyShouldStop)(x, f, g, numfg, numiter, t) + Δenergy = f - es.E_last + es.E_last = f + return abs(Δenergy) <= es.tol +end + +@kwdef mutable struct ΔEnergyHasConverged{T <: Real} + E_last::T = 0.0 + tol::T = 1.0e-12 +end +function (es::ΔEnergyHasConverged)(x, f, g, normgrad) + Δenergy = f - es.E_last + es.E_last = f + return abs(Δenergy) <= es.tol +end + +@testset "Early stopping with hasconverged and shouldstop" begin + maxiter = 100 + Random.seed!(123) + H = heisenberg_XYZ(InfiniteSquare()) + peps₀ = InfinitePEPS(ComplexSpace(2), ComplexSpace(Dbond)) + + # should converge when energy difference becomes small enough + env₀, = leading_boundary(CTMRGEnv(peps₀, ComplexSpace(χenv)), peps₀) + ΔEconverged = ΔEnergyHasConverged(; tol = 1.0e-4) + peps, env, E, info = fixedpoint( + H, peps₀, env₀; + optimizer_alg = (; maxiter, tol = 0), + hasconverged = ΔEconverged, + ) + @test length(info.costs) < maxiter + + # should stop when linesearching fails and returns α = 0 (i.e. ΔE = 0) + χenvsmall = 2 + env₀, = leading_boundary(CTMRGEnv(peps₀, ComplexSpace(χenvsmall)), peps₀) + ΔEstop = ΔEnergyShouldStop(; tol = 1.0e-8) + peps, env, E, info = fixedpoint( + H, peps₀, env₀; + optimizer_alg = (; maxiter, ls_maxfg = 2, ls_maxiter = 2), + shouldstop = ΔEstop, + ) + @test length(info.costs) < maxiter +end From ca1d8d1c0ee34f74366ac3911866b897d5803e0b Mon Sep 17 00:00:00 2001 From: Paul Brehmer Date: Mon, 29 Jun 2026 16:54:01 +0200 Subject: [PATCH 3/6] Fix docstring --- src/algorithms/optimization/peps_optimization.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algorithms/optimization/peps_optimization.jl b/src/algorithms/optimization/peps_optimization.jl index 96bb2dc78..08d2c60df 100644 --- a/src/algorithms/optimization/peps_optimization.jl +++ b/src/algorithms/optimization/peps_optimization.jl @@ -109,7 +109,7 @@ The optimization parameters can be supplied via the keyword arguments or directl 4. Debug info including AD outputs * `reuse_env::Bool=$(Defaults.reuse_env)` : If `true`, the current optimization step is initialized on the previous environment, otherwise a random environment is used. * `symmetrization::Union{Nothing,SymmetrizationStyle}=nothing` : Accepts `nothing` or a `SymmetrizationStyle`, in which case the PEPS and PEPS gradient are symmetrized after each optimization iteration. -* `hasconverged=OptimKit.DefaultHasConverged(optimizer_alg.tol)` : Function specifying the convergence criterion with signature `bool = hasconverged(state, cost, grad, gradnorm)`, see [OptimKit.optimize](https://github.com/Jutho/OptimKit.jl/blob/master/src/OptimKit.jl) for specifics. +* `hasconverged=OptimKit.DefaultHasConverged(optimizer_alg.gradtol)` : Function specifying the convergence criterion with signature `bool = hasconverged(state, cost, grad, gradnorm)`, see [OptimKit.optimize](https://github.com/Jutho/OptimKit.jl/blob/master/src/OptimKit.jl) for specifics. * `shouldstop=OptimKit.DefaultShouldStop(optimizer_alg.maxiter)` : Function specifying the stopping criterion with signature `bool = shouldstop(state, cost, grad, numfg, iter, timespent)`, see [OptimKit.optimize](https://github.com/Jutho/OptimKit.jl/blob/master/src/OptimKit.jl) for specifics. * `(finalize!)=OptimKit._finalize!` : Inserts a `finalize!` function call after each optimization step by utilizing the `finalize!` kwarg of `OptimKit.optimize`. The function maps `(state, env), f, g = finalize!((state, env), cost, grad, numiter)`. From 78f426cd39bc1f30b2aecf0b83e54fdf5e980378 Mon Sep 17 00:00:00 2001 From: Paul Brehmer Date: Wed, 1 Jul 2026 11:48:33 +0200 Subject: [PATCH 4/6] Slightly update test and `fixedpoint` docstring --- src/algorithms/optimization/peps_optimization.jl | 4 ++-- test/examples/heisenberg.jl | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/algorithms/optimization/peps_optimization.jl b/src/algorithms/optimization/peps_optimization.jl index 08d2c60df..2b2408a0f 100644 --- a/src/algorithms/optimization/peps_optimization.jl +++ b/src/algorithms/optimization/peps_optimization.jl @@ -109,8 +109,8 @@ The optimization parameters can be supplied via the keyword arguments or directl 4. Debug info including AD outputs * `reuse_env::Bool=$(Defaults.reuse_env)` : If `true`, the current optimization step is initialized on the previous environment, otherwise a random environment is used. * `symmetrization::Union{Nothing,SymmetrizationStyle}=nothing` : Accepts `nothing` or a `SymmetrizationStyle`, in which case the PEPS and PEPS gradient are symmetrized after each optimization iteration. -* `hasconverged=OptimKit.DefaultHasConverged(optimizer_alg.gradtol)` : Function specifying the convergence criterion with signature `bool = hasconverged(state, cost, grad, gradnorm)`, see [OptimKit.optimize](https://github.com/Jutho/OptimKit.jl/blob/master/src/OptimKit.jl) for specifics. -* `shouldstop=OptimKit.DefaultShouldStop(optimizer_alg.maxiter)` : Function specifying the stopping criterion with signature `bool = shouldstop(state, cost, grad, numfg, iter, timespent)`, see [OptimKit.optimize](https://github.com/Jutho/OptimKit.jl/blob/master/src/OptimKit.jl) for specifics. +* `hasconverged=OptimKit.DefaultHasConverged(optimizer_alg.gradtol)` : Function specifying the convergence criterion with signature `bool = hasconverged(state, cost, grad, gradnorm)`, see [OptimKit.optimize](https://github.com/Jutho/OptimKit.jl/blob/master/src/OptimKit.jl) for specifics. Note that this overrides the default convergence criterion. +* `shouldstop=OptimKit.DefaultShouldStop(optimizer_alg.maxiter)` : Function specifying the stopping criterion with signature `bool = shouldstop(state, cost, grad, numfg, iter, timespent)`, see [OptimKit.optimize](https://github.com/Jutho/OptimKit.jl/blob/master/src/OptimKit.jl) for specifics. Note that this overrides the default stopping criterion. * `(finalize!)=OptimKit._finalize!` : Inserts a `finalize!` function call after each optimization step by utilizing the `finalize!` kwarg of `OptimKit.optimize`. The function maps `(state, env), f, g = finalize!((state, env), cost, grad, numiter)`. ### Boundary algorithm diff --git a/test/examples/heisenberg.jl b/test/examples/heisenberg.jl index e937567fb..8c3b2b50f 100644 --- a/test/examples/heisenberg.jl +++ b/test/examples/heisenberg.jl @@ -154,11 +154,12 @@ end @kwdef mutable struct ΔEnergyShouldStop{T <: Real} E_last::T = 0.0 tol::T = 1.0e-12 + maxiter::Int = 100 end function (es::ΔEnergyShouldStop)(x, f, g, numfg, numiter, t) Δenergy = f - es.E_last es.E_last = f - return abs(Δenergy) <= es.tol + return (abs(Δenergy) <= es.tol) || numiter >= es.maxiter end @kwdef mutable struct ΔEnergyHasConverged{T <: Real} From f761d21e9bc00f3cf197c8c43bfe21bbf0af47f2 Mon Sep 17 00:00:00 2001 From: Paul Brehmer Date: Wed, 1 Jul 2026 14:41:01 +0200 Subject: [PATCH 5/6] Apply suggestions --- .../optimization/peps_optimization.jl | 17 ++++------------- src/algorithms/select_algorithm.jl | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/algorithms/optimization/peps_optimization.jl b/src/algorithms/optimization/peps_optimization.jl index 2b2408a0f..4100611bb 100644 --- a/src/algorithms/optimization/peps_optimization.jl +++ b/src/algorithms/optimization/peps_optimization.jl @@ -166,19 +166,10 @@ information `NamedTuple` which contains the following entries: * `gradnorms_unitcell` : History of gradient norms for each respective unit cell entry. * `times` : History of optimization step execution times. """ -function fixedpoint( - operator, peps₀::InfinitePEPS, env₀; - (finalize!) = OptimKit._finalize!, - hasconverged = nothing, shouldstop = nothing, kwargs..., - ) - # select OptimizationAlgorithm from kwargs - alg = select_algorithm(fixedpoint, env₀; kwargs...) - - # default stopping criteria initialized on alg parameters that have to be select first - isnothing(hasconverged) && (hasconverged = OptimKit.DefaultHasConverged(alg.optimizer_alg.gradtol)) - isnothing(shouldstop) && (shouldstop = OptimKit.DefaultShouldStop(alg.optimizer_alg.maxiter)) - - return fixedpoint(operator, peps₀, env₀, alg; finalize!, hasconverged, shouldstop) +function fixedpoint(operator, peps₀::InfinitePEPS, env₀; kwargs...) + extra_kwarg_keys = (:finalize!, :hasconverged, :shouldstop) # these will not be passed to `select_algorithm`, only to the 2nd `fixedpoint` call + alg = select_algorithm(fixedpoint, env₀; filter(kw -> !(first(kw) in extra_kwarg_keys), kwargs)...) + return fixedpoint(operator, peps₀, env₀, alg; filter(kw -> first(kw) in extra_kwarg_keys, kwargs)...) end function fixedpoint( operator, peps₀::InfinitePEPS, env₀, alg::PEPSOptimize; diff --git a/src/algorithms/select_algorithm.jl b/src/algorithms/select_algorithm.jl index f46c5cf22..93445a6b5 100644 --- a/src/algorithms/select_algorithm.jl +++ b/src/algorithms/select_algorithm.jl @@ -23,7 +23,7 @@ function select_algorithm( tol = Defaults.optimizer_tol, # top-level tolerance verbosity = 3, # top-level verbosity boundary_alg = (;), gradient_alg = (;), optimizer_alg = (;), - symmetrization = nothing, hasconverged = nothing, shouldstop = nothing, kwargs..., + symmetrization = nothing, kwargs..., ) # adjust CTMRG tols and verbosity if boundary_alg isa NamedTuple From 86bb8ff4b90d08ae7e8ad29ec31497a114e4219e Mon Sep 17 00:00:00 2001 From: Paul Brehmer Date: Thu, 2 Jul 2026 15:10:33 +0200 Subject: [PATCH 6/6] Restrict example type to Float64 --- test/examples/heisenberg.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/examples/heisenberg.jl b/test/examples/heisenberg.jl index 8c3b2b50f..2968e09c1 100644 --- a/test/examples/heisenberg.jl +++ b/test/examples/heisenberg.jl @@ -151,9 +151,9 @@ end @test all(@. ξ_h > 0 && ξ_v > 0) end -@kwdef mutable struct ΔEnergyShouldStop{T <: Real} - E_last::T = 0.0 - tol::T = 1.0e-12 +@kwdef mutable struct ΔEnergyShouldStop + E_last::Float64 = 0.0 + tol::Float64 = 1.0e-12 maxiter::Int = 100 end function (es::ΔEnergyShouldStop)(x, f, g, numfg, numiter, t) @@ -162,9 +162,9 @@ function (es::ΔEnergyShouldStop)(x, f, g, numfg, numiter, t) return (abs(Δenergy) <= es.tol) || numiter >= es.maxiter end -@kwdef mutable struct ΔEnergyHasConverged{T <: Real} - E_last::T = 0.0 - tol::T = 1.0e-12 +@kwdef mutable struct ΔEnergyHasConverged + E_last::Float64 = 0.0 + tol::Float64 = 1.0e-12 end function (es::ΔEnergyHasConverged)(x, f, g, normgrad) Δenergy = f - es.E_last