انتقل إلى المحتوى الرئيسي
هذا المشروع في مرحلة تجريبية. وجدت خطأ؟ افتح مشكلة على GitHub. لديك سؤال أو فكرة؟ اسأل في Discussions — نحب سماع ملاحظاتك!
كيف جودة الترجمة في هذه الصفحة؟

مدخلات ومخرجات Samplex

مقدمة​

Samplex هو النوع الأساسي في مكتبة samplomatic. يمثّل الـ samplex توزيعاً احتمالياً معلمياً على معاملات دائرة قالب ما، مع حقول أخرى قيمها مصفوفات تُستخدم عند المعالجة اللاحقة للبيانات المجمّعة من تنفيذ دائرة القالب بعد ربط قيمها. واجهته المركزية هي الدالة sample() التي تسحب من هذا التوزيع لإنتاج مجموعة من المصفوفات.

يشرح هذا الدليل كيفية استخدام واجهة sample()، وكيفية الاستعلام عن المدخلات المطلوبة وتحديدها، وكيفية فحص المخرجات المتوقعة. المصفوفات التي تُمرَّر إلى sample() والتي تُعاد منها محددة الأنواع بصرامة. أي أن أسماءها وأنواعها وأشكالها ثابتة ويمكن الاستعلام عنها قبل إجراء السحب، وذلك لأي نسخة من samplex. تعتمد المدخلات والمخرجات الموجودة في samplex على عوامل كثيرة، مثل وجود twirling للقياس أو الحاجة إلى حقن الضجيج. ولهذا السبب، لا يُتوقع عموماً أن يكون لنسختين من samplex نفس المدخلات والمخرجات.

الإعداد​

أولاً، ابنِ دائرة مُقسَّمة إلى صناديق وحوّلها إلى زوج samplex وقالب. ستُستخدم هذه في الأمثلة التالية. لاحظ أنه عند وضع الدائرة في صناديق يدوياً، يستخدم هذا الدليل الأسماء alpha وbeta وref1 وref2 وmod_ref1 وmod_ref2 وmod_ref3 وconclude. بالإضافة إلى ذلك، تحتوي الدائرة على أربعة كائنات Parameter. يوضح هذا الدليل كيف يؤدي كل منها دوراً في مدخلات ومخرجات samplex. راجع دليل المترجم (Transpiler) لمعرفة كيفية وضع الدوائر في صناديق تلقائياً.

import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit import ClassicalRegister, Parameter, QuantumCircuit, QuantumRegister
from qiskit.quantum_info import Operator, Pauli, PauliLindbladMap

from samplomatic import ChangeBasis, InjectNoise, Twirl, build

# our circuit has two classical registers named alpha and beta
circuit = QuantumCircuit(
QuantumRegister(4), alpha := ClassicalRegister(3, "alpha"), beta := ClassicalRegister(1, "beta")
)

# the first box is only twirled
with circuit.box([Twirl()]):
for idx in range(4):
circuit.rx(Parameter(f"a{idx}"), idx)
circuit.cz(0, 1)
circuit.cz(1, 2)

# the second box is twirled, and has noise injected
with circuit.box([Twirl(), InjectNoise(ref="ref1", modifier_ref="mod_ref1")]):
circuit.h(1)
circuit.cz(1, 2)

# the third box is twirled, and has different noise injected
with circuit.box([Twirl(), InjectNoise(ref="ref2", modifier_ref="mod_ref2")]):
circuit.rx(0.1, range(4))
circuit.cz(0, 1)
circuit.cz(1, 2)

# the fourth box is the same as the second, but with a different modifer ref
with circuit.box([Twirl(), InjectNoise(ref="ref1", modifier_ref="mod_ref3")]):
circuit.h(1)
circuit.cz(1, 2)

circuit.barrier()

# the final two boxes twirl, and add a basis change in one case
with circuit.box([Twirl(), ChangeBasis(ref="conclude")]):
circuit.measure(range(3), alpha)

with circuit.box([Twirl()]):
circuit.measure([3], beta)

circuit.draw("mpl")

../_images/88f6576d7e2fd76bfb52d5133c6bc07e908a28941c6538f5251ac508cfb202f7.png

بعد ذلك، استدعِ build() على الدائرة المقسّمة إلى صناديق لبناء زوج القالب وsamplex. سترى كيف يتحول كل صندوق إلى "شطيرة" حواجز (barrier) تتضمن مقاطع rz-sx-rz-sx-rz لتنفيذ التلبيس.

