メインコンテンツへスキップ
このプロジェクトはベータ版です。バグを見つけましたか?GitHub Issueを開く。質問やアイデアがありますか?Discussionsで質問する — フィードバックをお待ちしています!
このページの翻訳品質はいかがですか?

非マルコフ型エラーチェックを用いたポストセレクション

このガイドでは、非マルコフ型エラーチェックとは何か、そして qiskit-mitigation を使ってそれをソフトウェアで実装する方法を説明します。まず小さな2量子ビットの例でチェックを可視化し、その仕組みを説明します。直感がつかめたら、20量子ビットの例でチェックを実装し、さまざまなポストセレクション戦略を比較します。

非マルコフ型エラーチェックとは何か?​

非マルコフ型エラーチェックは、量子ビットの状態が非マルコフ型のエラー源によって破損している可能性が高いかどうかを測定に基づいて調べるテストです。このテストは、回路の先頭で各量子ビットに、持続時間を延ばしたビットフリップゲート(xslow、または角度 πN\frac{\pi}{N} の NN 個の rx ゲートの列)を配置し、続けて通常の XGate を適用して測定し、状態が ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle と正常に反転したことを確認することで行います。このテストは、回路の末尾で各終端測定の後にも実行でき、デバイスが測定された量子ビットの状態を ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle と正常に反転できることを確認できます。失敗したチェックに関連するサンプルは信頼できないとみなされる場合があり、すべてのチェックに合格したサンプルだけをポストセレクトすることで、ノイズのある分布の忠実度を高められます。

非マルコフ型エラーチェックに基づくポストセレクションの戦略​

  • ノードベースのポストセレクション: すべての量子ビットがチェックに合格したサンプルだけをポストセレクトします。これは最も厳格な戦略で、一般により多くのサンプルが破棄されます。

  • エッジベースのポストセレクション: 量子ビットの結合グラフの各エッジについて、少なくとも1つの量子ビットがチェックに合格することを確認します。この戦略はより寛容です。量子ビットがチェックに失敗しても、その隣接する量子ビットがすべてチェックに合格していれば、そのサンプルを残せる場合があります。

スペクター量子ビット​

元の回路の実行に使用する量子ビットに隣接する「スペクター」アンシラ量子ビットを指定できます。非マルコフ型エラーチェックをこれらのスペクターに配置すると、チェックが検出するよう設計された非マルコフ型ノイズに対して、より広いカバレッジが得られます。

パート1. 非マルコフ型エラーチェックを可視化する​

QPUでこの手法を実演する前に、小さな2量子ビット回路を作成し、チェックを追加してその仕組みを可視化します。まず、ibm_kingston バックエンドQPUにトランスパイルしたベル状態から始めます。

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_mitigation": "qiskit-mitigation", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
from qiskit import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService(channel="ibm_quantum_platform")
backend = service.backend("ibm_kingston")

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
qc_isa = generate_preset_pass_manager(
optimization_level=0, backend=backend
).run(qc)
qc_isa.draw("mpl")

Output of the previous code cell

Circuitの先頭に、非マルコフ性エラーのチェックを追加します。チェックは、測定で終わっているQubitにのみ配置されます。これらのチェックは、デバイスがQubitの初期状態を ∣0⟩↦∣1⟩↦∣0⟩|0\rangle \mapsto |1\rangle \mapsto |0\rangle のように正しく反転できることを確認するもので、チェックの測定結果は meas_pre という新しい古典レジスタに読み出されます。

from qiskit.transpiler import PassManager
from qiskit_mitigation.postselection.passes import (
AddPreCircuitNonMarkovianErrorChecks,
)

qc_w_checks = PassManager([AddPreCircuitNonMarkovianErrorChecks()]).run(
qc_isa
)
qc_w_checks.draw("mpl", idle_wires=False)

Output of the previous code cell

