伝播ノイズ吸収(PNA)を用いて期待値を改善する
使用見積もり: Heronプロセッサーで10分(注: これはあくまでも見積もりです。実際の実行時間は異なる場合があります。)
学習成果
-
伝播ノイズ吸収(PNA)とは何か、そして学習された逆ノイズチャネルを測定対象のオブザーバブルに吸収させることで、2-Qubit Gateノイズをどのように軽減するか
-
samplomaticを使用して、トワリング、基底変換、ノイズ注入のために回路層をボックス化し注釈付けする方法 -
[NoiseLearnerV3](/guides/directed-execution-model#noiselearnerv3) を用いて層のノイズを学習し、qiskit-addon-pnaを用いてノイズ緩和オブザーバブルに伝播させる方法 -
qiskit-ibm-runtimeのQuantumProgramとExecutorクラスを用いてランダム化された回路をサンプリングし、PNAとTREXおよび事後選択を組み合わせる方法
前提条件
-
Qiskitパターン ワークフロー
-
オブザーバブルの期待値を計算するための Estimator プリミティブの使用
-
Estimatorプリミティブとエラー緩和オプションを組み合わせる で扱われている、パウリ・トワリングやTREXなどのエラー緩和技術
背景
このチュートリアルでは、Qiskitの高度なエラー緩和ツールを活用して、ノイズのある量子実験における期待値推定を改善する方法を示します。
伝播ノイズ吸収(PNA)とは何か?
伝播ノイズ吸収とは、2量子ビット・ゲートに影響を与える逆ノイズ・チャネルを通じてオブザーバブルを伝播させることでゲート誤差を緩和し、ノイズ緩和オブザーバブルを得る技術です。
完全なフォールトトレランスを必要とせずに、ノイズのある量子ハードウェアから有用な期待値を抽出するために量子エラー緩和を使用できます。PNAは特に、回路操作を変更するのではなく、ノイズの影響をオブザーバブル自体に吸収させることに焦点を当てています。
量子回路内の各ノイズのあるゲートは、理想的なゲートに続いてノイズ・チャネルが作用したものとしてモデル化できます。PNAはこれらのノイズ・チャネルを学習または特性評価し、それらの逆を定義します。逆操作をハードウェア実行に挿入する(これは通常実現不可能です)代わりに、PNAは逆ノイズ・チャネルを回路を通じて前方に伝播させ、オブザーバブルに適用します。このプロセスにより、オブザーバブル は新しい演算子 に変換され、ノイズのある回路上で を測定すると、理想的なノイズのない回路上で を測定した場合と同じ期待値が得られます。
これは次のように要約できます。
- 各ノイズのあるゲート を、理想的な操作に続いてノイズ・チャネル が作用したものとしてモデル化する。
- ノイズ特性評価ツールを用いて各 を学習または推定する。
- パウリ転送手法を用いて、逆ノイズ・マップ を定義し、回路を通じて前方に伝播させる。
- これらの逆をオブザーバブルに吸収させ、ノイズ緩和された演算子 を得る。
ノイズがパウリ・チャネル(またはより一般的にはスパース・パウリ・リンドブラッド・チャネル)として記述される場合、この伝播はパウリ伝播を使用して効率的に実行できます。パウリ伝播は、逆ノイズ・チャネルがクリフォード演算や非クリフォード演算の層を通過する際にどのように変換されるかを、計算複雑性を制御しながら近似するためのフレームワークを提供します。
緩和をオブザーバブルにシフトすることで、PNAは回路に物理的な補正演算を挿入することによって生じる大きなサンプリング・オーバーヘッドを回避します。代わりに、元のノイズのある回路が実行され、オブザーバブルはノイズの影響を打ち消す期待値を持つ新しい演算子に変換されます。
PNAのワークフロー
このプロセスは、以下の概念的な段階を通して理解できます。最初の概略図は、標準的なノイズのある実験を示しています。
ノイズ・モデルを学習すれば、その逆を適用してノイズを打ち消すことができます。
確率的誤り消去(PEC)のようにQPU上でサンプリングすることで逆ノイズ・チャネルを実装する代わりに、パウリ伝播を使用して測定されたオブザーバブルに古典的に適用します。結果として得られるオブザーバブルは、測定されると学習されたゲート・ノイズを効果的に緩和します。

SamplomaticとExecutorによるモジュール式エラー緩和
PNAのアプローチは、Qiskitのモジュール式エラー緩和アーキテクチャの上に構築されています。このアーキテクチャは、samplomaticライブラリをQuantumProgramクラスおよびExecutorクラス(qiskit-ibm-runtime v0.47.0でQiskit Runtimeに追加)とともに使用して、伝播ノイズ吸収やパウリ・トワリングなどの手法を、さまざまな実験にわたって組み合わせ可能かつ再利用可能にします。
緩和ロジックを回路定義自体の中に埋め込むのではなく、緩和は samplomatic アノテーションを通じて宣言的に表現され、ランダム化された回路がどのように生成、実行、後処理されるかを制御する Executor を通じてプログラム的に管理されます。
このチュートリアルでは、Qiskitパターンを実装して、PNAが逆パウリ・ノイズ・チャネルをどのように伝播させ、それに応じてオブザーバブルを変更することで、ノイズのあるQPU上での期待値の推定を改善できるかを示します。
ワークフローの概要
-
ステップ1: 量子問題へのマッピング
- ミラーリングされたトロッター化キック・イジング・モデルとターゲット・オブザーバブルを構築します。
-
ステップ2: ノイズの特性評価と伝播
-
samplomaticを使用して、回路内の一意な2量子ビット層と測定を特定し、アノテーションを付けます。 -
NoiseLearnerV3を使用して、各一意な層に影響するノイズを学習します。 -
各
InjectNoiseアノテーションを対応する学習済みノイズ・モデルにマッピングします。 -
qiskit-addon-pnaを使用して、逆ノイズ・チャネルを回路を通して前方に伝播させ、ターゲット・オブザーバブルに吸収させます。
-
-
ステップ3: 量子実験の実行
samplex経由でランダム化サンプリングを指定するためにQuantumProgramを定義し、Executorを使用してバックエンド上で実験を実行します。
-
ステップ4: 結果の再構成と分析
- 緩和戦略(PNA、PNA+TREX、PNA+PS、PNA+PS+TREX)を比較し、緩和なしの結果に対する改善を可視化します。
要件
このチュートリアルを開始する前に、以下がインストールされていることを確認してください。
-
可視化サポート付きのQiskit SDK v2.2以降
-
Qiskit Runtime v0.47以降(
pip install qiskit-ibm-runtime) -
Samplomatic v0.13以降(
pip install samplomatic) -
PNA Qiskitアドオン(
pip install qiskit-addon-pna) -
Qiskitアドオン・ユーティリティ(
pip install qiskit-addon-utils)
セットアップ
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_pna": "qiskit-addon-pna", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "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.quantum_info import Pauli, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager, PassManager
from qiskit_ibm_runtime import (
QiskitRuntimeService,
QuantumProgram,
Executor,
NoiseLearnerV3,
)
from qiskit_addon_utils.exp_vals.measurement_bases import (
get_measurement_bases,
)
from qiskit_addon_utils.exp_vals.expectation_values import (
executor_expectation_values,
)
from qiskit_addon_utils.noise_management import trex_factors
from qiskit_addon_utils.noise_management.post_selection import PostSelector
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
AddPostSelectionMeasures,
AddSpectatorMeasures,
)
from qiskit_addon_pna import generate_noise_mitigating_observable
import samplomatic
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation, find_unique_box_instructions
import numpy as np
import matplotlib.pyplot as plt
# Selects a connected chain of low-error qubits on the target backend.
# The mirrored kicked-Ising circuit is a 1D chain, so we only need a line
# of connected physical qubits; the helper walks the backend's coupling map
# and grows a chain along the lowest-error two-qubit edges, so it works for
# any backend rather than relying on a hardcoded layout.
def find_qubit_chain(backend, length):
"""Find a connected chain of ``length`` physical qubits on ``backend``.
The chain is grown greedily along the lowest-error two-qubit edges, so it
favors better-performing qubits. Because the mirrored kicked-Ising circuit
is a 1D chain, a connected line is all we need.
"""
target = backend.target
# Identify the native two-qubit gate and build a per-edge error lookup.
two_qubit_gate = next(
name
for name in target.operation_names
if target[name]
and all(q is not None and len(q) == 2 for q in target[name])
)
edge_error = {
frozenset(qargs): (
1.0 if props is None or props.error is None else props.error
)
for qargs, props in target[two_qubit_gate].items()
}
graph = backend.coupling_map.graph.to_undirected(multigraph=False)
neighbors = {n: list(graph.neighbors(n)) for n in graph.node_indices()}
def first_chain_from(start):
path, visited = [start], {start}
def grow():
if len(path) == length:
return True
node = path[-1]
order = sorted(
neighbors[node],
key=lambda m: edge_error.get(frozenset((node, m)), 1.0),
)
for nxt in order:
if nxt not in visited:
visited.add(nxt)
path.append(nxt)
if grow():
return True
path.pop()
visited.remove(nxt)
return False
return path if grow() else None
def chain_cost(path):
return sum(
edge_error.get(frozenset((path[i], path[i + 1])), 1.0)
for i in range(len(path) - 1)
)
# Try low-degree qubits first (the natural ends of long chains) and keep
# the lowest-error chain found.
best_path, best_cost = None, float("inf")
for start in sorted(neighbors, key=lambda n: len(neighbors[n])):
chain = first_chain_from(start)
if chain is not None and (cost := chain_cost(chain)) < best_cost:
best_path, best_cost = chain, cost
if best_path is None:
raise ValueError(
f"Could not find a connected chain of {length} qubits "
f"on '{backend.name}'."
)
return best_path
小規模シミュレーターの例
PNAは特定の量子プロセッサーの物理的な2量子ビット・ゲート・ノイズを緩和します。このワークフローは、理想的なシミュレーターでは意味のある類似物を持たない2つのハードウェア・サービスに依存しています。
-
NoiseLearnerV3は、トランスパイルされた回路の各一意な2量子ビット層に付随するスパース・パウリ・リンドブラッド・ノイズ・チャネルを実験的に特性評価します。ノイズのないシミュレーターでは学習すべきノイズが存在せず、伝播される逆チャネルは恒等演算子になります。 -
Executorプリミティブは、samplomaticによって生成されたトワリングされたランダム化回路をバックエンド上でサンプリングします。
原理的には、合成ノイズ・モデルを代用することもできます。例えば、Qiskit Aerを使用してPauliLindbladError命令を付加し、結果として得られるノイズのある回路を直接generate_noise_mitigating_observableに渡すことができます。しかし、これは自分自身で注入したノイズに対する古典的な記録処理を検証するだけであり、この技術の要点を曖昧にしてしまいます。そのため、シミュレーターの例は省略し、Qiskitパターンの各ステップを以下に分解して、PNAのワークフロー全体をハードウェア上で直接示します。
大規模ハードウェアの例
ここでは、Qiskitパターンの4つのステップに従って、IBM Quantum®ハードウェア上で実行される30サイトのキック・イジング・モデルに対して、PNAのワークフロー全体を実行します。
ステップ1: 量子問題へのマッピング
ミラーリングされたトロッター回路とオブザーバブルの生成
この実験では、1次元スピン鎖上の30サイトのキック・イジング・モデルの時間発展を調べます。考慮するハミルトニアンは次のとおりです。
,
ここで、は最近接スピン()の結合を表し、大域的な横磁場はに設定されます。がクリフォード角(つまり)から離れるほど、アンチノイズ生成子を回路を通して伝播させることが難しくなります。
オブザーバブルの選択については、平均的な単一サイト磁化を考えます。ここで、はサイト数です。
num_qubits = 30
num_trotter_steps = 10
rx_angle = np.pi / 8
# Avg single-site magnetization
id_pauli = Pauli("I" * num_qubits)
observable = (
SparsePauliOp([id_pauli.dot(Pauli("Z"), [i]) for i in range(num_qubits)])
/ num_qubits
)
# Implement Trotterized kicked-Ising model
circuit = QuantumCircuit(num_qubits)
for _step in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
for first_qubit in (1, 2):
for idx in range(first_qubit, num_qubits, 2):
# equivalent to Rzz(-pi/2):
circuit.sdg([idx - 1, idx])
circuit.cz(idx - 1, idx)
# Append the inverse circuit to complete the mirroring
circuit.compose(circuit.inverse(), inplace=True)
circuit.measure_active()
circuit.draw("mpl", fold=-1)

