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 lexicographic-style score: 875 ``two_qubit_cost * max_gates + single_qubit_gate_count``. 878 circ: Squander circuit representation. 879 max_gates: Weight multiplier for the two-qubit cost term. 882 Integer gate-cost score used by optimization heuristics. 884 assert isinstance(circ, Circuit), \
885 "The input parameters should be an instance of Squander Circuit" 886 gate_counts = circ.get_Gate_Nums()
888 CNOT_COUNT_DICT.get(gate, 0) * count
for gate, count
in gate_counts.items()
891 return num_cnots * max_gates + sum(
892 y
for x, y
in gate_counts.items()
if CNOT_COUNT_DICT.get(x, -1) <= 0
898 """Count single-qubit gates in a circuit (U3, H, RX, RY, RZ, etc.). 900 Uses _GATE_DECOMPOSITION to count non-CNOT gates in each gate's breakdown. 903 circ: Squander circuit representation. 906 Total number of single-qubit gate operations when fully decomposed. 908 gate_counts = circ.get_Gate_Nums()
910 for gate, count
in gate_counts.items():
911 decomp = _GATE_DECOMPOSITION.get(gate, {})
912 total += count * sum(v
for k, v
in decomp.items()
if k !=
"CNOT")
917 """Total number of raw gate operations (single-qubit + multi-qubit). 920 circ: Squander circuit representation. 923 Total gate operation count. 925 return sum(circ.get_Gate_Nums().values())
929 """Return comprehensive gate statistics for a circuit. 931 Uses _GATE_DECOMPOSITION to compute fully-decomposed gate counts. 933 Returns dict with keys: cnot_equiv, single_qubit, total_raw, qubits, 934 and gate_breakdown (per-gate-type raw counts). 936 gate_counts = circ.get_Gate_Nums()
938 CNOT_COUNT_DICT.get(g, 0) * c
for g, c
in gate_counts.items()
941 for g, c
in gate_counts.items():
942 decomp = _GATE_DECOMPOSITION.get(g, {})
943 single += c * sum(v
for k, v
in decomp.items()
if k !=
"CNOT")
944 total = sum(gate_counts.values())
946 "cnot_equiv": cnot_equiv,
947 "single_qubit": single,
949 "qubits": circ.get_Qbit_Num(),
950 "gate_breakdown": dict(gate_counts),
955 """Optimize wide (many-qubit) circuits via partitioning and subcircuit decomposition. 957 Supports multiple decomposition strategies, optional global recombination (ILP), 958 and routing when the circuit does not match the target topology. 962 """Validate and store wide-circuit optimization ``config`` (strategy, topology, partitioning, tolerances).""" 964 config.setdefault(
"strategy",
"TreeSearch")
965 config.setdefault(
"parallel", 0)
966 config.setdefault(
"verbosity", 0)
967 config.setdefault(
"use_float",
False)
970 "circuit_validation_tolerance",
974 "bqskit_synthesis_validation_tolerance",
977 config.setdefault(
"test_subcircuits",
False)
978 config.setdefault(
"test_final_circuit",
True)
979 config.setdefault(
"max_partition_size", 3)
980 config.setdefault(
"topology",
None)
981 config.setdefault(
"partition_strategy",
"ilp")
982 config.setdefault(
"auto_expand_partition_size",
True)
983 config.setdefault(
"force_small_circuit_validation",
True)
986 strategy = config[
"strategy"]
987 allowed_startegies = [
994 if not strategy
in allowed_startegies:
996 f
"The decomposition startegy should be either of {allowed_startegies}, got {strategy}." 999 parallel = config[
"parallel"]
1000 allowed_parallel = [0, 1, 2]
1001 if not parallel
in allowed_parallel:
1003 f
"The parallel configuration should be either of {allowed_parallel}, got {parallel}." 1006 verbosity = config[
"verbosity"]
1007 if not isinstance(verbosity, int):
1008 raise Exception(f
"The verbosity parameter should be an integer.")
1010 tolerance = config[
"tolerance"]
1011 if not isinstance(tolerance, float):
1012 raise Exception(f
"The tolerance parameter should be a float.")
1014 use_float = config[
"use_float"]
1015 if not isinstance(use_float, bool):
1016 raise Exception(f
"The use_float parameter should be a bool.")
1018 bqskit_synthesis_validation_tolerance = config[
1019 "bqskit_synthesis_validation_tolerance" 1021 if not isinstance(bqskit_synthesis_validation_tolerance, float):
1023 "The bqskit_synthesis_validation_tolerance parameter should be a float." 1026 circuit_validation_tolerance = config[
"circuit_validation_tolerance"]
1027 if not isinstance(circuit_validation_tolerance, float):
1029 "The circuit_validation_tolerance parameter should be a float." 1032 test_subcircuits = config[
"test_subcircuits"]
1033 if not isinstance(test_subcircuits, bool):
1034 raise Exception(f
"The test_subcircuits parameter should be a bool.")
1036 test_final_circuit = config[
"test_final_circuit"]
1037 if not isinstance(test_final_circuit, bool):
1038 raise Exception(f
"The test_final_circuit parameter should be a bool.")
1040 max_partition_size = config[
"max_partition_size"]
1041 if not isinstance(max_partition_size, int):
1042 raise Exception(f
"The max_partition_size parameter should be an integer.")
1050 """Return the tree-search depth used for partition-local rewrites.""" 1052 target_depth = max(0,
CNOTGateCount(subcircuit, 0) - reduction)
1053 configured_limit = config.get(
"partition_tree_level_max",
None)
1054 if configured_limit
is None:
1055 configured_limit = target_depth
1056 return min(target_depth,
int(configured_limit))
1059 self, circs: List[Circuit], parameter_arrs: List[List[np.ndarray]]
1060 ) -> Tuple[Circuit, np.ndarray]:
1061 """Concatenate optimized partition circuits into a single wide circuit. 1064 circs: Partition circuits in execution order. 1065 parameter_arrs: Parameter arrays corresponding to ``circs``. 1068 Tuple of ``(wide_circuit, wide_parameters)``. 1071 if not isinstance(circs, list):
1072 raise Exception(
"First argument should be a list of squander circuits")
1074 if not isinstance(parameter_arrs, list):
1075 raise Exception(
"Second argument should be a list of numpy arrays")
1077 if len(circs) != len(parameter_arrs):
1078 raise Exception(
"The first two arguments should be of the same length")
1082 wide_parameters = np.concatenate(parameter_arrs, axis=0)
1084 wide_circuit = Circuit(qbit_num)
1087 wide_circuit.add_Circuit(circ)
1090 wide_circuit.get_Parameter_Num() == wide_parameters.size
1091 ), f
"Mismatch in the number of parameters: {wide_circuit.get_Parameter_Num()} vs {wide_parameters.size}" 1093 return wide_circuit, wide_parameters
1097 Umtx: np.ndarray, config: dict, mini_topology=
None, structure=
None 1098 ) -> list[tuple[Circuit, np.ndarray]]:
1099 """Decompose a unitary ``Umtx`` (e.g. from a partition) using ``config['strategy']``. 1102 Umtx: Complex unitary matrix. 1103 config: Must include ``strategy``, ``tolerance``, ``verbosity``, etc. 1104 mini_topology: Optional hardware couplers for topology-aware decomposers. 1105 structure: Required gate structure when ``strategy == "Custom"``. 1108 Normally ``[(circuit, parameters)]`` on success, or ``[]`` if the 1109 decomposition error exceeds ``tolerance``. If 1110 ``config.get('stop_first_solution')`` is false, returns 1111 ``cDecompose.all_solutions`` from the underlying decomposer instead of 1114 strategy = config[
"strategy"]
1115 if strategy ==
"TreeSearch":
1117 Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1119 elif strategy ==
"TabuSearch":
1121 Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1123 elif strategy ==
"Adaptive":
1128 topology=mini_topology,
1130 elif strategy ==
"Custom":
1131 cDecompose = N_Qubit_Decomposition_custom(
1132 Umtx.conj().T, config=config, accelerator_num=0
1135 structure
is not None 1136 ),
"Custom decomposition strategy requires a gate structure to be provided." 1137 cDecompose.set_Gate_Structure(structure)
1139 raise Exception(f
"Unsupported decomposition type: {strategy}")
1141 tolerance = config[
"tolerance"]
1142 cDecompose.set_Verbose(config[
"verbosity"])
1143 cDecompose.set_Cost_Function_Variant(3)
1144 cDecompose.set_Optimization_Tolerance(tolerance)
1147 cDecompose.set_Optimizer(
"BFGS")
1151 cDecompose.Start_Decomposition()
1152 except Exception
as e:
1156 if not config.get(
"stop_first_solution",
True):
1157 return cDecompose.all_solutions
1159 squander_circuit = cDecompose.get_Circuit()
1160 parameters = cDecompose.get_Optimized_Parameters()
1161 assert parameters
is not None 1163 if strategy ==
"Custom":
1164 err = cDecompose.Optimization_Problem(parameters)
1166 while err > tolerance
and it < 20:
1167 cDecompose.set_Optimized_Parameters(
1168 np.random.rand(cDecompose.get_Parameter_Num()) * (2 * np.pi)
1170 cDecompose.Start_Decomposition()
1171 parameters = cDecompose.get_Optimized_Parameters()
1172 err = cDecompose.Optimization_Problem(parameters)
1174 if err > tolerance
or it != 0:
1175 print(
"Decomposition error: ", err, it)
1177 err = cDecompose.get_Decomposition_Error()
1183 return [(squander_circuit, parameters)]
1187 circs: List[Circuit],
1188 parameter_arrs: List[np.ndarray],
1189 metric: Callable[[Circuit], int] = CNOTGateCount,
1190 ) -> tuple[Circuit, np.ndarray]:
1191 """Select the circuit with the lowest ``metric`` value. 1194 circs: Candidate Squander circuits (same length as ``parameter_arrs``). 1195 parameter_arrs: Parameter vectors aligned with ``circs``. 1196 metric: Scalar cost functional; lower is better. Defaults to ``CNOTGateCount``. 1199 ``(best_circuit, best_parameters)`` for the minimizing index. 1202 if not isinstance(circs, list):
1203 raise Exception(
"First argument should be a list of squander circuits")
1205 if not isinstance(parameter_arrs, list):
1206 raise Exception(
"Second argument should be a list of numpy arrays")
1208 if len(circs) != len(parameter_arrs):
1209 raise Exception(
"The first two arguments should be of the same length")
1211 metrics = [metric(circ)
for circ
in circs]
1213 metrics = np.array(metrics)
1215 min_idx = np.argmin(metrics)
1217 return circs[min_idx], parameter_arrs[min_idx]
1221 subcircuit: Circuit,
1222 subcircuit_parameters: np.ndarray,
1225 ) -> Tuple[Circuit, np.ndarray]:
1226 """Decompose one partition subcircuit (multiprocessing-safe entry point). 1229 subcircuit: Subcircuit acting on a subset of the wide register. 1230 subcircuit_parameters: Flat parameter vector slice for ``subcircuit``. 1231 config: Same keys as wide optimization (``strategy``, ``topology``, etc.). 1232 structure: Optional fixed gate structure when ``strategy == "Custom"``. 1235 Tuple of ``(decomposed_circuit, decomposed_parameters)`` pairs, each 1236 remapped back to the original qubit indices of ``subcircuit``. 1239 qbit_num_orig_circuit = subcircuit.get_Qbit_Num()
1241 involved_qbits = subcircuit.get_Qbits()
1243 qbit_num = len(involved_qbits)
1247 for idx
in range(len(involved_qbits)):
1248 qbit_map[involved_qbits[idx]] = idx
1249 mini_topology =
None 1250 if config[
"topology"]
is not None:
1253 remapped_subcircuit = subcircuit.Remap_Qbits(qbit_map, qbit_num)
1255 if not structure
is None:
1256 structure = structure.Remap_Qbits(qbit_map, qbit_num)
1259 unitary = remapped_subcircuit.get_Matrix(
1260 np.asarray(subcircuit_parameters, dtype=np.float64)
1264 all_decomposed = qgd_Wide_Circuit_Optimization.DecomposePartition(
1265 unitary, config, mini_topology, structure=structure
1268 inverse_qbit_map = {}
1269 for key, value
in qbit_map.items():
1270 inverse_qbit_map[value] = key
1272 for decomposed_circuit, decomposed_parameters
in all_decomposed:
1275 new_subcircuit = decomposed_circuit.Remap_Qbits(
1276 inverse_qbit_map, qbit_num_orig_circuit
1279 if config[
"test_subcircuits"]:
1282 subcircuit_parameters,
1284 decomposed_parameters,
1285 parallel=config[
"parallel"],
1289 new_subcircuit = new_subcircuit.get_Flat_Circuit()
1290 result.append((new_subcircuit, decomposed_parameters))
1291 return tuple(result)
1295 """Order partition gate-sets by dependencies and build a reverse-dependency map. 1298 allparts: List of sets of gate indices, one per partition. 1301 ``(ordered_parts, rg_new)`` where ``ordered_parts`` lists partitions in 1302 topological order and ``rg_new`` maps each new index to predecessors. 1305 for i, part
in enumerate(allparts):
1307 gate_to_parts.setdefault(gate, set()).add(i)
1308 g = {i: set()
for i
in range(len(allparts))}
1309 rg = {i: set()
for i
in range(len(allparts))}
1310 for i, part
in enumerate(allparts):
1312 for other_part
in gate_to_parts[gate]:
1313 if other_part != i
and (
1314 len(part & allparts[other_part]) > 0
1315 and (len(part) < len(allparts[other_part]))
1316 or part < allparts[other_part]
1318 g[i].add(other_part)
1319 rg[other_part].add(i)
1320 rg_ret = {i: set(rg[i])
for i
in range(len(allparts))}
1321 S = collections.deque(m
for m
in rg
if len(rg[m]) == 0)
1331 if len(L) != len(allparts):
1332 raise ValueError(
"Dependency graph is not a DAG")
1333 neworder = {old: new
for new, old
in enumerate(L)}
1335 neworder[i]: set(neworder[j]
for j
in rg_ret[i])
1336 for i
in range(len(allparts))
1339 allparts[i]
for i
in L
1344 """ILP-based partitioning: flatten ``circ`` into a circuit of sub-circuits with concatenated parameters. 1347 ``(partitioned_circuit, parameters, recombine_info, part_deps)`` for later fusion in 1348 ``recombine_all_partition_circuit``. 1350 from squander.partitioning.ilp
import get_all_partitions, _get_topo_order
1352 allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
1355 qbit_num_orig_circuit = circ.get_Qbit_Num()
1356 gate_dict = {i: gate
for i, gate
in enumerate(circ.get_Gates())}
1357 single_qubit_chains_pre = {x[0]: x
for x
in single_qubit_chains
if rgo[x[0]]}
1358 single_qubit_chains_post = {x[-1]: x
for x
in single_qubit_chains
if go[x[-1]]}
1359 single_qubit_chains_prepost = {
1361 for x
in single_qubit_chains
1362 if x[0]
in single_qubit_chains_pre
and x[-1]
in single_qubit_chains_post
1364 partitioned_circuit = Circuit(qbit_num_orig_circuit)
1366 allparts, part_deps = qgd_Wide_Circuit_Optimization.build_partition_topo_deps(
1369 for part
in allparts:
1370 surrounded_chains = {
1374 if t
in single_qubit_chains_prepost
1375 and go[single_qubit_chains_prepost[t][-1]]
1376 and next(iter(go[single_qubit_chains_prepost[t][-1]]))
in part
1378 gates = frozenset.union(
1379 part, *(single_qubit_chains_prepost[v]
for v
in surrounded_chains)
1382 c = Circuit(qbit_num_orig_circuit)
1384 {x: go[x] & gates
for x
in gates},
1385 {x: rgo[x] & gates
for x
in gates},
1388 c.add_Gate(gate_dict[gate_idx])
1389 start = gate_dict[gate_idx].get_Parameter_Start_Index()
1395 partitioned_circuit.add_Circuit(c)
1396 for chain
in single_qubit_chains:
1397 c = Circuit(qbit_num_orig_circuit)
1398 for gate_idx
in chain:
1399 c.add_Gate(gate_dict[gate_idx])
1400 start = gate_dict[gate_idx].get_Parameter_Start_Index()
1406 partitioned_circuit.add_Circuit(c)
1407 parameters = np.concatenate(params, axis=0)
1409 partitioned_circuit,
1411 (allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit),
1417 """Drop single-qubit gates that sit only at the head or tail of the dependency DAG. 1420 circ: Input circuit. 1421 params: Flat parameter array for ``circ``. 1424 ``(new_circuit, new_params)`` with head/tail single-qubit gates removed. 1427 newcirc = Circuit(circ.get_Qbit_Num())
1431 if len(gate_to_qubit[i]) == 1
and (len(g[i]) == 0
or len(rg[i]) == 0):
1433 newcirc.add_Gate(gate)
1434 start_idx = gate.get_Parameter_Start_Index()
1435 new_params.append(params[start_idx : start_idx + gate.get_Parameter_Num()])
1437 np.empty((0,), dtype=np.float64)
1438 if len(new_params) == 0
1439 else np.concatenate(new_params, axis=0)
1444 """Hashable signature of gate layout and parameters (for decomposition caching). 1447 circ: Squander circuit. 1448 params: Parameter array associated with ``circ``. 1451 Tuple usable as a dict key for memoizing decompositions. 1454 (gate.get_Name(), tuple(gate.get_Involved_Qbits()))
1455 for gate
in circ.get_Gates()
1460 circ, optimized_subcircuits, optimized_parameter_list, recombine_info
1462 """Reorder optimized partitions to respect global gate dependencies. 1465 circ: Original flat circuit (for topological ordering context). 1466 optimized_subcircuits: One optimized subcircuit per partition slot. 1467 optimized_parameter_list: Parameter lists aligned with ``optimized_subcircuits``. 1468 recombine_info: Tuple from ``make_all_partition_circuit`` (ILP metadata). 1471 ``(reordered_circuits, reordered_parameter_lists)`` in execution order. 1473 from squander.partitioning.ilp
import (
1474 topo_sort_partitions,
1476 recombine_single_qubit_chains,
1479 allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
1483 sum(y
for x, y
in c.get_Gate_Nums().items()
if CNOT_COUNT_DICT.get(x, -1) <= 0)
1484 for c
in optimized_subcircuits[: len(allparts)]
1488 for circ
in optimized_subcircuits[: len(allparts)]
1491 struct_idxs = list(L)
1495 single_qubit_chains,
1497 [allparts[i]
for i
in L],
1499 surrounded_only=
True,
1501 single_qubit_chain_idx = {
1502 frozenset(chain): idx + len(allparts)
1503 for idx, chain
in enumerate(single_qubit_chains)
1505 for extrapart
in parts[len(struct_idxs) :]:
1506 struct_idxs.append(single_qubit_chain_idx[frozenset(extrapart)])
1508 return [optimized_subcircuits[struct_idxs[i]]
for i
in L], [
1509 optimized_parameter_list[struct_idxs[i]]
for i
in L
1513 self, circ: Circuit, parameters: np.ndarray
1514 ) -> Tuple[Circuit, np.ndarray]:
1515 """Top-level wide-circuit pass: optional routing, then Qiskit / BQSKit / Squander partition optimization. 1517 Sets ``self.config`` timing and intermediate circuit keys (e.g. ``routed_circuit``, ``optimization_time``). 1519 if not qgd_Wide_Circuit_Optimization.is_valid_routing(
1520 circ, self.
config[
"topology"]
1523 print(
"fixing topology in the circuit")
1525 self.
config[
"topology"] =
None 1527 self.
config[
"strategy"] = self.
config[
"pre-opt-strategy"]
1529 print(
"Optimizing circuit with all-to-all (a2a) connectivity")
1531 self.
config[
"all_to_all_optimization_time"] = self.
config[
1534 self.
config[
"all_to_all_circuit"] = circ
1535 self.
config[
"all_to_all_parameters"] = parameters
1536 self.
config[
"strategy"] = strat
1537 self.
config[
"topology"] = topo
1538 start_time = time.time()
1540 print(
"Routing circuit to fix the topology")
1542 self.
config[
"routing_time"] = time.time() - start_time
1543 self.
config[
"routed_circuit"] = circ
1544 self.
config[
"routed_parameters"] = parameters
1546 if self.
config[
"topology"]
is not None:
1547 print(
"No additional routing is needed on the circuit")
1549 start_time = time.time()
1550 if self.
config[
"strategy"] ==
"bqskit":
1551 print(
"Optimizing circuit with BQSkit")
1552 from squander
import Qiskit_IO
1553 from bqskit
import compile
1555 from bqskit.compiler.machine
import MachineModel
1556 from bqskit.compiler
import Compiler
1557 from bqskit.ir.lang.qasm2
import OPENQASM2Language
1558 from qiskit
import qasm2, QuantumCircuit
1560 from bqskit.passes
import SetModelPass
1561 from bqskit.compiler.compile
import (
1562 build_multi_qudit_retarget_workflow,
1563 build_resynthesis_optimization_workflow,
1564 build_single_qudit_retarget_workflow,
1565 build_gate_deletion_optimization_workflow,
1570 model = MachineModel(circ.get_Qbit_Num(), self.
config[
"topology"])
1574 circo = Qiskit_IO.get_Qiskit_Circuit(
1575 circ, np.asarray(parameters, dtype=np.float64)
1578 bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
1580 compilation_workflow = [
1581 SetModelPass(model),
1582 build_multi_qudit_retarget_workflow(
1585 build_resynthesis_optimization_workflow(
1588 build_single_qudit_retarget_workflow(
1591 build_gate_deletion_optimization_workflow(
1597 with Compiler()
as compiler:
1598 routed_bqskit_circ, pass_data = compiler.compile(
1599 bqskit_circ, compilation_workflow,
True 1602 default = list(range(bqskit_circ.num_qudits))
1603 initial_map = pass_data.get(
"initial_mapping", default)
1604 final_map = pass_data.get(
"final_mapping", default)
1607 circuit_qiskit = QuantumCircuit.from_qasm_str(
1608 OPENQASM2Language().encode(routed_bqskit_circ)
1610 newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1614 qgd_Wide_Circuit_Optimization.check_valid_routing(
1615 newcirc, self.
config[
"topology"]
1617 print(
"OptimizeWideCircuit::check_compare_circuits")
1619 circ, parameters = newcirc, newparameters
1621 elif self.
config[
"strategy"] ==
"qiskit":
1622 print(
"Optimizing circuit with Qiskit")
1623 from squander
import Qiskit_IO
1624 from qiskit
import transpile
1625 from qiskit.transpiler
import CouplingMap
1628 SUPPORTED_GATES_NAMES = {
1629 n.lower().replace(
"cnot",
"cx")
1631 if not n.startswith(
"_")
1632 and issubclass(getattr(gate, n), gate.Gate)
1633 and n
not in (
"Gate",
"CROT",
"CR",
"SYC",
"CCX",
"CSWAP")
1635 circo = Qiskit_IO.get_Qiskit_Circuit(
1636 circ, np.asarray(parameters, dtype=np.float64)
1640 if self.
config[
"topology"]
is None 1641 else CouplingMap([[i, j]
for i, j
in self.
config[
"topology"]])
1643 circuit_qiskit = transpile(
1645 basis_gates=SUPPORTED_GATES_NAMES,
1646 coupling_map=coupling_map,
1647 optimization_level=3,
1649 newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1652 qgd_Wide_Circuit_Optimization.check_valid_routing(
1653 newcirc, self.
config[
"topology"]
1655 print(
"OptimizeWideCircuit::check_compare_circuits")
1657 circ, parameters = newcirc, newparameters
1660 print(
"Optimizing circuit with Squander")
1663 if self.
config.get(
"auto_expand_partition_size",
True)
and (
1664 self.
config.get(
"use_osr",
False)
1665 or self.
config.get(
"use_graph_search",
False)
1667 part_size_end = min(4, circ.get_Qbit_Num())
1669 fingerprint_dict = {}
1670 for max_part_size
in range(part_size_start, part_size_end + 1):
1673 {**self.
config,
"max_partition_size": max_part_size}
1677 circ_flat, parameters = (
1678 wide_circuit_optimizer.InnerOptimizeWideCircuit(
1679 circ, parameters, fingerprint_dict=fingerprint_dict
1682 circ = circ_flat.get_Flat_Circuit()
1684 no_improve = newcount >= count
1688 self.
config[
"optimization_time"] = time.time() - start_time
1689 return circ, parameters
1692 self, circ: Circuit, orig_parameters: np.ndarray, fingerprint_dict=
None 1693 ) -> Tuple[Circuit, np.ndarray]:
1694 """Optimize one pass of wide-circuit partition decomposition. 1696 The circuit is converted to a CNOT basis, partitioned, each partition is 1697 optimized (possibly in parallel), and then reconstructed into one circuit. 1700 circ: Input circuit to optimize. 1701 orig_parameters: Parameter array associated with ``circ``. 1702 fingerprint_dict: Optional decomposition cache shared across passes. 1705 Tuple of ``(optimized_circuit, optimized_parameters)``. 1711 y
for x, y
in circ.get_Gate_Nums().items()
if CNOT_COUNT_DICT.get(x, -1) <= 0
1714 global_min = self.
config.get(
"global_min",
True)
1716 partitioned_circuit, parameters, recombine_info, part_deps = (
1717 qgd_Wide_Circuit_Optimization.make_all_partition_circuit(
1727 strategy=self.
config[
"partition_strategy"],
1731 subcircuits = partitioned_circuit.get_Gates()
1735 in_parent = parent_process()
is not None 1738 print(len(subcircuits),
"partitions found to optimize")
1741 optimized_subcircuits: List[Optional[Circuit]] = [
None] * len(subcircuits)
1744 optimized_parameter_list: List[Optional[List[np.ndarray]]] = [
None] * len(
1749 async_results = [
None] * len(subcircuits)
1754 """Finalize async decomposition for partition ``partition_idx`` and update caches / lists.""" 1755 if optimized_subcircuits[partition_idx]
is not None:
1757 subcircuit = subcircuits[partition_idx]
1759 start_idx = subcircuit.get_Parameter_Start_Index()
1760 subcircuit_parameters = parameters[
1761 start_idx : start_idx + subcircuit.get_Parameter_Num()
1765 if fingerprint_dict
is None 1766 else qgd_Wide_Circuit_Optimization.get_fingerprint(
1767 subcircuit, subcircuit_parameters
1771 [subcircuit, *(z[0]
for z
in x)],
1772 [subcircuit_parameters, *(z[1]
for z
in x)],
1775 if fingerprint_dict
is not None and fingerprint
in fingerprint_dict:
1776 new_subcircuit, new_parameters = fingerprint_dict[fingerprint]
1778 new_subcircuit, new_parameters = callback_fnc(
1779 async_results[partition_idx][0](*async_results[partition_idx][1])
1781 else async_results[partition_idx].get(timeout=
None)
1784 if subcircuit != new_subcircuit:
1786 "original subcircuit: ",
1787 subcircuit.get_Gate_Nums(),
1790 print(
"reoptimized subcircuit: ", new_subcircuit.get_Gate_Nums())
1791 if fingerprint_dict
is not None:
1792 fingerprint_dict[fingerprint] = (new_subcircuit, new_parameters)
1794 qgd_Wide_Circuit_Optimization.get_fingerprint(
1795 new_subcircuit, new_parameters
1797 ] = (new_subcircuit, new_parameters)
1798 trim_subcirc, trim_parameters = (
1799 qgd_Wide_Circuit_Optimization.strip_single_qubit_head_tails(
1800 new_subcircuit, new_parameters
1804 qgd_Wide_Circuit_Optimization.get_fingerprint(
1805 trim_subcirc, trim_parameters
1807 ] = (trim_subcirc, trim_parameters)
1808 if total_opt[0] % 100 == 99:
1809 print(total_opt[0] + 1,
"partitions optimized")
1811 optimized_subcircuits[partition_idx] = new_subcircuit
1812 optimized_parameter_list[partition_idx] = new_parameters
1815 contextlib.nullcontext()
if in_parent
else Pool(processes=mp.cpu_count())
1817 remaining = list(range(len(subcircuits)))
1819 still_remaining = []
1821 for partition_idx
in remaining:
1822 subcircuit = subcircuits[partition_idx]
1825 start_idx = subcircuit.get_Parameter_Start_Index()
1826 end_idx = start_idx + subcircuit.get_Parameter_Num()
1827 subcircuit_parameters = parameters[start_idx:end_idx]
1831 if fingerprint_dict
is None 1832 else qgd_Wide_Circuit_Optimization.get_fingerprint(
1833 subcircuit, subcircuit_parameters
1836 if fingerprint_dict
is not None and fingerprint
in fingerprint_dict:
1838 optimized_subcircuits[partition_idx],
1839 optimized_parameter_list[partition_idx],
1840 ) = fingerprint_dict[fingerprint]
1842 if part_deps
is not None and partition_idx
in part_deps:
1843 any_optimized, any_remaining =
False,
False 1844 for dep_idx
in part_deps[partition_idx]:
1845 if optimized_subcircuits[dep_idx]
is None and (
1846 async_results[dep_idx]
is None 1847 or not isinstance(async_results[dep_idx], tuple)
1848 and not async_results[dep_idx].ready()
1850 any_remaining =
True 1852 elif optimized_subcircuits[dep_idx]
is None:
1855 optimized_subcircuits_loc = optimized_subcircuits[dep_idx]
1856 assert isinstance(optimized_subcircuits_loc, Circuit)
1857 assert optimized_subcircuits_loc
is not None 1860 subcircuits[dep_idx]
1862 any_optimized =
True 1865 optimized_subcircuits[partition_idx] = subcircuit
1866 optimized_parameter_list[partition_idx] = (
1867 subcircuit_parameters
1871 still_remaining.append(partition_idx)
1876 "tree_level_max": qgd_Wide_Circuit_Optimization.partition_tree_level_max(
1882 (subcircuit, subcircuit_parameters, config,
None),
1885 async_results[partition_idx] = (
1886 fargs
if in_parent
else pool.apply_async(*fargs)
1888 if len(remaining) == len(still_remaining):
1890 remaining = still_remaining
1892 for partition_idx
in range(len(subcircuits)):
1897 optimized_subcircuits, optimized_parameter_list = (
1898 qgd_Wide_Circuit_Optimization.recombine_all_partition_circuit(
1900 optimized_subcircuits,
1901 optimized_parameter_list,
1906 if any(c
is None for c
in optimized_subcircuits)
or any(
1907 p
is None for p
in optimized_parameter_list
1910 "Internal error: some partitions were not optimized before reconstruction." 1913 cast(List[Circuit], optimized_subcircuits),
1914 cast(List[List[np.ndarray]], optimized_parameter_list),
1918 print(
"original circuit: ", circ.get_Gate_Nums())
1919 print(
"reoptimized circuit: ", wide_circuit.get_Gate_Nums())
1921 qgd_Wide_Circuit_Optimization.check_valid_routing(
1922 wide_circuit, self.
config[
"topology"]
1929 label=
"InnerOptimizeWideCircuit",
1932 return wide_circuit, wide_parameters
1936 """Undirected all-to-all coupler list for ``num_qubits`` qubits.""" 1937 return [(i, j)
for i
in range(num_qubits)
for j
in range(i + 1, num_qubits)]
1941 """Path graph couplers ``(i, i+1)``.""" 1942 return [(i, i + 1)
for i
in range(num_qubits - 1)]
1946 """Star graph: hub qubit ``0`` connected to all others.""" 1947 return [(0, i)
for i
in range(1, num_qubits)]
1951 """Ring couplers including wrap-around ``(n-1, 0)``.""" 1952 return [(i, (i + 1) % num_qubits)
for i
in range(num_qubits)]
1956 """2D grid of size ``x_qbits`` by ``y_qbits`` with nearest-neighbor horizontal and vertical edges.""" 1958 (i * x_qbits + j, i * x_qbits + (j + 1))
1959 for i
in range(y_qbits)
1960 for j
in range(x_qbits - 1)
1962 (i * x_qbits + j, (i + 1) * x_qbits + j)
1963 for i
in range(y_qbits - 1)
1964 for j
in range(x_qbits)
1969 """Build a finite heavy-hex coupling list (honeycomb with subdivided edges). 1972 rows: Number of rows in the brick-wall honeycomb patch. 1973 cols: Number of columns in the patch. 1976 List of undirected edges ``(u, v)``. The first ``rows * cols`` qubit 1977 indices are honeycomb vertices; each original edge introduces one 1978 additional degree-2 qubit on the subdivided link. 1982 """Linear index for honeycomb vertex at row ``r``, column ``c``.""" 1988 for r
in range(rows):
1989 for c
in range(cols):
1992 base_edges.append((vid(r, c), vid(r + 1, c)))
1995 if c + 1 < cols
and ((r + c) % 2 == 0):
1996 base_edges.append((vid(r, c), vid(r, c + 1)))
1999 next_id = rows * cols
2002 for u, v
in base_edges:
2005 heavy_edges.append((u, w))
2006 heavy_edges.append((w, v))
2012 """Approximate Sycamore-like 6x9 grid topology (simplified; ignores known dead qubits).""" 2013 return qgd_Wide_Circuit_Optimization.lattice_topology(
2019 """True if every multi-qubit gate's qubits lie in a connected subgraph of undirected ``topo``.""" 2025 topo_set = {frozenset(edge)
for edge
in topo}
2027 def qubits_connected(qubits):
2028 """Whether pairwise couplers in ``topo_set`` connect all qubits in ``qubits``.""" 2029 if len(qubits) <= 1:
2033 for q1, q2
in itertools.combinations(qubits, 2)
2034 if frozenset((q1, q2))
in topo_set
2038 cur_set = set(edges.pop())
2040 next_edge = next((e
for e
in edges
if len(e & cur_set) > 0),
None)
2041 if next_edge
is None:
2043 cur_set |= next_edge
2044 edges.remove(next_edge)
2045 return set(qubits) <= cur_set
2048 qubits_connected(gate.get_Involved_Qbits())
2049 for gate
in wide_circuit.get_Flat_Circuit().get_Gates()
2050 if len(gate.get_Involved_Qbits()) > 1
2055 """Assert ``is_valid_routing``; raises if any gate violates ``topo``.""" 2056 if not qgd_Wide_Circuit_Optimization.is_valid_routing(wide_circuit, topo):
2057 import itertools, sys
2058 topo_set = {frozenset(e)
for e
in topo}
2059 for gate
in wide_circuit.get_Flat_Circuit().get_Gates():
2060 qbits = gate.get_Involved_Qbits()
2063 edges = {frozenset((q1,q2))
for q1,q2
in itertools.combinations(qbits,2)
if frozenset((q1,q2))
in topo_set}
2065 sys.stderr.write(f
'ROUTING_VIOLATION: {type(gate).__name__} on {qbits} topo={topo}\n')
2068 raise AssertionError(
"Final circuit contains gates that do not respect the routing constraints.")
2080 """Optionally verify equivalence of ``circ`` and ``wide_circuit`` via ``CompareCircuits``. 2083 circ: Original circuit. 2084 orig_parameters: Parameters for ``circ``. 2085 wide_circuit: Optimized or routed circuit. 2086 wide_parameters: Parameters for ``wide_circuit``. 2087 routing: If true and initial/final mappings exist in ``self.config``, 2088 pass them to ``CompareCircuits`` for layout-aware comparison. 2089 forced_test: If true, run the comparison even when ``test_final_circuit`` 2092 ``self.config['circuit_validation_tolerance']`` is an infidelity 2093 threshold for this whole-circuit state-vector check. It is deliberately 2094 separate from ``self.config['tolerance']``, which controls block 2095 synthesis and block-level validation. 2097 forced_test = forced_test
or (
2098 self.
config.get(
"force_small_circuit_validation",
True)
2099 and circ.get_Qbit_Num() <= 12
2101 if self.
config[
"test_final_circuit"]
or forced_test:
2102 if label
is not None:
2103 print(f
"{label}: check_compare_circuits")
2107 and self.
config.get(
"initial_mapping",
None)
is not None 2108 and self.
config.get(
"final_mapping",
None)
is not None 2115 initial_mapping=self.
config[
"initial_mapping"],
2116 final_mapping=self.
config[
"final_mapping"],
2117 tolerance=tolerance,
2126 tolerance=tolerance,
2130 """Map ``circ`` onto ``self.config['topology']`` using the configured router. 2132 The strategy is ``self.config['routing-strategy']``, e.g. ``seqpam-ilp``, 2133 ``seqpam-quick``, ``bqskit-sabre``, ``light-sabre`` (Qiskit), or ``sabre`` 2134 (Squander). Writes ``initial_mapping`` and ``final_mapping`` into 2135 ``self.config`` when the backend provides them. 2138 circ: Circuit before routing. 2139 orig_parameters: Parameter vector for ``circ``. 2142 ``(routed_circuit, routed_parameters)`` laid out for ``self.config['topology']``. 2144 strategy = self.
config.get(
"routing-strategy",
"seqpam-ilp")
2146 if strategy
in (
"seqpam-ilp",
"seqpam-quick",
"bqskit-sabre"):
2147 from squander
import Qiskit_IO
2148 import bqskit.compiler.compile
as bqskit_compile_module
2149 from bqskit.compiler
import Compiler
2150 from bqskit.compiler.compile
import (
2151 build_sabre_mapping_workflow,
2152 build_seqpam_mapping_optimization_workflow,
2155 from bqskit.passes
import (
2158 from bqskit.compiler.machine
import MachineModel
2159 from bqskit.ir.lang.qasm2
import OPENQASM2Language
2160 from qiskit
import qasm2, QuantumCircuit
2163 model = MachineModel(circ.get_Qbit_Num(), self.
config[
"topology"])
2167 circo = Qiskit_IO.get_Qiskit_Circuit(
2168 circ, np.asarray(orig_parameters, dtype=np.float64)
2171 bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
2174 if strategy ==
"seqpam-ilp":
2179 bqskit_compile_module,
2180 use_squander_partitioner=
True,
2183 mainflow = build_seqpam_mapping_optimization_workflow(
2186 elif strategy ==
"seqpam-quick":
2190 bqskit_compile_module,
2191 use_squander_partitioner=
False,
2194 mainflow = build_seqpam_mapping_optimization_workflow(
2197 elif strategy ==
"bqskit-sabre":
2198 mainflow = build_sabre_mapping_workflow()
2200 raise ValueError(f
"Unsupported BQSKit routing strategy: {strategy}")
2202 routing_workflow = [
2203 SetModelPass(model),
2209 import os
as _os, json
as _json
2210 old_patch_env = _os.environ.get(
'_SQUANDER_EAPP_FALLBACK_PATCH')
2211 old_config_env = _os.environ.get(
'_SQUANDER_BQSKIT_CONFIG')
2212 _os.environ[
'_SQUANDER_EAPP_FALLBACK_PATCH'] =
'1' 2213 _os.environ[
'_SQUANDER_BQSKIT_CONFIG'] = _json.dumps(
2218 with Compiler()
as compiler:
2219 routed_bqskit_circ, pass_data = compiler.compile(
2220 bqskit_circ, routing_workflow,
True 2223 if old_patch_env
is None:
2224 _os.environ.pop(
'_SQUANDER_EAPP_FALLBACK_PATCH',
None)
2226 _os.environ[
'_SQUANDER_EAPP_FALLBACK_PATCH'] = old_patch_env
2227 if old_config_env
is None:
2228 _os.environ.pop(
'_SQUANDER_BQSKIT_CONFIG',
None)
2230 _os.environ[
'_SQUANDER_BQSKIT_CONFIG'] = old_config_env
2233 circuit_qiskit_routed = QuantumCircuit.from_qasm_str(
2234 OPENQASM2Language().encode(routed_bqskit_circ)
2236 Squander_remapped_circuit, parameters_remapped_circuit = (
2237 Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_routed)
2239 self.
config[
"initial_mapping"] = list(pass_data.initial_mapping)
2240 self.
config[
"final_mapping"] = list(pass_data.final_mapping)
2242 elif strategy ==
"light-sabre":
2243 from squander
import Qiskit_IO
2244 from qiskit
import transpile
2245 from qiskit.transpiler.preset_passmanagers
import (
2246 generate_preset_pass_manager,
2248 from qiskit.transpiler.passes
import SabreLayout, SabreSwap
2249 from qiskit.transpiler
import PassManager, CouplingMap
2253 circo = Qiskit_IO.get_Qiskit_Circuit(
2254 circ, np.asarray(orig_parameters, dtype=np.float64)
2256 coupling_map = [[i, j]
for i, j
in self.
config[
"topology"]]
2258 coupling_map = CouplingMap(coupling_map)
2260 sabre_seed = self.
config.get(
"sabre_seed", 42)
2261 sabre_trials = self.
config.get(
"sabre_trials", 5)
2262 swap_trials = self.
config.get(
"sabre_swap_trials", sabre_trials)
2263 heuristic = self.
config.get(
2264 "sabre_heuristic",
"decay" 2267 layout_pass = SabreLayout(
2270 max_iterations=sabre_trials,
2271 swap_trials=swap_trials,
2273 swap_pass = SabreSwap(
2275 heuristic=heuristic,
2286 circuit_qiskit_sabre = pm.run(circo)
2287 Squander_remapped_circuit, parameters_remapped_circuit = (
2288 Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_sabre)
2290 self.
config[
"initial_mapping"] = (
2291 circuit_qiskit_sabre.layout.initial_index_layout()
2293 self.
config[
"final_mapping"] = (
2294 circuit_qiskit_sabre.layout.final_index_layout()
2296 elif strategy ==
"sabre":
2297 sabre = SABRE(circ, self.
config[
"topology"])
2299 Squander_remapped_circuit,
2300 parameters_remapped_circuit,
2304 ) = sabre.map_circuit(orig_parameters)
2305 self.
config[
"initial_mapping"] = pi
2306 self.
config[
"final_mapping"] = final_pi
2307 qgd_Wide_Circuit_Optimization.check_valid_routing(
2308 Squander_remapped_circuit, self.
config[
"topology"]
2311 print(
"checking circuit after routing")
2316 Squander_remapped_circuit,
2317 parameters_remapped_circuit,
2320 label=
"route_circuit",
2322 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)