同様に、各終端測定の後ろ、Circuitの末尾にもチェックを追加できます。これらのチェックは、デバイスがQubitの状態を ∣x⟩↦∣x⊕1⟩|x\rangle \mapsto |x\oplus1\rangle のように反転できることを確認するもので、その測定結果は meas_ps という新しい古典レジスタに読み出されます。

from qiskit_mitigation.postselection.passes import (
AddPostCircuitNonMarkovianErrorChecks,
)

qc_w_checks = PassManager([AddPostCircuitNonMarkovianErrorChecks()]).run(
qc_w_checks
)
qc_w_checks.draw("mpl", idle_wires=False)

Output of the previous code cell

冒頭で述べたように、データ量子ビットに隣接するアンシラ量子ビットを「スペクテーター」として使うことで、より多くのエラーをカバーできます。回路前および回路後のスペクテーターチェックを追加します。ibm_kingston 上の物理量子ビット 2 はデータ量子ビット 1 に隣接しているため、スペクテーターとして追加され、回路前および回路後のチェックが追加されていることがわかります。スペクテーターの測定は、spec_pre と spec という新しい古典レジスターに追加されています。

このような回路には、非マルコフ性エラーチェックのすべての可能性が含まれているため、この回路からサンプリングすることで、[回路前/回路後]チェックと[ノード/エッジ]ベースの事後選択戦略の任意の組み合わせに基づいて事後選択を行えます。量子ハードウェア上での動作については、このガイドのパート 2 を参照してください。

from qiskit_mitigation.postselection.passes import (
AddSpectatorPostCircuitNonMarkovianErrorChecks,
AddSpectatorPreCircuitNonMarkovianErrorChecks,
)

qc_w_checks = PassManager(
[
AddSpectatorPreCircuitNonMarkovianErrorChecks(
coupling_map=backend.coupling_map
),
AddSpectatorPostCircuitNonMarkovianErrorChecks(
coupling_map=backend.coupling_map
),
]
).run(qc_w_checks)
qc_w_checks.draw("mpl", idle_wires=False, fold=-1)

Output of the previous code cell

パート 2: QPU 上で非マルコフ性エラーチェックを実演し、事後選択戦略を比較する​

非マルコフ性エラーチェックとは何か、またソフトウェアでどのように実装されるかを理解できたので、QPU 上でより大きな例を実行して、その仕組みを確認できます。 まず、回路を準備します。回路の選択はそれほど重要ではありません。出力を簡単に検証できる回路があれば十分です。ここでは、20 量子ビットの 1 次元ミラー・イジング回路を選びます。この回路はハードウェア上で実行するのが容易ではなく、非マルコフ性エラーの影響を受けると考えられるため、非マルコフ性エラーチェックの有効性を示す機会になります。また、ミラー構造により、ノイズを含む期待値の計算結果を厳密値と簡単に比較できます。

この実験では、オブザーバブルの期待値を計算します。オブザーバブル OO の選択も重要ではないため、単純に平均の単一サイト磁化 O=1N∑i⟨Zi⟩O=\frac{1}{N}\sum_i\langle Z_i\rangle を選びます。

import numpy as np
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap

num_qubits = 20
initial_layout = initial_layout = [
124,
125,
126,
127,
128,
129,
130,
131,
132,
133,
134,
135,
139,
155,
154,
153,
152,
151,
150,
149,
][:num_qubits]
coupling_map = CouplingMap.from_line(num_qubits, bidirectional=False)

# Num Trotter steps and `rx` angle
num_steps = 6
theta_rx = np.pi / 6

# Average single-site magnetization
observable = (
SparsePauliOp(
[
"I" * iq + "Z" + "I" * (num_qubits - iq - 1)
for iq in range(num_qubits)
]
)
/ num_qubits
)

