Classical Minimizers
In the near-term many quantum computational chemistry algorithms use a combination of classical and quantum
computational resources. Typically, hybrid quantum-classical approaches to finding Hamiltonian eigenvalues and
eigenstates involve the minimization of a cost function, which is often the energy. The variables of the cost function are updated
on a classical device, and the value of the cost function at each iteration is evaluated on a quantum device. To
facilitate algorithms of this type, a variety of minimization methods are available in the InQuanto package. Each
of the minimizers
contains a minimize()
method,
which can be called by the user or by algorithms
objects as part of the workflow.
In this section, we will implement a bespoke state-vector VQE routine with gradients to showcase a few of the available minimizers.
MinimizerScipy
The simplest minimizer option available in InQuanto is the InQuanto MinimizerScipy
class,
which wraps the SciPy suite of minimizer classes into an InQuanto object. Below, we implement and optimize a VQE
objective function using the conjugate gradient method to demonstrate the functionality of the minimizer classes, and
the additional control that familiarity with these objects can provide. First, we load in a Hamiltonian from the
express module:
from inquanto.express import load_h5
from inquanto.spaces import FermionSpace
from inquanto.mappings import QubitMappingJordanWigner
h2_sto3g = load_h5("h2_sto3g.h5")
hamiltonian = h2_sto3g["hamiltonian_operator"]
space = FermionSpace(4)
state = space.generate_occupation_state_from_list([1, 1, 0, 0])
qubit_hamiltonian = QubitMappingJordanWigner().operator_map(hamiltonian)
We now choose an ansatz (UCCSD), and prepare functions to compute the energy and energy gradient, which will be the VQE objective and VQE gradient functions respectively:
from inquanto.ansatzes import FermionSpaceAnsatzUCCSD
from inquanto.computables import ExpectationValue, ExpectationValueDerivative
from inquanto.core import dict_to_vector
from inquanto.protocols import SparseStatevectorProtocol
from pytket.extensions.qiskit import AerStateBackend
import numpy as np
ansatz = FermionSpaceAnsatzUCCSD(space, state)
parameters = ansatz.state_symbols.construct_zeros()
sv_protocol = SparseStatevectorProtocol(AerStateBackend())
ev = ExpectationValue(kernel=qubit_hamiltonian, state=ansatz)
evg = ExpectationValueDerivative(ansatz, qubit_hamiltonian, ansatz.free_symbols_ordered())
def vqe_objective(variables):
parameters = ansatz.state_symbols.construct_from_array(variables)
sv_evaluator = sv_protocol.get_evaluator(parameters)
return ev.evaluate(sv_evaluator).real
def vqe_gradient(variables):
parameters = ansatz.state_symbols.construct_from_array(variables)
sv_evaluator = sv_protocol.get_evaluator(parameters)
gradient_dict = evg.evaluate(sv_evaluator)
return dict_to_vector(ansatz.free_symbols_ordered(), gradient_dict)
print("VQE energy with parameters at [0, 0, 0]:", vqe_objective(np.zeros(3)))
print("Gradients of parameters at [0, 0, 0]:", vqe_gradient(np.zeros(3)))
VQE energy with parameters at [0, 0, 0]: -1.1175058842043306
Gradients of parameters at [0, 0, 0]: [0.359 0. 0. ]
It should be noted that the minimizers work with NumPy arrays, not InQuanto SymbolDict
or
SymbolSet
parameter objects. The parameter objects compatible with the
computables
objects can be constructed with the
construct_from_array()
method as shown above.
We now initialize and execute the conjugate gradient minimizer. The underlying SciPy object can be configured
by passing an options dict
to the MinimizerScipy
constructor, according to the
solver-specific guidance in the
SciPy user manual. With this,
one may define, for example, a maximum number of iterations. With the
minimize()
method, we can leave the gradient
argument empty to
compute the gradient numerically, or pass the gradient function we defined above:
from inquanto.minimizers import MinimizerScipy
minimizer = MinimizerScipy("CG", disp=True)
minimizer.minimize(function=vqe_objective, initial=np.zeros(3))
Optimization terminated successfully.
Current function value: -1.136847
Iterations: 2
Function evaluations: 20
Gradient evaluations: 5
(-1.1368465754720538, array([-0.107, -0. , -0. ]))
minimizer = MinimizerScipy("CG", disp=True)
min, loc = minimizer.minimize(function=vqe_objective, initial=np.zeros(3), gradient=vqe_gradient)
print("Objective function minimum is {}, located at {}".format(min, loc))
Optimization terminated successfully.
Current function value: -1.136847
Iterations: 2
Function evaluations: 5
Gradient evaluations: 5
Objective function minimum is -1.1368465754720538, located at [-0.107 0. 0. ]
As one might expect, we observe that the optimizer converges to the same value in both cases, but requires less evaluations of the objective function when gradient information is provided.
MinimizerRotosolve
The Rotosolve minimizer [55], is a gradient-free optimizer
designed for minimization of VQE-like objective functions, and is available in the
MinimizerRotosolve
class. With this minimizer, one may define a maximum number of
iterations and convergence threshold on initialization.
A short example using rotosolve with an algorithm object is shown below.
from inquanto.algorithms import AlgorithmVQE
from inquanto.minimizers import MinimizerRotosolve
minimizer=MinimizerRotosolve(max_iterations=10, tolerance=1e-6, disp=True)
vqe = AlgorithmVQE(
objective_expression=ev,
minimizer=minimizer,
initial_parameters=ansatz.state_symbols.construct_zeros()
)
vqe.build(protocol_objective=SparseStatevectorProtocol(AerStateBackend()))
vqe.run()
print("VQE Energy:", vqe.generate_report()["final_value"])
# TIMER BLOCK-0 BEGINS AT 2024-09-23 17:09:15.628547
ROTOSOLVER – A gradient-free optimizer for parametric circuits
Iteration 1
fun = -1.1368465754720538 variance = 0.011499023526666223 p-norm = 0.10723350002059162
Iteration 2
fun = -1.1368465754720538 variance = 0.0 p-norm = 0.10723350002059162
Optimizer Converged
nit = 2 nfun = 19
final fun = -1.1368465754720538 final variance = 0.0 final p-norm = 0.10723350002059162
# TIMER BLOCK-0 ENDS - DURATION (s): 0.7059869 [0:00:00.705987]
VQE Energy: -1.1368465754720538
MinimizerSGD
The Stochastic Gradient Descent (SGD) approach to functional optimization is
available in the MinimizerSGD
class. This minimizer takes bespoke input arguments for the
learning_rate
and decay_rate
parameters, defined in [56].
A short example using MinimizerSGD
is shown below.
from inquanto.minimizers import MinimizerSGD
minimizer=MinimizerSGD(learning_rate=0.25, decay_rate=0.5, max_iterations=10, disp=True)
vqe = AlgorithmVQE(
objective_expression=ev,
minimizer=minimizer,
initial_parameters=ansatz.state_symbols.construct_zeros(),
gradient_expression=evg
)
vqe.build(
protocol_objective=SparseStatevectorProtocol(AerStateBackend()),
protocol_gradient=SparseStatevectorProtocol(AerStateBackend()),
)
vqe.run()
print("VQE Energy:", vqe.generate_report()["final_value"])
# TIMER BLOCK-1 BEGINS AT 2024-09-23 17:09:16.341545
Optimizer Stochastic Gradient Descent
Iteration 0
fun = -1.1175058842043306
p-norm = 0.0
g-norm = 0.35933735912603115
Iteration 1
fun = -1.1321535146012704
p-norm = 0.054487281372526786
g-norm = 0.17778365523875433
Iteration 2
fun = -1.1357239648371469
p-norm = 0.08144509079704805
g-norm = 0.08704389270781199
Iteration 3
fun = -1.136578976183475
p-norm = 0.09464378821405427
g-norm = 0.0425085425788404
Iteration 4
fun = -1.136782840579148
p-norm = 0.10108947180749593
g-norm = 0.020746679121849215
Iteration 5
fun = -1.136831398578587
p-norm = 0.10423534605115131
g-norm = 0.010124128478002536
Iteration 6
fun = -1.1368429616408458
p-norm = 0.10577049463234589
g-norm = 0.004940280683155496
Iteration 7
fun = -1.136845714977897
p-norm = 0.10651960255782586
g-norm = 0.0024106935679380492
Iteration 8
fun = -1.1368463705791978
p-norm = 0.10688514244785748
g-norm = 0.0011763364084182715
Iteration 9
fun = -1.1368465266849062
p-norm = 0.10706351347231796
g-norm = 0.0005740118592297838
Optimizer Converged
nit = 9 nfun = 9 njac = 9
final fun = -1.1368465266849062
final p-norm = 0.10715055242023334
final g-norm = 0.0005740118592297838
# TIMER BLOCK-1 ENDS - DURATION (s): 0.8397391 [0:00:00.839739]
VQE Energy: -1.1368465266849062
Minimizer SPSA
Simultaneous Perturbation Stochastic Approximation (SPSA) [57] is available in the
MinimizerSPSA
class. This minimizer is especially efficient in high dimensional
problems. SPSA approximates function gradients using
only two objective function measurements and is robust to noisy measurements of this objective function.
A short example using MinimizerSPSA
is shown below.
from inquanto.minimizers import MinimizerSPSA
minimizer=MinimizerSPSA(max_iterations=5, disp=True)
vqe = AlgorithmVQE(
objective_expression=ev,
minimizer=minimizer,
initial_parameters=ansatz.state_symbols.construct_zeros()
)
vqe.build(protocol_objective=SparseStatevectorProtocol(AerStateBackend()))
vqe.run()
print("VQE Energy:", vqe.generate_report()["final_value"])
# TIMER BLOCK-2 BEGINS AT 2024-09-23 17:09:17.187558
Starting SPSA minimization.
Result at iteration 0: [-0.174 0. 0. ].
Result at iteration 1: [-0.146 0. 0. ].
Result at iteration 2: [-0.094 0. 0.052].
Result at iteration 3: [-0.102 0. 0.052].
Result at iteration 4: [-0.139 -0.037 0.015].
Finishing SPSA minimization.
# TIMER BLOCK-2 ENDS - DURATION (s): 0.9272296 [0:00:00.927230]
VQE Energy: -1.1340125850338312