ステップ2: ハードウェア実行のための問題の最適化
次のステップは、実際のIBM Quantumハードウェア上での実行のために、ミラーリングされたトロッター回路を準備することです。QPU上で実行するには、単に抽象回路を構築するだけでは不十分であり、以下を満たすように最適化する必要があります。
-
バックエンドのネイティブ・ゲート・セットと接続性を尊重すること。 トランスパイルは、論理回路をターゲット・バックエンドがサポートするISA回路にマッピングします。これにより、すべてのゲートと量子ビットの相互作用が物理的に実現可能であることが保証されます。
-
回路層のレベルでノイズを特性評価できること。 PNAは、逆ノイズ・チャネルの学習と伝播に依存しています。これを効率的に行うために、トランスパイルされた回路を一意な2量子ビットの「ボックス化」層に分割します。これらのボックスにより、回路の各層をそれぞれの学習済みノイズ・モデルに関連付けることができます。
-
現実的なノイズ・モデルをPNAに供給できること。 回路がボックス化されたら、
NoiseLearnerV3サービスを使用して、各一意な2量子ビット層に影響するパウリ・ノイズ・チャネルを実験的に学習します。これらの学習済みモデルは、Samplomaticのアノテーションを通じて回路に結び付けられます。
全体として、このステップは理想化されたミラーリング回路と学習済みノイズ・モデルを持つハードウェア対応回路との間のギャップを埋めます。この設定により、PNAは回路を通して逆ノイズ・チャネルを伝播させ、それに応じてオブザーバブルを調整できます。
バックエンドへの接続とISA回路へのトランスパイル
まず、IBM Quantum Compute Serviceを初期化し、バックエンドを選択します。認証情報を保存する手順に従って自分のアカウントで認証すると、その後はQiskitRuntimeService()が自動的にそれらを取得します。
# Initialize IBM Quantum Compute Service using your saved credentials
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=num_qubits
)
# Re-fetch with fractional gates enabled (least_busy does not forward this)
# Fractional gates are enabled so the non-Clifford Rx rotations are supported natively.
backend = service.backend(backend.name, use_fractional_gates=True)
print(f"Selected backend: {backend.name}")
Selected backend: ibm_fez
次に、バックエンド上で接続された量子ビットの鎖を選択し、回路をその上にトランスパイルします。
セットアップ・セクションで定義されたfind_qubit_chainヘルパーを使用して、num_qubits個の物理量子ビットが接続された1本の鎖(直線)を選択します。次に、optimization_level=0でトランスパイルし、この鎖をinitial_layoutとして固定します。これにより、ミラーリングされた回路の2量子ビット・ゲート層構造が正確に保持されます。この構造は、ボックス化とノイズ学習のステップが依存するものであるため、(ミラーリングされたゲートを打ち消してしまう)より高い最適化レベルはここでは避ける必要があります。
# Find a connected, low-error chain of qubits on the chosen backend
layout = find_qubit_chain(backend, num_qubits)
# Transpile the circuit for the target backend, pinning the chain as the layout.
# optimization_level=0 preserves the mirrored two-qubit-gate layers that the
# boxing and noise-learning steps rely on.
pm = generate_preset_pass_manager(
backend=backend, optimization_level=0, initial_layout=layout
)
isa_circuit = pm.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_circuit.draw("mpl", fold=-1)
2量子ビット・ゲート層と測定をトワリングし、一意な層を見つける
samplomaticを使用して回路をボックス化し、一意な2量子ビット層を特定します。ボックスとは、特定の意図やアノテーションを後で同じボックス内のすべてのゲートに一様に適用できるように命令をグループ化する構成要素です。
ここでは、単に2量子ビット層を特定するだけではないメソッドgenerate_boxing_pass_managerを呼び出します。これはいくつかの重要なタスクを実行します。
-
回路内のすべての2量子ビット層をグループ化する、
-
それらの層に
TwirlアノテーションとChangeBasisアノテーションを適用する、 -
測定演算を独自のボックス化されたセクションにグループ化する、そして
-
各2量子ビット層に
InjectNoiseアノテーションを適用する。
これらのアノテーションは、回路全体でノイズ、基底の変更、およびトワリングがどのように処理されるかを定義します。また、後でノイズを学習して緩和する際に使用される構造も確立します。
主要な設定オプションは次のとおりです。
-
enable_gates/enable_measures: True: すべての2量子ビット・ゲート層と終端測定をボックス化します。単一量子ビット・ゲートはボックス内の左側(先頭)にまとめられます(left-dressed)。 -
measure_annotations: all: 測定ボックスにTwirlアノテーションとChangeBasisアノテーションを含めます。 -
twirling_strategy: active: エンタングリング・ゲートを含む各ボックス内のすべてのアクティブな量子ビットをトワリングします。 -
inject_noise_targets: gates: エンタングリング・ゲートを含むすべてのTwirlアノテーション付きボックスにInjectNoiseアノテーションを追加します。 -
inject_noise_strategy: uniform_modification: 回路全体ですべてのノイズ層を均等にスケーリングします。
# Box up circuit with Twirl and InjectNoise annotations
pm = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
twirling_strategy="active",
inject_noise_targets="gates",
inject_noise_strategy="uniform_modification",
)
boxed_circuit = pm.run(isa_circuit)
draw_circ = QuantumCircuit(boxed_circuit.num_qubits)
draw_circ.append(boxed_circuit.data[0], qargs=boxed_circuit.data[0].qubits)
draw_circ.append(boxed_circuit.data[1], qargs=boxed_circuit.data[1].qubits)
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)
回路のサンプリング方法を定義するテンプレート回路とsamplexを生成します。
ここでは、Executorから出力されたサンプルに対して事後選択を行うために必要な、スペクテーターおよび事後選択の測定も追加します。
# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)
# Add post-selection instructions to the template circuit
post_selection_pm = PassManager(
[
AddSpectatorMeasures(backend.coupling_map),
AddPostSelectionMeasures(x_pulse_type="rx"),
]
)
template_circuit = post_selection_pm.run(template_circuit)
draw_circ = template_circuit.copy_empty_like()
draw_circ.data = template_circuit.data[:324]
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

