انتقل إلى المحتوى الرئيسي

تحسين قيم التوقع: امتصاص الضوضاء المنتشرة (PNA)

في هذا البرنامج التعليمي، سنتعلم كيفية الاستفادة من أحدث الأدوات في نظام Qiskit البيئي لتنفيذ سير عمل قابل للتخصيص الكامل مع تخفيف الأخطاء. سنستعرض تقنية PNA ونستخدمها للتخفيف من أخطاء Gate. كما سنستخدم TREX للتخفيف من أخطاء القراءة، والانتقاء اللاحق للتخفيف من الأخطاء غير المأخوذة في الاعتبار ضمن نموذج الضوضاء المُتعلَّم.

المحتويات

  • تقديم نظرة عامة موجزة عن PNA
  • إنشاء دائرة Circuit كمومية تروترية وعنصر قابل للرصد. Transpile إلى Backend وتضمين قياسات الانتقاء اللاحق.
  • استخدام samplomatic لتدوير طبقات بوابات 2Q والقياسات. إيجاد الطبقات الفريدة ثنائية الكيوبت لتقليل تكلفة تعلم الضوضاء.
  • استخدام NoiseLearnerV3 لتعلم نموذج الخطأ المؤثر في بوابات 2Q والقياسات.
  • استخدام qiskit-addon-pna لتوليد عنصر قابل للرصد يخفف الضوضاء.
  • استخدام أداة qiskit-ibm-runtime.Executor الأولية لتوليد العينات الخام من وحدة معالجة الكم (QPU) التي تعكس كل لقطة لكل تعشية عشوائية وكل قاعدة قياس.
  • استخدام qiskit-addon-utils لمعالجة البيانات لاحقاً وتحويلها إلى قيمة توقع مخففة.

ما هو امتصاص الضوضاء المنتشرة (PNA)؟

تقنية للتخفيف من أخطاء Gate عن طريق نشر العنصر القابل للرصد عبر قناة الضوضاء العكسية المؤثرة في بوابات ثنائية الكيوبت، مما ينتج عنه عنصر قابل للرصد يخفف الضوضاء. ستتأثر بوابات 2Q في التجربة التي نريد تشغيلها بضوضاء كبيرة. Noisy experiment إذا تعلمنا نموذج الضوضاء، يمكننا تطبيق معكوسه وإلغاء الضوضاء. Noise-mitigated experiment بدلاً من تنفيذ قناة الضوضاء العكسية عن طريق أخذ عينات منها على وحدة معالجة الكم كما في PEC، يمكننا تنفيذها بشكل كلاسيكي في العنصر القابل للرصد المقيس باستخدام انتشار Pauli. ينتج عن ذلك عنصر قابل للرصد أكثر تعقيداً والذي، عند قياسه، يُحقق تأثير التخفيف من ضوضاء البوابات المُتعلَّمة. PNA overview

توليد دائرة Trotter المرآتية والعنصر القابل للرصد

في هذه التجربة، سندرس ديناميكيات الزمن لنموذج Ising المركل ذي 30 موقعاً على سلسلة دوران أحادية الأبعاد. الهاملتوني المأخوذ بعين الاعتبار هو:

H=Ji,jZiZj+hiXiH = -J\sum\limits_{\langle i,j \rangle} Z_iZ_j + h\sum\limits_iX_i,

حيث يصف J>0J>0 اقتران الدورانات الأقرب جواراً، i<ji<j، ويُعيَّن الحقل المستعرض العالمي hh على π8\frac{\pi}{8}. كلما ابتعد hh عن زاوية Clifford (أي θ=nπ2,nZ\theta=n\frac{\pi}{2}, n \in \mathbb{Z})، كلما أصبح نشر مولدات مكافحة الضوضاء عبر الدائرة أكثر صعوبة.

بالنسبة لاختيار العنصر القابل للرصد، سننظر في متوسط المغنطة أحادية الموقع، 1Ni=1Nzi\frac{1}{N} \sum_{i=1}^{N} \langle z_i \rangle، حيث NN هو عدد المواقع.

# 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)

Quantum circuit diagram

بعد ذلك، سنختار سلسلة من الكيوبتات على ibm_kingston تُسجل معدلات خطأ منخفضة وننقل الدائرة Circuit إلى 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.

Quantum circuit diagram

تدوير طبقات بوابات ثنائية الكيوبت والقياسات وإيجاد الطبقات الفريدة

