Samplex-Ein- und Ausgänge
Einführung
Samplex ist der Kern-Typ der Samplomatic-Bibliothek. Ein Samplex stellt eine parametrische Wahrscheinlichkeitsverteilung über die Parameter einer bestimmten Vorlagenschaltung dar und enthält weitere Felder mit Array-Werten, die bei der Nachbearbeitung der Daten verwendet werden, die bei der Ausführung der gebundenen Vorlagenschaltung erfasst wurden. Die zentrale Schnittstelle ist die Methode sample() , die aus dieser Verteilung eine Sammlung von Arrays generiert.
In dieser Anleitung wird beschrieben, wie man die sample() Schnittstelle nutzt, wie man erforderliche Eingaben abfragt und angibt und wie man erwartete Ausgaben überprüft. Die an übergebenen und von zurückgegebenen Arrays sind sample() stark typisiert. Das heißt, bei einer Instanz eines Samplexes liegen deren Namen, Typen und Formen bereits vor der Stichprobenziehung fest und können abgefragt werden. Die in einem Samplex vorhandenen Ein- und Ausgänge hängen von vielen Faktoren ab. Beispielsweise, ob ein Mess-Twirling vorliegt und ob eine Rauscheinjektion erforderlich ist. Aus diesem Grund ist im Allgemeinen nicht zu erwarten, dass zwei Samplex-Instanzen dieselben Ein- und Ausgänge haben.
Einrichtung
Erstellen Sie zunächst eine in einem Gehäuse untergebrachte Schaltung und bauen Sie diese in ein Samplex- und Template-Paar ein. Diese werden in den folgenden Beispielen verwendet. Beachten Sie, dass in dieser Anleitung beim manuellen Einrahmen der Schaltung die Bezeichnungen alpha, beta, ref1, ref2, mod_ref1, mod_ref2, mod_ref3, und verwendet werden conclude. Außerdem umfasst die Schaltung vier Objekte Parameter . In diesem Leitfaden wird erläutert, welche Rolle die einzelnen Komponenten bei den Ein- und Ausgängen des Samplex spielen. Informationen zum automatischen Einpacken von Schaltkreisen finden Sie im Transpiler-Handbuch.
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")
Rufen Sie anschließend die Funktion für build() die umrahmte Schaltung auf, um eine Vorlage und ein Samplex-Paar zu erstellen. Sie werden sehen, wie aus jedem der Kästen ein Barriere-„Sandwich“ entsteht, das Fragmente rz-sx-rz-sx-rz zur Umsetzung der Verbandtechnik enthält.
template, samplex = build(circuit)
template.draw("mpl", fold=100)
Stellen Sie den Samplex-DAG grafisch dar und bewegen Sie den Mauszeiger über die Knoten, um Folgendes zu sehen:
- Alle Abtastknoten (Sterne) sind dafür zuständig, Zufallswerte für das Twirling zu erzeugen, Werte aus Rauschmodellen abzutasten oder Basisänderungen einzufügen.
- Alle Erfassungsknoten (Schmetterlingsknoten) sind für die Darstellung von Ausgabeschnitten zuständig, bei denen es sich um Messumkehrungen oder Parameterwinkel handelt.
- Alle Zwischenknoten (Kreise) sind für verschiedene Arten der Datenbearbeitung zuständig.
samplex.draw()Ermitteln Sie die erforderlichen Eingaben und die erwarteten Ausgaben
Am einfachsten lässt sich die erforderlichen Eingaben und die erwarteten Ausgaben anzeigen, indem man das Objekt Samplex ausgibt. Array-Elemente werden als formatiert '{name}' <{type}[{shape}...]>, wobei die obligatorischen Eingaben vor den optionalen Eingaben stehen.
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.
Die und inputs() outputs() des Samplex sowie detailliertere Informationen wie deren Namen, Formen, Typen und Beschreibungen können ebenfalls programmgesteuert abgefragt werden. Der Rückgabetyp dieser beiden Methoden ist ein TensorInterface. Der folgende Code listet beispielsweise alle Spezifikationen der Eingabeschnittstelle auf:
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)]
Das nächste Beispiel gibt einige Informationen zu den Ausgabespekifikatoren aus, deren Namen die Zeichenfolge enthalten "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
Eingabewerte zuweisen und Beispiel anzeigen
Damit der Samplex berechnet werden kann sample(), benötigt er Werte für mindestens diejenigen Variablen, inputs() die als erforderlich gekennzeichnet sind. Diese Werte werden zunächst an eine Eingabeschnittstelle gebunden, damit diese alle erforderlichen Typüberprüfungen (oder in manchen Fällen Typumwandlungen) sowie die Formanalyse durchführen kann. Wenn bei den Typen oder Formen etwas nicht stimmt oder ein erforderlicher Wert fehlt, wird eine ausführliche Fehlermeldung ausgegeben.
Eingabe binden
Im folgenden Beispiel werden der Methode sample() die erforderlichen Eingabewerte übergeben. Anschließend werden damit drei Zufallsauswahlen aus dem Samplex gezogen. Die erforderlichen Abbildungen aus den Darstellungen sind die Pauli-Lindblad-Abbildungen für die Rauscheinjektion und das Basiswechsel-Array. Zudem werden Werte für die vier Parameter der ursprünglichen Box-Schaltung benötigt, die in diesem Fall auf. eingestellt ist np.linspace(0, 1, 4). Der Samplex integriert diese Parameter in die Dressings, sodass sie im Endeffekt zu Modifikationen der Ausgabeparameterwerte des Samplex werden.
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.
>)
Eingaben können durch mehrfache Aufrufe von oder bind() durch direkte Zuweisung von Elementen an die Schnittstelle gebunden werden. Trotz des verschachtelten Wörterbuchformats, das eine bequeme Handhabung bind() ermöglicht, handelt es sich um ein flaches Mapping-Objekt, das die gesamte übliche Mapping-Syntax unterstützt. Sobald alle erforderlichen Eingaben gefunden wurden, fully_bound wird wahr.
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
Alle TensorInterface Objekte, einschließlich der Unterklasse SamplexOutput , ordnen Objekte den Daten zu, die an sie gebunden wurden. Das bedeutet, dass Sie beispielsweise die Werte der Vorlagenparameter wie folgt abrufen können. In diesem Fall hat sie die Form, da (3, 48) drei Randomisierungen angefordert wurden und die Vorlagenschaltung 48 Parameter aufweist.
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]
Geben Sie die Eingabe als Wörterbuch an
Anstatt eine vollständig gebundene Struktur anzugeben TensorInterface, können Sie die Eingaben als Standard-Wörterbuch angeben, wie in der folgenden Zelle dargestellt. In diesem Fall bindet die Methode sample() die Wörterbucheinträge intern an die Eingabeschnittstelle des Samplex, um zu überprüfen, ob alle erforderlichen Eingaben vorhanden und kompatibel sind.
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)Eingabetypen
Derzeit gibt es zwei Werttypen für Schnittstellen:
- Array-Werte: Diese haben eine Form und einen Datentyp. Gebundene Werte werden, sofern möglich, in den Datentyp umgewandelt. Jede Dimension kann eine freie Dimension sein.
- Pauli-Lindblad-Abbildungen: Diese müssen vom Typ sein und
qiskit.quantum_info.PauliLindbladMapauf die vorgeschriebene Anzahl von Qubits wirken. Die Anzahl der Begriffe in der Karte ist eine freie 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)Freie Abmessungen
Die Klasse TensorInterface verfügt über das Konzept der freien Dimensionen, bei denen es sich um benannte ganzzahlige Größen handelt, deren Werte so lange unbestimmt sind, bis der Schnittstelle, die sie einschränkt, Daten zugewiesen wurden. Alle gleichnamigen freien Dimensionen müssen beim Binden der Daten eine einheitliche Größe aufweisen, andernfalls wird ein Fehler ausgelöst. Beispielsweise ist im Fall eines outputs() beliebigen Samplexes die den Randomisierungen entsprechende Array-Achse ein freier Parameter, der "num_randomizations" gemäß Konvention den Namen trägt. Wenn aufgerufen wird sample() , werden den Ausgängen Werte zugewiesen
print("All free dimensions:", outputs.free_dimensions)
print("Current constraints:", outputs.bound_dimensions)All free dimensions: {'num_randomizations'}
Current constraints: {'num_randomizations': 3}
Ein weiteres häufiges Szenario mit freien Dimensionen ist die gemeinsame Nutzung von Termbeschränkungen zwischen Pauli-Lindblad-Map-Spezifikationen und deren Modifikatoren. A qiskit.quantum_info.PauliLindbladMap ist eine geordnete Folge von Termen, wobei jeder Term einen Gleitkommawert und eine spärliche Pauli-Darstellung enthält. Die Funktion build() fügt für jede InjectNoise Anmerkung einen Spezifizierer "pauli_lindblad_maps.<ref>" für diesen Typ hinzu, und wenn diese ein enthält modifier_ref, wird ebenfalls ein Spezifizierer "local_scales.<modifier_ref>" hinzugefügt. Beiden ist die freie Dimension gemeinsam, die "num_terms_<ref>" angibt, wie viele Pauli-Lindblad-Terme im Rauschmodell vorhanden sind, da letzteres mit den Raten der ersteren abgeglichen werden muss, um diese zu skalieren.
# 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}
Konvention zur Anordnung von Qubits
Samplexe, die mithilfe der Funktion build() aus „Boxed Circuits“ erstellt werden, können Array-wertige Eingaben für erfordern sample(), wobei ein Index den Qubits in einer Schaltkreis-Box entspricht. In diesem Abschnitt wird die Reihenfolgekonvention erläutert, die für solche Achsen verwendet werden sollte. Zusammenfassend lässt sich sagen: Verwenden Sie die Qubit-Reihenfolge des eingerahmten Schaltkreises, beschränkt auf die Qubits des Rahmens.
Nehmen wir zum Beispiel an, Sie möchten herausfinden, wie man eine Pauli-Lindblad-Abbildung für … festlegt "noise1". Dann richtet sich die Reihenfolge der Qubits in der Rauschkarte nach diesen 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>]
Das heißt, wenn Sie mit X Rauschen „Lärm <Qubit register=(4, "q0"), index=2> machen“ wollen, sollten Sie eine Pauli-Lindblad-Abbildung wie diese definieren, da sie sich am Index 1 von befindet 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)>
Das Gleiche gilt für Basisänderungen. Wenn Sie die Qubits 0, 1 und 2 jeweils in die +1-Eigenzustände der Operatoren X, Y und Z rotieren möchten circuit, sollten Sie das folgende Array verwenden, wobei zu beachten ist, dass in dieser Bibliothek (und in Qiskit) die Konvention „ “, „ “, „ “ und „ “ verwendet wird.
samplex.inputs().bind(basis_changes={"conclude": (basis_change := [2, 3, 1])})
basis_change[2, 3, 1]
Beachten Sie, dass die Konvention zur Reihenfolge der Qubits nicht der Reihenfolge entspricht, die durch eine der folgenden Möglichkeiten definiert wird:
box_instruction.qubits, wobei einbox_instructionist, dessenqiskit.circuit.CircuitInstructionOperation ein istqiskit.circuit.BoxOp, da die Reihenfolge dieser Liste von Qubits von der Reihenfolge abhängen könnte, in der Anweisungen zum Box-Kontext hinzugefügt wurden, was eine verwirrende Konvention wäre. Beispielsweisewith box(): circuit.x([1, 0])würden undwith box(): circuit.x([0, 1])zu unterschiedlichen Reihenfolgen führen.box_instruction.operation.body.qubitsdenn diese Qubits tauchen möglicherweise gar nicht erst im äußeren Schaltkreis auf. Für Qubits innerhalb des eigentlichenBoxOpBefehlskörpers (oder einer anderen Steuerung der Programmablaufs wieIfElseOp) wird nur innerhalb dieses Geltungsbereichs eine konsistente Bedeutung zugesichert. Das wäre eine unklar definierte Konvention, die sich im Allgemeinen nur schwer anwenden ließe.
Lokale Tests mit der Vorlageschaltung
Samplex Wird ein mithilfe der Funktion build() erstellt, sollten die Werte der Ausgabeparameter direkt mit den Parametern der zugehörigen Vorlagenschaltung kompatibel sein. Sie können beispielsweise Werte für eine der Randomisierungen an die Vorlage binden, um sie lokal zu überprüfen.
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)
Auf diese Weise können Sie überprüfen, ob der Samplex die erwarteten Samples ausgibt. Der folgende Code wandelt beispielsweise die gebundene Schaltung in ein Objekt qiskit.quantum_info.Operator um und kombiniert sie mit den Bitflip-Paulis, um visuell zu überprüfen, ob alle drei Randomisierungen logisch äquivalent zur Basisschaltung sind.
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()
Ebenso übergibt der folgende Code die Werte der Ausgabeparameter und die Vorlage an einen Sampler, um 10.000 Durchläufe aller Randomisierungen zu simulieren, und vergleicht die daraus resultierenden Erwartungswerte (nur des Alpha-Registers) mit den Erwartungswerten einer Basisschaltungssimulation.
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 ]