Mga input at output ng Samplex
Panimula
Ang Samplex ang pangunahing uri ng samplomatic library. Kumakatawan ang isang samplex sa isang parametric probability distribution sa mga parameter ng isang template circuit, kasama ang iba pang array-valued na field na gagamitin sa post-processing ng data na nakolekta mula sa pag-execute ng nabind na template circuit. Ang sentral nitong interface ay ang method na sample() na kumukuha mula sa distribution na ito para makabuo ng koleksyon ng mga array.
Inilalarawan ng gabay na ito kung paano gamitin ang interface ng sample(), kung paano mag-query at magtukoy ng mga kinakailangang input, at kung paano suriin ang mga inaasahang output. Ang mga array na ibinibigay sa at ibinabalik ng sample() ay strongly typed. Ibig sabihin, para sa isang instance ng samplex, nakapirmi at maaaring i-query ang mga pangalan, uri, at hugis ng mga ito bago pa magsagawa ng sampling. Ang mga input at output na nasa isang samplex ay nakadepende sa maraming salik. Halimbawa, kung may measurement twirling at kung kailangan ang noise injection. Dahil dito, sa pangkalahatan, hindi inaasahang magkapareho ang mga input at output ng dalawang samplex instance.
Pag-setup
Una, bumuo ng boxed-up na circuit at i-build ito bilang pares ng samplex at template. Gagamitin ang mga ito sa mga susunod na halimbawa. Tandaan na kapag mano-manong nagba-box ng circuit, ginagamit ng gabay na ito ang mga pangalang alpha, beta, ref1, ref2, mod_ref1, mod_ref2, mod_ref3, at conclude. Bukod dito, ang circuit ay may apat na Parameter object. Ipinapakita ng gabay na ito kung paano gumaganap ng papel ang bawat isa sa mga input at output ng samplex. Tingnan ang gabay sa transpiler para sa impormasyon tungkol sa awtomatikong pag-box ng mga circuit.
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")
Susunod, tawagin ang build() sa boxed circuit para bumuo ng pares ng template at samplex. Makikita mo kung paano ginagawang barrier na “sandwich” ang bawat box na may kasamang mga fragment na rz-sx-rz-sx-rz para ipatupad ang dressing.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
I-plot ang samplex DAG at i-hover ang mga node para makita ang sumusunod:
-
Lahat ng sampling node (mga bituin) ay responsable sa paglikha ng mga randomization para sa twirling, pag-sample mula sa mga noise model, o pag-inject ng mga basis change.
-
Lahat ng collection node (mga bow tie) ay responsable sa pag-render ng mga output slice, na mga measurement flip o mga anggulo ng parameter.
-
Lahat ng intermediate node (mga bilog) ay responsable sa iba't ibang uri ng pagmamanipula ng data.
samplex.draw()
I-query ang mga kinakailangang input at inaasahang output
Ang pinakamadaling paraan para makita ang mga kinakailangang input at inaasahang output ay i-print ang Samplex object. Ang mga array item ay naka-format bilang '{name}' <{type}[{shape}...]>, at nauuna ang mga kinakailangang input bago ang mga opsyonal na input.
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.
Ang inputs() at outputs() ng samplex, pati ang mas detalyadong impormasyon tulad ng mga pangalan, hugis, uri, at paglalarawan ng mga ito, ay maaari ring i-query nang programmatic. Ang return type ng dalawang method na ito ay isang TensorInterface. Halimbawa, inililista ng sumusunod na code ang lahat ng specification ng input interface:
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)]
Ipi-print ng susunod na halimbawa ang ilang impormasyon tungkol sa mga output specifier na ang mga pangalan ay naglalaman ng string na "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
I-bind ang mga input value at mag-sample
Para ma-sample(), kailangan ng samplex ng mga value para sa hindi bababa sa mga inputs() na minarkahang kinakailangan. Ang mga value na ito ay unang bina-bind sa isang input interface para maisagawa nito ang lahat ng kinakailangang type checking (o type coercion sa ilang kaso) at shape analysis. Kung may mali sa mga uri o hugis, o kung may kulang na kinakailangang value, magtataas ng verbose na error.
I-bind ang input
Sa sumusunod na halimbawa, ipinapasa sa method na sample() ang mga kinakailangang input value. Ginagamit ito pagkatapos para kumuha ng tatlong randomization mula sa samplex. Ang mga kinakailangang binding mula sa mga dressing ay ang mga Pauli Lindblad map para sa noise injection at ang basis change array. Bukod dito, kailangan ang mga value para sa apat na parameter sa orihinal na boxed circuit, na sa kasong ito ay itinakda sa np.linspace(0, 1, 4). Isinasama ng samplex ang mga parameter na ito sa mga dressing, kaya epektibo silang nagiging mga pagbabago sa mga output parameter value ng 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.
>)
Maaaring i-bind ang mga input sa interface sa pamamagitan ng maraming tawag sa bind() o sa direktang item assignment. Bagama't pinapayagan ng bind() ang nested dictionary format para sa kaginhawahan, isa itong flat mapping object, at sinusuportahan nito ang lahat ng karaniwang mapping syntax. Kapag nahanap na ang lahat ng kinakailangang input, magiging true ang fully_bound.
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
Lahat ng TensorInterface object, kasama ang subclass na SamplexOutput, ay nagma-map ng mga object laban sa data na nabind sa mga ito. Ibig sabihin, makukuha mo, halimbawa, ang mga template parameter value gaya ng sumusunod. Sa kasong ito, ang hugis nito ay (3, 48) dahil tatlong randomization ang hiniling at ang template circuit ay may 48 parameter.
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]
Tukuyin ang input bilang dictionary
Sa halip na magbigay ng ganap na nabind na TensorInterface, maaari mong tukuyin ang mga input bilang karaniwang dictionary, gaya ng ipinapakita sa cell sa ibaba. Sa kasong ito, internal na bina-bind ng method na sample() ang mga item ng dictionary sa input interface ng samplex para i-validate na naroroon at compatible ang lahat ng kinakailangang input.
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)
Mga uri ng input
Sa kasalukuyan, may dalawang uri ng interface value:
-
Array-valued: May hugis at data type ang mga ito. Ang mga bound na value ay kino-coerce sa data type kung posible. Anumang dimensyon ay maaaring maging free dimension.
-
Mga Pauli Lindblad map: Dapat ay may uri na
qiskit.quantum_info.PauliLindbladMapat dapat umaksyon sa itinakdang bilang ng mga qubit. Ang bilang ng mga term sa map ay isang free dimension.
# 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)
Mga free dimension
Ang class na TensorInterface ay may konsepto ng mga free dimension, na mga pinangalanang integer size na ang mga value ay hindi pa tiyak hangga't walang data na nabind sa interface na naglilimita sa mga ito. Lahat ng free dimension na magkapareho ang pangalan ay dapat mag-resolve sa pare-parehong laki kapag nagbi-bind ng data, kung hindi ay magtataas ng error. Halimbawa, sa outputs() ng anumang samplex, ang array axis na tumutugma sa mga randomization ay isang free parameter na may pangalang "num_randomizations" ayon sa kumbensyon. Kapag tinawag ang sample(), nagbi-bind ito ng mga value sa mga output
print("All free dimensions:", outputs.free_dimensions)
print("Current constraints:", outputs.bound_dimensions)
All free dimensions: {'num_randomizations'}
Current constraints: {'num_randomizations': 3}
Isa pang karaniwang free-dimension na sitwasyon ay ang pagbabahagi ng mga term constraint sa pagitan ng mga specification ng Pauli lindblad map at ng mga modifier ng mga ito. Ang qiskit.quantum_info.PauliLindbladMap ay isang ordered na sequence ng mga term, kung saan bawat term ay may floating point na rate at sparse na Pauli representation. Nagdaragdag ang function na build() ng specifier na "pauli_lindblad_maps.<ref>" para sa uring ito para sa bawat InjectNoise annotation, at kung may modifier_ref ito, may idinaragdag ding specifier na "local_scales.<modifier_ref>". Parehong ibinabahagi ng dalawang ito ang free dimension na "num_terms_<ref>" na tumutukoy kung ilang Pauli Lindblad term ang nasa noise model dahil kailangang i-zip ng huli laban sa mga rate ng una para i-scale ang mga ito.
# 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}
Kumbensyon sa pagkakasunod ng mga qubit
Ang mga samplex na binuo mula sa mga boxed circuit gamit ang function na build() ay maaaring mangailangan ng array-valued na mga input sa sample(), kung saan ang isang index ay tumutugma sa mga qubit sa box ng circuit. Ipinapaliwanag ng seksyong ito ang kumbensyon sa pagkakasunod na dapat gamitin para sa mga ganoong axis. Sa madaling sabi: gamitin ang pagkakasunod ng qubit ng boxed circuit, na limitado sa mga qubit ng box.
Halimbawa, sabihin nating gusto mong alamin kung paano tukuyin ang isang Pauli Lindblad map para sa "noise1". Ang pagkakasunod ng mga qubit sa noise map ay batay sa mga qubit na ito:
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>]
Ibig sabihin, kung gusto mong maging noisy ang <Qubit register=(4, "q0"), index=2> na may X noise, dapat kang magtakda ng Pauli Lindblad map na tulad nito dahil nasa index na 1 ito ng 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)>
Ganoon din ang totoo para sa mga basis change. Kung gusto mong i-rotate sa mga +1 eigenstate ng mga operator na X, Y, at Z para sa mga qubit 0, 1, at 2 ng circuit, ayon sa pagkakasunod, dapat mong gamitin ang sumusunod na array, na tandaan ang kumbensyong , , , at na ginagamit sa library na ito (at sa Qiskit).
samplex.inputs().bind(basis_changes={"conclude": (basis_change := [2, 3, 1])})
basis_change
[2, 3, 1]
Tandaan na ang kumbensyon sa pagkakasunod ng qubit ay hindi ang pagkakasunod na tinutukoy ng alinman sa mga sumusunod:
-
box_instruction.qubits, kung saan angbox_instructionay isangqiskit.circuit.CircuitInstructionna ang operasyon ay isangqiskit.circuit.BoxOp, dahil ang pagkakasunod ng listahang ito ng mga qubit ay maaaring nakadepende sa pagkakasunod ng pagdaragdag ng mga instruction sa box context, na magiging nakalilitong kumbensyon. Halimbawa, angwith box(): circuit.x([0, 1])atwith box(): circuit.x([1, 0])ay magreresulta sa magkaibang pagkakasunod. -
box_instruction.operation.body.qubitsdahil maaaring hindi man lang lumitaw ang mga qubit na ito sa panlabas na circuit. Ang mga qubit sa loob ng aktwal na body ng mgaBoxOpinstruction (o anumang iba pang control flow operation tulad ngIfElseOp) ay ipinapangakong may pare-parehong kahulugan lamang sa loob ng scope na iyon. Magiging ill-defined ang kumbensyong ito, at mahirap gamitin sa pangkalahatan.
Lokal na pagsubok gamit ang template circuit
Kung ang isang Samplex ay binuo gamit ang function na build(), ang mga output parameter value ay dapat direktang compatible sa mga parameter ng kaugnay na template circuit. Halimbawa, maaari kang mag-bind ng mga value para sa isa sa mga randomization sa template para sa lokal na inspeksyon.
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)
Pinapayagan ka nitong i-verify na ang samplex ay naglalabas ng mga sample na inaasahan. Halimbawa, kino-cast ng sumusunod na code ang bound circuit sa isang qiskit.quantum_info.Operator object at kino-compose sa mga bitflip na Pauli para biswal na suriin na lahat ng tatlong randomization ay lohikal na katumbas ng base circuit.
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()

Gayundin, ipinapasa ng sumusunod na code ang mga output parameter value at ang template sa isang sampler para i-simulate ang 10,000 shot ng lahat ng randomization, at ikinukumpara ang mga nagresultang expectation value (ng alpha register lang) sa mga expectation value ng simulation ng base circuit.
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 ]

