Wejścia i wyjścia Samplex
Wprowadzenie
Samplex jest podstawowym typem biblioteki samplomatic. Samplex reprezentuje parametryczny rozkład prawdopodobieństwa nad parametrami pewnego obwodu szablonowego, wraz z innymi polami tablicowymi używanymi przy przetwarzaniu końcowym danych zebranych z wykonania związanego obwodu szablonowego. Jego głównym interfejsem jest metoda sample(), która losuje z tego rozkładu, tworząc zbiór tablic.
Ten przewodnik opisuje, jak używać interfejsu sample(), jak sprawdzać i określać wymagane wejścia oraz jak analizować oczekiwane wyjścia. Tablice przekazywane do sample() i z niego zwracane są silnie typowane. Oznacza to, że dla danej instancji samplexa ich nazwy, typy i kształty są ustalone i można je sprawdzić przed rozpoczęciem próbkowania. Wejścia i wyjścia obecne w samplexie zależą od wielu czynników, na przykład od tego, czy występuje twirling pomiarów i czy wymagane jest wstrzykiwanie szumu. Z tego powodu na ogół nie należy oczekiwać, że dwie instancje samplexa mają takie same wejścia i wyjścia.
Konfiguracja
Najpierw skonstruuj obwód podzielony na boxy i zbuduj z niego parę samplex i szablon. Będą one używane w kolejnych przykładach. Zwróć uwagę, że przy ręcznym dzieleniu obwodu na boxy ten przewodnik używa nazw alpha, beta, ref1, ref2, mod_ref1, mod_ref2, mod_ref3 i conclude. Ponadto obwód ma cztery obiekty Parameter. Przewodnik pokazuje, jaką rolę każdy z nich odgrywa w wejściach i wyjściach samplexa. Informacje o automatycznym dzieleniu obwodów na boxy znajdziesz w przewodniku o transpilerze.
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")
Następnie wywołaj build() na obwodzie z boxami, aby zbudować parę szablon i samplex. Zobaczysz, jak każdy z boxów zamienia się w „kanapkę” barier zawierającą fragmenty rz-sx-rz-sx-rz implementujące dressing.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
Narysuj DAG samplexa i najedź kursorem na węzły, aby zobaczyć następujące informacje:
-
Wszystkie węzły próbkowania (gwiazdy) odpowiadają za generowanie randomizacji do twirlingu, próbkowanie z modeli szumu lub wstrzykiwanie zmian bazy.
-
Wszystkie węzły zbierające (muszki) odpowiadają za renderowanie wycinków wyjściowych, którymi są odwrócenia bitów w pomiarach lub kąty parametrów.
-
Wszystkie węzły pośrednie (kółka) odpowiadają za różne rodzaje manipulacji danymi.
samplex.draw()
Sprawdzanie wymaganych wejść i oczekiwanych wyjść
Najłatwiejszym sposobem zobaczenia wymaganych wejść i oczekiwanych wyjść jest wypisanie obiektu Samplex. Elementy tablicowe są formatowane jako '{name}' <{type}[{shape}...]>, a wymagane wejścia poprzedzają wejścia opcjonalne.
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() samplexa, a także bardziej szczegółowe informacje, takie jak ich nazwy, kształty, typy i opisy, można również odpytywać programowo. Typem zwracanym przez obie te metody jest TensorInterface. Na przykład poniższy kod wypisuje wszystkie specyfikacje interfejsu wejściowego:
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)]
Następny przykład wypisuje informacje o tych specyfikatorach wyjść, których nazwy zawierają ciąg "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
Wiązanie wartości wejściowych i próbkowanie
Aby wykonać sample(), samplex potrzebuje wartości przynajmniej dla tych inputs(), które są oznaczone jako wymagane. Wartości te są najpierw wiązane z interfejsem wejściowym, aby mógł on przeprowadzić całą niezbędną kontrolę typów (lub w niektórych przypadkach koercję typów) oraz analizę kształtów. Jeśli typy lub kształty są nieprawidłowe albo brakuje wymaganej wartości, zgłaszany jest szczegółowy błąd.
Wiązanie wejścia
W poniższym przykładzie do metody sample() przekazywane są wymagane wartości wejściowe. Następnie służy ona do wylosowania z samplexa trzech randomizacji. Wymagane powiązania z dressingów to mapy Pauli Lindblada do wstrzykiwania szumu oraz tablica zmiany bazy. Dodatkowo wymagane są wartości czterech parametrów w oryginalnym obwodzie z boxami, które w tym przypadku ustawiono na np.linspace(0, 1, 4). Samplex składa te parametry z dressingami, więc skutecznie stają się one modyfikacjami wartości parametrów wyjściowych samplexa.
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.
>)
Wejścia można wiązać z interfejsem przez wielokrotne wywołania bind() lub przez bezpośrednie przypisanie elementów. Mimo zagnieżdżonego formatu słownika, który bind() dopuszcza dla wygody, jest to płaski obiekt mapujący, który obsługuje całą zwykłą składnię map. Gdy wszystkie wymagane wejścia zostaną znalezione, fully_bound przyjmuje wartość prawda.
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
Wszystkie obiekty TensorInterface, w tym podklasa SamplexOutput, mapują obiekty na dane, które zostały z nimi związane. Oznacza to, że możesz na przykład pobrać wartości parametrów szablonu w następujący sposób. W tym przypadku ma ona kształt (3, 48), ponieważ zażądano trzech randomizacji, a obwód szablonowy ma 48 parametrów.
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]
Określanie wejścia jako słownika
Zamiast podawać w pełni związany TensorInterface, możesz określić wejścia jako standardowy słownik, jak pokazano w poniższej komórce. W tym przypadku metoda sample() wewnętrznie wiąże elementy słownika z interfejsem wejściowym samplexa, aby sprawdzić, czy wszystkie wymagane wejścia są obecne i zgodne.
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)
Typy wejść
Obecnie istnieją dwa typy wartości interfejsu:
-
Wartości tablicowe: mają kształt i typ danych. Powiązane wartości są w miarę możliwości rzutowane na ten typ danych. Dowolny wymiar może być wymiarem swobodnym.
-
Mapy Pauli Lindblada: muszą być typu
qiskit.quantum_info.PauliLindbladMapi muszą działać na określonej liczbie kubitów. Liczba wyrazów w mapie jest wymiarem swobodnym.
# 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)
Wymiary swobodne
Klasa TensorInterface ma pojęcie wymiarów swobodnych, czyli nazwanych rozmiarów całkowitych, których wartości pozostają nieokreślone, dopóki do interfejsu nie zostaną związane dane, które je ograniczają. Wszystkie wymiary swobodne o tej samej nazwie muszą przy wiązaniu danych dać spójny rozmiar, w przeciwnym razie zostanie zgłoszony błąd. Na przykład w outputs() dowolnego samplexa oś tablicy odpowiadająca randomizacjom jest parametrem swobodnym o nazwie "num_randomizations" z konwencji. Gdy wywoływane jest sample(), wiąże ono wartości z wyjściami
print("All free dimensions:", outputs.free_dimensions)
print("Current constraints:", outputs.bound_dimensions)
All free dimensions: {'num_randomizations'}
Current constraints: {'num_randomizations': 3}
Innym częstym scenariuszem wymiaru swobodnego jest współdzielenie ograniczeń liczby wyrazów między specyfikacjami map Pauli Lindblada a ich modyfikatorami. qiskit.quantum_info.PauliLindbladMap jest uporządkowaną sekwencją wyrazów, z których każdy zawiera współczynnik zmiennoprzecinkowy i rzadką reprezentację Pauliego. Funkcja build() dodaje specyfikator "pauli_lindblad_maps.<ref>" tego typu dla każdej adnotacji InjectNoise, a jeśli zawiera ona modifier_ref, dodawany jest także specyfikator "local_scales.<modifier_ref>". Oba współdzielą wymiar swobodny "num_terms_<ref>", który określa, ile wyrazów Pauli Lindblada jest obecnych w modelu szumu, ponieważ drugi z nich musi być zestawiony z częstościami pierwszego, aby je przeskalować.
# 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}
Konwencja kolejności kubitów
Samplexy konstruowane z obwodów z boxami przez funkcję build() mogą wymagać wejść tablicowych dla sample(), w których jeden indeks odpowiada kubitom w boxie obwodu. Ta sekcja wyjaśnia konwencję kolejności, której należy używać dla takich osi. W skrócie: użyj kolejności kubitów obwodu z boxami, ograniczonej do kubitów boxa.
Załóżmy na przykład, że chcesz ustalić, jak określić mapę Pauli Lindblada dla "noise1". Wtedy kolejność kubitów w mapie szumu odnosi się do tych kubitów:
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>]
Oznacza to, że jeśli chcesz, aby <Qubit register=(4, "q0"), index=2> był zaszumiony szumem X, powinieneś zdefiniować mapę Pauli Lindblada taką jak ta, ponieważ znajduje się on pod indeksem 1 w 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)>
To samo dotyczy zmian bazy. Jeśli chcesz obrócić kubity 0, 1 i 2 z circuit odpowiednio do stanów własnych +1 operatorów X, Y i Z, powinieneś użyć poniższej tablicy, pamiętając, że w tej bibliotece (i w Qiskit) obowiązuje konwencja , , i .
samplex.inputs().bind(basis_changes={"conclude": (basis_change := [2, 3, 1])})
basis_change
[2, 3, 1]
Zwróć uwagę, że konwencja kolejności kubitów nie jest kolejnością zdefiniowaną przez żadne z poniższych:
-
box_instruction.qubits, gdziebox_instructiontoqiskit.circuit.CircuitInstruction, której operacją jestqiskit.circuit.BoxOp, ponieważ kolejność tej listy kubitów może zależeć od kolejności, w jakiej instrukcje zostały dodane do kontekstu boxa, co byłoby mylącą konwencją. Na przykładwith box(): circuit.x([0, 1])iwith box(): circuit.x([1, 0])dałyby różne kolejności. -
box_instruction.operation.body.qubits, ponieważ te kubity mogą w ogóle nie występować w obwodzie zewnętrznym. Kubity wewnątrz właściwego ciała instrukcjiBoxOp(lub dowolnej innej operacji przepływu sterowania, takiej jakIfElseOp) mają gwarantowane spójne znaczenie tylko w obrębie tego zakresu. Byłaby to źle zdefiniowana konwencja, trudna w użyciu w ogólnym przypadku.
Testowanie lokalne z obwodem szablonowym
Jeśli Samplex został skonstruowany za pomocą funkcji build(), wartości parametrów wyjściowych powinny być bezpośrednio zgodne z parametrami powiązanego obwodu szablonowego. Na przykład możesz związać z szablonem wartości jednej z randomizacji w celu lokalnej inspekcji.
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)
Pozwala to sprawdzić, czy samplex zwraca oczekiwane próbki. Na przykład poniższy kod rzutuje związany obwód na obiekt qiskit.quantum_info.Operator i składa go z bramkami Pauliego odwrócenia bitów, aby wizualnie sprawdzić, że wszystkie trzy randomizacje są logicznie równoważne obwodowi bazowemu.
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()

Podobnie poniższy kod przekazuje wartości parametrów wyjściowych i szablon do samplera, aby zasymulować 10 000 strzałów wszystkich randomizacji, i porównuje uzyskane wartości oczekiwane (tylko rejestru alpha) z wartościami oczekiwanymi symulacji obwodu bazowego.
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 ]

