Primeiros passos com OBP
Versões dos pacotes
O código nesta página foi desenvolvido usando os seguintes requisitos. Recomendamos usar essas versões ou mais recentes.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-addon-utils~=0.3.0
qiskit-addon-obp~=0.3.0
Ao preparar uma carga de trabalho quântica com retropropagação de operadores (OBP), primeiro você deve fazer uma seleção de "fatias de circuit", e segundo, você deve especificar um limiar de truncamento ou "orçamento de erro" para remover termos com coeficientes pequenos no operador retropropagado, além de definir um limite superior para o tamanho geral do operador retropropagado. Durante a retropropagação, o número de termos no operador de um circuit de qubits se aproximará de rapidamente no pior caso. Este guia demonstra as etapas envolvidas na aplicação de OBP a uma carga de trabalho quântica.
O componente principal do pacote qiskit-addons-obp é a função backpropagate(). Ela recebe argumentos para o observável final a ser reconstruído, um conjunto de fatias de circuit para computar classicamente e, opcionalmente, um TruncationErrorBudget ou OperatorBudget para fornecer restrições ao truncamento realizado. Uma vez especificados, o operador retropropagado computado classicamente é calculado iterativamente aplicando as portas de cada fatia, , da seguinte maneira:
onde é o número total de fatias e representa uma única fatia do circuit. Este exemplo usa o pacote qiskit-addons-utils para preparar as fatias do circuit, bem como gerar o circuit de exemplo.
Para começar, considere a evolução temporal de uma cadeia de Heisenberg XYZ. Este Hamiltoniano tem a forma
e o valor esperado a ser medido será .
O trecho de código a seguir gera o Hamiltoniano na forma de um SparsePauliOp usando o módulo qiskit_addons_utils.problem_generators e um CouplingMap. Defina as constantes de acoplamento como , , e os campos magnéticos externos como , , , e então gere um circuit que modela sua evolução 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 entradas para retropropagação
Em seguida, gere as fatias do circuit para a retropropagação. Em geral, a escolha de como fatiá-lo pode ter impacto no desempenho da retropropagação para um dado problema. Aqui, agrupe portas do mesmo tipo em fatias usando a função 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.
Depois que as fatias forem geradas, especifique um OperatorBudget para fornecer à função backpropagate() uma condição de parada da retropropagação do operador e evitar que a sobrecarga clássica cresça ainda mais. Você também pode especificar um orçamento de erro de truncamento para cada fatia, no qual termos de Pauli com coeficientes pequenos serão truncados de cada fatia até que o orçamento de erro seja preenchido. Qualquer orçamento restante será então adicionado ao orçamento da fatia seguinte.
Aqui, especifique que a retropropagação deve parar quando o número de grupos de Pauli com comutação qubit a qubit no operador ultrapassar , e aloque um orçamento de erro de para cada fatia.
op_budget = OperatorBudget(max_qwc_groups=8)
truncation_error_budget = setup_budget(max_error_per_slice=0.005)
Retropropagar fatias
Nesta etapa você definirá o observável final a ser medido e executará a retropropagação em cada fatia. A função backpropagate() retorna três saídas: o observável retropropagado, as fatias de circuit restantes que não foram retropropagadas (e que devem ser executadas em hardware quântico) e metadados sobre a retropropagação.
Note que tanto o OperatorBudget quanto o TruncationErrorBudget são parâmetros opcionais do método backpropagate(). Em geral, a melhor escolha para ambos deve ser determinada heuristicamente e requer alguma experimentação. Neste exemplo, retropropagaremos tanto com quanto sem um TruncationErrorBudget.
Por padrão, backpropagate() usa a norma dos coeficientes truncados para limitar o erro total incorrido pelo truncamento, mas outras normas podem ser usadas se você desejar modificar como o erro de truncamento é calculado.
# 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:
Os trechos de código abaixo retropropagam o circuit com um orçamento de erro de truncamento.
# 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 e executar a carga de trabalho quântica
Agora que você retropropagou o operador, pode executar a parte restante do circuit em uma QPU. A carga de trabalho quântica, usando o Estimator, deve incluir o circuit bp_circuit_trunc e deve medir o operador retropropagado backpropagated_observable.
Para demonstrar a eficácia do OBP por si só, o trecho de código a seguir transpila tanto o circuit original quanto o retropropagado (com e sem truncamento) e simula os circuits classicamente usando o 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 fim, o trecho de código a seguir irá transpilar e executar o circuit retropropagado em uma QPU (tanto com quanto sem truncamento).
# 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 passos
- Experimente o tutorial sobre como usar OBP para melhorar valores esperados.