PEC ร่วมกับ TREX
Probabilistic error cancellation (PEC) เป็นเทคนิคลดสัญญาณรบกวนควอนตัมในการคำนวณค่าคาดหมายของ observable ช่วยให้คุณคำนวณค่าประมาณของค่าคาดหมายที่ไม่เอนเอียงได้ เทคนิคนี้ทำงานโดยรันชุดของ circuit ที่มีสัญญาณรบกวนซึ่งถูกฉีดด้วยอินเวอร์สของโมเดลสัญญาณรบกวนที่เรียนรู้ได้ จากนั้นโมเดลสัญญาณรบกวนจะถูกประมาณเป็นช่องสัญญาณ Pauli ด้วยการทำ Pauli twirling (หรือเรียกว่า randomized compiling) โมเดลสัญญาณรบกวนแบบนี้เรียนรู้ได้อย่างมีประสิทธิภาพ และลดสัญญาณรบกวนได้ด้วย PEC (ดูรายละเอียดเพิ่มเติมใน Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors)
Twirled Readout Error eXtinction (TREX) เป็นเทคนิคที่ลดความผิดพลาดในการวัด และมีประโยชน์ในการประมาณค่าคาดหมายของ observable แบบ Pauli โดยอาศัยการทำ measurement twirling (การฉีด gate X แบบสุ่มก่อนการวัดแล้วพลิกผลลัพธ์ที่วัดได้) ซึ่งทำให้เมทริกซ์การถ่ายโอนความผิดพลาดในการอ่านค่าเป็นแนวทแยง และผกผันได้ง่าย พจน์ปรับสเกลถูกเรียนรู้จากช่องสัญญาณรบกวนแนวทแยงโดยวัด circuit สอบเทียบที่ถูก twirl ซึ่งเริ่มต้นในสถานะศูนย์
คู่มือนี้แสดงวิธีใช้ PEC เพื่อลดสัญญาณรบกวนของ gate และรวมกับ TREX เพื่อลดสัญญาณรบกวนจากการวัดด้วย เมื่อประมาณค่าคาดหมายของ observable
ขั้นตอนการทำงานมีดังนี้:
- กำหนด circuit และ observable
- คำนวณค่าคาดหมายที่แน่นอนด้วยซิมูเลเตอร์ statevector ของ Qiskit
- คำนวณค่าคาดหมายที่มีสัญญาณรบกวนและไม่ได้ลดสัญญาณรบกวน
- เรียนรู้สัญญาณรบกวนของเลเยอร์ที่สร้างพันกันด้วย qiskit-noise-learning
- เตรียมโปรแกรม PEC + TREX และเพิ่ม circuit สอบเทียบ TREX เข้าไป
- เปรียบเทียบค่าคาดหมายที่ลดสัญญาณรบกวนแล้ว
กำหนด circuit และ observable
เตรียม mirror circuit ของการวิวัฒน์ตามเวลาแบบ Trotter ของโมเดล Ising สนามตามขวางเชิงเส้นบน qubit แปดตัว ทำ mirror ให้ circuit เพื่อหักล้างยูนิทารีของ circuit และได้ identity circuit ซึ่งช่วยให้คุณคำนวณค่าคาดหมายในอุดมคติของแต่ละ observable ได้ง่าย นอกจากนี้ การทำ mirror ยังเพิ่มความลึกของ circuit เป็นสองเท่า ซึ่งทำให้ผลของสัญญาณรบกวนเพิ่มเป็นสองเท่าด้วย
ใช้มุม เพื่อให้ gate ทั้งหมดใน circuit อยู่ในกลุ่ม Clifford ซึ่งทำให้จำลองได้เร็วขึ้นด้วยซิมูเลเตอร์ stabilizer
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_mitigation": "qiskit-mitigation", "qiskit_noise_learning": "qiskit-noise-learning", "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")
import warnings
import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
warnings.filterwarnings("ignore")
def construct_ising_circuit(
num_qubits: int,
num_trotter_steps: int,
rx_angle: float,
barrier: bool = True,
) -> QuantumCircuit:
circuit = QuantumCircuit(num_qubits)
for _step in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
if barrier:
circuit.barrier()
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)
if barrier:
circuit.barrier()
return circuit
def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:
mirror_circ = circuit.copy_empty_like()
mirror_circ.compose(circuit, inplace=True)
mirror_circ.barrier()
mirror_circ.compose(circuit.inverse(), inplace=True)
mirror_circ.measure_active()
return mirror_circ
# define observables
num_qubits = 8
observables = [
SparsePauliOp("I" * ((num_qubits - 1) - q) + "Z" + "I" * q)
for q in range(num_qubits)
]
# Instantiate circuit
num_trotter_steps = 5
rx_angle = np.pi / 2
circuit = construct_ising_circuit(
num_qubits, num_trotter_steps, rx_angle, barrier=False
)
mirrored_circuit = mirror_circuit(circuit)
print("Visualization of 1 mirrored Trotter step")
mirror_circuit(
construct_ising_circuit(num_qubits, 1, rx_angle, barrier=False)
).draw("mpl", scale=0.7)
Visualization of 1 mirrored Trotter step
คำนวณค่าคาดหมายที่มีสัญญาณรบกวนและไม่ได้ลดสัญญาณรบกวน
QuantumProgram ประกอบด้วย quantum circuit และ samplex ในรูปแบบที่เหมาะกับ primitive Executor MitigationTask.prepare สร้าง QuantumProgram จาก circuit, observable และค่าพารามิเตอร์ และครอบ circuit ด้วยกล่องตามตัวเลือก boxing ที่ระบุ ตั้งตัวเลือก boxing measure_annotations เป็น "change_basis" เพื่อหมุนการวัดไปยังฐานที่ต้องการ และเพิ่ม add_tags: "unique_box" เพื่อให้ซิมูเลเตอร์จับคู่โมเดลสัญญาณรบกวนกับกล่องของ circuit ได้ โดยค่าเริ่มต้นโปรแกรมจะถูกทำ Pauli twirl ซึ่งบนฮาร์ดแวร์จะเปลี่ยนสัญญาณรบกวนแบบ coherent ให้เป็นสัญญาณรบกวนแบบ Pauli ตามที่วิธีลดสัญญาณรบกวนสมมติไว้ num_randomizations กำหนดจำนวนอินสแตนซ์ที่ถูก twirl ของแต่ละ circuit ที่สุ่มตัวอย่าง และ shots_per_randomization กำหนดจำนวน shot ที่แต่ละอินสแตนซ์ได้รับ ผลคูณของสองค่านี้จึงเป็นขีดจำกัด shot ต่อ circuit
หากคุณเข้าถึง QPU ได้ คุณสามารถรันโปรแกรมด้วย Executor ที่สร้างจาก backend ของ QiskitRuntimeService ตัวอย่างนี้ใช้ AerSimulator เพื่อรันโปรแกรมในเครื่อง ตัวเลือกซิมูเลเตอร์ layer_noise_model ของมันฉีดโมเดลสัญญาณรบกวนแบบ Pauli-Lindblad แบบสุ่มในทุกเลเยอร์ของ gate ที่สร้างพันกัน โดยสร้างด้วย random_pauli_lindblad_noise จาก qiskit-mitigation ที่มีค่าความผิดพลาดต่อ gate ในเลเยอร์ (EPLG) เท่ากับ 0.003 ซึ่งเป็นค่าทั่วไปของ QPU Heron ในปัจจุบัน
import matplotlib.pyplot as plt
from qiskit.circuit import CircuitInstruction
from qiskit.quantum_info import PauliLindbladMap, pauli_basis
from qiskit.transpiler import PassManager
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import Executor
from qiskit_mitigation import MitigationTask, find_combined_unique_layers
from qiskit_noise_learning.aer_executor.insert_noise_pass import (
InsertNoisePass,
)
from samplomatic import Tag
from samplomatic.utils import get_annotation
def generate_random_pauli_lindblad_noise(
layers,
num_qubits: int,
eplg: float = 3e-3,
readout_noise_scale: float = 3e-2,
seed: int | None = None,
) -> tuple[
list[tuple[CircuitInstruction, PauliLindbladMap]], PauliLindbladMap
]:
"""Generate random Pauli-Lindblad noise over the full Pauli basis."""
layer_noise_model = []
for layer in layers:
layer_instructions = layer.operation.params[0]
contain_measurement = any(
inst.name.startswith("measure") for inst in layer_instructions
)
if not contain_measurement:
edges = [
(inst.qubits[0]._index, inst.qubits[1]._index)
for inst in layer_instructions
] # extract the edges of the layer
basis_paulis = [p for p in pauli_basis(2) if np.sum(p.x + p.z)]
rng = np.random.default_rng(seed=seed)
num_generators = len(basis_paulis)
rates = rng.random(len(basis_paulis)) * eplg / num_generators
layer_noise = PauliLindbladMap.from_sparse_list(
[
(pauli.to_label(), edge, rate)
for pauli, rate in zip(basis_paulis, rates, strict=True)
for edge in edges
],
num_qubits=num_qubits,
)
layer_noise_model.append((layer, layer_noise))
# generate a readout noise model
rng = np.random.default_rng(seed=seed)
rates = rng.random(num_qubits) * readout_noise_scale
readout_noise_model = PauliLindbladMap.from_sparse_list(
[("X", [q], rate) for q, rate in enumerate(rates)],
num_qubits=num_qubits,
)
return layer_noise_model, readout_noise_model
def add_readout_noise(program, readout_noise_model):
"""Injects readout noise to the circuits in a quantum program."""
readout_noise_map = {}
for item in program.items:
readout_tag = item.circuit[-1].label[
-4:
] # extract the readout layer tag from the circuit
readout_noise_map[readout_tag] = readout_noise_model
item.circuit = PassManager(
[InsertNoisePass(noise_dict=readout_noise_map, noise_after=False)]
).run(item.circuit)
num_randomizations = 300
shots_per_randomization = 64
boxing_options = {"add_tags": "unique_box"}
# find the unique layers to generate the layered noise for
box_layers = find_combined_unique_layers(
[mirrored_circuit], custom_boxing_options=boxing_options
)
layer_noise_model, readout_noise_model = generate_random_pauli_lindblad_noise(
box_layers, num_qubits, seed=42
)
# a local noisy simulator in place of a QPU backend
noisy_executor = Executor(
AerSimulator(method="stabilizer"),
options={"simulator": {"layer_noise_model": layer_noise_model}},
)
task = MitigationTask()
program = task.prepare(
circuit=mirrored_circuit,
observables=observables,
parameters=None,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
)
# For the simulation - we inject readout noise to the circuits of the program
add_readout_noise(program, readout_noise_model)
program._semantic_role = "simulation" # flag the program for the simulator
unmitigated_res = task.postprocess(noisy_executor.run(program).result())
x_vals = np.array(range(num_qubits))
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(x_vals, [1] * len(x_vals), "k-", alpha=0.5, label="Exact")
ax.errorbar(
x_vals,
unmitigated_res.data.evs,
unmitigated_res.data.stds,
fmt="o",
label="Unmitigated",
)
x_tick_labels = [r"$Z_{ {i} }$" for i in range(num_qubits)]
ax.set_xlabel(r"$\theta/\pi$")
ax.set_ylabel("Expectation value")
ax.legend()
ax.set_xticks(x_vals, x_tick_labels)
plt.tight_layout()
plt.show()
เรียนรู้สัญญาณรบกวนของเลเยอร์ที่สร้างพันกัน
PEC เริ่มต้นด้วยการเรียนรู้โมเดลสัญญาณรบกวนแบบ twirled ของแต่ละเลเยอร์ของ gate ที่สร้างพันกันใน circuit หากคุณสมมติว่าเป็นโมเดลสัญญาณรบกวนแบบ Markovian จะถือว่าความลึกของ circuit ไม่ส่งผลต่อช่องสัญญาณรบกวนของเลเยอร์ที่สร้างพันกันเลเยอร์นั้น ดังนั้นจึงต้องเรียนรู้เฉพาะโมเดลสัญญาณรบกวนของแต่ละเลเยอร์ที่ไม่ซ้ำกันของ gate ที่สร้างพันกัน circuit ที่ใช้ในคู่มือนี้แบ่งได้เป็นสองเลเยอร์ที่ไม่ซ้ำกัน การออกแบบ circuit ให้มีชุดเลเยอร์ที่สร้างพันกันและไม่ซ้ำกันน้อยที่สุดเป็นเรื่องสำคัญหากคุณต้องการเรียนรู้โมเดลสัญญาณรบกวน เพราะต้องเรียนรู้แต่ละเลเยอร์ที่ไม่ซ้ำกันแยกจากกัน หาก circuit มีเลเยอร์ที่ไม่ซ้ำกันมากเกินไป การเรียนรู้สัญญาณรบกวนอาจทำไม่ได้จริงเพราะต้นทุนการสุ่มตัวอย่างในการเรียนรู้หลายเลเยอร์
ใช้ qiskit-noise-learning เพื่อเรียนรู้สัญญาณรบกวน ตัวเรียนรู้จะทำซ้ำแต่ละเลเยอร์ fragment_depths ครั้งใน circuit สำหรับเรียนรู้ ทำ twirl แต่ละ circuit num_randomizations ครั้ง และฟิตการสลายตัวของค่า fidelity ที่วัดได้เข้ากับโมเดล Pauli-Lindblad พารามิเตอร์เหล่านี้อาจส่งผลต่อความแม่นยำของโมเดลที่เรียนรู้ได้ ยิ่งโมเดลสัญญาณรบกวนที่เรียนรู้ได้แม่นยำเท่าไร การหักล้างสัญญาณรบกวนก็ยิ่งแม่นยำเท่านั้น
โค้ดต่อไปนี้วาดโมเดลสัญญาณรบกวนที่เรียนรู้ได้ โดยรวมสองเลเยอร์ไว้ในแผนที่เดียว แต่ละ qubit เป็นแผนภูมิวงกลมของอัตรา X, Y และ Z น้ำหนัก 1 ที่รวมจากสองเลเยอร์ และแต่ละเส้นเชื่อมแสดงอัตราน้ำหนัก 2 ที่เรียนรู้ได้ ตัวอย่างเช่น อัตรา 3e-4 หมายความว่าความผิดพลาดนั้นเกิดขึ้นประมาณสามครั้งในทุก 10,000 ครั้งที่ใช้เลเยอร์
การลดสัญญาณรบกวนจากการอ่านค่าทำในขั้นตอนหลังประมวลผลด้วย TREX ต่างจากโมเดลสัญญาณรบกวนของ gate ซึ่งจำเป็นต่อการเตรียมโปรแกรมเพื่อฉีดสัญญาณรบกวนตรงข้ามที่สอดคล้องกับโมเดลสัญญาณรบกวนที่เรียนรู้ได้ ดังนั้น สัญญาณรบกวนจากการอ่านค่าจึงรันและเรียนรู้ได้ทั้งตอนเรียนรู้สัญญาณรบกวนของ gate หรือตอนรันโปรแกรมที่ลดสัญญาณรบกวนแล้ว การเรียนรู้สัญญาณรบกวนจากการอ่านค่าไปพร้อมกับสัญญาณรบกวนของ gate อาจเป็นประโยชน์ต่อการเรียนรู้โมเดลสัญญาณรบกวนที่สอดคล้องกันในตัวเอง (ดูตัวอย่างได้ที่ Disambiguating Pauli noise in quantum computers) ในกรณีที่สัญญาณรบกวนของ gate และสัญญาณรบกวนจากการอ่านค่าแยกจากกันในโมเดลสัญญาณรบกวน แนะนำให้เรียนรู้สัญญาณรบกวนจากการอ่านค่าตอนรันโปรแกรมที่ลดสัญญาณรบกวนแล้ว เพื่อหลีกเลี่ยงการเลื่อนของสัญญาณรบกวนระหว่างตอนที่เรียนรู้กับตอนที่รัน
ในคู่มือนี้ สัญญาณรบกวนจากการอ่านค่าถูกเรียนรู้ระหว่างรันโปรแกรมที่ลดสัญญาณรบกวนแล้ว โดยใช้ circuit สอบเทียบ TREX
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.results import LayerError, PauliLindbladError
from qiskit_ibm_runtime.visualization.embeddings import Embedding
from qiskit_mitigation import PEC
from qiskit_noise_learning.aer_executor import AerExecutor
from qiskit_noise_learning.noise_learner import LearningOptions, NoiseLearner
from samplomatic import InjectNoise
line_coupling = [(i, i + 1) for i in range(num_qubits - 1)]
backend = GenericBackendV2(
num_qubits=num_qubits,
basis_gates=["id", "rz", "sx", "x", "cz"],
coupling_map=line_coupling,
)
task_pec = PEC()
# first learn only the layers PEC will inject noise into, i.e. not the measurement layer
pec_layers = [
layer
for layer in task_pec.find_unique_layers(mirrored_circuit, boxing_options)
if get_annotation(layer.operation, InjectNoise)
]
layers = task_pec.find_unique_layers(mirrored_circuit, boxing_options)
learning_options = LearningOptions(
fragment_depths=[2, 4, 8, 16],
num_randomizations=250,
shots_per_randomizations=16,
)
# qiskit-noise-learning is not compatible with the Executor in local mode,
# and requires the simulator present in the package for running simulation
noise_dict = {
get_annotation(layer.operation, Tag).ref: noise_layer
for layer, noise_layer in layer_noise_model
}
noise_learner_simulator = AerExecutor(
qasm_simulator=AerSimulator(method="stabilizer"), noise_dict=noise_dict
)
learner = NoiseLearner(
backend, options=learning_options, executor=noise_learner_simulator
)
learned_noise = (
learner.run(pec_layers).result().to_dict()
) # keyed by InjectNoise reference
# Draw the learned model on the line, laid out as a line with the qubits in order.
# Composing the two layers' maps sums their rates, giving one map over all the edges.
embedding = Embedding(
[(0, 0), (0, 2), (0, 4), (2, 4), (4, 4), (4, 2), (4, 0), (2, 0)],
line_coupling,
)
first, second = learned_noise.values()
learned_model = first.compose(second).simplify()
error = PauliLindbladError(
PauliList(
learned_model.get_qubit_sparse_pauli_list_copy().to_pauli_list()
),
learned_model.rates,
)
fig = LayerError(circuit, list(range(num_qubits)), error).draw_map(
embedding, radius=0.4, height=460, width=620
)
fig.update_traces(
marker={
"cmin": 0,
"cmax": max(learned_model.rates),
"colorbar": {"tickformat": ".1e", "title": "rate"},
},
selector={"mode": "markers"},
)
fig.show()
เตรียมโปรแกรม PEC และ TREX แบบรวม
ตรงนี้เราเตรียมทั้งงาน TREX และ PEC และเพิ่มงาน TREX เป็นตัวแปรให้งาน PEC ซึ่งทำให้มั่นใจว่าคุณสมบัติที่จำเป็นทั้งหมดสำหรับการลดสัญญาณรบกวนด้วย TREX ถูกเพิ่มเข้าไปในรายการที่ลดสัญญาณรบกวนด้วย PEC (เช่น measurement twirling) สัญญาณรบกวนที่ฉีดจะถูกสุ่มหนึ่งครั้งต่อหนึ่ง randomization ดังนั้นความคลาดเคลื่อนทางสถิติของ PEC จึงถูกควบคุมด้วยจำนวน randomization ไม่ใช่จำนวน shot ต่อ randomization ดังนั้นจึงใช้ randomization จำนวนมาก โดยแต่ละอันมีไม่กี่ shot และคงจำนวน shot ทั้งหมดให้เท่ากับโปรแกรมที่ไม่ได้ลดสัญญาณรบกวน
circuit สอบเทียบ TREX ถูกเพิ่มลงในโปรแกรมเพื่อเรียนรู้สัญญาณรบกวนจากการอ่านค่าระหว่างรันโปรแกรม circuit สอบเทียบ TREX ตื้นมาก ทำให้การเพิ่ม randomization ให้มันมีต้นทุนค่อนข้างถูก และช่วยให้เรียนรู้สัญญาณรบกวนจากการอ่านค่าได้แม่นยำขึ้น
from qiskit_mitigation import TREX
trex = TREX()
program_pec = task_pec.prepare(
circuit=mirrored_circuit,
observables=observables,
parameters=None,
num_randomizations=1200,
shots_per_randomization=16,
custom_boxing_options=boxing_options,
trex=trex,
noise_maps=learned_noise, # PEC option
noise_gain=0, # PEC option
max_sampling_overhead=100, # PEC option
)
program_pec = trex.prepare(
num_randomizations=1500,
quantum_program=program_pec,
custom_boxing_options=boxing_options,
)
# For the simulation - we inject readout noise to the circuits of the program
add_readout_noise(program_pec, readout_noise_model)
ตรวจสอบโอเวอร์เฮดของ shot รันโปรแกรม และประมวลผลหลังการรัน
PEC ให้ค่าประมาณของค่าคาดหมายที่ไม่เอนเอียง โดยแลกกับโอเวอร์เฮดของ shot ที่สูง โอเวอร์เฮดของ shot ที่ต้องใช้ประมาณได้จากการคำนวณ gamma factor ของ circuit ซึ่งสะท้อนผลของโมเดลสัญญาณรบกวนที่เรียนรู้ได้ต่อ circuit โดยค่าเริ่มต้น จำนวน randomization ของงาน PEC จะถูกปรับสเกลตามปริมาณโอเวอร์เฮดของ shot โดยประมาณที่ต้องใช้เพื่อให้ได้ค่าประมาณที่ไม่เอนเอียง (~) หากไม่ต้องการการปรับสเกลอัตโนมัตินี้ คุณสามารถตั้งพารามิเตอร์ scale_randomizations_by_gamma เป็น False โอเวอร์เฮดของ shot ถูกจำกัดด้วยพารามิเตอร์ max_sampling_overhead
คุณควรตรวจดูจำนวน shot ที่ PEC ต้องใช้เพื่อให้ได้ค่าประมาณที่ไม่เอนเอียงก่อนรัน เพื่อตัดสินใจว่าปริมาณนี้รับไหวหรือไม่ เพราะค่านี้เพิ่มแบบเอกซ์โพเนนเชียลตามอัตราสัญญาณรบกวนและความลึกของ circuit
print(f"The circuit calculated gamma is: {task_pec.gamma}")
print(f"The circuit shot overhead is ~{task_pec.gamma**2}")
The circuit calculated gamma is: 1.2774846896089953
The circuit shot overhead is ~1.631967132185391
program_pec._semantic_role = (
"simulation" # flag the program for the simulator
)
job_pec_results = noisy_executor.run(program_pec).result()
# post-processing
pec_res = task_pec.postprocess(job_pec_results)
# In order to post process the results without TREX, we call `postprocess` again, but instead of the default options we
# pass only the PEC item result and the `measure_noise_data` as `None`
pec_without_trex_res = task_pec.postprocess(
job_pec_results[task_pec._program_item_index], measure_noise_data=None
)
เปรียบเทียบค่าคาดหมายที่ลดสัญญาณรบกวนแล้ว
สุดท้าย พล็อตค่าคาดหมายของแต่ละ observable PEC + TREX กู้ค่าคาดหมายที่แน่นอนได้ภายในแถบความคลาดเคลื่อน ขณะที่ผลลัพธ์ที่ไม่ได้ลดสัญญาณรบกวนมีอคติต่ำกว่าค่านั้นมาก
x = np.array(range(num_qubits))
x_tick_labels = [r"$Z_{ {i} }$" for i in range(num_qubits)]
fig, ax = plt.subplots(figsize=(8, 5))
ax.errorbar(
x,
unmitigated_res.data.evs,
unmitigated_res.data.stds,
fmt="o",
label="Twirl only",
)
ax.errorbar(
x,
pec_without_trex_res.data.evs,
pec_without_trex_res.data.stds,
fmt="o",
label="PEC only",
)
ax.errorbar(
x,
pec_res.data.evs,
pec_res.data.stds,
fmt="o",
label="PEC + TREX",
)
ax.plot(x, [1] * len(x), "--", color="black", alpha=0.5, label="Exact")
tick_labels = [f"$Z_{ {i} }$" for i in range(num_qubits)]
ax.set_xticks(x, tick_labels)
ax.set_ylabel("Expectation value")
ax.legend()
ax.set_title("PEC+TREX Mitigated Expectation Values")
plt.tight_layout()
plt.show()