Skip to content

Commit 48c9e67

Browse files
itokocoruscating
andauthored
Pluggable Clifford synthesis for RB circuits (#1288)
### Summary Make the Clifford synthesis algorithm for RB circuits pluggable (implementing it as a `HighLevelSynthesisPlugin`). Fixes #1279 and #1023. Change to accept Clifford elements consisting only of instructions supported by the backend for `interleaved_element` option in `InterleavedRB`. Speed up 2Q RB/IRB for backends with unidirectional 2q gates, e.g. IBM's 127Q Eagle processors. ### Details and comments Previously, for 3Q+ RB circuits, entire circuit is transpiled at once and hence for each of the resulting Cliffords, the initial and the final layout may differ, that means sampled Cliffords are changed during the transpilation. Also in the worst case, the resulting circuit may use physical qubits not in the supplied `physical_qubits`. To avoid that, this commit changes to transpile an RB sequence Clifford by Clifford. The Clifford synthesis algorithm (`rb_default`) is implemented as a `HighLevelSynthesisPlugin` (see `RBDefaultCliffordSynthesis` in `clifford_synthesis.py`), which forces the initial layout (i.e. guarantees the initial layout = the final layout) and physical qubits to use. As a byproduct, the performance of 2Q RB/IRB for backends with directed 2q gates (e.g. IBM's 127Q Eagle processors) is drastically improved. For those cases, previously we had to rely on `transpile` function to make generated circuits comply with the coupling map, however, after this commit, we can synthesize Cliffords with considering the 2q-gate direction and go through the fast path introduced in #982. Depends on Qiskit/qiskit#10477 (qiskit 0.45) --------- Co-authored-by: Helena Zhang <[email protected]>
1 parent 0937bb4 commit 48c9e67

File tree

11 files changed

+468
-146
lines changed

11 files changed

+468
-146
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2023.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
"""
13+
Clifford synthesis plugins for randomized benchmarking
14+
"""
15+
from __future__ import annotations
16+
17+
from typing import Sequence
18+
19+
from qiskit.circuit import QuantumCircuit, Operation
20+
from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel
21+
from qiskit.exceptions import QiskitError
22+
from qiskit.synthesis.clifford import synth_clifford_full
23+
from qiskit.transpiler import PassManager, CouplingMap, Layout, Target
24+
from qiskit.transpiler.passes import (
25+
SabreSwap,
26+
LayoutTransformation,
27+
BasisTranslator,
28+
CheckGateDirection,
29+
GateDirection,
30+
Optimize1qGatesDecomposition,
31+
)
32+
from qiskit.transpiler.passes.synthesis.plugin import HighLevelSynthesisPlugin
33+
34+
35+
class RBDefaultCliffordSynthesis(HighLevelSynthesisPlugin):
36+
"""Default Clifford synthesis plugin for randomized benchmarking."""
37+
38+
def run(
39+
self,
40+
high_level_object: Operation,
41+
coupling_map: CouplingMap | None = None,
42+
target: Target | None = None,
43+
qubits: Sequence | None = None,
44+
**options,
45+
) -> QuantumCircuit:
46+
"""Run synthesis for the given Clifford.
47+
48+
Args:
49+
high_level_object: The operation to synthesize to a
50+
:class:`~qiskit.circuit.QuantumCircuit` object.
51+
coupling_map: The reduced coupling map of the backend. For example,
52+
if physical qubits [5, 6, 7] to be benchmarked is connected
53+
as 5 - 7 - 6 linearly, the reduced coupling map is 0 - 2 - 1.
54+
target: A target representing the target backend, which will be ignored in this plugin.
55+
qubits: List of physical qubits over which the operation is defined,
56+
which will be ignored in this plugin.
57+
options: Additional method-specific optional kwargs,
58+
which must include ``basis_gates``, basis gates to be used for the synthesis.
59+
60+
Returns:
61+
The quantum circuit representation of the Operation
62+
when successful, and ``None`` otherwise.
63+
64+
Raises:
65+
QiskitError: If basis_gates is not supplied.
66+
"""
67+
# synthesize cliffords
68+
circ = synth_clifford_full(high_level_object)
69+
70+
# post processing to comply with basis gates and coupling map
71+
if coupling_map is None: # Sabre does not work with coupling_map=None
72+
return circ
73+
74+
basis_gates = options.get("basis_gates", None)
75+
if basis_gates is None:
76+
raise QiskitError("basis_gates are required to run this synthesis plugin")
77+
78+
basis_gates = list(basis_gates)
79+
80+
# Run Sabre routing and undo the layout change
81+
# assuming Sabre routing does not change the initial layout.
82+
# And then decompose swap gates, fix 2q-gate direction and optimize 1q gates
83+
initial_layout = Layout.generate_trivial_layout(*circ.qubits)
84+
undo_layout_change = LayoutTransformation(
85+
coupling_map=coupling_map, from_layout="final_layout", to_layout=initial_layout
86+
)
87+
88+
def _direction_condition(property_set):
89+
return not property_set["is_direction_mapped"]
90+
91+
pm = PassManager(
92+
[
93+
SabreSwap(coupling_map),
94+
undo_layout_change,
95+
BasisTranslator(sel, basis_gates),
96+
CheckGateDirection(coupling_map),
97+
]
98+
)
99+
pm.append([GateDirection(coupling_map)], condition=_direction_condition)
100+
pm.append([Optimize1qGatesDecomposition(basis=basis_gates)])
101+
return pm.run(circ)

qiskit_experiments/library/randomized_benchmarking/clifford_utils.py

Lines changed: 154 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@
2929
from qiskit.compiler import transpile
3030
from qiskit.exceptions import QiskitError
3131
from qiskit.quantum_info import Clifford, random_clifford
32+
from qiskit.transpiler import CouplingMap, PassManager
33+
from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig, HighLevelSynthesis
3234
from qiskit.utils.deprecation import deprecate_func
3335

36+
DEFAULT_SYNTHESIS_METHOD = "rb_default"
37+
3438
_DATA_FOLDER = os.path.join(os.path.dirname(__file__), "data")
3539

3640
_CLIFFORD_COMPOSE_1Q = np.load(f"{_DATA_FOLDER}/clifford_compose_1q.npz")["table"]
@@ -110,37 +114,141 @@ def _circuit_compose(
110114
return self
111115

112116

113-
def _truncate_inactive_qubits(
114-
circ: QuantumCircuit, active_qubits: Sequence[Qubit]
117+
def _synthesize_clifford(
118+
clifford: Clifford,
119+
basis_gates: Optional[Tuple[str]],
120+
coupling_tuple: Optional[Tuple[Tuple[int, int]]] = None,
121+
synthesis_method: str = DEFAULT_SYNTHESIS_METHOD,
115122
) -> QuantumCircuit:
116-
res = QuantumCircuit(active_qubits, name=circ.name, metadata=circ.metadata)
117-
for inst in circ:
118-
if all(q in active_qubits for q in inst.qubits):
119-
res.append(inst)
120-
res.calibrations = circ.calibrations
121-
return res
123+
"""Synthesize a circuit of a Clifford element. The resulting circuit contains only
124+
``basis_gates`` and it complies with ``coupling_tuple``.
125+
126+
Args:
127+
clifford: Clifford element to be converted
128+
basis_gates: basis gates to use in the conversion
129+
coupling_tuple: coupling map to use in the conversion in the form of tuple of edges
130+
synthesis_method: conversion algorithm name
131+
132+
Returns:
133+
Synthesized circuit
134+
"""
135+
qc = QuantumCircuit(clifford.num_qubits, name=str(clifford))
136+
qc.append(clifford, qc.qubits)
137+
return _synthesize_clifford_circuit(
138+
qc,
139+
basis_gates=basis_gates,
140+
coupling_tuple=coupling_tuple,
141+
synthesis_method=synthesis_method,
142+
)
122143

123144

124145
def _synthesize_clifford_circuit(
125-
circuit: QuantumCircuit, basis_gates: Tuple[str]
146+
circuit: QuantumCircuit,
147+
basis_gates: Optional[Tuple[str]],
148+
coupling_tuple: Optional[Tuple[Tuple[int, int]]] = None,
149+
synthesis_method: str = DEFAULT_SYNTHESIS_METHOD,
126150
) -> QuantumCircuit:
127-
# synthesizes clifford circuits using given basis gates, for use during
128-
# custom transpilation during RB circuit generation.
129-
return transpile(circuit, basis_gates=list(basis_gates), optimization_level=1)
151+
"""Convert a Clifford circuit into one composed of ``basis_gates`` with
152+
satisfying ``coupling_tuple`` using the specified synthesis method.
153+
154+
Args:
155+
circuit: Clifford circuit to be converted
156+
basis_gates: basis gates to use in the conversion
157+
coupling_tuple: coupling map to use in the conversion in the form of tuple of edges
158+
synthesis_method: name of Clifford synthesis algorithm to use
159+
160+
Returns:
161+
Synthesized circuit
162+
"""
163+
if basis_gates:
164+
basis_gates = list(basis_gates)
165+
coupling_map = CouplingMap(coupling_tuple) if coupling_tuple else None
166+
167+
# special handling for 1q or 2q case for speed
168+
if circuit.num_qubits <= 2:
169+
if synthesis_method == DEFAULT_SYNTHESIS_METHOD:
170+
return transpile(
171+
circuit,
172+
basis_gates=basis_gates,
173+
coupling_map=coupling_map,
174+
optimization_level=1,
175+
)
176+
else:
177+
# Provided custom synthesis method, re-synthesize Clifford circuit
178+
# convert the circuit back to a Clifford object and then call the synthesis plugin
179+
new_circuit = QuantumCircuit(circuit.num_qubits, name=circuit.name)
180+
new_circuit.append(Clifford(circuit), new_circuit.qubits)
181+
circuit = new_circuit
182+
183+
# for 3q+ or custom synthesis method, synthesizes clifford circuit
184+
hls_config = HLSConfig(clifford=[(synthesis_method, {"basis_gates": basis_gates})])
185+
pm = PassManager([HighLevelSynthesis(hls_config=hls_config, coupling_map=coupling_map)])
186+
circuit = pm.run(circuit)
187+
return circuit
130188

131189

132-
@lru_cache(maxsize=None)
190+
@lru_cache(maxsize=256)
133191
def _clifford_1q_int_to_instruction(
134-
num: Integral, basis_gates: Optional[Tuple[str]]
192+
num: Integral,
193+
basis_gates: Optional[Tuple[str]],
194+
synthesis_method: str = DEFAULT_SYNTHESIS_METHOD,
135195
) -> Instruction:
136-
return CliffordUtils.clifford_1_qubit_circuit(num, basis_gates).to_instruction()
196+
return CliffordUtils.clifford_1_qubit_circuit(
197+
num, basis_gates=basis_gates, synthesis_method=synthesis_method
198+
).to_instruction()
137199

138200

139201
@lru_cache(maxsize=11520)
140202
def _clifford_2q_int_to_instruction(
141-
num: Integral, basis_gates: Optional[Tuple[str]]
203+
num: Integral,
204+
basis_gates: Optional[Tuple[str]],
205+
coupling_tuple: Optional[Tuple[Tuple[int, int]]],
206+
synthesis_method: str = DEFAULT_SYNTHESIS_METHOD,
142207
) -> Instruction:
143-
return CliffordUtils.clifford_2_qubit_circuit(num, basis_gates).to_instruction()
208+
return CliffordUtils.clifford_2_qubit_circuit(
209+
num,
210+
basis_gates=basis_gates,
211+
coupling_tuple=coupling_tuple,
212+
synthesis_method=synthesis_method,
213+
).to_instruction()
214+
215+
216+
def _hash_cliff(cliff):
217+
return cliff.tableau.tobytes(), cliff.tableau.shape
218+
219+
220+
def _dehash_cliff(cliff_hash):
221+
tableau = np.frombuffer(cliff_hash[0], dtype=bool).reshape(cliff_hash[1])
222+
return Clifford(tableau)
223+
224+
225+
def _clifford_to_instruction(
226+
clifford: Clifford,
227+
basis_gates: Optional[Tuple[str]],
228+
coupling_tuple: Optional[Tuple[Tuple[int, int]]],
229+
synthesis_method: str = DEFAULT_SYNTHESIS_METHOD,
230+
) -> Instruction:
231+
return _cached_clifford_to_instruction(
232+
_hash_cliff(clifford),
233+
basis_gates=basis_gates,
234+
coupling_tuple=coupling_tuple,
235+
synthesis_method=synthesis_method,
236+
)
237+
238+
239+
@lru_cache(maxsize=256)
240+
def _cached_clifford_to_instruction(
241+
cliff_hash: Tuple[str, Tuple[int, int]],
242+
basis_gates: Optional[Tuple[str]],
243+
coupling_tuple: Optional[Tuple[Tuple[int, int]]],
244+
synthesis_method: str = DEFAULT_SYNTHESIS_METHOD,
245+
) -> Instruction:
246+
return _synthesize_clifford(
247+
_dehash_cliff(cliff_hash),
248+
basis_gates=basis_gates,
249+
coupling_tuple=coupling_tuple,
250+
synthesis_method=synthesis_method,
251+
).to_instruction()
144252

145253

146254
# The classes VGate and WGate are not actually used in the code - we leave them here to give
@@ -254,7 +362,12 @@ def random_clifford_circuits(
254362

255363
@classmethod
256364
@lru_cache(maxsize=24)
257-
def clifford_1_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str, ...]] = None):
365+
def clifford_1_qubit_circuit(
366+
cls,
367+
num,
368+
basis_gates: Optional[Tuple[str, ...]] = None,
369+
synthesis_method: str = DEFAULT_SYNTHESIS_METHOD,
370+
):
258371
"""Return the 1-qubit clifford circuit corresponding to ``num``,
259372
where ``num`` is between 0 and 23.
260373
"""
@@ -275,20 +388,28 @@ def clifford_1_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str, ...]] =
275388
qc.z(0)
276389

