[1]:
import os, sys

# path to access c++ files
installation_path = os.getenv("INSTALL_PATH")
sys.path.append(installation_path)
[2]:
from cunqa import getQPUs

qpus  = getQPUs()

for q in qpus:
    print(f"QPU {q.id}, backend: {q.backend.name}, simulator: {q.backend.simulator}, version: {q.backend.version}.")

---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[2], line 1
----> 1 from cunqa import getQPUs
      3 qpus  = getQPUs()
      5 for q in qpus:

ModuleNotFoundError: No module named 'cunqa'

Examples for optimizations

Before sending a circuit to the QClient, a transpilation process occurs (if not, it is done by the user). This process, in some cases, can take much time and resources, in addition to the sending cost itself. If we were to execute a single circuit once, it shouldn´t be a big problem, but it is when it comes to variational algorithms.

This quantum-classical algorithms require several executions of the same circuit but changing the value of the parameters, which are optimized in the classical part. In order to optimize this, we developed a functionallity that allows the user to upgrade the circuit parameters with no extra transpilations of the circuit, sending to the QClient the list of the parameters ONLY. This is of much advantage to speed up the computation in the cases in which transpilation takes a significant part of the total time of the simulation.

Let´s see how to work with this feature taking as an example a Variational Quantum Algorithm for state preparation.

We start from a Hardware Efficient Ansatz to build our parametrized circuit:

[3]:
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

def hardware_efficient_ansatz(num_qubits, num_layers):
    qc = QuantumCircuit(num_qubits)
    param_idx = 0
    for _ in range(num_layers):
        for qubit in range(num_qubits):
            phi = Parameter(f'phi_{param_idx}_{qubit}')
            lam = Parameter(f'lam_{param_idx}_{qubit}')
            qc.ry(phi, qubit)
            qc.rz(lam, qubit)
        param_idx += 1
        for qubit in range(num_qubits - 1):
            qc.cx(qubit, qubit + 1)
    qc.measure_all()
    return qc
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[3], line 1
----> 1 from qiskit import QuantumCircuit
      2 from qiskit.circuit import Parameter
      4 def hardware_efficient_ansatz(num_qubits, num_layers):

ModuleNotFoundError: No module named 'qiskit'

The we need a cost function. We will define a target distribution and measure how far we are from it. We choose to prepare a normal distribution among all the \(2^n\) possible outcomes of the circuit.

[4]:
def target_distribution(num_qubits):
    # Define a normal distribution over the states
    num_states = 2 ** num_qubits
    states = np.arange(num_states)
    mean = num_states / 2
    std_dev = num_states / 4
    target_probs = norm.pdf(states, mean, std_dev)
    target_probs /= target_probs.sum()  # Normalize to make it a valid probability distribution
    target_dist = {format(i, f'0{num_qubits}b'): target_probs[i] for i in range(num_states)}
    return target_dist

import pandas as pd
from scipy.stats import entropy, norm

def KL_divergence(counts, n_shots, target_dist):
    # Convert counts to probabilities
    pdf = pd.DataFrame.from_dict(counts, orient="index").reset_index()
    pdf.rename(columns={"index": "state", 0: "counts"}, inplace=True)
    pdf["probability"] = pdf["counts"] / n_shots

    # Create a dictionary for the obtained distribution
    obtained_dist = pdf.set_index("state")["probability"].to_dict()

    # Ensure all states are present in the obtained distribution
    for state in target_dist:
        if state not in obtained_dist:
            obtained_dist[state] = 0.0

    # Convert distributions to lists for KL divergence calculation
    target_probs = [target_dist[state] for state in sorted(target_dist)]
    obtained_probs = [obtained_dist[state] for state in sorted(obtained_dist)]

    # Calculate KL divergence
    kl_divergence = entropy(obtained_probs, target_probs)

    return kl_divergence
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[4], line 12
      9     target_dist = {format(i, f'0{num_qubits}b'): target_probs[i] for i in range(num_states)}
     10     return target_dist