NoiseLearnerV3を使用してノイズを学習する
PNAをエラー緩和に適用する前に、まず回路内の各一意な2量子ビット層と測定層に作用するノイズを特性評価する必要があります。これを行うために、NoiseLearnerV3プログラムを使用して、先に特定した各層のノイズ・モデルを実験的に学習します。この学習器は、各層に影響するノイズ・チャネルを推定するベンチマーク形式の実験を実行し、学習済みモデルを含む結果オブジェクトを返します。
まず、samplomaticのfind_unique_box_instructionsを使用して、回路内の一意な層を特定することから始めます。これにより、異なる層タイプごとに一度だけノイズを学習し、実験の数と総ショット・コストを最小化できます。得られた層のリストがノイズ学習器に渡されます。
ノイズの学習方法を制御するいくつかの重要なパラメーターがあります。
-
num_randomizations: 各学習構成に使用されるランダム回路の数。 -
shots_per_randomization: 各ランダム学習回路に対して取得されるショット数。 -
layer_pair_depths: 学習実験で使用される回路深さ(ペア数で測定)。 -
post_selection: 測定後パルスを適用するためにrxゲートを使用したエッジベースの事後選択を有効にします。
# Noise learning parameters
num_randomizations_nl = 64
shots_per_randomization_nl = 128
# Match the deepest noise-learning depth to the depth of the circuit being
# mitigated. ``layer_pair_depths`` are measured in layer pairs (a layer + its
# inverse), so the deepest value is half the circuits's two-qubit-layer depth.
# Learning to this depth improves the quality of the mitigation.
depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)
max_layer_pair_depth = depth_2q // 2
# Use a fixed schedule of learning depths, but drop any that exceed the circuit's
# depth and always cap the deepest value at ``max_layer_pair_depth`` so we never
# learn deeper than the circuit being mitigated
candidate_depths = [1, 2, 4, 8, 12, 16, 24, 32, 40, 48]
layer_pair_depths = sorted(
{d for d in candidate_depths if d < max_layer_pair_depth}
| {max_layer_pair_depth}
)
# Find the unique instructions (layers) from the boxed-up circuit
unique_2q_layers_and_meas = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
# Configure and run the noise learner on the unique layers.
# Options can be passed directly as a dictionary.
noise_learner_options = {
"num_randomizations": num_randomizations_nl,
"shots_per_randomization": shots_per_randomization_nl,
"layer_pair_depths": layer_pair_depths,
"post_selection": {
"enable": True,
"strategy": "edge",
"x_pulse_type": "rx",
},
"environment": {"job_tags": ["TUT_PNA"]},
}
noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_layers_and_meas)
noise_learner_result = noise_learner_job.result()
学習されたノイズ率を可視化する
ノイズ・モデルを学習した後、1量子ビットおよび2量子ビット演算の両方について、推定された誤り率の分布を調べることができます。以下のコードは、学習されたノイズの結果からパウリ・リンドブラッド表現を抽出し、対応するノイズ率を収集します。
学習された各層について:
- ノイズ・モデルを
(pstr, qubits, rate)タプルのスパース・リストに変換します。ここで、pstrは与えられた量子ビットに作用するパウリ文字列であり、rateは関連する誤り率です。 - 率を1量子ビット項(
len(pstr) == 1)と2量子ビット項(len(pstr) == 2)に分離します。 - 率のリストをソートし、その中央値を計算します。
学習されたパウリ・リンドブラッド生成子の相対的な大きさを比較できるように、1量子ビット(赤)と2量子ビット(青)のノイズ率分布を対数スケールでプロットし、その中央値を垂直線でマークします。1量子ビット率と2量子ビット率の順序はデバイスや特性評価される特定の層に依存します。今回の実行では、単一量子ビット(重み1)の生成子がより大きな中央値を持ちました。
hw_rates_1q = []
hw_rates_2q = []
for nlr in noise_learner_result[:2]:
plm_list = nlr.to_pauli_lindblad_map().to_sparse_list()
hw_rates_1q += [
rate for (pstr, qubits, rate) in plm_list if len(pstr) == 1
]
hw_rates_2q += [
rate for (pstr, qubits, rate) in plm_list if len(pstr) == 2
]
hw_rates_1q = sorted(hw_rates_1q)
hw_rates_2q = sorted(hw_rates_2q)
median_1q = hw_rates_1q[len(hw_rates_1q) // 2]
median_2q = hw_rates_2q[len(hw_rates_2q) // 2]
fig, ax = plt.subplots(1, 1, figsize=(14, 5))
ax.scatter(
(hw_rates_1q),
[(i) / (len(hw_rates_1q) - 1) for i in range(len(hw_rates_1q))],
color="red",
label="1q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_1q, 0, 1, color="red")
ax.text(median_1q * 1.1, 0.1, f"{median_1q:.2e}")
ax.scatter(
(hw_rates_2q),
[(i) / (len(hw_rates_2q) - 1) for i in range(len(hw_rates_2q))],
color="blue",
label="2q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_2q, 0, 1, color="blue")
ax.text(median_2q * 1.1, 0.2, f"{median_2q:.2e}")
ax.set_title("Learned noise rates")
ax.set_xlabel("Noise rate")
ax.set_yticks([])
plt.legend()
<matplotlib.legend.Legend at 0x125336120>
回路ボックスを学習済みノイズと関連付ける
各一意な2量子ビット層のノイズ・モデルを取得したら、それらをボックス化された回路内の対応するInjectNoiseアノテーションにリンクする必要があります。
InjectNoiseディレクティブは、単一量子ビットのドレッサーを使用して、制御可能かつ設定可能な方法で回路にノイズを注入するsamplomaticアノテーションです。これにより、異なる層にわたるモジュール式のノイズ・モデリングが可能になります。
各InjectNoiseアノテーションには以下が含まれます。
InjectNoise.ref- アノテーションの一意な識別子。これは、samplexオブジェクトが対応するノイズ・モデルを正しく割り当てるために使用されます。InjectNoise.modifier_ref(オプション) - 割り当てられたノイズ・モデルを乗法的な係数でスケーリングできるようにする二次的な参照。
このステップでは、各InjectNoise.refから対応する学習済みノイズ・モデル(PauliLindbladMap)へのマッピングを構築します。このマッピングにより、回路内の各エンタングリング・ゲート層が適切なノイズ・モデルと対になることが保証され、サンプリングおよびその後のノイズ緩和ステップの間にノイズの影響が正確に適用されます。
# map inject noise refs to pauli lindblad maps
refs_to_noise_models = {}
for instruction, result in zip(
unique_2q_layers_and_meas, noise_learner_result, strict=False
):
if inject_noise_annot := get_annotation(
instruction.operation, InjectNoise
):
refs_to_noise_models[inject_noise_annot.ref] = (
result.to_pauli_lindblad_map()
)
学習されたアンチノイズを通してオブザーバブルを伝播させる
上で議論したように、これは2つのステップで行われます。まず、アンチノイズ生成子を回路の末尾まで伝播させます。その後、その進化した生成子を通してオブザーバブルを伝播させます。このプロセスは、回路内の各アンチノイズ生成子について繰り返されます。この実装では、特定の層内の各生成子は並列に回路の末尾まで伝播されます。さらに、Pythonのマルチプロセッシングを使用して、アンチノイズの前方伝播とオブザーバブルの後方伝播の両方を並列に実行します。これにより、進化した生成子がメモリ内に蓄積されるのを防ぎ、計算リソースを最大限に活用します。
PNAを実行する際には、常にノイズのある回路とオブザーバブルを提供する必要があります。ノイズのある回路がInjectNoiseアノテーションを持つボックス化回路である場合は、上記のステップで作成したマッピングを提供する必要があります。qiskit-aerのPauliLindbladError命令を含む非ボックス化回路を渡すこともできます。その場合、refs_to_noise_modelsを提供する必要はありません。主要な入力に加えて、以下も考慮してください。
-
max_err_terms: アンチノイズ生成子が前方伝播される際に、各生成子に保持する項の数。これを大きくすると一般に精度が向上しますが、この挙動は単調であることが保証されているわけではありません。 -
max_obs_terms: ノイズ緩和オブザーバブルが進化したアンチノイズを通して後方伝播される際に保持する項の数。値を大きくすると一般に精度が向上しますが、単調に向上することは保証されていません。 -
num_processes: プロセスに割り当てるコアの数。生成子は並列に前方伝播され、オブザーバブルに適用されることを思い出してください。 -
search_step: 後方伝播ステップは、パウリ基底で2つの演算子を近似的に共役させるために貪欲法を使用します。この方法はsearch_stepを増やすことで高速化できます。詳細については、pauli-propのドキュメントを参照してください。 -
num_to_measure: この変数はgenerate_noise_mitigating_observableへの入力ではありませんが、から実際に測定したい項の数を制御するために使用します。ここでは、元のオブザーバブルにある項である上位30項のみを測定します。これらの項は、測定すると学習されたゲート・ノイズを緩和する効果があるように再スケーリングされています。から30項しか測定しませんが、それを大きく成長させることは、先頭項のスケーリング係数の精度を高めるため、しばしば有用です。
# PNA parameters
num_processes = 8
max_err_terms = 10_000
max_obs_terms = 10_000
num_to_measure = num_qubits
obs_tilde_isa = generate_noise_mitigating_observable(
boxed_circuit,
isa_observable,
refs_to_noise_models,
max_err_terms=max_err_terms,
max_obs_terms=max_obs_terms,
num_processes=num_processes,
print_progress=True,
search_step=8,
)
p_2_v = {p: v for v, p in enumerate(layout)}
obs_tilde_virtual = SparsePauliOp.from_sparse_list(
[
(pstr, [p_2_v[p] for p in p_qubits], coeff)
for (pstr, p_qubits, coeff) in obs_tilde_isa.to_sparse_list()
],
num_qubits=num_qubits,
)
obs_tilde_virtual = obs_tilde_virtual[
np.argsort(np.abs(obs_tilde_virtual.coeffs))[::-1]
][:num_to_measure]
Finished! 13740 / 13740 generators propagated.
obs_tilde_isa = obs_tilde_isa[np.argsort(np.abs(obs_tilde_isa.coeffs))][::-1]
plt.xscale("log")
plt.yscale("log")
plt.title(r"$\tilde{O}$ coeff magnitudes")
plt.ylabel("Magnitude")
plt.xlabel("Pauli term index")
plt.plot(np.abs(obs_tilde_isa.coeffs), ".")
[<matplotlib.lines.Line2D at 0x124b0a120>]
測定基底を標準形に変換する
次に、測定されたオブザーバブル内のすべてのパウリ項を完全にカバーできるような、測定すべき最小限の基底セットを見つけます(量子ビット単位で可換な場合、多くのオブザーバブルを同時に測定できます)。元のオブザーバブルの項のみを測定しており、これはすべての単一Zパウリの和であるため、必要な基底は1つ、つまり全Z基底のみです。
パウリ測定基底のセットを見つけることに加えて、これらのパウリ項をExecutorが期待する標準形にマッピングする必要があります。標準的な量子ビットの順序付けについての詳細は、Samplomaticドキュメントを参照してください。
meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx
for idx, qubit in enumerate(boxed_circuit.qubits)
if qubit in meas_box.qubits
]
c_2_p = {
c: p for c, p in enumerate(canonical_qubits)
} # canonical -> physical
p_2_v = {p: v for v, p in enumerate(layout)} # physical -> virtual
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()} # canonical -> virtual
meas_bases, bases_reverser = get_measurement_bases(obs_tilde_virtual)
meas_bases_canonical = [
np.array([base[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)
for base in meas_bases
]
ステップ3: 量子実験の実行
QuantumProgramでのサンプリング方法の指定
ここで、Executorによって実行されるすべての回路とサンプリング設定の中心的なコンテナとして機能するQuantumProgramをセットアップします。このオブジェクトは、PNAで使用される測定結果を生成するために、ランダム化された回路インスタンスがどのように生成、バッチ処理、実行されるかを定義します。
QuantumProgramには、テンプレート回路と、ランダム化がどのように適用されるかを定義する対応するsamplexオブジェクトからなる複数の項目を含めることができます。この抽象化により、Executorはワークフロー全体を、ランダム化回路の生成からショットの収集と集約に至るまで、単一のモジュール式プログラムとして扱うことができます。
このステップでは、先に構築したテンプレート回路とsamplexを使用してPNA実験を実行するQuantumProgramを作成します。この設定には以下の要素が含まれます。
-
template_circuit: 望ましいすべてのランダム化(トワリングのランダム化、パラメーターなど)を実装するために必要なすべてのゲートを含む回路。 -
samplex: サンプリング元となる、あり得るすべての回路ランダム化に対する確率分布を定義するオブジェクト。 -
samplex_arguments:samplexを完全に定義するために必要なバインディング-
basis_changes: ここで、測定されたオブザーバブル内のすべてのパウリ項をカバーする、測定する基底のセットを指定します。 -
noise_scales.ref: サンプルに追加のノイズが注入されるのを防ぐために、各ノイズ層のスケールを0.0に設定します。 -
pauli_lindblad_maps:noise_scalesが渡される場合に必要です。これは単に、ノイズ層を関連するノイズ・モデルにマッピングするものです。
-
-
shape:samplex_argumentsによって定義される暗黙の形状を拡張するための形状タプル。この拡張によって導入される非自明な軸は、ランダム化を列挙します。
# Control the # of shots during execution
shots_per_randomization_exec = 64
num_randomizations_exec = 6144
# Zero out the noise to prevent noise from being injected during execution.
# We only added InjectNoise annotations so PNA could associate the noise
# to layers in the circuit
samplex_inputs = {f"noise_scales.{ref}": 0.0 for ref in refs_to_noise_models}
samplex_inputs |= {"pauli_lindblad_maps": refs_to_noise_models}
# Specify the bases to measure. The samplex exposes one basis-change input per
# ChangeBasis-annotated box; here a single all-Z basis covers every term. We
# look up the basis-change interface name rather than hardcoding an index, since
# the name depends on the circuit's box structure.
bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)
samplex_inputs |= {
spec.name: bases_broadcastable
for spec in samplex.inputs().get_specs(r"^basis_changes\.")
}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().make_broadcastable().bind(**samplex_inputs)
)
# Instantiate the QuantumProgram with the specified parameters
program = QuantumProgram(shots=shots_per_randomization_exec)
program.append_samplex_item(
circuit=template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations_exec,),
)
Executorを使用して回路をサンプリングする
QuantumProgramを定義したので、実験を実行するのは簡単です。単にExecutorオブジェクトをインスタンス化し、バックエンドを渡してプログラムを実行するだけです。
# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()
ステップ4: 結果の再構成と分析
エラー緩和された期待値を計算するために、以下を行います。
-
測定に影響する学習済みノイズに基づいてTREXスケーリング係数を計算する、
-
事後選択されたサンプルのみを保持するためのマスクを生成する、そして
-
qiskit-addon-utilsのexecutor_expectation_values関数を使用して、すべてのデータをエラー緩和された期待値に結合する。
# Computing the TREX factors
measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_rescale_factors = trex_factors(measurement_noise_map, bases_reverser)
# Post-select the results
post_selector = PostSelector.from_circuit(
circuit=template_circuit, coupling_map=backend.coupling_map
)
# Compute the ps mask for filtering results
mask = post_selector.compute_mask(exec_results[0], strategy="edge")
# Compute expvals using post selected results
results = executor_expectation_values(
exec_results[0]["meas"],
bases_reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=mask,
rescale_factors=trex_rescale_factors,
)
緩和戦略の比較: PNA、PNA+TREX、PNA+PS、PNA+PS+TREX
Executorの結果を使用して、いくつかの緩和バリアントの期待値を計算し可視化します。
bases_reverser_unmit = {Pauli("Z" * num_qubits): [observable]}
args = [
(bases_reverser_unmit, None, None),
(bases_reverser, None, None),
(bases_reverser, None, trex_rescale_factors),
(bases_reverser, mask, None),
(bases_reverser, mask, trex_rescale_factors),
]
evs = []
for reverser, postsel_mask, factors in args:
# Compute expvals using post selected results
res_ps = executor_expectation_values(
exec_results[0]["meas"],
reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=postsel_mask,
rescale_factors=factors,
)
res_ps = np.array(res_ps)
evs.append(res_ps[:, 0][0])
experiments = ["PNA", "PNA+TREX", "PNA+PS", "PNA+PS+TREX"]
colors = ["#d9d9d9", "#b0b0b0", "#7f7f7f", "#4c4c4c"]
plt.bar(experiments, evs[1:], color=colors)
plt.axhline(y=1, color="green", linestyle="--", linewidth=2, label="Ideal")
plt.axhline(
y=evs[0], color="red", linestyle="--", linewidth=2, label="Unmitigated"
)
plt.ylabel("Expectation value", fontsize=14)
plt.title(
r"30q Mirrored Ising, 10 Trotter steps, $\theta_{rx}=\frac{\pi}{8}$",
fontsize=14,
)
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1), borderaxespad=0.0)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
結果は、異なるエラー緩和手法を組み合わせることの累積的な利点を示しています。単純なPNAアプローチだけでも、期待値は理想的なベンチマークに近い値まで回復しており、逆ノイズ・チャネルをオブザーバブルに伝播させることが2量子ビット・ゲートの誤りを効果的に補償していることを示しています。
-
TREX再重み付け(PNA+TREX)を追加すると、ランダム化された回路におけるサンプリングの不均衡を補正することで、推定値がわずかに改善します。
-
事後選択(PNA+PS)は、残留誤りに起因する可能性の高い不整合な測定結果を除外することで、より顕著な向上をもたらします。
-
最後に、両方(PNA+PS+TREX)を組み合わせると、理想値にほぼ一致する最も正確な結果が得られ、これらのエラー緩和戦略が互いに補完し合うことが示されます。
全体として、この比較は、PNAがノイズを考慮した期待値推定のための堅牢な基盤として機能する一方で、TREXと事後選択がさらなる精度向上のための補完的な改良を提供することを浮き彫りにしています。
次のステップ
この内容に興味があれば、以下の資料も参考になるでしょう。
