[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 anumpy.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 wantscunqa
to transpile, they must setopt_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'