template, samplex = build(circuit)

template.draw("mpl", fold=100)

../_images/ec8456d0d0a5928c5d056184cbd1842dcf7aefdc7cdba428f62092b27646d601.png

ارسم DAG الخاص بـ samplex ومرّر المؤشر فوق العقد لترى ما يلي:

  • كل عقد السحب (النجوم) مسؤولة عن توليد عشوائيات لـ twirling، أو السحب من نماذج الضجيج، أو حقن تغييرات الأساس.

  • كل عقد التجميع (ربطات العنق) مسؤولة عن إخراج الشرائح الناتجة، وهي قلبات القياس أو زوايا المعاملات.

  • كل العقد الوسيطة (الدوائر) مسؤولة عن أنواع مختلفة من معالجة البيانات.

samplex.draw()

الاستعلام عن المدخلات المطلوبة والمخرجات المتوقعة​

أسهل طريقة لرؤية المدخلات المطلوبة والمخرجات المتوقعة هي طباعة الكائن Samplex. تُنسَّق عناصر المصفوفات بالشكل '{name}' <{type}[{shape}...]>، وتأتي المدخلات المطلوبة قبل المدخلات الاختيارية.

print(samplex)
Samplex(<58 nodes>)
Inputs:
- 'basis_changes.conclude' <uint8[3]>: Basis changing gates, in the symplectic ordering
I=0, Z=1, X=2, and Y=3.
- 'parameter_values' <float64[4]>: Input parameter values to use during sampling.
- 'pauli_lindblad_maps.ref1' <PauliLindbladMap>: A PauliLindblad map acting on 2 qubits,
with 'num_terms_ref1' terms.
- 'pauli_lindblad_maps.ref2' <PauliLindbladMap>: A PauliLindblad map acting on 4 qubits,
with 'num_terms_ref2' terms.

- 'local_scales.mod_ref1' <float64['num_terms_ref1']>: (Optional) An array of factors by
which to scale individual rates of a Pauli Lindblad map, where the order should match the
corresponding list of terms.
- 'local_scales.mod_ref2' <float64['num_terms_ref2']>: (Optional) An array of factors by
which to scale individual rates of a Pauli Lindblad map, where the order should match the
corresponding list of terms.
- 'local_scales.mod_ref3' <float64['num_terms_ref1']>: (Optional) An array of factors by
which to scale individual rates of a Pauli Lindblad map, where the order should match the
corresponding list of terms.
- 'noise_scales.mod_ref1' <float64[]>: (Optional) A scalar factor by which to scale rates
of a Pauli Lindblad map.
- 'noise_scales.mod_ref2' <float64[]>: (Optional) A scalar factor by which to scale rates
of a Pauli Lindblad map.
- 'noise_scales.mod_ref3' <float64[]>: (Optional) A scalar factor by which to scale rates
of a Pauli Lindblad map.
Outputs:
* 'measurement_flips.alpha' <bool['num_randomizations', 1, 3]>: Bit-flip corrections
for measurement twirling.
* 'measurement_flips.beta' <bool['num_randomizations', 1, 1]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 48]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 3]>: Signs from sampled Pauli Lindblad
maps, where boolean values represent the parity of the number of non-trivial factors in the
sampled error that arise from negative rates. In other words, in order to implement basic
PEC, the sign used to correct expectation values should be ``(-1)**bool_value``. The order
matches the iteration order of boxes in the original circuit with noise injection
annotations.

يمكن أيضاً الاستعلام برمجياً عن inputs() وoutputs() الخاصة بـ samplex، وكذلك عن معلومات أكثر تفصيلاً مثل أسمائها وأشكالها وأنواعها ووصفها. نوع الإرجاع لكلتا الدالتين هو TensorInterface. على سبيل المثال، الكود التالي يسرد كل مواصفات واجهة المدخلات:

samplex.inputs().specs
[TensorSpecification('basis_changes.conclude', (3,), dtype('uint8'), 'Basis changing gates, in the symplectic ordering I=0, Z=1, X=2, and Y=3.'),
TensorSpecification('local_scales.mod_ref1', ('num_terms_ref1',), dtype('float64'), 'An array of factors by which to scale individual rates of a Pauli Lindblad map, where the order should match the corresponding list of terms.', optional=True),
TensorSpecification('local_scales.mod_ref2', ('num_terms_ref2',), dtype('float64'), 'An array of factors by which to scale individual rates of a Pauli Lindblad map, where the order should match the corresponding list of terms.', optional=True),
TensorSpecification('local_scales.mod_ref3', ('num_terms_ref1',), dtype('float64'), 'An array of factors by which to scale individual rates of a Pauli Lindblad map, where the order should match the corresponding list of terms.', optional=True),
TensorSpecification('noise_scales.mod_ref1', (), dtype('float64'), 'A scalar factor by which to scale rates of a Pauli Lindblad map.', optional=True),
TensorSpecification('noise_scales.mod_ref2', (), dtype('float64'), 'A scalar factor by which to scale rates of a Pauli Lindblad map.', optional=True),
TensorSpecification('noise_scales.mod_ref3', (), dtype('float64'), 'A scalar factor by which to scale rates of a Pauli Lindblad map.', optional=True),
TensorSpecification('parameter_values', (4,), dtype('float64'), 'Input parameter values to use during sampling.'),
PauliLindbladMapSpecification('pauli_lindblad_maps.ref1', num_qubits=2, num_terms=num_terms_ref1),
PauliLindbladMapSpecification('pauli_lindblad_maps.ref2', num_qubits=4, num_terms=num_terms_ref2)]

المثال التالي يطبع بعض المعلومات عن محددات المخرجات التي تحتوي أسماؤها على السلسلة "flips".

for spec in samplex.outputs().get_specs("flips"):
print(spec.name, spec.shape, spec.dtype)
measurement_flips.alpha ('num_randomizations', 1, 3) bool
measurement_flips.beta ('num_randomizations', 1, 1) bool

ربط قيم المدخلات والسحب​

لإجراء sample()، يحتاج samplex إلى قيم لعناصر inputs() المعلَّمة بأنها مطلوبة على الأقل. تُربط هذه القيم أولاً بواجهة المدخلات كي تتمكن من إجراء كل فحوصات الأنواع اللازمة (أو تحويل الأنواع في بعض الحالات) وتحليل الأشكال. إذا كان هناك خطأ في الأنواع أو الأشكال، أو كانت قيمة مطلوبة مفقودة، يُطلق خطأ مفصّل.

ربط المدخلات​

في المثال التالي، تُمرَّر القيم المطلوبة للمدخلات إلى دالة sample()، ثم تُستخدم لسحب ثلاث عشوائيات من samplex. الارتباطات المطلوبة من التلبيسات هي خرائط Pauli Lindblad لحقن الضجيج ومصفوفة تغيير الأساس. بالإضافة إلى ذلك، يجب توفير قيم للمعاملات الأربعة في الدائرة الأصلية المقسّمة إلى صناديق، وهي هنا مضبوطة على np.linspace(0, 1, 4). يدمج samplex هذه المعاملات في التلبيسات، فتصبح فعلياً تعديلات على قيم معاملات المخرجات في samplex.

inputs = samplex.inputs().bind(
# bind() concatenates nested dicts with a '.' so that we can
# set 'pauli_lindblad_maps.noise0/1' as follows. note that the
# names 'ref1' and 'ref2' derive from the names in the InjectNoise
# annotation.
pauli_lindblad_maps={
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
},
# likewise, we can set all basis changes as follows, where
# the name 'conclude' comes from our basis change annotation
basis_changes={"conclude": [0, 1, 2]},
# we must provide values for all parameters in the original circuit
parameter_values=np.linspace(0, 1, 4),
)

outputs = samplex.sample(inputs, num_randomizations=3)
print(outputs)
SamplexOutput(<
- Dimension constraints: num_randomizations=3

* 'measurement_flips.alpha' <bool['num_randomizations', 1, 3]>: Bit-flip corrections for
measurement twirling.
* 'measurement_flips.beta' <bool['num_randomizations', 1, 1]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 48]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 3]>: Signs from sampled Pauli Lindblad maps,
where boolean values represent the parity of the number of non-trivial factors in the sampled
error that arise from negative rates. In other words, in order to implement basic PEC, the
sign used to correct expectation values should be ``(-1)**bool_value``. The order matches the
iteration order of boxes in the original circuit with noise injection annotations.
>)

يمكن ربط المدخلات بالواجهة عبر عدة استدعاءات لـ bind() أو بالإسناد المباشر للعناصر. رغم صيغة القاموس المتداخل التي تتيحها bind() للراحة، فإنه كائن تعيين (mapping) مسطّح ويدعم كل صيغ التعيين المعتادة. بمجرد العثور على كل المدخلات المطلوبة، تصبح fully_bound صحيحة (true).

