קלטים ופלטים של Samplex
מבוא
Samplex הוא הטיפוס המרכזי בספריית samplomatic. samplex מייצג התפלגות הסתברותית פרמטרית מעל הפרמטרים של מעגל תבנית כלשהו, יחד עם שדות נוספים בעלי ערכי מערך לשימוש בעיבוד נתונים שנאספו מהרצת מעגל התבנית לאחר קשירת הפרמטרים. הממשק המרכזי שלו הוא המתודה sample() שדוגמת מההתפלגות הזו ומפיקה אוסף של מערכים.
המדריך הזה מתאר איך להשתמש בממשק sample(), איך לשאול ולהגדיר את הקלטים הנדרשים, ואיך לבדוק את הפלטים הצפויים. המערכים שמועברים אל sample() ומוחזרים ממנו הם בעלי טיפוס חזק. כלומר, עבור מופע של samplex, השמות, הטיפוסים והצורות שלהם קבועים וניתנים לשאילתה לפני שמתבצעת הדגימה. הקלטים והפלטים שקיימים ב-samplex תלויים בגורמים רבים. למשל, האם יש twirling של מדידות והאם נדרשת הזרקת רעש. מסיבה זו, באופן כללי, אין לצפות ששני מופעים של samplex יהיו בעלי אותם קלטים ופלטים.
הגדרה
קודם כול, בונים מעגל ארוז ב-boxes ובונים ממנו זוג של samplex ותבנית. בהם נשתמש בדוגמאות הבאות. שימו לב שבאריזת המעגל ב-boxes ידנית, המדריך הזה משתמש בשמות alpha, beta, ref1, ref2, mod_ref1, mod_ref2, mod_ref3 ו-conclude. בנוסף, במעגל יש ארבעה אובייקטים מסוג Parameter. המדריך הזה מראה איך כל אחד מהם משחק תפקיד בקלטים ובפלטים של ה-samplex. לפרטים על אריזת מעגלים ב-boxes באופן אוטומטי, ראו מדריך ה-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")
בשלב הבא, קוראים ל-build() על המעגל הארוז כדי לבנות זוג של תבנית ו-samplex. תראו איך כל אחד מה-boxes הופך ל"סנדוויץ'" של barriers שכולל מקטעי rz-sx-rz-sx-rz למימוש ה-dressing.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
מציירים את ה-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. הקשירות הנדרשות מה-dressings הן מפות Pauli Lindblad להזרקת רעש ומערך שינוי הבסיס. בנוסף, נדרשים ערכים לארבעת הפרמטרים במעגל הארוז המקורי, ובמקרה הזה הם מוגדרים ל-np.linspace(0, 1, 4). ה-samplex מרכיב את הפרמטרים האלה לתוך ה-dressings, ולכן הם הופכים בפועל לשינויים בערכי הפרמטרים של הפלט של ה-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() מאפשר לשם נוחות, זהו אובייקט מיפוי שטוח, והוא תומך בכל התחביר הרגיל של מיפויים. ברגע שנמצאו כל הקלטים הנדרשים, 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וחייבות לפעול על מספר ה-qubits הנדרש. מספר האיברים במפה הוא ממד חופשי.
# 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 יש מושג של ממדים חופשיים, שהם גדלים שלמים בעלי שם שערכיהם אינם נקבעים עד שנקשרים לממשק נתונים שמגבילים אותם. כל הממדים החופשיים בעלי אותו שם חייבים להתאים לגודל עקבי בעת קשירת נתונים, אחרת תוצג שגיאה. למשל, ב-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 לבין המתאמים (modifiers) שלהן. 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}
מוסכמת סדר ה-qubits
Samplexes שנבנים ממעגלים ארוזים ב-boxes באמצעות הפונקציה build() יכולים לדרוש קלטים בעלי ערכי מערך ל-sample(), שבהם אינדקס אחד מתאים ל-qubits ב-box של המעגל. הסעיף הזה מסביר את מוסכמת הסדר שצריך להשתמש בה בצירים כאלה. בקצרה: משתמשים בסדר ה-qubits של המעגל הארוז, מוגבל ל-qubits של ה-box.
למשל, נניח שאתם רוצים להבין איך להגדיר מפת Pauli Lindblad עבור "noise1". אז סדר ה-qubits במפת הרעש הוא ביחס ל-qubits האלה:
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 עבור qubits 0, 1 ו-2 של circuit, בהתאמה, צריך להשתמש במערך הבא, תוך זכירה שבספרייה הזו (וב-Qiskit) משתמשים במוסכמה , , ו-.
samplex.inputs().bind(basis_changes={"conclude": (basis_change := [2, 3, 1])})
basis_change
[2, 3, 1]
שימו לב שמוסכמת סדר ה-qubits אינה הסדר שמוגדר על ידי אף אחד מהבאים:
-
box_instruction.qubits, כאשרbox_instructionהואqiskit.circuit.CircuitInstructionשהפעולה שלו היאqiskit.circuit.BoxOp, כי סדר רשימת ה-qubits הזו עשוי להיות תלוי בסדר שבו הוראות נוספו להקשר ה-box, וזו תהיה מוסכמה מבלבלת. למשל,with box(): circuit.x([0, 1])ו-with box(): circuit.x([1, 0])יובילו לסדרים שונים. -
box_instruction.operation.body.qubitsכי ה-qubits האלה אפילו לא בהכרח מופיעים במעגל החיצוני. ל-qubits בתוך הגוף של הוראותBoxOp(או כל פעולת בקרת זרימה אחרת כמוIfElseOp) מובטחת משמעות עקבית רק בתוך ה-scope הזה. זו תהיה מוסכמה לא מוגדרת היטב, וקשה לשימוש באופן כללי.
בדיקה מקומית עם מעגל התבנית
אם 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)
זה מאפשר לוודא שה-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()

באופן דומה, הקוד הבא מעביר ל-sampler את ערכי הפרמטרים של הפלט ואת התבנית כדי לדמות 10,000 shots של כל הרנדומיזציות, ומשווה את ערכי התצפית (expectation values) שהתקבלו (רק של האוגר 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 ]