---> 12 import pandas as pd
     13 from scipy.stats import entropy, norm
     15 def KL_divergence(counts, n_shots, target_dist):
     16     # Convert counts to probabilities

ModuleNotFoundError: No module named 'pandas'
[5]:
num_qubits = 6

num_layers = 3

n_shots = 1e5

Simply using the QPU.run() method

At first we should try the intiutive alternative: upgrading parameters at the QClient, transpiling and sending the whole circuit to the QPU.

[6]:
def cost_function_run(params):
    n_shots = 1e5
    target_dist = target_distribution(num_qubits)

    circuit = ansatz.assign_parameters(params)

    result = qpu.run(circuit, transpile = True, opt_level = 0, shots = n_shots).result()

    counts = result.get_counts()

    return KL_divergence(counts, n_shots, target_dist)

Our cost function updates the parameters given by the optimizer, asigns them to the ansatz and sends the circuit with the transpilation option set True. Let´s choose a QPU to work with and go ahead with the optimization:

[7]:
import numpy as np
import time

qpu = qpus[0]
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[7], line 1
----> 1 import numpy as np
      2 import time
      4 qpu = qpus[0]

ModuleNotFoundError: No module named 'numpy'
[8]:
ansatz = hardware_efficient_ansatz(num_qubits, num_layers)

num_parameters = ansatz.num_parameters

initial_parameters = np.zeros(num_parameters)

from scipy.optimize import minimize

i = 0

cost_run = []
individuals_run = []

def callback(xk):
    global i
    e = cost_function_run(xk)
    individuals_run.append(xk)
    cost_run.append(e)
    if i%20 == 0:
        print(f"Iteration step {i}: f(x) = {e}")
    i+=1

tick = time.time()
optimization_result_run = minimize(cost_function_run, initial_parameters, method='COBYLA',
        callback=callback, tol = 0.01,
        options={
        'disp': True,     # Print info at the end
        'maxiter': 4000   # Limit the number of iterations
    })
tack = time.time()
time_run = tack-tick
print()
print("Total optimization time: ", time_run, " s")
print()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 ansatz = hardware_efficient_ansatz(num_qubits, num_layers)
      3 num_parameters = ansatz.num_parameters
      5 initial_parameters = np.zeros(num_parameters)

NameError: name 'hardware_efficient_ansatz' is not defined
[9]:
%matplotlib inline

import matplotlib.pyplot as plt
plt.clf()
plt.plot(np.linspace(0, optimization_result_run.nfev, optimization_result_run.nfev), cost_run, label="Optimization path (run())")
upper_bound = optimization_result_run.nfev
plt.plot(np.linspace(0, upper_bound, upper_bound), np.zeros(upper_bound), "--", label="Target cost")
plt.xlabel("Step"); plt.ylabel("Cost"); plt.legend(loc="upper right"); plt.title(f"n = {num_qubits}, l = {num_layers}, # params = {num_parameters}")
plt.grid(True)
plt.show()
# plt.savefig(f"optimization_run_n_{num_qubits}_p_{num_parameters}.png", dpi=200)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[9], line 1
----> 1 get_ipython().run_line_magic('matplotlib', 'inline')
      3 import matplotlib.pyplot as plt
      4 plt.clf()

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/IPython/core/interactiveshell.py:2456, in InteractiveShell.run_line_magic(self, magic_name, line, _stack_depth)
   2454     kwargs['local_ns'] = self.get_local_scope(stack_depth)
   2455 with self.builtin_trap:
-> 2456     result = fn(*args, **kwargs)
   2458 # The code below prevents the output from being displayed
   2459 # when using magics with decorator @output_can_be_silenced
   2460 # when the last Python token in the expression is a ';'.
   2461 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/IPython/core/magics/pylab.py:99, in PylabMagics.matplotlib(self, line)
     97     print("Available matplotlib backends: %s" % backends_list)
     98 else:
