確率的エラー増幅によるユーティリティスケールのエラー軽減
使用量の目安: Heron r2 プロセッサで16分(注意: これは目安です。実際の実行時間は異なる場合があります。)
背景
このチュートリアルでは、Qiskit Runtime を使用して、確率的エラー増幅(PEA)を用いたゼロノイズ外挿(ZNE)の実験的バージョンによるユーティリティスケールのエラー軽減実験を実行する方法を説明します。
参考文献: Y. Kim et al. Evidence for the utility of quantum computing before fault tolerance. Nature 618.7965 (2023)
ゼロノイズ外挿(ZNE)
ゼロノイズ外挿(ZNE)は、回路実行中の未知のノイズの影響を、既知の方法でスケーリングできることを利用して除去するエラー軽減技術です。
この手法は、期待値がノイズに対して既知の関数でスケーリングすることを仮定しています。
ここで はノイズの強度をパラメータ化しており、増幅することができます。 ZNE は以下の手順で実装できます。
- 複数のノイズ係数 に対して回路のノイズを増幅します
- すべてのノイズ増幅回路を実行して を測定します
- ゼロノイズ極限 に外挿します

ZNE のノイズ増幅
ZNE を正しく実装する上での主な課題は、期待値におけるノイズの正確なモデルを持ち、既知の方法でノイズを増幅することです。
ZNE のエラー増幅を実装する一般的な方法は3つあります。
| パルス伸長 | ゲート折り畳み | 確率的エラー増幅 |
|---|---|---|
| キャリブレーションによりパルス持続時間をスケーリング | 恒等サイクルでゲートを繰り返す | パウリチャネルのサンプリングによりノイズを追加 |
| Kandala et al. Nature (2019) | Shultz et al. PRA (2022) | Li & Benjamin PRX (2017) |
| ユーティリティスケールの実験において、確率的エラー増幅(PEA)が最も魅力的です。 |
- パルス伸長は、ゲートノイズが持続時間に比例することを仮定しますが、通常これは正しくありません。また、キャリブレーションにもコストがかかります。
- ゲート折り畳みは大きなストレッチ係数を必要とし、実行可能な回路の深さを大幅に制限します。
- PEA は、ネイティブノイズ係数()で実行可能な任意の回路に適用できますが、ノイズモデルの学習が必要です。
PEA のためのノイズモデル学習
PEA は確率的エラーキャンセレーション(PEC)と同じ層ベースのノイズモデル を仮定しますが、回路ノイズに対して指数的にスケーリングするサンプリングオーバーヘッドを回避します。
| ステップ 1 | ステップ 2 | ステップ 3 |
|---|---|---|
| 2量子ビットゲートの層にパウリツイルを適用 | 層の恒等ペアを繰り返してノイズを学習 | 忠実度(各ノイズチャネルのエラー)を導出 |
![]() | ![]() |
参考文献: E. van den Berg, Z. Minev, A. Kandala, and K. Temme, Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors arXiv:2201.09866
要件
このチュートリアルを開始する前に、以下がインストールされていることを確認してください。
- Qiskit SDK v1.0 以降(visualization サポート付き)
- Qiskit Runtime v0.22 以降(
pip install qiskit-ibm-runtime)
セットアップ
from __future__ import annotations
from collections.abc import Sequence
from collections import defaultdict
import numpy as np
import rustworkx
import matplotlib.pyplot as plt
from qiskit.circuit import QuantumCircuit, Parameter
from qiskit.circuit.library import CXGate, CZGate, ECRGate
from qiskit.providers import Backend
from qiskit.visualization import plot_error_map
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives import PubResult
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2 as Estimator
ステップ 1: 古典的な入力を量子問題にマッピングする
パラメータ化されたイジングモデル回路の作成
まず、実行するバックエンドを選択します。このデモンストレーションでは127量子ビットのバックエンドで実行しますが、利用可能な任意のバックエンドに変更することができます。
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
backend
<IBMBackend('ibm_kingston')>
回路構築のためのヘルパー関数
次に、バックエンドのトポロジーに準拠した2次元横磁場イジングモデルのトロッター化された時間発展のための回路を構築するヘルパー関数を作成します。
"""Trotter circuit generation"""
def remove_qubit_couplings(
couplings: Sequence[tuple[int, int]], qubits: Sequence[int] | None = None
) -> list[tuple[int, int]]:
"""Remove qubits from a coupling list.
Args:
couplings: A sequence of qubit couplings.
qubits: Optional, the qubits to remove.
Returns:
The input couplings with the specified qubits removed.
"""
if qubits is None:
return couplings
qubits = set(qubits)
return [edge for edge in couplings if not qubits.intersection(edge)]
def coupling_qubits(
*couplings: Sequence[tuple[int, int]],
allowed_qubits: Sequence[int] | None = None,
) -> list[int]:
"""Return a sorted list of all qubits involved in one or more couplings lists.
Args:
couplings: one or more coupling lists.
allowed_qubits: Optional, the allowed qubits to include. If None all
qubits are allowed.
Returns:
The intersection of all qubits in the couplings and the allowed qubits.
"""
qubits = set()
for edges in couplings:
for edge in edges:
qubits.update(edge)
if allowed_qubits is not None:
qubits = qubits.intersection(allowed_qubits)
return list(qubits)
def construct_layer_couplings(
backend: Backend,
) -> list[list[tuple[int, int]]]:
"""Separate a coupling map into disjoint 2-qubit gate layers.
Args:
backend: A backend to construct layer couplings for.
Returns:
A list of disjoint layers of directed couplings for the input coupling map.
"""
coupling_graph = backend.coupling_map.graph.to_undirected(
multigraph=False
)
edge_coloring = rustworkx.graph_bipartite_edge_color(coupling_graph)
layers = defaultdict(list)
for edge_idx, color in edge_coloring.items():
layers[color].append(
coupling_graph.get_edge_endpoints_by_index(edge_idx)
)
layers = [sorted(layers[i]) for i in sorted(layers.keys())]
return layers
def entangling_layer(
gate_2q: str,
couplings: Sequence[tuple[int, int]],
qubits: Sequence[int] | None = None,
) -> QuantumCircuit:
"""Generating a entangling layer for the specified couplings.
This corresponds to a Trotter layer for a ZZ Ising term with angle Pi/2.
Args:
gate_2q: The 2-qubit basis gate for the layer, should be "cx", "cz", or "ecr".
couplings: A sequence of qubit couplings to add CX gates to.
qubits: Optional, the physical qubits for the layer. Any couplings involving
qubits not in this list will be removed. If None the range up to the largest
qubit in the couplings will be used.
Returns:
The QuantumCircuit for the entangling layer.
"""
# Get qubits and convert to set to order
if qubits is None:
qubits = range(1 + max(coupling_qubits(couplings)))
qubits = set(qubits)
# Mapping of physical qubit to virtual qubit
qubit_mapping = {q: i for i, q in enumerate(qubits)}
# Convert couplings to indices for virtual qubits
indices = [
[qubit_mapping[i] for i in edge]
for edge in couplings
if qubits.issuperset(edge)
]
# Layer circuit on virtual qubits
circuit = QuantumCircuit(len(qubits))
# Get 2-qubit basis gate and pre and post rotation circuits
gate2q = None
pre = QuantumCircuit(2)
post = QuantumCircuit(2)
if gate_2q == "cx":
gate2q = CXGate()
# Pre-rotation
pre.sdg(0)
pre.z(1)
pre.sx(1)
pre.s(1)
# Post-rotation
post.sdg(1)
post.sxdg(1)
post.s(1)
elif gate_2q == "ecr":
gate2q = ECRGate()
# Pre-rotation
pre.z(0)
pre.s(1)
pre.sx(1)
pre.s(1)
# Post-rotation
post.x(0)
post.sdg(1)
post.sxdg(1)
post.s(1)
elif gate_2q == "cz":
gate2q = CZGate()
# Identity pre-rotation
# Post-rotation
post.sdg([0, 1])
else:
raise ValueError(
f"Invalid 2-qubit basis gate {gate_2q}, should be 'cx', 'cz', or 'ecr'"
)
# Add 1Q pre-rotations
for inds in indices:
circuit.compose(pre, qubits=inds, inplace=True)
# Use barriers around 2-qubit basis gate to specify a layer for PEA noise learning
circuit.barrier()
for inds in indices:
circuit.append(gate2q, (inds[0], inds[1]))
circuit.barrier()
# Add 1Q post-rotations after barrier
for inds in indices:
circuit.compose(post, qubits=inds, inplace=True)
# Add physical qubits as metadata
circuit.metadata["physical_qubits"] = tuple(qubits)
return circuit
def trotter_circuit(
theta: Parameter | float,
layer_couplings: Sequence[Sequence[tuple[int, int]]],
num_steps: int,
gate_2q: str | None = "cx",
backend: Backend | None = None,
qubits: Sequence[int] | None = None,
) -> QuantumCircuit:
"""Generate a Trotter circuit for the 2D Ising
Args:
theta: The angle parameter for X.
layer_couplings: A list of couplings for each entangling layer.
num_steps: the number of Trotter steps.
gate_2q: The 2-qubit basis gate to use in entangling layers.
Can be "cx", "cz", "ecr", or None if a backend is provided.
backend: A backend to get the 2-qubit basis gate from, if provided
will override the basis_gate field.
qubits: Optional, the allowed physical qubits to truncate the
couplings to. If None the range up to the largest
qubit in the couplings will be used.
Returns:
The Trotter circuit.
"""
if backend is not None:
try:
basis_gates = backend.configuration().basis_gates
except AttributeError:
basis_gates = backend.basis_gates
for gate in ["cx", "cz", "ecr"]:
if gate in basis_gates:
gate_2q = gate
break
# If no qubits, get the largest qubit from all layers and
# specify the range so the same one is used for all layers.
if qubits is None:
qubits = range(1 + max(coupling_qubits(layer_couplings)))
# Generate the entangling layers
layers = [
entangling_layer(gate_2q, couplings, qubits=qubits)
for couplings in layer_couplings
]
# Construct the circuit for a single Trotter step
num_qubits = len(qubits)
trotter_step = QuantumCircuit(num_qubits)
trotter_step.rx(theta, range(num_qubits))
for layer in layers:
trotter_step.compose(layer, range(num_qubits), inplace=True)
# Construct the circuit for the specified number of Trotter steps
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(theta, range(num_qubits))
for layer in layers:
circuit.compose(layer, range(num_qubits), inplace=True)
circuit.metadata["physical_qubits"] = tuple(qubits)
return circuit
エンタングリング層の結合の定義
トロッター化されたイジングシミュレーションを実装するために、デバイスの2量子ビットゲート結合の3つの層を定義します。これらは各トロッターステップで繰り返されます。これにより、エラー軽減を実装するためにノイズを学習する必要がある3つのツイルされた層が定義されます。
layer_couplings = construct_layer_couplings(backend)
for i, layer in enumerate(layer_couplings):
print(f"Layer {i}:\n{layer}\n")
Layer 0:
[(2, 3), (4, 5), (6, 7), (8, 9), (10, 11), (12, 13), (14, 15), (16, 23), (18, 31), (19, 35), (20, 21), (25, 37), (26, 27), (28, 29), (33, 39), (36, 41), (38, 49), (42, 43), (45, 46), (47, 57), (51, 52), (53, 54), (56, 63), (58, 71), (59, 75), (61, 62), (64, 65), (66, 67), (68, 69), (72, 73), (76, 81), (79, 93), (82, 83), (84, 85), (86, 87), (88, 89), (91, 98), (94, 95), (97, 107), (99, 115), (100, 101), (102, 103), (105, 117), (108, 109), (110, 111), (113, 114), (116, 121), (118, 129), (123, 136), (124, 125), (126, 127), (130, 131), (132, 133), (135, 139), (138, 151), (142, 143), (144, 145), (146, 147), (152, 153), (154, 155)]
Layer 1:
[(0, 1), (3, 16), (5, 6), (7, 8), (11, 18), (13, 14), (17, 27), (21, 22), (23, 24), (25, 26), (29, 38), (30, 31), (32, 33), (34, 35), (39, 53), (41, 42), (43, 56), (44, 45), (47, 48), (49, 50), (51, 58), (54, 55), (57, 67), (60, 61), (62, 63), (65, 66), (69, 78), (70, 71), (73, 79), (74, 75), (77, 85), (80, 81), (83, 84), (87, 97), (89, 90), (91, 92), (93, 94), (96, 103), (101, 116), (104, 105), (106, 107), (109, 118), (111, 112), (113, 119), (114, 115), (117, 125), (121, 122), (123, 124), (127, 137), (128, 129), (131, 138), (133, 134), (136, 143), (139, 155), (140, 141), (145, 146), (147, 148), (149, 150), (151, 152)]
Layer 2:
[(1, 2), (3, 4), (7, 17), (9, 10), (11, 12), (15, 19), (21, 36), (22, 23), (24, 25), (27, 28), (29, 30), (31, 32), (33, 34), (37, 45), (40, 41), (43, 44), (46, 47), (48, 49), (50, 51), (52, 53), (55, 59), (61, 76), (63, 64), (65, 77), (67, 68), (69, 70), (71, 72), (73, 74), (78, 89), (81, 82), (83, 96), (85, 86), (87, 88), (90, 91), (92, 93), (95, 99), (98, 111), (101, 102), (103, 104), (105, 106), (107, 108), (109, 110), (112, 113), (119, 133), (120, 121), (122, 123), (125, 126), (127, 128), (129, 130), (131, 132), (134, 135), (137, 147), (141, 142), (143, 144), (148, 149), (150, 151), (153, 154)]
不良量子ビットの除去
バックエンドのカップリングマップを確認し、エラーの高い結合に接続されている量子ビットがないか調べます。これらの「不良」量子ビットを実験から除外します。
# Plot gate error map
# NOTE: These can change over time, so your results may look different
plot_error_map(backend)

