Sari la conținutul principal
Acest proiect este în versiune beta. Ai găsit o eroare? Deschide un issue pe GitHub. Ai o întrebare sau o idee? Întreabă în Discussions — ne-ar plăcea să aflăm părerea ta!
Cum este calitatea traducerii pe această pagină?

Intrările și ieșirile Samplex

Introducere​

Samplex este tipul central al bibliotecii samplomatic. Un samplex reprezintă o distribuție de probabilitate parametrică peste parametrii unui circuit șablon, cu alte câmpuri de tip array folosite la post-procesarea datelor colectate din execuția circuitului șablon legat. Interfața sa centrală este metoda sample(), care extrage din această distribuție pentru a produce o colecție de array-uri.

Acest ghid descrie cum se folosește interfața sample(), cum se interoghează și se specifică intrările necesare și cum se inspectează ieșirile așteptate. Array-urile furnizate către sample() și returnate de aceasta sunt puternic tipizate. Adică, pentru o instanță de samplex, numele, tipurile și formele lor sunt fixe și pot fi interogate înainte de eșantionare. Intrările și ieșirile prezente într-un samplex depind de mulți factori. De exemplu, dacă este prezent twirling-ul măsurătorilor și dacă este necesară injectarea zgomotului. Din acest motiv, în general, nu se așteaptă ca două instanțe de samplex să aibă aceleași intrări și ieșiri.

Configurare​

Mai întâi, construiește un circuit împărțit în casete și transformă-l într-o pereche samplex și șablon. Acestea vor fi folosite în exemplele de mai jos. Reține că, la împărțirea manuală a circuitului în casete, acest ghid folosește numele alpha, beta, ref1, ref2, mod_ref1, mod_ref2, mod_ref3 și conclude. În plus, circuitul are patru obiecte Parameter. Acest ghid arată ce rol joacă fiecare dintre ele în intrările și ieșirile samplex. Consultă ghidul Transpiler pentru informații despre împărțirea automată a circuitelor în casete.

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

Apoi, apelează build() pe circuitul cu casete pentru a construi o pereche șablon și samplex. Vei vedea cum fiecare casetă este transformată într-un „sandwich” de bariere care include fragmente rz-sx-rz-sx-rz pentru a implementa îmbrăcarea.

template, samplex = build(circuit)

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

../_images/ec8456d0d0a5928c5d056184cbd1842dcf7aefdc7cdba428f62092b27646d601.png

Reprezintă grafic DAG-ul samplex și plasează cursorul peste noduri pentru a vedea următoarele:

  • Toate nodurile de eșantionare (stelele) sunt responsabile de generarea randomizărilor pentru twirling, de eșantionarea din modele de zgomot sau de injectarea schimbărilor de bază.

  • Toate nodurile de colectare (papioanele) sunt responsabile de redarea secțiunilor de ieșire, care sunt inversări de măsurători sau unghiuri de parametri.

  • Toate nodurile intermediare (cercurile) sunt responsabile de diverse tipuri de manipulare a datelor.

samplex.draw()

Interoghează intrările necesare și ieșirile așteptate​

Cea mai simplă modalitate de a vedea intrările necesare și ieșirile așteptate este să afișezi obiectul Samplex. Elementele de tip array sunt formatate ca '{name}' <{type}[{shape}...]>, iar intrările necesare precedă intrările opționale.

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() și outputs() ale samplex-ului, precum și informații mai detaliate, cum ar fi numele, formele, tipurile și descrierile lor, pot fi interogate și programatic. Tipul returnat de ambele metode este TensorInterface. De exemplu, următorul cod listează toate specificațiile interfeței de intrare:

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

Exemplul următor afișează câteva informații despre acei specificatori de ieșire ale căror nume conțin șirul "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

Leagă valorile de intrare și eșantionează​

Pentru a apela sample(), samplex-ul are nevoie de valori cel puțin pentru acele inputs() marcate ca necesare. Aceste valori sunt legate mai întâi de o interfață de intrare, astfel încât aceasta să poată efectua toate verificările de tip necesare (sau, în unele cazuri, conversia de tip) și analiza formelor. Dacă ceva nu este în regulă cu tipurile sau formele, sau dacă lipsește o valoare necesară, este generată o eroare detaliată.

Leagă intrarea​

În exemplul următor, metodei sample() i se transmit valorile de intrare necesare. Apoi este folosită pentru a extrage trei randomizări din samplex. Legăturile necesare din îmbrăcări sunt hărțile Pauli Lindblad pentru injectarea zgomotului și array-ul de schimbare a bazei. În plus, sunt necesare valori pentru cei patru parametri din circuitul original cu casete, care în acest caz sunt setate la np.linspace(0, 1, 4). Samplex-ul compune acești parametri în îmbrăcări, astfel încât ei devin efectiv modificări ale valorilor parametrilor de ieșire ale samplex-ului.

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

Intrările pot fi legate de interfață prin mai multe apeluri la bind() sau prin atribuire directă a elementelor. În ciuda formatului de dicționar imbricat pe care bind() îl permite pentru comoditate, este un obiect de mapare plat și suportă toată sintaxa obișnuită de mapare. Odată ce toate intrările necesare au fost găsite, fully_bound devine adevărat.

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

Toate obiectele TensorInterface, inclusiv subclasa SamplexOutput, mapează obiecte pe datele care le-au fost legate. Aceasta înseamnă că poți obține, de exemplu, valorile parametrilor șablonului după cum urmează. În acest caz, are forma (3, 48), deoarece au fost solicitate trei randomizări, iar circuitul șablon are 48 de parametri.

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]

