Simuler classiquement des circuits avec l'OBP
Dans ce guide, tu vas apprendre à simuler classiquement des instances de QuantumCircuit pour estimer des valeurs moyennes uniquement au moyen de l'OBP.
Comme l'OBP prend une observable et la rétropropage à travers un circuit donné, la « simulation » d'un circuit revient à calculer la valeur moyenne de l'observable cible par rapport à ce circuit.
Comme tu le verras plus loin, le paquet qiskit-addon-obp est même capable de gérer des modèles de bruit simples, ce qui te permet de calculer aussi des valeurs moyennes bruitées !
Construction d'un circuit d'exemple
Pour les besoins de ce guide, nous utiliserons le même circuit d'exemple que dans le guide de troncature des termes de Pauli :
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "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
# we generate a linear chain of 10 qubits
num_qubits = 10
linear_chain = rustworkx.generators.path_graph(num_qubits)
# we 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,
)
# we 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)
Cependant, ce qui précède n'est que le circuit décrivant l'évolution temporelle sous un hamiltonien choisi. Il nous faut aussi un état initial de départ, par rapport auquel nous calculons les valeurs moyennes de notre observable.
Bien sûr, nous pourrions choisir l'état tout-zéro (ou vide) comme état initial, mais pour montrer comment insérer son propre état initial, nous en choisissons un autre ci-dessous.
Une possibilité serait de préfixer notre circuit d'évolution temporelle ci-dessus avec l'état initial : circuit.compose(initial_state, front=True).
Mais comme nous avons déjà découpé notre circuit en tranches, il est plus simple d'insérer l'état initial comme première tranche, ce que nous faisons ci-dessous.
De cette façon, nous pouvons simplement remplacer la première tranche par un autre état initial si nous voulons le changer à l'avenir, sans avoir à recalculer nos tranches.
from qiskit.circuit import QuantumCircuit
initial_state = QuantumCircuit(num_qubits)
for i in range(0, num_qubits, 2):
initial_state.x(i)
slices.insert(0, initial_state)
# for visualization purposes only, we 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)

