Meningkatkan Nilai Jangkaan: Penyerapan Hingar Terpropagat (PNA)
Dalam tutorial ini, kita akan belajar cara memanfaatkan alat terkini dalam ekosistem Qiskit untuk melaksanakan aliran kerja yang boleh disesuaikan sepenuhnya dengan pengurangan ralat. Kita akan memperkenalkan teknik PNA dan menggunakannya untuk mengurangkan ralat Gate. Kita juga akan menggunakan TREX untuk mengurangkan ralat pembacaan dan pemilihan pasca untuk mengurangkan ralat yang tidak ditangkap dalam model hingar yang dipelajari.
Garis Besar
- Berikan gambaran ringkas tentang
PNA - Cipta Circuit Trotter kuantum dan pemerhatian (observable). Transpail ke Backend dan masukkan ukuran pemilihan pasca.
- Guna
samplomaticuntuk memutar lapisan Gate 2Q dan ukuran. Cari lapisan 2Q unik untuk mengurangkan kos pembelajaran hingar. - Guna
NoiseLearnerV3untuk mempelajari model ralat yang mempengaruhi Gate 2Q dan ukuran. - Guna
qiskit-addon-pnauntuk menjana pemerhatian yang mengurangkan hingar - Guna primitif
qiskit-ibm-runtime.Executoruntuk menjana sampel QPU mentah yang mencerminkan setiap shot bagi setiap rawak putar dan asas yang diukur - Guna
qiskit-addon-utilsuntuk memproses data menjadi nilai jangkaan yang telah dikurangkan hingar.
Apakah penyerapan hingar terpropagat (PNA)?β
Teknik untuk mengurangkan ralat Gate dengan mempropagat pemerhatian melalui saluran hingar songsang yang mempengaruhi Gate 2-qubit, menghasilkan pemerhatian yang mengurangkan hingar.
Gate 2Q dalam eksperimen yang ingin kita jalankan akan dipengaruhi oleh hingar yang ketara.
Jika kita mempelajari model hingar, kita boleh menerapkan songsangannya dan membatalkan hingar tersebut.
Daripada melaksanakan saluran hingar songsang dengan mengambil sampelnya di QPU seperti dalam PEC, kita boleh melaksanakannya secara klasik dalam pemerhatian yang diukur menggunakan perambatan Pauli. Ini menghasilkan pemerhatian yang lebih kompleks yang, apabila diukur, memberi kesan mengurangkan hingar Gate yang dipelajari.

Jana Circuit Trotter yang dicerminkan dan pemerhatianβ
Untuk eksperimen ini, kita akan mengkaji dinamik masa bagi model Ising tertendang 30-tapak pada rantai spin 1D. Hamiltonian yang dipertimbangkan ialah:
,
di mana menerangkan gandingan spin jiran terdekat, , dan medan melintang global, , ditetapkan kepada . Semakin jauh dari sudut Clifford (iaitu ), semakin sukar untuk mempropagat penjana anti-hingar melalui Circuit.
Untuk pilihan pemerhatian, kita akan mempertimbangkan purata magnet tapak tunggal, , di mana ialah bilangan tapak.
# Added by doQumentation β required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-pna qiskit-addon-utils qiskit-ibm-runtime samplomatic
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli, SparsePauliOp
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)
circuit.compose(circuit.inverse(), inplace=True)
circuit.measure_active()
circuit.draw("mpl", fold=-1)

Seterusnya, kita akan memilih rantai Qubit pada ibm_kingston yang melaporkan kadar ralat rendah dan mentranspail Circuit ke Backend.
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
backend_name = "ibm_kingston"
service = QiskitRuntimeService()
backend = service.backend(backend_name, use_fractional_gates=True)
# Use a chain of low-noise qubits
layout = [
44,
45,
46,
47,
57,
67,
68,
69,
78,
89,
88,
87,
97,
107,
106,
105,
117,
125,
126,
127,
128,
129,
118,
109,
110,
111,
98,
91,
92,
93,
]
pm = generate_preset_pass_manager(backend=backend, initial_layout=layout, optimization_level=0)
isa_circuit = pm.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_circuit.draw("mpl", fold=-1)
qiskit_runtime_service._discover_account:WARNING:2025-11-10 14:30:57,148: Loading account with the given token. A saved account will not be used.

