Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
qgd_Wide_Circuit_Optimization.py
Go to the documentation of this file.
1 """
2 Wide-circuit optimization: partition large circuits into subcircuits, re-decompose
3 them, and optionally route or fuse results according to configuration.
4 """
5 
6 from squander.decomposition.qgd_N_Qubit_Decompositions_Wrapper import (
7  qgd_N_Qubit_Decomposition_adaptive as N_Qubit_Decomposition_adaptive,
8  qgd_N_Qubit_Decomposition_Tree_Search as N_Qubit_Decomposition_Tree_Search,
9  qgd_N_Qubit_Decomposition_Tabu_Search as N_Qubit_Decomposition_Tabu_Search,
10 )
11 from squander import N_Qubit_Decomposition_custom, N_Qubit_Decomposition
12 from squander.gates.qgd_Circuit import qgd_Circuit as Circuit
13 from squander.utils import CompareCircuits
14 
15 import numpy as np
16 from qiskit import QuantumCircuit
17 
18 from typing import List, Callable, Tuple, Optional, Set, Dict, Any, cast, Union
19 
20 import multiprocessing as mp
21 from multiprocessing import Process, Pool, parent_process
22 import os, contextlib, collections, time
23 
24 
25 from squander.partitioning.partition import PartitionCircuit
26 from squander.partitioning.tools import translate_param_order, build_dependency
27 from squander.synthesis.qgd_SABRE import qgd_SABRE as SABRE
28 
29 try:
30  from bqskit.compiler.basepass import BasePass as _BQSKitBasePass
31  from bqskit.passes.synthesis.synthesis import SynthesisPass as _BQSKitSynthesisPass
32 except Exception:
33  _BQSKitBasePass = object
34  _BQSKitSynthesisPass = object
35 
36 
37 _SQUANDER_BQSKIT_SYNTHESIS_CONFIG = None
38 
39 SQUANDER_FLOAT64_TOLERANCE = 1e-10
40 SQUANDER_FLOAT32_TOLERANCE = 1e-5
41 BQSKIT_FLOAT64_SYNTHESIS_VALIDATION_TOLERANCE = 1e-8
42 BQSKIT_FLOAT32_SYNTHESIS_VALIDATION_TOLERANCE = 1e-4
43 
44 
46  return bool(config.get("use_float", False))
47 
48 
50  return (
51  SQUANDER_FLOAT32_TOLERANCE
52  if _config_uses_float32(config)
53  else SQUANDER_FLOAT64_TOLERANCE
54  )
55 
56 
58  return (
59  BQSKIT_FLOAT32_SYNTHESIS_VALIDATION_TOLERANCE
60  if _config_uses_float32(config)
61  else BQSKIT_FLOAT64_SYNTHESIS_VALIDATION_TOLERANCE
62  )
63 
64 
66  return config.get(
67  "tolerance",
69  )
70 
71 
73  return config.get(
74  "bqskit_synthesis_validation_tolerance",
76  )
77 
78 
80  """Copy only plain data needed by BQSKit worker processes."""
81 
82  def copy_value(value):
83  if value is None or isinstance(value, (bool, int, float, str)):
84  return value
85  if isinstance(value, np.generic):
86  return value.item()
87  if isinstance(value, tuple):
88  copied = [copy_value(item) for item in value]
89  return tuple(item for item in copied if item is not _SKIP_CONFIG_VALUE)
90  if isinstance(value, list):
91  copied = [copy_value(item) for item in value]
92  return [item for item in copied if item is not _SKIP_CONFIG_VALUE]
93  if isinstance(value, dict):
94  copied = {}
95  for key, item in value.items():
96  copied_item = copy_value(item)
97  if copied_item is not _SKIP_CONFIG_VALUE:
98  copied[key] = copied_item
99  return copied
100  return _SKIP_CONFIG_VALUE
101 
102  copied_config = {}
103  for key, value in config.items():
104  copied_value = copy_value(value)
105  if copied_value is not _SKIP_CONFIG_VALUE:
106  copied_config[key] = copied_value
107  return copied_config
108 
109 
110 _SKIP_CONFIG_VALUE = object()
111 
112 
113 # ---------------------------------------------------------------------------
114 # Helper: insert a SWAP as 3 CNOTs so BQSKit's scoring function weights
115 # them honestly (3 two-qubit ops instead of 1). SEQPAM then avoids
116 # unnecessary SWAP insertions.
117 # ---------------------------------------------------------------------------
118 def _add_swap_as_cnots(circuit, a, b):
119  """Append CNOT(a,b); CNOT(b,a); CNOT(a,b) — equivalent to SWAP(a,b)."""
120  from bqskit.ir.gates import CNOTGate
121  circuit.append_gate(CNOTGate(), [a, b])
122  circuit.append_gate(CNOTGate(), [b, a])
123  circuit.append_gate(CNOTGate(), [a, b])
124 
125 
126 # ---------------------------------------------------------------------------
127 # Module-level EAPP monkey-patch for SWAP fallback.
128 # BQSKit's Compiler starts a runtime server via Popen([sys.executable, ...]),
129 # a fresh Python process. Class-level monkey-patches applied in the parent
130 # are invisible there. We use an environment variable that the Popen child
131 # inherits; when this module is imported inside a worker, the env var triggers
132 # the patch.
133 # ---------------------------------------------------------------------------
134 def _append_topology_safe(new_c, op, topo_edges, width):
135  """Append *op* to *new_c*, using SWAP bridges for edges not in *topo_edges*.
136 
137  For gates with ≥3 qubits, decomposes via :func:`squander.utils.circuit_to_CNOT_basis`
138  and recurses on each resulting gate.
139  """
140 
141  loc = list(op.location)
142  gate = op.gate
143  params = list(op.params) if op.params else None
144 
145  if gate.num_qudits == 1:
146  if params:
147  new_c.append_gate(gate, loc, params)
148  else:
149  new_c.append_gate(gate, loc)
150  return
151 
152  if gate.num_qudits == 2:
153  u, v = loc[0], loc[1]
154  if (u, v) in topo_edges:
155  if params:
156  new_c.append_gate(gate, [u, v], params)
157  else:
158  new_c.append_gate(gate, [u, v])
159  return
160  # Edge not in topology — find shortest SWAP path u↔v via BFS.
161  adj = {i: set() for i in range(width)}
162  for a, b in topo_edges:
163  adj[a].add(b)
164  adj[b].add(a)
165  from collections import deque
166  parent = {v: None}
167  q = deque([v])
168  while q:
169  node = q.popleft()
170  if node == u:
171  break
172  for nb in adj.get(node, set()):
173  if nb not in parent:
174  parent[nb] = node
175  q.append(nb)
176  if u not in parent:
177  # Cannot bridge this edge on the given topology.
178  raise ValueError(f"Cannot bridge ({u},{v}) on topology")
179  # Reconstruct path v -> ... -> u, then SWAP v along the path until it
180  # is adjacent to u, apply the gate, and unwind those same SWAPs.
181  path = [u]
182  node = u
183  while parent[node] is not None:
184  node = parent[node]
185  path.append(node)
186  path = list(reversed(path))
187  swaps = list(zip(path[:-2], path[1:-1]))
188  cur = v
189  for a, b in swaps:
190  _add_swap_as_cnots(new_c, a, b)
191  cur = b
192  if params:
193  new_c.append_gate(gate, [u, cur], params)
194  else:
195  new_c.append_gate(gate, [u, cur])
196  for a, b in reversed(swaps):
197  _add_swap_as_cnots(new_c, a, b)
198  return
199 
200  # gate.num_qudits >= 3: decompose to CNOT basis via Squander's utility
201  from bqskit.ir.lang.qasm2 import OPENQASM2Language
202  from qiskit import qasm2
203 
204  # 1) Build a minimal BQSKit circuit containing just this gate
205  from bqskit import Circuit as _BQCircuit
206  tmp_bq = _BQCircuit(width)
207  if params:
208  tmp_bq.append_gate(gate, loc, params)
209  else:
210  tmp_bq.append_gate(gate, loc)
211 
212  # 2) Encode to QASM, then decode via Squander
213  qasm_str = OPENQASM2Language().encode(tmp_bq)
214  from squander import Qiskit_IO as _QIO
215  qiskit_tmp = qasm2.loads(qasm_str)
216  sq_tmp, sq_params = _QIO.convert_Qiskit_to_Squander(qiskit_tmp)
217 
218  # 3) Decompose to CNOT basis
219  from squander.utils import circuit_to_CNOT_basis
220  sq_decomp, sq_decomp_params = circuit_to_CNOT_basis(sq_tmp, sq_params)
221 
222  # 4) Convert back to BQSKit and recurse on each gate
223  qiskit_decomp = _QIO.get_Qiskit_Circuit(sq_decomp, sq_decomp_params)
224  bq_decomp = OPENQASM2Language().decode(qasm2.dumps(qiskit_decomp))
225  for bq_op in bq_decomp:
226  _append_topology_safe(new_c, bq_op, topo_edges, width)
227 
228 
229 def _bqskit_location_respects_topology(location, topo_edges):
230  """Return true if ``location`` can be hosted by ``topo_edges``."""
231  loc = tuple(int(q) for q in location)
232  if len(loc) <= 1:
233  return True
234  if len(loc) == 2:
235  return (loc[0], loc[1]) in topo_edges or (loc[1], loc[0]) in topo_edges
236 
237  wanted = set(loc)
238  seen = {loc[0]}
239  stack = [loc[0]]
240  adjacency = {q: set() for q in wanted}
241  for u, v in topo_edges:
242  if u in wanted and v in wanted:
243  adjacency[u].add(v)
244  adjacency[v].add(u)
245  while stack:
246  cur = stack.pop()
247  for nxt in adjacency.get(cur, ()):
248  if nxt not in seen:
249  seen.add(nxt)
250  stack.append(nxt)
251  return wanted <= seen
252 
253 
254 def _assert_circuit_respects_topology(circuit, topo_edges):
255  """Raise AssertionError if ``circuit`` violates ``topo_edges``.
256 
257  Topology violations indicate a critical logic bug — the circuit cannot
258  physically execute on the target hardware. Execution must stop
259  immediately so the root cause can be investigated and fixed.
260  """
261  for op in circuit:
262  if op.gate.num_qudits <= 1:
263  continue
264  if not _bqskit_location_respects_topology(op.location, topo_edges):
265  raise AssertionError(
266  f"BUG: circuit contains {op.gate.name} on {list(op.location)}, "
267  f"outside topology {sorted(topo_edges)}."
268  )
269 
270 
271 def _fallback_circuit_for_permutation(original_circuit, graph, pi, po):
272  """Build a topology-valid fallback for ``Po.T @ U @ Pi``.
273 
274  ``original_circuit`` is the block circuit passed into BQSKit's
275  EmbedAllPermutationsPass. ``graph`` is the block-local coupling graph
276  selected by EAPP for this synthesis attempt.
277  """
278  from bqskit import Circuit as _BQCircuit
279 
280  width = original_circuit.num_qudits
281  if len(pi) != width or len(po) != width:
283  f"Permutation width mismatch for fallback: {pi}, {po}, width={width}."
284  )
285 
286  topo_edges = set()
287  for u, v in graph:
288  topo_edges.add((u, v))
289  topo_edges.add((v, u))
290 
291  fallback = _BQCircuit(width, original_circuit.radixes)
292 
293  for a, b in _topo_perm_to_swaps(pi, topo_edges, width):
294  if (a, b) not in topo_edges:
296  f"Cannot realize input permutation {pi} on topology {sorted(topo_edges)}."
297  )
298  _add_swap_as_cnots(fallback, a, b)
299 
300  for op in original_circuit:
301  _append_topology_safe(fallback, op, topo_edges, width)
302 
303  po_inv = tuple(po.index(k) for k in range(width))
304  for a, b in _topo_perm_to_swaps(po_inv, topo_edges, width):
305  if (a, b) not in topo_edges:
307  f"Cannot realize output permutation {po} on topology {sorted(topo_edges)}."
308  )
309  _add_swap_as_cnots(fallback, a, b)
310 
311  _assert_circuit_respects_topology(fallback, topo_edges)
312  return fallback
313 
314 
316  inner_synthesis,
317  target,
318  target_data,
319  original_circuit,
320  graph,
321  pi,
322  po,
323 ):
324  """Run Squander synthesis, falling back only for explicit Squander misses."""
325  try:
326  return await inner_synthesis.synthesize(target, target_data)
327  except _SquanderSynthesisFailed:
328  return _fallback_circuit_for_permutation(original_circuit, graph, pi, po)
329 
330 
332  """Monkey-patch EAPP.run to catch Squander OSR failures per permutation.
333 
334  IMPORTANT: This patch fully replaces ``EmbedAllPermutationsPass.run``.
335  It was written against BQSKit's internal EAPP implementation as of
336  the pip-installed version (see pyproject.toml / requirements for the
337  exact version). If BQSKit changes its EAPP internals (scoring function,
338  subtopology selection, permutation handling, or pass data keys), this
339  patch may silently diverge and should be re-audited against the new
340  BQSKit source.
341  """
342  import os as _os
343  if not _os.environ.get('_SQUANDER_EAPP_FALLBACK_PATCH'):
344  return
345 
346  from bqskit.passes.mapping.embed import EmbedAllPermutationsPass as __EAPP
347  if getattr(__EAPP.run, "_squander_fallback_patch", False):
348  return
349 
350  async def __patched_eapp_run(self, circuit, data):
351  import copy as _copy
352  import itertools as _it
353  import logging as _logging
354  from bqskit.compiler.machine import MachineModel as _MachineModel
355  from bqskit.passes.mapping.topology import SubtopologySelectionPass as _STSP
356  from bqskit.qis.graph import CouplingGraph as _CouplingGraph
357  from bqskit.qis.permutation import PermutationMatrix as _PermutationMatrix
358  from bqskit.runtime import get_runtime as _get_runtime
359 
360  _logger = _logging.getLogger("bqskit.passes.mapping.embed")
361  utry = data.target
362 
363  if not all(r == utry.radixes[0] for r in utry.radixes):
364  raise NotImplementedError(
365  'PermutationAwareSynthesisPass only supports unitaries '
366  'with the same radix on all qudits currently.',
367  )
368 
369  width = utry.num_qudits
370  perms = list(_it.permutations(range(width)))
371  no_perm = [tuple(range(width))]
372  Pis = [
373  _PermutationMatrix.from_qudit_location(width, utry.radixes[0], p)
374  for p in perms
375  ]
376  Pos = [
377  _PermutationMatrix.from_qudit_location(width, utry.radixes[0], p)
378  for p in perms
379  ]
380 
381  if self.input_perm and self.output_perm:
382  permsbyperms = list(_it.product(perms, perms))
383  targets = [Po.T @ utry @ Pi for Pi, Po in _it.product(Pis, Pos)]
384  elif self.input_perm:
385  permsbyperms = list(_it.product(perms, no_perm))
386  targets = [utry @ Pi for Pi in Pis]
387  elif self.output_perm:
388  permsbyperms = list(_it.product(no_perm, perms))
389  targets = [Po.T @ utry for Po in Pos]
390  else:
391  _logger.warning('No permutation is being used in PAS.')
392  permsbyperms = list(_it.product(no_perm, no_perm))
393  targets = [utry]
394 
395  if self.vary_topology and width != 1:
396  if _STSP.key not in data:
397  raise RuntimeError(
398  'Cannot find subtopologies, try running a'
399  ' SubtopologySelectionPass first.',
400  )
401  if width not in data[_STSP.key]:
402  raise RuntimeError(
403  'Subtopology information for block size'
404  f' {width} is not available.',
405  )
406  graphs = data[_STSP.key][width]
407  else:
408  graphs = [_CouplingGraph.all_to_all(width)]
409 
410  datas = []
411  for graph in graphs:
412  model = _MachineModel(
413  circuit.num_qudits, graph,
414  data.gate_set, data.model.radixes,
415  )
416  target_data = _copy.deepcopy(data)
417  target_data.model = model
418  datas.append(target_data)
419 
420  extended_targets = []
421  extended_datas = []
422  extended_graphs = []
423  extended_perms = []
424  original_circuits = []
425  for target_index, target in enumerate(targets):
426  for graph_index, graph in enumerate(graphs):
427  extended_targets.append(target)
428  extended_datas.append(datas[graph_index])
429  extended_graphs.append(graph)
430  extended_perms.append(permsbyperms[target_index])
431  original_circuits.append(circuit)
432 
433  circuits = await _get_runtime().map(
434  _squander_synthesize_or_fallback,
435  [self.inner_synthesis] * len(extended_targets),
436  extended_targets,
437  extended_datas,
438  original_circuits,
439  extended_graphs,
440  [perm[0] for perm in extended_perms],
441  [perm[1] for perm in extended_perms],
442  )
443 
444  perm_data = {}
445  all_perms = list(_it.permutations(range(width)))
446  for i, synthesized in enumerate(circuits):
447  graph = extended_graphs[i]
448  perm = extended_perms[i]
449 
450  if graph not in perm_data:
451  perm_data[graph] = {}
452 
453  if perm in perm_data[graph]:
454  s1 = self.scoring_fn(perm_data[graph][perm])
455  s2 = self.scoring_fn(synthesized)
456  if s2 < s1:
457  perm_data[graph][perm] = synthesized
458  else:
459  perm_data[graph][perm] = synthesized
460 
461  for univ_perm in all_perms[1:]:
462  renumber_c = synthesized.copy()
463  renumber_c.renumber_qudits(univ_perm)
464  new_pi = tuple(univ_perm[j] for j in perm[0])
465  new_pf = tuple(univ_perm[j] for j in perm[1])
466  new_graph = renumber_c.coupling_graph
467  if new_graph not in perm_data:
468  perm_data[new_graph] = {}
469 
470  new_perm = (new_pi, new_pf)
471  if new_perm not in perm_data[new_graph]:
472  perm_data[new_graph][new_perm] = renumber_c
473  else:
474  s1 = self.scoring_fn(perm_data[new_graph][new_perm])
475  s2 = self.scoring_fn(renumber_c)
476  if s2 < s1:
477  perm_data[new_graph][new_perm] = renumber_c
478 
479  if circuit.gate_set.issubset(data.model.gate_set):
480  for univ_perm in _it.permutations(range(width)):
481  uperm = (univ_perm, univ_perm)
482  renumber_c = circuit.copy()
483  renumber_c.renumber_qudits(univ_perm)
484  new_graph = renumber_c.coupling_graph
485  new_score = self.scoring_fn(renumber_c)
486  for graph, graph_data in perm_data.items():
487  if all(e in graph for e in new_graph):
488  if uperm not in graph_data:
489  graph_data[uperm] = renumber_c
490  elif new_score < self.scoring_fn(graph_data[uperm]):
491  graph_data[uperm] = renumber_c
492 
493  data['permutation_data'] = perm_data
494 
495  __patched_eapp_run._squander_fallback_patch = True
496  __EAPP.run = __patched_eapp_run
497 
498 
500 
501 
503  """BQSKit pass: replace circuit body with Squander ILP partition blocks."""
504 
505  def __init__(self, max_partition_size):
506  super().__init__()
507  self.max_partition_size = max_partition_size
508 
509  async def run(self, circuit, data=None):
510  from qiskit import qasm2, QuantumCircuit
511  from squander import Qiskit_IO
512  from bqskit import Circuit as BQSKitCircuit
513  from bqskit.ir.lang.qasm2 import OPENQASM2Language
514 
515  try:
516  circ_qiskit = QuantumCircuit.from_qasm_str(
517  OPENQASM2Language().encode(circuit)
518  )
519  except Exception:
520  # Circuit contains gates that can't be QASM-encoded (e.g.
521  # ConstantUnitaryGate from a prior pass). Keep as-is.
522  return
523 
524  circ, orig_parameters = Qiskit_IO.convert_Qiskit_to_Squander(circ_qiskit)
525  partitioned_circuit, parameters, _ = PartitionCircuit(
526  circ, orig_parameters, self.max_partition_size, strategy="ilp"
527  )
528  partitioned_circuit_bqskit = BQSKitCircuit(circ.get_Qbit_Num())
529  for subcircuit in partitioned_circuit.get_Gates():
530  if not isinstance(subcircuit, Circuit):
531  raise RuntimeError(
532  "Squander ILP partitioning returned a non-block gate; "
533  "BQSKit SEQPAM requires partition blocks."
534  )
535 
536  involved_qbits = sorted(subcircuit.get_Qbits())
537  qbit_map = {qbit: idx for idx, qbit in enumerate(involved_qbits)}
538  subcircuit_parameters = parameters[
539  subcircuit.get_Parameter_Start_Index() :
540  subcircuit.get_Parameter_Start_Index() + subcircuit.get_Parameter_Num()
541  ]
542  remapped_subcircuit = subcircuit.Remap_Qbits(qbit_map, len(involved_qbits))
543  subcircuit_qiskit = Qiskit_IO.get_Qiskit_Circuit(
544  remapped_subcircuit.get_Flat_Circuit(),
545  np.asarray(subcircuit_parameters, dtype=np.float64),
546  )
547  subcircuit_bqskit = OPENQASM2Language().decode(qasm2.dumps(subcircuit_qiskit))
548  partitioned_circuit_bqskit.append_circuit(
549  subcircuit_bqskit,
550  involved_qbits,
551  True,
552  True,
553  )
554  circuit.become(partitioned_circuit_bqskit, False)
555 
556 
558  """BQSKit synthesis pass: optimize partition blocks with Squander.
559 
560  Raises _SquanderSynthesisFailed when the configured Squander synthesis
561  strategy cannot produce a valid circuit for the requested subtopology. The
562  monkey-patched EmbedAllPermutationsPass catches this and installs a
563  SWAP-correct original-block fallback.
564  """
565 
566  def __init__(self, *args, **kwargs):
567  super().__init__()
568  cfg = _SQUANDER_BQSKIT_SYNTHESIS_CONFIG
569  if not cfg:
570  # Workers spawned via Popen inherit env vars but not Python
571  # globals. The main process serializes the config to
572  # _SQUANDER_BQSKIT_CONFIG before spawning workers.
573  import os as _os, json as _json
574  _env = _os.environ.get('_SQUANDER_BQSKIT_CONFIG')
575  if _env:
576  cfg = _json.loads(_env)
577  self.config = dict(cfg or {})
578 
579  @staticmethod
580  def _data_topology(data, qbit_num):
581  """Return block subtopology from *data*.
582 
583  BQSKit labels are reversed when circuits are converted through
584  Squander/Qiskit, so the topology supplied to Squander is reversed too.
585  """
586  if data is None or getattr(data, "model", None) is None:
587  return None
588 
589  edges = []
590  for u, v in data.model.coupling_graph:
591  if u == v:
592  continue
593  edges.append((qbit_num - 1 - int(u), qbit_num - 1 - int(v)))
594 
595  all_edges = {
596  frozenset((i, j))
597  for i in range(qbit_num)
598  for j in range(i + 1, qbit_num)
599  }
600  edge_set = {frozenset(edge) for edge in edges}
601  if edge_set == all_edges:
602  return None
603  return edges
604 
605  @staticmethod
607  """Return directed topology edges from BQSKit pass data."""
608  if data is None or getattr(data, "model", None) is None:
609  return None
610  topo_edges = set()
611  for u, v in data.model.coupling_graph:
612  topo_edges.add((int(u), int(v)))
613  topo_edges.add((int(v), int(u)))
614  return topo_edges
615 
616  async def synthesize(self, target, data=None):
617  from qiskit import qasm2
618  from squander import Qiskit_IO
619  from bqskit.ir.lang.qasm2 import OPENQASM2Language
620  from bqskit.qis.unitary.unitarymatrix import UnitaryMatrix
621 
622  target_matrix = np.asarray(target)
623  qbit_num = target.num_qudits
624  mini_topology = self._data_topology(data, qbit_num)
625 
626  config = {
627  **self.config,
628  "topology": mini_topology,
629  }
630 
631  candidates = qgd_Wide_Circuit_Optimization.DecomposePartition(
632  target_matrix,
633  config,
634  mini_topology=mini_topology,
635  )
636  if len(candidates) == 0:
637  tolerance = config.get("tolerance", _default_squander_tolerance(config))
639  f"Squander synthesis failed for {qbit_num}-qubit block "
640  f"at tolerance {tolerance}."
641  )
642 
643  optimized_circuit, optimized_parameters = (
644  qgd_Wide_Circuit_Optimization.CompareAndPickCircuits(
645  [candidate[0] for candidate in candidates],
646  [candidate[1] for candidate in candidates],
647  )
648  )
649 
650  optimized_qiskit = Qiskit_IO.get_Qiskit_Circuit(
651  optimized_circuit.get_Flat_Circuit(),
652  np.asarray(optimized_parameters, dtype=np.float64),
653  )
654  synthesized = OPENQASM2Language().decode(qasm2.dumps(optimized_qiskit))
655 
656  # The QASM round-trip preserves qubit labels but changes the physical
657  # interpretation (Squander MSB=0 → BQSKit LSB=0). Renumber qudits to
658  # compensate: Squander qubit k (MSB=0) → BQSKit qubit (qbit_num-1-k).
659  if qbit_num > 1:
660  synthesized.renumber_qudits(
661  [qbit_num - 1 - i for i in range(qbit_num)]
662  )
663 
664  topo_edges = self._topology_edges_from_data(data)
665  if topo_edges is not None:
666  _assert_circuit_respects_topology(synthesized, topo_edges)
667 
668  if self.config.get("bqskit_distance_test", False):
669  target_unitary = UnitaryMatrix(target)
670  distance = target_unitary.get_distance_from(synthesized.get_unitary())
672  if distance > tol:
674  f"BQSKit synthesis validation failed: {distance:.2e} > {tol:.2e}"
675  )
676 
677  return synthesized
678 
679 
681  """Raised when Squander cannot synthesize a partition block."""
682 
683 
684 def _topo_perm_to_swaps(pi, topo_edges, width):
685  """Decompose permutation *pi* into SWAPs using only edges in *topo_edges*.
686 
687  Uses BFS on the topology graph to find a SWAP sequence that implements
688  the permutation. Returns a list of (u, v) pairs valid in *topo_edges*.
689  """
690  # Build adjacency list from topo_edges (undirected)
691  adj = {i: set() for i in range(width)}
692  for u, v in topo_edges:
693  adj[u].add(v)
694  adj[v].add(u)
695 
696  # Greedy: for each position i, bring the target qubit pi[i] to position i
697  # by routing through the topology graph.
698  current = list(range(width)) # current[pos] = which qubit is at pos
699  swaps = []
700  for i in range(width):
701  target = pi[i]
702  if current[i] == target:
703  continue
704  # Find where target currently is
705  target_pos = current.index(target)
706  # BFS from target_pos to i, finding shortest path of SWAPs
707  from collections import deque
708  parent = {target_pos: None}
709  q = deque([target_pos])
710  while q:
711  u = q.popleft()
712  if u == i:
713  break
714  for v in adj[u]:
715  if v not in parent:
716  parent[v] = u
717  q.append(v)
718  # Reconstruct path and apply SWAPs
719  if i not in parent:
720  raise _SquanderSynthesisFailed(
721  f"Cannot realize permutation {pi} on disconnected topology "
722  f"{sorted(topo_edges)}."
723  )
724  path = []
725  v = i
726  while parent[v] is not None:
727  path.append(v)
728  v = parent[v]
729  path.append(target_pos)
730  # Apply SWAPs along the path (reverse order to bring target to i)
731  for k in range(len(path) - 1, 0, -1):
732  a, b = path[k], path[k - 1]
733  swaps.append((a, b))
734  # Update current positions
735  current[a], current[b] = current[b], current[a]
736  return swaps
737 
738 
739 @contextlib.contextmanager
740 def patched_seqpam_workflow_classes(bqskit_compile_module, use_squander_partitioner, config):
741  """Patch BQSKit workflow factories to use Squander passes.
742 
743  Replaces QSearch/LEAP with SquanderSynthesisPass. Squander failures are
744  caught by the EAPP patch and replaced with SWAP-correct fallbacks.
745  """
746 
747  global _SQUANDER_BQSKIT_SYNTHESIS_CONFIG
748 
749  import os as _os, json as _json
750 
751  original_quick = bqskit_compile_module.QuickPartitioner
752  original_qsearch = bqskit_compile_module.QSearchSynthesisPass
753  original_leap = bqskit_compile_module.LEAPSynthesisPass
754  original_config = _SQUANDER_BQSKIT_SYNTHESIS_CONFIG
755  original_config_env = _os.environ.get('_SQUANDER_BQSKIT_CONFIG')
756  try:
757  cfg = _copy_bqskit_synthesis_config(config)
758  _SQUANDER_BQSKIT_SYNTHESIS_CONFIG = cfg
759  # Also store in env var so worker processes (Popen) inherit it
760  _os.environ['_SQUANDER_BQSKIT_CONFIG'] = _json.dumps(cfg)
761  if use_squander_partitioner:
762  bqskit_compile_module.QuickPartitioner = SquanderPartitioner
763  bqskit_compile_module.QSearchSynthesisPass = SquanderSynthesisPass
764  bqskit_compile_module.LEAPSynthesisPass = SquanderSynthesisPass
765  yield
766  finally:
767  bqskit_compile_module.QuickPartitioner = original_quick
768  bqskit_compile_module.QSearchSynthesisPass = original_qsearch
769  bqskit_compile_module.LEAPSynthesisPass = original_leap
770  _SQUANDER_BQSKIT_SYNTHESIS_CONFIG = original_config
771  if original_config_env is None:
772  _os.environ.pop('_SQUANDER_BQSKIT_CONFIG', None)
773  else:
774  _os.environ['_SQUANDER_BQSKIT_CONFIG'] = original_config_env
775 
776 
777 def extract_subtopology(involved_qbits, qbit_map, config):
778  """Return topology edges restricted to ``involved_qbits``, with indices remapped via ``qbit_map``.
779 
780  Args:
781  involved_qbits: Qubit labels present in a partition.
782  qbit_map: Maps original qubit index to local index (0..n-1).
783  config: Configuration dict containing ``topology`` as a list of edges.
784 
785  Returns:
786  List of ``(u, v)`` pairs in local indices, each edge fully inside the partition.
787  """
788  mini_topology = []
789  for edge in config["topology"]:
790  if edge[0] in involved_qbits and edge[1] in involved_qbits:
791  mini_topology.append((qbit_map[edge[0]], qbit_map[edge[1]]))
792  return mini_topology
793 
794 
795 # Universal gate decomposition dictionary.
796 # Each gate maps to its exact breakdown into {CNOT, H, RX, RY, RZ, ...} basis
797 # as defined by circuit_to_CNOT_basis in squander/utils.py.
798 # Native single-qubit gates and CNOT map to themselves with count 1.
799 _GATE_DECOMPOSITION = {
800  # --- native gates (do not decompose) ---
801  "CNOT": {"CNOT": 1},
802  "H": {"H": 1},
803  "X": {"X": 1},
804  "Y": {"Y": 1},
805  "Z": {"Z": 1},
806  "S": {"S": 1},
807  "Sdg": {"Sdg": 1},
808  "T": {"T": 1},
809  "Tdg": {"Tdg": 1},
810  "SX": {"SX": 1},
811  "SXdg": {"SXdg": 1},
812  "RX": {"RX": 1},
813  "RY": {"RY": 1},
814  "RZ": {"RZ": 1},
815  "R": {"R": 1},
816  "U1": {"U1": 1},
817  "U2": {"U2": 1},
818  "U3": {"U3": 1},
819  # --- decomposed gates (counts from circuit_to_CNOT_basis) ---
820  "CH": {"CNOT": 1, "RY": 2}, # RY + CNOT + RY
821  "CZ": {"CNOT": 1, "H": 2}, # H + CNOT + H
822  "SYC": {"CNOT": 3, "U1": 3}, # U1 + U1 + CNOT + U1 + CNOT + CNOT
823  "CRY": {"CNOT": 2, "RY": 2}, # CNOT + RY + CNOT + RY
824  "CU": {"CNOT": 2, "U1": 1, "RZ": 3, "RY": 2}, # U1 + RZ + RY + CNOT + RY + RZ + CNOT + RZ
825  "CR": {"CNOT": 2, "RZ": 2, "RY": 2}, # RZ + CNOT + RY + CNOT + RY + RZ
826  "CROT": {"CNOT": 2, "RZ": 3, "RY": 2}, # RZ + RY + CNOT + RZ + CNOT + RY + RZ
827  "CRX": {"CNOT": 2, "H": 2, "RZ": 2}, # H + CNOT + RZ + CNOT + RZ + H
828  "CRZ": {"CNOT": 2, "RZ": 2}, # CNOT + RZ + CNOT + RZ
829  "CP": {"CNOT": 2, "U1": 3}, # U1 + CNOT + U1 + CNOT + U1
830  "CCX": {"CNOT": 6, "H": 2, "T": 4, "Tdg": 3}, # standard Toffoli: 7 CNOTs + 8 single-qubit
831  "CSWAP": {"CNOT": 7, "H": 1, "T": 5, "Tdg": 2, "SX": 1, "Sdg": 1, "S": 1}, # Fredkin
832  "SWAP": {"CNOT": 3}, # CNOT + CNOT + CNOT
833  "RXX": {"CNOT": 2, "RX": 1}, # CNOT + RX + CNOT
834  "RYY": {"CNOT": 2, "RX": 4, "RZ": 1}, # RX + RX + CNOT + RZ + CNOT + RX + RX
835  "RZZ": {"CNOT": 2, "RZ": 1}, # CNOT + RZ + CNOT
836 }
837 
838 # Backward-compatible: CNOT-equivalent cost (number of CNOTs in decomposition).
839 CNOT_COUNT_DICT = {g: d.get("CNOT", 0) for g, d in _GATE_DECOMPOSITION.items()}
840 
841 
842 def CNOTGateCount(circ: Circuit, max_gates: int = 0) -> int:
843  """Compute weighted two-qubit gate count for a circuit.
844 
845  The base count is the CNOT-equivalent cost derived from ``CNOT_COUNT_DICT``.
846  When ``max_gates > 0``, the function returns a lexicographic-style score:
847  ``two_qubit_cost * max_gates + single_qubit_gate_count``.
848 
849  Args:
850  circ: Squander circuit representation.
851  max_gates: Weight multiplier for the two-qubit cost term.
852 
853  Returns:
854  Integer gate-cost score used by optimization heuristics.
855  """
856  assert isinstance(circ, Circuit), \
857  "The input parameters should be an instance of Squander Circuit"
858  gate_counts = circ.get_Gate_Nums()
859  num_cnots = sum(
860  CNOT_COUNT_DICT.get(gate, 0) * count for gate, count in gate_counts.items()
861  )
862  if max_gates > 0:
863  return num_cnots * max_gates + sum(
864  y for x, y in gate_counts.items() if CNOT_COUNT_DICT.get(x, -1) <= 0
865  )
866  return num_cnots
867 
868 
869 def SingleQubitGateCount(circ: Circuit) -> int:
870  """Count single-qubit gates in a circuit (U3, H, RX, RY, RZ, etc.).
871 
872  Uses _GATE_DECOMPOSITION to count non-CNOT gates in each gate's breakdown.
873 
874  Args:
875  circ: Squander circuit representation.
876 
877  Returns:
878  Total number of single-qubit gate operations when fully decomposed.
879  """
880  gate_counts = circ.get_Gate_Nums()
881  total = 0
882  for gate, count in gate_counts.items():
883  decomp = _GATE_DECOMPOSITION.get(gate, {})
884  total += count * sum(v for k, v in decomp.items() if k != "CNOT")
885  return total
886 
887 
888 def TotalRawGateCount(circ: Circuit) -> int:
889  """Total number of raw gate operations (single-qubit + multi-qubit).
890 
891  Args:
892  circ: Squander circuit representation.
893 
894  Returns:
895  Total gate operation count.
896  """
897  return sum(circ.get_Gate_Nums().values())
898 
899 
900 def CircuitGateStats(circ: Circuit) -> dict:
901  """Return comprehensive gate statistics for a circuit.
902 
903  Uses _GATE_DECOMPOSITION to compute fully-decomposed gate counts.
904 
905  Returns dict with keys: cnot_equiv, single_qubit, total_raw, qubits,
906  and gate_breakdown (per-gate-type raw counts).
907  """
908  gate_counts = circ.get_Gate_Nums()
909  cnot_equiv = sum(
910  CNOT_COUNT_DICT.get(g, 0) * c for g, c in gate_counts.items()
911  )
912  single = 0
913  for g, c in gate_counts.items():
914  decomp = _GATE_DECOMPOSITION.get(g, {})
915  single += c * sum(v for k, v in decomp.items() if k != "CNOT")
916  total = sum(gate_counts.values())
917  return {
918  "cnot_equiv": cnot_equiv,
919  "single_qubit": single,
920  "total_raw": total,
921  "qubits": circ.get_Qbit_Num(),
922  "gate_breakdown": dict(gate_counts),
923  }
924 
925 
927  """Optimize wide (many-qubit) circuits via partitioning and subcircuit decomposition.
928 
929  Supports multiple decomposition strategies, optional global recombination (ILP),
930  and routing when the circuit does not match the target topology.
931  """
932 
933  def __init__(self, config):
934  """Validate and store wide-circuit optimization ``config`` (strategy, topology, partitioning, tolerances)."""
935 
936  config.setdefault("strategy", "TreeSearch")
937  config.setdefault("parallel", 0)
938  config.setdefault("verbosity", 0)
939  config.setdefault("use_float", False)
940  config.setdefault("tolerance", _default_squander_tolerance(config))
941  config.setdefault(
942  "bqskit_synthesis_validation_tolerance",
944  )
945  config.setdefault("test_subcircuits", False)
946  config.setdefault("test_final_circuit", True)
947  config.setdefault("max_partition_size", 3)
948  config.setdefault("topology", None)
949  config.setdefault("partition_strategy", "ilp")
950  config.setdefault("auto_expand_partition_size", True)
951  config.setdefault("force_small_circuit_validation", True)
952 
953  # testing the fields of config
954  strategy = config["strategy"]
955  allowed_startegies = [
956  "TreeSearch",
957  "TabuSearch",
958  "Adaptive",
959  "qiskit",
960  "bqskit",
961  ]
962  if not strategy in allowed_startegies:
963  raise Exception(
964  f"The decomposition startegy should be either of {allowed_startegies}, got {strategy}."
965  )
966 
967  parallel = config["parallel"]
968  allowed_parallel = [0, 1, 2]
969  if not parallel in allowed_parallel:
970  raise Exception(
971  f"The parallel configuration should be either of {allowed_parallel}, got {parallel}."
972  )
973 
974  verbosity = config["verbosity"]
975  if not isinstance(verbosity, int):
976  raise Exception(f"The verbosity parameter should be an integer.")
977 
978  tolerance = config["tolerance"]
979  if not isinstance(tolerance, float):
980  raise Exception(f"The tolerance parameter should be a float.")
981 
982  use_float = config["use_float"]
983  if not isinstance(use_float, bool):
984  raise Exception(f"The use_float parameter should be a bool.")
985 
986  bqskit_synthesis_validation_tolerance = config[
987  "bqskit_synthesis_validation_tolerance"
988  ]
989  if not isinstance(bqskit_synthesis_validation_tolerance, float):
990  raise Exception(
991  "The bqskit_synthesis_validation_tolerance parameter should be a float."
992  )
993 
994  squander_validation_tolerance = config.get(
995  "tolerance",
996  None,
997  )
998  if squander_validation_tolerance is not None and not isinstance(
999  squander_validation_tolerance,
1000  float,
1001  ):
1002  raise Exception(
1003  "The squander_validation_tolerance parameter should be a float."
1004  )
1005 
1006  test_subcircuits = config["test_subcircuits"]
1007  if not isinstance(test_subcircuits, bool):
1008  raise Exception(f"The test_subcircuits parameter should be a bool.")
1009 
1010  test_final_circuit = config["test_final_circuit"]
1011  if not isinstance(test_final_circuit, bool):
1012  raise Exception(f"The test_final_circuit parameter should be a bool.")
1013 
1014  max_partition_size = config["max_partition_size"]
1015  if not isinstance(max_partition_size, int):
1016  raise Exception(f"The max_partition_size parameter should be an integer.")
1017 
1018  self.config = config
1019 
1020  self.max_partition_size = max_partition_size
1021 
1022  @staticmethod
1023  def partition_tree_level_max(config, subcircuit, reduction=1):
1024  """Return the tree-search depth used for partition-local rewrites."""
1025 
1026  target_depth = max(0, CNOTGateCount(subcircuit, 0) - reduction)
1027  configured_limit = config.get("partition_tree_level_max", None)
1028  if configured_limit is None:
1029  configured_limit = target_depth
1030  return min(target_depth, int(configured_limit))
1031 
1033  self, circs: List[Circuit], parameter_arrs: List[List[np.ndarray]]
1034  ) -> Tuple[Circuit, np.ndarray]:
1035  """Concatenate optimized partition circuits into a single wide circuit.
1036 
1037  Args:
1038  circs: Partition circuits in execution order.
1039  parameter_arrs: Parameter arrays corresponding to ``circs``.
1040 
1041  Returns:
1042  Tuple of ``(wide_circuit, wide_parameters)``.
1043  """
1044 
1045  if not isinstance(circs, list):
1046  raise Exception("First argument should be a list of squander circuits")
1047 
1048  if not isinstance(parameter_arrs, list):
1049  raise Exception("Second argument should be a list of numpy arrays")
1050 
1051  if len(circs) != len(parameter_arrs):
1052  raise Exception("The first two arguments should be of the same length")
1053 
1054  qbit_num = circs[0].get_Qbit_Num()
1055 
1056  wide_parameters = np.concatenate(parameter_arrs, axis=0)
1057 
1058  wide_circuit = Circuit(qbit_num)
1059 
1060  for circ in circs:
1061  wide_circuit.add_Circuit(circ)
1062 
1063  assert (
1064  wide_circuit.get_Parameter_Num() == wide_parameters.size
1065  ), f"Mismatch in the number of parameters: {wide_circuit.get_Parameter_Num()} vs {wide_parameters.size}"
1066 
1067  return wide_circuit, wide_parameters
1068 
1069  @staticmethod
1070  def DecomposePartition(
1071  Umtx: np.ndarray, config: dict, mini_topology=None, structure=None
1072  ) -> list[tuple[Circuit, np.ndarray]]:
1073  """Decompose a unitary ``Umtx`` (e.g. from a partition) using ``config['strategy']``.
1074 
1075  Args:
1076  Umtx: Complex unitary matrix.
1077  config: Must include ``strategy``, ``tolerance``, ``verbosity``, etc.
1078  mini_topology: Optional hardware couplers for topology-aware decomposers.
1079  structure: Required gate structure when ``strategy == "Custom"``.
1080 
1081  Returns:
1082  Normally ``[(circuit, parameters)]`` on success, or ``[]`` if the
1083  decomposition error exceeds ``tolerance``. If
1084  ``config.get('stop_first_solution')`` is false, returns
1085  ``cDecompose.all_solutions`` from the underlying decomposer instead of
1086  a single best pair.
1087  """
1088  strategy = config["strategy"]
1089  if strategy == "TreeSearch":
1091  Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1092  )
1093  elif strategy == "TabuSearch":
1094  cDecompose = N_Qubit_Decomposition_Tabu_Search(
1095  Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1096  )
1097  elif strategy == "Adaptive":
1098  cDecompose = N_Qubit_Decomposition_adaptive(
1099  Umtx.conj().T,
1100  level_limit_max=5,
1101  level_limit_min=1,
1102  topology=mini_topology,
1103  )
1104  elif strategy == "Custom":
1105  cDecompose = N_Qubit_Decomposition_custom(
1106  Umtx.conj().T, config=config, accelerator_num=0
1107  )
1108  assert (
1109  structure is not None
1110  ), "Custom decomposition strategy requires a gate structure to be provided."
1111  cDecompose.set_Gate_Structure(structure)
1112  else:
1113  raise Exception(f"Unsupported decomposition type: {strategy}")
1114 
1115  tolerance = config["tolerance"]
1116  cDecompose.set_Verbose(config["verbosity"])
1117  cDecompose.set_Cost_Function_Variant(3)
1118  cDecompose.set_Optimization_Tolerance(tolerance)
1119 
1120  # adding new layer to the decomposition until threshold
1121  cDecompose.set_Optimizer("BFGS")
1122 
1123  # starting the decomposition
1124  try:
1125  cDecompose.Start_Decomposition()
1126  except Exception as e:
1127  # print(e)
1128  raise e
1129  # return []
1130  if not config.get("stop_first_solution", True):
1131  return cDecompose.all_solutions
1132 
1133  squander_circuit = cDecompose.get_Circuit()
1134  parameters = cDecompose.get_Optimized_Parameters()
1135  assert parameters is not None
1136 
1137  if strategy == "Custom":
1138  err = cDecompose.Optimization_Problem(parameters)
1139  it = 0
1140  while err > tolerance and it < 20:
1141  cDecompose.set_Optimized_Parameters(
1142  np.random.rand(cDecompose.get_Parameter_Num()) * (2 * np.pi)
1143  )
1144  cDecompose.Start_Decomposition()
1145  parameters = cDecompose.get_Optimized_Parameters()
1146  err = cDecompose.Optimization_Problem(parameters)
1147  it += 1
1148  if err > tolerance or it != 0:
1149  print("Decomposition error: ", err, it)
1150  else:
1151  err = cDecompose.get_Decomposition_Error()
1152  # print( "Decomposition error: ", err )
1153  if tolerance < err:
1154  # raise Exception(f"Decomposition error {err} exceeds the tolerance {tolerance}.")
1155  return []
1156 
1157  return [(squander_circuit, parameters)]
1158 
1159  @staticmethod
1161  circs: List[Circuit],
1162  parameter_arrs: List[np.ndarray],
1163  metric: Callable[[Circuit], int] = CNOTGateCount,
1164  ) -> tuple[Circuit, np.ndarray]:
1165  """Select the circuit with the lowest ``metric`` value.
1166 
1167  Args:
1168  circs: Candidate Squander circuits (same length as ``parameter_arrs``).
1169  parameter_arrs: Parameter vectors aligned with ``circs``.
1170  metric: Scalar cost functional; lower is better. Defaults to ``CNOTGateCount``.
1171 
1172  Returns:
1173  ``(best_circuit, best_parameters)`` for the minimizing index.
1174  """
1175 
1176  if not isinstance(circs, list):
1177  raise Exception("First argument should be a list of squander circuits")
1178 
1179  if not isinstance(parameter_arrs, list):
1180  raise Exception("Second argument should be a list of numpy arrays")
1181 
1182  if len(circs) != len(parameter_arrs):
1183  raise Exception("The first two arguments should be of the same length")
1184 
1185  metrics = [metric(circ) for circ in circs]
1186 
1187  metrics = np.array(metrics)
1188 
1189  min_idx = np.argmin(metrics)
1190 
1191  return circs[min_idx], parameter_arrs[min_idx]
1192 
1193  @staticmethod
1195  subcircuit: Circuit,
1196  subcircuit_parameters: np.ndarray,
1197  config: dict,
1198  structure=None,
1199  ) -> Tuple[Circuit, np.ndarray]:
1200  """Decompose one partition subcircuit (multiprocessing-safe entry point).
1201 
1202  Args:
1203  subcircuit: Subcircuit acting on a subset of the wide register.
1204  subcircuit_parameters: Flat parameter vector slice for ``subcircuit``.
1205  config: Same keys as wide optimization (``strategy``, ``topology``, etc.).
1206  structure: Optional fixed gate structure when ``strategy == "Custom"``.
1207 
1208  Returns:
1209  Tuple of ``(decomposed_circuit, decomposed_parameters)`` pairs, each
1210  remapped back to the original qubit indices of ``subcircuit``.
1211  """
1212 
1213  qbit_num_orig_circuit = subcircuit.get_Qbit_Num()
1214 
1215  involved_qbits = subcircuit.get_Qbits()
1216 
1217  qbit_num = len(involved_qbits)
1218 
1219  # create qbit map:
1220  qbit_map = {}
1221  for idx in range(len(involved_qbits)):
1222  qbit_map[involved_qbits[idx]] = idx
1223  mini_topology = None
1224  if config["topology"] is not None:
1225  mini_topology = extract_subtopology(involved_qbits, qbit_map, config)
1226  # remap the subcircuit to a smaller qubit register
1227  remapped_subcircuit = subcircuit.Remap_Qbits(qbit_map, qbit_num)
1228 
1229  if not structure is None:
1230  structure = structure.Remap_Qbits(qbit_map, qbit_num)
1231 
1232  # get the unitary representing the circuit
1233  unitary = remapped_subcircuit.get_Matrix(
1234  np.asarray(subcircuit_parameters, dtype=np.float64)
1235  )
1236 
1237  # decompose a small unitary into a new circuit
1238  all_decomposed = qgd_Wide_Circuit_Optimization.DecomposePartition(
1239  unitary, config, mini_topology, structure=structure
1240  )
1241  # create inverse qbit map:
1242  inverse_qbit_map = {}
1243  for key, value in qbit_map.items():
1244  inverse_qbit_map[value] = key
1245  result = []
1246  for decomposed_circuit, decomposed_parameters in all_decomposed:
1247 
1248  # remap the decomposed circuit in order to insert it into a large circuit
1249  new_subcircuit = decomposed_circuit.Remap_Qbits(
1250  inverse_qbit_map, qbit_num_orig_circuit
1251  )
1252 
1253  if config["test_subcircuits"]:
1255  subcircuit,
1256  subcircuit_parameters,
1257  new_subcircuit,
1258  decomposed_parameters,
1259  parallel=config["parallel"],
1260  tolerance=_squander_validation_tolerance(config)
1261  )
1262 
1263  new_subcircuit = new_subcircuit.get_Flat_Circuit()
1264  result.append((new_subcircuit, decomposed_parameters))
1265  return tuple(result)
1266 
1267  @staticmethod
1269  """Order partition gate-sets by dependencies and build a reverse-dependency map.
1270 
1271  Args:
1272  allparts: List of sets of gate indices, one per partition.
1273 
1274  Returns:
1275  ``(ordered_parts, rg_new)`` where ``ordered_parts`` lists partitions in
1276  topological order and ``rg_new`` maps each new index to predecessors.
1277  """
1278  gate_to_parts = {}
1279  for i, part in enumerate(allparts):
1280  for gate in part:
1281  gate_to_parts.setdefault(gate, set()).add(i)
1282  g = {i: set() for i in range(len(allparts))}
1283  rg = {i: set() for i in range(len(allparts))}
1284  for i, part in enumerate(allparts):
1285  for gate in part:
1286  for other_part in gate_to_parts[gate]:
1287  if other_part != i and (
1288  len(part & allparts[other_part]) > 0
1289  and (len(part) < len(allparts[other_part]))
1290  or part < allparts[other_part]
1291  ):
1292  g[i].add(other_part)
1293  rg[other_part].add(i)
1294  rg_ret = {i: set(rg[i]) for i in range(len(allparts))}
1295  S = collections.deque(m for m in rg if len(rg[m]) == 0)
1296  L = []
1297  while S:
1298  n = S.popleft()
1299  L.append(n)
1300  for m in set(g[n]):
1301  g[n].remove(m)
1302  rg[m].remove(n)
1303  if len(rg[m]) == 0:
1304  S.append(m)
1305  if len(L) != len(allparts):
1306  raise ValueError("Dependency graph is not a DAG")
1307  neworder = {old: new for new, old in enumerate(L)}
1308  rg_ret = {
1309  neworder[i]: set(neworder[j] for j in rg_ret[i])
1310  for i in range(len(allparts))
1311  }
1312  return [
1313  allparts[i] for i in L
1314  ], rg_ret # return partitions in dependency order and dependencies
1315 
1316  @staticmethod
1317  def make_all_partition_circuit(circ, orig_parameters, max_partition_size):
1318  """ILP-based partitioning: flatten ``circ`` into a circuit of sub-circuits with concatenated parameters.
1319 
1320  Returns:
1321  ``(partitioned_circuit, parameters, recombine_info, part_deps)`` for later fusion in
1322  ``recombine_all_partition_circuit``.
1323  """
1324  from squander.partitioning.ilp import get_all_partitions, _get_topo_order
1325 
1326  allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
1327  get_all_partitions(circ, max_partition_size)
1328  )
1329  qbit_num_orig_circuit = circ.get_Qbit_Num()
1330  gate_dict = {i: gate for i, gate in enumerate(circ.get_Gates())}
1331  single_qubit_chains_pre = {x[0]: x for x in single_qubit_chains if rgo[x[0]]}
1332  single_qubit_chains_post = {x[-1]: x for x in single_qubit_chains if go[x[-1]]}
1333  single_qubit_chains_prepost = {
1334  x[0]: x
1335  for x in single_qubit_chains
1336  if x[0] in single_qubit_chains_pre and x[-1] in single_qubit_chains_post
1337  }
1338  partitioned_circuit = Circuit(qbit_num_orig_circuit)
1339  params = []
1340  allparts, part_deps = qgd_Wide_Circuit_Optimization.build_partition_topo_deps(
1341  allparts
1342  )
1343  for part in allparts:
1344  surrounded_chains = {
1345  t
1346  for s in part
1347  for t in go[s]
1348  if t in single_qubit_chains_prepost
1349  and go[single_qubit_chains_prepost[t][-1]]
1350  and next(iter(go[single_qubit_chains_prepost[t][-1]])) in part
1351  }
1352  gates = frozenset.union(
1353  part, *(single_qubit_chains_prepost[v] for v in surrounded_chains)
1354  )
1355  # topo sort part + surrounded chains
1356  c = Circuit(qbit_num_orig_circuit)
1357  for gate_idx in _get_topo_order(
1358  {x: go[x] & gates for x in gates},
1359  {x: rgo[x] & gates for x in gates},
1360  gate_to_qubit,
1361  ):
1362  c.add_Gate(gate_dict[gate_idx])
1363  start = gate_dict[gate_idx].get_Parameter_Start_Index()
1364  params.append(
1365  orig_parameters[
1366  start : start + gate_dict[gate_idx].get_Parameter_Num()
1367  ]
1368  )
1369  partitioned_circuit.add_Circuit(c)
1370  for chain in single_qubit_chains:
1371  c = Circuit(qbit_num_orig_circuit)
1372  for gate_idx in chain:
1373  c.add_Gate(gate_dict[gate_idx])
1374  start = gate_dict[gate_idx].get_Parameter_Start_Index()
1375  params.append(
1376  orig_parameters[
1377  start : start + gate_dict[gate_idx].get_Parameter_Num()
1378  ]
1379  )
1380  partitioned_circuit.add_Circuit(c)
1381  parameters = np.concatenate(params, axis=0)
1382  return (
1383  partitioned_circuit,
1384  parameters,
1385  (allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit),
1386  part_deps,
1387  )
1388 
1389  @staticmethod
1391  """Drop single-qubit gates that sit only at the head or tail of the dependency DAG.
1392 
1393  Args:
1394  circ: Input circuit.
1395  params: Flat parameter array for ``circ``.
1396 
1397  Returns:
1398  ``(new_circuit, new_params)`` with head/tail single-qubit gates removed.
1399  """
1400  gate_dict, g, rg, gate_to_qubit, _ = build_dependency(circ)
1401  newcirc = Circuit(circ.get_Qbit_Num())
1402  new_params = []
1403  for i in gate_dict:
1404  gate = gate_dict[i]
1405  if len(gate_to_qubit[i]) == 1 and (len(g[i]) == 0 or len(rg[i]) == 0):
1406  continue
1407  newcirc.add_Gate(gate)
1408  start_idx = gate.get_Parameter_Start_Index()
1409  new_params.append(params[start_idx : start_idx + gate.get_Parameter_Num()])
1410  return newcirc, (
1411  np.empty((0,), dtype=np.float64)
1412  if len(new_params) == 0
1413  else np.concatenate(new_params, axis=0)
1414  )
1415 
1416  @staticmethod
1417  def get_fingerprint(circ, params):
1418  """Hashable signature of gate layout and parameters (for decomposition caching).
1419 
1420  Args:
1421  circ: Squander circuit.
1422  params: Parameter array associated with ``circ``.
1423 
1424  Returns:
1425  Tuple usable as a dict key for memoizing decompositions.
1426  """
1427  return tuple(
1428  (gate.get_Name(), tuple(gate.get_Involved_Qbits()))
1429  for gate in circ.get_Gates()
1430  ) + tuple(params)
1431 
1432  @staticmethod
1434  circ, optimized_subcircuits, optimized_parameter_list, recombine_info
1435  ):
1436  """Reorder optimized partitions to respect global gate dependencies.
1437 
1438  Args:
1439  circ: Original flat circuit (for topological ordering context).
1440  optimized_subcircuits: One optimized subcircuit per partition slot.
1441  optimized_parameter_list: Parameter lists aligned with ``optimized_subcircuits``.
1442  recombine_info: Tuple from ``make_all_partition_circuit`` (ILP metadata).
1443 
1444  Returns:
1445  ``(reordered_circuits, reordered_parameter_lists)`` in execution order.
1446  """
1447  from squander.partitioning.ilp import (
1448  topo_sort_partitions,
1449  ilp_global_optimal,
1450  recombine_single_qubit_chains,
1451  )
1452 
1453  allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
1454  recombine_info
1455  )
1456  max_gates = sum(
1457  sum(y for x, y in c.get_Gate_Nums().items() if CNOT_COUNT_DICT.get(x, -1) <= 0)
1458  for c in optimized_subcircuits[: len(allparts)]
1459  )
1460  weights = [
1461  CNOTGateCount(circ, max_gates)
1462  for circ in optimized_subcircuits[: len(allparts)]
1463  ]
1464  L, fusion_info = ilp_global_optimal(allparts, g, weights=weights)
1465  struct_idxs = list(L)
1467  go,
1468  rgo,
1469  single_qubit_chains,
1470  gate_to_tqubit,
1471  [allparts[i] for i in L],
1472  fusion_info,
1473  surrounded_only=True,
1474  )
1475  single_qubit_chain_idx = {
1476  frozenset(chain): idx + len(allparts)
1477  for idx, chain in enumerate(single_qubit_chains)
1478  }
1479  for extrapart in parts[len(struct_idxs) :]:
1480  struct_idxs.append(single_qubit_chain_idx[frozenset(extrapart)])
1481  L = topo_sort_partitions(circ, parts)
1482  return [optimized_subcircuits[struct_idxs[i]] for i in L], [
1483  optimized_parameter_list[struct_idxs[i]] for i in L
1484  ]
1485 
1486  def OptimizeWideCircuit(
1487  self, circ: Circuit, parameters: np.ndarray
1488  ) -> Tuple[Circuit, np.ndarray]:
1489  """Top-level wide-circuit pass: optional routing, then Qiskit / BQSKit / Squander partition optimization.
1490 
1491  Sets ``self.config`` timing and intermediate circuit keys (e.g. ``routed_circuit``, ``optimization_time``).
1492  """
1493  if not qgd_Wide_Circuit_Optimization.is_valid_routing(
1494  circ, self.config["topology"]
1495  ):
1496 
1497  print("fixing topology in the circuit")
1498  topo = self.config["topology"]
1499  self.config["topology"] = None
1500  strat = self.config["strategy"]
1501  self.config["strategy"] = self.config["pre-opt-strategy"]
1502 
1503  print("Optimizing circuit with all-to-all (a2a) connectivity")
1504  circ, parameters = self.OptimizeWideCircuit(circ, parameters)
1505  self.config["all_to_all_optimization_time"] = self.config[
1506  "optimization_time"
1507  ]
1508  self.config["all_to_all_circuit"] = circ
1509  self.config["all_to_all_parameters"] = parameters
1510  self.config["strategy"] = strat
1511  self.config["topology"] = topo
1512  start_time = time.time()
1513 
1514  print("Routing circuit to fix the topology")
1515  circ, parameters = self.route_circuit(circ, parameters)
1516  self.config["routing_time"] = time.time() - start_time
1517  self.config["routed_circuit"] = circ
1518  self.config["routed_parameters"] = parameters
1519  else:
1520  if self.config["topology"] is not None:
1521  print("No additional routing is needed on the circuit")
1522 
1523  start_time = time.time()
1524  if self.config["strategy"] == "bqskit":
1525  print("Optimizing circuit with BQSkit")
1526  from squander import Qiskit_IO
1527  from bqskit import compile
1528 
1529  from bqskit.compiler.machine import MachineModel
1530  from bqskit.compiler import Compiler
1531  from bqskit.ir.lang.qasm2 import OPENQASM2Language
1532  from qiskit import qasm2, QuantumCircuit
1533 
1534  from bqskit.passes import SetModelPass
1535  from bqskit.compiler.compile import (
1536  build_multi_qudit_retarget_workflow,
1537  build_resynthesis_optimization_workflow,
1538  build_single_qudit_retarget_workflow,
1539  build_gate_deletion_optimization_workflow,
1540  LogErrorPass,
1541  )
1542 
1543  # Build BQSKit machine model from your topology
1544  model = MachineModel(circ.get_Qbit_Num(), self.config["topology"])
1545 
1546  # Convert squander circuit → qiskit → BQSKit
1547  # (BQSKit has a from_qiskit helper if you go via Qiskit IR)
1548  circo = Qiskit_IO.get_Qiskit_Circuit(
1549  circ, np.asarray(parameters, dtype=np.float64)
1550  )
1551 
1552  bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
1553 
1554  compilation_workflow = [
1555  SetModelPass(model), # attach hardware model to circuit
1556  build_multi_qudit_retarget_workflow(
1557  4, max_synthesis_size=self.max_partition_size
1558  ),
1559  build_resynthesis_optimization_workflow(
1560  4, max_synthesis_size=self.max_partition_size, iterative=True
1561  ),
1562  build_single_qudit_retarget_workflow(
1563  4, max_synthesis_size=self.max_partition_size
1564  ),
1565  build_gate_deletion_optimization_workflow(
1566  4, max_synthesis_size=self.max_partition_size, iterative=True
1567  ),
1568  LogErrorPass(),
1569  ]
1570 
1571  with Compiler() as compiler:
1572  routed_bqskit_circ, pass_data = compiler.compile(
1573  bqskit_circ, compilation_workflow, True
1574  )
1575 
1576  default = list(range(bqskit_circ.num_qudits))
1577  initial_map = pass_data.get("initial_mapping", default)
1578  final_map = pass_data.get("final_mapping", default)
1579 
1580  # Convert back: BQSKit → Qiskit → Squander
1581  circuit_qiskit = QuantumCircuit.from_qasm_str(
1582  OPENQASM2Language().encode(routed_bqskit_circ)
1583  )
1584  newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1585  circuit_qiskit
1586  )
1587 
1588  qgd_Wide_Circuit_Optimization.check_valid_routing(
1589  newcirc, self.config["topology"]
1590  )
1591  print("OptimizeWideCircuit::check_compare_circuits")
1592  self.check_compare_circuits(circ, parameters, newcirc, newparameters)
1593  circ, parameters = newcirc, newparameters
1594 
1595  elif self.config["strategy"] == "qiskit":
1596  print("Optimizing circuit with Qiskit")
1597  from squander import Qiskit_IO
1598  from qiskit import transpile
1599  from qiskit.transpiler import CouplingMap
1600  from squander.gates import gates_Wrapper as gate
1601 
1602  SUPPORTED_GATES_NAMES = {
1603  n.lower().replace("cnot", "cx")
1604  for n in dir(gate)
1605  if not n.startswith("_")
1606  and issubclass(getattr(gate, n), gate.Gate)
1607  and n not in ("Gate", "CROT", "CR", "SYC", "CCX", "CSWAP")
1608  }
1609  circo = Qiskit_IO.get_Qiskit_Circuit(
1610  circ, np.asarray(parameters, dtype=np.float64)
1611  )
1612  coupling_map = (
1613  None
1614  if self.config["topology"] is None
1615  else CouplingMap([[i, j] for i, j in self.config["topology"]])
1616  )
1617  circuit_qiskit = transpile(
1618  circo,
1619  basis_gates=SUPPORTED_GATES_NAMES,
1620  coupling_map=coupling_map,
1621  optimization_level=3,
1622  )
1623  newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1624  circuit_qiskit
1625  )
1626  qgd_Wide_Circuit_Optimization.check_valid_routing(
1627  newcirc, self.config["topology"]
1628  )
1629  print("OptimizeWideCircuit::check_compare_circuits")
1630  self.check_compare_circuits(circ, parameters, newcirc, newparameters)
1631  circ, parameters = newcirc, newparameters
1632  else:
1633 
1634  print("Optimizing circuit with Squander")
1635  part_size_start = self.max_partition_size
1636  part_size_end = self.max_partition_size
1637  if self.config.get("auto_expand_partition_size", True) and (
1638  self.config.get("use_osr", False)
1639  or self.config.get("use_graph_search", False)
1640  ):
1641  part_size_end = min(4, circ.get_Qbit_Num())
1642  count = CNOTGateCount(circ, 0)
1643  fingerprint_dict = {}
1644  for max_part_size in range(part_size_start, part_size_end + 1):
1645  # instantiate the object for optimizing wide circuits
1646  wide_circuit_optimizer = qgd_Wide_Circuit_Optimization(
1647  {**self.config, "max_partition_size": max_part_size}
1648  )
1649  while True:
1650  # run circuit optimization
1651  circ_flat, parameters = (
1652  wide_circuit_optimizer.InnerOptimizeWideCircuit(
1653  circ, parameters, fingerprint_dict=fingerprint_dict
1654  )
1655  )
1656  circ = circ_flat.get_Flat_Circuit()
1657  newcount = CNOTGateCount(circ, 0)
1658  no_improve = newcount >= count
1659  count = newcount
1660  if no_improve:
1661  break
1662  self.config["optimization_time"] = time.time() - start_time
1663  return circ, parameters
1664 
1666  self, circ: Circuit, orig_parameters: np.ndarray, fingerprint_dict=None
1667  ) -> Tuple[Circuit, np.ndarray]:
1668  """Optimize one pass of wide-circuit partition decomposition.
1669 
1670  The circuit is converted to a CNOT basis, partitioned, each partition is
1671  optimized (possibly in parallel), and then reconstructed into one circuit.
1672 
1673  Args:
1674  circ: Input circuit to optimize.
1675  orig_parameters: Parameter array associated with ``circ``.
1676  fingerprint_dict: Optional decomposition cache shared across passes.
1677 
1678  Returns:
1679  Tuple of ``(optimized_circuit, optimized_parameters)``.
1680  """
1681  from squander.utils import circuit_to_CNOT_basis
1682 
1683  circ, orig_parameters = circuit_to_CNOT_basis(circ, orig_parameters)
1684  max_gates = sum(
1685  y for x, y in circ.get_Gate_Nums().items() if CNOT_COUNT_DICT.get(x, -1) <= 0
1686  )
1687 
1688  global_min = self.config.get("global_min", True)
1689  if global_min:
1690  partitioned_circuit, parameters, recombine_info, part_deps = (
1691  qgd_Wide_Circuit_Optimization.make_all_partition_circuit(
1692  circ, orig_parameters, self.max_partition_size
1693  )
1694  )
1695 
1696  else:
1697  partitioned_circuit, parameters, _ = PartitionCircuit(
1698  circ,
1699  orig_parameters,
1700  self.max_partition_size,
1701  strategy=self.config["partition_strategy"],
1702  )
1703  part_deps = None
1704 
1705  subcircuits = partitioned_circuit.get_Gates()
1706 
1707  # subcircuits = subcircuits[9:10]
1708 
1709  in_parent = parent_process() is not None
1710 
1711  if not in_parent:
1712  print(len(subcircuits), "partitions found to optimize")
1713 
1714  # the list of optimized subcircuits
1715  optimized_subcircuits: List[Optional[Circuit]] = [None] * len(subcircuits)
1716 
1717  # the list of parameters associated with the optimized subcircuits
1718  optimized_parameter_list: List[Optional[List[np.ndarray]]] = [None] * len(
1719  subcircuits
1720  )
1721 
1722  # list of AsyncResult objects
1723  async_results = [None] * len(subcircuits)
1724 
1725  total_opt = [0]
1726 
1727  def process_result(partition_idx):
1728  """Finalize async decomposition for partition ``partition_idx`` and update caches / lists."""
1729  if optimized_subcircuits[partition_idx] is not None:
1730  return
1731  subcircuit = subcircuits[partition_idx]
1732  # callback on the master process to compare the decomposed and original subcircuit
1733  start_idx = subcircuit.get_Parameter_Start_Index()
1734  subcircuit_parameters = parameters[
1735  start_idx : start_idx + subcircuit.get_Parameter_Num()
1736  ]
1737  fingerprint = (
1738  None
1739  if fingerprint_dict is None
1740  else qgd_Wide_Circuit_Optimization.get_fingerprint(
1741  subcircuit, subcircuit_parameters
1742  )
1743  )
1744  callback_fnc = lambda x: self.CompareAndPickCircuits(
1745  [subcircuit, *(z[0] for z in x)],
1746  [subcircuit_parameters, *(z[1] for z in x)],
1747  lambda c: CNOTGateCount(c, max_gates),
1748  )
1749  if fingerprint_dict is not None and fingerprint in fingerprint_dict:
1750  new_subcircuit, new_parameters = fingerprint_dict[fingerprint]
1751  else:
1752  new_subcircuit, new_parameters = callback_fnc(
1753  async_results[partition_idx][0](*async_results[partition_idx][1])
1754  if in_parent
1755  else async_results[partition_idx].get(timeout=None)
1756  )
1757 
1758  if subcircuit != new_subcircuit:
1759  print(
1760  "original subcircuit: ",
1761  subcircuit.get_Gate_Nums(),
1762  partition_idx,
1763  )
1764  print("reoptimized subcircuit: ", new_subcircuit.get_Gate_Nums())
1765  if fingerprint_dict is not None:
1766  fingerprint_dict[fingerprint] = (new_subcircuit, new_parameters)
1767  fingerprint_dict[
1768  qgd_Wide_Circuit_Optimization.get_fingerprint(
1769  new_subcircuit, new_parameters
1770  )
1771  ] = (new_subcircuit, new_parameters)
1772  trim_subcirc, trim_parameters = (
1773  qgd_Wide_Circuit_Optimization.strip_single_qubit_head_tails(
1774  new_subcircuit, new_parameters
1775  )
1776  )
1777  fingerprint_dict[
1778  qgd_Wide_Circuit_Optimization.get_fingerprint(
1779  trim_subcirc, trim_parameters
1780  )
1781  ] = (trim_subcirc, trim_parameters)
1782  if total_opt[0] % 100 == 99:
1783  print(total_opt[0] + 1, "partitions optimized")
1784  total_opt[0] += 1
1785  optimized_subcircuits[partition_idx] = new_subcircuit
1786  optimized_parameter_list[partition_idx] = new_parameters
1787 
1788  with (
1789  contextlib.nullcontext() if in_parent else Pool(processes=mp.cpu_count())
1790  ) as pool:
1791  remaining = list(range(len(subcircuits)))
1792  while remaining:
1793  still_remaining = []
1794  # code for iterate over partitions and optimize them
1795  for partition_idx in remaining:
1796  subcircuit = subcircuits[partition_idx]
1797 
1798  # isolate the parameters corresponding to the given sub-circuit
1799  start_idx = subcircuit.get_Parameter_Start_Index()
1800  end_idx = start_idx + subcircuit.get_Parameter_Num()
1801  subcircuit_parameters = parameters[start_idx:end_idx]
1802 
1803  fingerprint = (
1804  None
1805  if fingerprint_dict is None
1806  else qgd_Wide_Circuit_Optimization.get_fingerprint(
1807  subcircuit, subcircuit_parameters
1808  )
1809  )
1810  if fingerprint_dict is not None and fingerprint in fingerprint_dict:
1811  (
1812  optimized_subcircuits[partition_idx],
1813  optimized_parameter_list[partition_idx],
1814  ) = fingerprint_dict[fingerprint]
1815  continue
1816  if part_deps is not None and partition_idx in part_deps:
1817  any_optimized, any_remaining = False, False
1818  for dep_idx in part_deps[partition_idx]:
1819  if optimized_subcircuits[dep_idx] is None and (
1820  async_results[dep_idx] is None
1821  or not isinstance(async_results[dep_idx], tuple)
1822  and not async_results[dep_idx].ready()
1823  ):
1824  any_remaining = True
1825  continue
1826  elif optimized_subcircuits[dep_idx] is None:
1827  process_result(dep_idx)
1828 
1829  optimized_subcircuits_loc = optimized_subcircuits[dep_idx]
1830  assert isinstance(optimized_subcircuits_loc, Circuit)
1831  assert optimized_subcircuits_loc is not None
1832 
1833  if CNOTGateCount(optimized_subcircuits_loc) < CNOTGateCount(
1834  subcircuits[dep_idx]
1835  ): # if the dependency partition was optimized, skip
1836  any_optimized = True
1837  break
1838  if any_optimized:
1839  optimized_subcircuits[partition_idx] = subcircuit
1840  optimized_parameter_list[partition_idx] = (
1841  subcircuit_parameters
1842  )
1843  continue
1844  if any_remaining:
1845  still_remaining.append(partition_idx)
1846  continue
1847  # call a process to decompose a subcircuit
1848  config = {
1849  **self.config,
1850  "tree_level_max": qgd_Wide_Circuit_Optimization.partition_tree_level_max(
1851  self.config, subcircuit
1852  ),
1853  }
1854  fargs = (
1856  (subcircuit, subcircuit_parameters, config, None),
1857  )
1858  # print("Dispatching", subcircuit.get_Involved_Qubits(), "qubits with", CNOGateCount(subcircuit, 0), "CNOT gates, partition ", partition_idx)
1859  async_results[partition_idx] = (
1860  fargs if in_parent else pool.apply_async(*fargs) # type: ignore[union-attr]
1861  )
1862  if len(remaining) == len(still_remaining):
1863  time.sleep(0.1)
1864  remaining = still_remaining
1865  # code for iterate over async results and retrieve the new subcircuits
1866  for partition_idx in range(len(subcircuits)):
1867  process_result(partition_idx)
1868 
1869  # construct the wide circuit from the optimized subcircuits
1870  if global_min:
1871  optimized_subcircuits, optimized_parameter_list = (
1872  qgd_Wide_Circuit_Optimization.recombine_all_partition_circuit(
1873  circ,
1874  optimized_subcircuits,
1875  optimized_parameter_list,
1876  recombine_info,
1877  )
1878  )
1879 
1880  if any(c is None for c in optimized_subcircuits) or any(
1881  p is None for p in optimized_parameter_list
1882  ):
1883  raise RuntimeError(
1884  "Internal error: some partitions were not optimized before reconstruction."
1885  )
1886  wide_circuit, wide_parameters = self.ConstructCircuitFromPartitions(
1887  cast(List[Circuit], optimized_subcircuits),
1888  cast(List[List[np.ndarray]], optimized_parameter_list),
1889  )
1890 
1891  if not in_parent:
1892  print("original circuit: ", circ.get_Gate_Nums())
1893  print("reoptimized circuit: ", wide_circuit.get_Gate_Nums())
1894 
1895  qgd_Wide_Circuit_Optimization.check_valid_routing(
1896  wide_circuit, self.config["topology"]
1897  )
1899  circ,
1900  orig_parameters,
1901  wide_circuit,
1902  wide_parameters,
1903  label="InnerOptimizeWideCircuit",
1904  )
1905 
1906  return wide_circuit, wide_parameters
1907 
1908  @staticmethod
1909  def all_to_all_topology(num_qubits):
1910  """Undirected all-to-all coupler list for ``num_qubits`` qubits."""
1911  return [(i, j) for i in range(num_qubits) for j in range(i + 1, num_qubits)]
1912 
1913  @staticmethod
1914  def linear_topology(num_qubits):
1915  """Path graph couplers ``(i, i+1)``."""
1916  return [(i, i + 1) for i in range(num_qubits - 1)]
1917 
1918  @staticmethod
1919  def star_topology(num_qubits):
1920  """Star graph: hub qubit ``0`` connected to all others."""
1921  return [(0, i) for i in range(1, num_qubits)]
1922 
1923  @staticmethod
1924  def ring_topology(num_qubits):
1925  """Ring couplers including wrap-around ``(n-1, 0)``."""
1926  return [(i, (i + 1) % num_qubits) for i in range(num_qubits)]
1927 
1928  @staticmethod
1929  def lattice_topology(x_qbits, y_qbits):
1930  """2D grid of size ``x_qbits`` by ``y_qbits`` with nearest-neighbor horizontal and vertical edges."""
1931  return [
1932  (i * x_qbits + j, i * x_qbits + (j + 1))
1933  for i in range(y_qbits)
1934  for j in range(x_qbits - 1)
1935  ] + [
1936  (i * x_qbits + j, (i + 1) * x_qbits + j)
1937  for i in range(y_qbits - 1)
1938  for j in range(x_qbits)
1939  ]
1940 
1941  @staticmethod
1942  def heavy_hexagonal_topology(rows, cols):
1943  """Build a finite heavy-hex coupling list (honeycomb with subdivided edges).
1944 
1945  Args:
1946  rows: Number of rows in the brick-wall honeycomb patch.
1947  cols: Number of columns in the patch.
1948 
1949  Returns:
1950  List of undirected edges ``(u, v)``. The first ``rows * cols`` qubit
1951  indices are honeycomb vertices; each original edge introduces one
1952  additional degree-2 qubit on the subdivided link.
1953  """
1954 
1955  def vid(r, c):
1956  """Linear index for honeycomb vertex at row ``r``, column ``c``."""
1957  return r * cols + c
1958 
1959  # Underlying honeycomb / brick-wall edges
1960  base_edges = []
1961 
1962  for r in range(rows):
1963  for c in range(cols):
1964  # Vertical brick-wall edges
1965  if r + 1 < rows:
1966  base_edges.append((vid(r, c), vid(r + 1, c)))
1967 
1968  # Alternating horizontal edges
1969  if c + 1 < cols and ((r + c) % 2 == 0):
1970  base_edges.append((vid(r, c), vid(r, c + 1)))
1971 
1972  # Subdivide every honeycomb edge by inserting a qubit
1973  next_id = rows * cols
1974  heavy_edges = []
1975 
1976  for u, v in base_edges:
1977  w = next_id
1978  next_id += 1
1979  heavy_edges.append((u, w))
1980  heavy_edges.append((w, v))
1981 
1982  return heavy_edges
1983 
1984  @staticmethod
1986  """Approximate Sycamore-like 6x9 grid topology (simplified; ignores known dead qubits)."""
1987  return qgd_Wide_Circuit_Optimization.lattice_topology(
1988  6, 9
1989  ) # there is a defective qubit at (0, 3) in the sycamore chip, but we ignore it here for simplicity
1990 
1991  @staticmethod
1992  def is_valid_routing(wide_circuit, topo):
1993  """True if every multi-qubit gate's qubits lie in a connected subgraph of undirected ``topo``."""
1994  if topo is None:
1995  return True
1996 
1997  import itertools
1998 
1999  topo_set = {frozenset(edge) for edge in topo}
2000 
2001  def qubits_connected(qubits):
2002  """Whether pairwise couplers in ``topo_set`` connect all qubits in ``qubits``."""
2003  if len(qubits) <= 1:
2004  return True
2005  edges = {
2006  frozenset((q1, q2))
2007  for q1, q2 in itertools.combinations(qubits, 2)
2008  if frozenset((q1, q2)) in topo_set
2009  }
2010  if len(edges) == 0:
2011  return False
2012  cur_set = set(edges.pop())
2013  while edges:
2014  next_edge = next((e for e in edges if len(e & cur_set) > 0), None)
2015  if next_edge is None:
2016  return False
2017  cur_set |= next_edge
2018  edges.remove(next_edge)
2019  return set(qubits) <= cur_set
2020 
2021  return all(
2022  qubits_connected(gate.get_Involved_Qbits())
2023  for gate in wide_circuit.get_Flat_Circuit().get_Gates()
2024  if len(gate.get_Involved_Qbits()) > 1
2025  )
2026 
2027  @staticmethod
2028  def check_valid_routing(wide_circuit, topo):
2029  """Assert ``is_valid_routing``; raises if any gate violates ``topo``."""
2030  if not qgd_Wide_Circuit_Optimization.is_valid_routing(wide_circuit, topo):
2031  import itertools, sys
2032  topo_set = {frozenset(e) for e in topo}
2033  for gate in wide_circuit.get_Flat_Circuit().get_Gates():
2034  qbits = gate.get_Involved_Qbits()
2035  if len(qbits) <= 1:
2036  continue
2037  edges = {frozenset((q1,q2)) for q1,q2 in itertools.combinations(qbits,2) if frozenset((q1,q2)) in topo_set}
2038  if not edges:
2039  sys.stderr.write(f'ROUTING_VIOLATION: {type(gate).__name__} on {qbits} topo={topo}\n')
2040  sys.stderr.flush()
2041  break
2042  raise AssertionError("Final circuit contains gates that do not respect the routing constraints.")
2043 
2045  self,
2046  circ,
2047  orig_parameters,
2048  wide_circuit,
2049  wide_parameters,
2050  routing=False,
2051  forced_test=False,
2052  label=None,
2053  ):
2054  """Optionally verify equivalence of ``circ`` and ``wide_circuit`` via ``CompareCircuits``.
2055 
2056  Args:
2057  circ: Original circuit.
2058  orig_parameters: Parameters for ``circ``.
2059  wide_circuit: Optimized or routed circuit.
2060  wide_parameters: Parameters for ``wide_circuit``.
2061  routing: If true and initial/final mappings exist in ``self.config``,
2062  pass them to ``CompareCircuits`` for layout-aware comparison.
2063  forced_test: If true, run the comparison even when ``test_final_circuit``
2064  is false in config.
2065  """
2066  forced_test = forced_test or (
2067  self.config.get("force_small_circuit_validation", True)
2068  and circ.get_Qbit_Num() <= 12
2069  )
2070  if self.config["test_final_circuit"] or forced_test:
2071  if label is not None:
2072  print(f"{label}: check_compare_circuits")
2073  tolerance = _squander_validation_tolerance(self.config)
2074  if (
2075  routing
2076  and self.config.get("initial_mapping", None) is not None
2077  and self.config.get("final_mapping", None) is not None
2078  ):
2080  circ,
2081  orig_parameters,
2082  wide_circuit,
2083  wide_parameters,
2084  initial_mapping=self.config["initial_mapping"],
2085  final_mapping=self.config["final_mapping"],
2086  tolerance=tolerance,
2087  parallel=0,
2088  )
2089  else:
2091  circ,
2092  orig_parameters,
2093  wide_circuit,
2094  wide_parameters,
2095  tolerance=tolerance,
2096  )
2097 
2098  def route_circuit(self, circ: Circuit, orig_parameters: np.ndarray):
2099  """Map ``circ`` onto ``self.config['topology']`` using the configured router.
2100 
2101  The strategy is ``self.config['routing-strategy']``, e.g. ``seqpam-ilp``,
2102  ``seqpam-quick``, ``bqskit-sabre``, ``light-sabre`` (Qiskit), or ``sabre``
2103  (Squander). Writes ``initial_mapping`` and ``final_mapping`` into
2104  ``self.config`` when the backend provides them.
2105 
2106  Args:
2107  circ: Circuit before routing.
2108  orig_parameters: Parameter vector for ``circ``.
2109 
2110  Returns:
2111  ``(routed_circuit, routed_parameters)`` laid out for ``self.config['topology']``.
2112  """
2113  strategy = self.config.get("routing-strategy", "seqpam-ilp")
2114 
2115  if strategy in ("seqpam-ilp", "seqpam-quick", "bqskit-sabre"):
2116  from squander import Qiskit_IO
2117  import bqskit.compiler.compile as bqskit_compile_module
2118  from bqskit.compiler import Compiler
2119  from bqskit.compiler.compile import (
2120  build_sabre_mapping_workflow,
2121  build_seqpam_mapping_optimization_workflow,
2122  )
2123 
2124  from bqskit.passes import (
2125  SetModelPass,
2126  )
2127  from bqskit.compiler.machine import MachineModel
2128  from bqskit.ir.lang.qasm2 import OPENQASM2Language
2129  from qiskit import qasm2, QuantumCircuit
2130 
2131  # Build BQSKit machine model from your topology
2132  model = MachineModel(circ.get_Qbit_Num(), self.config["topology"])
2133 
2134  # Convert squander circuit → qiskit → BQSKit
2135  # (BQSKit has a from_qiskit helper if you go via Qiskit IR)
2136  circo = Qiskit_IO.get_Qiskit_Circuit(
2137  circ, np.asarray(orig_parameters, dtype=np.float64)
2138  )
2139 
2140  bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
2141  # Customizable knobs
2142 
2143  if strategy == "seqpam-ilp":
2144  # Routing-only SEQPAM pass pipeline. Patch the classes BQSKit's
2145  # workflow factory instantiates, so we do not depend on the private
2146  # shape of the returned Workflow.
2148  bqskit_compile_module,
2149  use_squander_partitioner=True,
2150  config=self.config,
2151  ):
2152  mainflow = build_seqpam_mapping_optimization_workflow(
2153  block_size=3 # SEQPAM uses 3-qubit blocks only
2154  )
2155  elif strategy == "seqpam-quick":
2156  # Keep BQSKit's QuickPartitioner, but replace QSearch/LEAP so
2157  # routing does not synthesize/decompose partition blocks.
2159  bqskit_compile_module,
2160  use_squander_partitioner=False,
2161  config=self.config,
2162  ):
2163  mainflow = build_seqpam_mapping_optimization_workflow(
2164  block_size=3 # SEQPAM uses 3-qubit blocks only
2165  )
2166  elif strategy == "bqskit-sabre":
2167  mainflow = build_sabre_mapping_workflow()
2168  else:
2169  raise ValueError(f"Unsupported BQSKit routing strategy: {strategy}")
2170 
2171  routing_workflow = [
2172  SetModelPass(model), # attach hardware model to circuit
2173  mainflow,
2174  ]
2175 
2176  # EAPP monkey-patch catches Squander OSR failures per permutation
2177  # and installs a SWAP-correct fallback in BQSKit worker processes.
2178  import os as _os, json as _json
2179  old_patch_env = _os.environ.get('_SQUANDER_EAPP_FALLBACK_PATCH')
2180  old_config_env = _os.environ.get('_SQUANDER_BQSKIT_CONFIG')
2181  _os.environ['_SQUANDER_EAPP_FALLBACK_PATCH'] = '1'
2182  _os.environ['_SQUANDER_BQSKIT_CONFIG'] = _json.dumps(
2184  )
2186  try:
2187  with Compiler() as compiler:
2188  routed_bqskit_circ, pass_data = compiler.compile(
2189  bqskit_circ, routing_workflow, True
2190  )
2191  finally:
2192  if old_patch_env is None:
2193  _os.environ.pop('_SQUANDER_EAPP_FALLBACK_PATCH', None)
2194  else:
2195  _os.environ['_SQUANDER_EAPP_FALLBACK_PATCH'] = old_patch_env
2196  if old_config_env is None:
2197  _os.environ.pop('_SQUANDER_BQSKIT_CONFIG', None)
2198  else:
2199  _os.environ['_SQUANDER_BQSKIT_CONFIG'] = old_config_env
2200 
2201  # Convert back: BQSKit → Qiskit → Squander
2202  circuit_qiskit_routed = QuantumCircuit.from_qasm_str(
2203  OPENQASM2Language().encode(routed_bqskit_circ)
2204  )
2205  Squander_remapped_circuit, parameters_remapped_circuit = (
2206  Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_routed)
2207  )
2208  self.config["initial_mapping"] = list(pass_data.initial_mapping)
2209  self.config["final_mapping"] = list(pass_data.final_mapping)
2210 
2211  elif strategy == "light-sabre":
2212  from squander import Qiskit_IO
2213  from qiskit import transpile
2214  from qiskit.transpiler.preset_passmanagers import (
2215  generate_preset_pass_manager,
2216  )
2217  from qiskit.transpiler.passes import SabreLayout, SabreSwap
2218  from qiskit.transpiler import PassManager, CouplingMap
2219  from squander.gates import gates_Wrapper as gate
2220 
2221  # SUPPORTED_GATES_NAMES = {n.lower().replace("cnot", "cx") for n in dir(gate) if not n.startswith("_") and issubclass(getattr(gate, n), gate.Gate) and n not in ("Gate", "CROT", "CR", "SYC", "CCX", "CSWAP")}
2222  circo = Qiskit_IO.get_Qiskit_Circuit(
2223  circ, np.asarray(orig_parameters, dtype=np.float64)
2224  )
2225  coupling_map = [[i, j] for i, j in self.config["topology"]]
2226  # circuit_qiskit_sabre = transpile(circo, basis_gates=SUPPORTED_GATES_NAMES, coupling_map=coupling_map, optimization_level=0)
2227  coupling_map = CouplingMap(coupling_map)
2228  # Customizable SABRE parameters
2229  sabre_seed = self.config.get("sabre_seed", 42)
2230  sabre_trials = self.config.get("sabre_trials", 5) # layout trials
2231  swap_trials = self.config.get("sabre_swap_trials", sabre_trials)
2232  heuristic = self.config.get(
2233  "sabre_heuristic", "decay"
2234  ) # "basic" | "lookahead" | "decay"
2235 
2236  layout_pass = SabreLayout(
2237  coupling_map,
2238  seed=sabre_seed,
2239  max_iterations=sabre_trials,
2240  swap_trials=swap_trials,
2241  )
2242  swap_pass = SabreSwap(
2243  coupling_map,
2244  heuristic=heuristic,
2245  seed=sabre_seed,
2246  trials=swap_trials,
2247  )
2248 
2249  pm = PassManager(
2250  [
2251  layout_pass, # find initial qubit mapping via SABRE
2252  swap_pass, # insert SWAP gates for routing
2253  ]
2254  )
2255  circuit_qiskit_sabre = pm.run(circo)
2256  Squander_remapped_circuit, parameters_remapped_circuit = (
2257  Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_sabre)
2258  )
2259  self.config["initial_mapping"] = (
2260  circuit_qiskit_sabre.layout.initial_index_layout()
2261  )
2262  self.config["final_mapping"] = (
2263  circuit_qiskit_sabre.layout.final_index_layout()
2264  )
2265  elif strategy == "sabre":
2266  sabre = SABRE(circ, self.config["topology"])
2267  (
2268  Squander_remapped_circuit,
2269  parameters_remapped_circuit,
2270  pi,
2271  final_pi,
2272  swap_count,
2273  ) = sabre.map_circuit(orig_parameters)
2274  self.config["initial_mapping"] = pi
2275  self.config["final_mapping"] = final_pi
2276  qgd_Wide_Circuit_Optimization.check_valid_routing(
2277  Squander_remapped_circuit, self.config["topology"]
2278  )
2279 
2280  print("checking circuit after routing")
2281  print(self.config)
2283  circ,
2284  orig_parameters,
2285  Squander_remapped_circuit,
2286  parameters_remapped_circuit,
2287  routing=True,
2288  forced_test=True,
2289  label="route_circuit",
2290  )
2291  return Squander_remapped_circuit, parameters_remapped_circuit
def recombine_all_partition_circuit(circ, optimized_subcircuits, optimized_parameter_list, recombine_info)
A base class to determine the decomposition of an N-qubit unitary into a sequence of CNOT and U3 gate...
def topo_sort_partitions(c, parts)
Definition: ilp.py:55
def _squander_synthesize_or_fallback(inner_synthesis, target, target_data, original_circuit, graph, pi, po)
A base class to determine the decomposition of an N-qubit unitary into a sequence of CNOT and U3 gate...
def _fallback_circuit_for_permutation(original_circuit, graph, pi, po)
def get_Qbit_Num(self)
Call to get the number of qubits in the circuit.
def PartitionCircuit
Definition: partition.py:51
def get_Parameter_Num(self)
Call to get the number of free parameters in the gate structure used for the decomposition.
A base class to determine the decomposition of an N-qubit unitary into a sequence of CNOT and U3 gate...
def get_all_partitions(c, max_qubits_per_partition)
Definition: ilp.py:859
def _get_topo_order(g, rg, gate_to_qubit)
Definition: ilp.py:240
def ilp_global_optimal(allparts, g, weighted_info=None, gurobi_direct=False, use_order=False, weights=None)
Definition: ilp.py:600
def patched_seqpam_workflow_classes(bqskit_compile_module, use_squander_partitioner, config)
def check_compare_circuits(self, circ, orig_parameters, wide_circuit, wide_parameters, routing=False, forced_test=False, label=None)
def recombine_single_qubit_chains(g, rg, single_qubit_chains, gate_to_tqubit, L, fusion_info, surrounded_only=False)
Definition: ilp.py:309
def circuit_to_CNOT_basis
Definition: utils.py:456
def build_dependency
Definition: tools.py:136
def CompareCircuits
Definition: utils.py:164