# Create the Trotter circuit
circuit = QuantumCircuit(num_qubits)
edges = CouplingMap.from_line(num_qubits, bidirectional=False).get_edges()
edges_a = [e for e in edges if e[0] % 2 == 0]
edges_b = [e for e in edges if e[0] % 2 == 1]
for _ in range(num_steps):
circuit.rx(theta_rx, [i for i in range(num_qubits)])
for edge in edges_a:
circuit.cz(edge[0], edge[1])
for edge in edges_b:
circuit.cz(edge[0], edge[1])
circuit = circuit.compose(circuit.inverse())
circuit.measure_all()

# Transpile circuit to backend
pm = generate_preset_pass_manager(
optimization_level=0, backend=backend, initial_layout=initial_layout
)
isa_circuit = pm.run(circuit)
print(
f"two-qubit depth: {isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)}"
)
print(f"total depth: {isa_circuit.depth()}")
isa_circuit.draw("mpl", fold=-1, idle_wires=False)
two-qubit depth: 24
total depth: 85

Output of the previous code cell

次に、隣接する 7 個のスペクテーター量子ビットを含むすべての量子ビットに、回路前および回路後のチェックを追加します。可能なチェックをすべて回路に追加しておくと、後処理の際にどの事後選択戦略を使うかを決められます。

# Use all non-Markovian error checks so we can compare all strategies in post-processing.
combined = PassManager(
[
AddPreCircuitNonMarkovianErrorChecks(x_pulse_type="rx"),
AddSpectatorPreCircuitNonMarkovianErrorChecks(
backend.coupling_map, x_pulse_type="rx"
),
AddPostCircuitNonMarkovianErrorChecks(x_pulse_type="rx"),
AddSpectatorPostCircuitNonMarkovianErrorChecks(
backend.coupling_map, x_pulse_type="rx"
),
]
).run(isa_circuit)
combined_isa = generate_preset_pass_manager(
optimization_level=0, backend=backend
).run(combined)

print("cregs:", [(c.name, c.size) for c in combined_isa.cregs])
cregs: [('meas', 20), ('meas_pre', 20), ('spec_pre', 7), ('meas_ps', 20), ('spec', 7), ('spec_ps', 7)]

Executor プリミティブを使って、チェックを含む回路をサンプリングします。

from qiskit_ibm_runtime import Executor
from samplomatic.quantum_program import QuantumProgram

# Raw and combined-checks circuits in a single 100k-shot program so they share calibration.
program = QuantumProgram(shots=100_000)
program.append_circuit_item(isa_circuit) # raw circuit, no checks
program.append_circuit_item(
combined_isa
) # combined pre + post non-Markovian error checks
job = Executor(backend).run(program)
print(f"job id: {job.job_id()}")
print(f"status: {job.status()}")
job id: d9badrm6hjac73ffe31g
status: QUEUED

PostSelector クラスはチェックの creg 構造を把握しているため、サンプル内の各ビットを、対応する回路測定に関連付けられます。その compute_mask メソッドを使うと、事後選択戦略(ノードまたはエッジ)と、対象とする非マルコフ性エラーチェックの種類(回路前または回路後)を指定して、サンプル列に対するマスクを作成できます。回路前と回路後の戦略を 1 つのマスクにまとめるには、それぞれのマスクを個別に計算し、AND 演算で結合します。ここでは PostSelector を使ってさまざまな戦略すべてのマスクを作成し、利用可能な戦略のあらゆる組み合わせで結果を確認します。

以下の可視化では、事後選択戦略が事後選択率と回答の品質にどのように影響するかがわかります。予想どおり、ノードベースの事後選択を用いる戦略では事後選択率が最も低くなりました。エッジベースの戦略はより寛容で、事後選択率が高くなる傾向があります。ユーザーは自分の問題でさまざまな戦略を試し、事後選択率とサンプリングされた分布の品質の適切なバランスを見つける必要があります。この例では、回路前・回路後のチェックを併用したノードベースの事後選択が、事後選択率 .028% で最良の回答をもたらしました。一部の戦略は、生の回路を上回る性能を示しませんでした。また、チェックを追加してもそのいずれも使わない場合(下のグラフの「no selection」のバー)が最も性能が低いこともわかります。

