`AlgorithmAdaptVQE`

and `AlgorithmIQEB`

¶

`AlgorithmAdaptVQE`

and `AlgorithmIQEB`

are algorithms that construct an ansatz iteratively from a collection or pool of excitation operators. [8] [9]
This differs from algorithms like `AlgorithmVQE`

in which the ansatz is fixed, i.e. parameters of a fixed set of operators are optimized.
At each ADAPT step, in `AlgorithmAdaptVQE`

and `AlgorithmIQEB`

, excitation operators are selected and their exponentials appended to the ansatz iteratively until convergence criteria are met.
Practically, this usually results in more circuits measurements required for these algorithms compared to VQE, however the resulting ansatz will (usually) have fewer terms
compared to a straight-forward application of a fixed ansatz e.g. UCCSD optimized in VQE for the same accuracy. Hence, compared to typical VQE, these algorithms trade-off number of measurements with circuit depth.

After \(N\) iterations of the algorithm, the resulting (ADAPT/IQEB) ansatz will have the form

Where the excitation operators \(\hat{A}\) are chosen by the user, and the corresponding parameters \(\theta\) are optimized,
by `AlgorithmAdaptVQE`

or `AlgorithmIQEB`

. In each iteration, the current ansatz is applied to a reference state;
a Hartree-Fock state is a common choice. The two major differences between `AlgorithmAdaptVQE`

and `AlgorithmIQEB`

are i) the space in which the
operators act on, and ii) the method to select the operators to append to the ansatz.

In the ADAPT (Adaptive Derivative-Assembled Pseudo-Trotter ansatz)-VQE algorithm [8], the operators \(\hat{A}_{\lambda}\) in the pool
consist of all possible spin complemented anti-hermitian operators within unitary coupled cluster ansatz (UCC), which act in fermionic space.
While these are fermionic operators, they must be transformed to qubit operators via a fermion-to-qubit mapping to be accepted by
`AlgorithmAdaptVQE`

. The operators to be appended to the ansatz at the \(n^{th}\) iteration
of the algorithm are chosen by calculating the following gradient of the total energy \(E\)

for each excitation operator. The \(\hat{A}_{\lambda}\) which yields the largest gradient is appended to the ansatz, and a regular VQE calculation is performed to determine the optimal ansatz parameters at this iteration. In the next iteration, the gradients are recalculated as before but with the \(e^{\theta_{\lambda} \hat{A}_{\lambda}}\) from the previous iteration appended. This is repeated until all gradients are below a tolerance threshold.

The following shows an example of how to run `AlgorithmAdaptVQE`

in which the operators of the `pool`

are
restricted to UCCSD (re-using the fermion space, qubit-encoded H_{2} Hamiltonian, and qubit mapping of the `AlgorithmVQE`

example).

```
from inquanto.algorithms import AlgorithmAdaptVQE
from inquanto.spaces import FermionSpace
from inquanto.states import QubitState, FermionState
from inquanto.mappings import QubitMappingJordanWigner
from inquanto.ansatzes import FermionSpaceAnsatzUCCSD
from inquanto.minimizers import MinimizerScipy
from inquanto.express import get_system
from inquanto.protocols import SparseStatevectorProtocol
from pytket.extensions.qiskit import AerStateBackend
fermion_hamiltonian, fermion_space, fermion_state = get_system("h2_sto3g.h5")
jw = QubitMappingJordanWigner()
qubit_hamiltonian = jw.operator_map(fermion_hamiltonian)
space = FermionSpace(4)
fermion_state = FermionState([1, 1, 0, 0])
qubit_state = QubitState([1, 1, 0, 0])
jw_map = QubitMappingJordanWigner()
pool = space.construct_single_ucc_operators(fermion_state)
pool += space.construct_double_ucc_operators(fermion_state)
pool = jw_map.operator_map(pool)
scipy_minimizer = MinimizerScipy(method="L-BFGS-B", disp=False)
adapt = AlgorithmAdaptVQE(
pool,
qubit_state,
qubit_hamiltonian,
scipy_minimizer,
tolerance=1.0e-3
)
protocol = SparseStatevectorProtocol(AerStateBackend())
adapt.build(
protocol,
protocol,
protocol
)
adapt.run()
results = adapt.generate_report()
print("Minimum Energy: {}".format(results["final_value"]))
param_report = results["final_parameters"]
for i in range(len(param_report)):
print(param_report[i]["symbol"], ":", param_report[i]["value"])
```