inputs = (
samplex.inputs()
.bind(
pauli_lindblad_maps={
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
}
)
.bind(parameter_values=np.linspace(0, 1, 4))
)

# once the final requirement is bound, fully_bound becomes True
print("Fully bound?", inputs.fully_bound)
inputs["basis_changes.conclude"] = [0, 2, 3]
print("Fully bound?", inputs.fully_bound)

# setting optional values does not affect whether the interface is fully bound
inputs["noise_scales.mod_ref1"] = 3
print("Fully bound?", inputs.fully_bound)
Fully bound? False
Fully bound? True
Fully bound? True

كل كائنات TensorInterface، بما فيها الصنف الفرعي SamplexOutput، تعيّن الكائنات إلى البيانات التي رُبطت بها. هذا يعني أنه يمكنك مثلاً الحصول على قيم معاملات القالب كما يلي. في هذه الحالة يكون شكلها (3, 48) لأن ثلاث عشوائيات طُلبت ولدائرة القالب 48 معاملاً.

print("(num_randomizations, num_template_params) =", outputs["parameter_values"].shape)
print(outputs["parameter_values"][0, :])
(num_randomizations, num_template_params) = (3, 48)
[ 0.0000000e+00 0.0000000e+00 0.0000000e+00 1.5707964e+00
3.3333334e-01 1.5707964e+00 -1.5707964e+00 2.4749260e+00
-1.5707964e+00 1.5707964e+00 1.0000000e+00 1.5707964e+00
0.0000000e+00 1.5707964e+00 0.0000000e+00 3.1415927e+00
0.0000000e+00 0.0000000e+00 1.5707964e+00 3.0415926e+00
-1.5707964e+00 -1.5707964e+00 3.0415926e+00 -1.5707964e+00
-1.5707964e+00 1.0000000e-01 1.5707964e+00 1.5707964e+00
1.0000000e-01 1.5707964e+00 3.1415927e+00 1.5707964e+00
0.0000000e+00 0.0000000e+00 0.0000000e+00 0.0000000e+00
0.0000000e+00 3.1415927e+00 0.0000000e+00 3.1415927e+00
0.0000000e+00 0.0000000e+00 3.1415927e+00 1.5707964e+00
0.0000000e+00 4.4408921e-16 3.1415927e+00 0.0000000e+00]

تحديد المدخلات كقاموس​

بدلاً من تمرير TensorInterface مرتبط بالكامل، يمكنك تحديد المدخلات كقاموس عادي، كما هو موضح في الخلية أدناه. في هذه الحالة تربط دالة sample() عناصر القاموس داخلياً بواجهة مدخلات samplex للتحقق من وجود كل المدخلات المطلوبة وتوافقها.

inputs = {
"pauli_lindblad_maps": {
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
},
"basis_changes": {"conclude": [0, 1, 2]},
"parameter_values": np.linspace(0, 1, 4),
}
outputs = samplex.sample(inputs, num_randomizations=3)

أنواع المدخلات​

حالياً هناك نوعان من قيم الواجهة:

  • قيم مصفوفية: لها شكل ونوع بيانات. تُحوَّل القيم المربوطة إلى نوع البيانات إن أمكن. يمكن لأي بُعد أن يكون بُعداً حراً.

  • خرائط Pauli Lindblad: يجب أن تكون من النوع qiskit.quantum_info.PauliLindbladMap ويجب أن تعمل على العدد المحدد من الكيوبتات. عدد الحدود في الخريطة بُعد حر.

# up to precision loss, these are equivalent via automatic type coersion of array inputs
samplex.inputs()["parameter_values"] = [1, 2, 8, 9]
samplex.inputs()["parameter_values"] = np.array([1, 2, 8, 9], dtype=np.float16)

الأبعاد الحرة​

يتضمن الصنف TensorInterface مفهوم الأبعاد الحرة (free dimensions)، وهي أحجام صحيحة مسماة لا تتحدد قيمها إلا بعد ربط بيانات بالواجهة تقيّدها. يجب أن تُحَل كل الأبعاد الحرة التي لها نفس الاسم إلى حجم متسق عند ربط البيانات، وإلا سيُطلق خطأ. على سبيل المثال، في outputs() لأي samplex، محور المصفوفة المقابل للعشوائيات هو معامل حر اسمه "num_randomizations" حسب الاصطلاح. عند استدعاء sample() تربط قيماً بالمخرجات

print("All free dimensions:", outputs.free_dimensions)
print("Current constraints:", outputs.bound_dimensions)
All free dimensions: {'num_randomizations'}
Current constraints: {'num_randomizations': 3}

