2 Wide-circuit optimization: partition large circuits into subcircuits, re-decompose 3 them, and optionally route or fuse results according to configuration. 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,
11 from squander
import N_Qubit_Decomposition_custom, N_Qubit_Decomposition
16 from qiskit
import QuantumCircuit
18 from typing
import List, Callable, Tuple, Optional, Set, Dict, Any, cast, Union
20 import multiprocessing
as mp
21 from multiprocessing
import Process, Pool, parent_process
22 import os, contextlib, collections, time
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
30 from bqskit.compiler.basepass
import BasePass
as _BQSKitBasePass
31 from bqskit.passes.synthesis.synthesis
import SynthesisPass
as _BQSKitSynthesisPass
33 _BQSKitBasePass = object
34 _BQSKitSynthesisPass = object
37 _SQUANDER_BQSKIT_SYNTHESIS_CONFIG =
None 39 _SQUANDER_NATIVE_STRATEGIES = frozenset(
40 (
"TreeSearch",
"TabuSearch",
"Adaptive",
"Custom")
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
52 return bool(config.get(
"use_float",
False))
57 SQUANDER_FLOAT32_TOLERANCE
59 else SQUANDER_FLOAT64_TOLERANCE
65 BQSKIT_FLOAT32_SYNTHESIS_VALIDATION_TOLERANCE
67 else BQSKIT_FLOAT64_SYNTHESIS_VALIDATION_TOLERANCE
73 CIRCUIT_FLOAT32_VALIDATION_TOLERANCE
75 else CIRCUIT_FLOAT64_VALIDATION_TOLERANCE
87 """Return the allowed whole-circuit infidelity for state-vector checks.""" 90 "circuit_validation_tolerance",
97 "bqskit_synthesis_validation_tolerance",
103 """Copy only plain data needed by BQSKit worker processes.""" 105 def copy_value(value):
106 if value
is None or isinstance(value, (bool, int, float, str)):
108 if isinstance(value, np.generic):
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):
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
123 return _SKIP_CONFIG_VALUE
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
133 _SKIP_CONFIG_VALUE = object()
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])
158 """Append *op* to *new_c*, using SWAP bridges for edges not in *topo_edges*. 160 For gates with â¥3 qubits, decomposes via :func:`squander.utils.circuit_to_CNOT_basis` 161 and recurses on each resulting gate. 164 loc = list(op.location)
166 params = list(op.params)
if op.params
else None 168 if gate.num_qudits == 1:
170 new_c.append_gate(gate, loc, params)
172 new_c.append_gate(gate, loc)
175 if gate.num_qudits == 2:
176 u, v = loc[0], loc[1]
177 if (u, v)
in topo_edges:
179 new_c.append_gate(gate, [u, v], params)
181 new_c.append_gate(gate, [u, v])
184 adj = {i: set()
for i
in range(width)}
185 for a, b
in topo_edges:
188 from collections
import deque
195 for nb
in adj.get(node, set()):
201 raise ValueError(f
"Cannot bridge ({u},{v}) on topology")
206 while parent[node]
is not None:
209 path = list(reversed(path))
210 swaps = list(zip(path[:-2], path[1:-1]))
216 new_c.append_gate(gate, [u, cur], params)
218 new_c.append_gate(gate, [u, cur])
219 for a, b
in reversed(swaps):
224 from bqskit.ir.lang.qasm2
import OPENQASM2Language
225 from qiskit
import qasm2
228 from bqskit
import Circuit
as _BQCircuit
229 tmp_bq = _BQCircuit(width)
231 tmp_bq.append_gate(gate, loc, params)
233 tmp_bq.append_gate(gate, loc)
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)
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:
253 """Return true if ``location`` can be hosted by ``topo_edges``.""" 254 loc = tuple(
int(q)
for q
in location)
258 return (loc[0], loc[1])
in topo_edges
or (loc[1], loc[0])
in topo_edges
263 adjacency = {q: set()
for q
in wanted}
264 for u, v
in topo_edges:
265 if u
in wanted
and v
in wanted:
270 for nxt
in adjacency.get(cur, ()):
274 return wanted <= seen
278 """Raise AssertionError if ``circuit`` violates ``topo_edges``. 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. 285 if op.gate.num_qudits <= 1:
288 raise AssertionError(
289 f
"BUG: circuit contains {op.gate.name} on {list(op.location)}, " 290 f
"outside topology {sorted(topo_edges)}." 295 """Build a topology-valid fallback for ``Po.T @ U @ Pi``. 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. 301 from bqskit
import Circuit
as _BQCircuit
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}." 311 topo_edges.add((u, v))
312 topo_edges.add((v, u))
314 fallback = _BQCircuit(width, original_circuit.radixes)
317 if (a, b)
not in topo_edges:
319 f
"Cannot realize input permutation {pi} on topology {sorted(topo_edges)}." 323 for op
in original_circuit:
326 po_inv = tuple(po.index(k)
for k
in range(width))
328 if (a, b)
not in topo_edges:
330 f
"Cannot realize output permutation {po} on topology {sorted(topo_edges)}." 347 """Run Squander synthesis, falling back only for explicit Squander misses.""" 349 return await inner_synthesis.synthesize(target, target_data)
350 except _SquanderSynthesisFailed:
355 """Monkey-patch EAPP.run to catch Squander OSR failures per permutation. 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 366 if not _os.environ.get(
'_SQUANDER_EAPP_FALLBACK_PATCH'):
369 from bqskit.passes.mapping.embed
import EmbedAllPermutationsPass
as __EAPP
370 if getattr(__EAPP.run,
"_squander_fallback_patch",
False):
373 async
def __patched_eapp_run(self, circuit, data):
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
383 _logger = _logging.getLogger(
"bqskit.passes.mapping.embed")
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.',
392 width = utry.num_qudits
393 perms = list(_it.permutations(range(width)))
394 no_perm = [tuple(range(width))]
396 _PermutationMatrix.from_qudit_location(width, utry.radixes[0], p)
400 _PermutationMatrix.from_qudit_location(width, utry.radixes[0], p)
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]
414 _logger.warning(
'No permutation is being used in PAS.')
415 permsbyperms = list(_it.product(no_perm, no_perm))
418 if self.vary_topology
and width != 1:
419 if _STSP.key
not in data:
421 'Cannot find subtopologies, try running a' 422 ' SubtopologySelectionPass first.',
424 if width
not in data[_STSP.key]:
426 'Subtopology information for block size' 427 f
' {width} is not available.',
429 graphs = data[_STSP.key][width]
431 graphs = [_CouplingGraph.all_to_all(width)]
435 model = _MachineModel(
436 circuit.num_qudits, graph,
437 data.gate_set, data.model.radixes,
439 target_data = _copy.deepcopy(data)
440 target_data.model = model
441 datas.append(target_data)
443 extended_targets = []
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)
456 circuits = await _get_runtime().map(
457 _squander_synthesize_or_fallback,
458 [self.inner_synthesis] * len(extended_targets),
463 [perm[0]
for perm
in extended_perms],
464 [perm[1]
for perm
in extended_perms],
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]
473 if graph
not in perm_data:
474 perm_data[graph] = {}
476 if perm
in perm_data[graph]:
477 s1 = self.scoring_fn(perm_data[graph][perm])
478 s2 = self.scoring_fn(synthesized)
480 perm_data[graph][perm] = synthesized
482 perm_data[graph][perm] = synthesized
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] = {}
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
497 s1 = self.scoring_fn(perm_data[new_graph][new_perm])
498 s2 = self.scoring_fn(renumber_c)
500 perm_data[new_graph][new_perm] = renumber_c
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
516 data[
'permutation_data'] = perm_data
518 __patched_eapp_run._squander_fallback_patch =
True 519 __EAPP.run = __patched_eapp_run
526 """BQSKit pass: replace circuit body with Squander ILP partition blocks.""" 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
539 circ_qiskit = QuantumCircuit.from_qasm_str(
540 OPENQASM2Language().encode(circuit)
547 circ, orig_parameters = Qiskit_IO.convert_Qiskit_to_Squander(circ_qiskit)
551 partitioned_circuit_bqskit = BQSKitCircuit(circ.get_Qbit_Num())
552 for subcircuit
in partitioned_circuit.get_Gates():
553 if not isinstance(subcircuit, Circuit):
555 "Squander ILP partitioning returned a non-block gate; " 556 "BQSKit SEQPAM requires partition blocks." 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()
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),
570 subcircuit_bqskit = OPENQASM2Language().decode(qasm2.dumps(subcircuit_qiskit))
571 partitioned_circuit_bqskit.append_circuit(
577 circuit.become(partitioned_circuit_bqskit,
False)
581 """BQSKit synthesis pass: optimize partition blocks with Squander. 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. 591 cfg = _SQUANDER_BQSKIT_SYNTHESIS_CONFIG
596 import os
as _os, json
as _json
597 _env = _os.environ.get(
'_SQUANDER_BQSKIT_CONFIG')
599 cfg = _json.loads(_env)
604 """Return block subtopology from *data*. 606 BQSKit labels are reversed when circuits are converted through 607 Squander/Qiskit, so the topology supplied to Squander is reversed too. 609 if data
is None or getattr(data,
"model",
None)
is None:
613 for u, v
in data.model.coupling_graph:
616 edges.append((qbit_num - 1 -
int(u), qbit_num - 1 -
int(v)))
620 for i
in range(qbit_num)
621 for j
in range(i + 1, qbit_num)
623 edge_set = {frozenset(edge)
for edge
in edges}
624 if edge_set == all_edges:
630 """Return directed topology edges from BQSKit pass data.""" 631 if data
is None or getattr(data,
"model",
None)
is None:
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)))
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
645 target_matrix = np.asarray(target)
646 qbit_num = target.num_qudits
651 "topology": mini_topology,
654 candidates = qgd_Wide_Circuit_Optimization.DecomposePartition(
657 mini_topology=mini_topology,
659 if len(candidates) == 0:
662 f
"Squander synthesis failed for {qbit_num}-qubit block " 663 f
"at tolerance {tolerance}." 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],
673 optimized_qiskit = Qiskit_IO.get_Qiskit_Circuit(
674 optimized_circuit.get_Flat_Circuit(),
675 np.asarray(optimized_parameters, dtype=np.float64),
677 synthesized = OPENQASM2Language().decode(qasm2.dumps(optimized_qiskit))
683 synthesized.renumber_qudits(
684 [qbit_num - 1 - i
for i
in range(qbit_num)]
688 if topo_edges
is not None:
691 if self.
config.get(
"bqskit_distance_test",
False):
692 target_unitary = UnitaryMatrix(target)
693 distance = target_unitary.get_distance_from(synthesized.get_unitary())
697 f
"BQSKit synthesis validation failed: {distance:.2e} > {tol:.2e}" 704 """Raised when Squander cannot synthesize a partition block.""" 708 """Decompose permutation *pi* into SWAPs using only edges in *topo_edges*. 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*. 714 adj = {i: set()
for i
in range(width)}
715 for u, v
in topo_edges:
721 current = list(range(width))
723 for i
in range(width):
725 if current[i] == target:
728 target_pos = current.index(target)
730 from collections
import deque
731 parent = {target_pos:
None}
732 q = deque([target_pos])
743 raise _SquanderSynthesisFailed(
744 f
"Cannot realize permutation {pi} on disconnected topology " 745 f
"{sorted(topo_edges)}." 749 while parent[v]
is not None:
752 path.append(target_pos)
754 for k
in range(len(path) - 1, 0, -1):
755 a, b = path[k], path[k - 1]
758 current[a], current[b] = current[b], current[a]
762 @contextlib.contextmanager
764 """Patch BQSKit workflow factories to use Squander passes. 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. 774 global _SQUANDER_BQSKIT_SYNTHESIS_CONFIG
776 import os
as _os, json
as _json
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')
785 _SQUANDER_BQSKIT_SYNTHESIS_CONFIG = cfg
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
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)
802 _os.environ[
'_SQUANDER_BQSKIT_CONFIG'] = original_config_env
806 """Return topology edges restricted to ``involved_qbits``, with indices remapped via ``qbit_map``. 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. 814 List of ``(u, v)`` pairs in local indices, each edge fully inside the partition. 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]]))
827 _GATE_DECOMPOSITION = {
848 "CH": {
"CNOT": 1,
"RY": 2},
849 "CZ": {
"CNOT": 1,
"H": 2},
850 "SYC": {
"CNOT": 3,
"U1": 3},
851 "CRY": {
"CNOT": 2,
"RY": 2},
852 "CU": {
"CNOT": 2,
"U1": 1,
"RZ": 3,
"RY": 2},
853 "CR": {
"CNOT": 2,
"RZ": 2,
"RY": 2},
854 "CROT": {
"CNOT": 2,
"RZ": 3,
"RY": 2},
855 "CRX": {
"CNOT": 2,
"H": 2,
"RZ": 2},
856 "CRZ": {
"CNOT": 2,
"RZ": 2},
857 "CP": {
"CNOT": 2,
"U1": 3},
858 "CCX": {
"CNOT": 6,
"H": 2,
"T": 4,
"Tdg": 3},
859 "CSWAP": {
"CNOT": 7,
"H": 1,
"T": 5,
"Tdg": 2,
"SX": 1,
"Sdg": 1,
"S": 1},
861 "RXX": {
"CNOT": 2,
"RX": 1},
862 "RYY": {
"CNOT": 2,
"RX": 4,
"RZ": 1},
863 "RZZ": {
"CNOT": 2,
"RZ": 1},
867 CNOT_COUNT_DICT = {g: d.get(
"CNOT", 0)
for g, d
in _GATE_DECOMPOSITION.items()}
871 """Compute weighted two-qubit gate count for a circuit. 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. 880 circ: Squander circuit representation. 881 max_gates: Weight multiplier for the two-qubit cost term. 884 Integer gate-cost score used by optimization heuristics. 886 assert isinstance(circ, Circuit), \
887 "The input parameters should be an instance of Squander Circuit" 888 gate_counts = circ.get_Gate_Nums()
890 CNOT_COUNT_DICT.get(gate, 0) * count
for gate, count
in gate_counts.items()
893 return num_cnots * max_gates + sum(
894 y
for x, y
in gate_counts.items()
if CNOT_COUNT_DICT.get(x, -1) <= 0
900 """Count single-qubit gates in a circuit (U3, H, RX, RY, RZ, etc.). 902 Uses _GATE_DECOMPOSITION to count non-CNOT gates in each gate's breakdown. 905 circ: Squander circuit representation. 908 Total number of single-qubit gate operations when fully decomposed. 910 gate_counts = circ.get_Gate_Nums()
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")
919 """Total number of raw gate operations (single-qubit + multi-qubit). 922 circ: Squander circuit representation. 925 Total gate operation count. 927 return sum(circ.get_Gate_Nums().values())
931 """Return comprehensive gate statistics for a circuit. 933 Uses _GATE_DECOMPOSITION to compute fully-decomposed gate counts. 935 Returns dict with keys: cnot_equiv, single_qubit, total_raw, qubits, 936 and gate_breakdown (per-gate-type raw counts). 938 gate_counts = circ.get_Gate_Nums()
940 CNOT_COUNT_DICT.get(g, 0) * c
for g, c
in gate_counts.items()
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())
948 "cnot_equiv": cnot_equiv,
949 "single_qubit": single,
951 "qubits": circ.get_Qbit_Num(),
952 "gate_breakdown": dict(gate_counts),
957 """Optimize wide (many-qubit) circuits via partitioning and subcircuit decomposition. 959 Supports multiple decomposition strategies, optional global recombination (ILP), 960 and routing when the circuit does not match the target topology. 964 """Validate and store wide-circuit optimization ``config`` (strategy, topology, partitioning, tolerances).""" 966 config.setdefault(
"strategy",
"TreeSearch")
967 config.setdefault(
"parallel", 0)
968 config.setdefault(
"verbosity", 0)
969 config.setdefault(
"use_float",
False)
972 "circuit_validation_tolerance",
976 "bqskit_synthesis_validation_tolerance",
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)
988 strategy = config[
"strategy"]
989 allowed_startegies = [
996 if not strategy
in allowed_startegies:
998 f
"The decomposition startegy should be either of {allowed_startegies}, got {strategy}." 1001 parallel = config[
"parallel"]
1002 allowed_parallel = [0, 1, 2]
1003 if not parallel
in allowed_parallel:
1005 f
"The parallel configuration should be either of {allowed_parallel}, got {parallel}." 1008 verbosity = config[
"verbosity"]
1009 if not isinstance(verbosity, int):
1010 raise Exception(f
"The verbosity parameter should be an integer.")
1012 tolerance = config[
"tolerance"]
1013 if not isinstance(tolerance, float):
1014 raise Exception(f
"The tolerance parameter should be a float.")
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.")
1020 bqskit_synthesis_validation_tolerance = config[
1021 "bqskit_synthesis_validation_tolerance" 1023 if not isinstance(bqskit_synthesis_validation_tolerance, float):
1025 "The bqskit_synthesis_validation_tolerance parameter should be a float." 1028 circuit_validation_tolerance = config[
"circuit_validation_tolerance"]
1029 if not isinstance(circuit_validation_tolerance, float):
1031 "The circuit_validation_tolerance parameter should be a float." 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.")
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.")
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.")
1052 """Return the tree-search depth used for partition-local rewrites.""" 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))
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. 1066 circs: Partition circuits in execution order. 1067 parameter_arrs: Parameter arrays corresponding to ``circs``. 1070 Tuple of ``(wide_circuit, wide_parameters)``. 1073 if not isinstance(circs, list):
1074 raise Exception(
"First argument should be a list of squander circuits")
1076 if not isinstance(parameter_arrs, list):
1077 raise Exception(
"Second argument should be a list of numpy arrays")
1079 if len(circs) != len(parameter_arrs):
1080 raise Exception(
"The first two arguments should be of the same length")
1084 wide_parameters = np.concatenate(parameter_arrs, axis=0)
1086 wide_circuit = Circuit(qbit_num)
1089 wide_circuit.add_Circuit(circ)
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}" 1095 return wide_circuit, wide_parameters
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']``. 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"``. 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 1116 strategy = config[
"strategy"]
1117 if strategy ==
"TreeSearch":
1119 Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1121 elif strategy ==
"TabuSearch":
1123 Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1125 elif strategy ==
"Adaptive":
1130 topology=mini_topology,
1132 elif strategy ==
"Custom":
1133 cDecompose = N_Qubit_Decomposition_custom(
1134 Umtx.conj().T, config=config, accelerator_num=0
1137 structure
is not None 1138 ),
"Custom decomposition strategy requires a gate structure to be provided." 1139 cDecompose.set_Gate_Structure(structure)
1141 raise Exception(f
"Unsupported decomposition type: {strategy}")
1143 tolerance = config[
"tolerance"]
1144 cDecompose.set_Verbose(config[
"verbosity"])
1145 cDecompose.set_Cost_Function_Variant(3)
1146 cDecompose.set_Optimization_Tolerance(tolerance)
1149 cDecompose.set_Optimizer(
"BFGS")
1153 cDecompose.Start_Decomposition()
1154 except Exception
as e:
1158 if not config.get(
"stop_first_solution",
True):
1159 return cDecompose.all_solutions
1161 squander_circuit = cDecompose.get_Circuit()
1162 parameters = cDecompose.get_Optimized_Parameters()
1163 assert parameters
is not None 1165 if strategy ==
"Custom":
1166 err = cDecompose.Optimization_Problem(parameters)
1168 while err > tolerance
and it < 20:
1169 cDecompose.set_Optimized_Parameters(
1170 np.random.rand(cDecompose.get_Parameter_Num()) * (2 * np.pi)
1172 cDecompose.Start_Decomposition()
1173 parameters = cDecompose.get_Optimized_Parameters()
1174 err = cDecompose.Optimization_Problem(parameters)
1176 if err > tolerance
or it != 0:
1177 print(
"Decomposition error: ", err, it)
1179 err = cDecompose.get_Decomposition_Error()
1185 return [(squander_circuit, parameters)]
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. 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. 1202 ``(best_circuit, best_parameters)`` for the minimizing index. 1205 if not isinstance(circs, list):
1206 raise Exception(
"First argument should be a list of squander circuits")
1208 if not isinstance(parameter_arrs, list):
1209 raise Exception(
"Second argument should be a list of numpy arrays")
1211 if len(circs) != len(parameter_arrs):
1212 raise Exception(
"The first two arguments should be of the same length")
1214 min_idx = min(range(len(circs)), key=
lambda idx: metric(circs[idx]))
1216 return circs[min_idx], parameter_arrs[min_idx]
1220 subcircuit: Circuit,
1221 subcircuit_parameters: np.ndarray,
1224 ) -> Tuple[Circuit, np.ndarray]:
1225 """Decompose one partition subcircuit (multiprocessing-safe entry point). 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"``. 1234 Tuple of ``(decomposed_circuit, decomposed_parameters)`` pairs, each 1235 remapped back to the original qubit indices of ``subcircuit``. 1238 qbit_num_orig_circuit = subcircuit.get_Qbit_Num()
1240 involved_qbits = subcircuit.get_Qbits()
1242 qbit_num = len(involved_qbits)
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:
1252 remapped_subcircuit = subcircuit.Remap_Qbits(qbit_map, qbit_num)
1254 if not structure
is None:
1255 structure = structure.Remap_Qbits(qbit_map, qbit_num)
1258 unitary = remapped_subcircuit.get_Matrix(
1259 np.asarray(subcircuit_parameters, dtype=np.float64)
1263 all_decomposed = qgd_Wide_Circuit_Optimization.DecomposePartition(
1264 unitary, config, mini_topology, structure=structure
1267 inverse_qbit_map = {}
1268 for key, value
in qbit_map.items():
1269 inverse_qbit_map[value] = key
1271 for decomposed_circuit, decomposed_parameters
in all_decomposed:
1274 new_subcircuit = decomposed_circuit.Remap_Qbits(
1275 inverse_qbit_map, qbit_num_orig_circuit
1278 if config[
"test_subcircuits"]:
1281 subcircuit_parameters,
1283 decomposed_parameters,
1284 parallel=config[
"parallel"],
1288 new_subcircuit = new_subcircuit.get_Flat_Circuit()
1289 result.append((new_subcircuit, decomposed_parameters))
1290 return tuple(result)
1294 """Order partition gate-sets by dependencies and build a reverse-dependency map. 1297 allparts: List of sets of gate indices, one per partition. 1300 ``(ordered_parts, rg_new)`` where ``ordered_parts`` lists partitions in 1301 topological order and ``rg_new`` maps each new index to predecessors. 1304 for i, part
in enumerate(allparts):
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):
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]
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)
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)}
1334 neworder[i]: set(neworder[j]
for j
in rg_ret[i])
1335 for i
in range(len(allparts))
1338 allparts[i]
for i
in L
1343 """ILP-based partitioning: flatten ``circ`` into a circuit of sub-circuits with concatenated parameters. 1346 ``(partitioned_circuit, parameters, recombine_info, part_deps)`` for later fusion in 1347 ``recombine_all_partition_circuit``. 1349 from squander.partitioning.ilp
import get_all_partitions, _get_topo_order
1351 allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
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 = {
1360 for x
in single_qubit_chains
1361 if x[0]
in single_qubit_chains_pre
and x[-1]
in single_qubit_chains_post
1363 partitioned_circuit = Circuit(qbit_num_orig_circuit)
1365 allparts, part_deps = qgd_Wide_Circuit_Optimization.build_partition_topo_deps(
1368 for part
in allparts:
1369 surrounded_chains = {
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
1377 gates = frozenset.union(
1378 part, *(single_qubit_chains_prepost[v]
for v
in surrounded_chains)
1381 c = Circuit(qbit_num_orig_circuit)
1383 {x: go[x] & gates
for x
in gates},
1384 {x: rgo[x] & gates
for x
in gates},
1387 c.add_Gate(gate_dict[gate_idx])
1388 start = gate_dict[gate_idx].get_Parameter_Start_Index()
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()
1405 partitioned_circuit.add_Circuit(c)
1406 parameters = np.concatenate(params, axis=0)
1408 partitioned_circuit,
1410 (allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit),
1416 """Drop single-qubit gates that sit only at the head or tail of the dependency DAG. 1419 circ: Input circuit. 1420 params: Flat parameter array for ``circ``. 1423 ``(new_circuit, new_params)`` with head/tail single-qubit gates removed. 1426 newcirc = Circuit(circ.get_Qbit_Num())
1430 if len(gate_to_qubit[i]) == 1
and (len(g[i]) == 0
or len(rg[i]) == 0):
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()])
1436 np.empty((0,), dtype=np.float64)
1437 if len(new_params) == 0
1438 else np.concatenate(new_params, axis=0)
1443 """Hashable signature of gate layout and parameters (for decomposition caching). 1446 circ: Squander circuit. 1447 params: Parameter array associated with ``circ``. 1450 Tuple usable as a dict key for memoizing decompositions. 1452 return (circ.get_Qbit_Num(),) + tuple(
1453 (gate.get_Name(), tuple(gate.get_Involved_Qbits()))
1454 for gate
in circ.get_Gates()
1459 circ, optimized_subcircuits, optimized_parameter_list, recombine_info
1461 """Reorder optimized partitions to respect global gate dependencies. 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). 1470 ``(reordered_circuits, reordered_parameter_lists)`` in execution order. 1472 from squander.partitioning.ilp
import (
1473 topo_sort_partitions,
1475 recombine_single_qubit_chains,
1478 allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
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)]
1489 for circ
in optimized_subcircuits[: len(allparts)]
1492 struct_idxs = list(L)
1496 single_qubit_chains,
1498 [allparts[i]
for i
in L],
1500 surrounded_only=
True,
1502 single_qubit_chain_idx = {
1503 frozenset(chain): idx + len(allparts)
1504 for idx, chain
in enumerate(single_qubit_chains)
1506 for extrapart
in parts[len(struct_idxs) :]:
1507 struct_idxs.append(single_qubit_chain_idx[frozenset(extrapart)])
1509 return [optimized_subcircuits[struct_idxs[i]]
for i
in L], [
1510 optimized_parameter_list[struct_idxs[i]]
for i
in L
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. 1518 Sets ``self.config`` timing and intermediate circuit keys (e.g. ``routed_circuit``, ``optimization_time``). 1520 if not qgd_Wide_Circuit_Optimization.is_valid_routing(
1521 circ, self.
config[
"topology"]
1524 print(
"fixing topology in the circuit")
1526 self.
config[
"topology"] =
None 1528 self.
config[
"strategy"] = self.
config[
"pre-opt-strategy"]
1530 print(
"Optimizing circuit with all-to-all (a2a) connectivity")
1532 self.
config[
"all_to_all_optimization_time"] = self.
config[
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()
1541 print(
"Routing circuit to fix the topology")
1543 self.
config[
"routing_time"] = time.time() - start_time
1544 self.
config[
"routed_circuit"] = circ
1545 self.
config[
"routed_parameters"] = parameters
1547 if self.
config[
"topology"]
is not None:
1548 print(
"No additional routing is needed on the circuit")
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
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
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,
1571 model = MachineModel(circ.get_Qbit_Num(), self.
config[
"topology"])
1575 circo = Qiskit_IO.get_Qiskit_Circuit(
1576 circ, np.asarray(parameters, dtype=np.float64)
1579 bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
1581 compilation_workflow = [
1582 SetModelPass(model),
1583 build_multi_qudit_retarget_workflow(
1586 build_resynthesis_optimization_workflow(
1589 build_single_qudit_retarget_workflow(
1592 build_gate_deletion_optimization_workflow(
1598 with Compiler()
as compiler:
1599 routed_bqskit_circ, pass_data = compiler.compile(
1600 bqskit_circ, compilation_workflow,
True 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)
1608 circuit_qiskit = QuantumCircuit.from_qasm_str(
1609 OPENQASM2Language().encode(routed_bqskit_circ)
1611 newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1615 qgd_Wide_Circuit_Optimization.check_valid_routing(
1616 newcirc, self.
config[
"topology"]
1618 print(
"OptimizeWideCircuit::check_compare_circuits")
1620 circ, parameters = newcirc, newparameters
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
1629 SUPPORTED_GATES_NAMES = {
1630 n.lower().replace(
"cnot",
"cx")
1632 if not n.startswith(
"_")
1633 and issubclass(getattr(gate, n), gate.Gate)
1634 and n
not in (
"Gate",
"CROT",
"CR",
"SYC",
"CCX",
"CSWAP")
1636 circo = Qiskit_IO.get_Qiskit_Circuit(
1637 circ, np.asarray(parameters, dtype=np.float64)
1641 if self.
config[
"topology"]
is None 1642 else CouplingMap([[i, j]
for i, j
in self.
config[
"topology"]])
1644 circuit_qiskit = transpile(
1646 basis_gates=SUPPORTED_GATES_NAMES,
1647 coupling_map=coupling_map,
1648 optimization_level=3,
1650 newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1653 qgd_Wide_Circuit_Optimization.check_valid_routing(
1654 newcirc, self.
config[
"topology"]
1656 print(
"OptimizeWideCircuit::check_compare_circuits")
1658 circ, parameters = newcirc, newparameters
1661 print(
"Optimizing circuit with Squander")
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)
1668 part_size_end = min(4, circ.get_Qbit_Num())
1670 fingerprint_dict = {}
1671 for max_part_size
in range(part_size_start, part_size_end + 1):
1674 {**self.
config,
"max_partition_size": max_part_size}
1678 circ_flat, parameters = (
1679 wide_circuit_optimizer.InnerOptimizeWideCircuit(
1680 circ, parameters, fingerprint_dict=fingerprint_dict
1683 circ = circ_flat.get_Flat_Circuit()
1685 no_improve = newcount >= count
1689 self.
config[
"optimization_time"] = time.time() - start_time
1690 return circ, parameters
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. 1697 The circuit is converted to a CNOT basis, partitioned, each partition is 1698 optimized (possibly in parallel), and then reconstructed into one circuit. 1701 circ: Input circuit to optimize. 1702 orig_parameters: Parameter array associated with ``circ``. 1703 fingerprint_dict: Optional decomposition cache shared across passes. 1706 Tuple of ``(optimized_circuit, optimized_parameters)``. 1711 global_min = self.
config.get(
"global_min",
True)
1713 partitioned_circuit, parameters, recombine_info, part_deps = (
1714 qgd_Wide_Circuit_Optimization.make_all_partition_circuit(
1724 strategy=self.
config[
"partition_strategy"],
1728 subcircuits = partitioned_circuit.get_Gates()
1732 in_parent = parent_process()
is not None 1735 print(len(subcircuits),
"partitions found to optimize")
1738 optimized_subcircuits: List[Optional[Circuit]] = [
None] * len(subcircuits)
1741 optimized_parameter_list: List[Optional[List[np.ndarray]]] = [
None] * len(
1746 async_results = [
None] * len(subcircuits)
1751 """Finalize async decomposition for partition ``partition_idx`` and update caches / lists.""" 1752 if optimized_subcircuits[partition_idx]
is not None:
1754 subcircuit = subcircuits[partition_idx]
1756 start_idx = subcircuit.get_Parameter_Start_Index()
1757 subcircuit_parameters = parameters[
1758 start_idx : start_idx + subcircuit.get_Parameter_Num()
1762 if fingerprint_dict
is None 1763 else qgd_Wide_Circuit_Optimization.get_fingerprint(
1764 subcircuit, subcircuit_parameters
1768 [subcircuit, *(z[0]
for z
in x)],
1769 [subcircuit_parameters, *(z[1]
for z
in x)],
1772 if fingerprint_dict
is not None and fingerprint
in fingerprint_dict:
1773 new_subcircuit, new_parameters = fingerprint_dict[fingerprint]
1775 new_subcircuit, new_parameters = callback_fnc(
1776 async_results[partition_idx][0](*async_results[partition_idx][1])
1778 else async_results[partition_idx].get(timeout=
None)
1781 if subcircuit != new_subcircuit:
1783 "original subcircuit: ",
1784 subcircuit.get_Gate_Nums(),
1787 print(
"reoptimized subcircuit: ", new_subcircuit.get_Gate_Nums())
1788 if fingerprint_dict
is not None:
1789 fingerprint_dict[fingerprint] = (new_subcircuit, new_parameters)
1791 qgd_Wide_Circuit_Optimization.get_fingerprint(
1792 new_subcircuit, new_parameters
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
1801 qgd_Wide_Circuit_Optimization.get_fingerprint(
1802 trim_subcirc, trim_parameters
1804 ] = (trim_subcirc, trim_parameters)
1805 if total_opt[0] % 100 == 99:
1806 print(total_opt[0] + 1,
"partitions optimized")
1808 optimized_subcircuits[partition_idx] = new_subcircuit
1809 optimized_parameter_list[partition_idx] = new_parameters
1812 contextlib.nullcontext()
if in_parent
else Pool(processes=mp.cpu_count())
1814 remaining = list(range(len(subcircuits)))
1816 still_remaining = []
1818 for partition_idx
in remaining:
1819 subcircuit = subcircuits[partition_idx]
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]
1828 if fingerprint_dict
is None 1829 else qgd_Wide_Circuit_Optimization.get_fingerprint(
1830 subcircuit, subcircuit_parameters
1833 if fingerprint_dict
is not None and fingerprint
in fingerprint_dict:
1835 optimized_subcircuits[partition_idx],
1836 optimized_parameter_list[partition_idx],
1837 ) = fingerprint_dict[fingerprint]
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()
1847 any_remaining =
True 1849 elif optimized_subcircuits[dep_idx]
is None:
1852 optimized_subcircuits_loc = optimized_subcircuits[dep_idx]
1853 assert isinstance(optimized_subcircuits_loc, Circuit)
1854 assert optimized_subcircuits_loc
is not None 1857 subcircuits[dep_idx]
1859 any_optimized =
True 1862 optimized_subcircuits[partition_idx] = subcircuit
1863 optimized_parameter_list[partition_idx] = (
1864 subcircuit_parameters
1868 still_remaining.append(partition_idx)
1873 "tree_level_max": qgd_Wide_Circuit_Optimization.partition_tree_level_max(
1879 (subcircuit, subcircuit_parameters, config,
None),
1882 async_results[partition_idx] = (
1883 fargs
if in_parent
else pool.apply_async(*fargs)
1885 if len(remaining) == len(still_remaining):
1887 remaining = still_remaining
1889 for partition_idx
in range(len(subcircuits)):
1894 optimized_subcircuits, optimized_parameter_list = (
1895 qgd_Wide_Circuit_Optimization.recombine_all_partition_circuit(
1897 optimized_subcircuits,
1898 optimized_parameter_list,
1903 if any(c
is None for c
in optimized_subcircuits)
or any(
1904 p
is None for p
in optimized_parameter_list
1907 "Internal error: some partitions were not optimized before reconstruction." 1910 cast(List[Circuit], optimized_subcircuits),
1911 cast(List[List[np.ndarray]], optimized_parameter_list),
1915 print(
"original circuit: ", circ.get_Gate_Nums())
1916 print(
"reoptimized circuit: ", wide_circuit.get_Gate_Nums())
1918 qgd_Wide_Circuit_Optimization.check_valid_routing(
1919 wide_circuit, self.
config[
"topology"]
1926 label=
"InnerOptimizeWideCircuit",
1929 return wide_circuit, wide_parameters
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)]
1938 """Path graph couplers ``(i, i+1)``.""" 1939 return [(i, i + 1)
for i
in range(num_qubits - 1)]
1943 """Star graph: hub qubit ``0`` connected to all others.""" 1944 return [(0, i)
for i
in range(1, num_qubits)]
1948 """Ring couplers including wrap-around ``(n-1, 0)``.""" 1949 return [(i, (i + 1) % num_qubits)
for i
in range(num_qubits)]
1953 """2D grid of size ``x_qbits`` by ``y_qbits`` with nearest-neighbor horizontal and vertical edges.""" 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)
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)
1966 """Build a finite heavy-hex coupling list (honeycomb with subdivided edges). 1969 rows: Number of rows in the brick-wall honeycomb patch. 1970 cols: Number of columns in the patch. 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. 1979 """Linear index for honeycomb vertex at row ``r``, column ``c``.""" 1985 for r
in range(rows):
1986 for c
in range(cols):
1989 base_edges.append((vid(r, c), vid(r + 1, c)))
1992 if c + 1 < cols
and ((r + c) % 2 == 0):
1993 base_edges.append((vid(r, c), vid(r, c + 1)))
1996 next_id = rows * cols
1999 for u, v
in base_edges:
2002 heavy_edges.append((u, w))
2003 heavy_edges.append((w, v))
2009 """Approximate Sycamore-like 6x9 grid topology (simplified; ignores known dead qubits).""" 2010 return qgd_Wide_Circuit_Optimization.lattice_topology(
2016 """True if every multi-qubit gate's qubits lie in a connected subgraph of undirected ``topo``.""" 2022 topo_set = {frozenset(edge)
for edge
in topo}
2024 def qubits_connected(qubits):
2025 """Whether pairwise couplers in ``topo_set`` connect all qubits in ``qubits``.""" 2026 if len(qubits) <= 1:
2030 for q1, q2
in itertools.combinations(qubits, 2)
2031 if frozenset((q1, q2))
in topo_set
2035 cur_set = set(edges.pop())
2037 next_edge = next((e
for e
in edges
if len(e & cur_set) > 0),
None)
2038 if next_edge
is None:
2040 cur_set |= next_edge
2041 edges.remove(next_edge)
2042 return set(qubits) <= cur_set
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
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()
2060 edges = {frozenset((q1,q2))
for q1,q2
in itertools.combinations(qbits,2)
if frozenset((q1,q2))
in topo_set}
2062 sys.stderr.write(f
'ROUTING_VIOLATION: {type(gate).__name__} on {qbits} topo={topo}\n')
2065 raise AssertionError(
"Final circuit contains gates that do not respect the routing constraints.")
2077 """Optionally verify equivalence of ``circ`` and ``wide_circuit`` via ``CompareCircuits``. 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`` 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. 2094 forced_test = forced_test
or (
2095 self.
config.get(
"force_small_circuit_validation",
True)
2096 and circ.get_Qbit_Num() <= 12
2098 if self.
config[
"test_final_circuit"]
or forced_test:
2099 if label
is not None:
2100 print(f
"{label}: check_compare_circuits")
2104 and self.
config.get(
"initial_mapping",
None)
is not None 2105 and self.
config.get(
"final_mapping",
None)
is not None 2112 initial_mapping=self.
config[
"initial_mapping"],
2113 final_mapping=self.
config[
"final_mapping"],
2114 tolerance=tolerance,
2123 tolerance=tolerance,
2127 """Map ``circ`` onto ``self.config['topology']`` using the configured router. 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. 2135 circ: Circuit before routing. 2136 orig_parameters: Parameter vector for ``circ``. 2139 ``(routed_circuit, routed_parameters)`` laid out for ``self.config['topology']``. 2141 strategy = self.
config.get(
"routing-strategy",
"seqpam-ilp")
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,
2152 from bqskit.passes
import (
2155 from bqskit.compiler.machine
import MachineModel
2156 from bqskit.ir.lang.qasm2
import OPENQASM2Language
2157 from qiskit
import qasm2, QuantumCircuit
2160 model = MachineModel(circ.get_Qbit_Num(), self.
config[
"topology"])
2164 circo = Qiskit_IO.get_Qiskit_Circuit(
2165 circ, np.asarray(orig_parameters, dtype=np.float64)
2168 bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
2171 if strategy ==
"seqpam-ilp":
2176 bqskit_compile_module,
2177 use_squander_partitioner=
True,
2180 mainflow = build_seqpam_mapping_optimization_workflow(
2183 elif strategy ==
"seqpam-quick":
2187 bqskit_compile_module,
2188 use_squander_partitioner=
False,
2191 mainflow = build_seqpam_mapping_optimization_workflow(
2194 elif strategy ==
"bqskit-sabre":
2195 mainflow = build_sabre_mapping_workflow()
2197 raise ValueError(f
"Unsupported BQSKit routing strategy: {strategy}")
2199 routing_workflow = [
2200 SetModelPass(model),
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(
2215 with Compiler()
as compiler:
2216 routed_bqskit_circ, pass_data = compiler.compile(
2217 bqskit_circ, routing_workflow,
True 2220 if old_patch_env
is None:
2221 _os.environ.pop(
'_SQUANDER_EAPP_FALLBACK_PATCH',
None)
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)
2227 _os.environ[
'_SQUANDER_BQSKIT_CONFIG'] = old_config_env
2230 circuit_qiskit_routed = QuantumCircuit.from_qasm_str(
2231 OPENQASM2Language().encode(routed_bqskit_circ)
2233 Squander_remapped_circuit, parameters_remapped_circuit = (
2234 Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_routed)
2236 self.
config[
"initial_mapping"] = list(pass_data.initial_mapping)
2237 self.
config[
"final_mapping"] = list(pass_data.final_mapping)
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,
2245 from qiskit.transpiler.passes
import SabreLayout, SabreSwap
2246 from qiskit.transpiler
import PassManager, CouplingMap
2250 circo = Qiskit_IO.get_Qiskit_Circuit(
2251 circ, np.asarray(orig_parameters, dtype=np.float64)
2253 coupling_map = [[i, j]
for i, j
in self.
config[
"topology"]]
2255 coupling_map = CouplingMap(coupling_map)
2257 sabre_seed = self.
config.get(
"sabre_seed", 42)
2258 sabre_trials = self.
config.get(
"sabre_trials", 5)
2259 swap_trials = self.
config.get(
"sabre_swap_trials", sabre_trials)
2260 heuristic = self.
config.get(
2261 "sabre_heuristic",
"decay" 2264 layout_pass = SabreLayout(
2267 max_iterations=sabre_trials,
2268 swap_trials=swap_trials,
2270 swap_pass = SabreSwap(
2272 heuristic=heuristic,
2283 circuit_qiskit_sabre = pm.run(circo)
2284 Squander_remapped_circuit, parameters_remapped_circuit = (
2285 Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_sabre)
2287 self.
config[
"initial_mapping"] = (
2288 circuit_qiskit_sabre.layout.initial_index_layout()
2290 self.
config[
"final_mapping"] = (
2291 circuit_qiskit_sabre.layout.final_index_layout()
2293 elif strategy ==
"sabre":
2294 sabre = SABRE(circ, self.
config[
"topology"])
2296 Squander_remapped_circuit,
2297 parameters_remapped_circuit,
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"]
2308 print(
"checking circuit after routing")
2313 Squander_remapped_circuit,
2314 parameters_remapped_circuit,
2317 label=
"route_circuit",
2319 return Squander_remapped_circuit, parameters_remapped_circuit
def _topo_perm_to_swaps(pi, topo_edges, width)
def recombine_all_partition_circuit(circ, optimized_subcircuits, optimized_parameter_list, recombine_info)
def extract_subtopology(involved_qbits, qbit_map, config)
def __init__(self, config)
def ConstructCircuitFromPartitions
def __init__(self, args, kwargs)
def check_valid_routing(wide_circuit, topo)
def is_valid_routing(wide_circuit, topo)
def _default_circuit_validation_tolerance(config)
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)
def heavy_hexagonal_topology(rows, cols)
def make_all_partition_circuit(circ, orig_parameters, max_partition_size)
def _config_uses_float32(config)
def _default_bqskit_synthesis_validation_tolerance(config)
def PartitionDecompositionProcess
def star_topology(num_qubits)
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 linear_topology(num_qubits)
def _fallback_circuit_for_permutation(original_circuit, graph, pi, po)
def _default_squander_tolerance(config)
def process_result(partition_idx)
def run(self, circuit, data=None)
def __init__(self, max_partition_size)
def _topology_edges_from_data(data)
def get_fingerprint(circ, params)
def get_Qbit_Num(self)
Call to get the number of qubits in the circuit.
def _bqskit_synthesis_validation_tolerance(config)
def synthesize(self, target, data=None)
def InnerOptimizeWideCircuit
def _circuit_validation_tolerance(config)
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)
def _squander_validation_tolerance(config)
def _copy_bqskit_synthesis_config(config)
def _get_topo_order(g, rg, gate_to_qubit)
def ilp_global_optimal(allparts, g, weighted_info=None, gurobi_direct=False, use_order=False, weights=None)
def all_to_all_topology(num_qubits)
def patched_seqpam_workflow_classes(bqskit_compile_module, use_squander_partitioner, config)
def _bqskit_location_respects_topology(location, topo_edges)
def lattice_topology(x_qbits, y_qbits)
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)
def CompareAndPickCircuits
def build_partition_topo_deps(allparts)
def circuit_to_CNOT_basis
def _assert_circuit_respects_topology(circuit, topo_edges)
def partition_tree_level_max(config, subcircuit, reduction=1)
def _data_topology(data, qbit_num)
def _append_topology_safe(new_c, op, topo_edges, width)
def ring_topology(num_qubits)
def _patch_eapp_if_needed()
def _add_swap_as_cnots(circuit, a, b)
def strip_single_qubit_head_tails(circ, params)