Simuler une valeur moyenne sans bruit
Comme observable cible, nous choisissons l'observable ZZ sur les qubits centraux :
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIZZIIII")
À ce stade, nous sommes prêts à simuler classiquement la valeur moyenne avec l'OBP.
Pour cela, il suffit de fournir toutes les tranches à la méthode backpropagate, comme ceci :
from qiskit_addon_obp import backpropagate
vacuum_state_obs, _, metadata = backpropagate(obs, slices)
Nous avons maintenant rétropropagé notre observable cible obs à travers le circuit entier (y compris le initial_state que nous avons placé dans slices[0]), ce qui donne un nouveau SparsePauliOp dont nous obtenons la valeur moyenne en le projetant sur l'état vide (|00...00>).
On y parvient de manière directe en sommant les coefficients de tous les termes de Pauli définis dans la base computationnelle :
vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)
Pour vérifier (et prouver que cela fonctionne), nous pouvons comparer notre résultat au Statevector de Qiskit :
from qiskit.quantum_info import Statevector
Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)
Quelques remarques sur les performances
L'efficacité de calcul de l'appel à backpropagate ci-dessus dépendra fortement de nombreux éléments, dont :
- la structure du
circuit - la méthode de découpage du circuit
- l'observable cible
- les paramètres de troncature
Comme la méthode backpropagate simplifie l'observable après l'application de chaque tranche, le nombre de portes dans une tranche peut influencer considérablement la charge de calcul.
La stratégie la plus agressive en termes de simplification de l'opérateur s'obtient en découpant ton circuit en tranches de portes individuelles.
De plus, tu peux exploiter tous les mécanismes de troncature intégrés à la méthode backpropagate.
Nous ne l'avons pas fait ci-dessus, ce qui donne effectivement une valeur moyenne exacte, mais tu peux apprendre comment le faire dans le guide de troncature des termes de Pauli.
Simuler une valeur moyenne bruitée
Le paquet qiskit-addon-obp prend aussi en charge les modèles de bruit sous la forme de PauliLindbladError.
C'est particulièrement utile lorsque tu as caractérisé le modèle de bruit des couches à 2 qubits de ton circuit, par exemple avec le NoiseLearner.
Dans cette section, tu verras comment utiliser les objets LayerError renvoyés par le NoiseLearner pour calculer des valeurs moyennes bruitées avec l'OBP.
Obtenir un modèle de bruit
Normalement, tu exécuterais le NoiseLearner pour obtenir un modèle de bruit de ton circuit spécifique.
Pour éviter la complexité (et l'aléa) dans ce tutoriel, nous nous en abstiendrons et coderons à la place en dur un modèle de bruit pour notre circuit ci-dessous.
Nous veillons toutefois à ce que la structure de nos données corresponde à celle du NoiseLearnerResult.
Sous sa forme actuelle (non transpilée), notre circuit contient 4 couches uniques de portes à 2 qubits :
slices[1]: qui contient des portesRxxagissant sur toutes les paires impaires de qubitsslices[2]: qui contient des portesRxxagissant sur toutes les paires paires de qubitsslices[3]: qui contient des portesRyyagissant sur toutes les paires impaires de qubitsslices[4]: qui contient des portesRyyagissant sur toutes les paires paires de qubits
Dans la cellule ci-dessous, nous construisons manuellement 4 instances de LayerError, une pour chacune de ces couches, avec des taux d'erreur aléatoires.
import numpy as np
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.utils.noise_learner_result import (
LayerError,
PauliLindbladError,
)
# fmt: off
pauli_errors_even = ['IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIXX', 'IIIIIIIIXY', 'IIIIIIIIXZ', 'IIIIIIIIYI', 'IIIIIIIIYX', 'IIIIIIIIYY', 'IIIIIIIIYZ', 'IIIIIIIIZI', 'IIIIIIIIZX', 'IIIIIIIIZY', 'IIIIIIIIZZ', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII', 'XIIIIIIIII', 'XXIIIIIIII', 'XYIIIIIIII', 'XZIIIIIIII', 'YIIIIIIIII', 'YXIIIIIIII', 'YYIIIIIIII', 'YZIIIIIIII', 'ZIIIIIIIII', 'ZXIIIIIIII', 'ZYIIIIIIII', 'ZZIIIIIIII']
pauli_errors_odd = ['IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII']
# fmt: on
np.random.seed(42)
layer_error_odd_xx = LayerError(
circuit=slices[1],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_xx = LayerError(
circuit=slices[2],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
layer_error_odd_yy = LayerError(
circuit=slices[3],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)
layer_error_even_yy = LayerError(
circuit=slices[4],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)
Si tu avais utilisé le NoiseLearner pour identifier les couches uniques de portes à 2 qubits de ton circuit et caractériser leur bruit, tu aurais obtenu un objet NoiseLearnerResult.
Ce résultat contiendrait une liste d'objets LayerError, exactement comme ceux que nous avons construits manuellement ci-dessus.
Pour chaque couche unique à 2 qubits, le LayerError contient :
- le
QuantumCircuitreprésentant cette couche de portes à 2 qubits - les indices de qubits sur lesquels ce circuit agit
- le
PauliLindbladErrorqui représente le modèle de bruit caractérisé de cette couche
Le PauliLindbladError contiendra deux objets :
- la liste des erreurs de Pauli qui ont été caractérisées
- les taux d'erreur correspondant à chacune de ces erreurs de Pauli
Normalement, la liste des erreurs de Pauli sera creuse. Plus précisément, elle contiendra les erreurs de Pauli à un qubit sur tous les qubits sur lesquels agissent des portes, ainsi que les erreurs de Pauli à deux qubits sur tous les qubits qui sont connectés.
Insérer les couches bruitées dans notre circuit
Dans la section précédente, nous avons spécifiquement construit un LayerError pour chacune de nos couches uniques de portes à 2 qubits connues.
Cela signifie que nous savons quel LayerError correspond exactement à telle ou telle de nos tranches.
Normalement, en utilisant le LayerError, tu devras déterminer quelle est la couche unique de portes à 2 qubits et où elle apparaît dans ton circuit.
Tu devras ensuite ajuster ton circuit et/ou tes slices pour insérer le LayerError en conséquence.
La façon de procéder dans le cas général dépasse le cadre de ce guide pratique.
TODO: link to external documentation, once it exists!
Ici, la tâche est plus simple car nous savons à quelle tranche correspond un LayerError.
Il suffit donc maintenant d'insérer de nouvelles tranches pour représenter le bruit.
Remarque que nous devons envelopper chaque PauliLindbladError issu de LayerError.error dans un PauliLindbladErrorInstruction pour qu'il soit une instruction QuantumCircuit valide.
from qiskit_addon_obp.utils.noise import PauliLindbladErrorInstruction
noisy_slices = []
for slice_ in slices:
if slice_ == layer_error_even_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_even_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_yy.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_yy.error
),
slice_.qubits,
)
]
)
)
noisy_slices.append(slice_)
Nous pouvons vérifier notre travail et dessiner le circuit ci-dessous :
combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)
Simuler une valeur moyenne bruitée
À ce stade, la simulation classique de la valeur moyenne fonctionne exactement comme avant, simplement
vacuum_state_noisy_obs, _, metadata = backpropagate(obs, noisy_slices)
vacuum_state_noisy_obs.coeffs[
~vacuum_state_noisy_obs.paulis.x.any(axis=1)
].sum()
np.complex128(-0.7230801696448901+7.082755280463563e-19j)
Nous soulignons à nouveau que plusieurs questions de performance doivent être prises en compte. Reporte-toi à la section correspondante ci-dessus.
