Primeros pasos con OBP
Versiones de paquetes
El código de esta página fue desarrollado con los siguientes requisitos. Se recomienda usar estas versiones o más recientes.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-addon-utils~=0.3.0
qiskit-addon-obp~=0.3.0
Cuando preparas una carga de trabajo cuántica con retropropagación de operadores (OBP), primero debes hacer una selección de "segmentos del circuito" y, en segundo lugar, debes especificar un umbral de truncamiento o "presupuesto de error" para eliminar términos con coeficientes pequeños en el operador retropropagado, así como establecer un límite superior al tamaño total del operador retropropagado. Durante la retropropagación, el número de términos en el operador de un circuito de qubits se aproximará a rápidamente en el peor caso. Esta guía demuestra los pasos involucrados en la aplicación de OBP a una carga de trabajo cuántica.
El componente principal del paquete qiskit-addons-obp es la función backpropagate(). Recibe argumentos para el observable final a reconstruir, un conjunto de segmentos del circuito a calcular clásicamente y, opcionalmente, un TruncationErrorBudget o OperatorBudget para imponer restricciones al truncamiento realizado. Una vez especificados estos parámetros, el operador retropropagado calculado clásicamente se obtiene de forma iterativa aplicando las puertas de cada segmento, , de la siguiente manera:
donde es el número total de segmentos y representa un único segmento del circuito. Este ejemplo usa el paquete qiskit-addons-utils para preparar los segmentos del circuito y generar el circuito de ejemplo.
Para comenzar, considera la evolución temporal de una cadena de Heisenberg XYZ. Este Hamiltoniano tiene la forma
y el valor esperado a medir será .
El siguiente fragmento de código genera el Hamiltoniano en forma de SparsePauliOp usando el módulo qiskit_addons_utils.problem_generators y un CouplingMap. Establece las constantes de acoplamiento en , , y los campos magnéticos externos en , , , y luego genera un circuito que modela su evolución temporal.
# 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")
Preparar las entradas para retropropagar
A continuación, genera los segmentos del circuito para la retropropagación. En general, la elección de cómo segmentar puede influir en el rendimiento de la retropropagación para un problema dado. Aquí, agrupa las puertas del mismo tipo en segmentos usando la función qiskit_addons_utils.slice_by_gate_types.
slices = slice_by_gate_types(circuit)
print(f"Separated the circuit into {len(slices)} slices.")
Separated the circuit into 18 slices.
Una vez generados los segmentos, especifica un OperatorBudget para proveer a la función backpropagate() con una condición para detener la retropropagación del operador y evitar que el costo clásico siga creciendo. También puedes especificar un presupuesto de error de truncamiento para cada segmento, en el cual los términos de Pauli con coeficientes pequeños serán truncados de cada segmento hasta que el presupuesto de error se agote. Cualquier presupuesto restante se añadirá al presupuesto del siguiente segmento.
Aquí, especifica que la retropropagación debe detenerse cuando el número de grupos de Pauli que conmutan qubit a qubit en el operador supere , y asigna un presupuesto de error de por segmento.
op_budget = OperatorBudget(max_qwc_groups=8)
truncation_error_budget = setup_budget(max_error_per_slice=0.005)
Retropropagar los segmentos
En este paso definirás el observable final a medir y ejecutarás la retropropagación a través de cada segmento. La función backpropagate() devuelve tres salidas: el observable retropropagado, los segmentos restantes del circuito que no fueron retropropagados (y que deben ejecutarse en hardware cuántico), y metadatos sobre la retropropagación.
Ten en cuenta que tanto el OperatorBudget como el TruncationErrorBudget son parámetros opcionales del método backpropagate(). En general, la mejor elección para ambos debe determinarse de forma heurística y requiere cierta experimentación. En este ejemplo retropropagaremos tanto con como sin un TruncationErrorBudget.
Por defecto, backpropagate() usa la norma de los coeficientes truncados para acotar el error total incurrido por el truncamiento, pero se pueden usar otras normas si deseas modificar cómo se calcula el error de truncamiento.
# 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:
Los siguientes fragmentos de código retropropagan el circuito con un presupuesto de error de truncamiento.
# 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:
Transpilar y ejecutar la carga de trabajo cuántica
Ahora que has retropropagado el operador, puedes ejecutar la parte restante del circuito en una QPU. La carga de trabajo cuántica, usando el Estimator, debe incluir el circuito bp_circuit_trunc y medir el operador retropropagado backpropagated_observable.
Para demostrar la eficacia de OBP por sí solo, el siguiente fragmento de código transpila tanto el circuito original como el circuito retropropagado (con y sin truncamiento) y simula los circuitos clásicamente usando el 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
Por último, el siguiente fragmento de código transpilará y ejecutará el circuito retropropagado en una QPU (tanto con como sin truncamiento).
# 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
Próximos pasos
- Prueba el tutorial sobre el uso de OBP para mejorar los valores esperados.