Pagpapabuti ng Expectation Values: Propagated Noise Absorption (PNA)
Sa tutorial na ito, matututuhan natin kung paano gamitin ang pinakabagong mga tool sa Qiskit ecosystem upang mag-implementa ng isang ganap na customizable, error mitigated workflow. Ipakikilala natin ang teknik na PNA at gagamitin ito upang i-mitigate ang gate errors. Gagamitin din natin ang TREX upang i-mitigate ang readout errors at post-selection upang i-mitigate ang mga error na hindi nakuha sa learned noise model.
Outline
- Magbigay ng maikling overview ng
PNA - Lumikha ng isang Trotterized quantum circuit at observable. I-transpile ito sa backend at isama ang post-selection measurements.
- Gamitin ang
samplomaticupang i-twirl ang mga layer ng 2Q gates at measurements. Hanapin ang mga unique 2Q layer upang bawasan ang noise learning cost. - Gamitin ang
NoiseLearnerV3upang matutuhan ang error model na nakakaapekto sa 2Q gates at measurements. - Gamitin ang
qiskit-addon-pnaupang bumuo ng noise-mitigating observable - Gamitin ang
qiskit-ibm-runtime.Executorprimitive upang bumuo ng raw QPU samples na nagpapakita ng bawat shot para sa bawat twirling randomization at measured basis - Gamitin ang
qiskit-addon-utilsupang i-post-process ang data para maging isang mitigated expectation value.
Ano ang propagated noise absorption (PNA)?β
Isang teknik para sa pagmiminimize ng gate errors sa pamamagitan ng pag-propagate ng observable sa inverse noise channel na nakakaapekto sa 2-qubit gates, na nagreresulta sa isang noise-mitigating observable.
Ang 2Q gates sa eksperimentong gusto nating patakbuhin ay maaapektuhan ng malaking ingay (noise).
Kung matututuhan natin ang noise model, maaari nating ilapat ang inverse nito at kanselahin ang noise.
Sa halip na i-implementa ang inverse noise channel sa pamamagitan ng pag-sample nito sa QPU tulad ng sa PEC, maaari nating i-implementa ito nang klasikal sa measured observable gamit ang Pauli propagation. Ito ay nagreresulta sa isang mas masalimuot na observable na, kapag na-measure, ay may epekto ng pag-mitigate sa learned gate noise.

Bumuo ng mirrored Trotter circuit at observableβ
Para sa eksperimentong ito, pag-aaralan natin ang time dynamics ng isang 30-site kicked Ising model sa isang 1D spin chain. Ang Hamiltonian na isinasaalang-alang ay:
,
kung saan ang ay naglalarawan ng coupling ng nearest-neighbor spins, , at ang global transverse field, , ay itinakda sa . Habang mas malayo ang sa isang Clifford angle (i.e. ), mas mahirap ipropagate ang anti-noise generators sa pamamagitan ng circuit.
Para sa pagpili ng observable, isasaalang-alang natin ang average single-site magnetization, , kung saan ang ay ang bilang ng mga site.
# 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)

Susunod, pipili tayo ng chain ng mga qubit sa ibm_kingston na nag-uulat ng mababang error rates at i-transpile natin ang circuit sa 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.

I-twirl ang mga 2-qubit gate layer at measurements at hanapin ang mga unique layerβ
Dito tinitiyak natin na ang pass manager ay nag-aannotate sa mga box gamit ang Twirl at InjectNoise annotations, na nagpapahintulot sa atin na matutuhan ang noise na makakaapekto sa ating circuit at iugnay ang noise na iyon sa kaukulang circuit layer nito.
enable_gates/enable_measure: True: I-box ang lahat ng 2q gate layers at terminal measurements. Ang single qubit gates ay ila-left-dressed sa loob ng mga box.measure_annotations: allIsama angTwirlatChangeBasisannotations sa measurement boxtwirling_strategy: active: I-twirl ang lahat ng active qubits sa bawat box na naglalaman ng entangling gatesinject_noise_targets: gates: AngInjectNoiseannotations ay dapat idagdag sa lahat ngTwirl-annotated boxes na naglalaman ng entangling gatesinject_noise_strategy: uniform_modification: Lahat ng noise layers ay dapat i-scale nang pantay-pantay.
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)

