Utiliser différentes normes Lp pour la troncature des termes de Pauli
Remarque : avant de lire ce guide, tu devrais lire le guide Tronquer les termes de Pauli, qui décrit la troncature des termes de Pauli de faible poids intégrée à la méthode backpropagate, à partir d'un TruncationErrorBudget spécifié.
Dans ce guide, tu vas découvrir l'argument nommé p_norm de backpropagate, qui permet de changer la norme Lp utilisée pour estimer l'erreur introduite par les termes de Pauli tronqués.
Construire un circuit d'exemple
Ce guide utilise le même circuit d'exemple que le guide Tronquer les termes de Pauli :
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types
# Generate a linear chain of 10 qubits
num_qubits = 10
linear_chain = rustworkx.generators.path_graph(num_qubits)
# Use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# Evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)
# For visualization purposes only, recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Définis une observable pour l'aimantation totale :
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp.from_sparse_list(
[("Z", [i], 1.0) for i in range(num_qubits)], num_qubits=num_qubits
)
À titre de référence, calcule la valeur moyenne exacte :
from qiskit.primitives import StatevectorEstimator
estimator = StatevectorEstimator()
job = estimator.run([(circuit, obs)])
res = job.result()
exact_exp = res[0].data.evs
print(exact_exp)
9.318197859862146
Utiliser la norme L1
Par défaut, et comme tu l'as déjà vu dans le guide Tronquer les termes de Pauli, p_norm=1, ce qui signifie que l'erreur est estimée ainsi :
où est l'état quantique, est la différence réelle entre l'observable exacte et l'observable tronquée (qui est inconnue), est l'ensemble des termes de Pauli qui ont été tronqués, et est le coefficient du terme de Pauli.
Cette inégalité est une borne supérieure rigoureuse mais très large dans la plupart des scénarios.
Ce guide rétropropage six tranches du circuit d'exemple, en utilisant une erreur constante par tranche de 0.001. Cette valeur doit être comprise comme le budget au sens du p_norm donné.
from qiskit_addon_obp.utils.truncating import setup_budget
l1_truncation_error_budget = setup_budget(max_error_per_slice=0.001, p_norm=1)
print(l1_truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
from qiskit_addon_obp import backpropagate
max_slices = 6
l1_bp_obs, l1_remaining_slices, l1_metadata = backpropagate(
obs,
slices[-max_slices:],
truncation_error_budget=l1_truncation_error_budget,
)
l1_reduced_circuit = combine_slices(
slices[:-max_slices] + l1_remaining_slices
)
print(
f"Backpropagated {max_slices - len(l1_remaining_slices)} circuit slices."
)
print(
f"New observable contains {len(l1_bp_obs)} terms and {len(l1_bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 6 circuit slices.
New observable contains 116 terms and 10 commuting groups.
Nous pouvons maintenant calculer la valeur moyenne de l'observable rétropropagée, ainsi que l'erreur par rapport à la référence exacte :
estimator = StatevectorEstimator()
job = estimator.run([(l1_reduced_circuit, l1_bp_obs)])
res = job.result()
l1_exp = res[0].data.evs
l1_error = exact_exp - l1_exp
print(l1_exp, l1_error)
9.317869899338842 0.00032796052330397174
Enfin, nous pouvons tracer l'erreur introduite lors de la rétropropagation de chaque tranche, ainsi que l'erreur cumulée. L'erreur cumulée est la somme des erreurs des tranches. Nous voyons que l'erreur cumulée est une borne supérieure très large de l'erreur réelle.
from matplotlib import pyplot as plt
from qiskit_addon_obp.utils.visualization import (
plot_accumulated_error,
plot_slice_errors,
)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[1].plot([6], [l1_error], "x", color="red", label="actual error")
plot_slice_errors(l1_metadata, axes[0])
plot_accumulated_error(l1_metadata, axes[1])

Utiliser la norme L2
On peut soutenir que la norme L2 approxime mieux l'erreur introduite que la norme L1. C'est parce que nous pouvons supposer que l'état quantique est tiré d'un ensemble aléatoire de Haar, auquel cas l'erreur introduite suit une distribution de moyenne nulle et de variance pouvant être approximativement bornée par la norme L2 :
Bien que cette borne ne soit pas rigoureuse, elle ne sera violée que dans des cas pathologiques.
Nous rétropropageons à nouveau six tranches du circuit d'exemple, en utilisant une erreur maximale par tranche de 0.001 (cette fois interprétée selon la norme L2).
l2_truncation_error_budget = setup_budget(max_error_per_slice=0.001, p_norm=2)
print(l2_truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=2)
max_slices = 6
l2_bp_obs, l2_remaining_slices, l2_metadata = backpropagate(
obs,
slices[-max_slices:],
truncation_error_budget=l2_truncation_error_budget,
)
l2_reduced_circuit = combine_slices(
slices[:-max_slices] + l2_remaining_slices
)
print(
f"Backpropagated {max_slices - len(l2_remaining_slices)} circuit slices."
)
print(
f"New observable contains {len(l2_bp_obs)} terms and {len(l2_bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 6 circuit slices.
New observable contains 84 terms and 6 commuting groups.
De nouveau, nous calculons la valeur moyenne de l'observable rétropropagée et l'erreur par rapport à la référence exacte :
estimator = StatevectorEstimator()
job = estimator.run([(l2_reduced_circuit, l2_bp_obs)])
res = job.result()
l2_exp = res[0].data.evs
l2_error = exact_exp - l2_exp
print(l2_exp, l2_error)
9.317829770853422 0.00036808900872387085
Le tracé des erreurs introduites par tranche et de l'erreur cumulée donne une image similaire à la précédente.
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[1].plot([6], [l2_error], "x", color="red", label="actual error")
plot_slice_errors(l2_metadata, axes[0])
plot_accumulated_error(l2_metadata, axes[1])

Remarque que l'erreur cumulée est à nouveau la somme des erreurs de chaque tranche. C'est une autre borne large, à cause de l'inégalité de Minkowski, puisque nous devons calculer cette borne de manière récursive :
où le nouvel indice indique l'itération de tranche courante, faisant de l'erreur réelle à l'itération de rétropropagation , de l'erreur de troncature approximée de l'itération , et de l'ensemble des termes de Pauli tronqués à l'itération .