Sequential Quantum Gate Decomposer  v1.9.3
Powerful decomposition of general unitarias into one- and two-qubit gates gates
qgd_N_Qubit_Decomposition_adaptive.py
Go to the documentation of this file.
1 
3 """
4 Created on Tue Jun 30 15:44:26 2020
5 Copyright 2020 Peter Rakyta, Ph.D.
6 
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
10 
11  http://www.apache.org/licenses/LICENSE-2.0
12 
13 Unless required by applicable law or agreed to in writing, software
14 distributed under the License is distributed on an "AS IS" BASIS,
15 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 See the License for the specific language governing permissions and
17 limitations under the License.
18 
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see http://www.gnu.org/licenses/.
21 
22 @author: Peter Rakyta, Ph.D.
23 """
24 
25 
27 
28 
29 import numpy as np
30 from os import path
31 from squander.decomposition.qgd_N_Qubit_Decomposition_adaptive_Wrapper import qgd_N_Qubit_Decomposition_adaptive_Wrapper
32 from squander.gates.qgd_Circuit import qgd_Circuit
33 
34 
35 
36 
38 class qgd_N_Qubit_Decomposition_adaptive(qgd_N_Qubit_Decomposition_adaptive_Wrapper):
39 
40 
41 
48  def __init__( self, Umtx, level_limit_max=8, level_limit_min=0, topology=None, config={}, accelerator_num=0 ):
49 
50 
51  self.qbit_num = int(round( np.log2( len(Umtx) ) ))
52 
53  # validate input parameters
54 
55  topology_validated = list()
56  if isinstance(topology, list) or isinstance(topology, tuple):
57  for item in topology:
58  if isinstance(item, tuple) and len(item) == 2:
59  item_validated = (np.intc(item[0]), np.intc(item[1]))
60  topology_validated.append(item_validated)
61  else:
62  print("Elements of topology should be two-component tuples (int, int)")
63  return
64  elif topology == None:
65  pass
66  else:
67  print("Input parameter topology should be a list of (int, int) describing the connected qubits in the topology")
68  return
69 
70 
71  # config
72  if not( type(config) is dict):
73  print("Input parameter config should be a dictionary describing the following hyperparameters:") #TODO
74  return
75 
76  # call the constructor of the wrapper class
77  super().__init__(Umtx, self.qbit_num, level_limit_max, level_limit_min, topology=topology_validated, config=config, accelerator_num=accelerator_num)
78 
79 
80 
82  def Start_Decomposition(self,):
83 
84  # call the C wrapper function
85  super().Start_Decomposition()
86 
89 
90  # call the C wrapper function
91  super().get_Initial_Circuit()
92 
93 
95  def Compress_Circuit(self):
96 
97  # call the C wrapper function
98  super().Compress_Circuit()
99 
100 
102  def Finalize_Circuit(self):
103 
104  # call the C wrapper function
105  super().Finalize_Circuit()
106 
107 
110  def Reorder_Qubits( self, qbit_list ):
111 
112  # call the C wrapper function
113  super().Reorder_Qubits(qbit_list)
114 
115 
116 
118  def List_Gates(self):
119 
120  # call the C wrapper function
121  super().List_Gates()
122 
123 
124 
127  def get_Circuit( self ):
128 
129  # call the C wrapper function
130  return super().get_Circuit()
131 
132 
133 
136  def get_Qiskit_Circuit( self ):
137 
138  from squander import Qiskit_IO
139 
140  squander_circuit = self.get_Circuit()
141  parameters = self.get_Optimized_Parameters()
142 
143  return Qiskit_IO.get_Qiskit_Circuit( squander_circuit, parameters )
144 
145 
146 
147 
148 
151  def get_Cirq_Circuit( self ):
152 
153  import cirq
154 
155 
156  # creating Cirq quantum circuit
157  circuit = cirq.Circuit()
158 
159  # creating qubit register
160  q = cirq.LineQubit.range(self.qbit_num)
161 
162  # retrive the list of decomposing gate structure
163  gates = self.get_Gates()
164 
165  # constructing quantum circuit
166  for idx in range(len(gates)-1, -1, -1):
167 
168  gate = gates[idx]
169 
170  if gate.get("type") == "CNOT":
171  # adding CNOT gate to the quantum circuit
172  circuit.append(cirq.CNOT(q[self.qbit_num-1-gate.get("control_qbit")], q[self.qbit_num-1-gate.get("target_qbit")]))
173 
174  if gate.get("type") == "CRY":
175  # adding CRY gate to the quantum circuit
176  print("CRY gate needs to be implemented")
177 
178  elif gate.get("type") == "CZ":
179  # adding CZ gate to the quantum circuit
180  circuit.append(cirq.CZ(q[self.qbit_num-1-gate.get("control_qbit")], q[self.qbit_num-1-gate.get("target_qbit")]))
181 
182  elif gate.get("type") == "CH":
183  # adding CZ gate to the quantum circuit
184  circuit.append(cirq.CH(q[self.qbit_num-1-gate.get("control_qbit")], q[self.qbit_num-1-gate.get("target_qbit")]))
185 
186  elif gate.get("type") == "SYC":
187  # Sycamore gate
188  circuit.append(cirq.google.SYC(q[self.qbit_num-1-gate.get("control_qbit")], q[self.qbit_num-1-gate.get("target_qbit")]))
189 
190  elif gate.get("type") == "U3":
191  print("Unsupported gate in the Cirq export: U3 gate")
192  return None;
193 
194  elif gate.get("type") == "RX":
195  # RX gate
196  circuit.append(cirq.rx(gate.get("Theta")).on(q[self.qbit_num-1-gate.get("target_qbit")]))
197 
198  elif gate.get("type") == "RY":
199  # RY gate
200  circuit.append(cirq.ry(gate.get("Theta")).on(q[self.qbit_num-1-gate.get("target_qbit")]))
201 
202  elif gate.get("type") == "RZ":
203  # RZ gate
204  circuit.append(cirq.rz(gate.get("Phi")).on(q[self.qbit_num-1-gate.get("target_qbit")]))
205 
206  elif gate.get("type") == "X":
207  # X gate
208  circuit.append(cirq.x(q[self.qbit_num-1-gate.get("target_qbit")]))
209 
210  elif gate.get("type") == "Y":
211  # Y gate
212  circuit.append(cirq.y(q[self.qbit_num-1-gate.get("target_qbit")]))
213 
214  elif gate.get("type") == "Z":
215  # Z gate
216  circuit.append(cirq.z(q[self.qbit_num-1-gate.get("target_qbit")]))
217 
218 
219  elif gate.get("type") == "SX":
220  # RZ gate
221  circuit.append(cirq.sx(q[self.qbit_num-1-gate.get("target_qbit")]))
222 
223 
224  return circuit
225 
226 
227 
230  def import_Qiskit_Circuit( self, qc_in ):
231 
232  from qiskit import QuantumCircuit, transpile
233  from qiskit.circuit import ParameterExpression
234  from qgd_python.gates.qgd_Circuit_Wrapper import qgd_Circuit_Wrapper
235 
236  qc = transpile(qc_in, optimization_level=0, basis_gates=['cz', 'u3'], layout_method='sabre')
237  #print('Depth of imported Qiskit transpiled quantum circuit:', qc.depth())
238  print('Gate counts in teh imported Qiskit transpiled quantum circuit:', qc.count_ops())
239  #print(qc)
240 
241 
242  # get the register of qubits
243  q_register = qc.qubits
244 
245  # get the size of the register
246  register_size = qc.num_qubits
247 
248  # prepare dictionary for single qubit gates
249  single_qubit_gates = dict()
250  for idx in range(register_size):
251  single_qubit_gates[idx] = []
252 
253  # prepare dictionary for two-qubit gates
254  two_qubit_gates = list()
255 
256  # construct the qgd gate structure
257  Circuit_ret = qgd_Circuit_Wrapper(register_size)
258  optimized_parameters = list()
259 
260  for gate in qc.data:
261 
262  name = gate.operation.name
263  if name == 'u3':
264  # add u3 gate
265  qubits = gate.qubits
266 
267  qubit = q_register.index( qubits[0] ) # qubits[0].index
268  single_qubit_gates[qubit].append( {'params': gate.operation.params, 'type': 'u3'} )
269 
270  elif name == 'cz':
271  # add cz gate
272  qubits = gate.qubits
273 
274  qubit0 = q_register.index( qubits[0] ) #qubits[0].index
275  qubit1 = q_register.index( qubits[1] ) #qubits[1].index
276 
277 
278  two_qubit_gate = {'type': 'cz', 'qubits': [qubit0, qubit1]}
279 
280 
281 
282  # creating an instance of the wrapper class qgd_Circuit_Wrapper
283  Layer = qgd_Circuit_Wrapper( register_size )
284 
285  # retrive the corresponding single qubit gates and create layer from them
286  if len(single_qubit_gates[qubit0])>0:
287  gate0 = single_qubit_gates[qubit0][0]
288  single_qubit_gates[qubit0].pop(0)
289 
290  if gate0['type'] == 'u3':
291  Layer.add_U3( qubit0, True, True, True )
292  params = gate0['params']
293  params.reverse()
294  for param in params:
295  optimized_parameters = optimized_parameters + [float(param)]
296  optimized_parameters[-1] = optimized_parameters[-1]/2
297 
298 
299  if len(single_qubit_gates[qubit1])>0:
300  gate1 = single_qubit_gates[qubit1][0]
301  single_qubit_gates[qubit1].pop(0)
302 
303  if gate1['type'] == 'u3':
304  Layer.add_U3( qubit1, True, True, True )
305  params = gate1['params']
306  params.reverse()
307  for param in params:
308  optimized_parameters = optimized_parameters + [float(param)]
309  optimized_parameters[-1] = optimized_parameters[-1]/2
310 
311 
313 
314 
315  Layer.add_RX( qubit0 )
316  Layer.add_adaptive( qubit0, qubit1 )
317  Layer.add_RZ( qubit1 )
318  Layer.add_RX( qubit0 )
319  optimized_parameters = optimized_parameters + [np.pi/4, np.pi/2, -np.pi/2, -np.pi/4]
320  #optimized_parameters = optimized_parameters + [np.pi]
321 
322  Circuit_ret.add_Circuit( Layer )
323 
324 
325 
326  # add remaining single qubit gates
327  # creating an instance of the wrapper class qgd_Circuit
328  Layer = qgd_Circuit( register_size )
329 
330  for qubit in range(register_size):
331 
332  gates = single_qubit_gates[qubit]
333  for gate in gates:
334 
335  if gate['type'] == 'u3':
336  Layer.add_U3( qubit, True, True, True )
337  params = gate['params']
338  params.reverse()
339  for param in params:
340  optimized_parameters = optimized_parameters + [float(param)]
341  optimized_parameters[-1] = optimized_parameters[-1]/2
342 
343  Circuit_ret.add_Circuit( Layer )
344 
345  optimized_parameters = np.asarray(optimized_parameters, dtype=np.float64)
346 
347  # setting gate structure and optimized initial parameters
348  self.set_Gate_Structure(Circuit_ret)
349 
350  self.set_Optimized_Parameters( np.flip(optimized_parameters,0) )
351  #self.set_Optimized_Parameters( optimized_parameters )
352 
353 
354 
357  def set_Gate_Structure( self, Gate_structure ):
358 
359  if not isinstance(Gate_structure, qgd_Circuit) :
360  raise Exception("Input parameter Gate_structure should be a an instance of Circuit")
361 
362 
363  return super().set_Gate_Structure( Gate_structure )
364 
365 
366 
367 
370  def set_Gate_Structure_From_Binary( self, filename ):
371 
372  return super().set_Gate_Structure_From_Binary( filename )
373 
374 
375 
378 
379  return super().add_Layer_To_Imported_Gate_Structure()
380 
381 
382 
383 
385  def set_Randomized_Radius( self, radius ):
386 
387  return super().set_Randomized_Radius(radius)
388 
389 
391  def add_Gate_Structure_From_Binary( self, filename ):
392 
393  return super().add_Gate_Structure_From_Binary( filename )
394 
395 
397  def set_Unitary_From_Binary( self, filename ):
398 
399  return super().set_Unitary_From_Binary( filename )
400 
401 
404  def set_Unitary( self, Umtx_arr ):
405 
406  return super().set_Unitary( Umtx_arr )
407 
408 
409 
412  def set_Optimization_Tolerance( self, tolerance ):
413 
414  return super().set_Optimization_Tolerance( tolerance )
415 
416 
418  def get_Unitary( self ):
419 
420  return super().get_Unitary()
421 
422 
425  def export_Unitary( self, filename ):
426 
427  return super().export_Unitary(filename)
428 
429 
430 
432  def get_Parameter_Num( self ):
433 
434  return super().get_Parameter_Num()
435 
436 
438  def get_Global_Phase( self ):
439 
440  return super().get_Global_Phase()
441 
442 
445  def set_Global_Phase( self, new_global_phase ):
446 
447  return super().set_Global_Phase(new_global_phase)
448 
450  def get_Project_Name( self ):
451 
452  return super().get_Project_Name()
453 
454 
457  def set_Project_Name( self, project_name_new ):
458 
459  return super().set_Project_Name(project_name_new)
460 
463 
464  return super().apply_Global_Phase_Factor()
465 
466 
468  def add_Adaptive_Layers( self ):
469 
470  return super().add_Adaptive_Layers()
471 
472 
475 
477 
478 
479 
482 
483  return super().apply_Imported_Gate_Structure()
484 
487  def set_Optimizer( self, optimizer="BFGS" ):
488 
489  # Set the optimizer
490  super().set_Optimizer(optimizer)
491 
492 
493 
496  def get_Matrix( self, parameters = None ):
497 
498 
499  if parameters is None:
500  print( "get_Matrix: arary of input parameters is None")
501  return None
502 
503  return super().get_Matrix( parameters )
504 
505 
508  def set_Cost_Function_Variant( self, costfnc=0 ):
509 
510  # Set the optimizer
511  super().set_Cost_Function_Variant(costfnc=costfnc)
512 
513 
514 
517  def set_Iteration_Threshold_of_Randomization( self, threshold=2500 ):
518 
519  # Set the threshold
520  super().set_Iteration_Threshold_of_Randomization(threshold)
521 
522 
523 
524 
527  def set_Trace_Offset( self, trace_offset=0 ):
528 
529  # Set the trace offset
530  super().set_Trace_Offset(trace_offset=trace_offset)
531 
532 
533 
536  def get_Trace_Offset( self ):
537 
538  # Set the optimizer
539  return super().get_Trace_Offset()
540 
541 
542 
546 
547  return super().get_Optimized_Parameters()
548 
549 
550 
553  def set_Optimized_Parameters(self, new_params):
554 
555  super().set_Optimized_Parameters(new_params)
556 
557 
558 
561  def Optimization_Problem( self, parameters=None ):
562 
563  if parameters is None:
564  print( "Optimization_Problem: array of input parameters is None")
565  return None
566 
567  # evaluate the cost function and gradients
568  cost_function = super().Optimization_Problem(parameters)
569 
570 
571  return cost_function
572 
573 
574 
577  def Optimization_Problem_Grad( self, parameters=None ):
578 
579  if parameters is None:
580  print( "Optimization_Problem: array of input parameters is None")
581  return None
582 
583  # evaluate the cost function and gradients
584  grad = super().Optimization_Problem_Grad(parameters)
585 
586  grad = grad.reshape( (-1,))
587 
588  return grad
589 
590 
591 
594  def Optimization_Problem_Combined( self, parameters=None ):
595 
596  if parameters is None:
597  print( "Optimization_Problem_Combined: array of input parameters is None")
598  return None
599 
600  # evaluate the cost function and gradients
601  cost_function, grad = super().Optimization_Problem_Combined(parameters)
602 
603  grad = grad.reshape( (-1,))
604 
605  return cost_function, grad
606 
607 
609  def get_Num_of_Iters(self):
610 
611  return super().get_Num_of_Iters()
612 
613 
616  def set_Max_Iterations(self, max_iters):
617 
618  super().set_Max_Iterations(max_iters)
619 
620 
623  def set_Cost_Function_Variant(self, cost_func):
624 
625  super().set_Cost_Function_Variant(cost_func)
626 
627 
628 
632 
633  return super().get_Decomposition_Error()
634 
635 
636 
637 
643  def get_Second_Renyi_Entropy(self, parameters=None, input_state=None, qubit_list=None ):
644 
645  qbit_num = self.get_Qbit_Num()
646 
647  qubit_list_validated = list()
648  if isinstance(qubit_list, list) or isinstance(qubit_list, tuple):
649  for item in qubit_list:
650  if isinstance(item, int):
651  qubit_list_validated.append(item)
652  qubit_list_validated = list(set(qubit_list_validated))
653  else:
654  print("Elements of qbit_list should be integers")
655  return
656  elif qubit_list == None:
657  qubit_list_validated = [ x for x in range(qbit_num) ]
658 
659  else:
660  print("Elements of qbit_list should be integers")
661  return
662 
663 
664  if parameters is None:
665  print( "get_Second_Renyi_entropy: array of input parameters is None")
666  return None
667 
668 
669  if input_state is None:
670  matrix_size = 1 << qbit_num
671  input_state = np.zeros( (matrix_size,1) )
672  input_state[0] = 1
673 
674  # evaluate the entropy
675  entropy = super().get_Second_Renyi_Entropy( parameters, input_state, qubit_list_validated)
676 
677 
678  return entropy
679 
680 
681 
684  def get_Qbit_Num(self):
685 
686  return super().get_Qbit_Num()
def set_Optimized_Parameters(self, new_params)
Call to set the parameters which are used as a starting point in the optimization.
def set_Trace_Offset(self, trace_offset=0)
Call to set the trace offset used in the cost function.
def get_Initial_Circuit(self)
Wrapper function to call the get_initial_circuit method of C++ class N_Qubit_Decomposition.
def export_Unitary(self, filename)
Call to export unitary matrix to binary file.
A QGD Python interface class for the Gates_Block.
Definition: qgd_Circuit.py:60
def Optimization_Problem_Grad(self, parameters=None)
Call to evaluate the gradient components.
def set_Gate_Structure(self, Gate_structure)
Call to set custom gate structure to used in the decomposition.
def set_Global_Phase(self, new_global_phase)
Call to set global phase.
def set_Max_Iterations(self, max_iters)
Call to set the maximum number of iterations for each optimization loop.
def apply_Imported_Gate_Structure(self)
Call to apply the imported gate structure on the unitary.
def set_Randomized_Radius(self, radius)
Call to set the radius in which randomized parameters are generated around the current minimum duting...
def add_Layer_To_Imported_Gate_Structure(self)
Call to add an adaptive layer to the gate structure previously imported gate structure.
def get_Matrix(self, parameters=None)
Call to retrieve the unitary of the circuit.
def List_Gates(self)
Call to print the gates decomposing the initial unitary.
def set_Project_Name(self, project_name_new)
Call to set the name of the SQUANDER project.
def get_Project_Name(self)
Call to get the name of the SQUANDER project.
def set_Unitary(self, Umtx_arr)
Call to set unitary matrix from a numpy array.
def Start_Decomposition(self)
Wrapper function to call the start_decomposition method of C++ class N_Qubit_Decomposition.
def Reorder_Qubits(self, qbit_list)
Call to reorder the qubits in the matrix of the gate.
def set_Iteration_Threshold_of_Randomization(self, threshold=2500)
Call to set the threshold value for the count of interations, above which the parameters are randomiz...
def add_Adaptive_Layers(self)
Call to add adaptive layers to the gate structure stored by the class.
def add_Finalyzing_Layer_To_Gate_Structure(self)
Call to add finalyzing layer (single qubit rotations on all of the qubits) to the gate structure...
def get_Second_Renyi_Entropy(self, parameters=None, input_state=None, qubit_list=None)
Call to get the second Rényi entropy.
def get_Qiskit_Circuit(self)
Export the unitary decomposition into Qiskit format.
def apply_Global_Phase_Factor(self)
Call to apply global phase on Unitary matrix.
def set_Optimization_Tolerance(self, tolerance)
Call to set the error tolerance of the decomposition.
def Compress_Circuit(self)
Wrapper function to call the compress_circuit method of C++ class N_Qubit_Decomposition.
def get_Parameter_Num(self)
Call to get the number of free parameters in the gate structure used for the decomposition.
def get_Trace_Offset(self)
Call to get the trace offset used in the cost function.
def Optimization_Problem(self, parameters=None)
Call to evaluate the cost function.
def get_Qbit_Num(self)
Call to get the number of qubits in the circuit.
A QGD Python interface class for the decomposition of N-qubit unitaries into U3 and CNOT gates...
def set_Unitary_From_Binary(self, filename)
Call to set unitary matrix from a binary file created from SQUANDER.
def get_Cirq_Circuit(self)
Export the unitary decomposition into Qiskit format.
def set_Cost_Function_Variant(self, costfnc=0)
Call to set the optimizer used in the gate synthesis process.
def __init__(self, Umtx, level_limit_max=8, level_limit_min=0, topology=None, config={}, accelerator_num=0)
Constructor of the class.
def get_Optimized_Parameters(self)
Call to get the optimized parameters set in numpy array.
def Optimization_Problem_Combined(self, parameters=None)
Call to evaluate the cost function and the gradient components.
def add_Gate_Structure_From_Binary(self, filename)
Call to append custom layers to the gate structure that are intended to be used in the decomposition ...
def set_Optimizer(self, optimizer="BFGS")
Call to set the optimizer used in the gate synthesis process.
def set_Gate_Structure_From_Binary(self, filename)
Call to set custom layers to the gate structure that are intended to be used in the decomposition fro...
Type definition of the qgd_Circuit_Wrapper Python class of the qgd_Circuit_Wrapper module...
def Finalize_Circuit(self)
Wrapper function to call the finalize_circuit method of C++ class N_Qubit_Decomposition.
def get_Decomposition_Error(self)
Call to get the error of the decomposition.
def get_Circuit(self)
Call to retrieve the incorporated quantum circuit (Squander format)
def import_Qiskit_Circuit(self, qc_in)
Call to import initial quantum circuit in QISKIT format to be further comporessed.