非マルコフ型誤り検査を用いた事後選択
このガイドでは、非マルコフ型エラーチェックとは何か、およびソフトウェアでそれを実装する方法について説明します qiskit-mitigation。 まずは、2量子ビットの簡単な例から始め、検証手順を具体的にイメージし、その仕組みについて解説します。 直感をつかんだら、チェックを実装し、さまざまなポストセレクション戦略を比較する20キュービットの例題に取り組みます。
非マルコフ型エラーチェックとは何ですか?
非マルコフ型エラーチェックとは、量子ビットの状態が非マルコフ型エラー源によって破損している可能性が高いかどうかを、測定に基づいて検証する手法である。 このテストは、回路の冒頭で各量子ビットに持続時間が延長されたビット反転ゲート(xslow または、角度が である一連の ゲート rx )を適用し、続いて通常の測定を行い XGate 、状態が から正常に反転したことを確認することで行われます。また、このテストは回路の終了時、各終端測定の後に実行することもでき、デバイスが量子ビットの測定済み状態を から正常に反転できることを確認できます。 チェックに失敗したサンプルは信頼できないものとみなすことができ、すべてのチェックに合格したサンプルのみを選別することで、ノイズの多い分布のフィデリティを向上させることができる。
非マルコフ型誤りチェックに基づくポストセレクションの戦略
- ノードベースのポストセレクション :各量子ビットがチェックに合格したサンプルのみをポストセレクションの対象とする。 これは最も厳格な戦略であり、一般的により多くのサンプルが除外される結果となります。
- エッジに基づくポストセレクション :量子ビットの結合グラフにおける各エッジについて、少なくとも1つの量子ビットがチェックをクリアするようにする。 この戦略はより寛容であり、ある量子ビットがチェックに失敗した場合でも、その量子ビットの隣接するすべての量子ビットがチェックに合格していれば、そのサンプルは保持されることがある。
スペクテーター量子ビット
元の回路の実行に使用される量子ビットに隣接する「傍観者」アンシラ量子ビットを指定することができます。 これらのスペクテーターには、非マルコフ型エラーチェックを適用することができ、それによって、これらのチェックが検出するように設計されている非マルコフ型ノイズに対する検出範囲を広げることができる。
パート1. 非マルコフ型のエラーチェックを可視化する
QPU上でこの手法を実演する前に、まず2クビットの小さな回路を作成し、その動作を可視化するためのチェック機能を追加します。 まず、バック ibm_kingston エンドのQPUにトランスパイルされたベル状態から始めます。
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:
回路の先頭に、非マルコフ型の誤りチェックをいくつか追加してください。 チェックは、測定で終了するキュービットに対してのみ行われます。 これらのチェックにより、デバイスが量子ビットの初期状態を から正常に反転させることができることが保証され、これらのチェックによる測定結果は、と呼ばれる新しい古典レジスタに読み出されます 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:
同様に、各端子の測定後に、回路の末端にチェックを追加することもできます。 これらのチェックにより、デバイスが量子ビットの状態を に切り替えることができることが保証され、その測定結果は、と呼ばれる新しい古典レジスタに読み出されます 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:
はじめに述べたように、データ量子ビットに隣接する補助量子ビットを「オブザーバー」として利用することで、エラーのカバー範囲を広げることができます。 サーキット走行前および走行後の観客確認を追加する。 物理量子ビット 2 がデータ量子ビット に隣接 ibm_kingston しているため 1、それがオブザーバーとして追加されており、回路の前後でチェックが追加されていることがわかります。 観測値は、および spec_pre と呼ばれる新しい古典レジスタに追加されました spec。
このような回路には、考えられるすべての非マルコフ型エラーチェックが含まれているため、この回路からサンプリングを行うことで、ユーザーは- [pre/post]回路チェックと- [node/edge]に基づくポストセレクション戦略の任意の組み合わせに基づいてポストセレクションを行うことが可能になる。 量子ハードウェア上での動作については、本ガイドの第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:
第2部:QPUにおける非マルコフ型エラーチェックの実証と、ポストセレクション戦略の比較
非マルコフ型エラーチェックとは何か、またそれがソフトウェアでどのように実装されているかについて理解できたところで、QPU上でより大規模な例を実行し、その動作を確認してみましょう。
まず、回路を用意します。 回路の選択はそれほど重要ではありません。出力を簡単に確認できる回路であれば十分です。 ここでは、20キュービットの、 1D のミラーリングされたイジング回路を選択する。 この回路をハードウェア上で実行するのは容易ではなく、非マルコフ誤差の影響を受けるはずである。これにより、非マルコフ誤差チェックの有効性を実証する機会が得られる。 また、この対称的な構造により、ノイズの混入した期待値の計算結果を、厳密な値と簡単に比較することができます。
この実験では、観測可能な期待値を算出しています。 観測量 の選択も重要ではないため、単に平均単一サイト磁化 を選択する。
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)Output:
two-qubit depth: 24
total depth: 85
ここで、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])Output:
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()}")Output:
job id: d9badrm6hjac73ffe31g
status: QUEUED
このクラ PostSelector スはチェックの構造 creg を把握しているため、サンプル内の各ビットを、それに対応する回路測定値に関連付けることができます。 この手法 compute_mask は、事後選択戦略(ノードまたはエッジ)と、検討対象とする非マルコフ誤差チェックの種類(回路前または回路後)が与えられれば、一連のサンプルに対してマスクを作成するために使用できる。 回路の前段戦略と後段戦略を単一のマスクに統合するには、それぞれのマスクを個別に計算し、その後、演算 AND を用いてそれらを組み合わせることができます。 ここでは、 を用いて、さまざまな戦略すべてについてマスクを作成 PostSelector し、利用可能な戦略のすべての組み合わせについて結果を確認します。
以下の可視化図では、ポストセレクション戦略がポストセレクション率や回答の質にどのような影響を与えるかがわかります。 予想通り、ノードベースのポストセレクションを用いた戦略では、ポストセレクション率が最も低かった。 エッジベースの戦略はより寛容であり、選択後の生存率がより高くなる傾向があります。 ユーザーは、自身の問題に対してさまざまな戦略を試行し、ポストセレクション率とサンプリングされた分布の品質との適切なバランスを見極める必要があります。 この例では、回路の事前チェックおよび事後チェックを伴うノードベースのポストセレクションを用いた結果、ポストセレクション率 が の最良解が得られた .028%。 一部の戦略は、素の回路の性能を上回らなかった。 また、チェック項目を追加したものの、それらを一切使用しなかった場合(下のグラフの「選択なし」と表示された棒グラフ)が、最もパフォーマンスが劣ることがわかります。
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()Output:
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)