هنا نضمن أن مدير التمرير يُضيف تعليقات Twirl وInjectNoise على الصناديق، مما يتيح لنا تعلم الضوضاء التي ستؤثر في دائرتنا Circuit وربط تلك الضوضاء بطبقة الدائرة المقابلة لها.

  • enable_gates/enable_measure: True: احتواء جميع طبقات بوابات ثنائية الكيوبت والقياسات الطرفية داخل صناديق. ستُلحق بوابات الكيوبت الواحد بالصناديق من الجانب الأيسر.
  • measure_annotations: all تضمين تعليقات Twirl وChangeBasis على صندوق القياس.
  • twirling_strategy: active: تدوير جميع الكيوبتات النشطة في كل صندوق يحتوي على بوابات تشابك.
  • inject_noise_targets: gates: ينبغي إضافة تعليقات InjectNoise إلى جميع الصناديق المُعلَّمة بـTwirl والتي تحتوي على بوابات تشابك.
  • inject_noise_strategy: uniform_modification: ينبغي تحجيم جميع طبقات الضوضاء بصورة متكافئة.
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)

Quantum circuit diagram

توليد الدائرة النموذجية و samplex، وتحديد كيفية أخذ عينات الدائرة

هنا نضيف أيضاً قياسات المراقبة والانتقاء اللاحق، وهي ضرورية لتطبيق الانتقاء اللاحق على العينات الناتجة عن 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)

Quantum circuit diagram

تعلم الضوضاء

قبل تشغيل التجارب، نتعلم نموذج الضوضاء المؤثر في بوابات التشابك والقياسات في الدائرة Circuit. يعد امتلاك نموذج ضوضاء دقيق أمراً ضرورياً للتخفيف الفعال من الأخطاء. إن تعلم الضوضاء قُبيل تنفيذ التجارب مباشرةً يمنح أفضل فرصة لكي يصف نموذج الضوضاء الضوضاء الفعلية المؤثرة في البوابات أثناء التنفيذ.

قبل تعلم الضوضاء، نحتاج إلى إيجاد الطبقات ثنائية الكيوبت الفريدة في دائرتنا Circuit، حتى نتمكن من تقليل عدد اللقطات اللازمة لتعلم الضوضاء لكامل الدائرة. نستخدم find_unique_box_instructions من samplomatic للحصول على الطبقات الفريدة من الدائرة ذات الصناديق، بما فيها طبقة القياس. هذه هي الطبقات التي نمررها إلى متعلم الضوضاء.

بمجرد معرفة الطبقات، يمكننا تعلم الضوضاء. ثمة بعض المعاملات التي نأخذها بعين الاعتبار:

  • num_randomizations: عدد الدوائر العشوائية المستخدمة لكل تكوين دائرة تعلم.
  • shots_per_randomization: العدد الإجمالي للقطات المستخدمة لكل دائرة تعلم عشوائية.
  • layer_pair_depths: أعماق الدائرة Circuit (قياساً بعدد الأزواج) المستخدمة في تجارب التعلم.
  • post_selection: سنستخدم الانتقاء اللاحق القائم على الحواف أثناء التعلم باستخدام بوابات rx لتنفيذ نبضات ما بعد القياس.
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>

Plot output

ربط صناديق الدوائر بالضوضاء المُتعلَّمة

هنا، ننشئ تعييناً بين معرّفات مرجع InjectNoise لكل صندوق ونموذج الضوضاء المُتعلَّم (PauliLindbladMap) الذي يؤثر على البوابات ذات الكيوبتين في ذلك الصندوق.

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()

نشر العنصر القابل للرصد عبر مضاد الضوضاء المُتعلَّم للحصول على عنصر قابل للرصد مُخفِّف للضوضاء

كما نوقش أعلاه، يتمّ ذلك في خطوتين. أولاً، نشر مولّد مضاد الضوضاء إلى نهاية الدائرة. بعد ذلك، يُنشر العنصر القابل للرصد عبر ذلك المولّد المتطوّر. تُكرَّر هذه العملية لكل مولّد مضاد للضوضاء في الدائرة. في هذا التنفيذ، يُنشر كل مولّد في طبقة معينة إلى نهاية الدائرة بالتوازي. علاوةً على ذلك، يُستخدم التعدد في المعالجة بلغة Python لتنفيذ كلٍّ من النشر الأمامي لمضاد الضوضاء والنشر الخلفي للعنصر القابل للرصد بالتوازي. يمنع ذلك تراكم المولّدات المتطوّرة في الذاكرة ويُعظّم استخدام موارد الحوسبة.

