ノイズの多い量子プロセッサにおける、堅牢かつコヒーレントな非アベル型ハドロンダイナミクスの観測
推定実行時間:Heronプロセッサ(ibm_boston または同等のもの)で 6 分(注:これはあくまで推定値です。 (実行時間は状況によって異なる場合があります。)
学習成果
このチュートリアルを完了すると、以下のことを習得できます:
- 非アーベル格子ゲージ理論(具体的にはSU(2))を、効率的な量子シミュレーションのためにループ・ストリング・ハドロン(LSH)フレームワークを用いてどのように再定式化できるか
- 近似SU(2)ゲージ理論のハミルトニアンに対するトロッター化時間発展回路の構築方法と、それらを量子ビットに写像する方法
- IBM Quantum® ハードウェア上で、読み出し誤差の低減機能を備えたQiskit Estimatorプリミティブを使用して、これらの回路を実行する方法
前提条件
以下のトピックについて、あらかじめ理解しておくことをお勧めします:
- 量子回路と量子ゲートの基礎
- Qiskit Estimator プリミティブの概要
- 量子場理論の概念について基本的な知識があること(あれば望ましいが必須ではない。背景のセクションで要点を解説している)
背景
モチベーション
量子色力学(QCD)は、強い力を記述するSU(3)ゲージ理論であり、クォークをハドロンに束縛し、閉じ込めや弦の破れを支配している。 古典格子QCDの手法は静的性質の解析には優れているが、符号問題のため、リアルタイムのダイナミクスをシミュレーションすることはできない。 量子コンピュータは、ゲージ場の自由度をクビットに直接エンコードすることで、この障壁を乗り越える道筋を提供する。
このチュートリアルでは、そのようなシミュレーションの手法を紹介します。 IBM Quantum ハードウェアを用いて、(1+1)次元のSU(2)格子ゲージ理論におけるハドロン伝播をリアルタイムでシミュレートします。これは、最も単純な非アーベルゲージ理論であり、完全なQCDへの足がかりとなるものです。
コグート・サスキンド・ハミルトニアン
この理論は、 1D の空間格子上で、サイト上にスタッガード配置のフェルミオン(物質)が、リンク上にSU(2)ゲージ場が存在するという設定に基づいて定式化されている。 無次元形に換算すると、ハミルトニアンは次のようになる:
ここで、 はクロモ電気場エネルギー、 はスタッガード質量項、 は物質・ゲージ相互作用(ホッピング)項、 はフェルミオン質量を表し、 は相互作用の強さを表す。 この理論の連続極限は、 および にある。
ループ・ストリング・ハドロン(LSH)フレームワーク
重要な課題の一つは、各リンク上のゲージ場のヒルベルト空間が無限次元であるという点である。 ループ・ストリング・ハドロン(LSH) フレームワークは、この問題に対処するため、理論をゲージ不変の変数――フラックスのループ、分離した電荷を結ぶストリング、およびハドロン(サイトにおけるゲージシングレットフェルミオン対)――を用いて再定式化している。 LSH基底では、ガウスの法則は構成上自動的に満たされるため、すべての基底状態は物理的な状態である。 各格子点は、ループ数、入射ストリング、および出射ストリングを表す3つの量子数 によって特徴づけられる。ここで、 はフェルミオン的であり、 はボソン的である。 これらから、局所フェルミオン数は、サイト数が偶数の場合は 、奇数の場合は と定義される。
完全ハミルトニアンから量子回路へ:3つの重要な近似
この量子回路は、SU(2)ハミルトニアン全体を厳密にシミュレートするものではない。 その代わりに、この手法では、 弱結合領域 ( )において有効な、制御された一連の近似を実装しています。何が近似され、何が近似されていないかを理解することが不可欠です:
近似 1 — における弱結合極限: 完全相互作用ハミルトニアン (式 [1] の式(16)には、 のような項を通じて、ボソン量子数 に依存する係数が含まれている。弱結合領域( )では、ダイナミクスは電気項 によって支配され、これにより が大きい状態が優先される。 の場合、比 となり、これらすべての係数は 1 に簡約される。 これにより、相互作用ハミルトニアンは、純粋に局所的な最近接ホッピングに還元される:
これは とは独立しており、フェルミオン系の 量子ビットにのみ作用する。
近似 2 — における全球平均フラックス: 電気エネルギーは、各リンクにおける に依存する。 弱結合の真空状態では、 は大きく、ほぼ一様である。 サイトごとの の値を、単一のグローバル平均値 に置き換え、 を、各サイトにおけるフェルミオン配置に比例する対角位相とする:
ここで、 は、フェルミオン配置 におけるサイトについて和をとったものであり、 は無視できるグローバル位相である。
近似 3 — トロッター化: 持続時間 の 1 ステップに対する時間発展演算子は、次のように分解される:
ここで、 、 、および である。この1次トロッター分解では誤差が生じるが、 となるにつれてこの誤差は消える。ここでは、 を全域で固定する。
これら3つの近似の結果、サイトあたり2つのフェルミオン量子ビット のみが動的であり、ボソン量子ビット の自由度は有効パラメータに吸収されている。 これにより、 個の格子点に対して 個の量子ビットからなるコンパクトな回路が得られる。ここで、各トロッターステップの2量子ビットゲートの深さは一定(1ステップあたり13)である。
このチュートリアルでシミュレートする内容
このチュートリアルでは、 ハドロンの伝播をシミュレートします。強結合真空(積状態)から始め、格子の中心にメゾンを配置し、時間を進めます。 差分測定プロトコル――中心のメゾンを含む場合と含まない場合で回路を動作させ、その差を算出する――により、ハードウェアノイズや境界効果からコヒーレントなハドロン信号を分離することができる。 その結果、閉じ込められたメソン呼吸モードに特徴的な、フェルミオン密度の振動による光錐パターンが得られる。
要件
このチュートリアルを始める前に、以下のものをインストールしてください:
- Qiskit SDK v2.0 またはそれ以降のバージョンで、 可視化機能をサポートしたもの
- Qiskit Runtime v0.22 またはそれ以降 (
pip install qiskit-ibm-runtime) - Pauli Propagation パッケージ (
pip install pauli-prop) - NumPy (
pip install numpy) - Matplotlib (
pip install matplotlib)
セットアップ
まず、必要なライブラリをインポートし、LSH時間発展のための量子回路を構築するヘルパー関数を定義します。 回路構築には、主に3つの機能があります:
-
pair_hamiltonian_circuit: 隣接するサイト間の近似相互作用ハミルトニアンに対して、2量子ビットのユニタリー演算 を実装する。 ゲートの分解は次のとおりです: 。 -
electric_hamiltonian_circuit: 各サイトにおける近似電界エネルギーに対して、2量子ビットのユニタリー演算 を実装する。 ゲートの分解は次のとおりです: 。 -
construct_circuit: インタラクション項、電気項、質量項をSWAPゲートを用いて重ね合わせ、量子ビット間の接続を制御することで、完全な「トロッター化」回路を構築する。
# Import libraries
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm
from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from typing import Optional
import warnings
warnings.filterwarnings("ignore")def pair_hamiltonian_circuit(c: float) -> QuantumCircuit:
"""Two-qubit unitary for the approximate interaction Hamiltonian H_I.
Implements exp(-i * c * H_I^approx) for one pair of neighboring sites,
where c = delta_tau * x.
"""
qc_temp = QuantumCircuit(2)
qc_temp.cx(1, 0)
qc_temp.h(1)
qc_temp.rz(-c, 1)
qc_temp.cx(0, 1)
qc_temp.rz(c, 1)
qc_temp.cx(0, 1)
qc_temp.h(1)
qc_temp.cx(1, 0)
return qc_temp
def electric_hamiltonian_circuit(theta: float) -> QuantumCircuit:
"""Two-qubit unitary for the approximate electric field Hamiltonian H_E.
Implements exp(-i * theta * H_E^approx) for one lattice site,
where theta = -delta_tau * (n_bar_l / 2 + 3/4).
"""
qc_temp = QuantumCircuit(2)
qc_temp.x(0)
qc_temp.rz(theta / 2, 0)
qc_temp.cx(0, 1)
qc_temp.rz(-theta / 2, 1)
qc_temp.cx(0, 1)
qc_temp.rz(theta / 2, 1)
qc_temp.x(0)
return qc_temp
def construct_circuit(
num_lattice_point: int,
num_trotter_steps: int,
c: float,
theta: float,
m: float,
theory: Optional[int] = 2,
barriers: Optional[bool] = False,
measurement: Optional[bool] = False,
add_init_state: Optional[bool] = True,
inverse_mid: Optional[bool] = False,
) -> QuantumCircuit:
"""Construct the full Trotterized time-evolution circuit.
Builds a circuit implementing n Trotter steps of the approximate SU(2)
LSH Hamiltonian evolution. The qubit layout uses a zigzag ordering:
n_i(0), n_i(1), n_o(0), n_o(1), n_i(2), n_i(3), n_o(2), n_o(3), ...
which minimizes the number of SWAP layers needed.
Args:
num_lattice_point: Number of lattice sites
(num_qubits = 2 * num_lattice_point).
num_trotter_steps: Number of Trotter steps.
c: Interaction parameter (delta_tau * x).
theta: Electric field phase parameter.
m: Mass parameter (m_tilde = delta_tau * mu).
theory: 1 for single chain, 2 for SU(2). Default 2.
barriers: Insert barriers between Trotter layers for
visualization.
measurement: Append measurements at the end.
add_init_state: Prepare the half-filled (strong-coupling vacuum)
initial state.
inverse_mid: Swap the central sites
(for differential measurement protocol).
"""
num_qubits = theory * num_lattice_point
qc = QuantumCircuit(num_qubits)
if num_trotter_steps <= 0:
return qc
# --- Initial state preparation ---
if add_init_state:
i = 1
while i < num_lattice_point:
for j in range(theory):
qc.x(i + j * num_lattice_point)
i = i + 2
if inverse_mid:
mid_lattice_qubits = [num_qubits // 2 - 1, num_qubits // 2]
qc.x(mid_lattice_qubits)
else:
i = 1
while i < num_qubits - 1:
qc.swap(i, i + 1)
i = i + 4
# --- Trotter steps ---
for step in range(num_trotter_steps):
if barriers:
qc.barrier()
# First SWAP layer (skipped at step 0 — absorbed into initial state mapping)
if step > 0:
i = 1
while i < num_qubits - 1:
qc.swap(i, i + 1)
i = i + 4
# First layer of pair interactions
j = 0
while j < num_qubits - 2:
circ = pair_hamiltonian_circuit(c)
qc.compose(circ, [j, j + 1], inplace=True)
j = j + 2
if num_lattice_point % 2 == 0:
circ = pair_hamiltonian_circuit(c)
qc.compose(circ, [j, j + 1], inplace=True)
# Second SWAP layer
i = 1
while i < num_qubits - 1:
qc.swap(i, i + 1)
i = i + theory
# Second layer of pair interactions
j = 2
while j < num_qubits - 3:
circ = pair_hamiltonian_circuit(c)
qc.compose(circ, [j, j + 1], inplace=True)
j = j + 2
if num_lattice_point % 2 != 0:
circ = pair_hamiltonian_circuit(c)
qc.compose(circ, [j, j + 1], inplace=True)
# Third SWAP layer
i = 3
while i < num_qubits - 1:
qc.swap(i, i + 1)
i = i + 2 * theory
# Electric field term
if theta != 0:
e_circ = electric_hamiltonian_circuit(theta)
for j in range(num_lattice_point):
qc.compose(e_circ, [2 * j, 2 * j + 1], inplace=True)
# Mass term: Rz(-m_tilde) for even sites, Rz(m_tilde) for odd sites
for q in range(num_qubits):
if q % 2 == 0:
qc.rz(-1 * m, q)
else:
qc.rz(m, q)
if measurement:
qc.measure_all()
return qcdef get_probabilities(expval: float):
"""Convert a Z-expectation value to site occupation probability.
Since <Z> = p(0) - p(1), the occupation probability is p(1) = (1 - <Z>) / 2.
"""
p1 = round((1 - expval) / 2, 3)
return p1
def get_number(expval_data, num_lattice_point):
"""Convert raw Z-expectation values to staggered fermion number n_f at each site.
n_f(r) = n_i(r) + n_o(r) for even r
n_f(r) = 2 - [n_i(r) + n_o(r)] for odd r
The two qubits per site encode (n_i, n_o), and occupation probabilities
give us <n_i> and <n_o>.
"""
N = []
for expvals in expval_data:
Pstep = [get_probabilities(expval) for expval in expvals]
Nstep = []
for k in range(num_lattice_point):
val = Pstep[2 * k] + Pstep[2 * k + 1]
a = 2 * (k % 2) + (1 - 2 * (k % 2)) * val
Nstep.append(float(a))
N.append(Nstep)
return N
def calculate_difference(N, N_mid, num_lattice_point):
"""Differential measurement protocol: |n_f(meson) - n_f(vacuum)|.
Subtracting the vacuum (SCV) evolution from the meson evolution
isolates the coherent hadron signal from symmetric noise and boundary effects.
"""
N_diff = []
for i in range(len(N)):
Nstep_diff = []
for j in range(num_lattice_point):
Nstep_diff.append(abs(N[i][j] - N_mid[i][j]))
N_diff.append(Nstep_diff)
return N_diff小規模シミュレータの例
まず、6サイト格子(12キュービット)を用いて小規模なワークフローを実証し、ハードウェア上で実行する前に、回路の構築を確認し、物理的観測量を理解できるようにします。
ステップ 1:古典的な入力を量子問題に写像する
本論文( 、 )で検討された弱結合領域に一致する物理パラメータを定義する。導出された回路パラメータは以下の通りである:
- (相互作用パラメータ)
- (電界の位相)
- (質量パラメータ)
トロッターのステップ数ごとに、 2つの回路を構築する。1つは中心でメゾンを初期化する回路(inverse_mid=True)であり、もう1つは強結合真空を準備する回路(inverse_mid=False)である。微分測定プロトコルでは、真空の進化を差し引くことで、ハドロン信号を分離する。
# Physical / circuit parameters
num_lattice_point = 6 # 6 lattice sites -> 12 qubits for SU(2)
num_qubits = 2 * num_lattice_point
c = 0.15 # delta_tau * x
theta = 0.01 # electric field phase
m = 0.03 # m_tilde = delta_tau * mu
trotter_steps = range(1, 11) # 10 Trotter steps
print(f"Lattice sites: {num_lattice_point}, Qubits: {num_qubits}")
print(f"Parameters: c={c}, theta={theta}, m_tilde={m}")Output:
Lattice sites: 6, Qubits: 12
Parameters: c=0.15, theta=0.01, m_tilde=0.03
# Build circuits: meson initial state and vacuum (SCV) initial state
circuits_mid = [
construct_circuit(
num_lattice_point,
d,
c,
theta,
m,
barriers=False,
measurement=False,
add_init_state=True,
inverse_mid=True,
)
for d in trotter_steps
]
circuits = [
construct_circuit(
num_lattice_point,
d,
c,
theta,
m,
barriers=False,
measurement=False,
add_init_state=True,
inverse_mid=False,
)
for d in trotter_steps
]
# Visualize a single Trotter step
print(
f"Circuit for 1 Trotter step: {circuits[0].num_qubits} qubits, depth {circuits[0].depth()}"
)
circuits[0].draw("mpl", fold=-1)Output:
Circuit for 1 Trotter step: 12 qubits, depth 26
ステップ 2:量子ハードウェアでの実行に向けて問題を最適化する
観測量として、各量子ビットに対する単一量子ビットの 測定を定義する。 から、占有確率を抽出し、各格子点 におけるスタッガードフェルミオン数 を算出することができます。
# Z observable on each qubit
observables = [
SparsePauliOp("I" * i + "Z" + "I" * (num_qubits - i - 1))
for i in range(num_qubits)
]
print(f"Number of observables: {len(observables)}")Output:
Number of observables: 12
ステップ 3: Qiskit primitives を使用して実行する
小規模でのノイズのない正確なシミュレーションには、 を使用 StatevectorEstimator してください。
from qiskit.primitives import StatevectorEstimator
estimator = StatevectorEstimator()
# Run meson circuits
pubs_mid = [(circuit, observables) for circuit in circuits_mid]
result_mid = estimator.run(pubs_mid).result()
# Run vacuum (SCV) circuits
pubs = [(circuit, observables) for circuit in circuits]
result = estimator.run(pubs).result()
# Extract expectation values
raw_expvals_mid = [
result_mid[i].data.evs[::-1] for i in range(len(circuits_mid))
]
raw_expvals = [result[i].data.evs[::-1] for i in range(len(circuits))]
print(f"Computed expectation values for {len(raw_expvals)} Trotter steps")Output:
Computed expectation values for 10 Trotter steps
ステップ4:後処理を行い、希望する従来の形式で結果を返す
期待値をスタッガードフェルミオン数 に変換し、微分測定プロトコル(メソン 真空)を適用して、ハドロン伝播ヒートマップを生成する。 これは、参考論文の図3の構成を再現したものです。x軸には格子点 、y軸にはトロッターステップ(時間) 、そして色スケールとして が用いられています。
# Compute fermion numbers
N_mid_sim = get_number(raw_expvals_mid, num_lattice_point)
N_sim = get_number(raw_expvals, num_lattice_point)
N_diff_sim = calculate_difference(N_mid_sim, N_sim, num_lattice_point)
# --- Reproduce Figure 3 style: Staggered Fermionic Occupation Number Dynamics ---
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# Convert to numpy arrays for plotting
N_mid_arr = np.array(N_mid_sim)
N_arr = np.array(N_sim)
N_diff_arr = np.array(N_diff_sim)
# Color scheme
vmax = max(max(sublist) for sublist in N_arr)
vmin = -vmax
# Meson evolution
norm1 = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im0 = axes[0].imshow(
N_mid_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm1,
extent=[0, num_lattice_point, 0.5, len(trotter_steps) + 0.5],
)
axes[0].set_xlabel("Lattice site $r$", fontsize=12)
axes[0].set_ylabel("Trotter step $t$", fontsize=12)
axes[0].set_title("$n_f(r,t)$ — Meson initial state", fontsize=12)
plt.colorbar(im0, ax=axes[0], label="$n_f(r,t)$")
# Vacuum (SCV) evolution
im1 = axes[1].imshow(
N_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm1,
extent=[0, num_lattice_point, 0.5, len(trotter_steps) + 0.5],
)
axes[1].set_xlabel("Lattice site $r$", fontsize=12)
axes[1].set_ylabel("Trotter step $t$", fontsize=12)
axes[1].set_title("$n_f(r,t)$ — Vacuum (SCV)", fontsize=12)
plt.colorbar(im1, ax=axes[1], label="$n_f(r,t)$")
# Differential: meson - vacuum
norm2 = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im2 = axes[2].imshow(
N_diff_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm2,
extent=[0, num_lattice_point, 0.5, len(trotter_steps) + 0.5],
)
axes[2].set_xlabel("Lattice site $r$", fontsize=12)
axes[2].set_ylabel("Trotter step $t$", fontsize=12)
axes[2].set_title(
"Staggered Fermionic Occupation Number Dynamics\n$|n_f^{\\mathrm{meson}} - n_f^{\\mathrm{vacuum}}|$",
fontsize=12,
)
plt.colorbar(im2, ax=axes[2], label="$n_f(r,t)$")
plt.suptitle(
f"Hadron propagation — {num_lattice_point}-site lattice (StatevectorEstimator)",
fontsize=14,
y=1.02,
)
plt.tight_layout()
plt.show()Output:
大規模なハードウェアの例
ここでは、 IBM Quantum ハードウェア上で、30サイトの格子(60キュービット)へとスケールアップします。 このスケールでは、10トロッターステップの回路は、3400個以上の2量子ビットゲートと14,000個の単一量子ビットゲートで構成されています。
手順 1~4(1つのコードブロックにまとめました)
ハードウェア・ワークフローの主なポイント:
- メソン回路および真空回路用の10ステップのトロッター法(ドリフトを最小限に抑えるためインターリーブ処理済み)
- — による
optimization_level=1トランスパイレーション — 回路レイアウトはすでにデバイスのトポロジー(線形チェーン)と同型であるため、配線によるSWAPは不要です。 このトランスパイラーは、ノイズの少ない物理量子ビットの連鎖を選択し、ゲートをネイティブゲートセットに分解することのみを目的として使用されます。 EstimatorV2TREXの読み出しエラーの低減とパウリ・トゥワーリングを用いてBatchすべてのジョブを一括で送信するセッション
# -------------------------Step 1: Define parameters & build circuits-------------------------
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2, Batch
from qiskit_ibm_runtime.options import (
EstimatorOptions,
ResilienceOptionsV2,
TwirlingOptions,
DynamicalDecouplingOptions,
)
service = QiskitRuntimeService()
num_lattice_point_hw = 30
num_qubits_hw = 2 * num_lattice_point_hw # 60 qubits
c_hw = 0.15
theta_hw = 0.01
m_hw = 0.03
trotter_steps_hw = range(1, 11) # 10 Trotter steps
# Build meson and vacuum circuits
circuits_mid_hw = [
construct_circuit(
num_lattice_point_hw,
d,
c_hw,
theta_hw,
m_hw,
barriers=False,
measurement=False,
add_init_state=True,
inverse_mid=True,
)
for d in trotter_steps_hw
]
circuits_hw = [
construct_circuit(
num_lattice_point_hw,
d,
c_hw,
theta_hw,
m_hw,
barriers=False,
measurement=False,
add_init_state=True,
inverse_mid=False,
)
for d in trotter_steps_hw
]
print(f"Built {len(circuits_hw)} circuit pairs for {num_qubits_hw} qubits")
# -------------------------Step 2: Transpile for hardware-------------------------
# The circuit topology is a linear chain, isomorphic to the device topology.
# We use optimization_level=1 since no routing SWAPs are needed — the transpiler
# only needs to select a low-noise qubit chain and decompose to native gates.
backend = service.backend("ibm_boston")
layout = [
140,
141,
142,
143,
136,
123,
122,
121,
116,
101,
102,
103,
96,
83,
82,
81,
76,
61,
62,
63,
64,
65,
66,
67,
68,
69,
78,
89,
88,
87,
97,
107,
106,
105,
117,
125,
126,
127,
137,
147,
148,
149,
150,
151,
152,
153,
154,
155,
139,
135,
134,
133,
132,
131,
130,
129,
118,
109,
110,
111,
]
pm = generate_preset_pass_manager(
optimization_level=1, backend=backend, initial_layout=layout
)
isa_circuits_mid = pm.run(circuits_mid_hw)
isa_circuits = pm.run(circuits_hw)
print(f"Transpiled circuits. Example depth: {isa_circuits[0].depth()}")
# Define and layout-map observables
observables_hw = [
SparsePauliOp("I" * i + "Z" + "I" * (num_qubits_hw - i - 1))
for i in range(num_qubits_hw)
]
isa_observables_mid = [
[obs.apply_layout(isa_circuits_mid[i].layout) for obs in observables_hw]
for i in range(len(isa_circuits_mid))
]
isa_observables = [
[obs.apply_layout(isa_circuits[i].layout) for obs in observables_hw]
for i in range(len(isa_circuits))
]
# Build PUBs — interleave meson and vacuum for each Trotter step
isa_pubs_mid = [
(circ, obs) for circ, obs in zip(isa_circuits_mid, isa_observables_mid)
]
isa_pubs = [(circ, obs) for circ, obs in zip(isa_circuits, isa_observables)]
pubs_to_execute = [
[isa_pubs_mid[i], isa_pubs[i]] for i in range(len(isa_pubs))
]
# -------------------------Step 3: Execute on hardware-------------------------
twirling_options = TwirlingOptions(
enable_gates=True,
enable_measure=True,
shots_per_randomization="auto",
strategy="active-circuit",
)
resilience_options = ResilienceOptionsV2(
measure_mitigation=True, # TREX readout error mitigation
zne_mitigation=False, # ZNE turned off
)
dd_options = DynamicalDecouplingOptions(
enable=False # Circuit is sufficiently dense
)
options = EstimatorOptions(
resilience=resilience_options,
twirling=twirling_options,
dynamical_decoupling=dd_options,
default_shots=10_000,
)
ids = []
with Batch(backend=backend) as batch:
for idx, pub in enumerate(pubs_to_execute):
print(f"Submitting job for Trotter step {idx + 1}")
estimator = EstimatorV2(mode=batch, options=options)
estimator.skip_transpilation = True
job = estimator.run(pub)
ids.append(job.job_id())
batch_id = batch.session_id
job_info = {"ids": ids, "batch_id": batch_id}
print(f"Submitted {len(ids)} jobs. Batch ID: {batch_id}")print(ids)# -------------------------Step 4: Post-process results-------------------------
jobs = [service.job(job_id) for job_id in ids]
results = [job.result() for job in jobs]
# Extract expectation values (index 0 = meson, index 1 = vacuum)
raw_expvals_mid_hw = [result[0].data.evs[::-1] for result in results]
raw_expvals_hw = [result[1].data.evs[::-1] for result in results]
# Compute fermion numbers and differential
N_mid_hw = get_number(raw_expvals_mid_hw, num_lattice_point_hw)
N_hw = get_number(raw_expvals_hw, num_lattice_point_hw)
N_diff_hw = calculate_difference(N_mid_hw, N_hw, num_lattice_point_hw)N_diff_hw_arr = np.array(N_diff_hw)
fig, ax = plt.subplots(figsize=(10, 6))
vmax = np.abs(N_hw).max()
vmin = -vmax
norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im = ax.imshow(
N_diff_hw_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm,
extent=[0, 8, 0.5, len(trotter_steps_hw) + 0.5],
)
ax.set_xlabel("Lattice site $r$", fontsize=13)
ax.set_ylabel("Trotter step $t$", fontsize=13)
ax.set_title(
"Staggered Fermionic Occupation Number Dynamics\nQuantum Simulation on IBM Hardware — 30-site lattice (60 qubits)",
fontsize=13,
)
cbar = plt.colorbar(im, ax=ax)
cbar.set_label("$n_f(r,t)$", fontsize=12)
plt.tight_layout()
plt.show()Output:
パウリ伝播による古典的なベンチマーク
パウリ伝播法(PPM)は、ハイゼンベルク図式において、測定された観測量を回路全体に逆伝播させることで、量子回路のノイズのない古典的シミュレーションを実現する。 クリフォード層(CNOT、H、S、Xゲート)の下では、パウリ演算子は、項の数を増やすことなく、他のパウリ演算子に写像される。 非クリフォード層(回路内の ゲート)は分岐を引き起こす可能性があり、最悪の場合、項の数が2倍になることもありますが、多くの分岐は係数が小さいため、切り捨てることができます。
の pauli-prop ワークフローは以下の通りです:
evolve_through_cliffordsを使用して、回路をクリフォード部分と非クリフォード部分に分割する。atol``propagate_through_circuit各観測量を、を用いて非クリフォード部分を通じて伝播させ、最大max_terms個のパウリ項まで保持し、係数が切り捨て閾値 未満の項は除外する。- Qiskitに組み込まれているクリフォード演算のサポート機能を使用して、クリフォード演算を通じて結果を導出します。
- 対角のパウリ項( および のみを含む)の係数を合計することで、期待値を求めます。
切り捨て閾値
の propagate_through_circuit パラメータは atol 、小さなパウリ分岐がどの程度積極的に剪定されるかを制御します。 1e-12しきい値を非常に厳しく設定すると(例えば、)、ほぼすべての分岐が保持され、正確な結果が得られますが、回路の深さに応じてシミュレーション時間が急激に増加します。 本論文で取り上げられた120キュービットのシミュレーションは、デフォルト設定では約 8.5 時間かかりました。 1e-3閾値を引き上げると(例えば、 または 1e-6 まで)、係数がその値を下回る項が除外され、追跡対象となる項の数が劇的に減少し、計算速度が向上します。 その代償として、わずかで制御可能な近似誤差が生じますが、これは異なる閾値での結果を比較することで検証することができます。
import time
from pauli_prop import evolve_through_cliffords, propagate_through_circuit
# ── PPM Configuration ──
# Truncation threshold: controls the speed/accuracy trade-off.
PPM_THRESHOLD = 1e-3
# Maximum Pauli terms to track per observable (hard cap on memory/time)
PPM_MAX_TERMS = 66_000
print(f"PPM settings: atol={PPM_THRESHOLD}, max_terms={PPM_MAX_TERMS}")
# We propagate each single-qubit Z observable through each circuit.
# For PPM, we work with the un-transpiled circuits (ideal noiseless simulation).
observables_pp = [
SparsePauliOp("I" * i + "Z" + "I" * (num_qubits_hw - i - 1))
for i in range(num_qubits_hw)
]
def ppm_expectation_values(
circuit, observables, max_terms=PPM_MAX_TERMS, atol=PPM_THRESHOLD
):
"""Compute expectation values of single-qubit Z observables
via Pauli propagation.
Args:
circuit: The quantum circuit to simulate.
observables: List of single-qubit Z observables.
max_terms: Maximum number of Pauli terms to retain (hard cap).
atol: Absolute tolerance — Pauli terms with coefficients below this
value are discarded during propagation. Larger values give
faster simulation at the cost of approximation accuracy.
"""
circuit = circuit.decompose(["swap"]) # decompose SWAPs into 3 CX gates
cliff, non_cliff = evolve_through_cliffords(circuit)
evs = []
for obs in observables:
evolved_obs = propagate_through_circuit(
obs, non_cliff, max_terms=max_terms, atol=atol, frame="h"
)[0]
evolved_obs.paulis = evolved_obs.paulis.evolve(cliff, frame="h")
diagonal_mask = ~evolved_obs.paulis.x.any(axis=1)
ev = float(evolved_obs.coeffs[diagonal_mask].sum().real)
evs.append(ev)
return np.array(evs)
# Run PPM for each Trotter step and record wall-clock time
pp_expvals_mid = []
pp_expvals = []
pp_times = []
for idx, d in enumerate(trotter_steps_hw):
t_start = time.perf_counter()
# Meson circuit
evs_mid = ppm_expectation_values(circuits_mid_hw[idx], observables_pp)
# Vacuum circuit
evs_vac = ppm_expectation_values(circuits_hw[idx], observables_pp)
elapsed = time.perf_counter() - t_start
pp_times.append(elapsed)
pp_expvals_mid.append(evs_mid[::-1])
pp_expvals.append(evs_vac[::-1])
print(f"Trotter step {d:2d}: {elapsed:.1f} s")
print(f"\nTotal PPM simulation time: {sum(pp_times):.1f} s")
print(f"Truncation threshold used: {PPM_THRESHOLD}")Output:
PPM settings: atol=0.001, max_terms=66000
Trotter step 1: 5.0 s
Trotter step 2: 7.5 s
Trotter step 3: 11.2 s
Trotter step 4: 14.7 s
Trotter step 5: 18.3 s
Trotter step 6: 22.1 s
Trotter step 7: 25.6 s
Trotter step 8: 29.4 s
Trotter step 9: 33.2 s
Trotter step 10: 36.6 s
Total PPM simulation time: 203.6 s
Truncation threshold used: 0.001
# --- PPM simulation time vs. Trotter steps ---
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(
list(trotter_steps_hw),
pp_times,
"o-",
color="tab:blue",
linewidth=2,
markersize=6,
)
ax.set_xlabel("Trotter step", fontsize=13)
ax.set_ylabel("Wall-clock time (s)", fontsize=13)
ax.set_title(
"Pauli Propagation simulation time vs. Trotter steps\n(30-site lattice, 60 qubits)",
fontsize=13,
)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()Output:
# --- PPM heatmap and comparison with hardware ---
N_mid_pp = get_number(pp_expvals_mid, num_lattice_point_hw)
N_pp = get_number(pp_expvals, num_lattice_point_hw)
N_diff_pp = calculate_difference(N_mid_pp, N_pp, num_lattice_point_hw)
N_diff_pp_arr = np.array(N_diff_pp)
fig, axes = plt.subplots(1, 2, figsize=(18, 6))
vmax = np.abs(N_hw).max()
vmin = -vmax
norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
# PPM result
im0 = axes[0].imshow(
N_diff_pp_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm,
extent=[0, num_lattice_point_hw, 0.5, len(trotter_steps_hw) + 0.5],
)
axes[0].set_xlabel("Lattice site $r$", fontsize=12)
axes[0].set_ylabel("Trotter step $t$", fontsize=12)
axes[0].set_title(
"Pauli Propagation\n(classical noiseless simulation)", fontsize=12
)
plt.colorbar(im0, ax=axes[0], label="$n_f(r,t)$")
# Hardware result
im1 = axes[1].imshow(
N_diff_hw_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm,
extent=[0, num_lattice_point_hw, 0.5, len(trotter_steps_hw) + 0.5],
)
axes[1].set_xlabel("Lattice site $r$", fontsize=12)
axes[1].set_ylabel("Trotter step $t$", fontsize=12)
axes[1].set_title(
"Quantum Simulation\n(IBM Hardware, readout error mitigation only)",
fontsize=12,
)
plt.colorbar(im1, ax=axes[1], label="$n_f(r,t)$")
plt.suptitle(
"Staggered Fermionic Occupation Number Dynamics — 30-site lattice",
fontsize=14,
y=1.02,
)
plt.tight_layout()
plt.show()Output:
次のステップ
この作品に興味を持たれた方は、以下の資料もぜひご覧ください:
- Qiskit Estimator プリミティブのドキュメント — エラー軽減オプションの設定に関する詳細
- エラーの軽減および抑制手法 — TREX、ZNE、その他の軽減手法について学ぶ
- Qiskit Pauli Propagation (pauli-prop) — パウリ逆伝播を用いたRustによる高速化された古典シミュレーション
参照
[1] 原著論文:Ilčić, Majumdar, Mathew ほか「ノイズの多い量子プロセッサにおける堅牢かつコヒーレントな非アベル型ハドロンダイナミクスの観測」『 arXiv:2602.18080 』(2026年)