Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

Samplex-Eingaben und -Ausgaben

Einführung​

Samplex ist der zentrale Typ der Samplomatic-Bibliothek. Ein Samplex stellt eine parametrische Wahrscheinlichkeitsverteilung über die Parameter eines Template-Circuits dar, zusammen mit weiteren arraywertigen Feldern, die bei der Nachverarbeitung der Daten aus der Ausführung des gebundenen Template-Circuits verwendet werden. Seine zentrale Schnittstelle ist die Methode sample(), die aus dieser Verteilung zieht und eine Sammlung von Arrays erzeugt.

Diese Anleitung beschreibt, wie du die Schnittstelle sample() verwendest, wie du die erforderlichen Eingaben abfragst und angibst und wie du die erwarteten Ausgaben untersuchst. Die an sample() übergebenen und von der Methode zurückgegebenen Arrays sind streng typisiert. Das heißt, für eine Samplex-Instanz sind ihre Namen, Typen und Shapes festgelegt und vor dem Sampling abfragbar. Welche Eingaben und Ausgaben ein Samplex hat, hängt von vielen Faktoren ab, zum Beispiel davon, ob Measurement-Twirling vorhanden ist und ob Noise-Injection erforderlich ist. Aus diesem Grund haben zwei Samplex-Instanzen im Allgemeinen nicht dieselben Eingaben und Ausgaben.

Einrichtung​

Konstruiere zunächst einen geboxten Circuit und baue daraus ein Paar aus Samplex und Template. Diese werden in den folgenden Beispielen verwendet. Beachte, dass diese Anleitung beim manuellen Boxen des Circuits die Namen alpha, beta, ref1, ref2, mod_ref1, mod_ref2, mod_ref3 und conclude verwendet. Außerdem hat der Circuit vier Parameter-Objekte. Diese Anleitung zeigt, welche Rolle jedes davon bei den Samplex-Eingaben und -Ausgaben spielt. Informationen zum automatischen Boxen von Circuits findest du in der Transpiler-Anleitung.

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

Rufe als Nächstes build() auf dem geboxten Circuit auf, um ein Paar aus Template und Samplex zu konstruieren. Du wirst sehen, wie jede der Boxen in ein Barrier-„Sandwich“ umgewandelt wird, das rz-sx-rz-sx-rz-Fragmente zur Implementierung des Dressings enthält.

template, samplex = build(circuit)

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

../_images/ec8456d0d0a5928c5d056184cbd1842dcf7aefdc7cdba428f62092b27646d601.png

Zeichne den Samplex-DAG und bewege den Mauszeiger über die Knoten, um Folgendes zu sehen:

  • Alle Sampling-Knoten (Sterne) sind dafür zuständig, Randomisierungen für das Twirling zu erzeugen, aus Rauschmodellen zu sampeln oder Basiswechsel einzufügen.

  • Alle Collection-Knoten (Fliegen) sind dafür zuständig, Ausgabe-Slices zu rendern, also Messflips oder Parameterwinkel.

  • Alle Zwischenknoten (Kreise) sind für verschiedene Arten der Datenmanipulation zuständig.

samplex.draw()

Erforderliche Eingaben und erwartete Ausgaben abfragen​

Am einfachsten siehst du die erforderlichen Eingaben und erwarteten Ausgaben, indem du das Samplex-Objekt ausgibst. Array-Elemente sind als '{name}' <{type}[{shape}...]> formatiert, und die erforderlichen Eingaben stehen vor den optionalen Eingaben.

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 inputs() und outputs() des Samplex sowie detailliertere Informationen wie ihre Namen, Shapes, Typen und Beschreibungen lassen sich auch programmatisch abfragen. Der Rückgabetyp beider Methoden ist ein TensorInterface. Der folgende Code listet zum Beispiel 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 Ausgabe-Spezifikationen aus, deren Namen den String "flips" enthalten.

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 binden und sampeln​

