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

OBPによる回路の古典シミュレーション

このガイドでは、QuantumCircuitインスタンスを古典的にシミュレーションし、OBPのみで期待値を推定する方法を学びます。

OBPはオブザーバブルを受け取り、与えられた回路を通して逆伝播するため、回路の「シミュレーション」とは、この回路に関する対象オブザーバブルの期待値を計算することに相当します。 後ほど説明するように、qiskit-addon-obpパッケージは単純なノイズモデルも扱えるため、ノイズを含む期待値も計算できます。

サンプル回路を構築する​

このガイドでは、Pauli項の切り捨てガイドと同じサンプル回路を使用します。

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

_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "rustworkx": "rustworkx"}
_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")
import rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types

# we generate a linear chain of 10 qubits
num_qubits = 10
linear_chain = rustworkx.generators.path_graph(num_qubits)

# we use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# we evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)

ただし、上記は選んだハミルトニアンによる時間発展を表す回路にすぎません。 オブザーバブルの期待値を計算する基準となる、開始用の初期状態も必要です。

もちろん、初期状態としてすべて0の状態(真空状態)を選ぶこともできますが、独自の初期状態を挿入する方法を示すために、以下では別の状態を選びます。

1つの方法は、上記の時間発展回路の前に初期状態を付け加えることです。circuit.compose(initial_state, front=True)。 ただし、すでにcircuitをスライスしているため、初期状態を最初のスライスとして挿入する方が簡単です。以下でそのようにします。

このようにすると、将来初期状態を入れ替えたくなった場合でも、スライスを再計算することなく、最初のスライスを別の初期状態に置き換えるだけで済みます。

from qiskit.circuit import QuantumCircuit

initial_state = QuantumCircuit(num_qubits)
for i in range(0, num_qubits, 2):
initial_state.x(i)

slices.insert(0, initial_state)
# for visualization purposes only, we recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Output of the previous code cell

ノイズのない期待値をシミュレーションする​

対象オブザーバブルとして、中央のキュービットに作用するZZオブザーバブルを選びます。

from qiskit.quantum_info import SparsePauliOp

obs = SparsePauliOp("IIIIZZIIII")

この時点で、OBPを使って期待値を古典的にシミュレーションする準備はできています。 そのためには、次のように、_すべての_スライスをbackpropagateメソッドに渡すだけです。

from qiskit_addon_obp import backpropagate

vacuum_state_obs, _, metadata = backpropagate(obs, slices)

これで、対象オブザーバブルobsを回路の_全体_(slices[0]に置いたinitial_stateを含む)に逆伝播できました。その結果、新しいSparsePauliOpが得られます。その期待値は、真空状態(|00...00>)に射影することで得られます。

これは、計算基底で定義されたすべてのPauli項の係数を足し合わせることで、簡単に実現できます。

vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)

動作確認として(またこれが正しく機能することを示すために)、結果をQiskitのStatevectorと比較できます。

from qiskit.quantum_info import Statevector

Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)

パフォーマンスに関するいくつかの注意​

上記のbackpropagate呼び出しの計算効率は、次のようなさまざまな要因に大きく左右されます。

  • circuitの構造
  • 回路のスライス方法
  • 対象オブザーバブル
  • 切り捨てパラメータ

backpropagateメソッドは、各_スライス_を適用するたびにオブザーバブルを簡約するため、1つのスライスに含まれるゲートの数は計算負荷に大きく影響します。 演算子の簡約という点で最も積極的な戦略は、回路を個々のゲートごとのスライスに分割することです。

さらに、backpropagateメソッドに組み込まれている切り捨て機構をすべて活用できます。 上記ではそうしなかったため、結果は厳密な期待値になっていますが、その方法はPauli項の切り捨てガイドで学べます。

ノイズのある期待値をシミュレーションする​

qiskit-addon-obpパッケージは、PauliLindbladErrorの形式でのノイズモデルの扱いにも対応しています。 これは、たとえばNoiseLearnerを使って、回路内の2キュービット層のノイズモデルを特徴付けた場合に特に有用です。

このセクションでは、NoiseLearnerが返すLayerErrorオブジェクトを使い、OBPでノイズのある期待値を計算する方法を説明します。

ノイズモデルを取得する​

通常は、NoiseLearnerを実行して、特定の回路のノイズモデルを取得します。 このチュートリアルでは複雑さ(とランダム性)を避けるため、そうはせず、代わりに以下で回路用のノイズモデルをハードコードします。