سيناريو شائع آخر للأبعاد الحرة هو مشاركة قيود الحدود بين مواصفات خرائط Pauli Lindblad ومُعدِّلاتها. qiskit.quantum_info.PauliLindbladMap هي متتالية مرتبة من الحدود، يحتوي كل حد على معدل بفاصلة عائمة وتمثيل Pauli متناثر. تضيف الدالة build() محدِّداً "pauli_lindblad_maps.<ref>" لهذا النوع لكل تعليق InjectNoise، وإذا كان يحتوي على modifier_ref فإنها تضيف أيضاً محدِّداً "local_scales.<modifier_ref>". يتشارك كلاهما البُعد الحر "num_terms_<ref>" الذي يحدد عدد حدود Pauli Lindblad الموجودة في نموذج الضجيج، لأن الأخير يحتاج إلى الضم (zip) مع معدلات الأول لتحجيمها.

# both the PauliLindbladMap and the local scales imply 2 terms, so that the free dimension
# 'num_terms_noise2' is satisfied
inputs = samplex.inputs().bind(
pauli_lindblad_maps={"ref2": PauliLindbladMap.from_list([("XXYZ", 0.1), ("IIXX", 0.2)])},
local_scales={"mod_ref2": [1, 2]},
)
print("All free dimensions:", inputs.free_dimensions)
print("Current constraints:", inputs.bound_dimensions)
All free dimensions: {'num_terms_ref1', 'num_terms_ref2'}
Current constraints: {'num_terms_ref2': 2}

اصطلاح ترتيب الكيوبتات​

قد تتطلب نسخ Samplex المبنية من دوائر مقسّمة إلى صناديق بواسطة الدالة build() مدخلات مصفوفية إلى sample()، حيث يقابل أحد الفهارس الكيوبتات في صندوق الدائرة. يشرح هذا القسم اصطلاح الترتيب الذي يجب استخدامه لمثل هذه المحاور. باختصار: استخدم ترتيب الكيوبتات في الدائرة المقسّمة إلى صناديق، مقيّداً بكيوبتات الصندوق.

على سبيل المثال، لنفترض أنك تريد معرفة كيفية تحديد خريطة Pauli Lindblad للصندوق "noise1". عندها يكون ترتيب الكيوبتات في خريطة الضجيج بالنسبة إلى هذه الكيوبتات:

box_of_interest = circuit[1]
qubit_ordering_convention = [qubit for qubit in circuit.qubits if qubit in box_of_interest.qubits]
qubit_ordering_convention
[<Qubit register=(4, "q0"), index=1>, <Qubit register=(4, "q0"), index=2>]

أي أنك إذا أردت أن يكون <Qubit register=(4, "q0"), index=2> ضجيجياً بضجيج X، فيجب أن تعرّف خريطة Pauli Lindblad مثل هذه لأنه في الفهرس 1 من qubit_ordering_convention.

samplex.inputs().bind(
pauli_lindblad_maps={
"ref1": (noise1 := PauliLindbladMap.from_sparse_list([("X", [1], 0.12)], num_qubits=2))
}
)
noise1
<PauliLindbladMap with 1 term on 2 qubits: (0.12)L(X_1)>

الأمر نفسه ينطبق على تغييرات الأساس. إذا أردت التدوير إلى الحالات الذاتية ذات القيمة +1 للمؤثرات X وY وZ للكيوبتات 0 و1 و2 من circuit على التوالي، فعليك استخدام المصفوفة التالية، مع تذكّر أن الاصطلاح I=0I=0 وZ=1Z=1 وX=2X=2 وY=3Y=3 هو المستخدم في هذه المكتبة (وفي Qiskit).

samplex.inputs().bind(basis_changes={"conclude": (basis_change := [2, 3, 1])})
basis_change
[2, 3, 1]

لاحظ أن اصطلاح ترتيب الكيوبتات ليس هو الترتيب الذي يحدده أي مما يلي:

  • box_instruction.qubits، حيث box_instruction هو qiskit.circuit.CircuitInstruction عمليته هي qiskit.circuit.BoxOp، لأن ترتيب قائمة الكيوبتات هذه قد يعتمد على الترتيب الذي أُضيفت به التعليمات إلى سياق الصندوق، وهذا اصطلاح مربك. على سبيل المثال، with box(): circuit.x([0, 1]) وwith box(): circuit.x([1, 0]) سينتجان ترتيبين مختلفين.

  • box_instruction.operation.body.qubits لأن هذه الكيوبتات قد لا تظهر أصلاً في الدائرة الخارجية. الكيوبتات داخل جسم تعليمات BoxOp الفعلي (أو أي عملية تحكم في التدفق أخرى مثل IfElseOp) مضمونة المعنى المتسق داخل ذلك النطاق فقط. سيكون هذا اصطلاحاً غير محدد جيداً ويصعب استخدامه عموماً.

