Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
tools.py
Go to the documentation of this file.
1 from typing import Set, List, Tuple, Dict, Any, Set
2 import numpy as np
3 
4 from squander import utils
5 from squander.gates.gates_Wrapper import Gate
6 from squander.gates.qgd_Circuit import qgd_Circuit as Circuit
7 
8 import asyncio
9 from qiskit.transpiler import PassManager
10 from qiskit.transpiler.passes import CollectMultiQBlocks
11 
12 
13 def get_qubits(gate: Gate) -> Set[int]:
14  """
15  Get qubit indices used by a gate
16  Args:
17 
18  gate: SQUANDER gate
19  Returns:
20 
21  Set of qubit indices
22  """
23  return set(gate.get_Involved_Qbits())
24 
25 
26 def get_float_ops(num_qubit, gate_qubits, control_qubits, is_pure=False, io_penalty=48):
27  """
28  Compute the number of floating-point operations (FLOPs) required
29  for simulating a quantum gate acting on a set of qubits.
30 
31  Args:
32  num_qubit (int): Total number of qubits in the system.
33  gate_qubits (int): Number of qubits the gate acts on (including controls).
34  control_qubits (int): Number of control qubits for the gate.
35  is_pure (bool, optional): Whether the gate is a "pure" controlled gate
36  (i.e., all controlled gates share the same target qubit). Defaults to False.
37 
38  Returns:
39  int: Estimated number of floating-point operations required.
40  """
41  g_size = 2 ** (gate_qubits - control_qubits)
42  # (a + bi) * (c + di) = (ac - bd) + (ad + bc)i => 6 ops for 4m2a
43  return 2 ** (num_qubit - (control_qubits if is_pure else 0)) * (
44  g_size * (4 + 2) + 2 * (g_size - 1) + io_penalty
45  )
46 
47 
48 def parts_to_float_ops(num_qubit, gate_to_qubit, gate_to_tqubit, allparts):
49  """
50  Compute FLOPs for each partition of gates in a quantum circuit.
51 
52  Args:
53  num_qubit (int): Total number of qubits in the system.
54  gate_to_qubit (dict): Mapping from gate ID to the set of qubits
55  it acts on.
56  gate_to_tqubit (dict or None): Mapping from gate ID to its target qubit
57  (used for distinguishing control vs. target qubits). If None,
58  control qubits are assumed to be 0.
59  allparts (list[list]): Partitioning of gates, where each part is a
60  collection (e.g., list or set) of gate IDs.
61 
62  Returns:
63  list[int]: FLOP counts for each partition in `allparts`.
64  """
65  weights = []
66  for part in allparts:
67  qubits = set.union(*(gate_to_qubit[x] for x in part))
68  if gate_to_tqubit is not None:
69  tqubits = {gate_to_tqubit[x] for x in part}
70  is_pure = (
71  len({frozenset(gate_to_qubit[x] - {gate_to_tqubit[x]}) for x in part})
72  == 1
73  )
74  weights.append(
76  num_qubit, len(qubits), len(qubits) - len(tqubits), is_pure
77  )
78  )
79  else:
80  weights.append(get_float_ops(num_qubit, len(qubits), 0, False))
81  return weights
82 
83 
84 def total_float_ops(
85  num_qubit, max_qubits_per_partition, gate_to_qubit, gate_to_tqubit, allparts
86 ):
87  """
88  Compute the total FLOPs across all partitions of a quantum circuit,
89  scaled by the number of qubits outside the maximum partition.
90 
91  Args:
92  num_qubit (int): Total number of qubits in the system.
93  max_qubits_per_partition (int): Maximum number of qubits any partition
94  can act on.
95  gate_to_qubit (dict): Mapping from gate ID to the set of qubits
96  it acts on.
97  gate_to_tqubit (dict or None): Mapping from gate ID to its target qubit.
98  If None, control qubits are assumed to be 0.
99  allparts (list[list]): Partitioning of gates, where each part is a
100  collection of gate IDs.
101 
102  Returns:
103  int: Total number of floating-point operations across all partitions.
104  """
105  weights = parts_to_float_ops(
106  max_qubits_per_partition, gate_to_qubit, gate_to_tqubit, allparts
107  )
108  return 2 ** (num_qubit - max_qubits_per_partition) * sum(weights)
109 
110 
112  params: np.ndarray, param_order: List[Tuple[int, int, int]]
113 ) -> np.ndarray:
114  """
115  Call to reorder circuit parameters based on partitioned execution order
116 
117  Args:
118 
119  params ( np.ndarray ) Original parameter array
120 
121  param_order (List[int] ) Tuples specifying new parameter positions: source_idx, dest_idx, param_count
122 
123  Return:
124 
125  Returns with the reordered Reordered parameter array
126  """
127  reordered = np.empty_like(params)
128 
129  for s_idx, n_idx, n_params in param_order:
130  reordered[n_idx : n_idx + n_params] = params[s_idx : s_idx + n_params]
131 
132  return reordered
133 
134 
135 def build_dependency(
136  c: Circuit,
137 ) -> Tuple[
138  Dict[int, Gate],
139  Dict[int, Set[int]],
140  Dict[int, Set[int]],
141  Dict[int, Set[int]],
142  Set[int],
143 ]:
144  """
145  Build dependency graphs for circuit gates
146  Args:
147 
148  c: SQUANDER Circuit.
149  Returns:
150 
151  Gate dict, forward graph, reverse graph, qubit mapping, start set.
152  """
153  gate_dict = {i: gate for i, gate in enumerate(c.get_Gates())}
154  gate_to_qubit = {i: get_qubits(g) for i, g in gate_dict.items()}
155  g, rg = {i: set() for i in gate_dict}, {i: set() for i in gate_dict}
156 
157  for gate in gate_dict:
158  for child in c.get_Children(gate_dict[gate]):
159  g[gate].add(child)
160  rg[child].add(gate)
161 
162  S = {m for m in rg if len(rg[m]) == 0}
163 
164  return gate_dict, g, rg, gate_to_qubit, S
165 
166 
167 def qiskit_to_squander_name(qiskit_name):
168  """
169  Convert Qiskit gate name to SQUANDER name
170  Args:
171 
172  qiskit_name: Qiskit gate name
173  Returns:
174 
175  SQUANDER gate name
176  """
177  name = qiskit_name.upper()
178  if name == "CX":
179  return "CNOT"
180  elif name == "U":
181  return "U3"
182  elif name == "TDG":
183  return "Tdg"
184  else:
185  return name
186 
187 
188 def gate_desc_to_gate_index(circ, preparts, qubit_groups_only=False):
189  """
190  Map gate descriptions to indices for partitioning
191  Args:
192 
193  circ: SQUANDER Circuit
194 
195  preparts: Partition descriptions
196  Returns:
197 
198  Partitioned gate indices
199  """
200  gate_dict, g, rg, gate_to_qubit, S = build_dependency(circ)
201  L = []
202 
203  curr_partition = set()
204  curr_idx = 0
205  total = 0
206  parts = [[]]
207 
208  while S:
209  if qubit_groups_only:
210  n = next(
211  iter(x for x in S if gate_to_qubit[x] <= preparts[len(parts) - 1]), None
212  )
213  else:
214  Scomp = {
215  (frozenset(gate_to_qubit[x]), gate_dict[x].get_Name()): x for x in S
216  }
217  rev_Scomp = {y: x for x, y in Scomp.items()}
218  n = next(iter(Scomp.keys() & preparts[len(parts) - 1]), None)
219  if n is not None:
220  n = Scomp[n]
221 
222  while n is None:
223  total += len(parts[-1])
224  curr_partition = set()
225  parts.append([])
226  if qubit_groups_only:
227  n = next(
228  iter(x for x in S if gate_to_qubit[x] <= preparts[len(parts) - 1]),
229  None,
230  )
231  else:
232  n = next(iter(Scomp.keys() & preparts[len(parts) - 1]), None)
233  if n is not None:
234  n = Scomp[n]
235 
236  if not qubit_groups_only:
237  preparts[len(parts) - 1].remove(rev_Scomp[n])
238  parts[-1].append(n)
239  curr_partition |= gate_to_qubit[n]
240  curr_idx += gate_dict[n].get_Parameter_Num()
241 
242  # Update dependencies
243  L.append(n)
244  S.remove(n)
245  assert len(rg[n]) == 0
246  for child in set(g[n]):
247  g[n].remove(child)
248  rg[child].remove(n)
249  if not rg[child]:
250  S.add(child)
251 
252  # Add the last partition
253  total += len(parts[-1])
254  assert total == len(gate_dict)
255  # print(parts)
256  return parts
257 
258 
259 def get_qiskit_partitions(filename, max_partition_size):
260  """
261  Partition circuit using Qiskit multi-qubit blocks
262  Args:
263 
264  filename: QASM file path
265 
266  max_partition_size: Max qubits per partition
267  Returns:
268 
269  Parameters, partitioned circuit, parameter order (source_idx, dest_idx, param_count), partitions
270  """
271  circ, parameters, qc = utils.qasm_to_squander_circuit(filename, True)
272  pm = PassManager(
273  [
274  CollectMultiQBlocks(max_block_size=max_partition_size),
275  ]
276  )
277 
278  assert qc is not None
279  pm.run(qc)
280  blocks = pm.property_set["block_list"] # is not in topological order
281 
282  L = [
283  [
284  (
285  frozenset(qc.find_bit(x)[0] for x in dagop.qargs),
286  qiskit_to_squander_name(dagop.name),
287  )
288  for dagop in block
289  ]
290  for block in blocks
291  ]
292  # L = [frozenset({qc.find_bit(x)[0] for dagop in block for x in dagop.qargs}) for block in blocks]
293  assert len(qc.data) == sum(map(len, blocks))
294  from squander.partitioning.kahn import kahn_partition_preparts
295 
296  partitioned_circ, param_order, parts = kahn_partition_preparts(
297  circ, max_partition_size, gate_desc_to_gate_index(circ, L)
298  )
299  return parameters, partitioned_circ, param_order, parts
300 
301 
302 def get_qiskit_fusion_partitions(filename, max_partition_size):
303  """
304  Generate circuit partitions from a QASM file using Qiskit's fusion
305  metadata and Squander's partitioning utilities.
306 
307  This function:
308  1. Parses the QASM file into a Squander circuit.
309  2. Runs the circuit on Qiskit's AerSimulator with gate fusion enabled.
310  3. Extracts the fusion metadata (grouped qubit operations).
311  4. Uses Squander's Kahn-based partitioning to produce circuit partitions.
312 
313  Args:
314  filename (str): Path to the QASM file to be parsed.
315  max_partition_size (int): Maximum number of qubits allowed in each
316  fusion partition (i.e., `fusion_max_qubit` for Qiskit).
317 
318  Returns:
319  tuple:
320  parameters (list): Parameter list extracted from the Squander circuit.
321  partitioned_circ (object): Partitioned Squander circuit object.
322  param_order (list): Order of parameters corresponding to the partitioned circuit.
323  parts (list[list]): Partitioning of the circuit into gate groups,
324  represented as lists of gate indices.
325  """
326  circ, parameters, qc = utils.qasm_to_squander_circuit(filename, True)
327 
328  assert qc is not None
329 
330  qc.save_statevector()
331  from qiskit import transpile
332  from qiskit_aer import AerSimulator
333 
334  backend = AerSimulator(
335  method="statevector",
336  fusion_enable=True,
337  fusion_verbose=True,
338  fusion_max_qubit=max_partition_size,
339  fusion_threshold=1,
340  shots=1,
341  )
342  tcirc = transpile(qc, backend=backend, optimization_level=0)
343  job = backend.run(tcirc, shots=1)
344  res = job.result()
345  meta = res.results[0].metadata.get("fusion", {})
346  qubits = [
347  frozenset(x["qubits"]) for x in meta["output_ops"][:-1]
348  ] # could try to determine control qubits by looking
349  from squander.partitioning.kahn import kahn_partition_preparts
350 
351  partitioned_circ, param_order, parts = kahn_partition_preparts(
352  circ,
353  max_partition_size,
354  gate_desc_to_gate_index(circ, qubits, qubit_groups_only=True),
355  )
356  return parameters, partitioned_circ, param_order, parts
357 
358 
359 def get_bqskit_partitions(filename, max_partition_size, partitioner):
360  """
361  Partition circuit using BQSKit partitioners
362  Args:
363 
364  filename: QASM file path
365 
366  max_partition_size: Max qubits per partition
367 
368  partitioner: BQSKit Partitioning strategy
369  Returns:
370 
371  Parameters, partitioned circuit, parameter order (source_idx, dest_idx, param_count), partitions
372  """
373  try:
374  from bqskit import Circuit
375  from bqskit.passes.partitioning.quick import QuickPartitioner
376  from bqskit.passes.partitioning.greedy import GreedyPartitioner # too slow
377  from bqskit.passes.partitioning.cluster import (
378  ClusteringPartitioner,
379  ) # does a bad job at minimizing partitions
380  from bqskit.passes.partitioning.scan import ScanPartitioner
381 
382  import bqskit.ir
383 
384  bqs_to_squander = {
385  bqskit.ir.gates.constant.cx.CNOTGate: "CNOT",
386  bqskit.ir.gates.constant.t.TGate: "T",
387  bqskit.ir.gates.constant.h.HGate: "H",
388  bqskit.ir.gates.constant.tdg.TdgGate: "Tdg",
389  bqskit.ir.gates.constant.x.XGate: "X",
390  bqskit.ir.gates.constant.y.YGate: "Y",
391  bqskit.ir.gates.constant.z.ZGate: "Z",
392  bqskit.ir.gates.constant.s.SGate: "S",
393  bqskit.ir.gates.constant.sdg.SdgGate: "Sdg",
394  bqskit.ir.gates.constant.r.RGate: "R",
395  bqskit.ir.gates.constant.sx.SXGate: "SX",
396  bqskit.ir.gates.constant.ch.CHGate: "CH",
397  bqskit.ir.gates.constant.cz.CZGate: "CZ",
398  bqskit.ir.gates.parameterized.cry.CRYGate: "CRY",
399  bqskit.ir.gates.parameterized.u3.U3Gate: "U3",
400  bqskit.ir.gates.parameterized.rz.RZGate: "RZ",
401  bqskit.ir.gates.parameterized.ry.RYGate: "RY",
402  bqskit.ir.gates.parameterized.rx.RXGate: "RX",
403  }
404 
405  if partitioner == "Quick":
406  partitioner = QuickPartitioner(block_size=max_partition_size)
407  elif partitioner == "Scan":
408  partitioner = ScanPartitioner(block_size=max_partition_size)
409  elif partitioner == "Greedy":
410  partitioner = GreedyPartitioner(block_size=max_partition_size)
411  elif partitioner == "Cluster":
412  partitioner = ClusteringPartitioner(block_size=max_partition_size)
413  bq_circuit = Circuit.from_file(filename)
414  asyncio.run(partitioner.run(bq_circuit, None))
415  # Count number of blocks (partitions)
416  circ, parameters, qc = utils.qasm_to_squander_circuit(filename, True)
417  L = [
418  [
419  (
420  frozenset(curloc.location[x] for x in op.location),
421  bqs_to_squander[type(op.gate)],
422  )
423  for op in curloc.gate._circuit.operations()
424  ]
425  for curloc in bq_circuit.operations()
426  ]
427  from squander.partitioning.kahn import kahn_partition_preparts
428 
429  partitioned_circ, param_order, parts = kahn_partition_preparts(
430  circ, max_partition_size, gate_desc_to_gate_index(circ, L)
431  )
432  return parameters, partitioned_circ, param_order, parts
433 
434  except ImportError as e:
435  raise ImportError(
436  f"bqskit is not installed: bqskit is required for bqskit-{partitioner} partitioning."
437  ) from e
def get_qiskit_fusion_partitions(filename, max_partition_size)
Definition: tools.py:302
def kahn_partition_preparts(c, max_qubit, preparts)
Definition: kahn.py:80
def get_qubits
Definition: tools.py:13
def get_float_ops(num_qubit, gate_qubits, control_qubits, is_pure=False, io_penalty=48)
Definition: tools.py:26
def get_qiskit_partitions(filename, max_partition_size)
Definition: tools.py:259
def get_Parameter_Num(self)
Call to get the number of free parameters in the gate structure used for the decomposition.
def gate_desc_to_gate_index(circ, preparts, qubit_groups_only=False)
Definition: tools.py:188
def qiskit_to_squander_name(qiskit_name)
Definition: tools.py:167
def total_float_ops(num_qubit, max_qubits_per_partition, gate_to_qubit, gate_to_tqubit, allparts)
Definition: tools.py:86
def get_bqskit_partitions(filename, max_partition_size, partitioner)
Definition: tools.py:359
def translate_param_order
Definition: tools.py:112
def build_dependency
Definition: tools.py:136
def parts_to_float_ops(num_qubit, gate_to_qubit, gate_to_tqubit, allparts)
Definition: tools.py:48