Putar lapisan Gate 2-qubit dan ukuran serta cari lapisan unikβ
Di sini kita memastikan pengurus laluan (pass manager) menganotasi kotak dengan anotasi Twirl dan InjectNoise, yang membolehkan kita mempelajari hingar yang akan mempengaruhi Circuit kita dan mengaitkan hingar tersebut dengan lapisan Circuit yang sepadan.
enable_gates/enable_measure: True: Kotakkan semua lapisan Gate 2q dan ukuran terminal. Gate qubit tunggal akan dipakaikan di dalam kotak.measure_annotations: allMasukkan anotasiTwirldanChangeBasispada kotak ukurantwirling_strategy: active: Pusing semua Qubit aktif dalam setiap kotak yang mengandungi Gate penjalininject_noise_targets: gates: AnotasiInjectNoiseperlu ditambah pada semua kotak beranotasiTwirlyang mengandungi Gate penjalininject_noise_strategy: uniform_modification: Semua lapisan hingar perlu diskala secara setara.
from samplomatic.transpiler import generate_boxing_pass_manager
# 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",
remove_barriers=True,
)
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)

Jana Circuit templat dan samplex, tentukan cara Circuit akan disamplingβ
Di sini kita juga menambah ukuran spektator dan pemilihan pasca, yang diperlukan untuk melakukan pemilihan pasca pada sampel output dari Executor.
import samplomatic
from qiskit.transpiler import PassManager
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
AddPostSelectionMeasures,
AddSpectatorMeasures,
)
# 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)

Pelajari hingarβ
Sebelum kita menjalankan eksperimen, kita mempelajari model hingar yang mempengaruhi Gate penjalin dan ukuran dalam Circuit. Mempunyai model hingar yang tepat adalah perlu untuk mengurangkan ralat dengan berkesan. Mempelajari hingar sejurus sebelum melaksanakan eksperimen memberi peluang terbaik agar model hingar menggambarkan hingar sebenar yang mempengaruhi Gate semasa pelaksanaan.
Sebelum kita mempelajari hingar, kita perlu mencari lapisan 2-qubit unik dalam Circuit kita, supaya kita boleh meminimumkan bilangan shot yang diperlukan untuk mempelajari hingar bagi keseluruhan Circuit. Kita menggunakan find_unique_box_instructions dari samplomatic untuk mendapatkan lapisan unik dari Circuit berkotak, termasuk lapisan ukuran. Lapisan-lapisan inilah yang kita hantar ke pemelajar hingar.
Setelah kita mengetahui lapisan-lapisan tersebut, kita boleh mempelajari hingar. Terdapat beberapa parameter yang perlu dipertimbangkan:
num_randomizations: Bilangan Circuit rawak yang digunakan per konfigurasi Circuit pembelajaranshots_per_randomization: Jumlah shot yang digunakan per Circuit pembelajaran rawaklayer_pair_depths: Kedalaman Circuit (diukur dalam bilangan pasangan) yang digunakan dalam eksperimen pembelajaran.post_selection: Kita akan menggunakan pemilihan pasca berasaskan tepi semasa pembelajaran menggunakan Gaterxuntuk melaksanakan denyutan pasca-ukuran
from qiskit_ibm_runtime.noise_learner_v3.noise_learner_v3 import NoiseLearnerV3
from qiskit_ibm_runtime.options import NoiseLearnerV3Options
from samplomatic.utils import find_unique_box_instructions
# Load noise learner data from a shared job
load_saved_nl_result = True
# Noise learning parameters
num_randomizations_nl = 64
shots_per_randomization_nl = 128
strategy = "edge"
enable_postsel = True
x_pulse_type = "rx"
# Find the unique instructions (layers) from boxed-up circuit
unique_2q_layers_and_meas = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
noise_learner_params = {
"num_randomizations": num_randomizations_nl,
"shots_per_randomization": shots_per_randomization_nl,
"layer_pair_depths": [1, 2, 4, 8, 12, 16, 24, 32, 40, 48],
"post_selection": {
"enable": enable_postsel,
"strategy": strategy,
"x_pulse_type": x_pulse_type,
},
"experimental": {},
}
# set the options
noise_learner_options = NoiseLearnerV3Options(**noise_learner_params)
# run the noise learner job
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()
nl_metadata = noise_learner_params | {"layout": layout}
import matplotlib.pyplot as plt
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 0x321dd63f0>

