06 - Vectorized: A fast QAOA simulator for prototyping

This notebook introduces and provides some simple benchmarks (against qiskit.AerSimulator('statevector')) to demonstrate the ability to simulate relatively modest QAOA circuits fast.

Begin by importing necessary modules

[1]:
#some regular python libraries
import networkx as nx
import numpy as np
from pprint import pprint
import matplotlib.pyplot as plt

#import problem classes from OQ for easy problem creation
from openqaoa.problems import MaximumCut, NumberPartition

#import the QAOA workflow model
from openqaoa import QAOA

#import method to specify the device
from openqaoa.backends import create_device

#importing the time module to record time for simulations
from time import time

Step 1: Create a problem instance

We begin by creating a problem instance for a simple MaximumCut problem for a random graph created using the python networkx module. MaximumCut is a go-to problem to demonstrate QAOA in action.

For this, we first: - create a random graph using the networkx module - using the MaximumCut problem class, we translate into the QUBO formalism to optimize with QAOA

[2]:
nodes = 6
edge_probability = 0.6
g = nx.generators.fast_gnp_random_graph(n=nodes,p=edge_probability, seed=12)

# import graph plotter from openqaoa
from openqaoa.utilities import plot_graph
plot_graph(g)
../_images/notebooks_06_fast_qaoa_simulator_4_0.png
[3]:
# Use the MaximumCut class to instantiate the problem.
maxcut_prob = MaximumCut(g)

# The property `qubo` translates the problem into a binary Qubo problem.
# The binary values can be access via the `asdict()` method.
maxcut_qubo = maxcut_prob.qubo
[4]:
pprint(maxcut_qubo.asdict())
{'constant': 0,
 'metadata': {},
 'n': 6,
 'problem_instance': {'G': {'directed': False,
                            'graph': {},
                            'links': [{'source': 0, 'target': 1},
                                      {'source': 0, 'target': 4},
                                      {'source': 0, 'target': 5},
                                      {'source': 1, 'target': 2},
                                      {'source': 1, 'target': 3},
                                      {'source': 1, 'target': 4},
                                      {'source': 2, 'target': 3},
                                      {'source': 2, 'target': 4},
                                      {'source': 2, 'target': 5},
                                      {'source': 4, 'target': 5}],
                            'multigraph': False,
                            'nodes': [{'id': 0},
                                      {'id': 1},
                                      {'id': 2},
                                      {'id': 3},
                                      {'id': 4},
                                      {'id': 5}]},
                      'problem_type': 'maximum_cut'},
 'terms': [[0, 1],
           [0, 4],
           [0, 5],
           [1, 2],
           [1, 3],
           [1, 4],
           [2, 3],
           [2, 4],
           [2, 5],
           [4, 5]],
 'weights': [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]}

Extract the exact solution for a small enough problem

[5]:
hamiltonian = maxcut_qubo.hamiltonian

# import the brute-force solver to obtain exact solution
from openqaoa.utilities import ground_state_hamiltonian
energy, configuration = ground_state_hamiltonian(hamiltonian)
print(f"Ground State energy: {energy}, Solution: {configuration}")
Ground State energy: -4.0, Solution: ['101000', '111000', '101100', '101010', '100110', '101110', '010001', '011001', '010101', '010011', '000111', '010111']
[6]:
#plot the solution on graph
g_sol = np.copy(g)
pos =  nx.spring_layout(g)
nx.draw_networkx_nodes(g, pos, nodelist=[idx for idx,bit in enumerate(configuration[0]) if bit == '1'], node_color="tab:red")
nx.draw_networkx_nodes(g, pos, nodelist=[idx for idx,bit in enumerate(configuration[0]) if bit == '0'], node_color="tab:blue")
nx.draw_networkx_edges(g, pos)
[6]:
<matplotlib.collections.LineCollection at 0x7fc81c2517e0>
../_images/notebooks_06_fast_qaoa_simulator_9_1.png

Step 2: Build the QAOA model