bad_qubits = {
56,
63,
67,
} # qubits removed based on high coupling error (1.00)
good_qubits = list(set(range(backend.num_qubits)).difference(bad_qubits))
print("Physical qubits:\n", good_qubits)
Physical qubits:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 57, 58, 59, 60, 61, 62, 64, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155]
メインのトロッター回路生成
num_steps = 6
theta = Parameter("theta")
circuit = trotter_circuit(
theta, layer_couplings, num_steps, qubits=good_qubits, backend=backend
)
後で割り当てるパラメータ値のリストを作成する
num_params = 12
# 12 parameter values for Rx between [0, pi/2].
# Reshape to outer product broadcast with observables
parameter_values = np.linspace(0, np.pi / 2, num_params).reshape(
(num_params, 1)
)
num_params = parameter_values.size
ステップ 2: 量子ハードウェア実行のための問題の最適化
ISA 回路
回路をハードウェア上で実行する前に、ハードウェア実行向けに最適化する必要があります。このプロセスにはいくつかのステップが含まれます。
- 回路の仮想量子ビットをハードウェア上の物理量子ビットにマッピングする量子ビットレイアウトを選択します。
- 接続されていない量子ビット間の相互作用をルーティングするために、必要に応じてスワップゲートを挿入します。
- 回路内のゲートを、ハードウェア上で直接実行可能な命令セットアーキテクチャ(ISA)命令に変換します。
- 回路の深さとゲート数を最小化するための回路最適化を実行します。
Qiskit に組み込まれたトランスパイラはこれらのステップをすべて実行できますが、このチュートリアルでは、ユーティリティスケールの Trotter 回路をゼロから構築する方法を示します。適切な物理量子ビットを選択し、選択した量子ビットの接続されたペアにエンタングルメント層を定義します。ただし、回路内の非 ISA ゲートの変換や、トランスパイラが提供する回路最適化を利用する必要があります。
プリセットパスマネージャーを作成し、それを回路に対して実行することで、選択したバックエンド向けに回路をトランスパイルします。また、回路の初期レイアウトを、すでに選択済みの good_qubits に固定します。パスマネージャーを作成する簡単な方法は、generate_preset_pass_manager 関数を使用することです。パスマネージャーを使用したトランスパイルの詳細については、パスマネージャーによるトランスパイルを参照してください。
pm = generate_preset_pass_manager(
backend=backend,
initial_layout=good_qubits,
layout_method="trivial",
optimization_level=1,
)
isa_circuit = pm.run(circuit)
ISA オブザーバブル
次に、各仮想量子ビットに対して、必要な数の 項をパディングして、すべての重み 1 の オブザーバブルを作成します。
observables = []
num_qubits = len(good_qubits)
for q in range(num_qubits):
observables.append(
SparsePauliOp("I" * (num_qubits - q - 1) + "Z" + "I" * q)
)
トランスパイルプロセスにより、回路の仮想量子ビットがハードウェア上の物理量子ビットにマッピングされています。量子ビットレイアウトに関する情報は、トランスパイル済み回路の layout 属性に格納されています。オブザーバブルも仮想量子ビットの観点で定義されているため、このレイアウトをオブザーバブルに適用する必要があります。これは SparsePauliOp の apply_layout メソッドを使用して行います。
以下のコードブロックでは、各オブザーバブルがリストで囲まれていることに注目してください。これは、各量子ビットのオブザーバブルが各 theta 値に対して測定されるように、パラメータ値と_ブロードキャスト_するためです。プリミティブのブロードキャストルールはこちらで確認できます。
isa_observables = [
[obs.apply_layout(layout=isa_circuit.layout)] for obs in observables
]
ステップ 3: Qiskit プリミティブを使用した実行
pub = (isa_circuit, isa_observables, parameter_values)