ただし、データの構造がNoiseLearnerResultの構造と一致するようにします。 現在の(トランスパイル前の)形では、この回路には2キュービットゲートの固有な層が4つあります。

  • slices[1]: すべての奇数番目のキュービットのペアにRxxゲートが作用する層
  • slices[2]: すべての偶数番目のキュービットのペアにRxxゲートが作用する層
  • slices[3]: すべての奇数番目のキュービットのペアにRyyゲートが作用する層
  • slices[4]: すべての偶数番目のキュービットのペアにRyyゲートが作用する層

以下のセルでは、これらの各層に対して、ランダム化した誤差率を持つ4つのLayerErrorインスタンスを手動で構築します。

import numpy as np
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.utils.noise_learner_result import (
LayerError,
PauliLindbladError,
)

# fmt: off
pauli_errors_even = ['IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIXX', 'IIIIIIIIXY', 'IIIIIIIIXZ', 'IIIIIIIIYI', 'IIIIIIIIYX', 'IIIIIIIIYY', 'IIIIIIIIYZ', 'IIIIIIIIZI', 'IIIIIIIIZX', 'IIIIIIIIZY', 'IIIIIIIIZZ', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII', 'XIIIIIIIII', 'XXIIIIIIII', 'XYIIIIIIII', 'XZIIIIIIII', 'YIIIIIIIII', 'YXIIIIIIII', 'YYIIIIIIII', 'YZIIIIIIII', 'ZIIIIIIIII', 'ZXIIIIIIII', 'ZYIIIIIIII', 'ZZIIIIIIII']
pauli_errors_odd = ['IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII']
# fmt: on

np.random.seed(42)

layer_error_odd_xx = LayerError(
circuit=slices[1],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)

layer_error_even_xx = LayerError(
circuit=slices[2],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)

layer_error_odd_yy = LayerError(
circuit=slices[3],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)

layer_error_even_yy = LayerError(
circuit=slices[4],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)

NoiseLearnerを使って回路の固有な2キュービットゲート層を特定し、そのノイズを特徴付けた場合は、NoiseLearnerResultオブジェクトが得られます。 この結果には、上で手動で構築したものと同様のLayerErrorオブジェクトのリストが含まれます。

固有な2キュービット層ごとに、LayerErrorには次のものが含まれます。

  • その2キュービットゲート層を表すQuantumCircuit
  • この回路が作用するキュービットのインデックス
  • この層の特徴付けられたノイズモデルを表すPauliLindbladError

PauliLindbladErrorには2つのオブジェクトが含まれます。

  • 特徴付けられたPauli誤差のリスト
  • それぞれのPauli誤差に対応する誤差率

通常、Pauli誤差のリストはスパースです。より具体的には、ゲートが作用するすべてのキュービットに対する1キュービットPauli誤差と、接続されているすべてのキュービットに対する2キュービットPauli誤差が含まれます。

ノイズのある層を回路に挿入する​

前のセクションでは、既知の固有な2キュービットゲート層のそれぞれに対して、LayerErrorを1つずつ構築しました。 つまり、どのLayerErrorがどのスライスに正確に対応するかがわかっています。

通常、LayerErrorを使う際には、固有な2キュービットゲート層が何であるか、そしてそれが回路内のどこに現れるかを突き止める必要があります。 そのうえで、LayerErrorを適切に挿入するよう、circuitやslicesを調整する必要があります。 一般的なケースでのこの方法は、このハウツーガイドの範囲外です。 TODO: 外部ドキュメントが公開されたら、リンクを追加する! ここでは、LayerErrorがどのスライスに対応するかがわかっているため、話は簡単です。 したがって、あとはノイズを表す新しいスライスを挿入するだけです。

各LayerError.errorのPauliLindbladErrorは、有効なQuantumCircuit命令にするために、PauliLindbladErrorInstructionでラップする必要があることに注意してください。

from qiskit_addon_obp.utils.noise import PauliLindbladErrorInstruction

noisy_slices = []
for slice_ in slices:
if slice_ == layer_error_even_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_even_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_yy.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_yy.error
),
slice_.qubits,
)
]
)
)
noisy_slices.append(slice_)

以下で回路を描画して、作業内容を確認できます。

combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)

Output of the previous code cell

ノイズのある期待値をシミュレーションする​

この時点で、期待値の古典的なシミュレーションは以前とまったく同じように動作します。ただし、

vacuum_state_noisy_obs, _, metadata = backpropagate(obs, noisy_slices)
vacuum_state_noisy_obs.coeffs[
~vacuum_state_noisy_obs.paulis.x.any(axis=1)
].sum()
np.complex128(-0.7230801696448901+7.082755280463563e-19j)

ここでも、パフォーマンスに関する複数の懸念事項を考慮する必要があることを指摘しておきます。 上記の該当セクションに戻ってご確認ください。