277390
if basis_gates:
278-
qc = _synthesize_clifford_circuit(qc, basis_gates)
391+
qc = _synthesize_clifford_circuit(qc, basis_gates, synthesis_method=synthesis_method)
279392

280393
return qc
281394

282395
@classmethod
283396
@lru_cache(maxsize=11520)
284-
def clifford_2_qubit_circuit(cls, num, basis_gates: Optional[Tuple[str, ...]] = None):
397+
def clifford_2_qubit_circuit(
398+
cls,
399+
num,
400+
basis_gates: Optional[Tuple[str, ...]] = None,
401+
coupling_tuple: Optional[Tuple[Tuple[int, int]]] = None,
402+
synthesis_method: str = DEFAULT_SYNTHESIS_METHOD,
403+
):
285404
"""Return the 2-qubit clifford circuit corresponding to `num`
286405
where `num` is between 0 and 11519.
287406
"""
288407
qc = QuantumCircuit(2, name=f"Clifford-2Q({num})")
289408
for layer, idx in enumerate(_layer_indices_from_num(num)):
290409
if basis_gates:
291-
layer_circ = _transformed_clifford_layer(layer, idx, basis_gates)
410+
layer_circ = _transformed_clifford_layer(
411+
layer, idx, basis_gates, coupling_tuple, synthesis_method=synthesis_method
412+
)
292413
else:
293414
layer_circ = _CLIFFORD_LAYER[layer][idx]
294415
_circuit_compose(qc, layer_circ, qubits=(0, 1))
@@ -578,13 +699,22 @@ def _clifford_2q_nums_from_2q_circuit(qc: QuantumCircuit) -> Iterable[Integral]:
578699
]
579700

580701

581-
@lru_cache(maxsize=None)
702+
@lru_cache(maxsize=256)
582703
def _transformed_clifford_layer(
583-
layer: int, index: Integral, basis_gates: Tuple[str, ...]
704+
layer: int,
705+
index: Integral,
706+
basis_gates: Tuple[str, ...],
707+
coupling_tuple: Optional[Tuple[Tuple[int, int]]],
708+
synthesis_method: str = DEFAULT_SYNTHESIS_METHOD,
584709
) -> QuantumCircuit:
585710
# Return the index-th quantum circuit of the layer translated with the basis_gates.
586711
# The result is cached for speed.
587-
return _synthesize_clifford_circuit(_CLIFFORD_LAYER[layer][index], basis_gates)
712+
return _synthesize_clifford_circuit(
713+
_CLIFFORD_LAYER[layer][index],
714+
basis_gates=basis_gates,
715+
coupling_tuple=coupling_tuple,
716+
synthesis_method=synthesis_method,
717+
)
588718

589719

590720
def _num_from_layer_indices(triplet: Tuple[Integral, Integral, Integral]) -> Integral:

0 commit comments

Comments
 (0)