```
# TIMER BLOCK-0 BEGINS AT 2024-10-14 13:05:39.230578
# TIMER BLOCK-0 ENDS - DURATION (s): 0.2107950 [0:00:00.210795]
```

```
Minimum Energy: -1.1368465754720527
d0 : -0.10723347230091601
```

Here, the pool of operators was generated by the `construct_single_ucc_operators()`

and `construct_double_ucc_operators()`

methods of the `FermionSpace`

class. Like the Hamiltonian, these operators were qubit-encoded before
passing into `AlgorithmAdaptVQE`

. The `tolerance`

parameter sets the gradient threshold for convergence of the algorithm.
The protocol `SparseStatevectorProtocol`

is needed to calculate the gradients defined above.

Alternatively, one can use fermionic states and operators as arguments to the algorithm class `AlgorithmFermionicAdaptVQE`

, which inherits
from `AlgorithmAdaptVQE`

, and performs the qubit mapping internally. Below is an example of `AlgorithmFermionicAdaptVQE`

where again the fermionic space has been re-used (also the `scipy_minimizer`

), and this time we use the fermionic H_{2} Hamiltonian (defined in the
`AlgorithmFermionicAdaptVQE`

example).

```
from inquanto.algorithms import AlgorithmFermionicAdaptVQE
state = FermionState([1, 1, 0, 0])
pool = space.construct_single_ucc_operators(state)
pool += space.construct_double_ucc_operators(state)
fermionic_adapt = AlgorithmFermionicAdaptVQE(
pool,
state,
fermion_hamiltonian,
scipy_minimizer,
tolerance=1.0e-3
)
protocol = SparseStatevectorProtocol(AerStateBackend())
fermionic_adapt.build(
protocol,
protocol,
protocol
)
fermionic_adapt.run()
results = fermionic_adapt.generate_report()
print("Minimum Energy: {}".format(results["final_value"]))
param_report = results["final_parameters"]
for i in range(len(param_report)):
print(param_report[i]["symbol"], ":", param_report[i]["value"])
```

```
# TIMER BLOCK-1 BEGINS AT 2024-10-14 13:05:39.537879
```

```
# TIMER BLOCK-1 ENDS - DURATION (s): 0.2178639 [0:00:00.217864]
Minimum Energy: -1.1368465754720527
d0 : -0.10723347230091601
```

Note that in this case the reference `state`

and `pool`

have not been qubit encoded before passing into
`AlgorithmFermionicAdaptVQE`

. Jordan-Wigner encoding is performed by default.

In the IQEB (Iterative Qubit-Excitation Based)-VQE algorithm [9], the operators in the pool correspond to qubit excitations. Qubit excitation operators are generated from the following ladder operators which obey the so-called parafermionic [10] commutation relations

where \(\hat{Q}_i\) (\(\hat{Q}_i^\dagger\)) is a qubit annihilation (creation) operator which changes the occupation of spin orbital \(i\) (assuming a Jordan-Wigner encoding of the Hamiltonian and reference state), and which can be represented in terms of Pauli gates

The pool in `AlgorithmIQEB`

consists of one- and two-body qubit excitation operators, built
from these parafermionic operators, and acting on qubit (or spin orbital) indexes \(i, j, k, l\) (where the set of
indexes is unique to the \(\lambda\)-th operator in the `pool`

).

Note that the `AlgorithmIQEB`

class inherits from `AlgorithmAdaptVQE`

. While gradients are also used
in the selection process of `AlgorithmIQEB`

, their purpose here is to narrow down the candidate operators from the
total IQEB `pool`

. Hence the convergence of `AlgorithmIQEB`

is not evaluated directly by gradients as in
`AlgorithmAdaptVQE`

. Instead, `AlgorithmIQEB`

checks the total energy difference between iterations, and
convergence is achieved when the decrease of energy between iterations is less than a threshold (the
`energy_tolerance`

parameter in the code block below).

The following example shows how to run `AlgorithmIQEB`

