カスタムバックエンドに対して作成およびトランスパイルする
このページのコードは、以下の要件に基づいて開発された。 これらのバージョンまたは新しいバージョンの使用をお勧めします。
qiskit[all]~=2.5.1
Qiskitのより強力な特徴の一つは、ユニークなデバイス構成をサポートする能力である。 Qiskitは、使用する量子ハードウェアのプロバイダに依存しないように構築されており、プロバイダは、 BackendV2 オブジェクトを独自のデバイスプロパティに設定することができます。 このトピックでは、独自のバックエンドを設定し、それに対して量子回路をトランスパイルする方法を示す。
異なるジオメトリや基底ゲートを持つユニークな BackendV2 オブジェクトを作成し、それらの構成を念頭に置いて回路をトランスパイルすることができます。 以下の例は、基底ゲートがバルク内とエッジに沿って異なる、不連続な量子ビット格子を持つバックエンドをカバーしている。
プロバイダー、 BackendV2、およびターゲットインターフェースを理解する
の使い方と目的を理解しておくとよい。 Provider, BackendV2および Target オブジェクトを理解するのに役立つ。
-
Qiskit SDKに統合したい量子デバイスやシミュレータがある場合は、独自の
Providerクラスを書く必要があります。 このクラスの目的はただ一つ、あなたが提供するバックエンド・オブジェクトを取得することです。 ここでは、必要なクレデンシャルや認証タスクが処理される。 インスタンス化されると、プロバイダオブジェクトはバックエンドのリストを提供し、バックエンドを獲得/インスタンス化する機能も提供する。 -
次に、バックエンドクラスは、Qiskit SDKと回路を実行するハードウェアやシミュレータとのインターフェースを提供します。 トランスパイラにバックエンドを記述するために必要な情報がすべて含まれているので、トランスパイラは制約に従って回路を最適化することができる。
BackendV2は4つの主要部分から構成されている:- A
Targetプロパティ。バックエンドの制約の説明を含み、トランスパイラにバックエンドのモデルを提供する max_circuits、バックエンドが1つのジョブで実行できる回路数の上限を定義するプロパティ- ジョブ投入を受け付ける
run()メソッド _default_optionsユーザーが設定可能なオプションとそのデフォルト値を定義するためのセット
- A
カスタム BackendV2 を作成する
BackendV2 オブジェクトは、プロバイダ( qiskit.providers 内または qiskit_ibm_runtime.IBMBackend). 上述したように、これらのオブジェクトにはいくつかの属性があります。 Target. Target には、バックエンドの属性を指定する情報が含まれている。 Coupling Mapのリスト Instructionsなどのバックエンドの属性を指定する情報が含まれています。 Targetのようなパルスレベルの詳細も定義できる。 DriveChannel または ControlChannel.
以下の例では、各チップがヘビーヘックス接続を持つマルチチップ・バックエンドをシミュレートすることで、このカスタマイズを実証している。 この例では、バックエンドの2量子ビット・ゲート・セットを各チップ内で CZGates 各チップ内と CXGates チップ間である。 まず、独自の BackendV2 を作成し、その Target を、先に説明した制約に従ってシングルおよび2量子ビットゲートでカスタマイズする。
カップリング・マップの作図には graphviz ライブラリがインストールされている必要があります。
import numpy as np
import rustworkx as rx
from qiskit.providers import BackendV2, Options
from qiskit.transpiler import Target, InstructionProperties
from qiskit.circuit.library import XGate, SXGate, RZGate, CZGate, ECRGate
from qiskit.circuit import Measure, Delay, Parameter, Reset
from qiskit import QuantumCircuit, transpile
from qiskit.visualization import plot_gate_map
class FakeLOCCBackend(BackendV2):
"""Fake multi chip backend."""
def __init__(self, distance=3, number_of_chips=3):
"""Instantiate a new fake multi chip backend.
Args:
distance (int): The heavy hex code distance to use for each chips'
coupling map. This number **must** be odd. The distance relates
to the number of qubits by:
:math:`n = \\frac{5d^2 - 2d - 1}{2}` where :math:`n` is the
number of qubits and :math:`d` is the ``distance``
number_of_chips (int): The number of chips to have in the multichip backend
each chip will be a heavy hex graph of ``distance`` code distance.
"""
super().__init__(name="Fake LOCC backend")
# Create a heavy-hex graph using the
# rustworkx library, then instantiate a new target
self._graph = rx.generators.directed_heavy_hex_graph(
distance, bidirectional=False
)
num_qubits = len(self._graph) * number_of_chips
self._target = Target(
"Fake multi-chip backend", num_qubits=num_qubits
)
# Generate instruction properties for single qubit gates and a measurement, delay,
# and reset operation to every qubit in the backend.
rng = np.random.default_rng(seed=12345678942)
rz_props = {}
x_props = {}
sx_props = {}
measure_props = {}
delay_props = {}
# Add 1q gates. Globally use virtual rz, x, sx, and measure
for i in range(num_qubits):
qarg = (i,)
rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0)
x_props[qarg] = InstructionProperties(
error=rng.uniform(1e-6, 1e-4),
duration=rng.uniform(1e-8, 9e-7),
)
sx_props[qarg] = InstructionProperties(
error=rng.uniform(1e-6, 1e-4),
duration=rng.uniform(1e-8, 9e-7),
)
measure_props[qarg] = InstructionProperties(
error=rng.uniform(1e-3, 1e-1),
duration=rng.uniform(1e-8, 9e-7),
)
delay_props[qarg] = None
self._target.add_instruction(XGate(), x_props)
self._target.add_instruction(SXGate(), sx_props)
self._target.add_instruction(RZGate(Parameter("theta")), rz_props)
self._target.add_instruction(Measure(), measure_props)
self._target.add_instruction(Reset(), measure_props)
self._target.add_instruction(Delay(Parameter("t")), delay_props)
# Add chip local 2q gate which is CZ
cz_props = {}
for i in range(number_of_chips):
for root_edge in self._graph.edge_list():
offset = i * len(self._graph)
edge = (root_edge[0] + offset, root_edge[1] + offset)
cz_props[edge] = InstructionProperties(
error=rng.uniform(7e-4, 5e-3),
duration=rng.uniform(1e-8, 9e-7),
)
self._target.add_instruction(CZGate(), cz_props)
cx_props = {}
# Add interchip 2q gates which are ecr (effectively CX)
# First determine which nodes to connect
node_indices = self._graph.node_indices()
edge_list = self._graph.edge_list()
inter_chip_nodes = {}
for node in node_indices:
count = 0
for edge in edge_list:
if node == edge[0]:
count += 1
if count == 1:
inter_chip_nodes[node] = count
# Create inter-chip ecr props
cx_props = {}
inter_chip_edges = list(inter_chip_nodes.keys())
for i in range(1, number_of_chips):
offset = i * len(self._graph)
edge = (
inter_chip_edges[1] + (len(self._graph) * (i - 1)),
inter_chip_edges[0] + offset,
)
cx_props[edge] = InstructionProperties(
error=rng.uniform(7e-4, 5e-3),
duration=rng.uniform(1e-8, 9e-7),
)
self._target.add_instruction(ECRGate(), cx_props)
@property
def target(self):
return self._target
@property
def max_circuits(self):
return None
@property
def graph(self):
return self._graph
@classmethod
def _default_options(cls):
return Options(shots=1024)
def run(self, circuit, **kwargs):
raise NotImplementedError(
"This backend does not contain a run method"
)バックエンドを可視化する
この新しいクラスの接続グラフは、 モジュールの plot_gate_map()qiskit.visualization メソッドで見ることができます。 この方法は plot_coupling_map() と plot_circuit_layout()とともに、バックエンドの量子ビットの配置や、バックエンドの量子ビットにまたがって回路がどのように配置されているかを視覚化するのに役立つツールです。 この例では、3つの小さなヘビーヘックス・チップを含むバックエンドを作成する。 これは、量子ビットを配置する座標のセットと、異なる2量子ビットゲートのカスタムカラーのセットを指定する。
backend = FakeLOCCBackend(3, 3)
target = backend.target
coupling_map_backend = target.build_coupling_map()
coordinates = [
(3, 1),
(3, -1),
(2, -2),
(1, 1),
(0, 0),
(-1, -1),
(-2, 2),
(-3, 1),
(-3, -1),
(2, 1),
(1, -1),
(-1, 1),
(-2, -1),
(3, 0),
(2, -1),
(0, 1),
(0, -1),
(-2, 1),
(-3, 0),
]
single_qubit_coordinates = []
total_qubit_coordinates = []
for coordinate in coordinates:
total_qubit_coordinates.append(coordinate)
for coordinate in coordinates:
total_qubit_coordinates.append(
(-1 * coordinate[0] + 1, coordinate[1] + 4)
)
for coordinate in coordinates:
total_qubit_coordinates.append((coordinate[0], coordinate[1] + 8))
line_colors = ["#adaaab" for edge in coupling_map_backend.get_edges()]
ecr_edges = []
# Get tuples for the edges which have an ecr instruction attached
for instruction in target.instructions:
if instruction[0].name == "ecr":
ecr_edges.append(instruction[1])
for i, edge in enumerate(coupling_map_backend.get_edges()):
if edge in ecr_edges:
line_colors[i] = "#000000"print(backend.name)
plot_gate_map(
backend,
plot_directed=True,
qubit_coordinates=total_qubit_coordinates,
line_color=line_colors,
)Output:
Fake LOCC backend
それぞれの量子ビットにはラベルが付けられ、色の付いた矢印は2量子ビットのゲートを表している。 グレーの矢印はCZゲート、黒の矢印はチップ間CXゲート(これらは量子ビット と を接続している)。 矢印の方向は、これらのゲートが実行されるデフォルトの方向を示し、各2量子ビット・チャンネルのデフォルトでどの量子ビットが制御/ターゲットになるかを指定します。
カスタムバックエンドに対してトランスパイルする
を持つカスタムバックエンドが定義されました。 Target が定義されたので、このバックエンドに対して量子回路をトランスパイルするのは簡単です。なぜなら、トランスパイラ・パスに必要なすべての関連制約(基底ゲート、量子ビットの接続性など)がこの属性に含まれているからです。 次の例では、大きなGHZステートを作成する回路を構築し、上で構築したバックエンドに対してそれをトランスパイルする。
from qiskit.transpiler import generate_preset_pass_manager
num_qubits = 50
ghz = QuantumCircuit(num_qubits)
ghz.h(range(num_qubits))
ghz.cx(0, range(1, num_qubits))
op_counts = ghz.count_ops()
print("Pre-Transpilation: ")
print(f"CX gates: {op_counts['cx']}")
print(f"H gates: {op_counts['h']}")
print("\n", 30 * "#", "\n")
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
transpiled_ghz = pm.run(ghz)
op_counts = transpiled_ghz.count_ops()
print("Post-Transpilation: ")
print(f"CZ gates: {op_counts['cz']}")
print(f"ECR gates: {op_counts['ecr']}")
print(f"SX gates: {op_counts['sx']}")
print(f"RZ gates: {op_counts['rz']}")Output:
Pre-Transpilation:
CX gates: 49
H gates: 50
##############################
Post-Transpilation:
CZ gates: 207
ECR gates: 8
SX gates: 379
RZ gates: 211
トランスパイルされた回路には現在、 ECR``CZ とゲートが混在しています。これらはバックエンドので基本ゲート Targetとして指定したものです。 レイアウト選択後にSWAP命令を挿入する必要があるため、開始時よりもゲートがかなり多くなっています。 以下では、可視 plot_circuit_layout() 化ツールを用いて、この回路で使用された量子ビットと2量子ビットチャネルを特定します。
from qiskit.visualization import plot_circuit_layout
plot_circuit_layout(
transpiled_ghz, backend, qubit_coordinates=total_qubit_coordinates
)Output:
独自のバックエンドを作成する
rustworkx パッケージには、さまざまなグラフの大規模なライブラリーが含まれており、カスタムグラフの作成が可能です。 以下のビジュアル的に興味深いコードは、トーリックコードにインスパイアされたバックエンドを作成する。 そして、 Visualize backends セクションの関数を使ってバックエンドを可視化することができます。
class FakeTorusBackend(BackendV2):
"""Fake multi chip backend."""
def __init__(self):
"""Instantiate a new backend that is inspired by a toric code"""
super().__init__(name="Fake LOCC backend")
graph = rx.generators.directed_grid_graph(20, 20)
for column in range(20):
graph.add_edge(column, 19 * 20 + column, None)
for row in range(20):
graph.add_edge(row * 20, row * 20 + 19, None)
num_qubits = len(graph)
rng = np.random.default_rng(seed=12345678942)
rz_props = {}
x_props = {}
sx_props = {}
measure_props = {}
delay_props = {}
self._target = Target("Fake Kookaburra", num_qubits=num_qubits)
# Add 1q gates. Globally use virtual rz, x, sx, and measure
for i in range(num_qubits):
qarg = (i,)
rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0)
x_props[qarg] = InstructionProperties(
error=rng.uniform(1e-6, 1e-4),
duration=rng.uniform(1e-8, 9e-7),
)
sx_props[qarg] = InstructionProperties(
error=rng.uniform(1e-6, 1e-4),
duration=rng.uniform(1e-8, 9e-7),
)
measure_props[qarg] = InstructionProperties(
error=rng.uniform(1e-3, 1e-1),
duration=rng.uniform(1e-8, 9e-7),
)
delay_props[qarg] = None
self._target.add_instruction(XGate(), x_props)
self._target.add_instruction(SXGate(), sx_props)
self._target.add_instruction(RZGate(Parameter("theta")), rz_props)
self._target.add_instruction(Measure(), measure_props)
self._target.add_instruction(Reset(), measure_props)
self._target.add_instruction(Delay(Parameter("t")), delay_props)
cz_props = {}
for edge in graph.edge_list():
cz_props[edge] = InstructionProperties(
error=rng.uniform(7e-4, 5e-3),
duration=rng.uniform(1e-8, 9e-7),
)
self._target.add_instruction(CZGate(), cz_props)
@property
def target(self):
return self._target
@property
def max_circuits(self):
return None
@classmethod
def _default_options(cls):
return Options(shots=1024)
def run(self, circuit, **kwargs):
raise NotImplementedError("Lasciate ogne speranza, voi ch'intrate")backend = FakeTorusBackend()
# We set `figsize` to a smaller size to make the documentation website faster
# to load. Normally, you do not need to set the argument.
plot_gate_map(backend, figsize=(4, 4))Output:
num_qubits = int(backend.num_qubits / 2)
full_device_bv = QuantumCircuit(num_qubits, num_qubits - 1)
full_device_bv.x(num_qubits - 1)
full_device_bv.h(range(num_qubits))
full_device_bv.cx(range(num_qubits - 1), num_qubits - 1)
full_device_bv.h(range(num_qubits))
full_device_bv.measure(range(num_qubits - 1), range(num_qubits - 1))
tqc = transpile(full_device_bv, backend, optimization_level=3)
op_counts = tqc.count_ops()
print(f"CZ gates: {op_counts['cz']}")
print(f"X gates: {op_counts['x']}")
print(f"SX gates: {op_counts['sx']}")
print(f"RZ gates: {op_counts['rz']}")Output:
CZ gates: 580
X gates: 129
SX gates: 936
RZ gates: 840