Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
qgd_SABRE.py
Go to the documentation of this file.
1 """
2 Simplified SABRE improvements focusing on key quality enhancements
3 without breaking existing functionality.
4 """
5 
6 import numpy as np
7 from collections import deque, defaultdict
8 from squander.gates.qgd_Circuit import qgd_Circuit as Circuit
9 from squander.gates.gates_Wrapper import CNOT
10 
11 
12 class qgd_SABRE:
13  def __init__(
14  self,
15  quantum_circuit,
16  topology,
17  max_lookahead=4,
18  max_E_size=20,
19  W=0.5,
20  alpha=0.9,
21  initial_layout="smart",
22  score_tolerance=0.01,
23  stochastic_selection=False,
24  random_seed=None,
25  ):
26  """
27  Parameters match original with addition of:
28  initial_layout: 'smart', 'random', or 'trivial'
29  """
30  self.circuit_qbit_num = quantum_circuit.get_Qbit_Num()
31  self.qbit_num = max(max(topology)) + 1
32  self.initial_circuit = quantum_circuit
33 
34  # Initialize RNG
35  self.rng = np.random.RandomState(random_seed)
36 
37  self.score_tolerance = score_tolerance
38  self.stochastic_selection = stochastic_selection
39 
40  # Build topology first
41  self.topology = topology
42  self.D = self.compute_distances_bfs()
43  self.possible_connections_control, self.neighbours = (
45  )
46 
47  # Smart initial mapping (KEY IMPROVEMENT)
48  if initial_layout == "smart":
49  self.pi = self._compute_smart_initial_layout(quantum_circuit)
50  elif initial_layout == "random":
51  self.pi = np.arange(self.qbit_num)
52  self.rng.shuffle(self.pi)
53  else: # 'trivial'
54  self.pi = np.arange(self.qbit_num)
55 
56  self.max_E_size = max_E_size
57  self.W = W
58  self.alpha = alpha
59  self.max_lookahead = max_lookahead
60 
61  def _compute_smart_initial_layout(self, circuit):
62  """
63  Smart initial layout based on circuit connectivity.
64  This is the MOST IMPORTANT improvement for reducing SWAPs.
65  """
66  # Count interactions between qubits
67  interaction_count = defaultdict(int)
68  gates = circuit.get_Gates()
69 
70  for gate in gates:
71  if gate.get_Control_Qbit() != -1:
72  q1 = gate.get_Target_Qbit()
73  q2 = gate.get_Control_Qbit()
74  if q1 < self.circuit_qbit_num and q2 < self.circuit_qbit_num:
75  key = (min(q1, q2), max(q1, q2))
76  interaction_count[key] += 1
77 
78  if not interaction_count:
79  # No 2-qubit gates, use trivial mapping
80  return np.arange(self.qbit_num)
81 
82  # Find most interacting qubit pair
83  most_connected = max(interaction_count.items(), key=lambda x: x[1])
84  q1, q2 = most_connected[0]
85 
86  # Find physical qubits that are connected
87  # Start with an arbitrary connected pair
88  for edge in self.topology:
89  p1, p2 = edge
90  break # Just take first edge
91 
92  # Initialize mapping
93  pi = np.arange(self.qbit_num)
94 
95  # Place most interacting qubits on connected physical qubits
96  pi[q1] = p1
97  pi[q2] = p2
98 
99  # Place other qubits using greedy approach
100  placed_logical = {q1, q2}
101  placed_physical = {p1, p2}
102 
103  # For each remaining logical qubit, find where to place it
104  remaining_logical = [
105  q for q in range(self.circuit_qbit_num) if q not in placed_logical
106  ]
107 
108  # Sort by how much they interact with already placed qubits
109  def interaction_score(q):
110  score = 0
111  for placed_q in placed_logical:
112  key = (min(q, placed_q), max(q, placed_q))
113  score += interaction_count.get(key, 0)
114  return score
115 
116  remaining_logical.sort(key=interaction_score, reverse=True)
117 
118  # Place them near their interacting partners
119  for logical_q in remaining_logical:
120  # Find best physical location
121  best_physical = None
122  best_score = float("inf")
123 
124  for physical_q in range(self.qbit_num):
125  if physical_q not in placed_physical:
126  # Calculate average distance to interacting qubits
127  total_dist = 0
128  count = 0
129  for other_q in placed_logical:
130  key = (min(logical_q, other_q), max(logical_q, other_q))
131  weight = interaction_count.get(key, 0)
132  if weight > 0:
133  other_physical = pi[other_q]
134  total_dist += self.D[physical_q][other_physical] * weight
135  count += weight
136 
137  if count > 0:
138  avg_dist = total_dist / count
139  else:
140  avg_dist = 0
141 
142  if avg_dist < best_score:
143  best_score = avg_dist
144  best_physical = physical_q
145 
146  if best_physical is not None:
147  pi[logical_q] = best_physical
148  placed_logical.add(logical_q)
149  placed_physical.add(best_physical)
150 
151  return pi
152 
154  """BFS distance computation - faster than Floyd-Warshall."""
155  D = np.ones((self.qbit_num, self.qbit_num)) * np.inf
156 
157  # Build adjacency list
158  adj = defaultdict(list)
159  for u, v in self.topology:
160  adj[u].append(v)
161  adj[v].append(u)
162 
163  # BFS from each vertex
164  for start in range(self.qbit_num):
165  D[start][start] = 0
166  queue = deque([(start, 0)])
167  visited = {start}
168 
169  while queue:
170  node, dist = queue.popleft()
171  for neighbor in adj[node]:
172  if neighbor not in visited:
173  visited.add(neighbor)
174  D[start][neighbor] = dist + 1
175  queue.append((neighbor, dist + 1))
176 
177  return D
178 
180  """Generate possible connections - using sets for O(1) lookup."""
181  possible_connections_control = [set() for _ in range(self.qbit_num)]
182  neighbours = [set() for _ in range(self.qbit_num)]
183 
184  for qbit1, qbit2 in self.topology:
185  possible_connections_control[qbit2].add(qbit1)
186  neighbours[qbit1].add(qbit2)
187  neighbours[qbit2].add(qbit1)
188 
189  return possible_connections_control, neighbours
190 
191  def is_gate_possible(self, pi, q_target, q_control):
192  """Check if gate is possible (unchanged from original)."""
193  if q_control == -1:
194  return True, 0
195  else:
196  Q_control = pi[q_control]
197  Q_target = pi[q_target]
198  if Q_target in self.possible_connections_control[Q_control]:
199  return True, 0
200  elif Q_control in self.possible_connections_control[Q_target]:
201  return True, 1
202  return False, 0
203 
204  def H_basic(self, pi, q1, q2):
205  """Basic cost function (unchanged)."""
206  if q2 == -1:
207  return 0
208  return self.D[pi[q1]][pi[q2]]
209 
210  def update_pi(self, pi, SWAP):
211  """Update mapping after swap."""
212  q1, q2 = SWAP
213  pi[q1], pi[q2] = pi[q2], pi[q1]
214 
215  @staticmethod
216  def get_inverse_pi(pi):
217  """Get inverse mapping - optimized."""
218  inverse_pi = np.empty(len(pi), dtype=int)
219  inverse_pi[pi] = np.arange(len(pi))
220  return inverse_pi
221 
222  def obtain_SWAPS(self, F, pi, DAG):
223  """Get candidate SWAPs - optimized with sets."""
224  swaps = set()
225  inverse_pi = self.get_inverse_pi(pi)
226  involved_qbits = set()
227 
228  for gate_idx in F:
229  gate = DAG[gate_idx][0]
230  q_control = gate.get_Control_Qbit()
231  q_target = gate.get_Target_Qbit()
232  if q_control == -1:
233  continue
234  involved_qbits.add(q_control)
235  involved_qbits.add(q_target)
236 
237  for qbit in involved_qbits:
238  Q = pi[qbit]
239  for Q_cand in self.neighbours[Q]:
240  q_cand = inverse_pi[Q_cand]
241  swap = (min(qbit, int(q_cand)), max(qbit, int(q_cand)))
242  swaps.add(swap)
243 
244  return list(swaps)
245 
246  def generate_E(self, F, DAG, IDAG, resolved_gates):
247  """Generate extended set - simplified version."""
248  E = []
249  E_set = set()
250 
251  for front_gate in F:
252  if len(E) >= self.max_E_size:
253  break
254 
255  involved_qbits = [
256  DAG[front_gate][0].get_Target_Qbit(),
257  DAG[front_gate][0].get_Control_Qbit(),
258  ]
259  lookahead_count = 0
260  children = list(DAG[front_gate][1])
261 
262  while lookahead_count < self.max_lookahead and children:
263  for gate_idx in children[:]: # Copy to avoid modification issues
264  gate = DAG[gate_idx][0]
265 
266  if gate.get_Control_Qbit() == -1:
267  children.extend(DAG[gate_idx][1])
268  else:
269  gate_depth = self.calculate_gate_depth(
270  gate_idx, IDAG, resolved_gates
271  )
272  if (
273  len(E) < self.max_E_size
274  and (
275  gate.get_Target_Qbit() in involved_qbits
276  or gate.get_Control_Qbit() in involved_qbits
277  )
278  and gate_idx not in E_set
279  and gate_depth < 6
280  ):
281  E.append(gate_idx)
282  E_set.add(gate_idx)
283  lookahead_count += 1
284  if lookahead_count < self.max_lookahead:
285  children.extend(DAG[gate_idx][1])
286 
287  children.remove(gate_idx)
288 
289  return E
290 
291  def calculate_gate_depth(self, gate_idx, IDAG, resolved_gates):
292  """Calculate gate depth - iterative to avoid stack issues."""
293  depth = 1
294  visited = set()
295  stack = list(IDAG[gate_idx][1])
296 
297  while stack:
298  parent_idx = stack.pop()
299  if parent_idx in visited:
300  continue
301  visited.add(parent_idx)
302 
303  if not resolved_gates[parent_idx]:
304  parent_gate = IDAG[parent_idx][0]
305  if parent_gate.get_Control_Qbit() != -1:
306  depth += 1
307  stack.extend(IDAG[parent_idx][1])
308 
309  return depth
310 
311  def check_dependencies(self, gate_idx, IDAG, resolved_gates):
312  """Check dependencies - iterative."""
313  visited = set()
314  stack = list(IDAG[gate_idx][1])
315 
316  while stack:
317  parent_idx = stack.pop()
318  if parent_idx in visited:
319  continue
320  visited.add(parent_idx)
321 
322  if not resolved_gates[parent_idx]:
323  return False
324  stack.extend(IDAG[parent_idx][1])
325 
326  return True
327 
328  def score_swap_improved(self, swap, F, E, pi, DAG, IDAG, resolved_gates):
329  """
330  Improved scoring that rewards immediately executable gates.
331  """
332  pi_temp = pi.copy()
333  self.update_pi(pi_temp, swap)
334 
335  score = 0
336  gates_resolved = 0
337 
338  # Score gates in F with bonus for resolution
339  for gate_idx in F:
340  gate = DAG[gate_idx][0]
341  q_target, q_control = gate.get_Target_Qbit(), gate.get_Control_Qbit()
342 
343  if q_control != -1:
344  dist = self.H_basic(pi_temp, q_target, q_control)
345  if dist == 1: # This swap makes the gate executable
346  gates_resolved += 1
347  score -= 5 # Bonus for resolving gates
348  else:
349  score += dist
350 
351  if len(F) > 0:
352  score /= len(F)
353 
354  # Score extended set
355  if len(E) != 0:
356  score_temp = 0
357  for gate_idx in E:
358  gate = DAG[gate_idx][0]
359  q_target, q_control = gate.get_Target_Qbit(), gate.get_Control_Qbit()
360  depth = self.calculate_gate_depth(gate_idx, IDAG, resolved_gates)
361  score_temp += self.H_basic(pi_temp, q_target, q_control) * (
362  self.alpha**depth
363  )
364  score_temp *= self.W / len(E)
365  score += score_temp
366 
367  # Penalty if no gates are resolved
368  if gates_resolved == 0:
369  score += 1
370 
371  return score
372 
373  def Heuristic_search(self, F, pi, DAG, IDAG):
374  """Main heuristic search - with improved scoring."""
375  swap_count = 0
376  resolved_gates = [False] * len(DAG)
377  flags = [0] * len(DAG)
378  swap_list = []
379  gate_order = []
380  E = self.generate_E(F, DAG, IDAG, resolved_gates)
381  swap_prev = None
382 
383  while len(F) != 0:
384  execute_gate_list = []
385 
386  # Check which gates can be executed
387  for gate_idx in F:
388  gate = DAG[gate_idx][0]
389  possible, flag = self.is_gate_possible(
390  pi, gate.get_Target_Qbit(), gate.get_Control_Qbit()
391  )
392  if possible:
393  execute_gate_list.append(gate_idx)
394  resolved_gates[gate_idx] = True
395  flags[gate_idx] = flag
396 
397  if len(execute_gate_list) != 0:
398  # Execute gates
399  for gate_idx in execute_gate_list:
400  F.remove(gate_idx)
401  gate_order.append(gate_idx)
402  successors = DAG[gate_idx][1]
403  for new_gate_idx in successors:
404  resolved = self.check_dependencies(
405  new_gate_idx, IDAG, resolved_gates
406  )
407  if resolved and new_gate_idx not in F:
408  F.append(new_gate_idx)
409 
410  E = self.generate_E(F, DAG, IDAG, resolved_gates)
411  continue
412  else:
413  # Need to find a SWAP
414  scores = []
415  swaps = self.obtain_SWAPS(F, pi, DAG)
416 
417  if len(swaps) != 0:
418  for SWAP in swaps:
419  if SWAP == swap_prev:
420  scores.append(np.inf)
421  continue
422 
423  # Use improved scoring
424  score = self.score_swap_improved(
425  SWAP, F, E, pi, DAG, IDAG, resolved_gates
426  )
427  scores.append(score)
428  if self.stochastic_selection:
429  min_swap = self.select_swap_stochastic(swaps, scores)
430  else:
431  min_idx = np.argmin(scores)
432  min_swap = swaps[min_idx]
433  swap_prev = min_swap
434  gate_order.append(min_swap)
435  self.update_pi(pi, min_swap)
436  swap_count += 1
437  else:
438  print("ERROR: circuit cannot be mapped please check topology")
439  break
440 
441  return gate_order, flags, swap_count
442 
443  # Keep all the original helper methods unchanged
444  def get_reverse_circuit(self, circuit):
445  gates = circuit.get_Gates()
446  reverse_circuit = Circuit(self.circuit_qbit_num)
447  for idx in range(len(gates)):
448  gate_idx = len(gates) - 1 - idx
449  gate = gates[gate_idx]
450  reverse_circuit.add_Gate(gate)
451  return reverse_circuit
452 
453  def generate_DAG(self, circuit):
454  DAG = []
455  gates = circuit.get_Gates()
456  for gate in gates:
457  DAG.append([gate, circuit.get_Children(gate)])
458  return DAG
459 
460  def generate_inverse_DAG(self, circuit):
461  inverse_DAG = []
462  gates = circuit.get_Gates()
463  for gate in gates:
464  inverse_DAG.append([gate, circuit.get_Parents(gate)])
465  return inverse_DAG
466 
467  def get_initial_layer(self, circuit):
468  initial_layer = []
469  gates = circuit.get_Gates()
470  for gate_idx in range(len(gates)):
471  gate = gates[gate_idx]
472  if len(circuit.get_Parents(gate)) == 0:
473  initial_layer.append(gate_idx)
474  return initial_layer
475 
476  def get_mapped_circuit(self, circuit, init_pi, gate_order, flags, parameters):
477  circuit_mapped = Circuit(self.qbit_num)
478  gates = circuit.get_Gates()
479  parameters_new = np.array([])
480 
481  for gate_idx in gate_order:
482  if isinstance(gate_idx, int):
483  gate = gates[gate_idx]
484  if gate.get_Parameter_Num() != 0:
485  start_idx = gate.get_Parameter_Start_Index()
486  params = parameters[
487  start_idx : start_idx + gate.get_Parameter_Num()
488  ]
489  parameters_new = np.append(parameters_new, params)
490 
491  q_target, q_control = gate.get_Target_Qbit(), gate.get_Control_Qbit()
492  Q_target = init_pi[q_target]
493  Q_control = init_pi[q_control] if q_control != -1 else -1
494 
495  gate.set_Target_Qbit(Q_target)
496  if q_control != -1:
497  gate.set_Control_Qbit(Q_control)
498 
499  if flags[gate_idx] == 1:
500  gate.set_Target_Qbit(Q_control)
501  gate.set_Control_Qbit(Q_target)
502  if isinstance(gate, CNOT):
503  circuit_mapped.add_H(Q_target)
504  circuit_mapped.add_H(Q_control)
505 
506  circuit_mapped.add_Gate(gate)
507 
508  if flags[gate_idx] == 1 and isinstance(gate, CNOT):
509  circuit_mapped.add_H(Q_target)
510  circuit_mapped.add_H(Q_control)
511  else:
512  q1, q2 = gate_idx[0], gate_idx[1]
513  Q1, Q2 = init_pi[q1], init_pi[q2]
514  circuit_mapped.add_CNOT(Q1, Q2)
515  circuit_mapped.add_CNOT(Q2, Q1)
516  circuit_mapped.add_CNOT(Q1, Q2)
517  self.update_pi(init_pi, [q1, q2])
518 
519  return circuit_mapped, parameters_new
520 
521  def select_swap_stochastic(self, swaps, scores):
522  """Stochastic swap selection."""
523  scores_array = np.array(scores)
524  valid_mask = ~np.isinf(scores_array)
525 
526  if not np.any(valid_mask):
527  return swaps[0]
528 
529  valid_scores = scores_array[valid_mask]
530  min_score = np.min(valid_scores)
531 
532  if self.stochastic_selection and min_score > 0:
533  threshold = min_score * (1 + self.score_tolerance)
534  close_indices = np.where((scores_array <= threshold) & valid_mask)[0]
535 
536  if len(close_indices) > 1:
537  selected_idx = self.rng.choice(close_indices)
538  return swaps[selected_idx]
539  else:
540  return swaps[close_indices[0]]
541  else:
542  min_idx = np.argmin(scores_array)
543  return swaps[min_idx]
544 
545  def map_circuit(self, parameters=np.array([]), iter_num=1):
546  """Main entry point - unchanged interface."""
547  init_pi = self.pi.copy()
548  circuit = self.initial_circuit
549  reverse_circuit = self.get_reverse_circuit(circuit)
550  DAG = self.generate_DAG(circuit)
551  IDAG = self.generate_inverse_DAG(circuit)
552  first_layer = self.get_initial_layer(circuit)
553  DAG_reverse = self.generate_DAG(reverse_circuit)
554  IDAG_reverse = self.generate_inverse_DAG(reverse_circuit)
555  first_layer_reverse = self.get_initial_layer(reverse_circuit)
556 
557  for idx in range(iter_num):
558  self.Heuristic_search(first_layer.copy(), init_pi, DAG, IDAG)
559  self.Heuristic_search(
560  first_layer_reverse.copy(), init_pi, DAG_reverse, IDAG_reverse
561  )
562 
563  final_pi = init_pi.copy()
564  gate_order, flags, swap_count = self.Heuristic_search(
565  first_layer.copy(), final_pi, DAG, IDAG
566  )
567  circuit_mapped, parameters_new = self.get_mapped_circuit(
568  circuit, init_pi.copy(), gate_order, flags, parameters
569  )
570  return circuit_mapped, parameters_new, init_pi, final_pi, swap_count
def get_mapped_circuit(self, circuit, init_pi, gate_order, flags, parameters)
Definition: qgd_SABRE.py:476
def map_circuit(self, parameters=np.array([]), iter_num=1)
Definition: qgd_SABRE.py:545
def obtain_SWAPS(self, F, pi, DAG)
Definition: qgd_SABRE.py:222
def generate_DAG(self, circuit)
Definition: qgd_SABRE.py:453
def calculate_gate_depth(self, gate_idx, IDAG, resolved_gates)
Definition: qgd_SABRE.py:291
def get_inverse_pi(pi)
Definition: qgd_SABRE.py:216
def __init__(self, quantum_circuit, topology, max_lookahead=4, max_E_size=20, W=0.5, alpha=0.9, initial_layout="smart", score_tolerance=0.01, stochastic_selection=False, random_seed=None)
Definition: qgd_SABRE.py:25
def Heuristic_search(self, F, pi, DAG, IDAG)
Definition: qgd_SABRE.py:373
def is_gate_possible(self, pi, q_target, q_control)
Definition: qgd_SABRE.py:191
def compute_distances_bfs(self)
Definition: qgd_SABRE.py:153
def select_swap_stochastic(self, swaps, scores)
Definition: qgd_SABRE.py:521
def check_dependencies(self, gate_idx, IDAG, resolved_gates)
Definition: qgd_SABRE.py:311
def generate_possible_connections(self)
Definition: qgd_SABRE.py:179
def H_basic(self, pi, q1, q2)
Definition: qgd_SABRE.py:204
def score_swap_improved(self, swap, F, E, pi, DAG, IDAG, resolved_gates)
Definition: qgd_SABRE.py:328
def get_reverse_circuit(self, circuit)
Definition: qgd_SABRE.py:444
def _compute_smart_initial_layout(self, circuit)
Definition: qgd_SABRE.py:61
def generate_E(self, F, DAG, IDAG, resolved_gates)
Definition: qgd_SABRE.py:246
def get_initial_layer(self, circuit)
Definition: qgd_SABRE.py:467
def update_pi(self, pi, SWAP)
Definition: qgd_SABRE.py:210
def generate_inverse_DAG(self, circuit)
Definition: qgd_SABRE.py:460