Optimized VQE Ansatz with qat.synthopline¶
In this notebook, we show how to generate an optimized UCCSD Ansatz for VQE using the qat.synthopline
module.
This module contains efficient tools to synthesize (parametrized) Trotter expansion of operators. It can be used to directly generate
efficient jobs from a collection of cluster operators.
Generating the cluster operators¶
First, let us start by generating a necessary cluster operators and Hamiltonian for some molecule.
The following lines of code are directly extracted from another tutorial.
import numpy as np
lih_data = np.load("lih_data.npz", allow_pickle=True)
rdm1 = lih_data["rdm1"]
orbital_energies = lih_data["orbital_energies"]
nuclear_repulsion = lih_data["nuclear_repulsion"]
n_electrons = lih_data["n_electrons"]
one_body_integrals = lih_data["one_body_integrals"]
two_body_integrals = lih_data["two_body_integrals"]
info = lih_data["info"].tolist()
nqbits = rdm1.shape[0] * 2
from qat.fermion.chemistry import MolecularHamiltonian, MoleculeInfo
from qat.fermion.chemistry.ucc import guess_init_params, get_hf_ket, get_cluster_ops
# Define the molecular hamiltonian
mol_h = MolecularHamiltonian(one_body_integrals, two_body_integrals, nuclear_repulsion)
print(mol_h)
# Compute the natural orbitals occupation numbers and the basis transformation matrix
noons, basis_change = np.linalg.eigh(rdm1)
# The noons should be in decreasing order
noons = list(reversed(noons))
# Since we reversed the noons, we have to flip the basis as well
basis_change = np.flip(basis_change, axis=1)
# Change the hamiltonian basis
mol_h_new_basis = mol_h.transform_basis(basis_change)
molecule = MoleculeInfo(
mol_h_new_basis,
n_electrons=n_electrons,
noons=noons,
orbital_energies=orbital_energies,
)
# Selection of the active space
molecule.restrict_active_space(threshold_1=0.02, threshold_2=0.002)
# Compute the cluster operators
cluster_ops = get_cluster_ops(molecule.n_electrons, noons=molecule.noons)
# Computation of the initial parameters
theta_list = guess_init_params(
molecule.two_body_integrals,
molecule.n_electrons,
molecule.orbital_energies,
)
# Define the initial Hartree-Fock state
ket_hf_init = get_hf_ket(molecule.n_electrons, nqbits=molecule.nqbits)
# from qat.fermion.transforms import transform_to_bk_basis as transform
# from qat.fermion.transforms import recode_integer, get_bk_code as code
from qat.fermion.transforms import transform_to_jw_basis as transform
from qat.fermion.transforms import recode_integer, get_jw_code as code
# Compute the ElectronicStructureHamiltonian
H_active = molecule.hamiltonian.get_electronic_hamiltonian()
# Transform the ElectronicStructureHamiltonian into a spin Hamiltonian
H_active_sp = transform(H_active)
# Express the cluster operator in spin terms
# Here we group all cluster operators into a single obserable
cluster_ops_sp = sum(transform(t_o) for t_o in cluster_ops)
# Encoding the initial state to new encoding
hf_init_sp = recode_integer(ket_hf_init, code(H_active_sp.nbqbits))
MolecularHamiltonian( - constant_coeff : 0.9071609330057144 - integrals shape * one_body_integrals : (11, 11) * two_body_integrals : (11, 11, 11, 11) )
Generating the Ansatz¶
Now that we have our cluster operators and our target Hamiltonian, we can use the qat.synthopline.generate_trotter_ansatz
method to produce an Ansatz.
This method supports various backend algorithms. Here we will iterate over all of them and compare the output circuits.
from qat.synthopline.pauli_synth import generate_trotter_ansatz, STRATEGIES
def cnot_depth(job):
""" Computes the CNOT/CZ depth of a job """
depths = [0] * job.circuit.nbqbits
for _, _, qbits in job.circuit.iterate_simple():
if len(qbits) > 1:
gate_depth = max(depths[q] for q in qbits) + 1
for q in qbits:
depths[q] = gate_depth
return max(depths)
def cnot_count(job):
"""" Computes the number of CNOT/CZ in a job """
stats = job.circuit.statistics()['gates']
return stats.get('CNOT', 0) + stats.get('CSIGN', 0) + 3 * stats.get('SWAP', 0)
for strategy in STRATEGIES:
job = generate_trotter_ansatz(cluster_ops_sp, final_observable=H_active_sp, init_state=hf_init_sp, strategy=strategy)
print(strategy.ljust(17), cnot_count(job), cnot_depth(job))
pauli_synth 15 13 commute 28 26 commute_improved 16 15 naive 64 64 greedy 13 10 greedy_depth 12 8
A few things to notice !
First, the "naive" method is by far the worse strategy. This strategy is the one implemented in the qat.fermion package.
The "commute" method (the second worse here) corresponds to the published state of the art method.
All other methods are in-house methods only available in the QLM.
Notice that the "greedy_depth" method should be the default choice in that particular case since it achieves the best CNOT count AND CNOT depth.
Hence, by using the generate_trotter_ansatz
one can bring the entangling count from 64 down to 12 and the entangling depth from 64 down to 8.
But we can probably do even better by further optimizing the output quantum circuit!
For this we will use the LazySynthesis
plugin to compile the circuit for a all-to-all connectivity.
Pushing the limit¶
Let us try to gain a bit more !
from qat.synthopline import LazySynthesis
from qat.devices import AllToAll
best_job = generate_trotter_ansatz(cluster_ops_sp, final_observable=H_active_sp, init_state=hf_init_sp, strategy="greedy_depth")
device = AllToAll(cluster_ops_sp.nbqbits)
plugin = LazySynthesis(depth=3, merge=True, reorder=True, bidirectional=True, timeout=4, optimize_initial=True)
new_job, data = plugin.compile_job(best_job, device)
print("Final CNOT count:", cnot_count(new_job))
Final CNOT count: 11
We gained a CNOT gate thanks to LazySynthesis. It is not much, but still corresponds to a $8.3\%$ improvement compared to the previous best job.
Overall we reduced the CNOT count by about $83\%$ !