QAOA with QCut

QAOA with QCut#

import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
from iqm.qiskit_iqm import IQMFakeAdonis
from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.primitives import BackendEstimator
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit_aer import AerSimulator
from scipy.optimize import minimize

import QCut as ck
from QCut import cut
edges = [(0,1), (1,2), (2,0), (2,3), (2,4), (3,4)]
G = nx.Graph(edges)
nx.draw(G, with_labels=True, alpha=0.8, node_size=500)
../_images/538c4308aaff7ac1c22f32ccf9df7dfed1adc0b879227f25b82d7d04527c5d11.png
reformattedHamiltonian = {'paulis': PauliList(['IIIZZ', 'IIZIZ', 'IIZZI', 'IZZII', 'ZIZII', 'ZZIII']),
 'coefs': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
 'const': 0.0}

nqubits = 5
observables = SparsePauliOp(reformattedHamiltonian["paulis"], reformattedHamiltonian["coefs"])
def create_cut_qaoa_circ(n_layers):
    beta = [Parameter(f'beta_{i}') for i in range(n_layers)]
    gamma = [Parameter(f'gamma_{i}') for i in range(n_layers)]
    
    #create actual circuit object
    qc = QuantumCircuit(nqubits)
    #entagling layer
    for i in range(nqubits):
        qc.h(i)
    for layer_index in range(n_layers):
        #cost layer
        count = 0
        for i,j in G.edges():
            qc.rzz(2*gamma[layer_index], i, j)
            if count == 2:
                qc.append(cut, [max(i,j)])
            count += 1
        #mixing layer
        for i in range(nqubits):
            qc.rx(2*beta[layer_index], i)
    return qc

def create_qaoa_circ(n_layers):

    beta = [Parameter(f'beta_{i}') for i in range(n_layers)]
    gamma = [Parameter(f'gamma_{i}') for i in range(n_layers)]

    #create actual circuit object
    qc = QuantumCircuit(nqubits)
    #entagling layer
    for i in range(nqubits):
        qc.h(i)
    for layer_index in range(n_layers):
        #cost layer
        for i,j in G.edges():
            qc.rzz(2*gamma[layer_index], i, j)
        #mixing layer
        for i in range(nqubits):
            qc.rx(2*beta[layer_index], i)
    return qc
qc = create_qaoa_circ(1)

qc.draw("mpl")
../_images/05ff4d264f7140a85245a8a358b4788d1780dca42b32eaf88527c4014ae89ea2.png
qca = create_cut_qaoa_circ(1)

qca.draw("mpl")
../_images/d3b62cc3fde8b8fb95aae6aa6729babd9345aada2285f2170911ea8e3ba8d17b.png
backend = IQMFakeAdonis()
sim = AerSimulator()

cut_circuit = ck.get_locations_and_subcircuits(qca)

transpiled = ck.transpile_subcircuits(cut_circuit, backend, optimization_level=3)

cut_experiment = ck.get_experiment_circuits(transpiled, observables)


tr_qc = transpile(qc, backend, optimization_level=3)
tr_obs = observables.apply_layout(tr_qc.layout)
def get_expectation_backend(circuit, hamiltonian, observables, backend):
    exps_uncut = np.empty(0)
    def execute_circ(theta):
        
        params = {}
        n_layers = len(theta)//2 # number of alternating unitaries
        for i in range(n_layers):
            params[f'beta_{i}'] = theta[i]
            params[f'gamma_{i}'] = theta[i + n_layers]

        p_circuit = circuit.assign_parameters(params, inplace=False)

        fake_estimator = BackendEstimator(backend)
        
        exps = (
            fake_estimator.run([p_circuit] * len([observables]), 
                               [observables]).result().values
        )

        f = hamiltonian["const"] + exps

        global exps_uncut
        exps_uncut = np.append(exps_uncut, f)

        return f
    
    return execute_circ, exps_uncut



def get_expectation_cut(cut_experiment, hamiltonian, observables, backend):
    exps_cut = np.empty(0) #store expectation value from each optimization step
    #execute circuit and calculate expectation value
    def execute_circ(theta):

        params = {}
        n_layers = len(theta)//2 # number of alternating unitaries
        for i in range(n_layers):
            params[f'beta_{i}'] = theta[i]
            params[f'gamma_{i}'] = theta[i + n_layers]

        p_cut_experiment = cut_experiment.assign_parameters(params, inplace=False)
        
        #run the experiments, apply the {0,1} -> [-1,1] post-processing function
        results = ck.run_experiments(p_cut_experiment, shots=2**12, backend=backend)

        reconstructed_expvals = ck.estimate_expectation_values(results, p_cut_experiment.expv_data())

        #Calculate the hamiltonian expectation value
        f = hamiltonian["const"]
        for i, j in zip(reconstructed_expvals, hamiltonian["coefs"]):
            f += i*j
        global exps_cut
        exps_cut =np.append(exps_cut, f)
        return f
    return execute_circ, exps_cut
initial_params = [-np.pi, -np.pi]

expectation, exps_uncut = get_expectation_backend(tr_qc, reformattedHamiltonian, tr_obs, sim)
#minimize uncut qaoa cost using COBYLA
res_uncut = minimize(expectation,
                initial_params,
                method='COBYLA',
                options={'maxiter': 100}
                )
res_uncut
 message: Return from COBYLA because the trust region radius reaches its lower bound.
 success: True
  status: 0
     fun: -1.890625
       x: [-1.878e+00 -2.853e+00]
    nfev: 33
   maxcv: 0.0
expectation_cut, exps_cut = get_expectation_cut(cut_experiment, reformattedHamiltonian, observables, sim)
#minimize uncut qaoa cost using COBYLA
res_cut = minimize(expectation_cut,
                initial_params,
                method='COBYLA',
                options={'maxiter': 100}
                )
res_cut
 message: Return from COBYLA because the trust region radius reaches its lower bound.
 success: True
  status: 0
     fun: -1.9163243770599214
       x: [-2.824e+00 -3.437e+00]
    nfev: 33
   maxcv: 0.0
x = range(len(exps_cut))
x1 = range(len(exps_uncut))
plt.plot(x, exps_cut, label='QCut')
plt.plot(x1, exps_uncut, label='uncut')
plt.axhline(y=-2, color='r', linestyle='--', label = 'Actual minimum')
plt.xlabel('Optimization round')
plt.ylabel('Cost')
plt.title('cut vs uncut Max-Cut QAOA cost on IQMFakeAdonis')
plt.legend()
plt.grid(True)
../_images/4889e6b5c7bc8a8322e2566084770a5cf98ed46cc4082263b3d0455df2bcf4fb.png