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