Sequential Quantum Gate Decomposer  v1.9.3
Powerful decomposition of general unitarias into one- and two-qubit gates gates
qgd_SABRE.py
Go to the documentation of this file.
1 #Source paper: https://arxiv.org/pdf/1809.02573
2 
4 """
5 Created on Tue Jun 30 15:44:26 2020
6 Copyright 2020 Peter Rakyta, Ph.D.
7 
8 Licensed under the Apache License, Version 2.0 (the "License");
9 you may not use this file except in compliance with the License.
10 You may obtain a copy of the License at
11 
12  http://www.apache.org/licenses/LICENSE-2.0
13 
14 Unless required by applicable law or agreed to in writing, software
15 distributed under the License is distributed on an "AS IS" BASIS,
16 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 See the License for the specific language governing permissions and
18 limitations under the License.
19 
20 @author: Peter Rakyta, Ph.D.
21 """
22 from os import path
23 import numpy as np
24 from squander.gates.qgd_Circuit import qgd_Circuit as Circuit
25 from squander.gates.gates_Wrapper import CNOT
26 
27 
29 
30 
31 class qgd_SABRE:
32 
38  def __init__(self,quantum_circuit,topology, max_lookahead=4, max_E_size=20,W=0.5,alpha=0.9):
39  self.circuit_qbit_num = quantum_circuit.get_Qbit_Num() # number of qubits in circuit to be mapped and routed
40  self.qbit_num = max(max(topology))+1 # number of qubits in system to be mapped to
41  self.initial_circuit = quantum_circuit
42  self.pi = np.arange(self.qbit_num) #initial mapping
43  np.random.shuffle(self.pi)
44  self.topology = topology
45  self.D = self.Floyd_Warshall()
46  self.max_E_size = max_E_size
47  self.W = W
48  self.alpha = alpha
49  self.possible_connections_control,self.neighbours = self.geneterate_possible_connections()
50  self.max_lookahead = max_lookahead
51 
52 
54  def Floyd_Warshall(self):
55  D = np.ones((self.qbit_num,self.qbit_num))*np.inf
56  for vertex in self.topology:
57  u,v = vertex
58  D[u][v] = 1
59  D[v][u] = 1
60  for idx in range(self.qbit_num):
61  D[idx][idx] = 0
62  for k in range(self.qbit_num):
63  for i in range(self.qbit_num):
64  for j in range(self.qbit_num):
65  if D[i][j] > D[i][k] + D[k][j]:
66  D[i][j] = D[i][k] + D[k][j]
67  return D
68 
71  possible_connections_control = [ [] for _ in range(self.qbit_num) ]
72  neighbours = [ [] for _ in range(self.qbit_num) ]
73  for connection in self.topology:
74  qbit1,qbit2 = connection
75  possible_connections_control[qbit2].append(qbit1)
76  if qbit2 not in neighbours[qbit1]:
77  neighbours[qbit1].append(qbit2)
78  if qbit1 not in neighbours[qbit2]:
79  neighbours[qbit2].append(qbit1)
80  return possible_connections_control,neighbours
81 
84  def get_reverse_circuit(self, circuit):
85  gates = circuit.get_Gates()
86  reverse_circuit = Circuit(self.circuit_qbit_num)
87  for idx in range(len(gates)):
88  gate_idx = len(gates)-1-idx
89  gate = gates[gate_idx]
90  reverse_circuit.add_Gate(gate)
91  return reverse_circuit
92 
95  def generate_DAG(self,circuit):
96  DAG = []
97  gates = circuit.get_Gates()
98  for gate in gates:
99  DAG.append([gate,circuit.get_Children(gate)])
100  return DAG
101 
104  def generate_inverse_DAG(self,circuit):
105  inverse_DAG = []
106  gates = circuit.get_Gates()
107  for gate in gates:
108  inverse_DAG.append([gate,circuit.get_Parents(gate)])
109  return inverse_DAG
110 
113  def get_initial_layer(self,circuit):
114  initial_layer = []
115  gates = circuit.get_Gates()
116  for gate_idx in range(len(gates)):
117  gate = gates[gate_idx]
118  if len(circuit.get_Parents(gate))==0:
119  initial_layer.append(gate_idx)
120  return initial_layer
121 
126  def is_gate_possible(self,pi,q_target,q_control):
127  if q_control == -1:
128  return True,0
129  else:
130  Q_control = pi[q_control]
131  Q_target = pi[q_target]
132  if Q_target in self.possible_connections_control[Q_control]:
133  return True,0
134  elif Q_control in self.possible_connections_control[Q_target]:
135  return True,1
136 
137  return False,0
138 
139 
144  def H_basic(self,pi,q1,q2):
145  if q2==-1:
146  return 0
147  return self.D[pi[q1]][pi[q2]]
148 
152  def update_pi(self, pi, SWAP):
153  q1,q2 = SWAP
154  placeholder = pi[q2]
155  pi[q2] = pi[q1]
156  pi[q1] = placeholder
157 
158 
161  def get_inverse_pi(self, pi):
162  inverse_pi = list(range(len(pi)))
163  for i in range(len(pi)):
164  Q = pi[i]
165  inverse_pi[Q] = i
166  return inverse_pi
167 
168 
173  def obtain_SWAPS(self, F, pi, DAG):
174  swaps = []
175  inverse_pi = self.get_inverse_pi(pi)
176  involved_qbits = []
177  for gate_idx in F:
178  gate = DAG[gate_idx][0]
179  q_control = gate.get_Control_Qbit()
180  q_target = gate.get_Target_Qbit()
181  if q_control == -1:
182  continue
183  if q_control not in involved_qbits:
184  involved_qbits.append(q_control)
185  if q_target not in involved_qbits:
186  involved_qbits.append(q_target)
187  for qbit in involved_qbits:
188  candidates = self.neighbours[pi[qbit]]
189  for Q_cand in candidates:
190  swap = [min(qbit,inverse_pi[Q_cand]),max(qbit,inverse_pi[Q_cand])]
191  if swap not in swaps:
192  swaps.append(swap)
193  return swaps
194 
195 
199  def generate_E(self,F,DAG,IDAG,resolved_gates):
200  E = []
201  for front_gate in F:
202  if len(E) < self.max_E_size:
203  involved_qbits = [DAG[front_gate][0].get_Target_Qbit(),DAG[front_gate][0].get_Control_Qbit()]
204  lookahead_count = 0
205  children = list(DAG[front_gate][1]).copy()
206  while (lookahead_count<self.max_lookahead and len(children)>0):
207  for gate_idx in children.copy():
208  gate = DAG[gate_idx][0]
209  if gate.get_Control_Qbit() == -1:
210  children.extend(DAG[gate_idx][1])
211  else:
212  gate_depth = self.calculate_gate_depth(gate_idx,IDAG,resolved_gates)
213  if len(E)< self.max_E_size and (gate.get_Target_Qbit() in involved_qbits or gate.get_Control_Qbit() in involved_qbits) and gate_idx not in E and gate_depth<6:
214  E.append(gate_idx)
215  lookahead_count +=1
216  if lookahead_count<self.max_lookahead:
217  children.extend(DAG[gate_idx][1])
218  children.remove(gate_idx)
219  return E
220 
221 
222 
228  def calculate_gate_depth(self,gate_idx,IDAG,resolved_gates):
229  depth = 1
230  gate = IDAG[gate_idx][0]
231  parents = list(IDAG[gate_idx][1])
232  while (len(parents)>0):
233  for parent_idx in parents.copy():
234  parent_gate = IDAG[parent_idx][0]
235  if resolved_gates[parent_idx]==False:
236  if parent_gate.get_Control_Qbit() != -1:
237  depth += 1
238  parents.extend(IDAG[parent_idx][1])
239  parents.remove(parent_idx)
240  return depth
241 
246  def check_dependencies(self,gate_idx,IDAG,resolved_gates):
247  resolved = True
248  gate = IDAG[gate_idx][0]
249  parents = list(IDAG[gate_idx][1])
250  while (len(parents)>0 and resolved!=False):
251  for parent_idx in parents.copy():
252  parent_gate = IDAG[parent_idx][0]
253  resolved *= resolved_gates[parent_idx]
254  parents.extend(IDAG[parent_idx][1])
255  parents.remove(parent_idx)
256  return resolved
257 
258 
259 
265 
266  def Heuristic_search(self,F,pi,DAG,IDAG):
267  swap_count = 0
268  resolved_gates = [False]*len(DAG)
269  flags = [0]*len(DAG)
270  swap_list = []
271  gate_order = []
272  E = self.generate_E(F,DAG,IDAG,resolved_gates)
273  swap_prev = []
274  while len(F)!=0:
275  execute_gate_list = []
276  for gate_idx in F:
277  gate = DAG[gate_idx][0]
278  possible, flag = self.is_gate_possible(pi,gate.get_Target_Qbit(),gate.get_Control_Qbit())
279  if possible:
280  execute_gate_list.append(gate_idx)
281  resolved_gates[gate_idx] = True
282  flags[gate_idx] = flag
283  if len(execute_gate_list ) != 0:
284  for gate_idx in execute_gate_list:
285  F.remove(gate_idx)
286  gate_order.append(gate_idx)
287  successors = DAG[gate_idx][1]
288  for new_gate_idx in successors:
289  resolved = self.check_dependencies(new_gate_idx,IDAG,resolved_gates)
290  if resolved and new_gate_idx not in F:
291  F.append(new_gate_idx)
292  E = self.generate_E(F,DAG,IDAG,resolved_gates)
293 
294  continue
295  else:
296  scores = []
297  swaps = self.obtain_SWAPS(F,pi,DAG)
298  if len(swaps)!=0:
299  for SWAP_idx in range(len(swaps)):
300  SWAP = swaps[SWAP_idx]
301  if SWAP == swap_prev:
302  scores.append(np.inf)
303  continue
304  pi_temp = pi.copy()
305  self.update_pi(pi_temp,SWAP)
306  score = 0
307  for gate_idx in F:
308  gate = DAG[gate_idx][0]
309  q_target, q_control = gate.get_Target_Qbit(),gate.get_Control_Qbit()
310  score += self.H_basic(pi_temp,q_target, q_control)
311  score *= 1/len(F)
312  if len(E) != 0:
313  score_temp = 0
314  for gate_idx in E:
315  gate = DAG[gate_idx][0]
316  q_target, q_control = gate.get_Target_Qbit(),gate.get_Control_Qbit()
317  score_temp += self.H_basic(pi_temp,q_target, q_control)*(self.alpha**self.calculate_gate_depth(gate_idx,IDAG,resolved_gates))
318  score_temp *= self.W/len(E)
319  score+=score_temp
320  scores.append(score)
321  min_idx = np.argmin(np.array(scores))
322  min_swap = swaps[min_idx]
323  swap_prev = min_swap
324  gate_order.append(min_swap)
325  self.update_pi(pi,min_swap)
326  swap_count +=1
327  else:
328  print("ERORR: circuit cannot be mapped please check topology")
329  break
330  return gate_order,flags,swap_count
331 
332 
339  def get_mapped_circuit(self,circuit,init_pi,gate_order,flags,parameters):
340  circuit_mapped = Circuit(self.qbit_num)
341  gates = circuit.get_Gates()
342  parameters_new = np.array([])
343  for gate_idx in gate_order:
344  if isinstance(gate_idx,int):
345  gate=gates[gate_idx]
346  if gate.get_Parameter_Num() != 0:
347  start_idx = gate.get_Parameter_Start_Index()
348  params = parameters[start_idx:start_idx + gate.get_Parameter_Num()]
349  parameters_new = np.append(parameters_new,params)
350  q_target,q_control = gate.get_Target_Qbit(),gate.get_Control_Qbit()
351  Q_target,Q_control = init_pi[q_target],init_pi[q_control]
352  gate.set_Target_Qbit(Q_target)
353  gate.set_Control_Qbit(Q_control)
354  if flags[gate_idx] == 1:
355  gate.set_Target_Qbit(Q_control)
356  gate.set_Control_Qbit(Q_target)
357  if isinstance(gate,CNOT):
358  circuit_mapped.add_H(Q_target)
359  circuit_mapped.add_H(Q_control)
360  circuit_mapped.add_Gate(gate)
361  if flags[gate_idx] == 1 and isinstance(gate,CNOT):
362  circuit_mapped.add_H(Q_target)
363  circuit_mapped.add_H(Q_control)
364  else:
365  q1,q2 = gate_idx[0],gate_idx[1]
366  Q1,Q2 = init_pi[q1],init_pi[q2]
367  circuit_mapped.add_CNOT(Q1,Q2)
368  circuit_mapped.add_CNOT(Q2,Q1)
369  circuit_mapped.add_CNOT(Q1,Q2)
370  self.update_pi(init_pi,[q1,q2])
371 
372  return circuit_mapped,parameters_new
373 
374 
378  def map_circuit(self,parameters=np.array([]),iter_num=1):
379  init_pi = self.pi.copy()
380  circuit = self.initial_circuit
381  reverse_circuit = self.get_reverse_circuit(circuit)
382  DAG = self.generate_DAG(circuit)
383  IDAG = self.generate_inverse_DAG(circuit)
384  first_layer = self.get_initial_layer(circuit)
385  DAG_reverse = self.generate_DAG(reverse_circuit)
386  IDAG_reverse = self.generate_inverse_DAG(reverse_circuit)
387  first_layer_reverse = self.get_initial_layer(reverse_circuit)
388  for idx in range(iter_num):
389  self.Heuristic_search(first_layer.copy(),init_pi,DAG,IDAG)
390  self.Heuristic_search(first_layer_reverse.copy(),init_pi,DAG_reverse,IDAG_reverse)
391  final_pi = init_pi.copy()
392  gate_order,flags,swap_count = self.Heuristic_search(first_layer.copy(),final_pi,DAG,IDAG)
393  circuit_mapped,parameters_new = self.get_mapped_circuit(circuit,init_pi.copy(),gate_order,flags,parameters)
394  return circuit_mapped,parameters_new,init_pi,final_pi,swap_count
def is_gate_possible(self, pi, q_target, q_control)
checks if connection between two virtual qubits q_target and q_control is possible, if second return is 1 it means gate might need to be inverted (with H gates in the case of CNOT)
Definition: qgd_SABRE.py:126
def get_initial_layer(self, circuit)
generates initial layer of input circuit for more see at: https://arxiv.org/pdf/1809.02573
Definition: qgd_SABRE.py:113
def update_pi(self, pi, SWAP)
basic cost function that returns distance of two virtual qubits q1 and q2
Definition: qgd_SABRE.py:152
def obtain_SWAPS(self, F, pi, DAG)
Get all possible swaps between all involved qubits in F F front layer containing unresolved gates...
Definition: qgd_SABRE.py:173
def calculate_gate_depth(self, gate_idx, IDAG, resolved_gates)
Calculates the number of predecessing unresolved gates for a gate.
Definition: qgd_SABRE.py:228
def map_circuit(self, parameters=np.array([]), iter_num=1)
Returns mapped circuit on physical qubits Q with swaps included.
Definition: qgd_SABRE.py:378
def H_basic(self, pi, q1, q2)
basic cost function that returns distance of two virtual qubits q1 and q2
Definition: qgd_SABRE.py:144
def Floyd_Warshall(self)
Floyd_warshall algorithm to calculate distance matrix.
Definition: qgd_SABRE.py:54
def generate_E(self, F, DAG, IDAG, resolved_gates)
Generates lookahead extended layer containing all upcoming two qubit gates.
Definition: qgd_SABRE.py:199
def geneterate_possible_connections(self)
generate list of possible connections based on inputted topology
Definition: qgd_SABRE.py:70
def Heuristic_search(self, F, pi, DAG, IDAG)
Heuristic search to map circuit to topology based on initial mapping pi this is an implementation of ...
Definition: qgd_SABRE.py:266
def __init__(self, quantum_circuit, topology, max_lookahead=4, max_E_size=20, W=0.5, alpha=0.9)
Constructor of the class.
Definition: qgd_SABRE.py:38
def generate_inverse_DAG(self, circuit)
generates inverse DAG of input circuit
Definition: qgd_SABRE.py:104
def get_reverse_circuit(self, circuit)
generates reverse circuit
Definition: qgd_SABRE.py:84
def get_mapped_circuit(self, circuit, init_pi, gate_order, flags, parameters)
Returns mapped circuit on physical qubits Q with swaps included.
Definition: qgd_SABRE.py:339
def check_dependencies(self, gate_idx, IDAG, resolved_gates)
Checks if gate dependencies have been resolved.
Definition: qgd_SABRE.py:246
def get_Control_Qbit(self)
Definition: qgd_CROT.py:87
def generate_DAG(self, circuit)
generates DAG of input circuit
Definition: qgd_SABRE.py:95
def get_inverse_pi(self, pi)
returns inverse pi mapping physical qubits Q to virtual qubits q
Definition: qgd_SABRE.py:161
def get_Target_Qbit(self)
Definition: qgd_CROT.py:80