Bumuo ng template circuit at samplex, tukuyin kung paano isasagawa ang pag-sample ng circuitβ
Dito din natin idinadagdag ang spectator at post-selection measurements, na kailangan upang magsagawa ng post-selection sa mga sample na output mula sa 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)

Matutuhan ang noiseβ
Bago natin patakbuhin ang mga eksperimento, matututuhan natin ang noise model na nakakaapekto sa entangling gates at measurements sa circuit. Ang pagkakaroon ng tumpak na noise model ay kinakailangan upang epektibong ma-mitigate ang error. Ang pag-aaral ng noise bago mismo isagawa ang mga eksperimento ay nagbibigay ng pinakamahusay na pagkakataon na ang noise model ay tapat na maglarawan ng aktwal na noise na nakakaapekto sa gates habang isinasagawa.
Bago natin matutuhan ang noise, kailangan nating hanapin ang mga unique 2-qubit layer sa ating circuit, upang ma-minimize natin ang bilang ng shots na kailangan upang matutuhan ang noise para sa buong circuit. Ginagamit natin ang find_unique_box_instructions mula sa samplomatic upang ibigay sa atin ang mga unique layer mula sa boxed circuit, kasama na ang measurement layer. Ito ang mga layer na ipinapasa natin sa noise learner.
Kapag alam na natin ang mga layer, maaari na nating matutuhan ang noise. May ilang parameter na isinasaalang-alang natin:
num_randomizations: Ang bilang ng random circuits na gagamitin sa bawat learning circuit configurationshots_per_randomization: Kabuuang bilang ng shots na gagamitin sa bawat random learning circuitlayer_pair_depths: Ang circuit depths (sinusukat sa bilang ng pairs) na gagamitin sa learning experiments.post_selection: Gagamit tayo ng edge-based post-selection habang nag-aaral gamit angrxgates upang i-implementa ang post-measurement pulses
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>

Iugnay ang circuit boxes sa learned noiseβ
Dito, gumagawa tayo ng mapping sa pagitan ng InjectNoise reference IDs ng bawat box patungo sa learned noise model (PauliLindbladMap) na nakakaapekto sa entangling gates sa box na iyon.
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()
Ipropagate ang observable sa learned anti-noise upang makakuha ng noise-mitigating observableβ
Tulad ng tinalakay sa itaas, ito ay ginagawa sa dalawang hakbang. Una, ipropagate natin ang isang anti-noise generator hanggang sa katapusan ng circuit. Pagkatapos noon, ipropagate natin ang observable sa pamamagitan ng evolved generator na iyon. Ang prosesong ito ay inuulit para sa bawat anti-noise generator sa circuit. Sa implementation na ito, ang bawat generator sa isang ibinigay na layer ay ipinopropagate sa katapusan ng circuit nang parallel. Bukod pa rito, ang Python multiprocessing ay ginagamit upang isagawa ang parehong forward-propagation ng anti-noise pati na rin ang back-propagation ng observable nang parallel. Pinipigilan nito ang pile-up ng evolved generators sa memorya at kasabay nito ay napapakinabangan ang compute resources.
Kapag nagpapatakbo ng PNA, kakailanganin mong palaging magbigay ng noisy circuit at observable. Kung ang iyong noisy circuit ay isang boxed circuit na may InjectNoise annotations, kailangan mong ibigay ang mapping na ginawa natin sa hakbang sa itaas. Maaari ka ring magpasa ng non-boxed circuit na naglalaman ng PauliLindbladError instructions mula sa qiskit-aer. Sa kasong iyon, hindi na kailangang ibigay ang refs_to_noise_models. Bukod sa primary inputs, gugustuhin ng mga user na isaalang-alang:
max_err_terms: Ang bilang ng mga terminong panatilihin sa bawat anti-noise generator habang ito ay forward-propagated. Ang pagpapahintulot na lumaki ito ay sa pangkalahatan ay nagpapataas ng katumpakan, ngunit ang pag-uugaling ito ay hindi garantisado na maging monotonic.max_obs_terms: Ang bilang ng mga terminong panatilihin sa noise-mitigating observable, , habang ito ay back-propagated sa pamamagitan ng evolved anti-noise. Ang mas malalaking values ay sa pangkalahatan ay nagpapataas ng katumpakan, ngunit hindi garantisado na gagawin ito nang monotonic.num_processes: Ang bilang ng cores na ilalaan sa proseso. Tandaan, ang generators ay forward-propagated at inilalapat sa observable nang parallel.search_step: Ang back-propagation step ay gumagamit ng greedy method upang humigit-kumulang i-conjugate ang dalawang operator sa Pauli basis. Ang method na ito ay maaaring mapabilis sa pamamagitan ng pagpapataas ngsearch_step. Tingnan ang pauli-prop docs para sa karagdagang info.num_to_measure: Bagaman ang variable na ito ay hindi input sagenerate_noise_mitigating_observable, ginagamit natin ito upang kontrolin kung ilang termino mula sa ang gusto nating talagang sukatin. Dito, susukatin lamang natin ang nangungunang 30 na termino, na siyang orihinal na mga termino sa ating observable. Ang mga termino ay na-rescale na ngayon nang sa gayon ang pagsukat sa mga ito ay may epekto ng pag-mitigate sa learned gate noise. Bagaman 30 termino lamang ang ating sinusukat mula sa , madalas ay kapaki-pakinabang pa rin na payagan itong lumaki, dahil pinapataas niyon ang katumpakan ng scaling factors ng nangungunang mga termino.
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>]

