A "quantum cost function" is the mean value of a Hermitian operator, wherein that mean value is calculated empirically from the data yielded by a physical quantum computer. (In this notebook, however, we fake that data with a Qubiter simulator. Just warming up. Brrmm! Brrmm! Everything is setup so that calls to a real physical qc such as Rigetti's qc can be easily substituted for the calls to the Qubiter simulator)
A Stairs circuit is a quantum circuit that in its full generality can parametrize a completely general quantum state vector.
In this notebook, we consider a special quantum cost function in which a Stairs circuit provides the state vector with which the mean values are calculated. I like to say that the Stairs circuit is the kernel of the quantum cost function. Our ultimate intention is to minimize this cost function using gradient descent. But to do that, we must first calculate the derivatives (gradients) of the cost function. That is what we will do in this notebook: calculate all the derivatives of a quantum cost function whose kernel is a Stairs circuit.
In particular, this notebook runs through their paces 4 Qubiter classes that increasingly build on each other
StairsCkt_writer
, writes English and Picture files for a Stairs quantum circuitStairsDeriv_writer
, writes English and Picture files for several quantum circuits that are needed to evaluate the derivatives of a Stairs circuitStairsDeriv_native
, evaluates the 4 derivatives of a single gate of a Stairs circuit. It uses native Qubiter simulators to do this. Qubiter also has an analogous class StairsDeriv_rigetti
that uses Rigetti simulators or their real physical qc. We will demo that class in a future notebook.StairsAllDeriv_native
, evaluates all the derivatives, for all the gates of a Stairs circuit. It uses native Qubiter simulators to do this. Qubiter also has an analogous class StairsAllDeriv_rigetti
that uses Rigetti simulators or their real physical qc. We will demo that class in a future notebook.For an explanation of the theory behind the software that is being demo-ed in this notebook, see the following pdf included with the Qubiter distribution.
Title:
Calculation of the Gradient of a Quantum Cost Function using "Threading". Application of these "threaded gradients" to a Quantum Neural Net inspired by Quantum Bayesian Networks,
https://github.com/artiste-qb-net/qubiter/blob/master/adv_applications/threaded_grad.pdf
First change your working directory to the Qubiter directory in your computer, and add its path to the path environment variable.
import os
import sys
print(os.getcwd())
os.chdir('../../')
print(os.getcwd())
sys.path.insert(0,os.getcwd())
/home/rrtucci/PycharmProjects/qubiter/qubiter/jupyter_notebooks /home/rrtucci/PycharmProjects/qubiter
from qubiter.adv_applications.StairsCkt_writer import *
loaded OneQubitGate, WITHOUT autograd.numpy
# print docstring of the class
print(StairsCkt_writer.__doc__)
This class is a subclass of class SEO_writer and it writes a "Stairs Circuit". For example, this is what the Picture file of a Stairs Circuit looks like for num_qbits = 3 U | | O---U | @---U | O---O---U O---@---U @---O---U @---@---U Here, U is a general U(2) matrix with 4 parameters, all of which can be made into placeholder variables. If each U is represented by a node and the controls of each U represent its parents, then this quantum circuit can be represented by a fully connected Quantum Bayesian Network (QB net). (See my >10 year old blog called "Quantum Bayesian Networks" for more info than you would ever want to know about QB nets). This class can also be asked to construct a QB net that is **not** fully connected, by limiting the number of controls for a given U to fewer than all the ones to its left. For example, suppose that in the num_qbits=3 case, we restrict the parents of the U in the last step to just one, instead of the 2 parents that it has in the fully connected case. Then we get U | | O---U | @---U | O---+---U @---+---U or U | | O---U | @---U | | O---U | @---U The constructor of this class has as input an ordered dictionary called gate_str_to_rads_list. This dictionary gives for each gate in the quantum circuit, a gate string gate_str that specifies the gate. gate_str_to_rads_list maps gate_str to a list of 4 floats (or placeholder variables for those floats) for the 4 parameters of the U matrix. For example, here are possible values for gate_str_to_rads_list for the num_qbits=3 fully connected qb net with every rads_list item filled with the same constant .3 {'prior': [0.3, 0.3, 0.3, 0.3], '2F': [0.3, 0.3, 0.3, 0.3], '2T': [0.3, 0.3, 0.3, 0.3], '2F1F': [0.3, 0.3, 0.3, 0.3], '2F1T': [0.3, 0.3, 0.3, 0.3], '2T1F': [0.3, 0.3, 0.3, 0.3], '2T1T': [0.3, 0.3, 0.3, 0.3]} with every rads_list item filled by a random number between 0 and 2pi {'prior': [0.46731839721496604, 0.012285135138256131, 0.20001353832948487, 0.36694428209569985], '2F': [4.1968011007222898, 5.1978252498063808, 4.8063090848060321, 4.2509081392354409], '2T': [4.3359074640905213, 2.0749617893052315, 4.555666727197961, 5.3092010293653802], '2F1F': [0.99177045463186475, 3.3344615340103325, 2.1441702948866386, 2.4603764283165521], '2F1T': [4.0909522483111145, 2.0714182784661888, 5.4034187072431923, 6.0856723571386766], '2T1F': [4.0000452017061194, 3.7193341571216658, 3.381322125034953, 5.4492142181489802], '2T1T': [6.2597553541046853, 0.077807529496169509, 3.7389318319862217, 6.2233264819972307]} with every rads_list item filled by a unique placeholder variable string {'prior': ['#50', '#51', '#52', '#53'], '2F': ['#500', '#501', '#502', '#503'], '2T': ['#510', '#511', '#512', '#513'], '2F1F': ['#5000', '#5001', '#5002', '#5003'], '2F1T': ['#5010', '#5011', '#5012', '#5013'], '2T1F': ['#5100', '#5101', '#5102', '#5103'], '2T1T': ['#5110', '#5111', '#5112', '#5113']} This is what gate_str_to_rads_list looks like in the num_qbits=3 case, when the last U has only one parent (qbit 2) instead of two parents ( qbits 1 and 2): {'prior': ['#50', '#51', '#52', '#53'], '2F': ['#500', '#501', '#502', '#503'], '2T': ['#510', '#511', '#512', '#513'], '2F1_': ['#5050', '#5051', '#5052', '#5053'], '2T1_': ['#5150', '#5151', '#5152', '#5153']} Note that all placeholder strings begin with '#5' to insure that once the hash character is removed, the remaining number doesn't start with '0'. Note that characters '_' and '5' represent bits whose values are unspecified. Attributes ---------- gate_str_to_rads_list : OrderedDict[str, list[float|str]]
Before using class StairsCkt_writer
to write a quantum circuit,
we introduce some static methods belonging to this class
that are very helpful, not just with
this class, but with all the other classes related to the stairs circuit.
get_gate_str_to_rads_list()
returns a dictionary that maps a gate string to
a list of 4 radians. The gate string is the unique name we will give to each gate of
the Stairs circuit, and the 4 radians are the values of the angles
characterizing a general U(2) transformation, $(t_0, t_1, t_2, t_3)$
in $e^{i(t_0, + t_1\sigma_X + t_2\sigma_Y+ t_3\sigma_Z)}$,
where $\sigma_X, \sigma_Y, \sigma_Z$ are the Pauli matrices.
The user can select among 3 fill types, either 'const' for a constant value,
'rand' for random values, or '#int' for a hash followed by a unique int.
num_qbits = 3
for fill_type in ['const', 'rand', '#int']:
di = StairsCkt_writer.get_gate_str_to_rads_list(
num_qbits, fill_type, rads_const=.3)
pp.pprint(di)
OrderedDict([('prior', [0.3, 0.3, 0.3, 0.3]), ('2F', [0.3, 0.3, 0.3, 0.3]), ('2T', [0.3, 0.3, 0.3, 0.3]), ('2F1F', [0.3, 0.3, 0.3, 0.3]), ('2F1T', [0.3, 0.3, 0.3, 0.3]), ('2T1F', [0.3, 0.3, 0.3, 0.3]), ('2T1T', [0.3, 0.3, 0.3, 0.3])]) OrderedDict([('prior', [0.3731542025285749, 0.8682261236993244, 0.4098652664932987, 0.18735731532561795]), ('2F', [5.371921981411959, 4.922695285300463, 1.2485666086608223, 0.24257745941669465]), ('2T', [3.822891578429376, 4.97195340729012, 4.558138553696292, 1.0280497429106734]), ('2F1F', [4.31713490932788, 3.094118464780498, 0.9737399907025676, 0.9334937040849988]), ('2F1T', [2.180330096430294, 3.389786926987428, 4.0467423880791715, 3.370095598522802]), ('2T1F', [2.558702747500637, 1.8959781670397253, 5.026146283583697, 0.09514512424355208]), ('2T1T', [4.404715054981431, 1.1885186137925632, 5.7845578291584445, 1.176567854555499])]) OrderedDict([('prior', ['#50', '#51', '#52', '#53']), ('2F', ['#500', '#501', '#502', '#503']), ('2T', ['#510', '#511', '#512', '#513']), ('2F1F', ['#5000', '#5001', '#5002', '#5003']), ('2F1T', ['#5010', '#5011', '#5012', '#5013']), ('2T1F', ['#5100', '#5101', '#5102', '#5103']), ('2T1T', ['#5110', '#5111', '#5112', '#5113'])])
Above, we asked get_gate_str_to_rads_list()
to print a dictionary for a fully connected Quantum Bayesian network,
one in which every gate has all qubits to the left of its U as parents. It is
also possible to ask get_gate_str_to_rads_list()
to print a dictionary for a non-fully connected QB net,
wherein only some, not all, of the qubits to the left of the U are parents.
This can be done by specifying a value for
u2_bit_to_higher_bits
For a 3 qubit case, if one sets that variable to
{0: [1, 2], 1: [2], 2: []}
or to None, one gets a fully connected QB net. This is what happens if one sets it to
{0: [2], 1: [2], 2: []}
instead:
u2_bit_to_higher_bits = {0: [2], 1: [2], 2: []}
di = StairsCkt_writer.get_gate_str_to_rads_list(
num_qbits, "#int", u2_bit_to_higher_bits=u2_bit_to_higher_bits)
pp.pprint(di)
OrderedDict([('prior', ['#50', '#51', '#52', '#53']), ('2F', ['#500', '#501', '#502', '#503']), ('2T', ['#510', '#511', '#512', '#513']), ('2F1_', ['#5050', '#5051', '#5052', '#5053']), ('2T1_', ['#5150', '#5151', '#5152', '#5153'])])
Another helpful static method of the class StairsCkt_writer
is get_var_num_to_rads()
.
If the input di
equals a gate_str_to_rads_list
dictionary,
then this method extracts all the ints to the right of a hash
and maps them to a float. The float can be either a constant (fill_type='const')
or a random number (fill_type='rand')
vn_to_r = StairsCkt_writer.get_var_num_to_rads(di,
fill_type='const',
rads_const=.3)
pp.pprint(vn_to_r)
{50: 0.3, 51: 0.3, 52: 0.3, 53: 0.3, 500: 0.3, 501: 0.3, 502: 0.3, 503: 0.3, 510: 0.3, 511: 0.3, 512: 0.3, 513: 0.3, 5050: 0.3, 5051: 0.3, 5052: 0.3, 5053: 0.3, 5150: 0.3, 5151: 0.3, 5152: 0.3, 5153: 0.3}
Another helpful static method of the class StairsCkt_writer
is make_array_from_gate_str_to_rads_list()
.
If the input di
equals a gate_str_to_rads_list
dictionary,
then this method converts that dictionary into a numpy array. (It works even for strings!)
arr = StairsCkt_writer.make_array_from_gate_str_to_rads_list(di)
print("arr=\n", arr)
arr= [['#50' '#51' '#52' '#53'] ['#500' '#501' '#502' '#503'] ['#510' '#511' '#512' '#513'] ['#5050' '#5051' '#5052' '#5053'] ['#5150' '#5151' '#5152' '#5153']]
After much ado, we finally call the constructor of class StairsCkt_writer
.
This writes English and Picture files in the io_folder
.
num_qbits = 4
gate_str_to_rads_list = StairsCkt_writer.get_gate_str_to_rads_list(
num_qbits, '#int')
file_prefix = 'stairs_writer_test'
emb = CktEmbedder(num_qbits, num_qbits)
wr = StairsCkt_writer(gate_str_to_rads_list, file_prefix, emb)
wr.close_files()
Next, we ask the writer wr
to print the English and Picture files that it just created
wr.print_eng_file(jup=True)
wr.print_pic_file(jup=True)
1 | U_2_ #50 #51 #52 #53 AT 3 | 2 | U_2_ #500 #501 #502 #503 AT 2 IF 3F | 3 | U_2_ #510 #511 #512 #513 AT 2 IF 3T | 4 | U_2_ #5000 #5001 #5002 #5003 AT 1 IF 3F 2F | 5 | U_2_ #5010 #5011 #5012 #5013 AT 1 IF 3F 2T | 6 | U_2_ #5100 #5101 #5102 #5103 AT 1 IF 3T 2F | 7 | U_2_ #5110 #5111 #5112 #5113 AT 1 IF 3T 2T | 8 | U_2_ #50000 #50001 #50002 #50003 AT 0 IF 3F 2F 1F | 9 | U_2_ #50010 #50011 #50012 #50013 AT 0 IF 3F 2F 1T | 10 | U_2_ #50100 #50101 #50102 #50103 AT 0 IF 3F 2T 1F | 11 | U_2_ #50110 #50111 #50112 #50113 AT 0 IF 3F 2T 1T | 12 | U_2_ #51000 #51001 #51002 #51003 AT 0 IF 3T 2F 1F | 13 | U_2_ #51010 #51011 #51012 #51013 AT 0 IF 3T 2F 1T | 14 | U_2_ #51100 #51101 #51102 #51103 AT 0 IF 3T 2T 1F | 15 | U_2_ #51110 #51111 #51112 #51113 AT 0 IF 3T 2T 1T |
1 | U | | | | 2 | O---U | | | 3 | @---U | | | 4 | O---O---U | | 5 | O---@---U | | 6 | @---O---U | | 7 | @---@---U | | 8 | O---O---O---U | 9 | O---O---@---U | 10 | O---@---O---U | 11 | O---@---@---U | 12 | @---O---O---U | 13 | @---O---@---U | 14 | @---@---O---U | 15 | @---@---@---U |
from qubiter.adv_applications.StairsDerivCkt_writer import *
# print docstring of the class
print(StairsDerivCkt_writer.__doc__)
This class is a subclass of `SEO_writer`. It writes several intermediary stairs derivative circuits that will be used in class `StairsDeriv_native` for calculating the gradients of a quantum cost function (mean hamiltonian). Suppose U = exp[i*(t_0 + t_1*sigx + t_2*sigy + t_3*sigz)], where sigx, sigy, sigz are the Pauli matrices and t_r for r in range(4) are 4 real parameters. To take the derivative wrt t_r of a given multi-controlled gate U in a stairs circuit, we need to evaluate several circuits (we call them dparts, which stands for derivative parts). Say, for instance, that GATE= @---O---+---U. To calculate d/dt_r GATE(t_0, t_1, t_2, t_3), for r=0,1, 2, 3, we need to calculate a new circuit wherein the GATE in the parent circuit is replaced by sum_k c_k @---@---O---+---U_k (which is said to have `has_neg_polarity`=False) and sum_k c_k @---@---O---+---U_k Z---@---O | | (which is said to have `has_neg_polarity`=True) Also, some extra stuff (a coda) must be appended to the end of the parent stairs circuit. Note that an extra "ancilla" qbit has been added (as the new last qubit) to the parent stairs circuit being differentiated. So if the parent stairs circuit has a number `parent_num_qbits` of qubits, then the one written by this class has that many qubits plus one. The index r which is in range(4) is called the derivative direction ( `deriv_direc`) `gate_str_to_rads_list` is the same as for the parent stairs circuit. `deriv_gate_str` is a well formed gate_str that specifies which U is being differentiated The index k is given as a string called `dpart_name` ("dpart" stands for derivative part). The coefficients c_k can be obtained via the method get_coef_of_dpart() Each U_k is a U(2) matrix itself, and its 4 parameters are defined in terms of the parameters tlist=[t_0, t_1, t_2, t_3] of the U(tlist) being differentiated, via 4 functions of tlist. These functions can be obtained via the method get_fun_name_to_fun(). Attributes ---------- deriv_direc : int in range(4) deriv_gate_str : str dpart_name : str gate_str_to_rads_list : dict[str, list[float]] has_neg_polarity : bool
num_qbits = 4
parent_num_qbits = num_qbits - 1 # one bit for ancilla
gate_str_to_rads_list = StairsCkt_writer.\
get_gate_str_to_rads_list(
parent_num_qbits, 'const', rads_const=np.pi/2)
file_prefix = 'stairs_deriv_writer_test'
emb = CktEmbedder(num_qbits, num_qbits)
One of the inputs to the constructor of class StairsDerivCkt_writer
is deriv_gate_str
, which should be a well-formed gate string specifying
what gate of the stairs circuit we want to differentiate.
For that, we will use the second key of the gate_str_to_rads_list
dictionary
deriv_gate_str = list(gate_str_to_rads_list.keys())[2]
print(deriv_gate_str)
2T
We next call the constructor of class
StairsDerivCkt_writer
for two typical cases,
and then print the English and Picutre files for each of the 2 cases
for deriv_direc, dpart_name, has_neg_polarity in \
[(0, 'single', None), (3, 's', True)]:
wr = StairsDerivCkt_writer(deriv_gate_str,
has_neg_polarity,
deriv_direc,
dpart_name,
gate_str_to_rads_list,
file_prefix, emb)
wr.close_files()
print("%%%%%%%%%%%%%%%%%%%%%%%%%%")
wr.print_eng_file(jup=True)
wr.print_pic_file(jup=True)
%%%%%%%%%%%%%%%%%%%%%%%%%%
1 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 2 | 2 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 1 IF 2F | 3 | HAD2 AT 3 | 4 | U_2_ 180.000000 90.000000 90.000000 90.000000 AT 1 IF 3T 2T | 5 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 0 IF 2F 1F | 6 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 0 IF 2F 1T | 7 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 0 IF 2T 1F | 8 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 0 IF 2T 1T |
1 | | U | | | 2 | | O---U | | 3 | H | | | | 4 | @---@---U | | 5 | | O---O---U | 6 | | O---@---U | 7 | | @---O---U | 8 | | @---@---U |
%%%%%%%%%%%%%%%%%%%%%%%%%%
1 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 2 | 2 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 1 IF 2F | 3 | HAD2 AT 3 | 4 | U_2_ 90.000000 0.000000 0.000000 90.000000 AT 1 IF 3T 2T | 5 | SIGZ AT 3 IF 2T | 6 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 0 IF 2F 1F | 7 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 0 IF 2F 1T | 8 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 0 IF 2T 1F | 9 | U_2_ 90.000000 90.000000 90.000000 90.000000 AT 0 IF 2T 1T |
1 | | U | | | 2 | | O---U | | 3 | H | | | | 4 | @---@---Uz | | 5 | Z---@ | | | 6 | | O---O---U | 7 | | O---@---U | 8 | | @---O---U | 9 | | @---@---U |
from qubiter.adv_applications.StairsDeriv_native import *
# print docstring of the class
print(StairsDeriv_native.__doc__)
This class is a child of StairsDeriv. Its main purpose is to override the method get_mean_val() of its abstract parent class StairsDeriv. In this class, the simulation necessary to evaluate the output of get_mean_val() is done by native, Qubiter simulators. Attributes ----------
num_qbits = 4
parent_num_qbits = num_qbits - 1 # one bit for ancilla
# u2_bit_to_higher_bits = None
u2_bit_to_higher_bits = {0: [2], 1: [2], 2: []}
gate_str_to_rads_list = StairsCkt_writer.\
get_gate_str_to_rads_list(parent_num_qbits,
'#int', rads_const=np.pi/2,
u2_bit_to_higher_bits=u2_bit_to_higher_bits)
pp.pprint(gate_str_to_rads_list)
OrderedDict([('prior', ['#50', '#51', '#52', '#53']), ('2F', ['#500', '#501', '#502', '#503']), ('2T', ['#510', '#511', '#512', '#513']), ('2F1_', ['#5050', '#5051', '#5052', '#5053']), ('2T1_', ['#5150', '#5151', '#5152', '#5153'])])
deriv_gate_str = list(gate_str_to_rads_list.keys())[2]
print(deriv_gate_str)
2T
file_prefix = 'stairs_deriv_native_test'
The Hamiltonian hamil
is entered as an object of class QubitOperator
of the open source library OpenFermion
. The class
constructor simplifies the input. Once simplified, hamil
must be a
linear combination with real coefficients of "pauli strings"
hamil = QubitOperator('X1 Y0 X1 Y1', .4) +\
QubitOperator('Y2 X1', .7)
print(hamil)
0.4 [Y0 Y1] + 0.7 [X1 Y2]
Creating an object der
of StairsCkt_writer
doesn't do the whole trick. You
then need to call der.get_mean_val(var_num_to_rads)
to get a list of the 4 partial derivatives wrt
the 4 parameters of the U(2) transformation for the gate called deriv_gate_str
der = StairsDeriv_native(deriv_gate_str,
gate_str_to_rads_list, file_prefix,
parent_num_qbits, hamil)
var_num_to_rads = StairsCkt_writer.get_var_num_to_rads(
gate_str_to_rads_list, 'const', rads_const=np.pi/2)
partials_list = der.get_mean_val(var_num_to_rads)
print('partials_list=', partials_list)
partials_list= [-0.21348209139938928, 0.17099652309690933, 0.21797739856238524, 0.20955227323123532]
from qubiter.adv_applications.StairsAllDeriv_native import *
# print docstring of the class
print(StairsAllDeriv_native.__doc__)
This class is a child of StairsDeriv_native. For the parent class, the get_mean_val() method returns a list of 4 partial derivatives belonging to a particular gate string (a gate_str is a key in gate_str_to_rads_list). For this class, get_mean_val() returns an ordered dictionary mapping each gate_str to its 4 partials. Attributes ---------- deriv_gate_str : str
num_qbits = 4
parent_num_qbits = num_qbits - 1 # one bit for ancilla
# u2_bit_to_higher_bits = None
u2_bit_to_higher_bits = {0: [2], 1: [2], 2: []}
gate_str_to_rads_list = StairsCkt_writer.\
get_gate_str_to_rads_list(parent_num_qbits,
'#int', rads_const=np.pi/2,
u2_bit_to_higher_bits=u2_bit_to_higher_bits)
pp.pprint(gate_str_to_rads_list)
file_prefix = 'stairs_all_deriv_native_test'
hamil = QubitOperator('Y0 X1', .4) +\
QubitOperator('X0', .7)
der = StairsAllDeriv_native(gate_str_to_rads_list, file_prefix,
parent_num_qbits, hamil)
var_num_to_rads = StairsCkt_writer.get_var_num_to_rads(
gate_str_to_rads_list, 'const', rads_const=np.pi/2)
gate_str_to_partials_list = der.get_mean_val(var_num_to_rads)
pp.pprint(gate_str_to_partials_list)
OrderedDict([('prior', ['#50', '#51', '#52', '#53']), ('2F', ['#500', '#501', '#502', '#503']), ('2T', ['#510', '#511', '#512', '#513']), ('2F1_', ['#5050', '#5051', '#5052', '#5053']), ('2T1_', ['#5150', '#5151', '#5152', '#5153'])]) OrderedDict([('prior', [-2.2204460492503132e-17, 0.109872851097167, 0.109872851097167, 0.06330213907425795]), ('2F', [-0.315426943689481, 0.19253599609389338, 0.15844289259167715, 0.05719217691065069]), ('2T', [-0.310778637864654, 0.0241105245882962, 0.01984117949460967, 0.007161951092978829]), ('2F1_', [-0.13581869634833252, 0.3904738483600582, 0.5194610247254097, 0.577318576543139]), ('2T1_', [-0.28828700434347215, 0.048897502352654526, 0.0650500585002077, 0.07229533187256816])])