Lucon (Loss optimization under Unitary CONstraint) optimizes loss functions mapping a unitary matrix onto a number. A conjugate-gradient algorithm is used following the work by T. Abrudan et al., Signal Processing 89 (2009) 1704–1714.
The module presents potential applications in various fields. For instance, it can be employed for tasks such as orbital rotations (e.g., orbital localization) in quantum chemistry and materials science, as well as for various tasks in signal processing applications or machine learning algorithms. The main motivation for Lucon.jl is given by orbital localizations for calculations in materials physics and quantum chemistry. These will be referenced here shortly.
The code is designed in a way that users can implement arbitrary loss functionals with little effort for optimization with Lucon.jl. As a template the BrockettLoss.jl functional can be used (see example below).
To provide a very simple and illustrative example of the module's potential use cases, consider the following loss functional that can be used to diagonalize a hermitian matrix.
Here,
In the Julia REPL, simply run the following commands:
using Pkg
Pkg.add("Lucon")In order to use Lucon to optimize a loss functional
The loss functional is any callable Gradient(U, CalcLoss) returning the tuple (Γ, Loss). The value of the loss is only read when CalcLoss is true, so its computation may be skipped otherwise. Nothing has to be sub-typed and no method of Lucon has to be overloaded, which means that optimize can be called with do syntax:
import Lucon
using LinearAlgebra
# set up your hermitian matrix H and initial unitary U
N = Diagonal([1.0*n for n=1:size(H,1)]) # the N matrix is a diagonal matrix with entries N_nn = n
Result = Lucon.optimize(U; UDegree=2, Maximize=true) do U, CalcLoss
Γ = H*U*N # Euclidean derivative has same type and dimension as U
# the trace of U'Γ is the Frobenius product of U and Γ
(Γ, CalcLoss ? real(dot(U, Γ)) : 0.0)
endWhen the functional has to carry precomputed quantities, give them to a struct and make the struct callable. Store them with a concrete type and build them once, since the functional is evaluated once per iteration and once for every sampling point of the line search, and therefore dominates the run time:
struct BrockettCriterion{T<:Number, A<:AbstractMatrix{T}}
H::Hermitian{T,A}
N::Diagonal{Float64,Vector{Float64}}
end
BrockettCriterion(H::Hermitian) = BrockettCriterion(H, Diagonal([1.0*n for n=1:size(H,1)]))
function (B::BrockettCriterion)(U::AbstractMatrix, CalcLoss::Bool)
Γ = B.H*U*B.N
(Γ, CalcLoss ? real(dot(U, Γ)) : 0.0)
end
Result = Lucon.optimize(BrockettCriterion(H), U; UDegree=2, Maximize=true)Accept an AbstractMatrix so that U may live on a GPU.
The full example and its usage can be found in the source file BrockettLoss.jl and in the test file runtests.jl.
Both can be used as a template to implement arbitrary loss functionals.
optimize returns a Lucon.Result:
julia> Result
Lucon.Result
Status: converged
Iterations: 79
Loss: 9.5330162221636101e+00
max|grad|: 4.030e-09
U: 6×6 Matrix{ComplexF64}
julia> Lucon.Converged(Result)
trueStatus is one of :converged, :maxiter, :callback, or :linesearch if the line search found no positive step size.
The full signature reads
Result = Lucon.optimize(
Gradient,
U;
UDegree,
Maximize=false,
MinIter=0,
MaxIter=typemax(Int),
MaxGradientTolerance=1.0e-8,
PolynomialLineSearchDegree=5,
Callback=nothing
)-
UDegreeis the order$q$ of the loss functional, i.e. the highest power of$t$ appearing in the Taylor expansion of$L(U + tZ)$ . It sets the width$T_\mu = 2\pi/(q,|\omega_\text{max}|)$ of the window the line search scans. It has no default because it is a property of the functional. For the Brockett criterion above$q=2$ . -
Maximizemaximizes$L(U)$ instead of minimizing it.
The element type of the initial U selects the group that is optimized over, the orthogonal group for a real and the unitary group for a complex element type. MaxIter limits the number of rotations of U and is unlimited by default.
Convergence is reached once the largest absolute element of the Riemannian gradient MaxGradientTolerance. This maximum norm is used instead of the Frobenius norm because it does not grow with the size of the system: if a supersystem is built from MaxGradientTolerance therefore converges subsystem and supersystem to the same accuracy per degree of freedom.
optimize prints nothing. Progress is reported through Callback, a function which is called once per iteration with the named tuple (; Iteration, MaxGradient, Loss, U) and which stops the iteration when it returns true. To print a convergence trace, pass the ready-made Lucon.PrintTrace:
Result = Lucon.optimize(
Gradient,
U;
UDegree=2,
Callback=Lucon.PrintTrace() # or Lucon.PrintTrace(stderr)
) #iter max|grad| loss-function
1 2.136e+00 -1.2117919646339959e+00
2 1.929e+00 3.4753409237499291e+00
3 1.604e+00 6.7459345042215935e+00
The callback is equally the place to record a convergence history, to checkpoint U, or to stop on a criterion of your own:
History = Float64[]
RecordLoss(State) = (push!(History, State.Loss); State.Iteration ≥ 100)
Result = Lucon.optimize(
Gradient,
U;
UDegree=2,
Callback=RecordLoss
)A callback which stopped the iteration leaves Result.Status == :callback. The reason for which the iteration stopped is in addition emitted as a @debug message and can be made visible with ENV["JULIA_DEBUG"] = "Lucon".
Benjamin Wöckinger, Alexander Rumpf, Tobias Schäfer. Convergence and Properties of Intrinsic Bond Orbitals in Solids, J. Chem. Theory Comput. 2025, 21, 20, 10515–10526