pqcprep.pqc_tools

Collection of functions relating to setting up a QCNN.

  1"""
  2Collection of functions relating to setting up a QCNN.
  3"""
  4import numpy as np
  5from qiskit import QuantumCircuit, QuantumRegister, transpile
  6from qiskit_aer import StatevectorSimulator
  7from qiskit.circuit import ParameterVector
  8from qiskit.circuit.library import U3Gate
  9from itertools import combinations
 10
 11def N_gate(params, real=False):
 12    r"""
 13    Constructs the two-qubit three-parameter $\mathcal{N}$ gate, defined as 
 14    $$\mathcal{N}(\alpha, \beta, \gamma) = \exp \left( i \left[ \alpha X \otimes X + \beta Y \otimes Y + \gamma Z \otimes Z \right] \right),$$
 15    where $X$, $Y$, $Z$ are the Pauli operators. 
 16
 17    This is implemented via three CX gates, three Rz gates and two Ry gates, as shown in [Vatan 2004](https://arxiv.org/pdf/quant-ph/0308006).  
 18
 19    Arguments:
 20    ----
 21    - **params** : *array_like*
 22
 23        Array-like object containing the gate parameters, corresponding to the qubit rotation angles. 
 24
 25    - **real** : *boolean*
 26       
 27        If True, a two-parameter version of the $\mathcal{N}$ gate is implemented instead, with one of the CX gates and all of the 
 28        Rz gates removed. This ensures real amplitudes. Default is False.   
 29             
 30    Returns:
 31    ----
 32    - **circuit** : *QuantumCircuit*
 33
 34        Implementation of the $\mathcal{N}$ gate as a qiskit `QuantumCircuit`. 
 35
 36    """
 37
 38    if real:
 39        circuit = QuantumCircuit(2, name="RN Gate")
 40        circuit.cx(1, 0)
 41        circuit.ry(params[0], 0)
 42        circuit.ry(params[1], 1)
 43        circuit.cx(0, 1)
 44    else:    
 45        circuit = QuantumCircuit(2, name="N Gate")
 46        circuit.rz(-np.pi / 2, 1)
 47        circuit.cx(1, 0)
 48        circuit.rz(params[0], 0)
 49        circuit.ry(params[1], 1)
 50        circuit.cx(0, 1)
 51        circuit.ry(params[2], 1)
 52        circuit.cx(1, 0)
 53        circuit.rz(np.pi / 2, 0)
 54
 55    return circuit 
 56
 57def input_layer(n, m, par_label, ctrl_state=0, real=False, params=None, AA=False, shift=0, wrap=False): 
 58    r"""
 59
 60    Implement a QCNN input layer as a parametrised quantum circuit. 
 61
 62    The layer consists of a sequence of controlled arbitrary single-qubit operations (CU3 operations)
 63    applied to the target register with the input qubits acting a controls. This feeds information 
 64    about the input register state into the target register. 
 65    
 66    Arguments:
 67    ----
 68    - **n** : *int*
 69
 70        Number of qubits in the input register. 
 71
 72    - **m** : *int* 
 73
 74        Number of qubits in the target register. 
 75
 76    - **par_label** : *str*
 77
 78        Label to assign to the parameter vector. 
 79
 80    - **ctrl_state** : *int* 
 81
 82        Control state of the controlled gates. If equal to 0, the controlled operation is applied 
 83        when the control qubit is in state $\ket{0}$. If equal to 1, the controlled operation is 
 84        applied when the control qubit is in state $\ket{1}$. Default is 0. 
 85
 86    - **real** : *boolean*
 87
 88        If True, controlled Ry rotations are applied instead of CU3 operations. This ensures real 
 89        amplitudes. Default is False.     
 90
 91    - **params** : *array_like*, *optional*
 92
 93        Directly assign values, stored in `params`, to the circuit parameters instead of creating a `ParameterVector`.      
 94
 95    - **AA** : *boolean* 
 96
 97        If True, each input qubit controls an operation on each target qubit, corresponding to an "all-to-all" layer topology. 
 98        Default is False. 
 99
100    - **shift** : *int* 
101
102        If `AA` is False, the $j$th input qubit controls an operation applied on the $(j+s)$th target qubit with wrap-around 
103        for `n > m`. Default is 0.    
104
105    - **wrap**: *boolean* 
106
107        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.     
108
109    Returns:
110    ---
111    - **circuit** : *QuantumCircuit* 
112
113        The qiskit `QuantumCircuit` implementation of the input layer. 
114    
115    """
116
117    # set up circuit 
118    qc = QuantumCircuit(n+m, name="Input Layer")
119    qubits = list(range(n+m))
120
121    # number of parameters used by each gate 
122    num_par = 3 if real==False else 1
123
124    # number of gates applied per layer 
125    num_gates = n if AA==False else n*m
126
127    # set up parameter vector 
128    if params == None:
129        params = ParameterVector(par_label, length= num_par * num_gates)
130    param_index = 0
131
132    # define weight re-mapping function:
133    def wrap_angle(theta):
134        if wrap:
135            return map_angle(theta)  
136        else:
137            return theta 
138
139    # apply gates to qubits 
140    if AA: 
141        for i in qubits[:n]:
142            for j in qubits[n:]:
143                if real:
144                    qc.cry(wrap_angle(params[int(param_index)]), qubits[i], qubits[j],ctrl_state=int(ctrl_state))
145                    param_index += 1
146                else:  
147                    par = params[int(param_index) : int(param_index + num_par)] 
148                    cu3 = U3Gate(wrap_angle(par[0]),wrap_angle(par[1]),wrap_angle(par[2])).control(1, ctrl_state=int(ctrl_state))
149                    qc.append(cu3, [qubits[i], qubits[j]])
150                    param_index += num_par   
151    else:    
152        for i in qubits[:n]:
153
154            j = i + shift  
155            if np.modf(j/m)[1] >= 1:
156                j -=int(np.modf(j/m)[1] * m)
157
158            if real:
159                qc.cry(wrap_angle(params[i]), qubits[i], qubits[j+n], ctrl_state=int(ctrl_state))
160            else:
161                par = params[int(param_index) : int(param_index + num_par)] 
162                cu3 = U3Gate(wrap_angle(par[0]),wrap_angle(par[1]),wrap_angle(par[2])).control(1, ctrl_state=int(ctrl_state))
163                qc.append(cu3, [qubits[i], qubits[j+n]])
164                param_index += num_par
165        
166    # package as instruction
167    qc_inst = qc.to_instruction()
168    circuit = QuantumCircuit(n+m)
169    circuit.append(qc_inst, qubits)
170    
171    return circuit 
172
173def conv_layer_NN(m, par_label, real=False, params=None, wrap=False):
174    """
175
176    Implement a QCNN neighbour-to-neighbour convolutional layer as a parametrised quantum circuit. 
177
178    The layer consists of the cascaded application of the two-qubit $\mathcal{N}$ gate on
179    the target register. $\mathcal{N}$ is applied to all neighbouring target qubits, including a connection between
180    the first and last qubit ("neighbour-to-neighbour" topology), resulting in a gate cost linear in the size of the target register. 
181    
182    Arguments:
183    ----
184    - **m** : *int* 
185
186        Number of qubits in the target register. 
187
188    - **par_label** : *str*
189
190        Label to assign to the parameter vector. 
191
192    - **real** : *boolean*
193
194        If True, the real version of the $\mathcal{N}$ gate is used (which only involves CX and Ry operations). This ensures real 
195        amplitudes. Default is False.     
196
197    - **params** : *array_like*, *optional*
198
199        Directly assign values, stored in `params`, to the circuit parameters instead of creating a `ParameterVector`. 
200
201    - **wrap**: *boolean* 
202
203        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.         
204  
205    Returns:
206    ---
207    - **circuit** : *QuantumCircuit* 
208
209        The qiskit `QuantumCircuit` implementation of the input layer.
210    """
211
212    # set up circuit 
213    qc = QuantumCircuit(m, name="Convolutional Layer (NN)")
214    qubits = list(range(m))
215
216    # number of parameters used by each N gate 
217    num_par = 3 if real==False else 2 
218
219    # number of gates applied per layer 
220    num_gates = m
221
222    # define weight re-mapping function:
223    def wrap_angle(theta):
224        if wrap:
225            return map_angle(theta)  
226        else:
227            return theta
228
229    # set up parameter vector 
230    param_index = 0
231    if params == None:
232        params = ParameterVector(par_label, length= int(num_par * num_gates))
233
234    # apply N gate linearly between neighbouring qubits
235    # (including circular connection between last and first) 
236    pairs = [tuple([i, i+1]) for i in qubits[:-1]]
237    pairs.append((qubits[-1], 0))
238
239    for j in np.arange(num_gates):
240        pars = params[int(param_index) : int(param_index + num_par)] 
241        qc.compose(N_gate([wrap_angle(i) for i in pars], real=real),pairs[int(j)],inplace=True)
242        param_index += num_par 
243    
244    # package as instruction
245    qc_inst = qc.to_instruction()
246    circuit = QuantumCircuit(m)
247    circuit.append(qc_inst, qubits)
248    
249    return circuit 
250
251def conv_layer_AA(m, par_label, real=False, params=None, wrap=False): 
252    """
253
254    Implement a QCNN all-to-all convolutional layer as a parametrised quantum circuit. 
255
256    The layer consists of the cascaded application of the two-qubit $\mathcal{N}$ gate on
257    the target register. $\mathcal{N}$ is applied to all combinations of target qubits ("all-to-all" topology),
258    resulting in a gate cost quadratic in the size of the target register. 
259    
260    Arguments:
261    ----
262    - **m** : *int* 
263
264        Number of qubits in the target register. 
265
266    - **par_label** : *str*
267
268        Label to assign to the parameter vector. 
269
270    - **real** : *boolean*
271
272        If True, the real version of the $\mathcal{N}$ gate is used (which only involves CX and Ry operations). This ensures real 
273        amplitudes. Default is False.     
274
275    - **params** : *array_like*, *optional*
276
277        Directly assign values, stored in `params`, to the circuit parameters instead of creating a `ParameterVector`.   
278
279    - **wrap**: *boolean* 
280
281        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.        
282  
283    Returns:
284    ---
285    - **circuit** : *QuantumCircuit* 
286
287        The qiskit `QuantumCircuit` implementation of the input layer.
288    """
289
290    # set up circuit 
291    qc = QuantumCircuit(m, name="Convolutional Layer (AA)")
292    qubits = list(range(m))
293
294    # number of parameters used by each N gate 
295    num_par = 3 if real==False else 2 
296
297    # number of gates applied per layer 
298    num_gates = 0.5 * m * (m-1)
299
300    # define weight re-mapping function:
301    def wrap_angle(theta):
302        if wrap:
303            return map_angle(theta)  
304        else:
305            return theta
306
307    # set up parameter vector 
308    param_index = 0
309    if params == None:
310        params = ParameterVector(par_label, length= int(num_par * num_gates))
311
312    # apply N gate linearly between neighbouring qubits
313    # (including circular connection between last and first) 
314    pairs = list(combinations(qubits,2))
315
316    for j in np.arange(num_gates):
317        pars = params[int(param_index) : int(param_index + num_par)]
318        qc.compose(N_gate([wrap_angle(i) for i in pars], real=real),pairs[int(j)],inplace=True)
319        if j != num_gates -1:
320            #qc.barrier()
321            s=" "
322        param_index += num_par 
323    
324    # package as instruction
325    qc_inst = qc.to_instruction()
326    circuit = QuantumCircuit(m)
327    circuit.append(qc_inst, qubits)
328    
329    return circuit 
330
331def digital_encoding(n):   
332    r"""
333    Set up a parametrised quantum circuit for the digital encoding of a binary number onto 
334    a quantum register. 
335
336    The encoding is set by assigning the value 0 to the $i$th parameter of the circuit to 
337    represent a state $\ket{0}$ for the $i$th bit and assigning $\pi$ for the case $\ket{1}$. 
338    Values can be assigned to circuit parameters using qiskit's `QuantumCircuit.assign_parameters()`.  
339
340    This is to be used in conjunction with the parameter array generated by `binary_to_encode_param()`. 
341
342    Example usage: 
343
344            >>> binary='011001'                         # bit string to encode 
345            >>> params=binary_to_encode_param(binary)   # generate parameter array 
346            >>> qc=digital_encoding(len(binary))        # generate circuit 
347            >>> qc.assign_parameters(params)            # assign parameters to circuit 
348
349    Arguments:
350    ----
351    - **n** : *int* 
352
353        Number of qubits in the register. Should be equal to the number of bits 
354        in the bit strings that are to be encoded. 
355
356    Returns:
357    ----
358    - **circuit** : *QuantumCircuit* 
359
360        The qiskit `QuantumCircuit` implementation of the encoding circuit, with `n` unassigned parameters. 
361
362    """
363
364    # set up circuit and parameter vector
365    qc = QuantumCircuit(n,name="Digital Encoding")
366    qubits = list(range(n))
367    params = ParameterVector("enc", length=n)
368
369    # flip ith qubit if params[i]==np.pi
370    for i in np.arange(n):
371        qc.rx(params[i], qubits[i]) 
372        qc.p(params[i]/2, qubits[i])
373
374    # package as instruction
375    qc_inst = qc.to_instruction()
376    circuit = QuantumCircuit(n)
377    circuit.append(qc_inst, qubits)    
378
379    return circuit 
380
381def binary_to_encode_param(binary):        
382    """
383
384    Generate the parameter array for digital encoding corresponding to a binary string. 
385
386    This is to be used in conjunction with the encoding circuit generated by `digital_encoding()`. 
387
388    Example usage: 
389
390            >>> binary='011001'                         # bit string to encode 
391            >>> params=binary_to_encode_param(binary)   # generate parameter array 
392            >>> qc=digital_encoding(len(binary))        # generate circuit 
393            >>> qc.assign_parameters(params)            # assign parameters to circuit 
394
395    Arguments:
396    ----
397    - **binary** : *str* 
398
399        The binary string. Little endian convention is assumed. 
400
401    Returns:
402    ----
403    - **params** : *array_like* 
404
405        The parameter array of length `len(binary)` corresponding to `binary`. Big endian convention is used. 
406
407
408    Convert an n-bit binary string to the associated parameter array to feed 
409    into the digital encoding circuit. 
410    """
411
412    params = np.empty(len(binary))
413
414    binary = binary[::-1]  # reverse binary string (small endian for binary, big endian for arrays)
415
416    for i in np.arange(len(binary)):
417        if binary[i]=="0":
418            params[i]=0 
419        elif binary[i]=="1":
420            params[i]=np.pi 
421        else: 
422            raise ValueError("Binary string should only include characters '0' and '1'.")        
423
424    return params 
425
426def generate_network(n,m,L, encode=False, toggle_IL=True, initial_IL=True, input_Ry=False, real=False, inverse=False, repeat_params=None, wrap=False):
427    r"""
428    Set up a QCNN consisting of input and convolutional layers acting on two distinct registers, the 'input register' and the 'target register'. 
429
430    Input layers consist of single-qubit operations applied to the target register, controlled by the input register qubits. Convolutional layers
431    consist of two-qubit operations applied to the target register. See `input_layer()` for more information on input layers and `conv_layer_AA()`, 
432    `conv_layer_NN()` for more information on convolutional layers. 
433
434    The network was designed for the task of performing function evaluation, i.e. to implement an operator $\hat{Q}_\Psi$ such that 
435    $$ \hat{Q}_\Psi \ket{j}_i \ket{0}_t  = \ket{j}_i \ket{\Psi(j)}_t,$$
436    for some function $\Psi$ with the subscripts $i$ and $t$ denoting the input and target registers, respectively. 
437    With some adaptation, the network structure could also be used for other applications. 
438
439    Arguments:
440    ---
441    - **n** : *int* 
442
443        Number of qubits in the input register. 
444
445    - **m** : *int* 
446
447        Number of qubits in the target register. 
448
449    - **L** : *int* 
450
451        Number of layers in the network. Note that `L` does not take into account the optional initial input layer        
452        added by `initial_IL`. Further, `L` does not take into account the padding of the network with additional input 
453        layers to ensure the number of input layers is at least equal to `m`. 
454
455    - **encode** : *boolean*
456
457        If True, apply an initial encoding circuit to the input register which can be used control the input states of the network. 
458        Default is False. 
459
460    - **toggle_IL** : *boolean*  
461
462        If True, every third layer added is an input layer and additional input layers are added to ensure the number of input layers is at least equal to
463        `m`, with input layers alternating between control states 0 and 1. The input layer `shift` parameter is successively increased for each new input layer, 
464        resulting in each input qubit controlling an operation on each target qubit at some point in the network. 
465        
466        If False, only convolutional layers are added. In either case, convolutional layers alternate
467        between all-to-all and neighbour-to-neighbour layers. Default is True.    
468    
469    - **initial_IL** : *boolean*
470
471        If True, add an input layer at the beginning of the circuit. Default is True. 
472
473    - **input_Ry** : *boolean*
474
475        If True, initially apply a sequence of parametrised Ry rotations to the input register. Default is False. 
476
477    - **real** : *boolean*
478
479        If True, use the real versions of input and convolutional layers, which only contain CX and Ry gates and hence ensure 
480        real amplitudes. Default is False. 
481
482    - **inverse** : *boolean* 
483
484        If True, invert the circuit and return the inverse of the network. 
485
486    - **repeat_params** : *str*, *optional*
487
488        Keep parameters fixed for different layer types, i.e. use the same parameter values for each instance of a layer type. 
489        Options are `'CL'` (keep parameters fixed for convolutional layers), `'IL'` (keep parameters fixed for input layers), `'both'` 
490        (keep parameters fixed for both convolutional and input layers). 
491
492    - **wrap**: *boolean* 
493
494        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.     
495
496        
497    Returns:
498    ----
499    - **circuit** : *QuantumCircuit* 
500
501        The qiskit `QuantumCircuit` implementation of the network, with unassigned parameters.  
502 
503    """
504    # initialise parameter vector 
505    if repeat_params==None:
506        AA_CL_params=None 
507        NN_CL_params=None 
508        IL_params=None 
509    elif repeat_params=="CL":
510        IL_params=None  
511        AA_CL_params=ParameterVector(u"\u03B8_CL_AA", length= int((3 if real==False else 2 ) * (0.5 * m * (m-1))))
512        NN_CL_params=ParameterVector(u"\u03B8_CL_NN", length= int((3 if real==False else 2) * m))     
513    elif repeat_params=="IL":
514        IL_params=ParameterVector(u"\u03B8_IL", length= int((3 if real==False else 1) * n))
515        AA_CL_params=None 
516        NN_CL_params=None 
517    elif repeat_params=="both":
518        AA_CL_params=ParameterVector(u"\u03B8_CL_AA", length= int((3 if real==False else 2 ) * (0.5 * m * (m-1))))
519        NN_CL_params=ParameterVector(u"\u03B8_CL_NN", length= int((3 if real==False else 2) * m))
520        IL_params=ParameterVector(u"\u03B8_IL", length= int((3 if real==False else 1) * n))
521    else:
522        raise ValueError("Unrecognised option for 'repeat_params'. Should be None, 'CL', 'IL', or 'both'.")        
523        
524    # initialise empty input and target registers 
525    input_register = QuantumRegister(n, "input")
526    target_register = QuantumRegister(m, "target")
527    circuit = QuantumCircuit(input_register, target_register) 
528
529    # prepare registers 
530    circuit.h(target_register)
531    if encode:
532        circuit.compose(digital_encoding(n), input_register, inplace=True)
533
534    if input_Ry:
535        input_Ry_params = ParameterVector("Input_Ry",n)
536        for i in np.arange(n):
537            circuit.ry(input_Ry_params[i], input_register[i])
538         
539    if initial_IL: 
540        # apply input layer 
541        circuit.compose(input_layer(n,m, u"\u03B8_IL_0", real=real, params=IL_params, wrap=wrap), circuit.qubits, inplace=True)
542        
543    # apply convolutional layers (alternating between AA and NN)
544    # if toggle_IL is True, additional input layers are added after each NN
545    for i in np.arange(L):
546
547        if toggle_IL==False:
548
549            if i % 2 ==0:
550                circuit.compose(conv_layer_AA(m, u"\u03B8_CL_AA_{0}".format(i // 2), real=real, params=AA_CL_params,wrap=wrap), target_register, inplace=True)
551            elif i % 2 ==1:
552                circuit.compose(conv_layer_NN(m, u"\u03B8_CL_NN_{0}".format(i // 2),real=real, params=NN_CL_params, wrap=wrap), target_register, inplace=True)
553        
554        if toggle_IL==True:
555
556            # difference between number of input layers and number of target qubits:
557            del_IL = m - (int(initial_IL)+ L //3) 
558
559            if del_IL <= 0:
560                if i % 3 ==0:
561                    circuit.compose(conv_layer_AA(m, u"\u03B8_CL_AA_{0}".format(i // 3),real=real,params=AA_CL_params, wrap=wrap), target_register, inplace=True)
562                elif i % 3 ==1:
563                    circuit.compose(conv_layer_NN(m, u"\u03B8_CL_NN_{0}".format(i // 3),real=real,params=NN_CL_params, wrap=wrap), target_register, inplace=True)
564                elif i % 3 ==2:
565                    circuit.compose(input_layer(n,m, u"\u03B8_IL_{0}".format(i // 3 +1),shift=(i//3)+1, ctrl_state=(i % 2),real=real,params=IL_params, wrap=wrap), circuit.qubits, inplace=True) 
566            else: 
567                if i % 3 ==0:
568                    circuit.compose(conv_layer_AA(m, u"\u03B8_CL_AA_{0}".format(i // 3),real=real,params=AA_CL_params, wrap=wrap), target_register, inplace=True)
569                elif i % 3 ==1:
570                    circuit.compose(conv_layer_NN(m, u"\u03B8_CL_NN_{0}".format(i // 3),real=real,params=NN_CL_params, wrap=wrap), target_register, inplace=True)
571                elif i % 3 ==2:
572                    # padd with additional input layers
573                    for j in np.arange(del_IL // (L //3) +2):
574                        shift = (i // 3) +int(initial_IL) + j * (L //3)
575                        if shift <= m-1:
576                            circuit.compose(input_layer(n,m, u"\u03B8_IL_{0}".format(shift),shift=shift, ctrl_state=((shift-int(initial_IL)) % 2),real=real,params=IL_params, wrap=wrap), circuit.qubits, inplace=True) 
577
578        if inverse:
579            circuit=circuit.inverse()   
580
581    return circuit
582
583def A_generate_network(n,L, repeat_params=False, wrap=False):
584    r"""
585    Set up a network consisting of real convolutional layers acting on a single qubit register. 
586
587    Layers alternate between all-to-all and neighbour-to-neighbour convolutional layers. For more 
588    information see `conv_layer_AA()`, `conv_layer_NN()`. The convolutional layers are *real* in 
589    the sense of involving only CX and Ry operations, ensuring real amplitudes. 
590
591    The network was designed for the task of encoding a suitably normalised real function, $A(j)$, into the amplitudes of the 
592    register, i.e. to implement an operator $\hat{U}_A$ such that  
593    $$ \hat{U}_A \ket{j}= A(j) \ket{j}.$$
594    With some adaptation, the network structure could also be used for other applications. 
595
596    Arguments:
597    ---
598    - **n** : *int* 
599
600        Number of qubits in the register. 
601
602    - **L** : *int* 
603
604        Number of layers in the network. 
605
606    - **repeat_params** : *boolean* 
607
608        If True, use the same parameter values for each layer type. Default is False. 
609
610    - **wrap**: *boolean* 
611
612        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.      
613
614    Returns:
615    ---
616    - **circuit** : *QuantumCircuit* 
617
618        The qiskit `QuantumCircuit` implementation of the network, with unassigned parameters.  
619                    
620    """
621
622    # initialise parameter vector 
623    if repeat_params:
624        AA_CL_params=ParameterVector(u"\u03B8_AA", length= int(2 * (0.5 * n * (n-1))))
625        NN_CL_params=ParameterVector(u"\u03B8_NN", length= int(2 * n))
626    else:
627        AA_CL_params = None 
628        NN_CL_params = None      
629
630
631    # initialise empty input register 
632    register = QuantumRegister(n, "reg")
633    circuit = QuantumCircuit(register) 
634
635    # prepare register
636    circuit.h(register)
637    
638    # apply L convolutional layers (alternating between AA and NN)
639    for i in np.arange(L):
640
641        if i % 2 ==0:
642            circuit.compose(conv_layer_AA(n, u"\u03B8_R_AA_{0}".format(i // 2), real=True, params=AA_CL_params,wrap=wrap), register, inplace=True)
643        elif i % 2 ==1:
644            circuit.compose(conv_layer_NN(n, u"\u03B8_R_NN_{0}".format(i // 2), real=True, params=NN_CL_params,wrap=wrap), register, inplace=True)
645         
646    return circuit 
647
648def get_state_vec(circuit):
649    """
650    Get statevector produced by a quantum circuit. 
651
652    Uses the `qiskit_aer` backend. 
653
654    Arguments:
655    ----
656    - **circuit** : *QuantumCircuit* 
657
658        The circuit to be evaluated. 
659
660    Returns:
661    ----
662    - **state_vector** : *array_like* 
663
664        Array storing the complex amplitudes of the system to be in each of the 
665        `2**circuit.num_qubits` basis states.     
666
667    """
668    
669    # Transpile for simulator
670    simulator = StatevectorSimulator()
671    circuit = transpile(circuit, simulator)
672
673    # Run and get counts
674    result = simulator.run(circuit).result()
675    state_vector= result.get_statevector(circuit)
676
677    return np.asarray(state_vector)
678
679def map_angle(theta):
680    r"""
681    Map a rotation angle to the interval $(0, \frac{\pi}{2})$.
682
683    An inverse tan function is used for the mapping. 
684
685    Arguments:
686    ----
687    - **theta** : *float* 
688
689        Rotation angle. 
690
691    Returns:
692    ---
693    - **theta_mapped** : *float* 
694
695        Rotation angle mapped to the given interval.     
696
697    """
698
699    return np.arctan(theta) + np.pi /2  
def N_gate(params, real=False):
12def N_gate(params, real=False):
13    r"""
14    Constructs the two-qubit three-parameter $\mathcal{N}$ gate, defined as 
15    $$\mathcal{N}(\alpha, \beta, \gamma) = \exp \left( i \left[ \alpha X \otimes X + \beta Y \otimes Y + \gamma Z \otimes Z \right] \right),$$
16    where $X$, $Y$, $Z$ are the Pauli operators. 
17
18    This is implemented via three CX gates, three Rz gates and two Ry gates, as shown in [Vatan 2004](https://arxiv.org/pdf/quant-ph/0308006).  
19
20    Arguments:
21    ----
22    - **params** : *array_like*
23
24        Array-like object containing the gate parameters, corresponding to the qubit rotation angles. 
25
26    - **real** : *boolean*
27       
28        If True, a two-parameter version of the $\mathcal{N}$ gate is implemented instead, with one of the CX gates and all of the 
29        Rz gates removed. This ensures real amplitudes. Default is False.   
30             
31    Returns:
32    ----
33    - **circuit** : *QuantumCircuit*
34
35        Implementation of the $\mathcal{N}$ gate as a qiskit `QuantumCircuit`. 
36
37    """
38
39    if real:
40        circuit = QuantumCircuit(2, name="RN Gate")
41        circuit.cx(1, 0)
42        circuit.ry(params[0], 0)
43        circuit.ry(params[1], 1)
44        circuit.cx(0, 1)
45    else:    
46        circuit = QuantumCircuit(2, name="N Gate")
47        circuit.rz(-np.pi / 2, 1)
48        circuit.cx(1, 0)
49        circuit.rz(params[0], 0)
50        circuit.ry(params[1], 1)
51        circuit.cx(0, 1)
52        circuit.ry(params[2], 1)
53        circuit.cx(1, 0)
54        circuit.rz(np.pi / 2, 0)
55
56    return circuit 

Constructs the two-qubit three-parameter $\mathcal{N}$ gate, defined as $$\mathcal{N}(\alpha, \beta, \gamma) = \exp \left( i \left[ \alpha X \otimes X + \beta Y \otimes Y + \gamma Z \otimes Z \right] \right),$$ where $X$, $Y$, $Z$ are the Pauli operators.

This is implemented via three CX gates, three Rz gates and two Ry gates, as shown in Vatan 2004.

Arguments:

  • params : array_like

    Array-like object containing the gate parameters, corresponding to the qubit rotation angles.

  • real : boolean

    If True, a two-parameter version of the $\mathcal{N}$ gate is implemented instead, with one of the CX gates and all of the Rz gates removed. This ensures real amplitudes. Default is False.

Returns:

  • circuit : QuantumCircuit

    Implementation of the $\mathcal{N}$ gate as a qiskit QuantumCircuit.

def input_layer( n, m, par_label, ctrl_state=0, real=False, params=None, AA=False, shift=0, wrap=False):
 58def input_layer(n, m, par_label, ctrl_state=0, real=False, params=None, AA=False, shift=0, wrap=False): 
 59    r"""
 60
 61    Implement a QCNN input layer as a parametrised quantum circuit. 
 62
 63    The layer consists of a sequence of controlled arbitrary single-qubit operations (CU3 operations)
 64    applied to the target register with the input qubits acting a controls. This feeds information 
 65    about the input register state into the target register. 
 66    
 67    Arguments:
 68    ----
 69    - **n** : *int*
 70
 71        Number of qubits in the input register. 
 72
 73    - **m** : *int* 
 74
 75        Number of qubits in the target register. 
 76
 77    - **par_label** : *str*
 78
 79        Label to assign to the parameter vector. 
 80
 81    - **ctrl_state** : *int* 
 82
 83        Control state of the controlled gates. If equal to 0, the controlled operation is applied 
 84        when the control qubit is in state $\ket{0}$. If equal to 1, the controlled operation is 
 85        applied when the control qubit is in state $\ket{1}$. Default is 0. 
 86
 87    - **real** : *boolean*
 88
 89        If True, controlled Ry rotations are applied instead of CU3 operations. This ensures real 
 90        amplitudes. Default is False.     
 91
 92    - **params** : *array_like*, *optional*
 93
 94        Directly assign values, stored in `params`, to the circuit parameters instead of creating a `ParameterVector`.      
 95
 96    - **AA** : *boolean* 
 97
 98        If True, each input qubit controls an operation on each target qubit, corresponding to an "all-to-all" layer topology. 
 99        Default is False. 
100
101    - **shift** : *int* 
102
103        If `AA` is False, the $j$th input qubit controls an operation applied on the $(j+s)$th target qubit with wrap-around 
104        for `n > m`. Default is 0.    
105
106    - **wrap**: *boolean* 
107
108        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.     
109
110    Returns:
111    ---
112    - **circuit** : *QuantumCircuit* 
113
114        The qiskit `QuantumCircuit` implementation of the input layer. 
115    
116    """
117
118    # set up circuit 
119    qc = QuantumCircuit(n+m, name="Input Layer")
120    qubits = list(range(n+m))
121
122    # number of parameters used by each gate 
123    num_par = 3 if real==False else 1
124
125    # number of gates applied per layer 
126    num_gates = n if AA==False else n*m
127
128    # set up parameter vector 
129    if params == None:
130        params = ParameterVector(par_label, length= num_par * num_gates)
131    param_index = 0
132
133    # define weight re-mapping function:
134    def wrap_angle(theta):
135        if wrap:
136            return map_angle(theta)  
137        else:
138            return theta 
139
140    # apply gates to qubits 
141    if AA: 
142        for i in qubits[:n]:
143            for j in qubits[n:]:
144                if real:
145                    qc.cry(wrap_angle(params[int(param_index)]), qubits[i], qubits[j],ctrl_state=int(ctrl_state))
146                    param_index += 1
147                else:  
148                    par = params[int(param_index) : int(param_index + num_par)] 
149                    cu3 = U3Gate(wrap_angle(par[0]),wrap_angle(par[1]),wrap_angle(par[2])).control(1, ctrl_state=int(ctrl_state))
150                    qc.append(cu3, [qubits[i], qubits[j]])
151                    param_index += num_par   
152    else:    
153        for i in qubits[:n]:
154
155            j = i + shift  
156            if np.modf(j/m)[1] >= 1:
157                j -=int(np.modf(j/m)[1] * m)
158
159            if real:
160                qc.cry(wrap_angle(params[i]), qubits[i], qubits[j+n], ctrl_state=int(ctrl_state))
161            else:
162                par = params[int(param_index) : int(param_index + num_par)] 
163                cu3 = U3Gate(wrap_angle(par[0]),wrap_angle(par[1]),wrap_angle(par[2])).control(1, ctrl_state=int(ctrl_state))
164                qc.append(cu3, [qubits[i], qubits[j+n]])
165                param_index += num_par
166        
167    # package as instruction
168    qc_inst = qc.to_instruction()
169    circuit = QuantumCircuit(n+m)
170    circuit.append(qc_inst, qubits)
171    
172    return circuit 

Implement a QCNN input layer as a parametrised quantum circuit.

The layer consists of a sequence of controlled arbitrary single-qubit operations (CU3 operations) applied to the target register with the input qubits acting a controls. This feeds information about the input register state into the target register.

Arguments:

  • n : int

    Number of qubits in the input register.

  • m : int

    Number of qubits in the target register.

  • par_label : str

    Label to assign to the parameter vector.

  • ctrl_state : int

    Control state of the controlled gates. If equal to 0, the controlled operation is applied when the control qubit is in state $\ket{0}$. If equal to 1, the controlled operation is applied when the control qubit is in state $\ket{1}$. Default is 0.

  • real : boolean

    If True, controlled Ry rotations are applied instead of CU3 operations. This ensures real amplitudes. Default is False.

  • params : array_like, optional

    Directly assign values, stored in params, to the circuit parameters instead of creating a ParameterVector.

  • AA : boolean

    If True, each input qubit controls an operation on each target qubit, corresponding to an "all-to-all" layer topology. Default is False.

  • shift : int

    If AA is False, the $j$th input qubit controls an operation applied on the $(j+s)$th target qubit with wrap-around for n > m. Default is 0.

  • wrap: boolean

    If True, map rotation angles to an interval specified by map_angle(). Default is False.

Returns:

  • circuit : QuantumCircuit

    The qiskit QuantumCircuit implementation of the input layer.

def conv_layer_NN(m, par_label, real=False, params=None, wrap=False):
174def conv_layer_NN(m, par_label, real=False, params=None, wrap=False):
175    """
176
177    Implement a QCNN neighbour-to-neighbour convolutional layer as a parametrised quantum circuit. 
178
179    The layer consists of the cascaded application of the two-qubit $\mathcal{N}$ gate on
180    the target register. $\mathcal{N}$ is applied to all neighbouring target qubits, including a connection between
181    the first and last qubit ("neighbour-to-neighbour" topology), resulting in a gate cost linear in the size of the target register. 
182    
183    Arguments:
184    ----
185    - **m** : *int* 
186
187        Number of qubits in the target register. 
188
189    - **par_label** : *str*
190
191        Label to assign to the parameter vector. 
192
193    - **real** : *boolean*
194
195        If True, the real version of the $\mathcal{N}$ gate is used (which only involves CX and Ry operations). This ensures real 
196        amplitudes. Default is False.     
197
198    - **params** : *array_like*, *optional*
199
200        Directly assign values, stored in `params`, to the circuit parameters instead of creating a `ParameterVector`. 
201
202    - **wrap**: *boolean* 
203
204        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.         
205  
206    Returns:
207    ---
208    - **circuit** : *QuantumCircuit* 
209
210        The qiskit `QuantumCircuit` implementation of the input layer.
211    """
212
213    # set up circuit 
214    qc = QuantumCircuit(m, name="Convolutional Layer (NN)")
215    qubits = list(range(m))
216
217    # number of parameters used by each N gate 
218    num_par = 3 if real==False else 2 
219
220    # number of gates applied per layer 
221    num_gates = m
222
223    # define weight re-mapping function:
224    def wrap_angle(theta):
225        if wrap:
226            return map_angle(theta)  
227        else:
228            return theta
229
230    # set up parameter vector 
231    param_index = 0
232    if params == None:
233        params = ParameterVector(par_label, length= int(num_par * num_gates))
234
235    # apply N gate linearly between neighbouring qubits
236    # (including circular connection between last and first) 
237    pairs = [tuple([i, i+1]) for i in qubits[:-1]]
238    pairs.append((qubits[-1], 0))
239
240    for j in np.arange(num_gates):
241        pars = params[int(param_index) : int(param_index + num_par)] 
242        qc.compose(N_gate([wrap_angle(i) for i in pars], real=real),pairs[int(j)],inplace=True)
243        param_index += num_par 
244    
245    # package as instruction
246    qc_inst = qc.to_instruction()
247    circuit = QuantumCircuit(m)
248    circuit.append(qc_inst, qubits)
249    
250    return circuit 

Implement a QCNN neighbour-to-neighbour convolutional layer as a parametrised quantum circuit.

The layer consists of the cascaded application of the two-qubit $\mathcal{N}$ gate on the target register. $\mathcal{N}$ is applied to all neighbouring target qubits, including a connection between the first and last qubit ("neighbour-to-neighbour" topology), resulting in a gate cost linear in the size of the target register.

Arguments:

  • m : int

    Number of qubits in the target register.

  • par_label : str

    Label to assign to the parameter vector.

  • real : boolean

    If True, the real version of the $\mathcal{N}$ gate is used (which only involves CX and Ry operations). This ensures real amplitudes. Default is False.

  • params : array_like, optional

    Directly assign values, stored in params, to the circuit parameters instead of creating a ParameterVector.

  • wrap: boolean

    If True, map rotation angles to an interval specified by map_angle(). Default is False.

Returns:

  • circuit : QuantumCircuit

    The qiskit QuantumCircuit implementation of the input layer.

def conv_layer_AA(m, par_label, real=False, params=None, wrap=False):
252def conv_layer_AA(m, par_label, real=False, params=None, wrap=False): 
253    """
254
255    Implement a QCNN all-to-all convolutional layer as a parametrised quantum circuit. 
256
257    The layer consists of the cascaded application of the two-qubit $\mathcal{N}$ gate on
258    the target register. $\mathcal{N}$ is applied to all combinations of target qubits ("all-to-all" topology),
259    resulting in a gate cost quadratic in the size of the target register. 
260    
261    Arguments:
262    ----
263    - **m** : *int* 
264
265        Number of qubits in the target register. 
266
267    - **par_label** : *str*
268
269        Label to assign to the parameter vector. 
270
271    - **real** : *boolean*
272
273        If True, the real version of the $\mathcal{N}$ gate is used (which only involves CX and Ry operations). This ensures real 
274        amplitudes. Default is False.     
275
276    - **params** : *array_like*, *optional*
277
278        Directly assign values, stored in `params`, to the circuit parameters instead of creating a `ParameterVector`.   
279
280    - **wrap**: *boolean* 
281
282        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.        
283  
284    Returns:
285    ---
286    - **circuit** : *QuantumCircuit* 
287
288        The qiskit `QuantumCircuit` implementation of the input layer.
289    """
290
291    # set up circuit 
292    qc = QuantumCircuit(m, name="Convolutional Layer (AA)")
293    qubits = list(range(m))
294
295    # number of parameters used by each N gate 
296    num_par = 3 if real==False else 2 
297
298    # number of gates applied per layer 
299    num_gates = 0.5 * m * (m-1)
300
301    # define weight re-mapping function:
302    def wrap_angle(theta):
303        if wrap:
304            return map_angle(theta)  
305        else:
306            return theta
307
308    # set up parameter vector 
309    param_index = 0
310    if params == None:
311        params = ParameterVector(par_label, length= int(num_par * num_gates))
312
313    # apply N gate linearly between neighbouring qubits
314    # (including circular connection between last and first) 
315    pairs = list(combinations(qubits,2))
316
317    for j in np.arange(num_gates):
318        pars = params[int(param_index) : int(param_index + num_par)]
319        qc.compose(N_gate([wrap_angle(i) for i in pars], real=real),pairs[int(j)],inplace=True)
320        if j != num_gates -1:
321            #qc.barrier()
322            s=" "
323        param_index += num_par 
324    
325    # package as instruction
326    qc_inst = qc.to_instruction()
327    circuit = QuantumCircuit(m)
328    circuit.append(qc_inst, qubits)
329    
330    return circuit 

Implement a QCNN all-to-all convolutional layer as a parametrised quantum circuit.

The layer consists of the cascaded application of the two-qubit $\mathcal{N}$ gate on the target register. $\mathcal{N}$ is applied to all combinations of target qubits ("all-to-all" topology), resulting in a gate cost quadratic in the size of the target register.

Arguments:

  • m : int

    Number of qubits in the target register.

  • par_label : str

    Label to assign to the parameter vector.

  • real : boolean

    If True, the real version of the $\mathcal{N}$ gate is used (which only involves CX and Ry operations). This ensures real amplitudes. Default is False.

  • params : array_like, optional

    Directly assign values, stored in params, to the circuit parameters instead of creating a ParameterVector.

  • wrap: boolean

    If True, map rotation angles to an interval specified by map_angle(). Default is False.

Returns:

  • circuit : QuantumCircuit

    The qiskit QuantumCircuit implementation of the input layer.

def digital_encoding(n):
332def digital_encoding(n):   
333    r"""
334    Set up a parametrised quantum circuit for the digital encoding of a binary number onto 
335    a quantum register. 
336
337    The encoding is set by assigning the value 0 to the $i$th parameter of the circuit to 
338    represent a state $\ket{0}$ for the $i$th bit and assigning $\pi$ for the case $\ket{1}$. 
339    Values can be assigned to circuit parameters using qiskit's `QuantumCircuit.assign_parameters()`.  
340
341    This is to be used in conjunction with the parameter array generated by `binary_to_encode_param()`. 
342
343    Example usage: 
344
345            >>> binary='011001'                         # bit string to encode 
346            >>> params=binary_to_encode_param(binary)   # generate parameter array 
347            >>> qc=digital_encoding(len(binary))        # generate circuit 
348            >>> qc.assign_parameters(params)            # assign parameters to circuit 
349
350    Arguments:
351    ----
352    - **n** : *int* 
353
354        Number of qubits in the register. Should be equal to the number of bits 
355        in the bit strings that are to be encoded. 
356
357    Returns:
358    ----
359    - **circuit** : *QuantumCircuit* 
360
361        The qiskit `QuantumCircuit` implementation of the encoding circuit, with `n` unassigned parameters. 
362
363    """
364
365    # set up circuit and parameter vector
366    qc = QuantumCircuit(n,name="Digital Encoding")
367    qubits = list(range(n))
368    params = ParameterVector("enc", length=n)
369
370    # flip ith qubit if params[i]==np.pi
371    for i in np.arange(n):
372        qc.rx(params[i], qubits[i]) 
373        qc.p(params[i]/2, qubits[i])
374
375    # package as instruction
376    qc_inst = qc.to_instruction()
377    circuit = QuantumCircuit(n)
378    circuit.append(qc_inst, qubits)    
379
380    return circuit 

Set up a parametrised quantum circuit for the digital encoding of a binary number onto a quantum register.

The encoding is set by assigning the value 0 to the $i$th parameter of the circuit to represent a state $\ket{0}$ for the $i$th bit and assigning $\pi$ for the case $\ket{1}$. Values can be assigned to circuit parameters using qiskit's QuantumCircuit.assign_parameters().

This is to be used in conjunction with the parameter array generated by binary_to_encode_param().

Example usage:

    >>> binary='011001'                         # bit string to encode 
    >>> params=binary_to_encode_param(binary)   # generate parameter array 
    >>> qc=digital_encoding(len(binary))        # generate circuit 
    >>> qc.assign_parameters(params)            # assign parameters to circuit

Arguments:

  • n : int

    Number of qubits in the register. Should be equal to the number of bits in the bit strings that are to be encoded.

Returns:

  • circuit : QuantumCircuit

    The qiskit QuantumCircuit implementation of the encoding circuit, with n unassigned parameters.

def binary_to_encode_param(binary):
382def binary_to_encode_param(binary):        
383    """
384
385    Generate the parameter array for digital encoding corresponding to a binary string. 
386
387    This is to be used in conjunction with the encoding circuit generated by `digital_encoding()`. 
388
389    Example usage: 
390
391            >>> binary='011001'                         # bit string to encode 
392            >>> params=binary_to_encode_param(binary)   # generate parameter array 
393            >>> qc=digital_encoding(len(binary))        # generate circuit 
394            >>> qc.assign_parameters(params)            # assign parameters to circuit 
395
396    Arguments:
397    ----
398    - **binary** : *str* 
399
400        The binary string. Little endian convention is assumed. 
401
402    Returns:
403    ----
404    - **params** : *array_like* 
405
406        The parameter array of length `len(binary)` corresponding to `binary`. Big endian convention is used. 
407
408
409    Convert an n-bit binary string to the associated parameter array to feed 
410    into the digital encoding circuit. 
411    """
412
413    params = np.empty(len(binary))
414
415    binary = binary[::-1]  # reverse binary string (small endian for binary, big endian for arrays)
416
417    for i in np.arange(len(binary)):
418        if binary[i]=="0":
419            params[i]=0 
420        elif binary[i]=="1":
421            params[i]=np.pi 
422        else: 
423            raise ValueError("Binary string should only include characters '0' and '1'.")        
424
425    return params 

Generate the parameter array for digital encoding corresponding to a binary string.

This is to be used in conjunction with the encoding circuit generated by digital_encoding().

Example usage:

    >>> binary='011001'                         # bit string to encode 
    >>> params=binary_to_encode_param(binary)   # generate parameter array 
    >>> qc=digital_encoding(len(binary))        # generate circuit 
    >>> qc.assign_parameters(params)            # assign parameters to circuit

Arguments:

  • binary : str

    The binary string. Little endian convention is assumed.

Returns:

  • params : array_like

    The parameter array of length len(binary) corresponding to binary. Big endian convention is used.

Convert an n-bit binary string to the associated parameter array to feed into the digital encoding circuit.

def generate_network( n, m, L, encode=False, toggle_IL=True, initial_IL=True, input_Ry=False, real=False, inverse=False, repeat_params=None, wrap=False):
427def generate_network(n,m,L, encode=False, toggle_IL=True, initial_IL=True, input_Ry=False, real=False, inverse=False, repeat_params=None, wrap=False):
428    r"""
429    Set up a QCNN consisting of input and convolutional layers acting on two distinct registers, the 'input register' and the 'target register'. 
430
431    Input layers consist of single-qubit operations applied to the target register, controlled by the input register qubits. Convolutional layers
432    consist of two-qubit operations applied to the target register. See `input_layer()` for more information on input layers and `conv_layer_AA()`, 
433    `conv_layer_NN()` for more information on convolutional layers. 
434
435    The network was designed for the task of performing function evaluation, i.e. to implement an operator $\hat{Q}_\Psi$ such that 
436    $$ \hat{Q}_\Psi \ket{j}_i \ket{0}_t  = \ket{j}_i \ket{\Psi(j)}_t,$$
437    for some function $\Psi$ with the subscripts $i$ and $t$ denoting the input and target registers, respectively. 
438    With some adaptation, the network structure could also be used for other applications. 
439
440    Arguments:
441    ---
442    - **n** : *int* 
443
444        Number of qubits in the input register. 
445
446    - **m** : *int* 
447
448        Number of qubits in the target register. 
449
450    - **L** : *int* 
451
452        Number of layers in the network. Note that `L` does not take into account the optional initial input layer        
453        added by `initial_IL`. Further, `L` does not take into account the padding of the network with additional input 
454        layers to ensure the number of input layers is at least equal to `m`. 
455
456    - **encode** : *boolean*
457
458        If True, apply an initial encoding circuit to the input register which can be used control the input states of the network. 
459        Default is False. 
460
461    - **toggle_IL** : *boolean*  
462
463        If True, every third layer added is an input layer and additional input layers are added to ensure the number of input layers is at least equal to
464        `m`, with input layers alternating between control states 0 and 1. The input layer `shift` parameter is successively increased for each new input layer, 
465        resulting in each input qubit controlling an operation on each target qubit at some point in the network. 
466        
467        If False, only convolutional layers are added. In either case, convolutional layers alternate
468        between all-to-all and neighbour-to-neighbour layers. Default is True.    
469    
470    - **initial_IL** : *boolean*
471
472        If True, add an input layer at the beginning of the circuit. Default is True. 
473
474    - **input_Ry** : *boolean*
475
476        If True, initially apply a sequence of parametrised Ry rotations to the input register. Default is False. 
477
478    - **real** : *boolean*
479
480        If True, use the real versions of input and convolutional layers, which only contain CX and Ry gates and hence ensure 
481        real amplitudes. Default is False. 
482
483    - **inverse** : *boolean* 
484
485        If True, invert the circuit and return the inverse of the network. 
486
487    - **repeat_params** : *str*, *optional*
488
489        Keep parameters fixed for different layer types, i.e. use the same parameter values for each instance of a layer type. 
490        Options are `'CL'` (keep parameters fixed for convolutional layers), `'IL'` (keep parameters fixed for input layers), `'both'` 
491        (keep parameters fixed for both convolutional and input layers). 
492
493    - **wrap**: *boolean* 
494
495        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.     
496
497        
498    Returns:
499    ----
500    - **circuit** : *QuantumCircuit* 
501
502        The qiskit `QuantumCircuit` implementation of the network, with unassigned parameters.  
503 
504    """
505    # initialise parameter vector 
506    if repeat_params==None:
507        AA_CL_params=None 
508        NN_CL_params=None 
509        IL_params=None 
510    elif repeat_params=="CL":
511        IL_params=None  
512        AA_CL_params=ParameterVector(u"\u03B8_CL_AA", length= int((3 if real==False else 2 ) * (0.5 * m * (m-1))))
513        NN_CL_params=ParameterVector(u"\u03B8_CL_NN", length= int((3 if real==False else 2) * m))     
514    elif repeat_params=="IL":
515        IL_params=ParameterVector(u"\u03B8_IL", length= int((3 if real==False else 1) * n))
516        AA_CL_params=None 
517        NN_CL_params=None 
518    elif repeat_params=="both":
519        AA_CL_params=ParameterVector(u"\u03B8_CL_AA", length= int((3 if real==False else 2 ) * (0.5 * m * (m-1))))
520        NN_CL_params=ParameterVector(u"\u03B8_CL_NN", length= int((3 if real==False else 2) * m))
521        IL_params=ParameterVector(u"\u03B8_IL", length= int((3 if real==False else 1) * n))
522    else:
523        raise ValueError("Unrecognised option for 'repeat_params'. Should be None, 'CL', 'IL', or 'both'.")        
524        
525    # initialise empty input and target registers 
526    input_register = QuantumRegister(n, "input")
527    target_register = QuantumRegister(m, "target")
528    circuit = QuantumCircuit(input_register, target_register) 
529
530    # prepare registers 
531    circuit.h(target_register)
532    if encode:
533        circuit.compose(digital_encoding(n), input_register, inplace=True)
534
535    if input_Ry:
536        input_Ry_params = ParameterVector("Input_Ry",n)
537        for i in np.arange(n):
538            circuit.ry(input_Ry_params[i], input_register[i])
539         
540    if initial_IL: 
541        # apply input layer 
542        circuit.compose(input_layer(n,m, u"\u03B8_IL_0", real=real, params=IL_params, wrap=wrap), circuit.qubits, inplace=True)
543        
544    # apply convolutional layers (alternating between AA and NN)
545    # if toggle_IL is True, additional input layers are added after each NN
546    for i in np.arange(L):
547
548        if toggle_IL==False:
549
550            if i % 2 ==0:
551                circuit.compose(conv_layer_AA(m, u"\u03B8_CL_AA_{0}".format(i // 2), real=real, params=AA_CL_params,wrap=wrap), target_register, inplace=True)
552            elif i % 2 ==1:
553                circuit.compose(conv_layer_NN(m, u"\u03B8_CL_NN_{0}".format(i // 2),real=real, params=NN_CL_params, wrap=wrap), target_register, inplace=True)
554        
555        if toggle_IL==True:
556
557            # difference between number of input layers and number of target qubits:
558            del_IL = m - (int(initial_IL)+ L //3) 
559
560            if del_IL <= 0:
561                if i % 3 ==0:
562                    circuit.compose(conv_layer_AA(m, u"\u03B8_CL_AA_{0}".format(i // 3),real=real,params=AA_CL_params, wrap=wrap), target_register, inplace=True)
563                elif i % 3 ==1:
564                    circuit.compose(conv_layer_NN(m, u"\u03B8_CL_NN_{0}".format(i // 3),real=real,params=NN_CL_params, wrap=wrap), target_register, inplace=True)
565                elif i % 3 ==2:
566                    circuit.compose(input_layer(n,m, u"\u03B8_IL_{0}".format(i // 3 +1),shift=(i//3)+1, ctrl_state=(i % 2),real=real,params=IL_params, wrap=wrap), circuit.qubits, inplace=True) 
567            else: 
568                if i % 3 ==0:
569                    circuit.compose(conv_layer_AA(m, u"\u03B8_CL_AA_{0}".format(i // 3),real=real,params=AA_CL_params, wrap=wrap), target_register, inplace=True)
570                elif i % 3 ==1:
571                    circuit.compose(conv_layer_NN(m, u"\u03B8_CL_NN_{0}".format(i // 3),real=real,params=NN_CL_params, wrap=wrap), target_register, inplace=True)
572                elif i % 3 ==2:
573                    # padd with additional input layers
574                    for j in np.arange(del_IL // (L //3) +2):
575                        shift = (i // 3) +int(initial_IL) + j * (L //3)
576                        if shift <= m-1:
577                            circuit.compose(input_layer(n,m, u"\u03B8_IL_{0}".format(shift),shift=shift, ctrl_state=((shift-int(initial_IL)) % 2),real=real,params=IL_params, wrap=wrap), circuit.qubits, inplace=True) 
578
579        if inverse:
580            circuit=circuit.inverse()   
581
582    return circuit

Set up a QCNN consisting of input and convolutional layers acting on two distinct registers, the 'input register' and the 'target register'.

Input layers consist of single-qubit operations applied to the target register, controlled by the input register qubits. Convolutional layers consist of two-qubit operations applied to the target register. See input_layer() for more information on input layers and conv_layer_AA(), conv_layer_NN() for more information on convolutional layers.

The network was designed for the task of performing function evaluation, i.e. to implement an operator $\hat{Q}_\Psi$ such that $$ \hat{Q}_\Psi \ket{j}_i \ket{0}_t = \ket{j}_i \ket{\Psi(j)}_t,$$ for some function $\Psi$ with the subscripts $i$ and $t$ denoting the input and target registers, respectively. With some adaptation, the network structure could also be used for other applications.

Arguments:

  • n : int

    Number of qubits in the input register.

  • m : int

    Number of qubits in the target register.

  • L : int

    Number of layers in the network. Note that L does not take into account the optional initial input layer
    added by initial_IL. Further, L does not take into account the padding of the network with additional input layers to ensure the number of input layers is at least equal to m.

  • encode : boolean

    If True, apply an initial encoding circuit to the input register which can be used control the input states of the network. Default is False.

  • toggle_IL : boolean

    If True, every third layer added is an input layer and additional input layers are added to ensure the number of input layers is at least equal to m, with input layers alternating between control states 0 and 1. The input layer shift parameter is successively increased for each new input layer, resulting in each input qubit controlling an operation on each target qubit at some point in the network.

    If False, only convolutional layers are added. In either case, convolutional layers alternate between all-to-all and neighbour-to-neighbour layers. Default is True.

  • initial_IL : boolean

    If True, add an input layer at the beginning of the circuit. Default is True.

  • input_Ry : boolean

    If True, initially apply a sequence of parametrised Ry rotations to the input register. Default is False.

  • real : boolean

    If True, use the real versions of input and convolutional layers, which only contain CX and Ry gates and hence ensure real amplitudes. Default is False.

  • inverse : boolean

    If True, invert the circuit and return the inverse of the network.

  • repeat_params : str, optional

    Keep parameters fixed for different layer types, i.e. use the same parameter values for each instance of a layer type. Options are 'CL' (keep parameters fixed for convolutional layers), 'IL' (keep parameters fixed for input layers), 'both' (keep parameters fixed for both convolutional and input layers).

  • wrap: boolean

    If True, map rotation angles to an interval specified by map_angle(). Default is False.

Returns:

  • circuit : QuantumCircuit

    The qiskit QuantumCircuit implementation of the network, with unassigned parameters.

def A_generate_network(n, L, repeat_params=False, wrap=False):
584def A_generate_network(n,L, repeat_params=False, wrap=False):
585    r"""
586    Set up a network consisting of real convolutional layers acting on a single qubit register. 
587
588    Layers alternate between all-to-all and neighbour-to-neighbour convolutional layers. For more 
589    information see `conv_layer_AA()`, `conv_layer_NN()`. The convolutional layers are *real* in 
590    the sense of involving only CX and Ry operations, ensuring real amplitudes. 
591
592    The network was designed for the task of encoding a suitably normalised real function, $A(j)$, into the amplitudes of the 
593    register, i.e. to implement an operator $\hat{U}_A$ such that  
594    $$ \hat{U}_A \ket{j}= A(j) \ket{j}.$$
595    With some adaptation, the network structure could also be used for other applications. 
596
597    Arguments:
598    ---
599    - **n** : *int* 
600
601        Number of qubits in the register. 
602
603    - **L** : *int* 
604
605        Number of layers in the network. 
606
607    - **repeat_params** : *boolean* 
608
609        If True, use the same parameter values for each layer type. Default is False. 
610
611    - **wrap**: *boolean* 
612
613        If True, map rotation angles to an interval specified by `map_angle()`. Default is False.      
614
615    Returns:
616    ---
617    - **circuit** : *QuantumCircuit* 
618
619        The qiskit `QuantumCircuit` implementation of the network, with unassigned parameters.  
620                    
621    """
622
623    # initialise parameter vector 
624    if repeat_params:
625        AA_CL_params=ParameterVector(u"\u03B8_AA", length= int(2 * (0.5 * n * (n-1))))
626        NN_CL_params=ParameterVector(u"\u03B8_NN", length= int(2 * n))
627    else:
628        AA_CL_params = None 
629        NN_CL_params = None      
630
631
632    # initialise empty input register 
633    register = QuantumRegister(n, "reg")
634    circuit = QuantumCircuit(register) 
635
636    # prepare register
637    circuit.h(register)
638    
639    # apply L convolutional layers (alternating between AA and NN)
640    for i in np.arange(L):
641
642        if i % 2 ==0:
643            circuit.compose(conv_layer_AA(n, u"\u03B8_R_AA_{0}".format(i // 2), real=True, params=AA_CL_params,wrap=wrap), register, inplace=True)
644        elif i % 2 ==1:
645            circuit.compose(conv_layer_NN(n, u"\u03B8_R_NN_{0}".format(i // 2), real=True, params=NN_CL_params,wrap=wrap), register, inplace=True)
646         
647    return circuit 

Set up a network consisting of real convolutional layers acting on a single qubit register.

Layers alternate between all-to-all and neighbour-to-neighbour convolutional layers. For more information see conv_layer_AA(), conv_layer_NN(). The convolutional layers are real in the sense of involving only CX and Ry operations, ensuring real amplitudes.

The network was designed for the task of encoding a suitably normalised real function, $A(j)$, into the amplitudes of the register, i.e. to implement an operator $\hat{U}_A$ such that
$$ \hat{U}_A \ket{j}= A(j) \ket{j}.$$ With some adaptation, the network structure could also be used for other applications.

Arguments:

  • n : int

    Number of qubits in the register.

  • L : int

    Number of layers in the network.

  • repeat_params : boolean

    If True, use the same parameter values for each layer type. Default is False.

  • wrap: boolean

    If True, map rotation angles to an interval specified by map_angle(). Default is False.

Returns:

  • circuit : QuantumCircuit

    The qiskit QuantumCircuit implementation of the network, with unassigned parameters.

def get_state_vec(circuit):
649def get_state_vec(circuit):
650    """
651    Get statevector produced by a quantum circuit. 
652
653    Uses the `qiskit_aer` backend. 
654
655    Arguments:
656    ----
657    - **circuit** : *QuantumCircuit* 
658
659        The circuit to be evaluated. 
660
661    Returns:
662    ----
663    - **state_vector** : *array_like* 
664
665        Array storing the complex amplitudes of the system to be in each of the 
666        `2**circuit.num_qubits` basis states.     
667
668    """
669    
670    # Transpile for simulator
671    simulator = StatevectorSimulator()
672    circuit = transpile(circuit, simulator)
673
674    # Run and get counts
675    result = simulator.run(circuit).result()
676    state_vector= result.get_statevector(circuit)
677
678    return np.asarray(state_vector)

Get statevector produced by a quantum circuit.

Uses the qiskit_aer backend.

Arguments:

  • circuit : QuantumCircuit

    The circuit to be evaluated.

Returns:

  • state_vector : array_like

    Array storing the complex amplitudes of the system to be in each of the 2**circuit.num_qubits basis states.

def map_angle(theta):
680def map_angle(theta):
681    r"""
682    Map a rotation angle to the interval $(0, \frac{\pi}{2})$.
683
684    An inverse tan function is used for the mapping. 
685
686    Arguments:
687    ----
688    - **theta** : *float* 
689
690        Rotation angle. 
691
692    Returns:
693    ---
694    - **theta_mapped** : *float* 
695
696        Rotation angle mapped to the given interval.     
697
698    """
699
700    return np.arctan(theta) + np.pi /2  

Map a rotation angle to the interval $(0, \frac{\pi}{2})$.

An inverse tan function is used for the mapping.

Arguments:

  • theta : float

    Rotation angle.

Returns:

  • theta_mapped : float

    Rotation angle mapped to the given interval.