Pauli-Terme während der Rückpropagation abschneiden
Diese Anleitung erklärt, wie du den Mechanismus zum Abschneiden von Pauli-Termen konfigurierst, der vom Modul qiskit_addon_obp.utils.truncating bereitgestellt wird.
Operator Backpropagation (OBP) kann verwendet werden, um die Tiefe von Quanten-Circuits auf Kosten einer komplexeren Observable zu verringern. Um aussagekräftige Ergebnisse aus OBP zu erhalten, muss man in der Regel Terme aus der Observable abschneiden, damit sie nicht zu groß wird. Eine Möglichkeit, tiefer in den Circuit zurückzupropagieren und gleichzeitig zu verhindern, dass der Operator zu groß wird, besteht darin, Terme mit kleinen Koeffizienten abzuschneiden, statt sie dem Operator hinzuzufügen. Das Abschneiden von Termen kann zu weniger auszuführenden Quanten-Circuits führen, verursacht aber einen Fehler in der endgültigen Berechnung des Erwartungswerts, der proportional zur Größe der Koeffizienten der abgeschnittenen Terme ist.
Die Methode backpropagate akzeptiert ein optionales TruncationErrorBudget. Es konfiguriert das Abschneiden von Pauli-Termen mit niedrigem Gewicht für jede Observable nach der erfolgreichen Rückpropagation jedes Slices. Die Anzahl der abgeschnittenen Terme hängt von verschiedenen vom Benutzer festgelegten Konfigurationsparametern ab. Derzeit ist nur eine einzige Abschneidestrategie verfügbar: die Methode truncate_binary_search. Für eine gegebene Observable und ein Budget führt sie eine binäre Suche über die Pauli-Terme und Koeffizienten dieser Observable durch, um den optimalen Schwellenwert zu finden, sodass die Summe der abgeschnittenen Koeffizienten maximal, aber kleiner als das Budget ist.
Hinweis: Standardmäßig wird die L1-Norm verwendet, um den Abschneidefehler auszuwerten und zu beschränken; die Einstellung p_norm ermöglicht jedoch, die zu verwendende Lp-Norm festzulegen.
Weitere Informationen zur Verwendung dieser Einstellung findest du in der Anleitung Verschiedene Lp-Normen für das Abschneiden von Pauli-Termen verwenden.
Die folgenden Beispiele zeigen verschiedene Möglichkeiten, ein TruncationErrorBudget mit der zugehörigen Funktion setup_budget zu erstellen.
Einen Beispiel-Circuit erstellen
Diese Anleitung verwendet die folgenden Circuit-Slices:
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "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
# Generate a linear chain of 10 qubits
linear_chain = rustworkx.generators.path_graph(10)
# 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,
)
# 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)
# for visualization purposes, 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)