I-transform ang measurement bases sa canonical formβ
Susunod, hahanapin natin ang isang minimal na set ng bases na susukatin upang ganap nating masaklaw ang bawat Pauli term sa measured observable (maraming observables ang maaaring sukatin nang sabay-sabay kung qubit-wise silang nag-c-commute). Dahil sinusukat lamang natin ang mga termino sa ating orihinal na observable, na siyang sum ng lahat ng single-Z Paulis, isang basis lamang ang kailangan -- ang all-Z basis.
Bukod sa paghahanap ng set ng Pauli measurement bases, kailangan nating i-map ang Pauli terms na ito sa canonical form na inaasahan ng Executor primitive. Para sa karagdagang impormasyon sa canonical qubit ordering, bisitahin ang samplomatic docs.
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
]
Tukuyin kung paano mag-sample sa QuantumProgramβ
Ang QuantumProgram ay kung saan natin tinutukoy kung paano i-sample ang eksperimento:
template_circuit: Ang circuit na naglalaman ng lahat ng gates na kailangan upang i-implementa ang lahat ng nais na randomizations (mula sa twirling randomizations, parameters, atbp).samplex: Isang object na tumutukoy sa probability distribution sa lahat ng posibleng circuit randomizations kung saan magsa-sample.samplex_arguments: Mga binding na kinakailangan upang ganap na tukuyin ang samplexbasis_changes: Dito tinutukoy natin ang isang set ng bases na susukatin na sasaklaw sa lahat ng Pauli terms sa measured observable.noise_scales.ref: Itinakda natin ang scale ng bawat noise layer sa0.0upang maiwasan ang anumang karagdagang noise na ma-inject sa ating samplespauli_lindblad_maps: Kinakailangan kung ipinapasa angnoise_scales. Ito ay nagma-map lamang ng noise layers sa kaukulang noise model.
shape: Isang shape tuple para palawakin ang implicit shape na tinutukoy ngsamplex_arguments. Ang non-trivial axes na ipinakilala ng pagpapalawig na ito ay nag-e-enumerate ng randomizations.
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),
)
I-sample ang circuit gamit ang Executor primitive prototypeβ
Ngayong natukoy na natin ang ating QuantumProgram, ang pag-execute ng eksperimento ay simple. Itinatatag natin ang Executor object, ibinibigay sa ito ang backend, at pinapatakbo ang program.
from qiskit_ibm_runtime import Executor
# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()
I-post-process ang samples upang makalkula ang error-mitigated expectation valueβ
Upang makalkula ang error-mitigated expectation value, gagawin natin ang:
- Kalkulahin ang TREX scaling factors batay sa learned noise na nakakaapekto sa measurements
- Bumuo ng mask para panatilihin lamang ang post-selected samples
- Gamitin ang
executor_expectation_valuesfunction mula saqiskit-addon-utilspara pagsamahin ang lahat ng data sa isang error-mitigated expectation value.
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()
