Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
tdag.py
Go to the documentation of this file.
1 # TDAG (Tree-based Directed Acyclic Graph)
2 # https://www.osti.gov/servlets/purl/1985363
3 # https://dl.acm.org/doi/10.1145/3583781.3590234
4 # https://arxiv.org/pdf/2410.02901
5 from squander.gates.qgd_Circuit import qgd_Circuit as Circuit
6 from squander.partitioning.tools import build_dependency
7 
8 def _get_starting_gates(g, rg, gate_to_qubit, S):
9  """
10  Get starting gates for each qubit in the circuit
11 
12  Args:
13 
14  g: Forward dependency graph
15 
16  rg: Reverse dependency graph
17 
18  gate_to_qubit: Mapping of gates to qubits
19 
20  S: Set of initial gates
21 
22  Returns:
23 
24  Dictionary mapping qubits to their starting gates
25  """
26  g = { x: set(y) for x, y in g.items() }
27  rg = { x: set(y) for x, y in rg.items() }
28  S = set(S)
29  start_qubit = {}
30  while S:
31  n = S.pop()
32  assert not rg[n]
33  for m in set(g[n]):
34  g[n].remove(m)
35  rg[m].remove(n)
36  if not rg[m]:
37  S.add(m)
38  for qubit in gate_to_qubit[n]:
39  if not qubit in start_qubit: start_qubit[qubit] = {}
40  start_qubit[qubit][n] = None
41  return start_qubit
42 
43 def tdag_max_partitions(c, max_qubit, use_gtqcp=False):
44  """
45  Partition a circuit using TDAG algorithm
46 
47  Args:
48 
49  c: SQUANDER Circuit to partition
50 
51  max_qubit: Max qubits per partition
52 
53  use_gtqcp: Use GTQCP variant
54 
55  Returns:
56 
57  Partitioned circuit, parameter order (source_idx, dest_idx, param_count), partition assignments
58  """
59  gate_dict, g, rg, gate_to_qubit, S = build_dependency(c)
60  L = []
61  start_qubit = _get_starting_gates(g, rg, gate_to_qubit, S)
62  while g:
63  groups, deps = _enumerate_groups(g, rg, gate_to_qubit, start_qubit, max_qubit, use_gtqcp)
64  L.append(_remove_best_partition(groups, g, rg, gate_to_qubit, start_qubit, deps))
65  assert sum(len(x) for x in L) == len(gate_dict), (sum(len(x) for x in L), len(gate_dict))
66  from squander.partitioning.kahn import kahn_partition_preparts
67  return kahn_partition_preparts(c, max_qubit, preparts=L)
68 
69 
70 def _get_gate_dependencies(g, rg, gate_to_qubit, S, max_qubit):
71  """
72  Get gate dependencies for each gate within qubit constraints
73 
74  Args:
75 
76  g: Forward dependency graph
77 
78  rg: Reverse dependency graph
79 
80  gate_to_qubit: Mapping of gates to qubits
81 
82  S: Starting gates per qubit
83 
84  max_qubit: Max qubits per partition
85 
86  Returns:
87 
88  Dictionary of gate dependencies
89  """
90  deps, visited = {}, set()
91  level, next_level = set(), set()
92  for qubit in S:
93  level.add(next(iter(S[qubit])))
94  while level:
95  for curr_gate in level:
96  if not rg[curr_gate] <= visited: continue
97  deps[curr_gate] = set.union(gate_to_qubit[curr_gate], *(deps[gate] for gate in rg[curr_gate]))
98  if len(deps[curr_gate]) <= max_qubit:
99  next_level |= g[curr_gate]
100  visited.add(curr_gate)
101  else: del deps[curr_gate]
102  level, next_level = next_level, level
103  next_level.clear()
104  return deps
105 
106 
107 def _enumerate_groups(g, rg, gate_to_qubit, S, max_qubit, use_gtqcp):
108  """
109  Enumerate possible gate groups for partitioning
110 
111  Args:
112 
113  g: Forward dependency graph
114 
115  rg: Reverse dependency graph
116 
117  gate_to_qubit: Mapping of gates to qubits
118 
119  S: Starting gates per qubit
120 
121  max_qubit: Max qubits per partition
122 
123  use_gtqcp: Use GTQCP variant
124 
125  Returns:
126 
127  Set of gate groups and their dependencies
128  """
129  deps = _get_gate_dependencies(g, rg, gate_to_qubit, S, max_qubit)
130  result = set()
131  func = _enumerate if not use_gtqcp else _enumerate_gtqcp
132  for qubit in S:
133  result |= func(next(iter(S[qubit])), qubit, {qubit} if use_gtqcp else {frozenset({qubit})}, deps, g, gate_to_qubit, S, max_qubit)
134  return result, deps
135 
136 
137 def _enumerate(target_gate, target_qubit, input_groups, deps, g, gate_to_qubit, S, max_qubit):
138  """
139  Recursively enumerate gate groups for a target gate
140 
141  Args:
142 
143  target_gate: Gate to start enumeration
144 
145  target_qubit: Qubit index
146 
147  input_groups: Current qubit groups
148 
149  deps: Gate dependencies
150 
151  g: Forward dependency graph
152 
153  gate_to_qubit: Mapping of gates to qubits
154 
155  S: Starting gates per qubit
156 
157  max_qubit: Max qubits per partition
158 
159  Returns:
160 
161  Set of valid qubit groups
162  """
163  output, result = set(), set() # qubit groups
164  if not target_gate in deps: return result
165  for group in input_groups:
166  qubits = frozenset.union(group, deps[target_gate])
167  if len(qubits) <= max_qubit:
168  result.add(qubits)
169  if not deps[target_gate] <= group: # is not subset
170  output.add(qubits)
171 
172  while any(len(group) < max_qubit for group in output):
173  target_gate = next(iter(x for x in g[target_gate] if target_qubit in gate_to_qubit[x]), None)
174  if target_gate is None:
175  break
176  input_groups = output
177  next_qubit = next(iter(gate_to_qubit[target_gate] - {target_qubit}), None)
178  if next_qubit is None:
179  continue
180  output = _enumerate(target_gate, next_qubit, input_groups, deps, g, gate_to_qubit, S, max_qubit)
181  result |= output
182 
183  return result
184 
185 def _enumerate_gtqcp(target_gate, target_qubit, input_groups, deps, g, gate_to_qubit, S, max_qubit):
186  """
187  Recursively enumerate gate groups for a target gate using GTQCP variant.
188 
189  Args:
190 
191  target_gate: Gate to start enumeration
192 
193  target_qubit: Qubit index
194 
195  input_groups: Current qubit groups
196 
197  deps: Gate dependencies
198 
199  g: Forward dependency graph
200 
201  gate_to_qubit: Mapping of gates to qubits
202 
203  S: Starting gates per qubit
204 
205  max_qubit: Max qubits per partition
206 
207  Returns:
208 
209  Set of valid qubit groups
210  """
211  result = set()
212  if not target_gate in deps: return result
213  gate = target_gate
214  while True:
215  next_gate = next(iter(x for x in g[gate] if target_qubit in gate_to_qubit[x]), None)
216  if next_gate is None or next_gate not in deps or len(input_groups | deps[next_gate]) > max_qubit:
217  break
218  gate = next_gate
219  group = frozenset(input_groups) | frozenset(deps[gate])
220  if len(group) > max_qubit:
221  return result
222  if group not in result:
223  result.add(group)
224  if len(group) < max_qubit:
225  for qubit in group - input_groups:
226  if qubit not in S:
227  continue
228  gate = next(iter(S[qubit]))
229  _enumerate_gtqcp(gate, qubit, input_groups | frozenset({qubit}), deps, g, gate_to_qubit, S, max_qubit)
230  return result
231 
232 
233 def _remove_best_partition(qubit_results, g, rg, gate_to_qubit, start_qubit, deps):
234  """
235  Remove the best partition from the dependency graphs
236 
237  Args:
238 
239  qubit_results: Candidate qubit groups
240 
241  g: Forward dependency graph
242 
243  rg: Reverse dependency graph
244 
245  gate_to_qubit: Mapping of gates to qubits
246 
247  start_qubit: Starting gates per qubit
248 
249  deps: Gate dependencies
250 
251  Returns:
252 
253  List of gate indices in the best partition
254  """
255  gate_info = [[x for x, y in deps.items() if y <= result] for result in qubit_results]
256  best_part = max(gate_info, key=lambda x: len(x))
257  for gate in best_part:
258  for child in g[gate]:
259  rg[child].remove(gate)
260  for child in rg[gate]:
261  g[child].remove(gate)
262  for gate in best_part:
263  del g[gate]
264  del rg[gate]
265  for qubit in gate_to_qubit[gate]:
266  del start_qubit[qubit][gate]
267  if len(start_qubit[qubit]) == 0: del start_qubit[qubit]
268 
269  return best_part
270 
271 def _test_tdag_qasm(use_gtqcp=False):
272  """
273  Test TDAG partitioning on a QASM file
274 
275  Args:
276 
277  use_gtqcp: Use GTQCP variant
278  """
279  K = 3
280  # filename = "examples/partitioning/qasm_samples/heisenberg-16-20.qasm"
281  filename = "benchmarks/partitioning/test_circuit/9symml_195.qasm"
282  from squander import utils
283 
284  circ, parameters = utils.qasm_to_squander_circuit(filename)
285  partition = tdag_max_partitions(circ, K, use_gtqcp)
286 
287  print(partition[2])
288 
289 
291  """
292  Test TDAG partitioning on a simple two-qubit circuit
293  """
294  K = 4
295 
296  c = Circuit(2)
297  c.add_H(0)
298  c.add_H(1)
299  c.add_CNOT(0, 1)
300 
301  gate_dict, g, rg, gate_to_qubit, S = build_dependency(c)
302 
303  result = _get_gate_dependencies(g, rg, gate_to_qubit, S, K)
304 
305  # print("gate_dependencies: ", result)
306 
307  _enumerate_groups(g, gate_to_qubit, S, K, False)
308 
309  partition = tdag_max_partitions(c, K)
310 
311  # print(partition)
312 
314  """
315  Test GTQCP variant for TDAG partitioning
316  """
317  K = 5
318 
319  c = Circuit(6)
320  c.add_CNOT(0, 1)
321  c.add_CNOT(2, 3)
322  c.add_CNOT(3, 4)
323  c.add_CNOT(2, 3)
324  c.add_CNOT(4, 5)
325  c.add_CNOT(1, 2)
326 
327  expected = {0: {0, 1}, 1: {2, 3}, 2: {2, 3, 4}, 3: {2, 3, 4}, 4: {2, 3, 4, 5}, 5: {0, 1, 2, 3, 4}}
328 
329  expected_groups = {frozenset({2, 3, 4}), frozenset({0, 1, 2, 3, 4}), frozenset({0, 1}), frozenset({2, 3, 4, 5})} # we don't consider starting qubits in the middle of topological sort
330 
331  gate_dict, g, rg, gate_to_qubit, S = build_dependency(c)
332 
333  start_qubit = _get_starting_gates(g, rg, gate_to_qubit, S)
334 
335  result = _get_gate_dependencies(g, rg, gate_to_qubit, start_qubit, K)
336 
337  assert result == expected, (result, expected)
338 
339  result_groups, _ = _enumerate_groups(g, rg, gate_to_qubit, start_qubit, K, True)
340 
341  assert result_groups == expected_groups, (result_groups, expected_groups)
342 
343  partition = tdag_max_partitions(c, K)
344 
345  print(partition[2])
346 
347 
349  """
350  Test TDAG partitioning on a sample circuit
351  """
352  K = 4
353 
354  c = Circuit(8)
355  c.add_CNOT(3, 4)
356  c.add_CNOT(2, 3)
357  c.add_CNOT(4, 5)
358  c.add_CNOT(1, 2)
359  c.add_CNOT(5, 6)
360  c.add_CNOT(0, 3)
361  c.add_CNOT(4, 7)
362 
363 
364  expected = {0: {3, 4}, 1: {2, 3, 4}, 2: {3, 4, 5}, 3: {1, 2, 3, 4}, 4: {3, 4, 5, 6}, 5: {0, 2, 3, 4}, 6: {3, 4, 5, 7}}
365 
366  expected_groups = {frozenset({3, 4}), frozenset({3, 4, 5, 6}), frozenset({2, 3, 4}), frozenset({0, 2, 3, 4}), frozenset({1, 2, 3, 4}), frozenset({3, 4, 5}), frozenset({3, 4, 5, 7})}
367 
368  gate_dict, g, rg, gate_to_qubit, S = build_dependency(c)
369 
370  start_qubit = _get_starting_gates(g, rg, gate_to_qubit, S)
371 
372  result = _get_gate_dependencies(g, rg, gate_to_qubit, start_qubit, K)
373 
374  assert result == expected, (result, expected)
375 
376  result_groups, _ = _enumerate_groups(g, rg, gate_to_qubit, start_qubit, K, False)
377 
378  assert result_groups == expected_groups, (result_groups, expected_groups)
379 
380  partition = tdag_max_partitions(c, K)
381 
382  print(partition[2])
383 
384 
385 
386 if __name__ == "__main__":
387  _test_tdag()
388  _test_gtqcp()
389  _test_tdag_qasm(False)
390  _test_tdag_qasm(True)
def _test_tdag_qasm(use_gtqcp=False)
Definition: tdag.py:271
def kahn_partition_preparts(c, max_qubit, preparts)
Definition: kahn.py:80
def _test_gtqcp()
Definition: tdag.py:313
def _remove_best_partition(qubit_results, g, rg, gate_to_qubit, start_qubit, deps)
Definition: tdag.py:233
def _enumerate_gtqcp(target_gate, target_qubit, input_groups, deps, g, gate_to_qubit, S, max_qubit)
Definition: tdag.py:185
def _get_starting_gates(g, rg, gate_to_qubit, S)
Definition: tdag.py:8
def tdag_max_partitions(c, max_qubit, use_gtqcp=False)
Definition: tdag.py:43
def _test_tdag_single_qubit()
Definition: tdag.py:290
def _test_tdag()
Definition: tdag.py:348
def _get_gate_dependencies(g, rg, gate_to_qubit, S, max_qubit)
Definition: tdag.py:70
def build_dependency
Definition: tools.py:136
def _enumerate(target_gate, target_qubit, input_groups, deps, g, gate_to_qubit, S, max_qubit)
Definition: tdag.py:137
def _enumerate_groups(g, rg, gate_to_qubit, S, max_qubit, use_gtqcp)
Definition: tdag.py:107