Wir betrachten eine einzelne einfache Observable:
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIIZIIII")
Der einfachste Fall: ein festes Abschneidebudget für jeden Slice
Das verfügbare Budget für das Abschneiden von Pauli-Termen kann sich bei jedem Schritt der Rückpropagation ändern. Um zu verstehen, wie das funktioniert, betrachten wir zuerst den einfachsten Fall eines festen Abschneidebudgets, das der Benutzer vorgibt.
Der einfachste Weg, das Abschneidebudget anzugeben, ist das Argument max_error_per_slice.
Tatsächlich geschieht genau das im Tutorial Circuit-Tiefe mit Operator Backpropagation verringern. Wird max_error_per_slice auf einen float gesetzt, erhält jeder Slice ein Budget in dieser Höhe.
Im folgenden Beispiel setzen wir diesen Wert auf 0.001, was einen impliziten Abschneidefehler von höchstens 0.018 garantiert, falls alle 18 Slices rückpropagiert würden.
Beachte, dass ein Fehlerbudget, das nach der Rückpropagation eines Slices und dem Abschneiden von Termen mit kleinen Koeffizienten übrig bleibt, immer zum Fehlerbudget des folgenden Slices hinzugefügt wird.
from qiskit_addon_obp.utils.truncating import setup_budget
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.simplify import OperatorBudget
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 29 terms and 10 commuting groups.
Als Nächstes verwenden wir die OBPMetadata-Instanz und die Werkzeuge des Moduls visualization, um den Rückpropagationsprozess zu visualisieren.
-
Das Diagramm oben links zeigt, dass wir nach der Rückpropagation des dritten Slices genug Budget haben, um mit dem Abschneiden von Observable-Termen zu beginnen. Ab dem dritten Slice wissen wir, dass wir aus jedem rückpropagierten Slice mindestens einen Term abschneiden, weil nach jedem Slice ein gewisser Abschneidefehler entsteht.
-
Das Diagramm oben rechts zeigt, dass das Fehlerbudget für den dritten Slice auf
.003anwächst. Wir sehen einen starken Abfall des verbleibenden Budgets, was bedeutet, dass Terme aus der Observable abgeschnitten wurden. Das stimmt mit dem überein, was wir aus dem Diagramm oben links geschlossen haben. -
Das Diagramm unten links zeigt, dass unser gesamter akkumulierter Fehler monoton wächst, während wir Terme aus unserer Observable abschneiden. Dieses Diagramm bestätigt auch, dass erst nach der Rückpropagation des dritten Slices Terme abgeschnitten wurden.
-
Das Diagramm unten rechts zeigt, dass die Anzahl der kommutierenden Pauli-Gruppen in unserer Observable auf die festgelegte Grenze von
10angewachsen ist. Dieses Diagramm zeigt auch, wie die Rückpropagation einer weiteren Schicht dazu führen würde, dass unsere Observable die festgelegte Schranke überschreitet, wie du am Kreuzungspunkt der schwarzen und der roten Linie sehen kannst.
Beachte, dass in all diesen Diagrammen die x-Achse die rückpropagierten Slices aufzählt. Da OBP jedoch am Ende des Circuits arbeitet, ist slice 1 der allerletzte Slice, slice 2 der davor und so weiter.
from matplotlib import pyplot as plt
from qiskit_addon_obp.utils.visualization import (
plot_accumulated_error,
plot_left_over_error_budget,
plot_num_qwc_groups,
plot_slice_errors,
)
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Das Slice-Budget explizit angeben
Wenn du weißt, wie du jedem Slice ein Budget zuweist, sodass die Performance der Rückpropagation optimiert wird, kannst du jedem Slice explizit ein Budget zuweisen. Zu Demonstrationszwecken weisen wir den ersten drei Slices ein Budget von null zu und verwenden für die übrigen Slices ein Budget von .001 pro Slice.
# Zero out the first 3 slices' budgets
max_error_per_slice = [0.0] * 3 + [0.001] * (len(slices) - 3)
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 32 terms and 10 commuting groups.
Der Entzug des Budgets für die ersten drei Schichten führte dazu, dass erst nach der vierten Schicht Terme abgeschnitten wurden, wie sich in drei dieser Diagramme bestätigen lässt. Etwas überraschend ist vielleicht, dass trotz fehlendem übrig gebliebenem Budget für den vierten Slice dennoch ein Term mit dem zugewiesenen Budget von .001 abgeschnitten wurde. Das ist im Diagramm oben links offensichtlich, lässt sich aber auch im Diagramm oben rechts beobachten, da die Kurve des übrigen Budgets zwischen Slice drei und vier abflacht. Ein weiteres bemerkenswertes Detail ist, dass ab diesem Punkt mindestens ein Term abgeschnitten wurde, genau wie im vorherigen Beispiel.
Die wichtigste Erkenntnis ist: Obwohl im zweiten Beispiel durch das Nullsetzen der Budgets einiger Slices ein geringerer Abschneidefehler entstand, konnten wir dieselbe Anzahl von Slices rückpropagieren, und unsere Observable enthält dieselbe Anzahl kommutierender Pauli-Gruppen. Anhand des Diagramms unten links können wir bestätigen, dass die Schranke für unseren Fehler im zweiten Beispiel kleiner ist.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Das Budget zyklisch angeben
Wenn du einen Circuit mit einem sich wiederholenden Muster hast, etwa einen Trotter-Circuit, möchtest du vielleicht ein Budget für diese wiederholte Teilmenge von Slices angeben und es für alle folgenden Wiederholungen dieser Slices verwenden lassen.
Konkret hat der von uns verwendete Beispiel-Circuit sechs Slices, die dreimal wiederholt werden, insgesamt also 18 Slices. Wir weisen den Ein-Qubit-Schichten und den RYY-Schichten willkürlich ein Budget von null zu und jeder RXX-Schicht ein Budget von .003. Wir beobachten, wie die Angabe des Budgets als Sequenz der Länge 6 dazu führt, dass das Budget zyklisch auf alle 18 Slices angewendet wird.
Beachte erneut, dass die Slices in umgekehrter Reihenfolge rückpropagiert werden (das heißt, vom Ende her). Daher wird der erste Eintrag unseres zyklischen Budgets tatsächlich für den letzten Slice verwendet, der zweite Eintrag für den Slice davor und so weiter.
# Specify a length-6 per-slice budget.
# This will be cycled over three times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=inf, p_norm=1)
op_budget = OperatorBudget(max_qwc_groups=20)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 13 circuit slices.
New observable contains 49 terms and 14 commuting groups.
Wie in den Abbildungen oben links und unten links zu sehen ist, wurde über die ersten vier Slices hinweg nichts abgeschnitten, da ihnen kein Fehlerbudget zugewiesen war. In den Slices fünf und sechs wurden einige Terme abgeschnitten, sobald Budget verfügbar war.
Wie in der Abbildung oben links zu sehen ist, entstand nach der Rückpropagation von Slice sieben ein relativ großer Fehler, obwohl diesem Slice kein Budget zugewiesen war. Das liegt daran, dass nur etwa .002 des gesamten Budgets von .006, das den Slices fünf und sechs zugewiesen war, verbraucht wurde. Der Rest wurde an Slice sieben weitergegeben und größtenteils aufgebraucht, wie in der Abbildung oben rechts zu sehen ist.
Die Abbildungen oben links und oben rechts zeigen, dass das kleine übrige Budget zwischen den Slices 8 bis 10 aufgebraucht wird und in Slice 11 wie erwartet neues Budget verfügbar wird. Die Abbildungen oben links, oben rechts und unten links zeigen alle das zyklische Verhalten des Arguments max_error_per_slice, wenn seine Länge kleiner als die Anzahl der Slices ist. Dieses zyklische Verhalten wäre über alle Slices hinweg fortgesetzt worden, doch das Abbruchkriterium max_qwc_groups wurde nach der Rückpropagation von 13 Slices erreicht, wie in der Abbildung unten rechts zu sehen ist.
Interessant ist auch, dass die Anzahl der Pauli-Gruppen nach der zweiten Runde verfügbaren Budgets bei Slice 11 tatsächlich sinkt. Das liegt daran, dass es Gruppen mit kleinen Koeffizienten gab, die erst abgeschnitten werden konnten, als nach der Rückpropagation von Slice 11 mehr Budget vorhanden war. Sie sammelten sich daher über mehrere Iterationen in der Observable an. Dieser Fall zeigt auch, dass max_qwc_groups überschritten werden muss, damit der Algorithmus terminiert.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Den Gesamtfehler begrenzen
Zusätzlich zum Fehlerbudget pro Slice kann man den maximalen Fehler angeben, der durch das Abschneiden entstehen darf. Sobald diese Grenze erreicht ist, wird nicht mehr abgeschnitten; die Rückpropagation läuft jedoch weiter, bis die Observable zu groß wird und eines der Abbruchkriterien erfüllt ist.
Wir führen als Nächstes das obige Experiment erneut aus und setzen eine Obergrenze für max_error_total, sodass das Fehlerbudget nach der Rückpropagation von Slice sieben aufgebraucht ist.
# Specify a length-6 per-slice budget.
# This will be cycled over 3 times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice, max_error_total=0.006
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=0.006, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 10 circuit slices.
New observable contains 67 terms and 20 commuting groups.
Wie erwartet erreicht unser Abschneidefehler seine Obergrenze bei .006 (Diagramm unten links). Bemerkenswert ist, dass wir in diesem Durchlauf Slice 11 nicht rückpropagieren konnten. Das liegt daran, dass wir nicht genug Budget zum Abschneiden von Termen hatten und die Anzahl der kommutierenden Pauli-Gruppen die Grenze von 20 überschritt, wie in der Abbildung unten rechts zu sehen ist.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Es kann wünschenswert sein, das Gesamtbudget einfach mit max_error_total zu begrenzen, ohne max_error_per_slice anzugeben.
truncation_error_budget = setup_budget(max_error_total=0.018)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.018], max_error_total=0.018, p_norm=1)
Die Ausgabe der obigen Zelle ist vielleicht etwas überraschend, weil per_slice_budget auf max_error_total gesetzt ist. Das zeigt an, dass das gesamte verfügbare Budget gierig verbraucht wird.
Du kannst es dir so vorstellen: Das gesamte Budget steht jedem Slice zur Verfügung (weil wir über per_slice_budget iterieren). Aber jedes bereits verbrauchte Budget wird von dem Budget abgezogen, das zu diesem Zeitpunkt im Algorithmus verfügbar ist.
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 9 circuit slices.
New observable contains 25 terms and 9 commuting groups.
Die Abbildung oben rechts zeigt, wie das gesamte Fehlerbudget von .018 dem ersten Slice zur Verfügung gestellt wird. Bis zum dritten Slice wird nichts abgeschnitten, sodass das übrige Budget konstant bleibt. Das Budget nimmt monoton ab, da jedem rückpropagierten Slice das volle Budget zur Verfügung gestellt wird, bis es aufgebraucht ist.
Bemerkenswert ist, dass dieses Experiment zwei Slices weniger rückpropagiert hat als das erste Experiment in diesem Notebook, das nahezu identisch ist. Das zeigt, dass es bei manchen Problemen optimal sein kann, das Budget gleichmäßig zu verteilen. Bei anderen Problemen kann es bessere Performance bringen, Slices das volle Budget gierig verbrauchen zu lassen.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Die Anzahl der rückpropagierten Slices und den Gesamtfehler gemeinsam begrenzen
Wenn du dein Fehlerbudget nicht über den gesamten Circuit verteilen, es aber auch nicht gierig verbrauchen möchtest, kannst du die Anzahl der Slices, die du rückpropagieren willst (num_slices), zusammen mit einem Gesamtfehlerbudget (max_error_total) angeben. Dadurch wird das Fehlerbudget gleichmäßig (gemäß p_norm) auf die Eingabe-Slices verteilt.
Hier begrenzen wir die Anzahl der Slices, die wir eventuell rückpropagieren, auf 12 und behalten dasselbe Gesamtfehlerbudget bei.
num_slices = 12
truncation_error_budget = setup_budget(
max_error_total=0.018, num_slices=num_slices, p_norm=1
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0014999999999999998], max_error_total=0.018, p_norm=1)
Nun versuchen wir, die 12 Slices rückzupropagieren, für die wir im vorherigen Schritt ein Budget zugewiesen haben. Dazu übergeben wir einfach die letzten 12 Slices unseres Circuits. Mit p_norm=1 hat jeder der 12 Slices ein verfügbares Budget von 0.018 / num_slices = 0.0015.
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices[-num_slices:],
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
Da wir eine Teilmenge unserer Slices (im Code slices[-num_slices:]) an backpropagate übergeben haben, müssen wir die nach der Rückpropagation verbleibenden Slices mit den Slices kombinieren, die nie zur Rückpropagation geschickt wurden (im Code slices[:-num_slices]).
Sobald wir alle verbleibenden Slices kombiniert haben, können wir mit combine_slices den QuantumCircuit mit reduzierter Tiefe erzeugen. Anschließend prüfen wir, wie viele Slices rückpropagiert wurden und wie groß unsere Observable geworden ist.
# Recombine the slices remaining after backprop with the rest of the original circuit
reduced_circuit = combine_slices(slices[:-num_slices] + remaining_slices)
print(f"Backpropagated {num_slices - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 12 circuit slices.
New observable contains 29 terms and 9 commuting groups.
Die Plots zeigen, dass wir alle 12 Slices erfolgreich zurückpropagiert haben und dabei unsere Observable unter 10 kommutierenden Pauli-Gruppen geblieben ist. Wir sehen außerdem, dass die Verwendung von num_slices zusammen mit max_error_total zu einer Verteilung der Budgets auf die Slices führt und ungenutztes Budget erneut an den nächsten Slice weitergegeben wird. Am deutlichsten ist das im Plot oben rechts zu erkennen, da das Budget während der Rückpropagation sowohl verbraucht als auch wieder aufgefüllt wird.
Bemerkenswert ist, dass diese Methode der Fehlerverteilung die besten Ergebnisse lieferte (mehr zurückpropagierte Slices), verglichen mit dem ersten Experiment in diesem Notebook und dem Beispiel direkt davor. Alle diese Beispiele haben ein Fehlerbudget von .018 zugeteilt, aber die Rückpropagation schnitt je nach Verteilung dieses Budgets unterschiedlich ab.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

Mit mehreren Observablen arbeiten
Die Methode qiskit_addon_obp.backpropagate erlaubt es, eine Sequenz von Observablen zu übergeben. Das vereinfacht den Workflow, wenn man mit mehreren Ziel-Observablen arbeitet.
Wir erwähnen das hier ausdrücklich, um dir zu zeigen, wie die Truncation-Strategie mit einem solchen Fall umgeht. Für dieses Beispiel fügen wir der bisher verwendeten Observable eine weitere hinzu:
obs = [SparsePauliOp("IIIIIZIIII"), SparsePauliOp("IIIIIXIIII")]
Schließlich wiederholen wir das erste Experiment aus diesem Tutorial, aber mit zwei Observablen, in die der Circuit zurückpropagiert wird.
In diesem Beispiel hat das keinen Einfluss auf die Anzahl der Slices, die zurückpropagiert werden konnten. Wir sehen jedoch, dass die beiden Observablen zu unterschiedlich vielen Pauli-Termen und kommutierenden Gruppen geführt haben.
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"The new first observable contains {len(bp_obs[0])} terms and {len(bp_obs[0].group_commuting(qubit_wise=True))} commuting groups."
)
print(
f"The new second observable contains {len(bp_obs[1])} terms and {len(bp_obs[1].group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
The new first observable contains 29 terms and 10 commuting groups.
The new second observable contains 23 terms and 8 commuting groups.
Die folgenden Plots veranschaulichen, wie der Rückpropagations-Algorithmus mit mehreren Observablen umgeht.
Zunächst zeigen die Plots oben links, oben rechts und unten links, dass das Budget für das Abschneiden von Termen für jede Observable einzeln festgelegt wird. Mit anderen Worten: Beide Observablen können Terme abschneiden, wobei pro zurückpropagiertem Slice ein Fehler von 0.001 angenommen wird.
Aufgrund der unterschiedlichen Natur der Observablen führt das zu einem unterschiedlichen Verbrauch des Budgets. In diesem Beispiel überlappen sie stark, was nicht immer der Fall ist.
Der Plot unten rechts zeigt, dass max_qwc_groups alle Observablen berücksichtigt. Das heißt, die Terme aller Observablen werden zusammengefasst, um eine endgültige Anzahl qubit-weise kommutierender Gruppen zu erhalten, die mit max_qwc_paulis verglichen wird. Dasselbe geschieht beim Schwellenwert max_paulis (in diesem Notebook nicht behandelt), mit dem du ein Limit für die Anzahl der Pauli-Terme über alle Observablen hinweg festlegen kannst.
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])