عند تشغيل PNA، ستحتاج دائماً إلى توفير دائرة مُضوضَأة وعنصر قابل للرصد. إذا كانت دائرتك المُضوضَأة دائرةً في صندوق مع تعليقات توضيحية InjectNoise، فستحتاج إلى توفير التعيين الذي أنشأناه في الخطوة أعلاه. يمكن أيضاً تمرير دائرة غير مُعبَّأة في صندوق تحتوي على تعليمات PauliLindbladError من qiskit-aer. في هذه الحالة، لا يلزم توفير refs_to_noise_models. بالإضافة إلى المدخلات الأساسية، يودّ المستخدمون مراعاة ما يلي:

  • max_err_terms: عدد الحدود التي يجب الاحتفاظ بها في كل مولّد لمضاد الضوضاء أثناء نشره إلى الأمام. السماح بقيمة أكبر يزيد الدقة عموماً، لكن هذا السلوك غير مضمون أن يكون رتيباً.
  • max_obs_terms: عدد الحدود التي يجب الاحتفاظ بها في العنصر القابل للرصد المُخفِّف للضوضاء، O~\tilde{O}، أثناء نشره خلفياً عبر مضاد الضوضاء المتطوّر. القيم الأكبر تزيد الدقة عموماً، لكن لا يُضمن ذلك بشكل رتيب.
  • num_processes: عدد الأنوية المخصّصة للعملية. تذكّر أن المولّدات تُنشر إلى الأمام وتُطبَّق على العنصر القابل للرصد بالتوازي.
  • search_step: يستخدم النشر الخلفي طريقةً جشعة لتقريب الاقتران بين عاملين في أساس باولي. يمكن تسريع هذه الطريقة بزيادة search_step. راجع توثيق pauli-prop للمزيد من المعلومات.
  • num_to_measure: رغم أن هذا المتغير ليس مدخلاً لـgenerate_noise_mitigating_observable، نستخدمه للتحكم في عدد الحدود من O~\tilde{O} التي نريد قياسها فعلياً. هنا سنقيس أعلى 30 حدّاً فقط، وهي الحدود الأصلية في عنصرنا القابل للرصد. أُعيد قياس الحدود الآن بحيث يؤدي قياسها إلى تخفيف ضوضاء البوابات المُتعلَّمة. على الرغم من أننا نقيس 30 حدّاً فقط من O~\tilde{O}، لا يزال من المفيد السماح له بالنمو، إذ يزيد ذلك من دقة معاملات القياس الرائدة.
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>]

Plot output

تحويل أسس القياس إلى الصيغة القانونية

بعد ذلك، سنجد مجموعةً صغرى من الأسس للقياس بحيث نتمكّن من تغطية كل حدّ باولي في العنصر القابل للرصد المقيس (يمكن قياس عديد من العناصر القابلة للرصد في آنٍ واحد إذا كانت تتبادل الإيهام بيت بيت). بما أننا نقيس فقط الحدود في عنصرنا القابل للرصد الأصلي، وهو مجموع جميع Paulis أحادية الـZ، فيكفي أساس واحد -- أساس الـZ الكلي.

إضافةً إلى إيجاد مجموعة من أسس قياس باولي، نحتاج إلى تعيين هذه الحدود الباولية إلى الصيغة القانونية المتوقعة بواسطة الكيان الأولي Executor. للمزيد من المعلومات حول ترتيب Qubit القانوني، تفضّل بزيارة توثيق 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
]

تحديد طريقة أخذ العيّنات في QuantumProgram

QuantumProgram هو المكان الذي نحدد فيه كيفية أخذ عيّنات التجربة:

  • template_circuit: الدائرة التي تحتوي على جميع البوابات اللازمة لتنفيذ جميع التعشية المرغوبة (من تعشية العشوائية، والمعاملات، وما إلى ذلك).
  • samplex: كائن يُعرِّف توزيعاً احتمالياً على جميع التعشيات الممكنة للدائرة ليُؤخذ منها عيّنات.
  • samplex_arguments: الارتباطات اللازمة لتعريف samplex بالكامل
    • basis_changes: هنا نحدد مجموعة من الأسس للقياس تغطي جميع الحدود الباولية في العنصر القابل للرصد المقيس.
    • noise_scales.ref: نضبط مقياس كل طبقة ضوضاء على 0.0 لمنع حقن أي ضوضاء إضافية في عيّناتنا.
    • pauli_lindblad_maps: مطلوب إذا مُرِّرت noise_scales. يعيّن هذا طبقات الضوضاء إلى نموذج الضوضاء المرتبط بها.
  • shape: مجموعة شكل لتمديد الشكل الضمني المُعرَّف بواسطة samplex_arguments. تُعدِّد المحاور غير التافهة التي يُدخلها هذا التمديد التعشيات.
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),
)

أخذ عيّنات من الدائرة باستخدام النموذج الأولي للكيان Executor

الآن بعد أن عرّفنا QuantumProgram، أصبح تنفيذ التجربة أمراً مباشراً. نُنشئ ببساطة كائن Executor، ونزوّده بالواجهة الخلفية Backend، ونشغّل البرنامج.

from qiskit_ibm_runtime import Executor

# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()

المعالجة اللاحقة للعيّنات لحساب قيمة توقع مُخفَّفة الأخطاء

لحساب قيمة توقع مُخفَّفة الأخطاء، سنقوم بما يلي:

  • حساب معاملات TREX للقياس استناداً إلى الضوضاء المُتعلَّمة التي تؤثر على القياسات
  • إنشاء قناع للاحتفاظ فقط بالعيّنات المُختارة بعد الاختيار اللاحق
  • استخدام الدالة executor_expectation_values من qiskit-addon-utils لدمج جميع البيانات في قيمة توقع مُخفَّفة الأخطاء.
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()

Plot output