Usa distintas normas Lp para el truncamiento de términos de Pauli
Nota: Antes de leer esta guía, deberías leer la guía Truncar términos de Pauli, que describe el truncamiento de términos de Pauli de bajo peso integrado en el método backpropagate a partir de un TruncationErrorBudget especificado.
En esta guía aprenderás sobre el argumento de palabra clave p_norm de backpropagate, que se puede usar para cambiar la norma Lp utilizada para estimar el error incurrido por los términos de Pauli truncados.
Construir un circuito de ejemplo
Esta guía usa el mismo circuito de ejemplo que la guía Truncar términos 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)

Define un observable para la magnetización total:
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
)
Como referencia, calcula el valor esperado exacto:
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
Usar la norma L1
Por defecto, y como ya has visto en la guía Truncar términos de Pauli, p_norm=1, lo que significa que el error se estima como:
donde es el estado cuántico, es la diferencia real entre el observable exacto y el truncado (que se desconoce), es el conjunto de términos de Pauli que se truncaron y es el coeficiente del término de Pauli.
Esta desigualdad es una cota superior rigurosa pero muy holgada en la mayoría de los escenarios.
Esta guía retropropaga seis fragmentos del circuito de ejemplo, usando un error constante por fragmento de 0.001. Este valor debe entenderse como el presupuesto dentro del p_norm dado.
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.
Ahora podemos calcular el valor esperado del observable retropropagado, así como el error con respecto a la referencia exacta:
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
Por último, podemos graficar el error incurrido durante la retropropagación de cada fragmento, así como el error acumulado. El error acumulado es la suma de los errores de los fragmentos. Vemos que el error acumulado es una cota superior muy holgada del error real.
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])

Usar la norma L2
Se puede argumentar que la norma L2 es una mejor aproximación del error incurrido que la norma L1. Eso se debe a que podemos suponer que el estado cuántico se extrae de un conjunto aleatorio de Haar, en cuyo caso el error incurrido sigue una distribución con media nula y una varianza que puede acotarse aproximadamente por la norma L2:
Aunque la cota no es rigurosa, solo se violará en casos patológicos.
Volvemos a retropropagar seis fragmentos del circuito de ejemplo, usando un error máximo por fragmento de 0.001 (esta vez, interpretado en la norma 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 nuevo, calculamos el valor esperado del observable retropropagado y el error con respecto a la referencia exacta:
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
Al graficar los errores incurridos por fragmento y el error acumulado se obtiene una imagen similar a la anterior.
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])

Ten en cuenta que el error acumulado es de nuevo la suma de los errores de cada fragmento. Esta es otra cota holgada debido a la desigualdad de Minkowski, ya que tenemos que calcular esta cota de forma recursiva:
donde el nuevo subíndice indica la iteración actual de fragmento, haciendo de el error real en la iteración de retropropagación , de el error de truncamiento aproximado de la iteración , y de el conjunto de términos de Pauli truncados en la iteración .