Kaitkan kotak litar dengan hingar yang dipelajariβ
Di sini, kita buat pemetaan antara ID rujukan InjectNoise bagi setiap kotak kepada model hingar yang dipelajari (PauliLindbladMap) yang mempengaruhi get-get belitan dalam kotak tersebut.
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation
# 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()
Sebarkan observable melalui anti-hingar yang dipelajari untuk mendapatkan observable pengurang hingarβ
Seperti yang dibincangkan di atas, ini dilakukan dalam dua langkah. Pertama, kita sebarkan penjana anti-hingar ke penghujung litar. Selepas itu, kita sebarkan observable melalui penjana yang telah berkembang itu. Proses ini diulang bagi setiap penjana anti-hingar dalam litar. Dalam pelaksanaan ini, setiap penjana dalam lapisan tertentu disebarkan ke penghujung litar secara selari. Selain itu, pemprosesan berbilang Python digunakan untuk melakukan penyebaran ke hadapan anti-hingar serta penyebaran ke belakang observable secara selari. Ini mencegah penumpukan penjana yang telah berkembang dalam ingatan dan juga memaksimumkan sumber pengiraan.
Apabila menjalankan PNA, kamu perlu sentiasa menyediakan litar berderau dan observable. Jika litar berderau kamu ialah litar berkotak dengan anotasi InjectNoise, kamu perlu menyediakan pemetaan yang kita buat dalam langkah di atas. Seseorang juga boleh menghantar litar tanpa kotak yang mengandungi arahan PauliLindbladError daripada qiskit-aer. Dalam kes itu, refs_to_noise_models tidak perlu disediakan. Selain input utama, pengguna perlu mempertimbangkan:
max_err_terms: Bilangan sebutan yang dikekalkan dalam setiap penjana anti-hingar semasa ia disebarkan ke hadapan. Membenarkan nilai ini lebih besar secara amnya meningkatkan ketepatan, tetapi kelakuan ini tidak dijamin bersifat monoton.max_obs_terms: Bilangan sebutan yang dikekalkan dalam observable pengurang hingar, , semasa ia disebarkan ke belakang melalui anti-hingar yang telah berkembang. Nilai yang lebih besar secara amnya meningkatkan ketepatan, tetapi tidak dijamin berlaku secara monoton.num_processes: Bilangan teras yang diperuntukkan untuk proses ini. Ingat, penjana disebarkan ke hadapan dan digunakan pada observable secara selari.search_step: Langkah penyebaran ke belakang menggunakan kaedah tamak untuk mengkonjugat secara anggaran dua pengoperasi dalam asas Pauli. Kaedah ini boleh dipercepatkan dengan meningkatkansearch_step. Lihat dokumentasi pauli-prop untuk maklumat lanjut.num_to_measure: Walaupun pemboleh ubah ini bukan input kepadagenerate_noise_mitigating_observable, kita gunakannya untuk mengawal berapa banyak sebutan daripada yang sebenarnya kita mahu ukur. Di sini kita hanya akan mengukur 30 sebutan teratas, iaitu sebutan asal dalam observable kita. Sebutan-sebutan tersebut kini telah diskalakan semula supaya mengukurnya mempunyai kesan mengurangkan hingar get yang dipelajari. Walaupun kita hanya mengukur 30 sebutan daripada , seringkali masih berguna untuk membiarkannya berkembang besar, kerana itu meningkatkan ketepatan faktor penskalaan sebutan-sebutan terdepan.
from qiskit_addon_pna import generate_noise_mitigating_observable
# 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! 13560 / 13560 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 0x16b69e840>]

