Vai al contenuto principale

Iniziare con OBP

Versioni dei pacchetti

Il codice in questa pagina è stato sviluppato con i seguenti requisiti. Si consiglia di usare queste versioni o versioni più recenti.

qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-addon-utils~=0.3.0
qiskit-addon-obp~=0.3.0

Quando prepari un workload quantistico con la backpropagation degli operatori (OBP), devi prima selezionare le "slice del circuito" e poi specificare una soglia di troncamento o "error budget" per eliminare i termini con coefficienti piccoli nell'operatore backpropagato, oltre a impostare un limite superiore alla dimensione complessiva dell'operatore backpropagato. Durante la backpropagation, il numero di termini nell'operatore di un circuito a NN qubit si avvicina rapidamente a 4N4^N nel caso peggiore. Questa guida illustra i passaggi necessari per applicare OBP a un workload quantistico.

Il componente principale del pacchetto qiskit-addons-obp è la funzione backpropagate(). Essa riceve come argomenti l'osservabile finale da ricostruire, un insieme di slice del circuito da calcolare classicamente e, facoltativamente, un TruncationErrorBudget o un OperatorBudget per definire i vincoli sul troncamento effettuato. Una volta specificati questi elementi, l'operatore backpropagato O′O' calcolato classicamente viene determinato in modo iterativo applicando le porte di ogni slice, ss, nel modo seguente:

O′(s)=US−s+1†O′(s−1)US−s+1O'^{(s)} = \mathcal{U}_{S-s+1}^\dagger O'^{(s-1)} \mathcal{U}_{S-s+1}

dove SS è il numero totale di slice e Us\mathcal{U}_{s} rappresenta una singola slice del circuito. Questo esempio usa il pacchetto qiskit-addons-utils per preparare le slice del circuito e generare il circuito di esempio.

Per iniziare, considera l'evoluzione temporale di una catena di Heisenberg XYZ. Questa Hamiltoniana ha la 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 il valore di aspettazione da misurare sarà ⟨Z0⟩\langle Z_0 \rangle.

Il seguente frammento di codice genera l'Hamiltoniana nella forma di uno SparsePauliOp usando il modulo qiskit_addons_utils.problem_generators e una CouplingMap. Imposta le costanti di accoppiamento a Jx=Ï€/8J_x=\pi/8, Jy=Ï€/4J_y=\pi/4, Jz=Ï€/2J_z=\pi/2 e i campi magnetici esterni a hx=Ï€/3h_x=\pi/3, hy=Ï€/6h_y=\pi/6, hz=Ï€/9h_z=\pi/9, quindi genera un circuito che modella la sua evoluzione temporale.

# 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")

Output della cella di codice precedente

Preparare gli input per la backpropagation​

Successivamente, genera le slice del circuito per la backpropagation. In generale, la scelta di come effettuare il taglio in slice può influire sulle prestazioni della backpropagation per un determinato problema. Qui, raggruppa le porte dello stesso tipo in slice usando la funzione 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 volta generate le slice, specifica un OperatorBudget per fornire alla funzione backpropagate() una condizione di arresto che impedisca all'overhead classico di crescere ulteriormente. Puoi anche specificare un error budget di troncamento per ogni slice, in cui i termini di Pauli con coefficienti piccoli verranno troncati da ogni slice finché l'error budget non è esaurito. Qualsiasi budget residuo verrà aggiunto al budget della slice successiva.

In questo caso, specifica che la backpropagation deve fermarsi quando il numero di gruppi di Pauli commutanti qubit-per-qubit nell'operatore supera 88, e alloca un error budget di 0.0050.005 per ogni slice.

op_budget = OperatorBudget(max_qwc_groups=8)
truncation_error_budget = setup_budget(max_error_per_slice=0.005)

Backpropagation delle slice​

In questo passaggio definirai l'osservabile finale da misurare ed eseguirai la backpropagation su ogni slice. La funzione backpropagate() restituisce tre output: l'osservabile backpropagato, le slice del circuito rimanenti che non sono state backpropagate (e che devono essere eseguite su hardware quantistico) e i metadati relativi alla backpropagation.

Tieni presente che sia OperatorBudget che TruncationErrorBudget sono parametri facoltativi per il metodo backpropagate(). In generale, la scelta migliore per entrambi dovrebbe essere determinata euristicamente e richiede una certa sperimentazione. In questo esempio eseguiremo la backpropagation sia con che senza un TruncationErrorBudget.

Nota

Per impostazione predefinita, backpropagate() usa la norma L1L_1 dei coefficienti troncati per limitare l'errore totale derivante dal troncamento, ma è possibile usare altre norme LpL_p se si desidera modificare il calcolo dell'errore di troncamento.

# 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:

Output della cella di codice precedente

I frammenti di codice seguenti eseguono la backpropagation del circuito con un error budget di troncamento.

# 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:

Output della cella di codice precedente

Transpilare ed eseguire il workload quantistico​

Ora che hai eseguito la backpropagation dell'operatore, puoi eseguire la parte rimanente del circuito su una QPU. Il workload quantistico, che usa l'Estimator, deve includere il circuito bp_circuit_trunc e misurare l'operatore backpropagato backpropagated_observable.

Per dimostrare l'efficacia di OBP da solo, il seguente frammento di codice transpila sia il circuito originale sia quello backpropagato (con e senza troncamento) e simula i circuiti classicamente usando 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

Infine, il seguente frammento di codice transpilerà ed eseguirà il circuito backpropagato su una QPU (sia con che senza troncamento).

# 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

Passi successivi​

Raccomandazioni