---> 99     gui, backend = self.shell.enable_matplotlib(args.gui.lower() if isinstance(args.gui, str) else args.gui)
    100     self._show_matplotlib_backend(args.gui, backend)

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/IPython/core/interactiveshell.py:3630, in InteractiveShell.enable_matplotlib(self, gui)
   3609 def enable_matplotlib(self, gui=None):
   3610     """Enable interactive matplotlib and inline figure support.
   3611
   3612     This takes the following steps:
   (...)
   3628         display figures inline.
   3629     """
-> 3630     from matplotlib_inline.backend_inline import configure_inline_support
   3632     from IPython.core import pylabtools as pt
   3633     gui, backend = pt.find_gui_and_backend(gui, self.pylab_gui_select)

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/matplotlib_inline/__init__.py:1
----> 1 from . import backend_inline, config  # noqa
      2 __version__ = "0.1.7"  # noqa

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/matplotlib_inline/backend_inline.py:6
      1 """A matplotlib backend for publishing figures via display_data"""
      3 # Copyright (c) IPython Development Team.
      4 # Distributed under the terms of the BSD 3-Clause License.
----> 6 import matplotlib
      7 from matplotlib import colors
      8 from matplotlib.backends import backend_agg

ModuleNotFoundError: No module named 'matplotlib'

Using QJob.upgrade_parameters()

The first step now is to create the qjob.QJob object that which parameters we are going to upgrade in each step of the optimization; for that, we must run a circuit with initial parameters in a QPU, the procedure is as we explained above:

[10]:
ansatz = hardware_efficient_ansatz(num_qubits, num_layers)

num_parameters = ansatz.num_parameters

initial_parameters = np.zeros(num_parameters)

circuit = ansatz.assign_parameters(initial_parameters)

qjob = qpu.run(circuit, transpile = True, opt_level = 0, shots = n_shots)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 1
----> 1 ansatz = hardware_efficient_ansatz(num_qubits, num_layers)
      3 num_parameters = ansatz.num_parameters
      5 initial_parameters = np.zeros(num_parameters)

NameError: name 'hardware_efficient_ansatz' is not defined

Now that we have sent to the virtual QPU the transpiled circuit, we can use the method qjob.QJob.upgrade_parameters() to change the rotations of the gates:

[11]:
print("Result with initial_parameters: ")
print(qjob.result().get_counts())

random_parameters = np.random.uniform(0, 2 * np.pi, num_parameters).tolist()
qjob.upgrade_parameters(random_parameters)

print()
print("Result with random_parameters: ")
print(qjob.result().get_counts())
Result with initial_parameters:
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[11], line 2
      1 print("Result with initial_parameters: ")
----> 2 print(qjob.result().get_counts())
      4 random_parameters = np.random.uniform(0, 2 * np.pi, num_parameters).tolist()
      5 qjob.upgrade_parameters(random_parameters)

NameError: name 'qjob' is not defined

Important considerations:

  • The method acepts parameters in a list, if you have a numpy.array, simply apply .tolist() to transform it.

  • When sending the circuit and setting transpile=True, we should be carefull that the transpilation process doesn’t condense gates and combine parameters, therefore, if the user wants cunqato transpile, they must set opt_level=0.

Note that qjob.QJob.upgrade_parameters() is a non-blocking call, as it was qpu.QPU.run().

Now that we are familiar with the procedure, we can design a cost funtion that takes a set of parameters, upgrades the qjob.QJob, gets the result and calculates the divergence from the desired distribution:

[12]:
def cost_function(params):
    n_shots = 100000
    target_dist = target_distribution(num_qubits)

    result = qjob.upgrade_parameters(params.tolist()).result()

    counts = result.get_counts()

    return KL_divergence(counts, n_shots, target_dist)

Now we are ready to start our optimization. We will use scipy.optimize to minimize the divergence of our result distribution from the target one:

