Sequential Quantum Gate Decomposer  v1.9.7
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 weighted scalar score:
875  ``two_qubit_cost * max_gates + single_qubit_gate_count``.
876  This is lexicographic only when ``max_gates`` is strictly greater than the
877  largest possible difference in single-qubit counts.
878 
879  Args:
880  circ: Squander circuit representation.
881  max_gates: Weight multiplier for the two-qubit cost term.
882 
883  Returns:
884  Integer gate-cost score used by optimization heuristics.
885  """
886  assert isinstance(circ, Circuit), \
887  "The input parameters should be an instance of Squander Circuit"
888  gate_counts = circ.get_Gate_Nums()
889  num_cnots = sum(
890  CNOT_COUNT_DICT.get(gate, 0) * count for gate, count in gate_counts.items()
891  )
892  if max_gates > 0:
893  return num_cnots * max_gates + sum(
894  y for x, y in gate_counts.items() if CNOT_COUNT_DICT.get(x, -1) <= 0
895  )
896  return num_cnots
897 
898 
899 def SingleQubitGateCount(circ: Circuit) -> int:
900  """Count single-qubit gates in a circuit (U3, H, RX, RY, RZ, etc.).
901 
902  Uses _GATE_DECOMPOSITION to count non-CNOT gates in each gate's breakdown.
903 
904  Args:
905  circ: Squander circuit representation.
906 
907  Returns:
908  Total number of single-qubit gate operations when fully decomposed.
909  """
910  gate_counts = circ.get_Gate_Nums()
911  total = 0
912  for gate, count in gate_counts.items():
913  decomp = _GATE_DECOMPOSITION.get(gate, {})
914  total += count * sum(v for k, v in decomp.items() if k != "CNOT")
915  return total
916 
917 
918 def TotalRawGateCount(circ: Circuit) -> int:
919  """Total number of raw gate operations (single-qubit + multi-qubit).
920 
921  Args:
922  circ: Squander circuit representation.
923 
924  Returns:
925  Total gate operation count.
926  """
927  return sum(circ.get_Gate_Nums().values())
928 
929 
930 def CircuitGateStats(circ: Circuit) -> dict:
931  """Return comprehensive gate statistics for a circuit.
932 
933  Uses _GATE_DECOMPOSITION to compute fully-decomposed gate counts.
934 
935  Returns dict with keys: cnot_equiv, single_qubit, total_raw, qubits,
936  and gate_breakdown (per-gate-type raw counts).
937  """
938  gate_counts = circ.get_Gate_Nums()
939  cnot_equiv = sum(
940  CNOT_COUNT_DICT.get(g, 0) * c for g, c in gate_counts.items()
941  )
942  single = 0
943  for g, c in gate_counts.items():
944  decomp = _GATE_DECOMPOSITION.get(g, {})
945  single += c * sum(v for k, v in decomp.items() if k != "CNOT")
946  total = sum(gate_counts.values())
947  return {
948  "cnot_equiv": cnot_equiv,
949  "single_qubit": single,
950  "total_raw": total,
951  "qubits": circ.get_Qbit_Num(),
952  "gate_breakdown": dict(gate_counts),
953  }
954 
955 
957  """Optimize wide (many-qubit) circuits via partitioning and subcircuit decomposition.
958 
959  Supports multiple decomposition strategies, optional global recombination (ILP),
960  and routing when the circuit does not match the target topology.
961  """
962 
963  def __init__(self, config):
964  """Validate and store wide-circuit optimization ``config`` (strategy, topology, partitioning, tolerances)."""
965 
966  config.setdefault("strategy", "TreeSearch")
967  config.setdefault("parallel", 0)
968  config.setdefault("verbosity", 0)
969  config.setdefault("use_float", False)
970  config.setdefault("tolerance", _default_squander_tolerance(config))
971  config.setdefault(
972  "circuit_validation_tolerance",
974  )
975  config.setdefault(
976  "bqskit_synthesis_validation_tolerance",
978  )
979  config.setdefault("test_subcircuits", False)
980  config.setdefault("test_final_circuit", True)
981  config.setdefault("max_partition_size", 3)
982  config.setdefault("topology", None)
983  config.setdefault("partition_strategy", "ilp")
984  config.setdefault("auto_expand_partition_size", True)
985  config.setdefault("force_small_circuit_validation", True)
986 
987  # testing the fields of config
988  strategy = config["strategy"]
989  allowed_startegies = [
990  "TreeSearch",
991  "TabuSearch",
992  "Adaptive",
993  "qiskit",
994  "bqskit",
995  ]
996  if not strategy in allowed_startegies:
997  raise Exception(
998  f"The decomposition startegy should be either of {allowed_startegies}, got {strategy}."
999  )
1000 
1001  parallel = config["parallel"]
1002  allowed_parallel = [0, 1, 2]
1003  if not parallel in allowed_parallel:
1004  raise Exception(
1005  f"The parallel configuration should be either of {allowed_parallel}, got {parallel}."
1006  )
1007 
1008  verbosity = config["verbosity"]
1009  if not isinstance(verbosity, int):
1010  raise Exception(f"The verbosity parameter should be an integer.")
1011 
1012  tolerance = config["tolerance"]
1013  if not isinstance(tolerance, float):
1014  raise Exception(f"The tolerance parameter should be a float.")
1015 
1016  use_float = config["use_float"]
1017  if not isinstance(use_float, bool):
1018  raise Exception(f"The use_float parameter should be a bool.")
1019 
1020  bqskit_synthesis_validation_tolerance = config[
1021  "bqskit_synthesis_validation_tolerance"
1022  ]
1023  if not isinstance(bqskit_synthesis_validation_tolerance, float):
1024  raise Exception(
1025  "The bqskit_synthesis_validation_tolerance parameter should be a float."
1026  )
1027 
1028  circuit_validation_tolerance = config["circuit_validation_tolerance"]
1029  if not isinstance(circuit_validation_tolerance, float):
1030  raise Exception(
1031  "The circuit_validation_tolerance parameter should be a float."
1032  )
1033 
1034  test_subcircuits = config["test_subcircuits"]
1035  if not isinstance(test_subcircuits, bool):
1036  raise Exception(f"The test_subcircuits parameter should be a bool.")
1037 
1038  test_final_circuit = config["test_final_circuit"]
1039  if not isinstance(test_final_circuit, bool):
1040  raise Exception(f"The test_final_circuit parameter should be a bool.")
1041 
1042  max_partition_size = config["max_partition_size"]
1043  if not isinstance(max_partition_size, int):
1044  raise Exception(f"The max_partition_size parameter should be an integer.")
1045 
1046  self.config = config
1047 
1048  self.max_partition_size = max_partition_size
1049 
1050  @staticmethod
1051  def partition_tree_level_max(config, subcircuit, reduction=1):
1052  """Return the tree-search depth used for partition-local rewrites."""
1053 
1054  target_depth = max(0, CNOTGateCount(subcircuit, 0) - reduction)
1055  configured_limit = config.get("partition_tree_level_max", None)
1056  if configured_limit is None:
1057  configured_limit = target_depth
1058  return min(target_depth, int(configured_limit))
1059 
1061  self, circs: List[Circuit], parameter_arrs: List[List[np.ndarray]]
1062  ) -> Tuple[Circuit, np.ndarray]:
1063  """Concatenate optimized partition circuits into a single wide circuit.
1064 
1065  Args:
1066  circs: Partition circuits in execution order.
1067  parameter_arrs: Parameter arrays corresponding to ``circs``.
1068 
1069  Returns:
1070  Tuple of ``(wide_circuit, wide_parameters)``.
1071  """
1072 
1073  if not isinstance(circs, list):
1074  raise Exception("First argument should be a list of squander circuits")
1075 
1076  if not isinstance(parameter_arrs, list):
1077  raise Exception("Second argument should be a list of numpy arrays")
1078 
1079  if len(circs) != len(parameter_arrs):
1080  raise Exception("The first two arguments should be of the same length")
1081 
1082  qbit_num = circs[0].get_Qbit_Num()
1083 
1084  wide_parameters = np.concatenate(parameter_arrs, axis=0)
1085 
1086  wide_circuit = Circuit(qbit_num)
1087 
1088  for circ in circs:
1089  wide_circuit.add_Circuit(circ)
1090 
1091  assert (
1092  wide_circuit.get_Parameter_Num() == wide_parameters.size
1093  ), f"Mismatch in the number of parameters: {wide_circuit.get_Parameter_Num()} vs {wide_parameters.size}"
1094 
1095  return wide_circuit, wide_parameters
1096 
1097  @staticmethod
1098  def DecomposePartition(
1099  Umtx: np.ndarray, config: dict, mini_topology=None, structure=None
1100  ) -> list[tuple[Circuit, np.ndarray]]:
1101  """Decompose a unitary ``Umtx`` (e.g. from a partition) using ``config['strategy']``.
1102 
1103  Args:
1104  Umtx: Complex unitary matrix.
1105  config: Must include ``strategy``, ``tolerance``, ``verbosity``, etc.
1106  mini_topology: Optional hardware couplers for topology-aware decomposers.
1107  structure: Required gate structure when ``strategy == "Custom"``.
1108 
1109  Returns:
1110  Normally ``[(circuit, parameters)]`` on success, or ``[]`` if the
1111  decomposition error exceeds ``tolerance``. If
1112  ``config.get('stop_first_solution')`` is false, returns
1113  ``cDecompose.all_solutions`` from the underlying decomposer instead of
1114  a single best pair.
1115  """
1116  strategy = config["strategy"]
1117  if strategy == "TreeSearch":
1119  Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1120  )
1121  elif strategy == "TabuSearch":
1122  cDecompose = N_Qubit_Decomposition_Tabu_Search(
1123  Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1124  )
1125  elif strategy == "Adaptive":
1126  cDecompose = N_Qubit_Decomposition_adaptive(
1127  Umtx.conj().T,
1128  level_limit_max=5,
1129  level_limit_min=1,
1130  topology=mini_topology,
1131  )
1132  elif strategy == "Custom":
1133  cDecompose = N_Qubit_Decomposition_custom(
1134  Umtx.conj().T, config=config, accelerator_num=0
1135  )
1136  assert (
1137  structure is not None
1138  ), "Custom decomposition strategy requires a gate structure to be provided."
1139  cDecompose.set_Gate_Structure(structure)
1140  else:
1141  raise Exception(f"Unsupported decomposition type: {strategy}")
1142 
1143  tolerance = config["tolerance"]
1144  cDecompose.set_Verbose(config["verbosity"])
1145  cDecompose.set_Cost_Function_Variant(3)
1146  cDecompose.set_Optimization_Tolerance(tolerance)
1147 
1148  # adding new layer to the decomposition until threshold
1149  cDecompose.set_Optimizer("BFGS")
1150 
1151  # starting the decomposition
1152  try:
1153  cDecompose.Start_Decomposition()
1154  except Exception as e:
1155  # print(e)
1156  raise e
1157  # return []
1158  if not config.get("stop_first_solution", True):
1159  return cDecompose.all_solutions
1160 
1161  squander_circuit = cDecompose.get_Circuit()
1162  parameters = cDecompose.get_Optimized_Parameters()
1163  assert parameters is not None
1164 
1165  if strategy == "Custom":
1166  err = cDecompose.Optimization_Problem(parameters)
1167  it = 0
1168  while err > tolerance and it < 20:
1169  cDecompose.set_Optimized_Parameters(
1170  np.random.rand(cDecompose.get_Parameter_Num()) * (2 * np.pi)
1171  )
1172  cDecompose.Start_Decomposition()
1173  parameters = cDecompose.get_Optimized_Parameters()
1174  err = cDecompose.Optimization_Problem(parameters)
1175  it += 1
1176  if err > tolerance or it != 0:
1177  print("Decomposition error: ", err, it)
1178  else:
1179  err = cDecompose.get_Decomposition_Error()
1180  # print( "Decomposition error: ", err )
1181  if tolerance < err:
1182  # raise Exception(f"Decomposition error {err} exceeds the tolerance {tolerance}.")
1183  return []
1184 
1185  return [(squander_circuit, parameters)]
1186 
1187  @staticmethod
1189  circs: List[Circuit],
1190  parameter_arrs: List[np.ndarray],
1191  metric: Callable[[Circuit], Any] = CNOTGateCount,
1192  ) -> tuple[Circuit, np.ndarray]:
1193  """Select the circuit with the lowest ``metric`` value.
1194 
1195  Args:
1196  circs: Candidate Squander circuits (same length as ``parameter_arrs``).
1197  parameter_arrs: Parameter vectors aligned with ``circs``.
1198  metric: Comparable cost value; lower is better. Defaults to
1199  ``CNOTGateCount``. Tuples may be used for lexicographic ordering.
1200 
1201  Returns:
1202  ``(best_circuit, best_parameters)`` for the minimizing index.
1203  """
1204 
1205  if not isinstance(circs, list):
1206  raise Exception("First argument should be a list of squander circuits")
1207 
1208  if not isinstance(parameter_arrs, list):
1209  raise Exception("Second argument should be a list of numpy arrays")
1210 
1211  if len(circs) != len(parameter_arrs):
1212  raise Exception("The first two arguments should be of the same length")
1213 
1214  min_idx = min(range(len(circs)), key=lambda idx: metric(circs[idx]))
1215 
1216  return circs[min_idx], parameter_arrs[min_idx]
1217 
1218  @staticmethod
1220  subcircuit: Circuit,
1221  subcircuit_parameters: np.ndarray,
1222  config: dict,
1223  structure=None,
1224  ) -> Tuple[Circuit, np.ndarray]:
1225  """Decompose one partition subcircuit (multiprocessing-safe entry point).
1226 
1227  Args:
1228  subcircuit: Subcircuit acting on a subset of the wide register.
1229  subcircuit_parameters: Flat parameter vector slice for ``subcircuit``.
1230  config: Same keys as wide optimization (``strategy``, ``topology``, etc.).
1231  structure: Optional fixed gate structure when ``strategy == "Custom"``.
1232 
1233  Returns:
1234  Tuple of ``(decomposed_circuit, decomposed_parameters)`` pairs, each
1235  remapped back to the original qubit indices of ``subcircuit``.
1236  """
1237 
1238  qbit_num_orig_circuit = subcircuit.get_Qbit_Num()
1239 
1240  involved_qbits = subcircuit.get_Qbits()
1241 
1242  qbit_num = len(involved_qbits)
1243 
1244  # create qbit map:
1245  qbit_map = {}
1246  for idx in range(len(involved_qbits)):
1247  qbit_map[involved_qbits[idx]] = idx
1248  mini_topology = None
1249  if config["topology"] is not None:
1250  mini_topology = extract_subtopology(involved_qbits, qbit_map, config)
1251  # remap the subcircuit to a smaller qubit register
1252  remapped_subcircuit = subcircuit.Remap_Qbits(qbit_map, qbit_num)
1253 
1254  if not structure is None:
1255  structure = structure.Remap_Qbits(qbit_map, qbit_num)
1256 
1257  # get the unitary representing the circuit
1258  unitary = remapped_subcircuit.get_Matrix(
1259  np.asarray(subcircuit_parameters, dtype=np.float64)
1260  )
1261 
1262  # decompose a small unitary into a new circuit
1263  all_decomposed = qgd_Wide_Circuit_Optimization.DecomposePartition(
1264  unitary, config, mini_topology, structure=structure
1265  )
1266  # create inverse qbit map:
1267  inverse_qbit_map = {}
1268  for key, value in qbit_map.items():
1269  inverse_qbit_map[value] = key
1270  result = []
1271  for decomposed_circuit, decomposed_parameters in all_decomposed:
1272 
1273  # remap the decomposed circuit in order to insert it into a large circuit
1274  new_subcircuit = decomposed_circuit.Remap_Qbits(
1275  inverse_qbit_map, qbit_num_orig_circuit
1276  )
1277 
1278  if config["test_subcircuits"]:
1280  subcircuit,
1281  subcircuit_parameters,
1282  new_subcircuit,
1283  decomposed_parameters,
1284  parallel=config["parallel"],
1285  tolerance=_squander_validation_tolerance(config)
1286  )
1287 
1288  new_subcircuit = new_subcircuit.get_Flat_Circuit()
1289  result.append((new_subcircuit, decomposed_parameters))
1290  return tuple(result)
1291 
1292  @staticmethod
1294  """Order partition gate-sets by dependencies and build a reverse-dependency map.
1295 
1296  Args:
1297  allparts: List of sets of gate indices, one per partition.
1298 
1299  Returns:
1300  ``(ordered_parts, rg_new)`` where ``ordered_parts`` lists partitions in
1301  topological order and ``rg_new`` maps each new index to predecessors.
1302  """
1303  gate_to_parts = {}
1304  for i, part in enumerate(allparts):
1305  for gate in part:
1306  gate_to_parts.setdefault(gate, set()).add(i)
1307  g = {i: set() for i in range(len(allparts))}
1308  rg = {i: set() for i in range(len(allparts))}
1309  for i, part in enumerate(allparts):
1310  for gate in part:
1311  for other_part in gate_to_parts[gate]:
1312  if other_part != i and (
1313  len(part & allparts[other_part]) > 0
1314  and (len(part) < len(allparts[other_part]))
1315  or part < allparts[other_part]
1316  ):
1317  g[i].add(other_part)
1318  rg[other_part].add(i)
1319  rg_ret = {i: set(rg[i]) for i in range(len(allparts))}
1320  S = collections.deque(m for m in rg if len(rg[m]) == 0)
1321  L = []
1322  while S:
1323  n = S.popleft()
1324  L.append(n)
1325  for m in set(g[n]):
1326  g[n].remove(m)
1327  rg[m].remove(n)
1328  if len(rg[m]) == 0:
1329  S.append(m)
1330  if len(L) != len(allparts):
1331  raise ValueError("Dependency graph is not a DAG")
1332  neworder = {old: new for new, old in enumerate(L)}
1333  rg_ret = {
1334  neworder[i]: set(neworder[j] for j in rg_ret[i])
1335  for i in range(len(allparts))
1336  }
1337  return [
1338  allparts[i] for i in L
1339  ], rg_ret # return partitions in dependency order and dependencies
1340 
1341  @staticmethod
1342  def make_all_partition_circuit(circ, orig_parameters, max_partition_size):
1343  """ILP-based partitioning: flatten ``circ`` into a circuit of sub-circuits with concatenated parameters.
1344 
1345  Returns:
1346  ``(partitioned_circuit, parameters, recombine_info, part_deps)`` for later fusion in
1347  ``recombine_all_partition_circuit``.
1348  """
1349  from squander.partitioning.ilp import get_all_partitions, _get_topo_order
1350 
1351  allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
1352  get_all_partitions(circ, max_partition_size)
1353  )
1354  qbit_num_orig_circuit = circ.get_Qbit_Num()
1355  gate_dict = {i: gate for i, gate in enumerate(circ.get_Gates())}
1356  single_qubit_chains_pre = {x[0]: x for x in single_qubit_chains if rgo[x[0]]}
1357  single_qubit_chains_post = {x[-1]: x for x in single_qubit_chains if go[x[-1]]}
1358  single_qubit_chains_prepost = {
1359  x[0]: x
1360  for x in single_qubit_chains
1361  if x[0] in single_qubit_chains_pre and x[-1] in single_qubit_chains_post
1362  }
1363  partitioned_circuit = Circuit(qbit_num_orig_circuit)
1364  params = []
1365  allparts, part_deps = qgd_Wide_Circuit_Optimization.build_partition_topo_deps(
1366  allparts
1367  )
1368  for part in allparts:
1369  surrounded_chains = {
1370  t
1371  for s in part
1372  for t in go[s]
1373  if t in single_qubit_chains_prepost
1374  and go[single_qubit_chains_prepost[t][-1]]
1375  and next(iter(go[single_qubit_chains_prepost[t][-1]])) in part
1376  }
1377  gates = frozenset.union(
1378  part, *(single_qubit_chains_prepost[v] for v in surrounded_chains)
1379  )
1380  # topo sort part + surrounded chains
1381  c = Circuit(qbit_num_orig_circuit)
1382  for gate_idx in _get_topo_order(
1383  {x: go[x] & gates for x in gates},
1384  {x: rgo[x] & gates for x in gates},
1385  gate_to_qubit,
1386  ):
1387  c.add_Gate(gate_dict[gate_idx])
1388  start = gate_dict[gate_idx].get_Parameter_Start_Index()
1389  params.append(
1390  orig_parameters[
1391  start : start + gate_dict[gate_idx].get_Parameter_Num()
1392  ]
1393  )
1394  partitioned_circuit.add_Circuit(c)
1395  for chain in single_qubit_chains:
1396  c = Circuit(qbit_num_orig_circuit)
1397  for gate_idx in chain:
1398  c.add_Gate(gate_dict[gate_idx])
1399  start = gate_dict[gate_idx].get_Parameter_Start_Index()
1400  params.append(
1401  orig_parameters[
1402  start : start + gate_dict[gate_idx].get_Parameter_Num()
1403  ]
1404  )
1405  partitioned_circuit.add_Circuit(c)
1406  parameters = np.concatenate(params, axis=0)
1407  return (
1408  partitioned_circuit,
1409  parameters,
1410  (allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit),
1411  part_deps,
1412  )
1413 
1414  @staticmethod
1416  """Drop single-qubit gates that sit only at the head or tail of the dependency DAG.
1417 
1418  Args:
1419  circ: Input circuit.
1420  params: Flat parameter array for ``circ``.
1421 
1422  Returns:
1423  ``(new_circuit, new_params)`` with head/tail single-qubit gates removed.
1424  """
1425  gate_dict, g, rg, gate_to_qubit, _ = build_dependency(circ)
1426  newcirc = Circuit(circ.get_Qbit_Num())
1427  new_params = []
1428  for i in gate_dict:
1429  gate = gate_dict[i]
1430  if len(gate_to_qubit[i]) == 1 and (len(g[i]) == 0 or len(rg[i]) == 0):
1431  continue
1432  newcirc.add_Gate(gate)
1433  start_idx = gate.get_Parameter_Start_Index()
1434  new_params.append(params[start_idx : start_idx + gate.get_Parameter_Num()])
1435  return newcirc, (
1436  np.empty((0,), dtype=np.float64)
1437  if len(new_params) == 0
1438  else np.concatenate(new_params, axis=0)
1439  )
1440 
1441  @staticmethod
1442  def get_fingerprint(circ, params):
1443  """Hashable signature of gate layout and parameters (for decomposition caching).
1444 
1445  Args:
1446  circ: Squander circuit.
1447  params: Parameter array associated with ``circ``.
1448 
1449  Returns:
1450  Tuple usable as a dict key for memoizing decompositions.
1451  """
1452  return (circ.get_Qbit_Num(),) + tuple(
1453  (gate.get_Name(), tuple(gate.get_Involved_Qbits()))
1454  for gate in circ.get_Gates()
1455  ) + tuple(params)
1456 
1457  @staticmethod
1459  circ, optimized_subcircuits, optimized_parameter_list, recombine_info
1460  ):
1461  """Reorder optimized partitions to respect global gate dependencies.
1462 
1463  Args:
1464  circ: Original flat circuit (for topological ordering context).
1465  optimized_subcircuits: One optimized subcircuit per partition slot.
1466  optimized_parameter_list: Parameter lists aligned with ``optimized_subcircuits``.
1467  recombine_info: Tuple from ``make_all_partition_circuit`` (ILP metadata).
1468 
1469  Returns:
1470  ``(reordered_circuits, reordered_parameter_lists)`` in execution order.
1471  """
1472  from squander.partitioning.ilp import (
1473  topo_sort_partitions,
1474  ilp_global_optimal,
1475  recombine_single_qubit_chains,
1476  )
1477 
1478  allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
1479  recombine_info
1480  )
1481  # One additional CNOT must cost more than every possible difference in
1482  # the selected partitions' combined single-qubit count.
1483  cnot_weight = 1 + sum(
1484  sum(y for x, y in c.get_Gate_Nums().items() if CNOT_COUNT_DICT.get(x, -1) <= 0)
1485  for c in optimized_subcircuits[: len(allparts)]
1486  )
1487  weights = [
1488  CNOTGateCount(circ, cnot_weight)
1489  for circ in optimized_subcircuits[: len(allparts)]
1490  ]
1491  L, fusion_info = ilp_global_optimal(allparts, g, weights=weights)
1492  struct_idxs = list(L)
1494  go,
1495  rgo,
1496  single_qubit_chains,
1497  gate_to_tqubit,
1498  [allparts[i] for i in L],
1499  fusion_info,
1500  surrounded_only=True,
1501  )
1502  single_qubit_chain_idx = {
1503  frozenset(chain): idx + len(allparts)
1504  for idx, chain in enumerate(single_qubit_chains)
1505  }
1506  for extrapart in parts[len(struct_idxs) :]:
1507  struct_idxs.append(single_qubit_chain_idx[frozenset(extrapart)])
1508  L = topo_sort_partitions(circ, parts)
1509  return [optimized_subcircuits[struct_idxs[i]] for i in L], [
1510  optimized_parameter_list[struct_idxs[i]] for i in L
1511  ]
1512 
1513  def OptimizeWideCircuit(
1514  self, circ: Circuit, parameters: np.ndarray
1515  ) -> Tuple[Circuit, np.ndarray]:
1516  """Top-level wide-circuit pass: optional routing, then Qiskit / BQSKit / Squander partition optimization.
1517 
1518  Sets ``self.config`` timing and intermediate circuit keys (e.g. ``routed_circuit``, ``optimization_time``).
1519  """
1520  if not qgd_Wide_Circuit_Optimization.is_valid_routing(
1521  circ, self.config["topology"]
1522  ):
1523 
1524  print("fixing topology in the circuit")
1525  topo = self.config["topology"]
1526  self.config["topology"] = None
1527  strat = self.config["strategy"]
1528  self.config["strategy"] = self.config["pre-opt-strategy"]
1529 
1530  print("Optimizing circuit with all-to-all (a2a) connectivity")
1531  circ, parameters = self.OptimizeWideCircuit(circ, parameters)
1532  self.config["all_to_all_optimization_time"] = self.config[
1533  "optimization_time"
1534  ]
1535  self.config["all_to_all_circuit"] = circ
1536  self.config["all_to_all_parameters"] = parameters
1537  self.config["strategy"] = strat
1538  self.config["topology"] = topo
1539  start_time = time.time()
1540 
1541  print("Routing circuit to fix the topology")
1542  circ, parameters = self.route_circuit(circ, parameters)
1543  self.config["routing_time"] = time.time() - start_time
1544  self.config["routed_circuit"] = circ
1545  self.config["routed_parameters"] = parameters
1546  else:
1547  if self.config["topology"] is not None:
1548  print("No additional routing is needed on the circuit")
1549 
1550  start_time = time.time()
1551  if self.config["strategy"] == "bqskit":
1552  print("Optimizing circuit with BQSkit")
1553  from squander import Qiskit_IO
1554  from bqskit import compile
1555 
1556  from bqskit.compiler.machine import MachineModel
1557  from bqskit.compiler import Compiler
1558  from bqskit.ir.lang.qasm2 import OPENQASM2Language
1559  from qiskit import qasm2, QuantumCircuit
1560 
1561  from bqskit.passes import SetModelPass
1562  from bqskit.compiler.compile import (
1563  build_multi_qudit_retarget_workflow,
1564  build_resynthesis_optimization_workflow,
1565  build_single_qudit_retarget_workflow,
1566  build_gate_deletion_optimization_workflow,
1567  LogErrorPass,
1568  )
1569 
1570  # Build BQSKit machine model from your topology
1571  model = MachineModel(circ.get_Qbit_Num(), self.config["topology"])
1572 
1573  # Convert squander circuit → qiskit → BQSKit
1574  # (BQSKit has a from_qiskit helper if you go via Qiskit IR)
1575  circo = Qiskit_IO.get_Qiskit_Circuit(
1576  circ, np.asarray(parameters, dtype=np.float64)
1577  )
1578 
1579  bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
1580 
1581  compilation_workflow = [
1582  SetModelPass(model), # attach hardware model to circuit
1583  build_multi_qudit_retarget_workflow(
1584  4, max_synthesis_size=self.max_partition_size
1585  ),
1586  build_resynthesis_optimization_workflow(
1587  4, max_synthesis_size=self.max_partition_size, iterative=True
1588  ),
1589  build_single_qudit_retarget_workflow(
1590  4, max_synthesis_size=self.max_partition_size
1591  ),
1592  build_gate_deletion_optimization_workflow(
1593  4, max_synthesis_size=self.max_partition_size, iterative=True
1594  ),
1595  LogErrorPass(),
1596  ]
1597 
1598  with Compiler() as compiler:
1599  routed_bqskit_circ, pass_data = compiler.compile(
1600  bqskit_circ, compilation_workflow, True
1601  )
1602 
1603  default = list(range(bqskit_circ.num_qudits))
1604  initial_map = pass_data.get("initial_mapping", default)
1605  final_map = pass_data.get("final_mapping", default)
1606 
1607  # Convert back: BQSKit → Qiskit → Squander
1608  circuit_qiskit = QuantumCircuit.from_qasm_str(
1609  OPENQASM2Language().encode(routed_bqskit_circ)
1610  )
1611  newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1612  circuit_qiskit
1613  )
1614 
1615  qgd_Wide_Circuit_Optimization.check_valid_routing(
1616  newcirc, self.config["topology"]
1617  )
1618  print("OptimizeWideCircuit::check_compare_circuits")
1619  self.check_compare_circuits(circ, parameters, newcirc, newparameters)
1620  circ, parameters = newcirc, newparameters
1621 
1622  elif self.config["strategy"] == "qiskit":
1623  print("Optimizing circuit with Qiskit")
1624  from squander import Qiskit_IO
1625  from qiskit import transpile
1626  from qiskit.transpiler import CouplingMap
1627  from squander.gates import gates_Wrapper as gate
1628 
1629  SUPPORTED_GATES_NAMES = {
1630  n.lower().replace("cnot", "cx")
1631  for n in dir(gate)
1632  if not n.startswith("_")
1633  and issubclass(getattr(gate, n), gate.Gate)
1634  and n not in ("Gate", "CROT", "CR", "SYC", "CCX", "CSWAP")
1635  }
1636  circo = Qiskit_IO.get_Qiskit_Circuit(
1637  circ, np.asarray(parameters, dtype=np.float64)
1638  )
1639  coupling_map = (
1640  None
1641  if self.config["topology"] is None
1642  else CouplingMap([[i, j] for i, j in self.config["topology"]])
1643  )
1644  circuit_qiskit = transpile(
1645  circo,
1646  basis_gates=SUPPORTED_GATES_NAMES,
1647  coupling_map=coupling_map,
1648  optimization_level=3,
1649  )
1650  newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1651  circuit_qiskit
1652  )
1653  qgd_Wide_Circuit_Optimization.check_valid_routing(
1654  newcirc, self.config["topology"]
1655  )
1656  print("OptimizeWideCircuit::check_compare_circuits")
1657  self.check_compare_circuits(circ, parameters, newcirc, newparameters)
1658  circ, parameters = newcirc, newparameters
1659  else:
1660 
1661  print("Optimizing circuit with Squander")
1662  part_size_start = self.max_partition_size
1663  part_size_end = self.max_partition_size
1664  if self.config.get("auto_expand_partition_size", True) and (
1665  self.config.get("use_osr", False)
1666  or self.config.get("use_graph_search", False)
1667  ):
1668  part_size_end = min(4, circ.get_Qbit_Num())
1669  count = CNOTGateCount(circ, 0)
1670  fingerprint_dict = {}
1671  for max_part_size in range(part_size_start, part_size_end + 1):
1672  # instantiate the object for optimizing wide circuits
1673  wide_circuit_optimizer = qgd_Wide_Circuit_Optimization(
1674  {**self.config, "max_partition_size": max_part_size}
1675  )
1676  while True:
1677  # run circuit optimization
1678  circ_flat, parameters = (
1679  wide_circuit_optimizer.InnerOptimizeWideCircuit(
1680  circ, parameters, fingerprint_dict=fingerprint_dict
1681  )
1682  )
1683  circ = circ_flat.get_Flat_Circuit()
1684  newcount = CNOTGateCount(circ, 0)
1685  no_improve = newcount >= count
1686  count = newcount
1687  if no_improve:
1688  break
1689  self.config["optimization_time"] = time.time() - start_time
1690  return circ, parameters
1691 
1693  self, circ: Circuit, orig_parameters: np.ndarray, fingerprint_dict=None
1694  ) -> Tuple[Circuit, np.ndarray]:
1695  """Optimize one pass of wide-circuit partition decomposition.
1696 
1697  The circuit is converted to a CNOT basis, partitioned, each partition is
1698  optimized (possibly in parallel), and then reconstructed into one circuit.
1699 
1700  Args:
1701  circ: Input circuit to optimize.
1702  orig_parameters: Parameter array associated with ``circ``.
1703  fingerprint_dict: Optional decomposition cache shared across passes.
1704 
1705  Returns:
1706  Tuple of ``(optimized_circuit, optimized_parameters)``.
1707  """
1708  from squander.utils import circuit_to_CNOT_basis
1709 
1710  circ, orig_parameters = circuit_to_CNOT_basis(circ, orig_parameters)
1711  global_min = self.config.get("global_min", True)
1712  if global_min:
1713  partitioned_circuit, parameters, recombine_info, part_deps = (
1714  qgd_Wide_Circuit_Optimization.make_all_partition_circuit(
1715  circ, orig_parameters, self.max_partition_size
1716  )
1717  )
1718 
1719  else:
1720  partitioned_circuit, parameters, _ = PartitionCircuit(
1721  circ,
1722  orig_parameters,
1723  self.max_partition_size,
1724  strategy=self.config["partition_strategy"],
1725  )
1726  part_deps = None
1727 
1728  subcircuits = partitioned_circuit.get_Gates()
1729 
1730  # subcircuits = subcircuits[9:10]
1731 
1732  in_parent = parent_process() is not None
1733 
1734  if not in_parent:
1735  print(len(subcircuits), "partitions found to optimize")
1736 
1737  # the list of optimized subcircuits
1738  optimized_subcircuits: List[Optional[Circuit]] = [None] * len(subcircuits)
1739 
1740  # the list of parameters associated with the optimized subcircuits
1741  optimized_parameter_list: List[Optional[List[np.ndarray]]] = [None] * len(
1742  subcircuits
1743  )
1744 
1745  # list of AsyncResult objects
1746  async_results = [None] * len(subcircuits)
1747 
1748  total_opt = [0]
1749 
1750  def process_result(partition_idx):
1751  """Finalize async decomposition for partition ``partition_idx`` and update caches / lists."""
1752  if optimized_subcircuits[partition_idx] is not None:
1753  return
1754  subcircuit = subcircuits[partition_idx]
1755  # callback on the master process to compare the decomposed and original subcircuit
1756  start_idx = subcircuit.get_Parameter_Start_Index()
1757  subcircuit_parameters = parameters[
1758  start_idx : start_idx + subcircuit.get_Parameter_Num()
1759  ]
1760  fingerprint = (
1761  None
1762  if fingerprint_dict is None
1763  else qgd_Wide_Circuit_Optimization.get_fingerprint(
1764  subcircuit, subcircuit_parameters
1765  )
1766  )
1767  callback_fnc = lambda x: self.CompareAndPickCircuits(
1768  [subcircuit, *(z[0] for z in x)],
1769  [subcircuit_parameters, *(z[1] for z in x)],
1770  lambda c: (CNOTGateCount(c), SingleQubitGateCount(c)),
1771  )
1772  if fingerprint_dict is not None and fingerprint in fingerprint_dict:
1773  new_subcircuit, new_parameters = fingerprint_dict[fingerprint]
1774  else:
1775  new_subcircuit, new_parameters = callback_fnc(
1776  async_results[partition_idx][0](*async_results[partition_idx][1])
1777  if in_parent
1778  else async_results[partition_idx].get(timeout=None)
1779  )
1780 
1781  if subcircuit != new_subcircuit:
1782  print(
1783  "original subcircuit: ",
1784  subcircuit.get_Gate_Nums(),
1785  partition_idx,
1786  )
1787  print("reoptimized subcircuit: ", new_subcircuit.get_Gate_Nums())
1788  if fingerprint_dict is not None:
1789  fingerprint_dict[fingerprint] = (new_subcircuit, new_parameters)
1790  fingerprint_dict[
1791  qgd_Wide_Circuit_Optimization.get_fingerprint(
1792  new_subcircuit, new_parameters
1793  )
1794  ] = (new_subcircuit, new_parameters)
1795  trim_subcirc, trim_parameters = (
1796  qgd_Wide_Circuit_Optimization.strip_single_qubit_head_tails(
1797  new_subcircuit, new_parameters
1798  )
1799  )
1800  fingerprint_dict[
1801  qgd_Wide_Circuit_Optimization.get_fingerprint(
1802  trim_subcirc, trim_parameters
1803  )
1804  ] = (trim_subcirc, trim_parameters)
1805  if total_opt[0] % 100 == 99:
1806  print(total_opt[0] + 1, "partitions optimized")
1807  total_opt[0] += 1
1808  optimized_subcircuits[partition_idx] = new_subcircuit
1809  optimized_parameter_list[partition_idx] = new_parameters
1810 
1811  with (
1812  contextlib.nullcontext() if in_parent else Pool(processes=mp.cpu_count())
1813  ) as pool:
1814  remaining = list(range(len(subcircuits)))
1815  while remaining:
1816  still_remaining = []
1817  # code for iterate over partitions and optimize them
1818  for partition_idx in remaining:
1819  subcircuit = subcircuits[partition_idx]
1820 
1821  # isolate the parameters corresponding to the given sub-circuit
1822  start_idx = subcircuit.get_Parameter_Start_Index()
1823  end_idx = start_idx + subcircuit.get_Parameter_Num()
1824  subcircuit_parameters = parameters[start_idx:end_idx]
1825 
1826  fingerprint = (
1827  None
1828  if fingerprint_dict is None
1829  else qgd_Wide_Circuit_Optimization.get_fingerprint(
1830  subcircuit, subcircuit_parameters
1831  )
1832  )
1833  if fingerprint_dict is not None and fingerprint in fingerprint_dict:
1834  (
1835  optimized_subcircuits[partition_idx],
1836  optimized_parameter_list[partition_idx],
1837  ) = fingerprint_dict[fingerprint]
1838  continue
1839  if part_deps is not None and partition_idx in part_deps:
1840  any_optimized, any_remaining = False, False
1841  for dep_idx in part_deps[partition_idx]:
1842  if optimized_subcircuits[dep_idx] is None and (
1843  async_results[dep_idx] is None
1844  or not isinstance(async_results[dep_idx], tuple)
1845  and not async_results[dep_idx].ready()
1846  ):
1847  any_remaining = True
1848  continue
1849  elif optimized_subcircuits[dep_idx] is None:
1850  process_result(dep_idx)
1851 
1852  optimized_subcircuits_loc = optimized_subcircuits[dep_idx]
1853  assert isinstance(optimized_subcircuits_loc, Circuit)
1854  assert optimized_subcircuits_loc is not None
1855 
1856  if CNOTGateCount(optimized_subcircuits_loc) < CNOTGateCount(
1857  subcircuits[dep_idx]
1858  ): # if the dependency partition was optimized, skip
1859  any_optimized = True
1860  break
1861  if any_optimized:
1862  optimized_subcircuits[partition_idx] = subcircuit
1863  optimized_parameter_list[partition_idx] = (
1864  subcircuit_parameters
1865  )
1866  continue
1867  if any_remaining:
1868  still_remaining.append(partition_idx)
1869  continue
1870  # call a process to decompose a subcircuit
1871  config = {
1872  **self.config,
1873  "tree_level_max": qgd_Wide_Circuit_Optimization.partition_tree_level_max(
1874  self.config, subcircuit
1875  ),
1876  }
1877  fargs = (
1879  (subcircuit, subcircuit_parameters, config, None),
1880  )
1881  # print("Dispatching", subcircuit.get_Involved_Qubits(), "qubits with", CNOGateCount(subcircuit, 0), "CNOT gates, partition ", partition_idx)
1882  async_results[partition_idx] = (
1883  fargs if in_parent else pool.apply_async(*fargs) # type: ignore[union-attr]
1884  )
1885  if len(remaining) == len(still_remaining):
1886  time.sleep(0.1)
1887  remaining = still_remaining
1888  # code for iterate over async results and retrieve the new subcircuits
1889  for partition_idx in range(len(subcircuits)):
1890  process_result(partition_idx)
1891 
1892  # construct the wide circuit from the optimized subcircuits
1893  if global_min:
1894  optimized_subcircuits, optimized_parameter_list = (
1895  qgd_Wide_Circuit_Optimization.recombine_all_partition_circuit(
1896  circ,
1897  optimized_subcircuits,
1898  optimized_parameter_list,
1899  recombine_info,
1900  )
1901  )
1902 
1903  if any(c is None for c in optimized_subcircuits) or any(
1904  p is None for p in optimized_parameter_list
1905  ):
1906  raise RuntimeError(
1907  "Internal error: some partitions were not optimized before reconstruction."
1908  )
1909  wide_circuit, wide_parameters = self.ConstructCircuitFromPartitions(
1910  cast(List[Circuit], optimized_subcircuits),
1911  cast(List[List[np.ndarray]], optimized_parameter_list),
1912  )
1913 
1914  if not in_parent:
1915  print("original circuit: ", circ.get_Gate_Nums())
1916  print("reoptimized circuit: ", wide_circuit.get_Gate_Nums())
1917 
1918  qgd_Wide_Circuit_Optimization.check_valid_routing(
1919  wide_circuit, self.config["topology"]
1920  )
1922  circ,
1923  orig_parameters,
1924  wide_circuit,
1925  wide_parameters,
1926  label="InnerOptimizeWideCircuit",
1927  )
1928 
1929  return wide_circuit, wide_parameters
1930 
1931  @staticmethod
1932  def all_to_all_topology(num_qubits):
1933  """Undirected all-to-all coupler list for ``num_qubits`` qubits."""
1934  return [(i, j) for i in range(num_qubits) for j in range(i + 1, num_qubits)]
1935 
1936  @staticmethod
1937  def linear_topology(num_qubits):
1938  """Path graph couplers ``(i, i+1)``."""
1939  return [(i, i + 1) for i in range(num_qubits - 1)]
1940 
1941  @staticmethod
1942  def star_topology(num_qubits):
1943  """Star graph: hub qubit ``0`` connected to all others."""
1944  return [(0, i) for i in range(1, num_qubits)]
1945 
1946  @staticmethod
1947  def ring_topology(num_qubits):
1948  """Ring couplers including wrap-around ``(n-1, 0)``."""
1949  return [(i, (i + 1) % num_qubits) for i in range(num_qubits)]
1950 
1951  @staticmethod
1952  def lattice_topology(x_qbits, y_qbits):
1953  """2D grid of size ``x_qbits`` by ``y_qbits`` with nearest-neighbor horizontal and vertical edges."""
1954  return [
1955  (i * x_qbits + j, i * x_qbits + (j + 1))
1956  for i in range(y_qbits)
1957  for j in range(x_qbits - 1)
1958  ] + [
1959  (i * x_qbits + j, (i + 1) * x_qbits + j)
1960  for i in range(y_qbits - 1)
1961  for j in range(x_qbits)
1962  ]
1963 
1964  @staticmethod
1965  def heavy_hexagonal_topology(rows, cols):
1966  """Build a finite heavy-hex coupling list (honeycomb with subdivided edges).
1967 
1968  Args:
1969  rows: Number of rows in the brick-wall honeycomb patch.
1970  cols: Number of columns in the patch.
1971 
1972  Returns:
1973  List of undirected edges ``(u, v)``. The first ``rows * cols`` qubit
1974  indices are honeycomb vertices; each original edge introduces one
1975  additional degree-2 qubit on the subdivided link.
1976  """
1977 
1978  def vid(r, c):
1979  """Linear index for honeycomb vertex at row ``r``, column ``c``."""
1980  return r * cols + c
1981 
1982  # Underlying honeycomb / brick-wall edges
1983  base_edges = []
1984 
1985  for r in range(rows):
1986  for c in range(cols):
1987  # Vertical brick-wall edges
1988  if r + 1 < rows:
1989  base_edges.append((vid(r, c), vid(r + 1, c)))
1990 
1991  # Alternating horizontal edges
1992  if c + 1 < cols and ((r + c) % 2 == 0):
1993  base_edges.append((vid(r, c), vid(r, c + 1)))
1994 
1995  # Subdivide every honeycomb edge by inserting a qubit
1996  next_id = rows * cols
1997  heavy_edges = []
1998 
1999  for u, v in base_edges:
2000  w = next_id
2001  next_id += 1
2002  heavy_edges.append((u, w))
2003  heavy_edges.append((w, v))
2004 
2005  return heavy_edges
2006 
2007  @staticmethod
2009  """Approximate Sycamore-like 6x9 grid topology (simplified; ignores known dead qubits)."""
2010  return qgd_Wide_Circuit_Optimization.lattice_topology(
2011  6, 9
2012  ) # there is a defective qubit at (0, 3) in the sycamore chip, but we ignore it here for simplicity
2013 
2014  @staticmethod
2015  def is_valid_routing(wide_circuit, topo):
2016  """True if every multi-qubit gate's qubits lie in a connected subgraph of undirected ``topo``."""
2017  if topo is None:
2018  return True
2019 
2020  import itertools
2021 
2022  topo_set = {frozenset(edge) for edge in topo}
2023 
2024  def qubits_connected(qubits):
2025  """Whether pairwise couplers in ``topo_set`` connect all qubits in ``qubits``."""
2026  if len(qubits) <= 1:
2027  return True
2028  edges = {
2029  frozenset((q1, q2))
2030  for q1, q2 in itertools.combinations(qubits, 2)
2031  if frozenset((q1, q2)) in topo_set
2032  }
2033  if len(edges) == 0:
2034  return False
2035  cur_set = set(edges.pop())
2036  while edges:
2037  next_edge = next((e for e in edges if len(e & cur_set) > 0), None)
2038  if next_edge is None:
2039  return False
2040  cur_set |= next_edge
2041  edges.remove(next_edge)
2042  return set(qubits) <= cur_set
2043 
2044  return all(
2045  qubits_connected(gate.get_Involved_Qbits())
2046  for gate in wide_circuit.get_Flat_Circuit().get_Gates()
2047  if len(gate.get_Involved_Qbits()) > 1
2048  )
2049 
2050  @staticmethod
2051  def check_valid_routing(wide_circuit, topo):
2052  """Assert ``is_valid_routing``; raises if any gate violates ``topo``."""
2053  if not qgd_Wide_Circuit_Optimization.is_valid_routing(wide_circuit, topo):
2054  import itertools, sys
2055  topo_set = {frozenset(e) for e in topo}
2056  for gate in wide_circuit.get_Flat_Circuit().get_Gates():
2057  qbits = gate.get_Involved_Qbits()
2058  if len(qbits) <= 1:
2059  continue
2060  edges = {frozenset((q1,q2)) for q1,q2 in itertools.combinations(qbits,2) if frozenset((q1,q2)) in topo_set}
2061  if not edges:
2062  sys.stderr.write(f'ROUTING_VIOLATION: {type(gate).__name__} on {qbits} topo={topo}\n')
2063  sys.stderr.flush()
2064  break
2065  raise AssertionError("Final circuit contains gates that do not respect the routing constraints.")
2066 
2068  self,
2069  circ,
2070  orig_parameters,
2071  wide_circuit,
2072  wide_parameters,
2073  routing=False,
2074  forced_test=False,
2075  label=None,
2076  ):
2077  """Optionally verify equivalence of ``circ`` and ``wide_circuit`` via ``CompareCircuits``.
2078 
2079  Args:
2080  circ: Original circuit.
2081  orig_parameters: Parameters for ``circ``.
2082  wide_circuit: Optimized or routed circuit.
2083  wide_parameters: Parameters for ``wide_circuit``.
2084  routing: If true and initial/final mappings exist in ``self.config``,
2085  pass them to ``CompareCircuits`` for layout-aware comparison.
2086  forced_test: If true, run the comparison even when ``test_final_circuit``
2087  is false in config.
2088 
2089  ``self.config['circuit_validation_tolerance']`` is an infidelity
2090  threshold for this whole-circuit state-vector check. It is deliberately
2091  separate from ``self.config['tolerance']``, which controls block
2092  synthesis and block-level validation.
2093  """
2094  forced_test = forced_test or (
2095  self.config.get("force_small_circuit_validation", True)
2096  and circ.get_Qbit_Num() <= 12
2097  )
2098  if self.config["test_final_circuit"] or forced_test:
2099  if label is not None:
2100  print(f"{label}: check_compare_circuits")
2101  tolerance = _circuit_validation_tolerance(self.config)
2102  if (
2103  routing
2104  and self.config.get("initial_mapping", None) is not None
2105  and self.config.get("final_mapping", None) is not None
2106  ):
2108  circ,
2109  orig_parameters,
2110  wide_circuit,
2111  wide_parameters,
2112  initial_mapping=self.config["initial_mapping"],
2113  final_mapping=self.config["final_mapping"],
2114  tolerance=tolerance,
2115  parallel=0,
2116  )
2117  else:
2119  circ,
2120  orig_parameters,
2121  wide_circuit,
2122  wide_parameters,
2123  tolerance=tolerance,
2124  )
2125 
2126  def route_circuit(self, circ: Circuit, orig_parameters: np.ndarray):
2127  """Map ``circ`` onto ``self.config['topology']`` using the configured router.
2128 
2129  The strategy is ``self.config['routing-strategy']``, e.g. ``seqpam-ilp``,
2130  ``seqpam-quick``, ``bqskit-sabre``, ``light-sabre`` (Qiskit), or ``sabre``
2131  (Squander). Writes ``initial_mapping`` and ``final_mapping`` into
2132  ``self.config`` when the backend provides them.
2133 
2134  Args:
2135  circ: Circuit before routing.
2136  orig_parameters: Parameter vector for ``circ``.
2137 
2138  Returns:
2139  ``(routed_circuit, routed_parameters)`` laid out for ``self.config['topology']``.
2140  """
2141  strategy = self.config.get("routing-strategy", "seqpam-ilp")
2142 
2143  if strategy in ("seqpam-ilp", "seqpam-quick", "bqskit-sabre"):
2144  from squander import Qiskit_IO
2145  import bqskit.compiler.compile as bqskit_compile_module
2146  from bqskit.compiler import Compiler
2147  from bqskit.compiler.compile import (
2148  build_sabre_mapping_workflow,
2149  build_seqpam_mapping_optimization_workflow,
2150  )
2151 
2152  from bqskit.passes import (
2153  SetModelPass,
2154  )
2155  from bqskit.compiler.machine import MachineModel
2156  from bqskit.ir.lang.qasm2 import OPENQASM2Language
2157  from qiskit import qasm2, QuantumCircuit
2158 
2159  # Build BQSKit machine model from your topology
2160  model = MachineModel(circ.get_Qbit_Num(), self.config["topology"])
2161 
2162  # Convert squander circuit → qiskit → BQSKit
2163  # (BQSKit has a from_qiskit helper if you go via Qiskit IR)
2164  circo = Qiskit_IO.get_Qiskit_Circuit(
2165  circ, np.asarray(orig_parameters, dtype=np.float64)
2166  )
2167 
2168  bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
2169  # Customizable knobs
2170 
2171  if strategy == "seqpam-ilp":
2172  # Routing-only SEQPAM pass pipeline. Patch the classes BQSKit's
2173  # workflow factory instantiates, so we do not depend on the private
2174  # shape of the returned Workflow.
2176  bqskit_compile_module,
2177  use_squander_partitioner=True,
2178  config=self.config,
2179  ):
2180  mainflow = build_seqpam_mapping_optimization_workflow(
2181  block_size=3 # SEQPAM uses 3-qubit blocks only
2182  )
2183  elif strategy == "seqpam-quick":
2184  # Keep BQSKit's QuickPartitioner. QSearch/LEAP are replaced
2185  # only when the configured optimizer is Squander-native.
2187  bqskit_compile_module,
2188  use_squander_partitioner=False,
2189  config=self.config,
2190  ):
2191  mainflow = build_seqpam_mapping_optimization_workflow(
2192  block_size=3 # SEQPAM uses 3-qubit blocks only
2193  )
2194  elif strategy == "bqskit-sabre":
2195  mainflow = build_sabre_mapping_workflow()
2196  else:
2197  raise ValueError(f"Unsupported BQSKit routing strategy: {strategy}")
2198 
2199  routing_workflow = [
2200  SetModelPass(model), # attach hardware model to circuit
2201  mainflow,
2202  ]
2203 
2204  # EAPP monkey-patch catches Squander OSR failures per permutation
2205  # and installs a SWAP-correct fallback in BQSKit worker processes.
2206  import os as _os, json as _json
2207  old_patch_env = _os.environ.get('_SQUANDER_EAPP_FALLBACK_PATCH')
2208  old_config_env = _os.environ.get('_SQUANDER_BQSKIT_CONFIG')
2209  _os.environ['_SQUANDER_EAPP_FALLBACK_PATCH'] = '1'
2210  _os.environ['_SQUANDER_BQSKIT_CONFIG'] = _json.dumps(
2212  )
2214  try:
2215  with Compiler() as compiler:
2216  routed_bqskit_circ, pass_data = compiler.compile(
2217  bqskit_circ, routing_workflow, True
2218  )
2219  finally:
2220  if old_patch_env is None:
2221  _os.environ.pop('_SQUANDER_EAPP_FALLBACK_PATCH', None)
2222  else:
2223  _os.environ['_SQUANDER_EAPP_FALLBACK_PATCH'] = old_patch_env
2224  if old_config_env is None:
2225  _os.environ.pop('_SQUANDER_BQSKIT_CONFIG', None)
2226  else:
2227  _os.environ['_SQUANDER_BQSKIT_CONFIG'] = old_config_env
2228 
2229  # Convert back: BQSKit → Qiskit → Squander
2230  circuit_qiskit_routed = QuantumCircuit.from_qasm_str(
2231  OPENQASM2Language().encode(routed_bqskit_circ)
2232  )
2233  Squander_remapped_circuit, parameters_remapped_circuit = (
2234  Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_routed)
2235  )
2236  self.config["initial_mapping"] = list(pass_data.initial_mapping)
2237  self.config["final_mapping"] = list(pass_data.final_mapping)
2238 
2239  elif strategy == "light-sabre":
2240  from squander import Qiskit_IO
2241  from qiskit import transpile
2242  from qiskit.transpiler.preset_passmanagers import (
2243  generate_preset_pass_manager,
2244  )
2245  from qiskit.transpiler.passes import SabreLayout, SabreSwap
2246  from qiskit.transpiler import PassManager, CouplingMap
2247  from squander.gates import gates_Wrapper as gate
2248 
2249  # 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")}
2250  circo = Qiskit_IO.get_Qiskit_Circuit(
2251  circ, np.asarray(orig_parameters, dtype=np.float64)
2252  )
2253  coupling_map = [[i, j] for i, j in self.config["topology"]]
2254  # circuit_qiskit_sabre = transpile(circo, basis_gates=SUPPORTED_GATES_NAMES, coupling_map=coupling_map, optimization_level=0)
2255  coupling_map = CouplingMap(coupling_map)
2256  # Customizable SABRE parameters
2257  sabre_seed = self.config.get("sabre_seed", 42)
2258  sabre_trials = self.config.get("sabre_trials", 5) # layout trials
2259  swap_trials = self.config.get("sabre_swap_trials", sabre_trials)
2260  heuristic = self.config.get(
2261  "sabre_heuristic", "decay"
2262  ) # "basic" | "lookahead" | "decay"
2263 
2264  layout_pass = SabreLayout(
2265  coupling_map,
2266  seed=sabre_seed,
2267  max_iterations=sabre_trials,
2268  swap_trials=swap_trials,
2269  )
2270  swap_pass = SabreSwap(
2271  coupling_map,
2272  heuristic=heuristic,
2273  seed=sabre_seed,
2274  trials=swap_trials,
2275  )
2276 
2277  pm = PassManager(
2278  [
2279  layout_pass, # find initial qubit mapping via SABRE
2280  swap_pass, # insert SWAP gates for routing
2281  ]
2282  )
2283  circuit_qiskit_sabre = pm.run(circo)
2284  Squander_remapped_circuit, parameters_remapped_circuit = (
2285  Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_sabre)
2286  )
2287  self.config["initial_mapping"] = (
2288  circuit_qiskit_sabre.layout.initial_index_layout()
2289  )
2290  self.config["final_mapping"] = (
2291  circuit_qiskit_sabre.layout.final_index_layout()
2292  )
2293  elif strategy == "sabre":
2294  sabre = SABRE(circ, self.config["topology"])
2295  (
2296  Squander_remapped_circuit,
2297  parameters_remapped_circuit,
2298  pi,
2299  final_pi,
2300  swap_count,
2301  ) = sabre.map_circuit(orig_parameters)
2302  self.config["initial_mapping"] = pi
2303  self.config["final_mapping"] = final_pi
2304  qgd_Wide_Circuit_Optimization.check_valid_routing(
2305  Squander_remapped_circuit, self.config["topology"]
2306  )
2307 
2308  print("checking circuit after routing")
2309  print(self.config)
2311  circ,
2312  orig_parameters,
2313  Squander_remapped_circuit,
2314  parameters_remapped_circuit,
2315  routing=True,
2316  forced_test=True,
2317  label="route_circuit",
2318  )
2319  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