الاختبار المحلي باستخدام دائرة القالب​

إذا بُني Samplex باستخدام الدالة build()، فيجب أن تكون قيم معاملات المخرجات متوافقة مباشرة مع معاملات دائرة القالب المرتبطة. على سبيل المثال، يمكنك ربط قيم إحدى العشوائيات بالقالب لفحصها محلياً.

inputs = samplex.inputs().bind(
pauli_lindblad_maps={
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
},
basis_changes={"conclude": [0, 0, 0]},
parameter_values=np.linspace(0, 1, 4),
)

outputs = samplex.sample(inputs, num_randomizations=3)

bound_template = template.assign_parameters(outputs["parameter_values"][2])
bound_template.draw("mpl", fold=1000)

../_images/1d82c46209f8648a2df572cfef8ca256f47952566d34386761ab942aa6e50afd.png

هذا يتيح لك التحقق من أن samplex ينتج العينات المتوقعة. على سبيل المثال، الكود التالي يحوّل الدائرة المربوطة إلى كائن qiskit.quantum_info.Operator ويركّبها مع Pauli قلب البتات للتحقق بصرياً من أن العشوائيات الثلاث كلها مكافئة منطقياً للدائرة الأساسية.

from samplomatic.utils import unbox

# Operator requires that we unpack all of the boxes first
unboxed = unbox(circuit).assign_parameters(inputs["parameter_values"])
# We want to plot unitary matrix amplitudes, so we need to remove the non-unitary measurements
unboxed.remove_final_measurements()
unboxed_unitary = Operator(unboxed)

# Plot magnitudes of the unitary matrix represenation of the circuit
plt.subplot(1, 4, 1)
plt.imshow(np.abs(unboxed_unitary))
plt.title("Base Circuit")

# For each randomization we sampled, plot the unitary matrix representation
for idx in range(3):
bound_template = template.assign_parameters(outputs["parameter_values"][idx])
bound_template.remove_final_measurements()

# The example does measurement twirling by compiling random bitflip gates before the
# measurements, so we need to undo these at the operator level for each randomization.
# We do this by casting them to Paulis and composing with the bound circuit's unitary
alpha_flips = Pauli(([0, 0, 0], outputs["measurement_flips.alpha"][idx, 0]))
beta_flips = Pauli(([0], outputs["measurement_flips.beta"][idx, 0]))
flips = beta_flips ^ alpha_flips
bound_unitary = Operator(bound_template) & flips

# Plot magnitudes of the unitary matrix represenation of the circuit
plt.subplot(1, 4, idx + 2)
plt.imshow(np.abs(bound_unitary))
plt.title(f"Randomization {idx}")

plt.tight_layout()

../_images/af1aa74f8a0e8abed673d4cd43ff8b810c44297d2f3a42fcfb49ade811e51c58.png

وبالمثل، يمرر الكود التالي قيم معاملات المخرجات والقالب إلى sampler لمحاكاة 10,000 تشغيل (shot) لكل العشوائيات، ويقارن قيم التوقع الناتجة (لسجل alpha فقط) مع قيم التوقع من محاكاة الدائرة الأساسية.

from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
from qiskit.primitives.containers import BitArray

# simulate some expectation values direction from the base circuit
estimator_job = Estimator().run([(unboxed, ["IZII", "IIZI", "IIIZ"])])
evs = estimator_job.result()[0].data["evs"]

# get the sampler data from each randomization, and do bitflip correction
sampler_job = Sampler().run([(template, outputs["parameter_values"])], shots=10_000)
alpha_data = sampler_job.result()[0].data["alpha"]
alpha_data ^= BitArray.from_bool_array(outputs["measurement_flips.alpha"], "little")

# compare the results
print(" EVs from base circuit:", evs)
print("EVs from randomizations:", alpha_data.expectation_values(["ZII", "IZI", "IIZ"]))
EVs from base circuit: [0.7819611 0.93553883 0.99500417]
EVs from randomizations: [0.789 0.9366 0.995 ]