Ubah asas pengukuran ke bentuk kanonikβ
Seterusnya, kita akan mencari set asas minimum untuk diukur supaya kita dapat meliputi sepenuhnya setiap sebutan Pauli dalam observable yang diukur (banyak observable boleh diukur serentak jika ia komut secara qubit-wise). Memandangkan kita hanya mengukur sebutan dalam observable asal kita, iaitu jumlah semua Pauli tunggal-Z, hanya satu asas diperlukan -- asas semua-Z.
Selain mencari set asas pengukuran Pauli, kita perlu memetakan sebutan-sebutan Pauli ini ke bentuk kanonik yang dijangkakan oleh primitif Executor. Untuk maklumat lanjut tentang pengaturan Qubit kanonik, layari dokumentasi samplomatic.
from qiskit_addon_utils.exp_vals.measurement_bases import get_measurement_bases
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
]
Tentukan cara pensampelan dalam QuantumProgramβ
QuantumProgram adalah tempat kita menentukan cara untuk mensampel eksperimen:
template_circuit: Litar yang mengandungi semua get yang diperlukan untuk melaksanakan semua rawak yang dikehendaki (daripada rawak twirling, parameter, dll.).samplex: Objek yang mendefinisikan taburan kebarangkalian ke atas semua kemungkinan rawak litar untuk disampel.samplex_arguments: Pengikatan yang diperlukan untuk menentukan samplex sepenuhnyabasis_changes: Di sinilah kita menentukan set asas untuk diukur yang akan meliputi semua sebutan Pauli dalam observable yang diukur.noise_scales.ref: Kita tetapkan skala setiap lapisan hingar kepada0.0untuk mencegah hingar tambahan daripada disuntik ke dalam sampel kitapauli_lindblad_maps: Diperlukan jikanoise_scalesdihantar. Ini hanya memetakan lapisan hingar kepada model hingar yang berkaitan.
shape: Tupel bentuk untuk melanjutkan bentuk implisit yang ditakrifkan olehsamplex_arguments. Paksi bukan trivial yang diperkenalkan oleh pelanjutan ini menghitung rawak.
from qiskit_ibm_runtime import QuantumProgram
# 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
bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)
samplex_inputs |= {"basis_changes": {"basis0": bases_broadcastable}}
# 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(
circuit=template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations_exec),
)
Sampel litar menggunakan prototaip primitif Executorβ
Setelah kita mendefinisikan QuantumProgram, melaksanakan eksperimen adalah mudah. Kita hanya perlu membuat instantiasi objek Executor, menyediakan Backend, dan menjalankan program.
from qiskit_ibm_runtime import Executor
# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()
Proses pasca sampel untuk mengira nilai jangkaan termitigasi ralatβ
Untuk mengira nilai jangkaan termitigasi ralat, kita akan:
- Mengira faktor penskalaan TREX berdasarkan hingar yang dipelajari yang mempengaruhi pengukuran
- Menjana topeng untuk mengekalkan hanya sampel terpilih pasca
- Menggunakan fungsi
executor_expectation_valuesdaripadaqiskit-addon-utilsuntuk menggabungkan semua data menjadi nilai jangkaan termitigasi ralat.
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
# 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,
)
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()
