Variational Quantum Eigensolver AlgorithmVQE
The Variational Quantum Eigensolver (VQE) is the most well known and widely used Variational Quantum Algorithm (VQA). [6] It serves as one of the main hopes for quantum advantage in the Noisy Intermediate Scale Quantum (NISQ) era, due to its relatively low depth quantum circuits in contrast to other quantum algorithms.
In InQuanto, the AlgorithmVQE
class may be used to perform a VQE experiment. Like all Algorithm
classes, this requires several precursor steps to generate input data. We will briefly cover these, however detailed explanations of relevant modules can be found later in this manual.
Firstly, we generate the system of interest. Here we choose the hydrogen molecule in a minimal basis. The H2 Hamiltonian (FermionOperator
object) is obtained from the inquanto.express
module, with the corresponding FermionSpace
and a FermionState
objects constructed explicitly thereafter.
from inquanto.express import get_system
fermion_hamiltonian, fermion_fock_space, fermion_state = get_system("h2_sto3g.h5")
We can then map the fermion operator onto a qubit operator with one of the available mapping methods.
from inquanto.mappings import QubitMappingJordanWigner
mapping = QubitMappingJordanWigner()
qubit_hamiltonian = mapping.operator_map(fermion_hamiltonian)
Next, we must define the parameterized wave function ansatz, which will be optimized during the VQE minimisation.
InQuanto provides a suite of ansatzes, such as the Unitary Coupled Cluster (UCC) or the Hardware Efficient Ansatz (HEA). It is additionally possible to define a custom ansatz based on either fermionic excitations or explicit state generation circuits. Ansatz states in InQuanto are constructed using the inquanto.ansatzes
module, which are detailed in the Ansatzes section.
from inquanto.ansatzes import FermionSpaceAnsatzUCCSD
ansatz = FermionSpaceAnsatzUCCSD(fermion_fock_space, fermion_state, mapping)
Now that we have the qubit Hamiltonian and the ansatz, we can define a Computable
object we would like to pass to the minimizer during the VQE execution cycle.
In this case we choose the expectation value of the molecular Hamiltonian - variationally minimizing this finds the ground state of the system and its energy. In InQuanto, Computable objects represent quantities that can be computed from a given quantum simulation. For variational algorithms, ansatz and qubit operator objects must be specified when constructing a ExpectationValue
Computable object.
from inquanto.computables import ExpectationValue
expectation_value = ExpectationValue(ansatz, qubit_hamiltonian)
We then define a classical minimizer. There are a variety of minimizers to choose from within InQuanto. In this case, we will use the MinimizerScipy
with default options.
from inquanto.minimizers import MinimizerScipy
minimizer = MinimizerScipy()
We can then define the initial parameters for our ansatz. Here, we use random coefficients, but these values may be user-constructed as specified in the Ansatzes section.
initial_parameters = ansatz.state_symbols.construct_zeros()
We can then finally initialize the AlgorithmVQE
object itself.
from inquanto.algorithms import AlgorithmVQE
vqe = AlgorithmVQE(
objective_expression=expectation_value,
minimizer=minimizer,
initial_parameters=initial_parameters)
AlgorithmVQE
also accepts an optional auxiliary_expression
argument for any additional Computable
(or their collection, ComputableTuple
) object to be evaluated at the minimum ansatz parameters at the end of the VQE optimisation. It also accepts a gradient_expression
argument for a special Computable
type object, enabling calculation of analytical circuit gradients with respect to variational parameters (see below for an example).
A user must also define a protocol, defining how a Computable
supplied to the
objective expression will be computed at the circuit level (or using a state vector
simulator). For more details see the protocols section. Here, we
choose to use a state vector simulation. On instantiating a protocol object,
one must provide the pytket
Backend
of choice. These can be found in the
pytket.extensions where a
range of emulators and quantum hardware backends are provided.
from inquanto.protocols import SparseStatevectorProtocol
from pytket.extensions.qiskit import AerStateBackend
backend = AerStateBackend()
protocol_objective = SparseStatevectorProtocol(backend)
Prior to running any algorithm, its procedures must be set up with the build()
method. This method performs any classical preprocessing, and can be thought of as the step which defines how the circuit for the Computable
is run.
vqe.build(protocol_objective=protocol_objective)
<inquanto.algorithms.vqe._algorithm_vqe.AlgorithmVQE at 0x7f3a88e2e110>
We can then finally execute the algorithm using the run()
method. This step performs the simulation on either the actual quantum device or a simulator backend specified above. During the VQE optimization cycle an expectation value is calculated and the parameters changed to minimize the expectation value at each step.
vqe.run()
# TIMER BLOCK-0 BEGINS AT 2024-10-30 18:14:08.401501
# TIMER BLOCK-0 ENDS - DURATION (s): 0.9716464 [0:00:00.971646]
<inquanto.algorithms.vqe._algorithm_vqe.AlgorithmVQE at 0x7f3a88e2e110>
The results are obtained by calling the generate_report()
method, which returns a dictionary. This dictionary stores all important information generated throughout the algorithm, such as the final value of the Computable
quantity and the optimized values of ansatz parameters (final parameters).
print(vqe.generate_report())
{'minimizer': {'final_value': -1.136846575472054, 'final_parameters': array([-0.107, 0. , 0. ])}, 'final_value': -1.136846575472054, 'initial_parameters': [{'ordering': 0, 'symbol': 'd0', 'value': 0.0}, {'ordering': 1, 'symbol': 's0', 'value': 0.0}, {'ordering': 2, 'symbol': 's1', 'value': 0.0}], 'final_parameters': [{'ordering': 0, 'symbol': 'd0', 'value': -0.1072334945073921}, {'ordering': 1, 'symbol': 's0', 'value': 0.0}, {'ordering': 2, 'symbol': 's1', 'value': 0.0}]}
A modified initialization of the AlgorithmVQE
allows
to use analytical circuit gradients as part of the calculation. Note here the additional
protocol_gradient
argument passed to the
build()
method, but the same state-vector
protocol object can be used for the the two expressions for efficiency reasons (this is
not the case for the shot-based protocols).
from inquanto.computables import ExpectationValueDerivative
protocol = protocol_objective
gradient_expression = ExpectationValueDerivative(ansatz, qubit_hamiltonian, ansatz.free_symbols_ordered())
vqe_with_gradient = (
AlgorithmVQE(
objective_expression=expectation_value,
minimizer=minimizer,
initial_parameters=initial_parameters,
gradient_expression=gradient_expression,
)
.build(
protocol_objective=protocol,
protocol_gradient=protocol
)
.run()
)
results = vqe_with_gradient.generate_report()
print(f"Minimum Energy: {results['final_value']}")
param_report = results["final_parameters"]
for i in range(len(param_report)):
print(f"{param_report[i]['symbol']}: {param_report[i]['value']}")
# TIMER BLOCK-1 BEGINS AT 2024-10-30 18:14:09.385581
# TIMER BLOCK-1 ENDS - DURATION (s): 0.2552561 [0:00:00.255256]
Minimum Energy: -1.1368465754720527
d0: -0.10723347230091601
s0: 0.0
s1: 0.0
The use of analytic gradients may reduce the computational cost of the overall algorithm and can impact convergence.