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.

(2)\[\min E({\theta}) = \langle \Psi(\vec{\theta})| \hat{H} |\Psi(\vec{\theta})\rangle\]

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 0x7f04ae535110>

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-09-23 17:06:02.952399
# TIMER BLOCK-0 ENDS - DURATION (s):  1.0047266 [0:00:01.004727]
<inquanto.algorithms.vqe._algorithm_vqe.AlgorithmVQE at 0x7f04ae535110>

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-09-23 17:06:03.969346
# TIMER BLOCK-1 ENDS - DURATION (s):  0.2617781 [0:00:00.261778]
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.