Erste Schritte mit OBP
Paketversionen
Der Code auf dieser Seite wurde mit den folgenden Anforderungen entwickelt. Wir empfehlen, diese oder neuere Versionen zu verwenden.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-addon-utils~=0.3.0
qiskit-addon-obp~=0.3.0
Wenn du einen Quantenworkload mit Operator-Backpropagation (OBP) vorbereitest, musst du zunächst eine Auswahl an „Circuit-Slices" treffen und anschließend einen Abschneidungsschwellenwert bzw. ein „Fehlerbudget" festlegen, um Terme mit kleinen Koeffizienten im rückpropagiererten Operator zu entfernen sowie eine Obergrenze für die Gesamtgröße des rückpropagatierten Operators festzulegen. Während der Backpropagation nähert sich die Anzahl der Terme im Operator eines -Qubit-Circuits im schlimmsten Fall schnell an. Diese Anleitung zeigt die Schritte, die bei der Anwendung von OBP auf einen Quantenworkload erforderlich sind.
Die Hauptkomponente des qiskit-addons-obp-Pakets ist die Funktion backpropagate(). Sie nimmt Argumente für das zu rekonstruierende finale Observable, eine Menge von Circuit-Slices, die klassisch berechnet werden sollen, und optional ein TruncationErrorBudget oder OperatorBudget entgegen, um Einschränkungen für die durchgeführte Abschneidung bereitzustellen. Sobald diese angegeben sind, wird der klassisch berechnete rückpropagierte Operator iterativ berechnet, indem die Gates aus jedem Slice auf folgende Weise angewendet werden:
wobei die Gesamtzahl der Slices ist und einen einzelnen Slice des Circuits darstellt. Dieses Beispiel verwendet das qiskit-addons-utils-Paket, um die Circuit-Slices vorzubereiten und den Beispiel-Circuit zu generieren.
Betrachte zunächst die Zeitentwicklung einer Heisenberg-XYZ-Kette. Dieser Hamiltonoperator hat die Form
und der zu messende Erwartungswert ist .
Der folgende Codeabschnitt generiert den Hamiltonoperator in Form eines SparsePauliOp mithilfe des Moduls qiskit_addons_utils.problem_generators und einer CouplingMap. Setze die Kopplungskonstanten auf , , und die externen Magnetfelder auf , , , und generiere dann einen Circuit, der seine Zeitentwicklung modelliert.
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-obp qiskit-addon-utils qiskit-ibm-runtime
import numpy as np
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2
from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import slice_by_gate_types
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp.utils.truncating import setup_budget
from qiskit_addon_obp import backpropagate
from qiskit_addon_utils.slicing import combine_slices
coupling_map = CouplingMap.from_heavy_hex(3, bidirectional=False)
# Choose a 10-qubit linear chain on this coupling map
reduced_coupling_map = coupling_map.reduce(
[0, 13, 1, 14, 10, 16, 5, 12, 8, 18]
)
# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
reduced_coupling_map,
coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)
# we evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=2), time=0.2
)
circuit.draw("mpl")
Eingaben für die Backpropagation vorbereiten​
Als Nächstes generierst du die Circuit-Slices für die Backpropagation. Im Allgemeinen kann die Wahl der Slicing-Methode einen Einfluss darauf haben, wie gut die Backpropagation für ein gegebenes Problem funktioniert. Hier werden Gates desselben Typs mithilfe der Funktion qiskit_addons_utils.slice_by_gate_types in Slices gruppiert.
slices = slice_by_gate_types(circuit)
print(f"Separated the circuit into {len(slices)} slices.")
Separated the circuit into 18 slices.
Sobald die Slices generiert wurden, gib ein OperatorBudget an, um der Funktion backpropagate() eine Bedingung bereitzustellen, bei der die Backpropagation des Operators gestoppt und ein weiteres Anwachsen des klassischen Overheads verhindert werden soll. Du kannst auch ein Abschneidungsfehlerbudget für jeden Slice angeben, bei dem Pauli-Terme mit kleinen Koeffizienten aus jedem Slice abgeschnitten werden, bis das Fehlerbudget ausgeschöpft ist. Jedes verbleibende Budget wird dann dem Budget des folgenden Slices hinzugefügt.
Hier wird festgelegt, dass die Backpropagation gestoppt werden soll, wenn die Anzahl der qubit-weisen kommutierenden Pauli-Gruppen im Operator überschreitet, und es wird ein Fehlerbudget von für jeden Slice zugewiesen.
op_budget = OperatorBudget(max_qwc_groups=8)
truncation_error_budget = setup_budget(max_error_per_slice=0.005)
Slices rückpropagieren​
In diesem Schritt definierst du das finale zu messende Observable und führst die Backpropagation über jeden Slice durch. Die Funktion backpropagate() gibt drei Ausgaben zurück: das rückpropagierte Observable, die verbleibenden Circuit-Slices, die nicht rückpropagiert wurden (und die auf Quantenhardware ausgeführt werden sollten), sowie Metadaten über die Backpropagation.
Beachte, dass sowohl das OperatorBudget als auch das TruncationErrorBudget optionale Parameter für die Methode backpropagate() sind. Im Allgemeinen sollte die beste Wahl für beide heuristisch getroffen werden und erfordert ein gewisses Maß an Experimentieren. In diesem Beispiel werden wir die Backpropagation sowohl mit als auch ohne TruncationErrorBudget durchführen.
Standardmäßig verwendet backpropagate() die -Norm der abgeschnittenen Koeffizienten, um den durch die Abschneidung verursachten Gesamtfehler zu begrenzen. Es können jedoch auch andere -Normen verwendet werden, wenn du ändern möchtest, wie der Abschneidungsfehler berechnet wird.
# Specify a single-qubit observable
observable = SparsePauliOp("IIIIIIIIIZ")
# Backpropagate without the truncation error budget
backpropagated_observable, remaining_slices, metadata = backpropagate(
observable,
slices,
operator_budget=op_budget,
)
# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices, include_barriers=True)
print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
f"New observable has {len(backpropagated_observable.paulis)} terms, which can be combined into "
f"{len(backpropagated_observable.group_commuting(qubit_wise=True))} groups.\n"
f"After truncation, the error in our observable is bounded by {metadata.accumulated_error(0):.3e}"
)
print(
f"Note that backpropagating one more slice would result in {metadata.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
Backpropagated 7 slices.
New observable has 18 terms, which can be combined into 8 groups.
After truncation, the error in our observable is bounded by 0.000e+00
Note that backpropagating one more slice would result in 27 terms across 12 groups.
print(
"The remaining circuit after backpropagation without truncation looks as follows:"
)
bp_circuit.draw("mpl", scale=0.6)
The remaining circuit after backpropagation without truncation looks as follows:
Der folgende Codeabschnitt führt die Backpropagation des Circuits mit einem Abschneidungsfehlerbudget durch.
# Backpropagate *with* the truncation error budget
backpropagated_observable_trunc, remaining_slices_trunc, metadata_trunc = (
backpropagate(
observable,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
)
# Recombine the slices remaining after backpropagation
bp_circuit_trunc = combine_slices(
remaining_slices_trunc, include_barriers=True
)
print(f"Backpropagated {metadata_trunc.num_backpropagated_slices} slices.")
print(
f"New observable has {len(backpropagated_observable_trunc.paulis)} terms, which can be combined into "
f"{len(backpropagated_observable_trunc.group_commuting(qubit_wise=True))} groups.\n"
f"After truncation, the error in our observable is bounded by {metadata_trunc.accumulated_error(0):.3e}"
)
print(
f"Note that backpropagating one more slice would result in {metadata_trunc.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata_trunc.backpropagation_history[-1].num_qwc_groups} groups."
)
Backpropagated 10 slices.
New observable has 19 terms, which can be combined into 8 groups.
After truncation, the error in our observable is bounded by 4.933e-02
Note that backpropagating one more slice would result in 27 terms across 13 groups.
print(
"The remaining circuit after backpropagation with truncation looks as follows:"
)
bp_circuit_trunc.draw("mpl", scale=0.6)
The remaining circuit after backpropagation with truncation looks as follows:
Quantenworkload transpilieren und ausführen​
Nachdem du den Operator rückpropagiert hast, kannst du den verbleibenden Teil des Circuits auf einem QPU ausführen. Der Quantenworkload mit dem Estimator sollte den bp_circuit_trunc-Circuit enthalten und muss den rückpropagatierten Operator backpropagated_observable messen.
Um die Wirksamkeit von OBP allein zu demonstrieren, transpiliert der folgende Codeabschnitt sowohl den ursprünglichen als auch den rückpropagatierten Circuit (mit und ohne Abschneidung) und simuliert die Circuits klassisch mithilfe des StatevectorEstimator.
# Specify a backend and a pass manager for transpilation
backend = FakeMelbourneV2()
# pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
# Transpile original experiment
circuit_isa = pm.run(circuit)
observable_isa = observable.apply_layout(circuit_isa.layout)
# Transpile backpropagated experiment without truncation
bp_circuit_isa = pm.run(bp_circuit)
bp_obs_isa = backpropagated_observable.apply_layout(bp_circuit_isa.layout)
# Transpile the backpropagated experiment with truncated observable terms
bp_circuit_trunc_isa = pm.run(bp_circuit_trunc)
bp_obs_trunc_isa = backpropagated_observable_trunc.apply_layout(
bp_circuit_trunc_isa.layout
)
estimator = StatevectorEstimator()
# Run the experiments using the exact statevector estimator
result_exact = (
estimator.run([(circuit, observable)]).result()[0].data.evs.item()
)
result_bp = (
estimator.run([(bp_circuit_isa, bp_obs_isa)]).result()[0].data.evs.item()
)
result_bp_trunc = (
estimator.run([(bp_circuit_trunc_isa, bp_obs_trunc_isa)])
.result()[0]
.data.evs.item()
)
print(f"Exact expectation value: {result_exact}")
print(f"Backpropagated expectation value without truncation: {result_bp}")
print(f"Backpropagated expectation value with truncation: {result_bp_trunc}")
print(
f" - Expected Error for truncated observable: {metadata_trunc.accumulated_error(0):.3e}"
)
print(
f" - Observed Error for truncated observable: {abs(result_exact - result_bp_trunc):.3e}"
)
Exact expectation value: 0.8854160687717517
Backpropagated expectation value without truncation: 0.8854160687717533
Backpropagated expectation value with truncation: 0.8850236647156081
- Expected Error for truncated observable: 4.933e-02
- Observed Error for truncated observable: 3.924e-04
Abschließend transpiliert und führt der folgende Codeabschnitt den rückpropagatierten Circuit auf einem QPU aus (sowohl mit als auch ohne Abschneidung).
# Specify a backend and a pass manager for transpilation
service = QiskitRuntimeService()
backend = service.least_busy()
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
# Transpile backpropagated experiment without truncation
bp_circuit_isa = pm.run(bp_circuit)
bp_obs_isa = backpropagated_observable.apply_layout(bp_circuit_isa.layout)
# Transpile the backpropagated experiment with truncated observable terms
bp_circuit_trunc_isa = pm.run(bp_circuit_trunc)
bp_obs_trunc_isa = backpropagated_observable_trunc.apply_layout(
bp_circuit_trunc_isa.layout
)
# Run the experiments using Estimator primitive
estimator = EstimatorV2(mode=backend)
result_bp_qpu = (
estimator.run([(bp_circuit_isa, bp_obs_isa)]).result()[0].data.evs.item()
)
result_bp_trunc_qpu = (
estimator.run([(bp_circuit_trunc_isa, bp_obs_trunc_isa)])
.result()[0]
.data.evs.item()
)
print(f"Exact expectation value: {result_exact}")
print(f"Backpropagated expectation value without truncation: {result_bp_qpu}")
print(
f"Backpropagated expectation value with truncation: {result_bp_trunc_qpu}"
)
print(
f" - Observed Error for observable without truncation: {abs(result_exact - result_bp_qpu):.3e}"
)
print(
f" - Observed Error for truncated observable: {abs(result_exact - result_bp_trunc_qpu):.3e}"
)
Exact expectation value: 0.8854160687717517
Backpropagated expectation value without truncation: 0.8790435084647706
Backpropagated expectation value with truncation: 0.8759838342768448
- Observed Error for observable without truncation: 6.373e-03
- Observed Error for truncated observable: 9.432e-03
Nächste Schritte​
- Probiere das Tutorial zur Verwendung von OBP zur Verbesserung von Erwartungswerten.