(using the previously defined H_{2} qubit Hamiltonian
and `scipy_minimizer`

(see here for minimizers).

```
from inquanto.algorithms import AlgorithmIQEB
from inquanto.spaces import ParaFermionSpace
space = ParaFermionSpace(4)
state = QubitState([1, 1, 0, 0])
pool = space.construct_single_qubit_excitation_operators()
pool += space.construct_double_qubit_excitation_operators()
iqeb = AlgorithmIQEB(
pool,
state,
qubit_hamiltonian,
scipy_minimizer,
n_grads=3,
energy_tolerance=1.0e-10
)
protocol = SparseStatevectorProtocol(AerStateBackend())
iqeb.build(
protocol,
protocol,
protocol,
)
iqeb.run()
results = iqeb.generate_report()
print("Minimum Energy: {}".format(results["final_value"]))
param_report = results["final_parameters"]
for i in range(len(param_report)):
print(param_report[i]["symbol"], ":", param_report[i]["value"])
```

```
System has zero net spin -> will append spin-complementary exponents.
# TIMER BLOCK-2 BEGINS AT 2024-10-14 13:05:39.872826
```

```
# TIMER BLOCK-2 ENDS - DURATION (s): 0.2134800 [0:00:00.213480]
# TIMER BLOCK-3 BEGINS AT 2024-10-14 13:05:40.086883
# TIMER BLOCK-3 ENDS - DURATION (s): 0.0169404 [0:00:00.016940]
# TIMER BLOCK-4 BEGINS AT 2024-10-14 13:05:40.104269
# TIMER BLOCK-4 ENDS - DURATION (s): 0.0159712 [0:00:00.015971]
```

```
# TIMER BLOCK-5 BEGINS AT 2024-10-14 13:05:40.299860
# TIMER BLOCK-5 ENDS - DURATION (s): 0.0755414 [0:00:00.075541]
# TIMER BLOCK-6 BEGINS AT 2024-10-14 13:05:40.376071
# TIMER BLOCK-6 ENDS - DURATION (s): 0.0527517 [0:00:00.052752]
# TIMER BLOCK-7 BEGINS AT 2024-10-14 13:05:40.429425
# TIMER BLOCK-7 ENDS - DURATION (s): 0.0535147 [0:00:00.053515]
CONVERGED!!!
Final ansatz elements after 2 iteration(s):
r_1_1 [(0.125j, X0 Y1 X2 X3), (0.125j, Y0 X1 X2 X3), (0.125j, Y0 Y1 Y2 X3), (0.125j, Y0 Y1 X2 Y3), (-0.125j, X0 X1 Y2 X3), (-0.125j, X0 X1 X2 Y3), (-0.125j, X0 Y1 Y2 Y3), (-0.125j, Y0 X1 Y2 Y3)]
Minimum Energy: -1.1368465754720527
r_1_1 : -0.10723347230091601
```

Notice that six separate VQE calculations have been performed (one for each `TIMER BLOCK`

in the output log). This is due to two reasons.
i) Our choice of `n_grads=3`

, which tells `AlgorithmIQEB`

that we want to narrow down the `pool`

to those terms which have the three largest gradients, and a VQE calculation for each term will be run.
Of these three, the term which has the largest effect on the energy will be appended to the ansatz in this iteration.
ii) Since `AlgorithmIQEB`

establishes convergence by comparing the energy difference between iterations, a second iteration is performed, again with three separate terms (appended to the previously found term).
In this case, convergence is found at the second iteration, which means the resulting ansatz will have the form of the first iteration. Note that the internal VQE initial parameter coefficients are set to be all zeros.

As in the case of `AlgorithmAdaptVQE`

, we define a pool of operators. However here we employ the
`ParaFermionSpace`

class to handle the parafermionic operator algebra. The operators in the IQEB `pool`

consist of all unique permutations of qubit indexes for one- and two-body terms, obtained by the `construct_single_qubit_excitation_operators()`

and `construct_double_qubit_excitation_operators()`

methods of `ParaFermionSpace()`

(which do not need a reference state). This results in an
asymptotically larger `pool`

than `AlgorithmAdaptVQE`

[9]. However, the advantage of `AlgorithmIQEB`

is that excitation operators act directly in parafermionic space, hence the strings of Pauli-Z operators resulting from Jordan-Wigner encoding,
in order to maintain fermionic exchange symmetry, are not required. Therefore each qubit excitation of `AlgorithmIQEB`

acts on a fixed number of qubits, independent of the system size.