Um sample() aufzurufen, benötigt der Samplex Werte für mindestens diejenigen inputs(), die als erforderlich markiert sind. Diese Werte werden zunächst an eine Eingabeschnittstelle gebunden, damit sie alle notwendigen Typprüfungen (in einigen Fällen auch Typumwandlungen) und die Shape-Analyse durchführen kann. Wenn mit den Typen oder Shapes etwas nicht stimmt oder ein erforderlicher Wert fehlt, wird ein ausführlicher Fehler ausgelöst.

Eingabe binden​

Im folgenden Beispiel werden der Methode sample() die erforderlichen Eingabewerte übergeben. Damit werden dann drei Randomisierungen aus dem Samplex gezogen. Die erforderlichen Bindungen aus den Dressings sind die Pauli-Lindblad-Maps für die Noise-Injection und das Basiswechsel-Array. Zusätzlich sind Werte für die vier Parameter des ursprünglichen geboxten Circuits erforderlich; sie werden hier auf np.linspace(0, 1, 4) gesetzt. Der Samplex komponiert diese Parameter in die Dressings, sodass sie effektiv zu Modifikationen der Ausgabe-Parameterwerte 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 über mehrere Aufrufe von bind() oder durch direkte Elementzuweisung an die Schnittstelle gebunden werden. Trotz des verschachtelten Dictionary-Formats, das bind() der Bequemlichkeit halber erlaubt, handelt es sich um ein flaches Mapping-Objekt, das die gesamte übliche Mapping-Syntax unterstützt. Sobald alle erforderlichen Eingaben gefunden wurden, wird fully_bound 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, bilden Namen auf die an sie gebundenen Daten ab. Das bedeutet, dass du zum Beispiel die Template-Parameterwerte wie folgt abrufen kannst. In diesem Fall hat das Ergebnis die Shape (3, 48), da drei Randomisierungen angefordert wurden und der Template-Circuit 48 Parameter hat.

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]

Eingabe als Dictionary angeben​

Anstatt ein vollständig gebundenes TensorInterface bereitzustellen, kannst du die Eingaben als gewöhnliches Dictionary angeben, wie in der folgenden Zelle gezeigt. In diesem Fall bindet die Methode sample() die Dictionary-Elemente intern an die Eingabeschnittstelle des Samplex, um zu prüfen, dass 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 Typen von Schnittstellenwerten:

  • Arraywertig: Diese haben eine Shape und einen Datentyp. Gebundene Werte werden, wenn möglich, in den Datentyp umgewandelt. Jede Dimension kann eine freie Dimension sein.

  • Pauli-Lindblad-Maps: Diese müssen vom Typ qiskit.quantum_info.PauliLindbladMap sein und auf die vorgeschriebene Anzahl von Qubits wirken. Die Anzahl der Terme in der Map 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 Dimensionen​

Die Klasse TensorInterface kennt den Begriff der freien Dimensionen. Das sind benannte ganzzahlige Größen, deren Werte unbestimmt bleiben, bis Daten an die Schnittstelle gebunden werden, die sie einschränken. Alle freien Dimensionen mit demselben Namen müssen beim Binden von Daten auf eine konsistente Größe aufgelöst werden, sonst wird ein Fehler ausgelöst. Zum Beispiel ist in den outputs() eines beliebigen Samplex die Array-Achse, die den Randomisierungen entspricht, ein freier Parameter, der per Konvention den Namen "num_randomizations" trägt. Wenn sample() aufgerufen wird, bindet es Werte an die Ausgaben

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 das gemeinsame Nutzen von Term-Constraints zwischen Pauli-Lindblad-Map-Spezifikationen und ihren Modifikatoren. Eine qiskit.quantum_info.PauliLindbladMap ist eine geordnete Folge von Termen, wobei jeder Term eine Gleitkomma-Rate und eine dünnbesetzte Pauli-Darstellung enthält. Die Funktion build() fügt für jede InjectNoise-Annotation einen Spezifizierer "pauli_lindblad_maps.<ref>" für diesen Typ hinzu, und wenn sie ein modifier_ref enthält, wird außerdem ein Spezifizierer "local_scales.<modifier_ref>" hinzugefügt. Beide teilen sich die freie Dimension "num_terms_<ref>", die angibt, wie viele Pauli-Lindblad-Terme im Rauschmodell vorhanden sind, da Letzterer mit den Raten des Ersteren gezippt werden muss, um sie 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 für die Qubit-Reihenfolge​