We will build two different models with the same exact configurations except the device used. To compare the performance of vectorized backend to that of qiskit.statevector_simulator.

[7]:
q_qiskit_list = []
q_vector_list = []

for p in range(1,10):

    ## QAOA with Qiskit Simulator
    # initialize model with default configuration
    q_qiskit = QAOA()

    # optionally configure the following properties of the model

    # device
    qiskit_device = create_device(location='local', name='qiskit.statevector_simulator')
    q_qiskit.set_device(qiskit_device)

    # circuit properties
    q_qiskit.set_circuit_properties(p=p, param_type='standard', init_type='rand', mixer_hamiltonian='x', seed=12)

    # backend properties (already set by default)
    q_qiskit.set_backend_properties(prepend_state=None, append_state=None)

    # classical optimizer properties
    q_qiskit.set_classical_optimizer(method='nelder-mead', maxiter=200, tol=0.001,
                                        optimization_progress=True, cost_progress=True, parameter_log=True)

    q_qiskit_list.append(q_qiskit)


    ## QAOA with Vectorized Simulator
    # initialize model with default configuration
    q_vector = QAOA()

    # optionally configure the following properties of the model

    # device
    vectorized_device = create_device(location='local', name='vectorized')
    q_vector.set_device(vectorized_device)

    # circuit properties
    q_vector.set_circuit_properties(p=p, param_type='standard', init_type='rand', mixer_hamiltonian='x', seed=12)

    # backend properties (already set by default)
    q_vector.set_backend_properties(prepend_state=None, append_state=None)

    # classical optimizer properties
    q_vector.set_classical_optimizer(method='nelder-mead', maxiter=200, tol=0.001,
                            optimization_progress=True, cost_progress=True, parameter_log=True)

    q_vector_list.append(q_vector)

Step 3: Compile and Optimize

  • Once the QAOA model is configured, we need to compile it. Compilation is necessary because the QAOA solver has to interact with the problem in to be able to create the underlying QAOA circuit.

  • The problem is ready to be optimized now. The user can call model.optimize() to initiate the optimization loop.

Since the purpose of this notebook is to compare the speed performance of vectorized, we will run the optimization loop and record the time required to finish the optimization. Repeating this step for different depth of circuits is also presented.

[8]:
times_qiskit = []
times_vectorized = []

for q_qiskit, q_vector in zip(q_qiskit_list, q_vector_list):

    q_qiskit.compile(maxcut_qubo,verbose=False)
    q_vector.compile(maxcut_qubo,verbose=False)

    t1=time()
    q_qiskit.optimize()

    t2=time()

    q_vector.optimize()
    t3=time()

    times_qiskit.append(t2-t1)
    times_vectorized.append(t3-t2)
[9]:
ps = range(1,10)
figure = plt.figure(figsize=(10,7))
plt.plot(ps, times_qiskit, ls = '--', marker = 'o', label='qiskit.statevector_simulator')
plt.plot(ps, times_vectorized, color='red', ls='-.', marker='x', label = 'vectorized')
plt.xlabel('Depth (p)')
plt.ylabel('Time (s)')
plt.title('6-qubits circuit optimization with Nelder-Mead and maxiter=200')
plt.legend(fontsize=12)
[9]:
<matplotlib.legend.Legend at 0x7fc81c100880>
../_images/notebooks_06_fast_qaoa_simulator_14_1.png

Step 4: Accessing the results

Compare the evolution of cost value for both devices for a sanity check

[10]:
vector_opt_results = q_vector.result
qiskit_opt_results = q_qiskit.result
[11]:
fig,ax = plt.subplots(1,1,figsize=(10,7))
fig, ax = vector_opt_results.plot_cost(label='Cost Vector',color='red',ax=ax)
fig, ax = qiskit_opt_results.plot_cost(label='Cost Qiskit',color='blue',ax=ax)
../_images/notebooks_06_fast_qaoa_simulator_17_0.png

The two cost histories show a complete match between the two devices