Pular para o conteúdo principal

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 NN qubits se aproximará de 4N4^N 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 OO' é calculado iterativamente aplicando as portas de cada fatia, ss, da seguinte maneira:

O(s)=USs+1O(s1)USs+1O'^{(s)} = \mathcal{U}_{S-s+1}^\dagger O'^{(s-1)} \mathcal{U}_{S-s+1}

onde SS é o número total de fatias e Us\mathcal{U}_{s} 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

H^=(j,k)(JxXjXk+JyYjYk+JzZjZk)+j(hxXj+hyYj+hzZj) \hat{H} = \sum_{(j,k)} \left( J_xX_jX_k + J_yY_jY_k + J_z Z_jZ_k \right) + \sum_{j} \left(h_xX_j + h_yY_j + h_zZ_j\right)

e o valor esperado a ser medido será Z0\langle Z_0 \rangle.

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 Jx=π/8J_x=\pi/8, Jy=π/4J_y=\pi/4, Jz=π/2J_z=\pi/2 e os campos magnéticos externos como hx=π/3h_x=\pi/3, hy=π/6h_y=\pi/6, hz=π/9h_z=\pi/9, 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")

Saída da célula de código anterior

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 88, e aloque um orçamento de erro de 0.0050.005 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.

Nota

Por padrão, backpropagate() usa a norma L1L_1 dos coeficientes truncados para limitar o erro total incorrido pelo truncamento, mas outras normas LpL_p 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:

Saída da célula de código anterior

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:

Saída da célula de código anterior

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

Recomendações