Specifică intrarea ca dicționar​

În loc să furnizezi un TensorInterface complet legat, poți specifica intrările ca un dicționar standard, așa cum se arată în celula de mai jos. În acest caz, metoda sample() leagă intern elementele dicționarului de interfața de intrare a samplex-ului pentru a valida că toate intrările necesare sunt prezente și compatibile.

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)

Tipuri de intrare​

În prezent, există două tipuri de valori de interfață:

  • Cu valori de tip array: Acestea au o formă și un tip de date. Valorile legate sunt convertite în tipul de date, dacă este posibil. Orice dimensiune poate fi o dimensiune liberă.

  • Hărți Pauli Lindblad: Acestea trebuie să fie de tipul qiskit.quantum_info.PauliLindbladMap și trebuie să acționeze asupra numărului prescris de qubiți. Numărul de termeni din hartă este o dimensiune liberă.

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

Dimensiuni libere​

Clasa TensorInterface are noțiunea de dimensiuni libere, adică dimensiuni întregi cu nume, ale căror valori rămân nedeterminate până când sunt legate de interfață date care le constrâng. Toate dimensiunile libere cu același nume trebuie să se rezolve la o dimensiune consecventă la legarea datelor, altfel va fi generată o eroare. De exemplu, în outputs() oricărui samplex, axa array-ului corespunzătoare randomizărilor este un parametru liber care are, prin convenție, numele "num_randomizations". Când se apelează sample(), aceasta leagă valori de ieșiri

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

Un alt scenariu comun de dimensiune liberă este partajarea constrângerilor de termeni între specificațiile hărților Pauli Lindblad și modificatorii lor. Un qiskit.quantum_info.PauliLindbladMap este o secvență ordonată de termeni, în care fiecare termen conține o rată în virgulă mobilă și o reprezentare Pauli rară. Funcția build() adaugă un specificator "pauli_lindblad_maps.<ref>" pentru acest tip pentru fiecare adnotare InjectNoise, iar dacă aceasta conține un modifier_ref, se adaugă și un specificator "local_scales.<modifier_ref>". Ambii împart dimensiunea liberă "num_terms_<ref>", care specifică câți termeni Pauli Lindblad sunt prezenți în modelul de zgomot, deoarece cel din urmă trebuie să fie asociat element cu element cu ratele celui dintâi pentru a le scala.

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

Convenția de ordonare a qubiților​

Samplex-urile construite din circuite cu casete prin funcția build() pot necesita intrări de tip array pentru sample(), unde un indice corespunde qubiților din caseta unui circuit. Această secțiune explică convenția de ordonare care trebuie folosită pentru astfel de axe. Pe scurt: folosește ordinea qubiților circuitului cu casete, restricționată la qubiții casetei.

De exemplu, să presupunem că vrei să afli cum să specifici o hartă Pauli Lindblad pentru "noise1". Atunci ordinea qubiților în harta de zgomot este în raport cu acești qubiți:

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>]

Adică, dacă vrei ca <Qubit register=(4, "q0"), index=2> să fie zgomotos cu zgomot X, atunci ar trebui să definești o hartă Pauli Lindblad ca aceasta, deoarece se află la indicele 1 din 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)>

Același lucru este valabil și pentru schimbările de bază. Dacă vrei să rotești în stările proprii +1 ale operatorilor X, Y și Z pentru qubiții 0, 1 și 2 ai lui circuit, respectiv, atunci ar trebui să folosești următorul array, amintindu-ți că în această bibliotecă (și în Qiskit) se folosește convenția I=0I=0, Z=1Z=1, X=2X=2 și Y=3Y=3.

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

Reține că convenția de ordonare a qubiților nu este ordinea definită de niciuna dintre următoarele:

  • box_instruction.qubits, unde box_instruction este un qiskit.circuit.CircuitInstruction a cărui operație este un qiskit.circuit.BoxOp, deoarece ordinea acestei liste de qubiți ar putea depinde de ordinea în care au fost adăugate instrucțiunile în contextul casetei, ceea ce ar fi o convenție derutantă. De exemplu, with box(): circuit.x([0, 1]) și with box(): circuit.x([1, 0]) ar duce la ordonări diferite.

  • box_instruction.operation.body.qubits, deoarece acești qubiți ar putea chiar să nu apară în circuitul exterior. Qubiții din interiorul corpului propriu-zis al instrucțiunilor BoxOp (sau al oricărei alte operații de control al fluxului, precum IfElseOp) au garantat un înțeles consecvent doar în acel domeniu. Aceasta ar fi o convenție prost definită și greu de folosit în general.

Testare locală cu circuitul șablon​

Dacă un Samplex este construit folosind funcția build(), valorile parametrilor de ieșire ar trebui să fie direct compatibile cu parametrii circuitului șablon asociat. De exemplu, poți lega valorile pentru una dintre randomizări de șablon, pentru inspecție locală.

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

Acest lucru îți permite să verifici că samplex-ul produce eșantioane conforme cu așteptările. De exemplu, următorul cod convertește circuitul legat într-un obiect qiskit.quantum_info.Operator și îl compune cu Pauli-ii de inversare de biți pentru a inspecta vizual că toate cele trei randomizări sunt logic echivalente cu circuitul de bază.

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

În mod similar, următorul cod transmite valorile parametrilor de ieșire și șablonul unui Sampler pentru a simula 10.000 de shot-uri ale tuturor randomizărilor și compară valorile medii rezultate (doar ale registrului alpha) cu valorile medii ale unei simulări a circuitului de bază.

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 ]