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_FLOAT64_TOLERANCE = 1e-10
40 SQUANDER_FLOAT32_TOLERANCE = 1e-5
41 BQSKIT_FLOAT64_SYNTHESIS_VALIDATION_TOLERANCE = 1e-8
42 BQSKIT_FLOAT32_SYNTHESIS_VALIDATION_TOLERANCE = 1e-4
46 return bool(config.get(
"use_float",
False))
51 SQUANDER_FLOAT32_TOLERANCE
53 else SQUANDER_FLOAT64_TOLERANCE
59 BQSKIT_FLOAT32_SYNTHESIS_VALIDATION_TOLERANCE
61 else BQSKIT_FLOAT64_SYNTHESIS_VALIDATION_TOLERANCE
74 "bqskit_synthesis_validation_tolerance",
80 """Copy only plain data needed by BQSKit worker processes.""" 82 def copy_value(value):
83 if value
is None or isinstance(value, (bool, int, float, str)):
85 if isinstance(value, np.generic):
87 if isinstance(value, tuple):
88 copied = [copy_value(item)
for item
in value]
89 return tuple(item
for item
in copied
if item
is not _SKIP_CONFIG_VALUE)
90 if isinstance(value, list):
91 copied = [copy_value(item)
for item
in value]
92 return [item
for item
in copied
if item
is not _SKIP_CONFIG_VALUE]
93 if isinstance(value, dict):
95 for key, item
in value.items():
96 copied_item = copy_value(item)
97 if copied_item
is not _SKIP_CONFIG_VALUE:
98 copied[key] = copied_item
100 return _SKIP_CONFIG_VALUE
103 for key, value
in config.items():
104 copied_value = copy_value(value)
105 if copied_value
is not _SKIP_CONFIG_VALUE:
106 copied_config[key] = copied_value
110 _SKIP_CONFIG_VALUE = object()
119 """Append CNOT(a,b); CNOT(b,a); CNOT(a,b) â equivalent to SWAP(a,b).""" 120 from bqskit.ir.gates
import CNOTGate
121 circuit.append_gate(CNOTGate(), [a, b])
122 circuit.append_gate(CNOTGate(), [b, a])
123 circuit.append_gate(CNOTGate(), [a, b])
135 """Append *op* to *new_c*, using SWAP bridges for edges not in *topo_edges*. 137 For gates with â¥3 qubits, decomposes via :func:`squander.utils.circuit_to_CNOT_basis` 138 and recurses on each resulting gate. 141 loc = list(op.location)
143 params = list(op.params)
if op.params
else None 145 if gate.num_qudits == 1:
147 new_c.append_gate(gate, loc, params)
149 new_c.append_gate(gate, loc)
152 if gate.num_qudits == 2:
153 u, v = loc[0], loc[1]
154 if (u, v)
in topo_edges:
156 new_c.append_gate(gate, [u, v], params)
158 new_c.append_gate(gate, [u, v])
161 adj = {i: set()
for i
in range(width)}
162 for a, b
in topo_edges:
165 from collections
import deque
172 for nb
in adj.get(node, set()):
178 raise ValueError(f
"Cannot bridge ({u},{v}) on topology")
183 while parent[node]
is not None:
186 path = list(reversed(path))
187 swaps = list(zip(path[:-2], path[1:-1]))
193 new_c.append_gate(gate, [u, cur], params)
195 new_c.append_gate(gate, [u, cur])
196 for a, b
in reversed(swaps):
201 from bqskit.ir.lang.qasm2
import OPENQASM2Language
202 from qiskit
import qasm2
205 from bqskit
import Circuit
as _BQCircuit
206 tmp_bq = _BQCircuit(width)
208 tmp_bq.append_gate(gate, loc, params)
210 tmp_bq.append_gate(gate, loc)
213 qasm_str = OPENQASM2Language().encode(tmp_bq)
214 from squander
import Qiskit_IO
as _QIO
215 qiskit_tmp = qasm2.loads(qasm_str)
216 sq_tmp, sq_params = _QIO.convert_Qiskit_to_Squander(qiskit_tmp)
223 qiskit_decomp = _QIO.get_Qiskit_Circuit(sq_decomp, sq_decomp_params)
224 bq_decomp = OPENQASM2Language().decode(qasm2.dumps(qiskit_decomp))
225 for bq_op
in bq_decomp:
230 """Return true if ``location`` can be hosted by ``topo_edges``.""" 231 loc = tuple(
int(q)
for q
in location)
235 return (loc[0], loc[1])
in topo_edges
or (loc[1], loc[0])
in topo_edges
240 adjacency = {q: set()
for q
in wanted}
241 for u, v
in topo_edges:
242 if u
in wanted
and v
in wanted:
247 for nxt
in adjacency.get(cur, ()):
251 return wanted <= seen
255 """Raise AssertionError if ``circuit`` violates ``topo_edges``. 257 Topology violations indicate a critical logic bug â the circuit cannot 258 physically execute on the target hardware. Execution must stop 259 immediately so the root cause can be investigated and fixed. 262 if op.gate.num_qudits <= 1:
265 raise AssertionError(
266 f
"BUG: circuit contains {op.gate.name} on {list(op.location)}, " 267 f
"outside topology {sorted(topo_edges)}." 272 """Build a topology-valid fallback for ``Po.T @ U @ Pi``. 274 ``original_circuit`` is the block circuit passed into BQSKit's 275 EmbedAllPermutationsPass. ``graph`` is the block-local coupling graph 276 selected by EAPP for this synthesis attempt. 278 from bqskit
import Circuit
as _BQCircuit
280 width = original_circuit.num_qudits
281 if len(pi) != width
or len(po) != width:
283 f
"Permutation width mismatch for fallback: {pi}, {po}, width={width}." 288 topo_edges.add((u, v))
289 topo_edges.add((v, u))
291 fallback = _BQCircuit(width, original_circuit.radixes)
294 if (a, b)
not in topo_edges:
296 f
"Cannot realize input permutation {pi} on topology {sorted(topo_edges)}." 300 for op
in original_circuit:
303 po_inv = tuple(po.index(k)
for k
in range(width))
305 if (a, b)
not in topo_edges:
307 f
"Cannot realize output permutation {po} on topology {sorted(topo_edges)}." 324 """Run Squander synthesis, falling back only for explicit Squander misses.""" 326 return await inner_synthesis.synthesize(target, target_data)
327 except _SquanderSynthesisFailed:
332 """Monkey-patch EAPP.run to catch Squander OSR failures per permutation. 334 IMPORTANT: This patch fully replaces ``EmbedAllPermutationsPass.run``. 335 It was written against BQSKit's internal EAPP implementation as of 336 the pip-installed version (see pyproject.toml / requirements for the 337 exact version). If BQSKit changes its EAPP internals (scoring function, 338 subtopology selection, permutation handling, or pass data keys), this 339 patch may silently diverge and should be re-audited against the new 343 if not _os.environ.get(
'_SQUANDER_EAPP_FALLBACK_PATCH'):
346 from bqskit.passes.mapping.embed
import EmbedAllPermutationsPass
as __EAPP
347 if getattr(__EAPP.run,
"_squander_fallback_patch",
False):
350 async
def __patched_eapp_run(self, circuit, data):
352 import itertools
as _it
353 import logging
as _logging
354 from bqskit.compiler.machine
import MachineModel
as _MachineModel
355 from bqskit.passes.mapping.topology
import SubtopologySelectionPass
as _STSP
356 from bqskit.qis.graph
import CouplingGraph
as _CouplingGraph
357 from bqskit.qis.permutation
import PermutationMatrix
as _PermutationMatrix
358 from bqskit.runtime
import get_runtime
as _get_runtime
360 _logger = _logging.getLogger(
"bqskit.passes.mapping.embed")
363 if not all(r == utry.radixes[0]
for r
in utry.radixes):
364 raise NotImplementedError(
365 'PermutationAwareSynthesisPass only supports unitaries ' 366 'with the same radix on all qudits currently.',
369 width = utry.num_qudits
370 perms = list(_it.permutations(range(width)))
371 no_perm = [tuple(range(width))]
373 _PermutationMatrix.from_qudit_location(width, utry.radixes[0], p)
377 _PermutationMatrix.from_qudit_location(width, utry.radixes[0], p)
381 if self.input_perm
and self.output_perm:
382 permsbyperms = list(_it.product(perms, perms))
383 targets = [Po.T @ utry @ Pi
for Pi, Po
in _it.product(Pis, Pos)]
384 elif self.input_perm:
385 permsbyperms = list(_it.product(perms, no_perm))
386 targets = [utry @ Pi
for Pi
in Pis]
387 elif self.output_perm:
388 permsbyperms = list(_it.product(no_perm, perms))
389 targets = [Po.T @ utry
for Po
in Pos]
391 _logger.warning(
'No permutation is being used in PAS.')
392 permsbyperms = list(_it.product(no_perm, no_perm))
395 if self.vary_topology
and width != 1:
396 if _STSP.key
not in data:
398 'Cannot find subtopologies, try running a' 399 ' SubtopologySelectionPass first.',
401 if width
not in data[_STSP.key]:
403 'Subtopology information for block size' 404 f
' {width} is not available.',
406 graphs = data[_STSP.key][width]
408 graphs = [_CouplingGraph.all_to_all(width)]
412 model = _MachineModel(
413 circuit.num_qudits, graph,
414 data.gate_set, data.model.radixes,
416 target_data = _copy.deepcopy(data)
417 target_data.model = model
418 datas.append(target_data)
420 extended_targets = []
424 original_circuits = []
425 for target_index, target
in enumerate(targets):
426 for graph_index, graph
in enumerate(graphs):
427 extended_targets.append(target)
428 extended_datas.append(datas[graph_index])
429 extended_graphs.append(graph)
430 extended_perms.append(permsbyperms[target_index])
431 original_circuits.append(circuit)
433 circuits = await _get_runtime().map(
434 _squander_synthesize_or_fallback,
435 [self.inner_synthesis] * len(extended_targets),
440 [perm[0]
for perm
in extended_perms],
441 [perm[1]
for perm
in extended_perms],
445 all_perms = list(_it.permutations(range(width)))
446 for i, synthesized
in enumerate(circuits):
447 graph = extended_graphs[i]
448 perm = extended_perms[i]
450 if graph
not in perm_data:
451 perm_data[graph] = {}
453 if perm
in perm_data[graph]:
454 s1 = self.scoring_fn(perm_data[graph][perm])
455 s2 = self.scoring_fn(synthesized)
457 perm_data[graph][perm] = synthesized
459 perm_data[graph][perm] = synthesized
461 for univ_perm
in all_perms[1:]:
462 renumber_c = synthesized.copy()
463 renumber_c.renumber_qudits(univ_perm)
464 new_pi = tuple(univ_perm[j]
for j
in perm[0])
465 new_pf = tuple(univ_perm[j]
for j
in perm[1])
466 new_graph = renumber_c.coupling_graph
467 if new_graph
not in perm_data:
468 perm_data[new_graph] = {}
470 new_perm = (new_pi, new_pf)
471 if new_perm
not in perm_data[new_graph]:
472 perm_data[new_graph][new_perm] = renumber_c
474 s1 = self.scoring_fn(perm_data[new_graph][new_perm])
475 s2 = self.scoring_fn(renumber_c)
477 perm_data[new_graph][new_perm] = renumber_c
479 if circuit.gate_set.issubset(data.model.gate_set):
480 for univ_perm
in _it.permutations(range(width)):
481 uperm = (univ_perm, univ_perm)
482 renumber_c = circuit.copy()
483 renumber_c.renumber_qudits(univ_perm)
484 new_graph = renumber_c.coupling_graph
485 new_score = self.scoring_fn(renumber_c)
486 for graph, graph_data
in perm_data.items():
487 if all(e
in graph
for e
in new_graph):
488 if uperm
not in graph_data:
489 graph_data[uperm] = renumber_c
490 elif new_score < self.scoring_fn(graph_data[uperm]):
491 graph_data[uperm] = renumber_c
493 data[
'permutation_data'] = perm_data
495 __patched_eapp_run._squander_fallback_patch =
True 496 __EAPP.run = __patched_eapp_run
503 """BQSKit pass: replace circuit body with Squander ILP partition blocks.""" 509 async
def run(self, circuit, data=None):
510 from qiskit
import qasm2, QuantumCircuit
511 from squander
import Qiskit_IO
512 from bqskit
import Circuit
as BQSKitCircuit
513 from bqskit.ir.lang.qasm2
import OPENQASM2Language
516 circ_qiskit = QuantumCircuit.from_qasm_str(
517 OPENQASM2Language().encode(circuit)
524 circ, orig_parameters = Qiskit_IO.convert_Qiskit_to_Squander(circ_qiskit)
528 partitioned_circuit_bqskit = BQSKitCircuit(circ.get_Qbit_Num())
529 for subcircuit
in partitioned_circuit.get_Gates():
530 if not isinstance(subcircuit, Circuit):
532 "Squander ILP partitioning returned a non-block gate; " 533 "BQSKit SEQPAM requires partition blocks." 536 involved_qbits = sorted(subcircuit.get_Qbits())
537 qbit_map = {qbit: idx
for idx, qbit
in enumerate(involved_qbits)}
538 subcircuit_parameters = parameters[
539 subcircuit.get_Parameter_Start_Index() :
540 subcircuit.get_Parameter_Start_Index() + subcircuit.get_Parameter_Num()
542 remapped_subcircuit = subcircuit.Remap_Qbits(qbit_map, len(involved_qbits))
543 subcircuit_qiskit = Qiskit_IO.get_Qiskit_Circuit(
544 remapped_subcircuit.get_Flat_Circuit(),
545 np.asarray(subcircuit_parameters, dtype=np.float64),
547 subcircuit_bqskit = OPENQASM2Language().decode(qasm2.dumps(subcircuit_qiskit))
548 partitioned_circuit_bqskit.append_circuit(
554 circuit.become(partitioned_circuit_bqskit,
False)
558 """BQSKit synthesis pass: optimize partition blocks with Squander. 560 Raises _SquanderSynthesisFailed when the configured Squander synthesis 561 strategy cannot produce a valid circuit for the requested subtopology. The 562 monkey-patched EmbedAllPermutationsPass catches this and installs a 563 SWAP-correct original-block fallback. 568 cfg = _SQUANDER_BQSKIT_SYNTHESIS_CONFIG
573 import os
as _os, json
as _json
574 _env = _os.environ.get(
'_SQUANDER_BQSKIT_CONFIG')
576 cfg = _json.loads(_env)
581 """Return block subtopology from *data*. 583 BQSKit labels are reversed when circuits are converted through 584 Squander/Qiskit, so the topology supplied to Squander is reversed too. 586 if data
is None or getattr(data,
"model",
None)
is None:
590 for u, v
in data.model.coupling_graph:
593 edges.append((qbit_num - 1 -
int(u), qbit_num - 1 -
int(v)))
597 for i
in range(qbit_num)
598 for j
in range(i + 1, qbit_num)
600 edge_set = {frozenset(edge)
for edge
in edges}
601 if edge_set == all_edges:
607 """Return directed topology edges from BQSKit pass data.""" 608 if data
is None or getattr(data,
"model",
None)
is None:
611 for u, v
in data.model.coupling_graph:
612 topo_edges.add((
int(u),
int(v)))
613 topo_edges.add((
int(v),
int(u)))
617 from qiskit
import qasm2
618 from squander
import Qiskit_IO
619 from bqskit.ir.lang.qasm2
import OPENQASM2Language
620 from bqskit.qis.unitary.unitarymatrix
import UnitaryMatrix
622 target_matrix = np.asarray(target)
623 qbit_num = target.num_qudits
628 "topology": mini_topology,
631 candidates = qgd_Wide_Circuit_Optimization.DecomposePartition(
634 mini_topology=mini_topology,
636 if len(candidates) == 0:
639 f
"Squander synthesis failed for {qbit_num}-qubit block " 640 f
"at tolerance {tolerance}." 643 optimized_circuit, optimized_parameters = (
644 qgd_Wide_Circuit_Optimization.CompareAndPickCircuits(
645 [candidate[0]
for candidate
in candidates],
646 [candidate[1]
for candidate
in candidates],
650 optimized_qiskit = Qiskit_IO.get_Qiskit_Circuit(
651 optimized_circuit.get_Flat_Circuit(),
652 np.asarray(optimized_parameters, dtype=np.float64),
654 synthesized = OPENQASM2Language().decode(qasm2.dumps(optimized_qiskit))
660 synthesized.renumber_qudits(
661 [qbit_num - 1 - i
for i
in range(qbit_num)]
665 if topo_edges
is not None:
668 if self.
config.get(
"bqskit_distance_test",
False):
669 target_unitary = UnitaryMatrix(target)
670 distance = target_unitary.get_distance_from(synthesized.get_unitary())
674 f
"BQSKit synthesis validation failed: {distance:.2e} > {tol:.2e}" 681 """Raised when Squander cannot synthesize a partition block.""" 685 """Decompose permutation *pi* into SWAPs using only edges in *topo_edges*. 687 Uses BFS on the topology graph to find a SWAP sequence that implements 688 the permutation. Returns a list of (u, v) pairs valid in *topo_edges*. 691 adj = {i: set()
for i
in range(width)}
692 for u, v
in topo_edges:
698 current = list(range(width))
700 for i
in range(width):
702 if current[i] == target:
705 target_pos = current.index(target)
707 from collections
import deque
708 parent = {target_pos:
None}
709 q = deque([target_pos])
720 raise _SquanderSynthesisFailed(
721 f
"Cannot realize permutation {pi} on disconnected topology " 722 f
"{sorted(topo_edges)}." 726 while parent[v]
is not None:
729 path.append(target_pos)
731 for k
in range(len(path) - 1, 0, -1):
732 a, b = path[k], path[k - 1]
735 current[a], current[b] = current[b], current[a]
739 @contextlib.contextmanager
741 """Patch BQSKit workflow factories to use Squander passes. 743 Replaces QSearch/LEAP with SquanderSynthesisPass. Squander failures are 744 caught by the EAPP patch and replaced with SWAP-correct fallbacks. 747 global _SQUANDER_BQSKIT_SYNTHESIS_CONFIG
749 import os
as _os, json
as _json
751 original_quick = bqskit_compile_module.QuickPartitioner
752 original_qsearch = bqskit_compile_module.QSearchSynthesisPass
753 original_leap = bqskit_compile_module.LEAPSynthesisPass
754 original_config = _SQUANDER_BQSKIT_SYNTHESIS_CONFIG
755 original_config_env = _os.environ.get(
'_SQUANDER_BQSKIT_CONFIG')
758 _SQUANDER_BQSKIT_SYNTHESIS_CONFIG = cfg
760 _os.environ[
'_SQUANDER_BQSKIT_CONFIG'] = _json.dumps(cfg)
761 if use_squander_partitioner:
762 bqskit_compile_module.QuickPartitioner = SquanderPartitioner
763 bqskit_compile_module.QSearchSynthesisPass = SquanderSynthesisPass
764 bqskit_compile_module.LEAPSynthesisPass = SquanderSynthesisPass
767 bqskit_compile_module.QuickPartitioner = original_quick
768 bqskit_compile_module.QSearchSynthesisPass = original_qsearch
769 bqskit_compile_module.LEAPSynthesisPass = original_leap
770 _SQUANDER_BQSKIT_SYNTHESIS_CONFIG = original_config
771 if original_config_env
is None:
772 _os.environ.pop(
'_SQUANDER_BQSKIT_CONFIG',
None)
774 _os.environ[
'_SQUANDER_BQSKIT_CONFIG'] = original_config_env
778 """Return topology edges restricted to ``involved_qbits``, with indices remapped via ``qbit_map``. 781 involved_qbits: Qubit labels present in a partition. 782 qbit_map: Maps original qubit index to local index (0..n-1). 783 config: Configuration dict containing ``topology`` as a list of edges. 786 List of ``(u, v)`` pairs in local indices, each edge fully inside the partition. 789 for edge
in config[
"topology"]:
790 if edge[0]
in involved_qbits
and edge[1]
in involved_qbits:
791 mini_topology.append((qbit_map[edge[0]], qbit_map[edge[1]]))
799 _GATE_DECOMPOSITION = {
820 "CH": {
"CNOT": 1,
"RY": 2},
821 "CZ": {
"CNOT": 1,
"H": 2},
822 "SYC": {
"CNOT": 3,
"U1": 3},
823 "CRY": {
"CNOT": 2,
"RY": 2},
824 "CU": {
"CNOT": 2,
"U1": 1,
"RZ": 3,
"RY": 2},
825 "CR": {
"CNOT": 2,
"RZ": 2,
"RY": 2},
826 "CROT": {
"CNOT": 2,
"RZ": 3,
"RY": 2},
827 "CRX": {
"CNOT": 2,
"H": 2,
"RZ": 2},
828 "CRZ": {
"CNOT": 2,
"RZ": 2},
829 "CP": {
"CNOT": 2,
"U1": 3},
830 "CCX": {
"CNOT": 6,
"H": 2,
"T": 4,
"Tdg": 3},
831 "CSWAP": {
"CNOT": 7,
"H": 1,
"T": 5,
"Tdg": 2,
"SX": 1,
"Sdg": 1,
"S": 1},
833 "RXX": {
"CNOT": 2,
"RX": 1},
834 "RYY": {
"CNOT": 2,
"RX": 4,
"RZ": 1},
835 "RZZ": {
"CNOT": 2,
"RZ": 1},
839 CNOT_COUNT_DICT = {g: d.get(
"CNOT", 0)
for g, d
in _GATE_DECOMPOSITION.items()}
843 """Compute weighted two-qubit gate count for a circuit. 845 The base count is the CNOT-equivalent cost derived from ``CNOT_COUNT_DICT``. 846 When ``max_gates > 0``, the function returns a lexicographic-style score: 847 ``two_qubit_cost * max_gates + single_qubit_gate_count``. 850 circ: Squander circuit representation. 851 max_gates: Weight multiplier for the two-qubit cost term. 854 Integer gate-cost score used by optimization heuristics. 856 assert isinstance(circ, Circuit), \
857 "The input parameters should be an instance of Squander Circuit" 858 gate_counts = circ.get_Gate_Nums()
860 CNOT_COUNT_DICT.get(gate, 0) * count
for gate, count
in gate_counts.items()
863 return num_cnots * max_gates + sum(
864 y
for x, y
in gate_counts.items()
if CNOT_COUNT_DICT.get(x, -1) <= 0
870 """Count single-qubit gates in a circuit (U3, H, RX, RY, RZ, etc.). 872 Uses _GATE_DECOMPOSITION to count non-CNOT gates in each gate's breakdown. 875 circ: Squander circuit representation. 878 Total number of single-qubit gate operations when fully decomposed. 880 gate_counts = circ.get_Gate_Nums()
882 for gate, count
in gate_counts.items():
883 decomp = _GATE_DECOMPOSITION.get(gate, {})
884 total += count * sum(v
for k, v
in decomp.items()
if k !=
"CNOT")
889 """Total number of raw gate operations (single-qubit + multi-qubit). 892 circ: Squander circuit representation. 895 Total gate operation count. 897 return sum(circ.get_Gate_Nums().values())
901 """Return comprehensive gate statistics for a circuit. 903 Uses _GATE_DECOMPOSITION to compute fully-decomposed gate counts. 905 Returns dict with keys: cnot_equiv, single_qubit, total_raw, qubits, 906 and gate_breakdown (per-gate-type raw counts). 908 gate_counts = circ.get_Gate_Nums()
910 CNOT_COUNT_DICT.get(g, 0) * c
for g, c
in gate_counts.items()
913 for g, c
in gate_counts.items():
914 decomp = _GATE_DECOMPOSITION.get(g, {})
915 single += c * sum(v
for k, v
in decomp.items()
if k !=
"CNOT")
916 total = sum(gate_counts.values())
918 "cnot_equiv": cnot_equiv,
919 "single_qubit": single,
921 "qubits": circ.get_Qbit_Num(),
922 "gate_breakdown": dict(gate_counts),
927 """Optimize wide (many-qubit) circuits via partitioning and subcircuit decomposition. 929 Supports multiple decomposition strategies, optional global recombination (ILP), 930 and routing when the circuit does not match the target topology. 934 """Validate and store wide-circuit optimization ``config`` (strategy, topology, partitioning, tolerances).""" 936 config.setdefault(
"strategy",
"TreeSearch")
937 config.setdefault(
"parallel", 0)
938 config.setdefault(
"verbosity", 0)
939 config.setdefault(
"use_float",
False)
942 "bqskit_synthesis_validation_tolerance",
945 config.setdefault(
"test_subcircuits",
False)
946 config.setdefault(
"test_final_circuit",
True)
947 config.setdefault(
"max_partition_size", 3)
948 config.setdefault(
"topology",
None)
949 config.setdefault(
"partition_strategy",
"ilp")
950 config.setdefault(
"auto_expand_partition_size",
True)
951 config.setdefault(
"force_small_circuit_validation",
True)
954 strategy = config[
"strategy"]
955 allowed_startegies = [
962 if not strategy
in allowed_startegies:
964 f
"The decomposition startegy should be either of {allowed_startegies}, got {strategy}." 967 parallel = config[
"parallel"]
968 allowed_parallel = [0, 1, 2]
969 if not parallel
in allowed_parallel:
971 f
"The parallel configuration should be either of {allowed_parallel}, got {parallel}." 974 verbosity = config[
"verbosity"]
975 if not isinstance(verbosity, int):
976 raise Exception(f
"The verbosity parameter should be an integer.")
978 tolerance = config[
"tolerance"]
979 if not isinstance(tolerance, float):
980 raise Exception(f
"The tolerance parameter should be a float.")
982 use_float = config[
"use_float"]
983 if not isinstance(use_float, bool):
984 raise Exception(f
"The use_float parameter should be a bool.")
986 bqskit_synthesis_validation_tolerance = config[
987 "bqskit_synthesis_validation_tolerance" 989 if not isinstance(bqskit_synthesis_validation_tolerance, float):
991 "The bqskit_synthesis_validation_tolerance parameter should be a float." 994 squander_validation_tolerance = config.get(
998 if squander_validation_tolerance
is not None and not isinstance(
999 squander_validation_tolerance,
1003 "The squander_validation_tolerance parameter should be a float." 1006 test_subcircuits = config[
"test_subcircuits"]
1007 if not isinstance(test_subcircuits, bool):
1008 raise Exception(f
"The test_subcircuits parameter should be a bool.")
1010 test_final_circuit = config[
"test_final_circuit"]
1011 if not isinstance(test_final_circuit, bool):
1012 raise Exception(f
"The test_final_circuit parameter should be a bool.")
1014 max_partition_size = config[
"max_partition_size"]
1015 if not isinstance(max_partition_size, int):
1016 raise Exception(f
"The max_partition_size parameter should be an integer.")
1024 """Return the tree-search depth used for partition-local rewrites.""" 1026 target_depth = max(0,
CNOTGateCount(subcircuit, 0) - reduction)
1027 configured_limit = config.get(
"partition_tree_level_max",
None)
1028 if configured_limit
is None:
1029 configured_limit = target_depth
1030 return min(target_depth,
int(configured_limit))
1033 self, circs: List[Circuit], parameter_arrs: List[List[np.ndarray]]
1034 ) -> Tuple[Circuit, np.ndarray]:
1035 """Concatenate optimized partition circuits into a single wide circuit. 1038 circs: Partition circuits in execution order. 1039 parameter_arrs: Parameter arrays corresponding to ``circs``. 1042 Tuple of ``(wide_circuit, wide_parameters)``. 1045 if not isinstance(circs, list):
1046 raise Exception(
"First argument should be a list of squander circuits")
1048 if not isinstance(parameter_arrs, list):
1049 raise Exception(
"Second argument should be a list of numpy arrays")
1051 if len(circs) != len(parameter_arrs):
1052 raise Exception(
"The first two arguments should be of the same length")
1056 wide_parameters = np.concatenate(parameter_arrs, axis=0)
1058 wide_circuit = Circuit(qbit_num)
1061 wide_circuit.add_Circuit(circ)
1064 wide_circuit.get_Parameter_Num() == wide_parameters.size
1065 ), f
"Mismatch in the number of parameters: {wide_circuit.get_Parameter_Num()} vs {wide_parameters.size}" 1067 return wide_circuit, wide_parameters
1071 Umtx: np.ndarray, config: dict, mini_topology=
None, structure=
None 1072 ) -> list[tuple[Circuit, np.ndarray]]:
1073 """Decompose a unitary ``Umtx`` (e.g. from a partition) using ``config['strategy']``. 1076 Umtx: Complex unitary matrix. 1077 config: Must include ``strategy``, ``tolerance``, ``verbosity``, etc. 1078 mini_topology: Optional hardware couplers for topology-aware decomposers. 1079 structure: Required gate structure when ``strategy == "Custom"``. 1082 Normally ``[(circuit, parameters)]`` on success, or ``[]`` if the 1083 decomposition error exceeds ``tolerance``. If 1084 ``config.get('stop_first_solution')`` is false, returns 1085 ``cDecompose.all_solutions`` from the underlying decomposer instead of 1088 strategy = config[
"strategy"]
1089 if strategy ==
"TreeSearch":
1091 Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1093 elif strategy ==
"TabuSearch":
1095 Umtx.conj().T, config=config, accelerator_num=0, topology=mini_topology
1097 elif strategy ==
"Adaptive":
1102 topology=mini_topology,
1104 elif strategy ==
"Custom":
1105 cDecompose = N_Qubit_Decomposition_custom(
1106 Umtx.conj().T, config=config, accelerator_num=0
1109 structure
is not None 1110 ),
"Custom decomposition strategy requires a gate structure to be provided." 1111 cDecompose.set_Gate_Structure(structure)
1113 raise Exception(f
"Unsupported decomposition type: {strategy}")
1115 tolerance = config[
"tolerance"]
1116 cDecompose.set_Verbose(config[
"verbosity"])
1117 cDecompose.set_Cost_Function_Variant(3)
1118 cDecompose.set_Optimization_Tolerance(tolerance)
1121 cDecompose.set_Optimizer(
"BFGS")
1125 cDecompose.Start_Decomposition()
1126 except Exception
as e:
1130 if not config.get(
"stop_first_solution",
True):
1131 return cDecompose.all_solutions
1133 squander_circuit = cDecompose.get_Circuit()
1134 parameters = cDecompose.get_Optimized_Parameters()
1135 assert parameters
is not None 1137 if strategy ==
"Custom":
1138 err = cDecompose.Optimization_Problem(parameters)
1140 while err > tolerance
and it < 20:
1141 cDecompose.set_Optimized_Parameters(
1142 np.random.rand(cDecompose.get_Parameter_Num()) * (2 * np.pi)
1144 cDecompose.Start_Decomposition()
1145 parameters = cDecompose.get_Optimized_Parameters()
1146 err = cDecompose.Optimization_Problem(parameters)
1148 if err > tolerance
or it != 0:
1149 print(
"Decomposition error: ", err, it)
1151 err = cDecompose.get_Decomposition_Error()
1157 return [(squander_circuit, parameters)]
1161 circs: List[Circuit],
1162 parameter_arrs: List[np.ndarray],
1163 metric: Callable[[Circuit], int] = CNOTGateCount,
1164 ) -> tuple[Circuit, np.ndarray]:
1165 """Select the circuit with the lowest ``metric`` value. 1168 circs: Candidate Squander circuits (same length as ``parameter_arrs``). 1169 parameter_arrs: Parameter vectors aligned with ``circs``. 1170 metric: Scalar cost functional; lower is better. Defaults to ``CNOTGateCount``. 1173 ``(best_circuit, best_parameters)`` for the minimizing index. 1176 if not isinstance(circs, list):
1177 raise Exception(
"First argument should be a list of squander circuits")
1179 if not isinstance(parameter_arrs, list):
1180 raise Exception(
"Second argument should be a list of numpy arrays")
1182 if len(circs) != len(parameter_arrs):
1183 raise Exception(
"The first two arguments should be of the same length")
1185 metrics = [metric(circ)
for circ
in circs]
1187 metrics = np.array(metrics)
1189 min_idx = np.argmin(metrics)
1191 return circs[min_idx], parameter_arrs[min_idx]
1195 subcircuit: Circuit,
1196 subcircuit_parameters: np.ndarray,
1199 ) -> Tuple[Circuit, np.ndarray]:
1200 """Decompose one partition subcircuit (multiprocessing-safe entry point). 1203 subcircuit: Subcircuit acting on a subset of the wide register. 1204 subcircuit_parameters: Flat parameter vector slice for ``subcircuit``. 1205 config: Same keys as wide optimization (``strategy``, ``topology``, etc.). 1206 structure: Optional fixed gate structure when ``strategy == "Custom"``. 1209 Tuple of ``(decomposed_circuit, decomposed_parameters)`` pairs, each 1210 remapped back to the original qubit indices of ``subcircuit``. 1213 qbit_num_orig_circuit = subcircuit.get_Qbit_Num()
1215 involved_qbits = subcircuit.get_Qbits()
1217 qbit_num = len(involved_qbits)
1221 for idx
in range(len(involved_qbits)):
1222 qbit_map[involved_qbits[idx]] = idx
1223 mini_topology =
None 1224 if config[
"topology"]
is not None:
1227 remapped_subcircuit = subcircuit.Remap_Qbits(qbit_map, qbit_num)
1229 if not structure
is None:
1230 structure = structure.Remap_Qbits(qbit_map, qbit_num)
1233 unitary = remapped_subcircuit.get_Matrix(
1234 np.asarray(subcircuit_parameters, dtype=np.float64)
1238 all_decomposed = qgd_Wide_Circuit_Optimization.DecomposePartition(
1239 unitary, config, mini_topology, structure=structure
1242 inverse_qbit_map = {}
1243 for key, value
in qbit_map.items():
1244 inverse_qbit_map[value] = key
1246 for decomposed_circuit, decomposed_parameters
in all_decomposed:
1249 new_subcircuit = decomposed_circuit.Remap_Qbits(
1250 inverse_qbit_map, qbit_num_orig_circuit
1253 if config[
"test_subcircuits"]:
1256 subcircuit_parameters,
1258 decomposed_parameters,
1259 parallel=config[
"parallel"],
1263 new_subcircuit = new_subcircuit.get_Flat_Circuit()
1264 result.append((new_subcircuit, decomposed_parameters))
1265 return tuple(result)
1269 """Order partition gate-sets by dependencies and build a reverse-dependency map. 1272 allparts: List of sets of gate indices, one per partition. 1275 ``(ordered_parts, rg_new)`` where ``ordered_parts`` lists partitions in 1276 topological order and ``rg_new`` maps each new index to predecessors. 1279 for i, part
in enumerate(allparts):
1281 gate_to_parts.setdefault(gate, set()).add(i)
1282 g = {i: set()
for i
in range(len(allparts))}
1283 rg = {i: set()
for i
in range(len(allparts))}
1284 for i, part
in enumerate(allparts):
1286 for other_part
in gate_to_parts[gate]:
1287 if other_part != i
and (
1288 len(part & allparts[other_part]) > 0
1289 and (len(part) < len(allparts[other_part]))
1290 or part < allparts[other_part]
1292 g[i].add(other_part)
1293 rg[other_part].add(i)
1294 rg_ret = {i: set(rg[i])
for i
in range(len(allparts))}
1295 S = collections.deque(m
for m
in rg
if len(rg[m]) == 0)
1305 if len(L) != len(allparts):
1306 raise ValueError(
"Dependency graph is not a DAG")
1307 neworder = {old: new
for new, old
in enumerate(L)}
1309 neworder[i]: set(neworder[j]
for j
in rg_ret[i])
1310 for i
in range(len(allparts))
1313 allparts[i]
for i
in L
1318 """ILP-based partitioning: flatten ``circ`` into a circuit of sub-circuits with concatenated parameters. 1321 ``(partitioned_circuit, parameters, recombine_info, part_deps)`` for later fusion in 1322 ``recombine_all_partition_circuit``. 1324 from squander.partitioning.ilp
import get_all_partitions, _get_topo_order
1326 allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
1329 qbit_num_orig_circuit = circ.get_Qbit_Num()
1330 gate_dict = {i: gate
for i, gate
in enumerate(circ.get_Gates())}
1331 single_qubit_chains_pre = {x[0]: x
for x
in single_qubit_chains
if rgo[x[0]]}
1332 single_qubit_chains_post = {x[-1]: x
for x
in single_qubit_chains
if go[x[-1]]}
1333 single_qubit_chains_prepost = {
1335 for x
in single_qubit_chains
1336 if x[0]
in single_qubit_chains_pre
and x[-1]
in single_qubit_chains_post
1338 partitioned_circuit = Circuit(qbit_num_orig_circuit)
1340 allparts, part_deps = qgd_Wide_Circuit_Optimization.build_partition_topo_deps(
1343 for part
in allparts:
1344 surrounded_chains = {
1348 if t
in single_qubit_chains_prepost
1349 and go[single_qubit_chains_prepost[t][-1]]
1350 and next(iter(go[single_qubit_chains_prepost[t][-1]]))
in part
1352 gates = frozenset.union(
1353 part, *(single_qubit_chains_prepost[v]
for v
in surrounded_chains)
1356 c = Circuit(qbit_num_orig_circuit)
1358 {x: go[x] & gates
for x
in gates},
1359 {x: rgo[x] & gates
for x
in gates},
1362 c.add_Gate(gate_dict[gate_idx])
1363 start = gate_dict[gate_idx].get_Parameter_Start_Index()
1369 partitioned_circuit.add_Circuit(c)
1370 for chain
in single_qubit_chains:
1371 c = Circuit(qbit_num_orig_circuit)
1372 for gate_idx
in chain:
1373 c.add_Gate(gate_dict[gate_idx])
1374 start = gate_dict[gate_idx].get_Parameter_Start_Index()
1380 partitioned_circuit.add_Circuit(c)
1381 parameters = np.concatenate(params, axis=0)
1383 partitioned_circuit,
1385 (allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit),
1391 """Drop single-qubit gates that sit only at the head or tail of the dependency DAG. 1394 circ: Input circuit. 1395 params: Flat parameter array for ``circ``. 1398 ``(new_circuit, new_params)`` with head/tail single-qubit gates removed. 1401 newcirc = Circuit(circ.get_Qbit_Num())
1405 if len(gate_to_qubit[i]) == 1
and (len(g[i]) == 0
or len(rg[i]) == 0):
1407 newcirc.add_Gate(gate)
1408 start_idx = gate.get_Parameter_Start_Index()
1409 new_params.append(params[start_idx : start_idx + gate.get_Parameter_Num()])
1411 np.empty((0,), dtype=np.float64)
1412 if len(new_params) == 0
1413 else np.concatenate(new_params, axis=0)
1418 """Hashable signature of gate layout and parameters (for decomposition caching). 1421 circ: Squander circuit. 1422 params: Parameter array associated with ``circ``. 1425 Tuple usable as a dict key for memoizing decompositions. 1428 (gate.get_Name(), tuple(gate.get_Involved_Qbits()))
1429 for gate
in circ.get_Gates()
1434 circ, optimized_subcircuits, optimized_parameter_list, recombine_info
1436 """Reorder optimized partitions to respect global gate dependencies. 1439 circ: Original flat circuit (for topological ordering context). 1440 optimized_subcircuits: One optimized subcircuit per partition slot. 1441 optimized_parameter_list: Parameter lists aligned with ``optimized_subcircuits``. 1442 recombine_info: Tuple from ``make_all_partition_circuit`` (ILP metadata). 1445 ``(reordered_circuits, reordered_parameter_lists)`` in execution order. 1447 from squander.partitioning.ilp
import (
1448 topo_sort_partitions,
1450 recombine_single_qubit_chains,
1453 allparts, g, go, rgo, single_qubit_chains, gate_to_qubit, gate_to_tqubit = (
1457 sum(y
for x, y
in c.get_Gate_Nums().items()
if CNOT_COUNT_DICT.get(x, -1) <= 0)
1458 for c
in optimized_subcircuits[: len(allparts)]
1462 for circ
in optimized_subcircuits[: len(allparts)]
1465 struct_idxs = list(L)
1469 single_qubit_chains,
1471 [allparts[i]
for i
in L],
1473 surrounded_only=
True,
1475 single_qubit_chain_idx = {
1476 frozenset(chain): idx + len(allparts)
1477 for idx, chain
in enumerate(single_qubit_chains)
1479 for extrapart
in parts[len(struct_idxs) :]:
1480 struct_idxs.append(single_qubit_chain_idx[frozenset(extrapart)])
1482 return [optimized_subcircuits[struct_idxs[i]]
for i
in L], [
1483 optimized_parameter_list[struct_idxs[i]]
for i
in L
1487 self, circ: Circuit, parameters: np.ndarray
1488 ) -> Tuple[Circuit, np.ndarray]:
1489 """Top-level wide-circuit pass: optional routing, then Qiskit / BQSKit / Squander partition optimization. 1491 Sets ``self.config`` timing and intermediate circuit keys (e.g. ``routed_circuit``, ``optimization_time``). 1493 if not qgd_Wide_Circuit_Optimization.is_valid_routing(
1494 circ, self.
config[
"topology"]
1497 print(
"fixing topology in the circuit")
1499 self.
config[
"topology"] =
None 1501 self.
config[
"strategy"] = self.
config[
"pre-opt-strategy"]
1503 print(
"Optimizing circuit with all-to-all (a2a) connectivity")
1505 self.
config[
"all_to_all_optimization_time"] = self.
config[
1508 self.
config[
"all_to_all_circuit"] = circ
1509 self.
config[
"all_to_all_parameters"] = parameters
1510 self.
config[
"strategy"] = strat
1511 self.
config[
"topology"] = topo
1512 start_time = time.time()
1514 print(
"Routing circuit to fix the topology")
1516 self.
config[
"routing_time"] = time.time() - start_time
1517 self.
config[
"routed_circuit"] = circ
1518 self.
config[
"routed_parameters"] = parameters
1520 if self.
config[
"topology"]
is not None:
1521 print(
"No additional routing is needed on the circuit")
1523 start_time = time.time()
1524 if self.
config[
"strategy"] ==
"bqskit":
1525 print(
"Optimizing circuit with BQSkit")
1526 from squander
import Qiskit_IO
1527 from bqskit
import compile
1529 from bqskit.compiler.machine
import MachineModel
1530 from bqskit.compiler
import Compiler
1531 from bqskit.ir.lang.qasm2
import OPENQASM2Language
1532 from qiskit
import qasm2, QuantumCircuit
1534 from bqskit.passes
import SetModelPass
1535 from bqskit.compiler.compile
import (
1536 build_multi_qudit_retarget_workflow,
1537 build_resynthesis_optimization_workflow,
1538 build_single_qudit_retarget_workflow,
1539 build_gate_deletion_optimization_workflow,
1544 model = MachineModel(circ.get_Qbit_Num(), self.
config[
"topology"])
1548 circo = Qiskit_IO.get_Qiskit_Circuit(
1549 circ, np.asarray(parameters, dtype=np.float64)
1552 bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
1554 compilation_workflow = [
1555 SetModelPass(model),
1556 build_multi_qudit_retarget_workflow(
1559 build_resynthesis_optimization_workflow(
1562 build_single_qudit_retarget_workflow(
1565 build_gate_deletion_optimization_workflow(
1571 with Compiler()
as compiler:
1572 routed_bqskit_circ, pass_data = compiler.compile(
1573 bqskit_circ, compilation_workflow,
True 1576 default = list(range(bqskit_circ.num_qudits))
1577 initial_map = pass_data.get(
"initial_mapping", default)
1578 final_map = pass_data.get(
"final_mapping", default)
1581 circuit_qiskit = QuantumCircuit.from_qasm_str(
1582 OPENQASM2Language().encode(routed_bqskit_circ)
1584 newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1588 qgd_Wide_Circuit_Optimization.check_valid_routing(
1589 newcirc, self.
config[
"topology"]
1591 print(
"OptimizeWideCircuit::check_compare_circuits")
1593 circ, parameters = newcirc, newparameters
1595 elif self.
config[
"strategy"] ==
"qiskit":
1596 print(
"Optimizing circuit with Qiskit")
1597 from squander
import Qiskit_IO
1598 from qiskit
import transpile
1599 from qiskit.transpiler
import CouplingMap
1602 SUPPORTED_GATES_NAMES = {
1603 n.lower().replace(
"cnot",
"cx")
1605 if not n.startswith(
"_")
1606 and issubclass(getattr(gate, n), gate.Gate)
1607 and n
not in (
"Gate",
"CROT",
"CR",
"SYC",
"CCX",
"CSWAP")
1609 circo = Qiskit_IO.get_Qiskit_Circuit(
1610 circ, np.asarray(parameters, dtype=np.float64)
1614 if self.
config[
"topology"]
is None 1615 else CouplingMap([[i, j]
for i, j
in self.
config[
"topology"]])
1617 circuit_qiskit = transpile(
1619 basis_gates=SUPPORTED_GATES_NAMES,
1620 coupling_map=coupling_map,
1621 optimization_level=3,
1623 newcirc, newparameters = Qiskit_IO.convert_Qiskit_to_Squander(
1626 qgd_Wide_Circuit_Optimization.check_valid_routing(
1627 newcirc, self.
config[
"topology"]
1629 print(
"OptimizeWideCircuit::check_compare_circuits")
1631 circ, parameters = newcirc, newparameters
1634 print(
"Optimizing circuit with Squander")
1637 if self.
config.get(
"auto_expand_partition_size",
True)
and (
1638 self.
config.get(
"use_osr",
False)
1639 or self.
config.get(
"use_graph_search",
False)
1641 part_size_end = min(4, circ.get_Qbit_Num())
1643 fingerprint_dict = {}
1644 for max_part_size
in range(part_size_start, part_size_end + 1):
1647 {**self.
config,
"max_partition_size": max_part_size}
1651 circ_flat, parameters = (
1652 wide_circuit_optimizer.InnerOptimizeWideCircuit(
1653 circ, parameters, fingerprint_dict=fingerprint_dict
1656 circ = circ_flat.get_Flat_Circuit()
1658 no_improve = newcount >= count
1662 self.
config[
"optimization_time"] = time.time() - start_time
1663 return circ, parameters
1666 self, circ: Circuit, orig_parameters: np.ndarray, fingerprint_dict=
None 1667 ) -> Tuple[Circuit, np.ndarray]:
1668 """Optimize one pass of wide-circuit partition decomposition. 1670 The circuit is converted to a CNOT basis, partitioned, each partition is 1671 optimized (possibly in parallel), and then reconstructed into one circuit. 1674 circ: Input circuit to optimize. 1675 orig_parameters: Parameter array associated with ``circ``. 1676 fingerprint_dict: Optional decomposition cache shared across passes. 1679 Tuple of ``(optimized_circuit, optimized_parameters)``. 1685 y
for x, y
in circ.get_Gate_Nums().items()
if CNOT_COUNT_DICT.get(x, -1) <= 0
1688 global_min = self.
config.get(
"global_min",
True)
1690 partitioned_circuit, parameters, recombine_info, part_deps = (
1691 qgd_Wide_Circuit_Optimization.make_all_partition_circuit(
1701 strategy=self.
config[
"partition_strategy"],
1705 subcircuits = partitioned_circuit.get_Gates()
1709 in_parent = parent_process()
is not None 1712 print(len(subcircuits),
"partitions found to optimize")
1715 optimized_subcircuits: List[Optional[Circuit]] = [
None] * len(subcircuits)
1718 optimized_parameter_list: List[Optional[List[np.ndarray]]] = [
None] * len(
1723 async_results = [
None] * len(subcircuits)
1728 """Finalize async decomposition for partition ``partition_idx`` and update caches / lists.""" 1729 if optimized_subcircuits[partition_idx]
is not None:
1731 subcircuit = subcircuits[partition_idx]
1733 start_idx = subcircuit.get_Parameter_Start_Index()
1734 subcircuit_parameters = parameters[
1735 start_idx : start_idx + subcircuit.get_Parameter_Num()
1739 if fingerprint_dict
is None 1740 else qgd_Wide_Circuit_Optimization.get_fingerprint(
1741 subcircuit, subcircuit_parameters
1745 [subcircuit, *(z[0]
for z
in x)],
1746 [subcircuit_parameters, *(z[1]
for z
in x)],
1749 if fingerprint_dict
is not None and fingerprint
in fingerprint_dict:
1750 new_subcircuit, new_parameters = fingerprint_dict[fingerprint]
1752 new_subcircuit, new_parameters = callback_fnc(
1753 async_results[partition_idx][0](*async_results[partition_idx][1])
1755 else async_results[partition_idx].get(timeout=
None)
1758 if subcircuit != new_subcircuit:
1760 "original subcircuit: ",
1761 subcircuit.get_Gate_Nums(),
1764 print(
"reoptimized subcircuit: ", new_subcircuit.get_Gate_Nums())
1765 if fingerprint_dict
is not None:
1766 fingerprint_dict[fingerprint] = (new_subcircuit, new_parameters)
1768 qgd_Wide_Circuit_Optimization.get_fingerprint(
1769 new_subcircuit, new_parameters
1771 ] = (new_subcircuit, new_parameters)
1772 trim_subcirc, trim_parameters = (
1773 qgd_Wide_Circuit_Optimization.strip_single_qubit_head_tails(
1774 new_subcircuit, new_parameters
1778 qgd_Wide_Circuit_Optimization.get_fingerprint(
1779 trim_subcirc, trim_parameters
1781 ] = (trim_subcirc, trim_parameters)
1782 if total_opt[0] % 100 == 99:
1783 print(total_opt[0] + 1,
"partitions optimized")
1785 optimized_subcircuits[partition_idx] = new_subcircuit
1786 optimized_parameter_list[partition_idx] = new_parameters
1789 contextlib.nullcontext()
if in_parent
else Pool(processes=mp.cpu_count())
1791 remaining = list(range(len(subcircuits)))
1793 still_remaining = []
1795 for partition_idx
in remaining:
1796 subcircuit = subcircuits[partition_idx]
1799 start_idx = subcircuit.get_Parameter_Start_Index()
1800 end_idx = start_idx + subcircuit.get_Parameter_Num()
1801 subcircuit_parameters = parameters[start_idx:end_idx]
1805 if fingerprint_dict
is None 1806 else qgd_Wide_Circuit_Optimization.get_fingerprint(
1807 subcircuit, subcircuit_parameters
1810 if fingerprint_dict
is not None and fingerprint
in fingerprint_dict:
1812 optimized_subcircuits[partition_idx],
1813 optimized_parameter_list[partition_idx],
1814 ) = fingerprint_dict[fingerprint]
1816 if part_deps
is not None and partition_idx
in part_deps:
1817 any_optimized, any_remaining =
False,
False 1818 for dep_idx
in part_deps[partition_idx]:
1819 if optimized_subcircuits[dep_idx]
is None and (
1820 async_results[dep_idx]
is None 1821 or not isinstance(async_results[dep_idx], tuple)
1822 and not async_results[dep_idx].ready()
1824 any_remaining =
True 1826 elif optimized_subcircuits[dep_idx]
is None:
1829 optimized_subcircuits_loc = optimized_subcircuits[dep_idx]
1830 assert isinstance(optimized_subcircuits_loc, Circuit)
1831 assert optimized_subcircuits_loc
is not None 1834 subcircuits[dep_idx]
1836 any_optimized =
True 1839 optimized_subcircuits[partition_idx] = subcircuit
1840 optimized_parameter_list[partition_idx] = (
1841 subcircuit_parameters
1845 still_remaining.append(partition_idx)
1850 "tree_level_max": qgd_Wide_Circuit_Optimization.partition_tree_level_max(
1856 (subcircuit, subcircuit_parameters, config,
None),
1859 async_results[partition_idx] = (
1860 fargs
if in_parent
else pool.apply_async(*fargs)
1862 if len(remaining) == len(still_remaining):
1864 remaining = still_remaining
1866 for partition_idx
in range(len(subcircuits)):
1871 optimized_subcircuits, optimized_parameter_list = (
1872 qgd_Wide_Circuit_Optimization.recombine_all_partition_circuit(
1874 optimized_subcircuits,
1875 optimized_parameter_list,
1880 if any(c
is None for c
in optimized_subcircuits)
or any(
1881 p
is None for p
in optimized_parameter_list
1884 "Internal error: some partitions were not optimized before reconstruction." 1887 cast(List[Circuit], optimized_subcircuits),
1888 cast(List[List[np.ndarray]], optimized_parameter_list),
1892 print(
"original circuit: ", circ.get_Gate_Nums())
1893 print(
"reoptimized circuit: ", wide_circuit.get_Gate_Nums())
1895 qgd_Wide_Circuit_Optimization.check_valid_routing(
1896 wide_circuit, self.
config[
"topology"]
1903 label=
"InnerOptimizeWideCircuit",
1906 return wide_circuit, wide_parameters
1910 """Undirected all-to-all coupler list for ``num_qubits`` qubits.""" 1911 return [(i, j)
for i
in range(num_qubits)
for j
in range(i + 1, num_qubits)]
1915 """Path graph couplers ``(i, i+1)``.""" 1916 return [(i, i + 1)
for i
in range(num_qubits - 1)]
1920 """Star graph: hub qubit ``0`` connected to all others.""" 1921 return [(0, i)
for i
in range(1, num_qubits)]
1925 """Ring couplers including wrap-around ``(n-1, 0)``.""" 1926 return [(i, (i + 1) % num_qubits)
for i
in range(num_qubits)]
1930 """2D grid of size ``x_qbits`` by ``y_qbits`` with nearest-neighbor horizontal and vertical edges.""" 1932 (i * x_qbits + j, i * x_qbits + (j + 1))
1933 for i
in range(y_qbits)
1934 for j
in range(x_qbits - 1)
1936 (i * x_qbits + j, (i + 1) * x_qbits + j)
1937 for i
in range(y_qbits - 1)
1938 for j
in range(x_qbits)
1943 """Build a finite heavy-hex coupling list (honeycomb with subdivided edges). 1946 rows: Number of rows in the brick-wall honeycomb patch. 1947 cols: Number of columns in the patch. 1950 List of undirected edges ``(u, v)``. The first ``rows * cols`` qubit 1951 indices are honeycomb vertices; each original edge introduces one 1952 additional degree-2 qubit on the subdivided link. 1956 """Linear index for honeycomb vertex at row ``r``, column ``c``.""" 1962 for r
in range(rows):
1963 for c
in range(cols):
1966 base_edges.append((vid(r, c), vid(r + 1, c)))
1969 if c + 1 < cols
and ((r + c) % 2 == 0):
1970 base_edges.append((vid(r, c), vid(r, c + 1)))
1973 next_id = rows * cols
1976 for u, v
in base_edges:
1979 heavy_edges.append((u, w))
1980 heavy_edges.append((w, v))
1986 """Approximate Sycamore-like 6x9 grid topology (simplified; ignores known dead qubits).""" 1987 return qgd_Wide_Circuit_Optimization.lattice_topology(
1993 """True if every multi-qubit gate's qubits lie in a connected subgraph of undirected ``topo``.""" 1999 topo_set = {frozenset(edge)
for edge
in topo}
2001 def qubits_connected(qubits):
2002 """Whether pairwise couplers in ``topo_set`` connect all qubits in ``qubits``.""" 2003 if len(qubits) <= 1:
2007 for q1, q2
in itertools.combinations(qubits, 2)
2008 if frozenset((q1, q2))
in topo_set
2012 cur_set = set(edges.pop())
2014 next_edge = next((e
for e
in edges
if len(e & cur_set) > 0),
None)
2015 if next_edge
is None:
2017 cur_set |= next_edge
2018 edges.remove(next_edge)
2019 return set(qubits) <= cur_set
2022 qubits_connected(gate.get_Involved_Qbits())
2023 for gate
in wide_circuit.get_Flat_Circuit().get_Gates()
2024 if len(gate.get_Involved_Qbits()) > 1
2029 """Assert ``is_valid_routing``; raises if any gate violates ``topo``.""" 2030 if not qgd_Wide_Circuit_Optimization.is_valid_routing(wide_circuit, topo):
2031 import itertools, sys
2032 topo_set = {frozenset(e)
for e
in topo}
2033 for gate
in wide_circuit.get_Flat_Circuit().get_Gates():
2034 qbits = gate.get_Involved_Qbits()
2037 edges = {frozenset((q1,q2))
for q1,q2
in itertools.combinations(qbits,2)
if frozenset((q1,q2))
in topo_set}
2039 sys.stderr.write(f
'ROUTING_VIOLATION: {type(gate).__name__} on {qbits} topo={topo}\n')
2042 raise AssertionError(
"Final circuit contains gates that do not respect the routing constraints.")
2054 """Optionally verify equivalence of ``circ`` and ``wide_circuit`` via ``CompareCircuits``. 2057 circ: Original circuit. 2058 orig_parameters: Parameters for ``circ``. 2059 wide_circuit: Optimized or routed circuit. 2060 wide_parameters: Parameters for ``wide_circuit``. 2061 routing: If true and initial/final mappings exist in ``self.config``, 2062 pass them to ``CompareCircuits`` for layout-aware comparison. 2063 forced_test: If true, run the comparison even when ``test_final_circuit`` 2066 forced_test = forced_test
or (
2067 self.
config.get(
"force_small_circuit_validation",
True)
2068 and circ.get_Qbit_Num() <= 12
2070 if self.
config[
"test_final_circuit"]
or forced_test:
2071 if label
is not None:
2072 print(f
"{label}: check_compare_circuits")
2076 and self.
config.get(
"initial_mapping",
None)
is not None 2077 and self.
config.get(
"final_mapping",
None)
is not None 2084 initial_mapping=self.
config[
"initial_mapping"],
2085 final_mapping=self.
config[
"final_mapping"],
2086 tolerance=tolerance,
2095 tolerance=tolerance,
2099 """Map ``circ`` onto ``self.config['topology']`` using the configured router. 2101 The strategy is ``self.config['routing-strategy']``, e.g. ``seqpam-ilp``, 2102 ``seqpam-quick``, ``bqskit-sabre``, ``light-sabre`` (Qiskit), or ``sabre`` 2103 (Squander). Writes ``initial_mapping`` and ``final_mapping`` into 2104 ``self.config`` when the backend provides them. 2107 circ: Circuit before routing. 2108 orig_parameters: Parameter vector for ``circ``. 2111 ``(routed_circuit, routed_parameters)`` laid out for ``self.config['topology']``. 2113 strategy = self.
config.get(
"routing-strategy",
"seqpam-ilp")
2115 if strategy
in (
"seqpam-ilp",
"seqpam-quick",
"bqskit-sabre"):
2116 from squander
import Qiskit_IO
2117 import bqskit.compiler.compile
as bqskit_compile_module
2118 from bqskit.compiler
import Compiler
2119 from bqskit.compiler.compile
import (
2120 build_sabre_mapping_workflow,
2121 build_seqpam_mapping_optimization_workflow,
2124 from bqskit.passes
import (
2127 from bqskit.compiler.machine
import MachineModel
2128 from bqskit.ir.lang.qasm2
import OPENQASM2Language
2129 from qiskit
import qasm2, QuantumCircuit
2132 model = MachineModel(circ.get_Qbit_Num(), self.
config[
"topology"])
2136 circo = Qiskit_IO.get_Qiskit_Circuit(
2137 circ, np.asarray(orig_parameters, dtype=np.float64)
2140 bqskit_circ = OPENQASM2Language().decode(qasm2.dumps(circo))
2143 if strategy ==
"seqpam-ilp":
2148 bqskit_compile_module,
2149 use_squander_partitioner=
True,
2152 mainflow = build_seqpam_mapping_optimization_workflow(
2155 elif strategy ==
"seqpam-quick":
2159 bqskit_compile_module,
2160 use_squander_partitioner=
False,
2163 mainflow = build_seqpam_mapping_optimization_workflow(
2166 elif strategy ==
"bqskit-sabre":
2167 mainflow = build_sabre_mapping_workflow()
2169 raise ValueError(f
"Unsupported BQSKit routing strategy: {strategy}")
2171 routing_workflow = [
2172 SetModelPass(model),
2178 import os
as _os, json
as _json
2179 old_patch_env = _os.environ.get(
'_SQUANDER_EAPP_FALLBACK_PATCH')
2180 old_config_env = _os.environ.get(
'_SQUANDER_BQSKIT_CONFIG')
2181 _os.environ[
'_SQUANDER_EAPP_FALLBACK_PATCH'] =
'1' 2182 _os.environ[
'_SQUANDER_BQSKIT_CONFIG'] = _json.dumps(
2187 with Compiler()
as compiler:
2188 routed_bqskit_circ, pass_data = compiler.compile(
2189 bqskit_circ, routing_workflow,
True 2192 if old_patch_env
is None:
2193 _os.environ.pop(
'_SQUANDER_EAPP_FALLBACK_PATCH',
None)
2195 _os.environ[
'_SQUANDER_EAPP_FALLBACK_PATCH'] = old_patch_env
2196 if old_config_env
is None:
2197 _os.environ.pop(
'_SQUANDER_BQSKIT_CONFIG',
None)
2199 _os.environ[
'_SQUANDER_BQSKIT_CONFIG'] = old_config_env
2202 circuit_qiskit_routed = QuantumCircuit.from_qasm_str(
2203 OPENQASM2Language().encode(routed_bqskit_circ)
2205 Squander_remapped_circuit, parameters_remapped_circuit = (
2206 Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_routed)
2208 self.
config[
"initial_mapping"] = list(pass_data.initial_mapping)
2209 self.
config[
"final_mapping"] = list(pass_data.final_mapping)
2211 elif strategy ==
"light-sabre":
2212 from squander
import Qiskit_IO
2213 from qiskit
import transpile
2214 from qiskit.transpiler.preset_passmanagers
import (
2215 generate_preset_pass_manager,
2217 from qiskit.transpiler.passes
import SabreLayout, SabreSwap
2218 from qiskit.transpiler
import PassManager, CouplingMap
2222 circo = Qiskit_IO.get_Qiskit_Circuit(
2223 circ, np.asarray(orig_parameters, dtype=np.float64)
2225 coupling_map = [[i, j]
for i, j
in self.
config[
"topology"]]
2227 coupling_map = CouplingMap(coupling_map)
2229 sabre_seed = self.
config.get(
"sabre_seed", 42)
2230 sabre_trials = self.
config.get(
"sabre_trials", 5)
2231 swap_trials = self.
config.get(
"sabre_swap_trials", sabre_trials)
2232 heuristic = self.
config.get(
2233 "sabre_heuristic",
"decay" 2236 layout_pass = SabreLayout(
2239 max_iterations=sabre_trials,
2240 swap_trials=swap_trials,
2242 swap_pass = SabreSwap(
2244 heuristic=heuristic,
2255 circuit_qiskit_sabre = pm.run(circo)
2256 Squander_remapped_circuit, parameters_remapped_circuit = (
2257 Qiskit_IO.convert_Qiskit_to_Squander(circuit_qiskit_sabre)
2259 self.
config[
"initial_mapping"] = (
2260 circuit_qiskit_sabre.layout.initial_index_layout()
2262 self.
config[
"final_mapping"] = (
2263 circuit_qiskit_sabre.layout.final_index_layout()
2265 elif strategy ==
"sabre":
2266 sabre = SABRE(circ, self.
config[
"topology"])
2268 Squander_remapped_circuit,
2269 parameters_remapped_circuit,
2273 ) = sabre.map_circuit(orig_parameters)
2274 self.
config[
"initial_mapping"] = pi
2275 self.
config[
"final_mapping"] = final_pi
2276 qgd_Wide_Circuit_Optimization.check_valid_routing(
2277 Squander_remapped_circuit, self.
config[
"topology"]
2280 print(
"checking circuit after routing")
2285 Squander_remapped_circuit,
2286 parameters_remapped_circuit,
2289 label=
"route_circuit",
2291 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)
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 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)