Samplexes, die von der Funktion build() aus geboxten Circuits konstruiert werden, können arraywertige Eingaben für sample() erfordern, bei denen ein Index den Qubits in der Box eines Circuits entspricht. Dieser Abschnitt erklärt die Reihenfolgenkonvention, die für solche Achsen verwendet werden sollte. Kurz gesagt: Verwende die Qubit-Reihenfolge des geboxten Circuits, eingeschränkt auf die Qubits der Box.

Angenommen, du möchtest herausfinden, wie du eine Pauli-Lindblad-Map für "noise1" angibst. Dann bezieht sich die Reihenfolge der Qubits in der Noise-Map auf diese 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 du möchtest, dass <Qubit register=(4, "q0"), index=2> mit X-Rauschen verrauscht ist, solltest du eine Pauli-Lindblad-Map wie diese definieren, da es sich bei Index 1 von qubit_ordering_convention befindet.

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

Dasselbe gilt für Basiswechsel. Wenn du in die +1-Eigenzustände der Operatoren X, Y und Z für die Qubits 0, 1 beziehungsweise 2 von circuit rotieren möchtest, solltest du das folgende Array verwenden. Denke dabei daran, dass in dieser Bibliothek (und in Qiskit) die Konvention I=0I=0, Z=1Z=1, X=2X=2 und Y=3Y=3 gilt.

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

Beachte, dass die Konvention für die Qubit-Reihenfolge nicht die Reihenfolge ist, die durch eine der folgenden Angaben definiert wird:

  • box_instruction.qubits, wobei box_instruction eine qiskit.circuit.CircuitInstruction ist, deren Operation eine qiskit.circuit.BoxOp ist, da die Reihenfolge dieser Qubit-Liste von der Reihenfolge abhängen kann, in der Anweisungen zum Box-Kontext hinzugefügt wurden, was eine verwirrende Konvention wäre. Zum Beispiel würden with box(): circuit.x([0, 1]) und with box(): circuit.x([1, 0]) zu unterschiedlichen Reihenfolgen führen.

  • box_instruction.operation.body.qubits, da diese Qubits möglicherweise gar nicht im äußeren Circuit vorkommen. Für Qubits im eigentlichen Body von BoxOp-Anweisungen (oder jeder anderen Kontrollfluss-Operation wie IfElseOp) ist nur innerhalb dieses Scopes eine konsistente Bedeutung zugesichert. Das wäre eine schlecht definierte und im Allgemeinen schwer zu nutzende Konvention.

Lokales Testen mit dem Template-Circuit​

Wenn ein Samplex mit der Funktion build() konstruiert wird, sollten die Ausgabe-Parameterwerte direkt mit den Parametern des zugehörigen Template-Circuits kompatibel sein. Du kannst zum Beispiel Werte für eine der Randomisierungen zur lokalen Inspektion an das Template binden.

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

So kannst du überprüfen, ob der Samplex die erwarteten Samples ausgibt. Der folgende Code wandelt zum Beispiel den gebundenen Circuit in ein qiskit.quantum_info.Operator-Objekt um und komponiert es mit den Bitflip-Paulis, um visuell zu prüfen, dass alle drei Randomisierungen logisch äquivalent zum Basis-Circuit 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()

../_images/af1aa74f8a0e8abed673d4cd43ff8b810c44297d2f3a42fcfb49ade811e51c58.png

Ebenso übergibt der folgende Code die Ausgabe-Parameterwerte und das Template an einen Sampler, um 10.000 Shots aller Randomisierungen zu simulieren, und vergleicht die resultierenden Erwartungswerte (nur des alpha-Registers) mit den Erwartungswerten einer Simulation des Basis-Circuits.

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 ]