[13]:
from scipy.optimize import minimize
import time

i = 0

initial_parameters = np.zeros(num_parameters)

cost = []
individuals = []

def callback(xk):
    global i
    e = cost_function(xk)
    individuals.append(xk)
    cost.append(e)
    if i%10 == 0:
        print(f"Iteration step {i}: f(x) = {e}")
    i+=1

tick = time.time()
optimization_result = minimize(cost_function, initial_parameters, method='COBYLA',
        callback=callback, tol = 0.01,
        options={
        'disp': True,     # Print info during iterations
        'maxiter': 4000     # Limit the number of iterations
    })
tack = time.time()
time_up = tack-tick
print()
print("Total optimization time: ", time_up, " s")
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[13], line 1
----> 1 from scipy.optimize import minimize
      2 import time
      4 i = 0

ModuleNotFoundError: No module named 'scipy'

We can plot the evolution of the cost function during the optimization:

[14]:
%matplotlib inline

import matplotlib.pyplot as plt
plt.clf()
plt.plot(np.linspace(0, optimization_result.nfev, optimization_result.nfev), cost, label="Optimization path (upgrade_params())")
plt.plot(np.linspace(0, optimization_result_run.nfev, optimization_result_run.nfev), cost_run, label="Optimization path (run())")
upper_bound = max(optimization_result_run.nfev, optimization_result.nfev)
plt.plot(np.linspace(0, upper_bound, upper_bound), np.zeros(upper_bound), "--", label="Target cost")
plt.xlabel("Step"); plt.ylabel("Cost"); plt.legend(loc="upper right"); plt.title(f"n = {num_qubits}, l = {num_layers}, # params = {num_parameters}")
plt.grid(True)
plt.show()
# plt.savefig(f"optimization_n_{num_qubits}_p_{num_parameters}.png", dpi=200)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[14], line 1
----> 1 get_ipython().run_line_magic('matplotlib', 'inline')
      3 import matplotlib.pyplot as plt
      4 plt.clf()

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/IPython/core/interactiveshell.py:2456, in InteractiveShell.run_line_magic(self, magic_name, line, _stack_depth)
   2454     kwargs['local_ns'] = self.get_local_scope(stack_depth)
   2455 with self.builtin_trap:
-> 2456     result = fn(*args, **kwargs)
   2458 # The code below prevents the output from being displayed
   2459 # when using magics with decorator @output_can_be_silenced
   2460 # when the last Python token in the expression is a ';'.
   2461 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/IPython/core/magics/pylab.py:99, in PylabMagics.matplotlib(self, line)
     97     print("Available matplotlib backends: %s" % backends_list)
     98 else:
---> 99     gui, backend = self.shell.enable_matplotlib(args.gui.lower() if isinstance(args.gui, str) else args.gui)
    100     self._show_matplotlib_backend(args.gui, backend)

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/IPython/core/interactiveshell.py:3630, in InteractiveShell.enable_matplotlib(self, gui)
   3609 def enable_matplotlib(self, gui=None):
   3610     """Enable interactive matplotlib and inline figure support.
   3611
   3612     This takes the following steps:
   (...)
   3628         display figures inline.
   3629     """
-> 3630     from matplotlib_inline.backend_inline import configure_inline_support
   3632     from IPython.core import pylabtools as pt
   3633     gui, backend = pt.find_gui_and_backend(gui, self.pylab_gui_select)

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/matplotlib_inline/__init__.py:1
----> 1 from . import backend_inline, config  # noqa
      2 __version__ = "0.1.7"  # noqa

File /opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/matplotlib_inline/backend_inline.py:6
      1 """A matplotlib backend for publishing figures via display_data"""
      3 # Copyright (c) IPython Development Team.
      4 # Distributed under the terms of the BSD 3-Clause License.
----> 6 import matplotlib
      7 from matplotlib import colors
      8 from matplotlib.backends import backend_agg

ModuleNotFoundError: No module named 'matplotlib'