Circuits klassisch mit OBP simulieren
In dieser Anleitung lernst du, wie du QuantumCircuit-Instanzen klassisch simulierst, um Erwartungswerte ausschließlich mit Hilfe von OBP zu schätzen.
Da OBP eine Observable nimmt und sie durch einen gegebenen Circuit rückpropagiert, läuft die „Simulation“ eines Circuits darauf hinaus, den Erwartungswert der Ziel-Observable bezüglich dieses Circuits zu berechnen.
Wie du später sehen wirst, kann das Paket qiskit-addon-obp sogar einfache Rauschmodelle verarbeiten, sodass du auch verrauschte Erwartungswerte berechnen kannst!
Einen Beispiel-Circuit konstruieren
Für diese Anleitung verwenden wir denselben Beispiel-Circuit wie in der Anleitung zum Abschneiden von Pauli-Termen:
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types
# we generate a linear chain of 10 qubits
num_qubits = 10
linear_chain = rustworkx.generators.path_graph(num_qubits)
# we use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# we evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)
Das Obige ist jedoch lediglich der Circuit, der die Zeitentwicklung unter einem gewählten Hamilton-Operator beschreibt. Wir brauchen außerdem einen Anfangszustand, von dem aus wir starten und bezüglich dessen wir die Erwartungswerte unserer Observable berechnen.
Natürlich könnten wir den Nullzustand (oder Vakuumzustand) als Anfangszustand wählen. Um jedoch zu zeigen, wie man einen eigenen Anfangszustand einfügt, wählen wir unten einen anderen.
Eine Möglichkeit wäre, den Anfangszustand dem obigen Zeitentwicklungs-Circuit voranzustellen: circuit.compose(initial_state, front=True).
Da wir unseren circuit aber bereits in Slices zerlegt haben, ist es einfacher, den Anfangszustand als ersten Slice einzufügen, was wir unten tun.
Auf diese Weise können wir den ersten Slice einfach durch einen anderen Anfangszustand ersetzen, falls wir ihn in Zukunft austauschen möchten, ohne unsere Slices neu berechnen zu müssen.
from qiskit.circuit import QuantumCircuit
initial_state = QuantumCircuit(num_qubits)
for i in range(0, num_qubits, 2):
initial_state.x(i)
slices.insert(0, initial_state)
# for visualization purposes only, we recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Einen rauschfreien Erwartungswert simulieren
Als Ziel-Observable wählen wir die Observable ZZ auf den zentralen Qubits:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIZZIIII")
Damit sind wir bereit, den Erwartungswert mit OBP klassisch zu simulieren.
Dazu übergeben wir einfach alle Slices an die Methode backpropagate, etwa so:
from qiskit_addon_obp import backpropagate
vacuum_state_obs, _, metadata = backpropagate(obs, slices)
Wir haben nun unsere Ziel-Observable obs durch den gesamten Circuit rückpropagiert (einschließlich des initial_state, den wir auf slices[0] gelegt haben). Das Ergebnis ist ein neues SparsePauliOp, dessen Erwartungswert wir durch Projektion auf den Vakuumzustand (|00...00>) erhalten.
Das lässt sich einfach erreichen, indem man die Koeffizienten aller in der Rechenbasis definierten Pauli-Terme aufsummiert:
vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)
Als Plausibilitätsprüfung (und um zu zeigen, dass das funktioniert) können wir unser Ergebnis mit dem Statevector von Qiskit vergleichen:
from qiskit.quantum_info import Statevector
Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)
Einige Hinweise zur Performance
Die Recheneffizienz des obigen Aufrufs von backpropagate hängt stark von mehreren Faktoren ab, darunter:
- die Struktur des
circuit - die Methode, mit der der Circuit in Slices zerlegt wird
- die Ziel-Observable
- die Abschneideparameter
Da die Methode backpropagate die Observable nach jedem angewendeten Slice vereinfacht, kann die Anzahl der Gates in einem Slice den Rechenaufwand dramatisch beeinflussen.
Die aggressivste Strategie zur Operatorvereinfachung erreichst du, indem du deinen Circuit in Slices aus einzelnen Gates zerlegst.
Zusätzlich kannst du den gesamten in die Methode backpropagate eingebauten Abschneidemechanismus nutzen.
Das haben wir oben nicht getan, was effektiv zu einem exakten Erwartungswert führt. In der Anleitung zum Abschneiden von Pauli-Termen erfährst du aber, wie es geht.
Einen verrauschten Erwartungswert simulieren
Das Paket qiskit-addon-obp unterstützt auch die Verarbeitung von Rauschmodellen in Form von PauliLindbladErrors.
Das ist besonders nützlich, wenn du das Rauschmodell der 2-Qubit-Schichten in deinem Circuit charakterisiert hast, zum Beispiel mit dem NoiseLearner.
In diesem Abschnitt siehst du, wie du die vom NoiseLearner zurückgegebenen LayerError-Objekte verwenden kannst, um mit OBP verrauschte Erwartungswerte zu berechnen.
Ein Rauschmodell erhalten
Normalerweise würdest du den NoiseLearner ausführen, um ein Rauschmodell deines konkreten Circuits zu erhalten.
Um in diesem Tutorial Komplexität (und Zufall) zu vermeiden, verzichten wir darauf und legen stattdessen unten ein Rauschmodell für unseren Circuit fest.
Wir stellen jedoch sicher, dass die Struktur unserer Daten der des NoiseLearnerResult entspricht.
In seiner aktuellen (nicht transpilierten) Form enthält unser Circuit 4 eindeutige Schichten von 2-Qubit-Gates:
slices[1]: mitRxx-Gates auf allen ungeraden Qubit-Paarenslices[2]: mitRxx-Gates auf allen geraden Qubit-Paarenslices[3]: mitRyy-Gates auf allen ungeraden Qubit-Paarenslices[4]: mitRyy-Gates auf allen geraden Qubit-Paaren
In der Zelle unten konstruieren wir manuell 4 LayerError-Instanzen, eine für jede dieser Schichten, mit zufälligen Fehlerraten.
import numpy as np
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.utils.noise_learner_result import (
LayerError,
PauliLindbladError,
)
# fmt: off
pauli_errors_even = ['IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIXX', 'IIIIIIIIXY', 'IIIIIIIIXZ', 'IIIIIIIIYI', 'IIIIIIIIYX', 'IIIIIIIIYY', 'IIIIIIIIYZ', 'IIIIIIIIZI', 'IIIIIIIIZX', 'IIIIIIIIZY', 'IIIIIIIIZZ', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII', 'XIIIIIIIII', 'XXIIIIIIII', 'XYIIIIIIII', 'XZIIIIIIII', 'YIIIIIIIII', 'YXIIIIIIII', 'YYIIIIIIII', 'YZIIIIIIII', 'ZIIIIIIIII', 'ZXIIIIIIII', 'ZYIIIIIIII', 'ZZIIIIIIII']
pauli_errors_odd = ['IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII']
# fmt: on
np.random.seed(42)
layer_error_odd_xx = LayerError(
circuit=slices[1],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_xx = LayerError(
circuit=slices[2],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
layer_error_odd_yy = LayerError(
circuit=slices[3],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_yy = LayerError(
circuit=slices[4],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
Hättest du den NoiseLearner verwendet, um die eindeutigen 2-Qubit-Gate-Schichten deines Circuits zu identifizieren und ihr Rauschen zu charakterisieren, hättest du ein NoiseLearnerResult-Objekt erhalten.
Dieses Ergebnis würde eine Liste von LayerError-Objekten enthalten, genau wie die, die wir oben manuell konstruiert haben.
Für jede eindeutige 2-Qubit-Schicht enthält der LayerError:
- den
QuantumCircuit, der diese 2-Qubit-Gate-Schicht darstellt - die Qubit-Indizes, auf die dieser Circuit wirkt
- den
PauliLindbladError, der das charakterisierte Rauschmodell dieser Schicht darstellt
Der PauliLindbladError enthält zwei Objekte:
- die Liste der charakterisierten Pauli-Fehler
- die Fehlerraten, die jedem dieser Pauli-Fehler entsprechen
Normalerweise ist die Liste der Pauli-Fehler dünn besetzt. Genauer gesagt enthält sie die Ein-Qubit-Pauli-Fehler auf allen Qubits, auf die Gates wirken, sowie die Zwei-Qubit-Pauli-Fehler auf allen Qubits, die verbunden sind.
Die verrauschten Schichten in unseren Circuit einfügen
Im vorherigen Abschnitt haben wir gezielt für jede unserer bekannten eindeutigen 2-Qubit-Gate-Schichten einen LayerError konstruiert.
Das heißt, wir wissen genau, welcher LayerError zu welchem unserer Slices passt.
Normalerweise musst du bei der Verwendung des LayerError herausfinden, was die eindeutige 2-Qubit-Gate-Schicht ist und wo sie in deinem Circuit vorkommt.
Anschließend musst du deinen circuit und/oder deine slices anpassen, um den LayerError entsprechend einzufügen.
Wie das im allgemeinen Fall geht, liegt außerhalb des Umfangs dieser Anleitung.
TODO: link to external documentation, once it exists!
Hier ist es einfacher, weil wir wissen, welchem Slice ein LayerError entspricht.
Es genügt also, neue Slices einzufügen, die das Rauschen darstellen.
Beachte, dass wir jeden PauliLindbladError aus LayerError.error in einen PauliLindbladErrorInstruction einpacken müssen, damit er eine gültige QuantumCircuit-Anweisung ist.
from qiskit_addon_obp.utils.noise import PauliLindbladErrorInstruction
noisy_slices = []
for slice_ in slices:
if slice_ == layer_error_even_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_even_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_yy.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_yy.error
),
slice_.qubits,
)
]
)
)
noisy_slices.append(slice_)
Wir können unsere Arbeit prüfen und den Circuit unten zeichnen:
combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)
Einen verrauschten Erwartungswert simulieren
An diesem Punkt funktioniert die klassische Simulation des Erwartungswerts genau wie zuvor, nur dass
vacuum_state_noisy_obs, _, metadata = backpropagate(obs, noisy_slices)
vacuum_state_noisy_obs.coeffs[
~vacuum_state_noisy_obs.paulis.x.any(axis=1)
].sum()
np.complex128(-0.7230801696448901+7.082755280463563e-19j)
Wir weisen erneut darauf hin, dass mehrere Performance-Aspekte zu beachten sind. Bitte gehe zurück zum entsprechenden Abschnitt oben.