import matplotlib.pyplot as plt
from qiskit_mitigation.postselection import PostSelector

selector = PostSelector.from_circuit(combined_isa, backend.coupling_map)

exec_results = job.result()
# The Executor returns numpy arrays keyed by classical-register name, so each result item is
# already the {creg_name: ndarray} mapping that compute_mask expects.
raw_bits = exec_results[0]["meas"] # raw circuit, no checks
bits = exec_results[1] # combined pre+post circuit

meas_bits = bits["meas"]
total = meas_bits[..., 0].size

def magnetization(b, mask=None):
"""Average single-site magnetization over the (optionally masked) measurement bits."""
kept = b if mask is None else b[mask]
return (1 - 2 * kept.astype(int)).mean() # bit 0 -> +1, bit 1 -> -1

rows = []

def report(label, mask=None, b=meas_bits):
"""Record and print the magnetization and kept fraction for one selection strategy."""
kept = b[..., 0].size if mask is None else int(mask.sum())
ev = magnetization(b, mask)
rows.append((label, ev, kept / total))
print(f"{label:<24}: <m> = {ev:+.4f} (kept {kept:>6}/{total})")

# Compare all possible postselection strategies
report("raw circuit", b=raw_bits)
report("no selection")
for s in ("node", "edge"):
report(f"pre-only ({s})", selector.compute_mask(bits, s, mode="pre"))
for s in ("node", "edge"):
report(f"post-only ({s})", selector.compute_mask(bits, s, mode="post"))
for pre_s in ("node", "edge"):
for post_s in ("node", "edge"):
keep = selector.compute_mask(
bits, pre_s, mode="pre"
) & selector.compute_mask(bits, post_s, mode="post")
report(f"pre={pre_s} + post={post_s}", keep)

labels = [r[0] for r in rows]
evs = [r[1] for r in rows]
fracs = [r[2] for r in rows]
x = range(len(labels))

fig, ax1 = plt.subplots(figsize=(11, 5))

# Bars: expectation value per selection strategy.
bar_colors = ["firebrick"] + ["steelblue"] * (len(evs) - 1)
ax1.bar(x, evs, color=bar_colors, alpha=0.85)
ax1.axhline(1.0, color="green", ls=":", lw=1.5, label="exact <m>")
ax1.axhline(rows[0][1], color="firebrick", ls="--", lw=1, label="raw <m>")
ax1.set_ylabel("<m> (avg single-site magnetization)", color="steelblue")
ax1.tick_params(axis="y", labelcolor="steelblue")
ax1.set_xticks(list(x))
ax1.set_xticklabels(labels, rotation=45, ha="right")
ax1.legend(loc="upper right")

# Line: fraction of shots retained (right axis).
ax2 = ax1.twinx()
ax2.plot(x, fracs, "o-", color="darkorange")
ax2.set_ylabel("kept fraction", color="darkorange")
ax2.tick_params(axis="y", labelcolor="darkorange")
ax2.set_ylim(0, 1.05)

ax1.set_title("Selection strategy: expectation value vs shots retained")
fig.tight_layout()
plt.show()
raw circuit : <m> = +0.5791 (kept 100000/100000)
no selection : <m> = +0.5211 (kept 100000/100000)
pre-only (node) : <m> = +0.6026 (kept 1357/100000)
pre-only (edge) : <m> = +0.5637 (kept 13029/100000)
post-only (node) : <m> = +0.5682 (kept 2254/100000)
post-only (edge) : <m> = +0.5265 (kept 27514/100000)
pre=node + post=node : <m> = +0.6393 (kept 28/100000)
pre=node + post=edge : <m> = +0.5799 (kept 442/100000)
pre=edge + post=node : <m> = +0.6006 (kept 317/100000)
pre=edge + post=edge : <m> = +0.5521 (kept 3836/100000)

Output of the previous code cell