Découpe de fils pour l'estimation des valeurs d'espérance
Estimation d'utilisation : une minute sur un processeur Eagle (NOTE : ceci n'est qu'une estimation. Votre temps d'exécution peut varier.)
Contexte
Le circuit-knitting est un terme générique qui englobe diverses méthodes de partitionnement d'un circuit en plusieurs sous-circuits plus petits impliquant moins de portes et/ou de qubits. Chacun des sous-circuits peut être exécuté indépendamment et le résultat final est obtenu par un post-traitement classique sur le résultat de chaque sous-circuit. Cette technique est accessible dans l'addon Qiskit de découpe de circuits, une explication détaillée de la technique est donnée dans la documentation ainsi que d'autres ressources d'introduction.
Ce notebook traite d'une méthode appelée découpe de fils (wire cutting) où le circuit est partitionné le long du fil [1], [2]. Note que le partitionnement est simple dans les circuits classiques puisque le résultat au point de partition peut être déterminé de manière déterministe, et est soit 0, soit 1. Cependant, l'état du qubit au point de découpe est, en général, un état mixte. Par conséquent, chaque sous-circuit doit être mesuré plusieurs fois dans différentes bases (généralement un ensemble tomographiquement complet de bases comme la base de Pauli [3], [4]) et préparé de manière correspondante dans son état propre. La figure ci-dessous (source : thèse de doctorat, Ritajit Majumdar) montre un exemple de découpe de fils pour un état GHZ à 4 qubits en trois sous-circuits. Ici désigne un ensemble de bases (généralement Pauli X, Y et Z) et désigne un ensemble d'états propres (généralement , , et ).
Étant donné que chaque sous-circuit possède moins de qubits et/ou de portes, ils sont censés être moins sensibles au bruit. Ce notebook présente un exemple où cette méthode peut être utilisée pour supprimer efficacement le bruit dans le système.
Prérequis
Avant de commencer ce tutoriel, assure-toi d'avoir installé les éléments suivants :
- Qiskit SDK v2.0 ou ultérieur, avec le support de visualisation
- Qiskit Runtime v0.22 ou ultérieur (
pip install qiskit-ibm-runtime) - Addon Qiskit de découpe de circuits v0.9.0 ou ultérieur (
pip install qiskit-addon-cutting)
Nous considérerons un circuit de Localisation à Plusieurs Corps (MBL) pour ce notebook. Le circuit MBL est un circuit efficace en termes de matériel et est paramétré par deux paramètres et . Lorsque est fixé à et que l'état initial est préparé dans pour tous les qubits, la valeur d'espérance idéale de est pour chaque site de qubit , indépendamment des valeurs de . Vous pouvez consulter plus de détails sur les circuits MBL dans cet article.
Configuration
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
import numpy as np
import matplotlib.pyplot as plt
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.result import sampled_expectation_value
from qiskit_addon_cutting.instructions import CutWire
from qiskit_addon_cutting import (
cut_wires,
expand_observables,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2, Batch
class MBLChainCircuit(QuantumCircuit):
def __init__(
self, num_qubits: int, depth: int, use_cut: bool = False
) -> None:
super().__init__(
num_qubits, name=f"MBLChainCircuit<{num_qubits}, {depth}>"
)
evolution = MBLChainEvolution(num_qubits, depth, use_cut)
self.compose(evolution, inplace=True)
class MBLChainEvolution(QuantumCircuit):
def __init__(self, num_qubits: int, depth: int, use_cut) -> None:
super().__init__(
num_qubits, name=f"MBLChainEvolution<{num_qubits}, {depth}>"
)
theta = Parameter("θ")
phis = ParameterVector("φ", num_qubits)
for layer in range(depth):
layer_parity = layer % 2
# print("layer parity", layer_parity)
for qubit in range(layer_parity, num_qubits - 1, 2):
# print(qubit)
self.cz(qubit, qubit + 1)
self.u(theta, 0, np.pi, qubit)
self.u(theta, 0, np.pi, qubit + 1)
if (
use_cut
and layer_parity == 0
and (
qubit == num_qubits // 2 - 1
or qubit == num_qubits // 2
)
):
self.append(CutWire(), [num_qubits // 2])
if use_cut and layer < depth - 1 and layer_parity == 1:
if qubit == num_qubits // 2:
self.append(CutWire(), [qubit])
for qubit in range(num_qubits):
self.p(phis[qubit], qubit)
Partie I. Exemple à petite échelle
Étape 1 : Traduire les entrées classiques en un problème quantique
Dans un premier temps, nous construisons un circuit modèle sans valeurs de paramètres spécifiques. Nous fournissons également des marqueurs, appelés CutWire, pour annoter la position des découpes. Pour l'exemple à petite échelle, nous considérons un circuit MBL à 10 qubits.
num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)
Rappelons que nous cherchons à calculer la valeur d'espérance de l'observable lorsque . Nous attribuerons des valeurs aléatoires au paramètre .
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
params
[0,
0.2376615174332788,
0.28244289857682414,
0.019248960591717768,
0.46140600996102477,
0.31408025180068433,
0.718184005135733,
0.991153920182475,
0.09289485768301442,
0.8857848280067783,
0.6177529765767047]
Nous annotons maintenant le circuit pour la découpe en insérant les instructions CutWire appropriées pour créer deux découpes à peu près égales. Nous définissons use_cut=True dans la fonction, et permettons l'annotation apr ès qubits, étant le nombre de qubits dans le circuit original.
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)
Étape 2 : Optimiser le problème pour l'exécution sur le matériel quantique
Ensuite, nous découpons le circuit en deux sous-circuits plus petits. Pour cet exemple, nous nous limitons à seulement 2 sous-circuits. Pour cela, nous utilisons l'Addon Qiskit : Circuit Cutting.
Découper le circuit en sous-circuits plus petits
Découper le fil en un point augmente le nombre de qubits de un. En plus du qubit original, il y a désormais un qubit supplémentaire servant de marqueur pour le circuit après la découpe. L'image suivante en donne une représentation :
Cet addon utilise la fonction cut_wires pour tenir compte des qubits supplémentaires résultant de la découpe.
mbl_move = cut_wires(mbl_cut)
Créer et étendre les observables
Nous construisons maintenant l'observable . Puisque le résultat idéal de pour chaque est , le résultat idéal de est également .
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observable
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])
Cependant, note que le nombre de qubits dans le circuit a augmenté après l'insertion des opérations virtuelles Move à 2 qubits résultant de la découpe. Par conséquent, nous devons également étendre les observables en insérant des identités pour les adapter au circuit actuel.
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
Note que chaque observable a maintenant été étendu pour prendre en compte sept qubits, comme dans le circuit avec l'opération Move, au lieu des 6 qubits originaux. Ensuite, nous partitionnons le circuit en deux sous-circuits.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
Visualisons les sous-circuits
subcircuits = partitioned_problem.subcircuits
subcircuits[0].draw("mpl", fold=-1)
subcircuits[1].draw("mpl", fold=-1)
Les observables ont également été partitionnés pour correspondre aux sous-circuits
subobservables = partitioned_problem.subobservables
subobservables
{0: PauliList(['IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IZIIII',
'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']),
1: PauliList(['ZIIII', 'IZIII', 'IIZII', 'IIIZI', 'IIIIZ', 'IIIII', 'IIIII',
'IIIII', 'IIIII', 'IIIII'])}
Note que chaque sous-circuit génère un certain nombre d'échantillons. La reconstruction prend en compte le résultat de chacun de ces échantillons. Chacun de ces échantillons est appelé une subexperiment (sous-expérience).
L'extension de l'observable à l'aide de l'opération Move nécessite une structure de données PauliList. Nous pouvons également créer l'observable dans la structure de données plus générique SparsePauliOp, qui sera utile ultérieurement lors de la reconstruction des sous-expériences.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
M_z
SparsePauliOp(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII', 'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII', 'IIIIIIIIZI', 'IIIIIIIIIZ'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
Examinons deux exemples où les qubits découpés sont mesurés dans deux bases différentes. Premièrement, la mesure est effectuée dans la base Z standard, puis dans la base X.
subexperiments[0][6].draw("mpl", fold=-1)
subexperiments[0][2].draw("mpl", fold=-1)
Transpiler chaque sous-expérience
Actuellement, nous devons transpiler nos circuits avant de les soumettre pour exécution. Nous allons donc transpiler chaque circuit des sous-expériences en premier.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
Nous devons maintenant transpiler chacun des circuits des sous-expériences. Pour cela, nous créons d'abord un gestionnaire de passes, puis nous l'utilisons pour transpiler chacun des circuits.
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
isa_subexperiments[0][0].draw("mpl", fold=-1, idle_wires=False)
Étape 3 : Exécuter à l'aide des primitives Qiskit
Nous allons maintenant exécuter chaque circuit des sous-expériences. Qiskit-addon-cutting utilise SamplerV2 pour exécuter les sous-expériences.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
Étape 4 : Post-traiter et retourner le résultat dans le format classique souhaité
Une fois les circuits exécutés, nous devons récupérer les résultats et reconstruire la valeur d'espérance pour le circuit non découpé et l'observable original.
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9674376845359803
Vérification croisée
Exécutons maintenant le circuit sans découpe et vérifions le résultat obtenu. Note que pour l'exécution du circuit non découpé, nous pourrions utiliser directement EstimatorV2 pour calculer les valeurs d'espérance. Mais nous utiliserons la même Primitive tout au long. Nous utiliserons donc SamplerV2 pour obtenir la distribution de probabilité et calculer la valeur d'espérance à l'aide de la fonction sampled_expectation_value.
Nous devons d'abord transpiler le circuit mbl non découpé.
sampler = SamplerV2(mode=backend)
if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)
Ensuite, nous construisons le pub et exécutons le circuit non découpé.
pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9498046875000001
Nous constatons que la valeur d'espérance obtenue via la découpe de fils est plus proche de la valeur idéale de que celle du circuit non découpé. Passons maintenant à une échelle supérieure.
Partie II. Passage à l'échelle !
Précédemment, nous avons présenté les résultats pour un circuit MBL à 10 qubits. Nous montrons maintenant que l'amélioration de la valeur d'espérance est également obtenue pour des circuits plus grands. Pour le démontrer, nous répétons le processus pour un circuit MBL à 60 qubits.
Étape 1 : Traduire les entrées classiques en un problème quantique
num_qubits = 60
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
Nous créons un ensemble aléatoire de valeurs pour
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
Ensuite, nous construisons le circuit découpé
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)
Étape 2 : Optimiser le problème pour l'exécution sur le matériel quantique
Comme montré dans l'exemple à petite échelle, nous partitionnons le circuit et l'observable pour les expériences de découpe.
mbl_move = cut_wires(mbl_cut)
# Define observable
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)
# Partition the circuit into subcircuits
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
# Get subcircuits
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
Nous créons également un objet SparsePauliOp pour l'observable avec les coefficients appropriés.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
Ensuite, nous générons les sous-expériences et transpilons chaque circuit de la sous-expérience.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
Étape 3 : Exécuter à l'aide des primitives Qiskit
Nous utilisons le mode Batch pour exécuter tous les circuits des sous-expériences.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
Étape 4 : Post-traiter et retourner le résultat dans le format classique souhaité
Récupérons maintenant les résultats de chaque circuit des sous-expériences et reconstruisons la valeur d'espérance correspondant au circuit non découpé et à l'observable original.
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9631355921427409
Vérification croisée
Comme dans l'exemple à petite échelle, nous obtiendrons une fois de plus la valeur d'espérance en exécutant le circuit non découpé, et comparerons le résultat avec la découpe de circuit. Nous utiliserons SamplerV2 pour maintenir l'uniformité dans l'utilisation des Primitives.
sampler = SamplerV2(mode=backend)
if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)
pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9426757812499998
Visualisation
Visualisons l'amélioration obtenue dans la valeur d'espérance en utilisant la découpe de fils.
ax = plt.gca()
methods = ["cut", "uncut"]
values = [reconstructed_expval, uncut_expval]
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
ax.set_ylim([0.85, 1.02])
plt.text(0.3, 0.99, "Exact result")
plt.show()
Conclusion
Nous observons que, aussi bien dans les problèmes à petite qu'à grande échelle, la découpe de fils conduit à un meilleur résultat que le circuit non découpé. Note qu'aucune technique d'atténuation d'erreurs n'a été utilisée pour ces expériences. Par conséquent, l'amélioration des résultats obtenue est uniquement due à la découpe de fils. Il pourrait être possible d'améliorer davantage les résultats en utilisant différentes méthodes d'atténuation conjointement avec la découpe de circuit.
De plus, dans ce notebook, nous avons exécuté les deux sous-circuits sur le même matériel. Dans [5], [6], les auteurs présentent une méthode pour distribuer les sous-circuits sur différents matériels en utilisant les informations de bruit afin de maximiser la suppression du bruit et de paralléliser le processus.
Annexe : considérations sur la mise à l'échelle des ressources
Le nombre de circuits à exécuter augmente avec le nombre de découpes. Par conséquent, bien que de nombreuses découpes puissent produire des sous-circuits plus petits, améliorant ainsi davantage les performances, cela entraîne également un nombre significativement élevé d'exécutions de circuits, ce qui peut ne pas être pratique dans la plupart des cas. Ci-dessous, nous montrons un exemple du nombre de sous-circuits correspondant au nombre de découpes pour un circuit à 50 qubits.
Note que même pour cinq découpes, le nombre de sous-expériences est d'environ 200 000. Par conséquent, la découpe de circuit ne devrait être utilisée que lorsque le nombre de découpes est faible.
Un exemple de circuit adapté et un exemple de circuit inadapté à la découpe
Circuit adapté à la découpe
Comme mentionné précédemment, un circuit est adapté à la découpe lorsqu'il peut être partitionné en sous-circuits disjoints plus petits avec un petit nombre de découpes. Tout circuit efficace en termes de matériel, c'est-à-dire un circuit qui nécessite peu ou pas de portes SWAP lorsqu'il est mappé sur la carte de couplage du matériel, est en général adapté à la découpe. Ci-dessous, nous montrons un exemple d'un ansatz à préservation d'excitation, utilisé en chimie quantique. Note qu'un tel circuit peut être partitionné en deux sous-circuits avec une seule découpe, quel que soit le nombre de qubits.

Circuit inadapté à la découpe
Un circuit est inadapté à la découpe si, en général, le nombre de découpes nécessaires pour former des partitions disjointes croît significativement avec la profondeur ou le nombre de qubits. Rappelons qu'avec chaque découpe, un qubit supplémentaire est nécessaire. Ainsi, avec le nombre de découpes, le nombre effectif de qubits augmente également. Ci-dessous, nous montrons un exemple d'un circuit de Grover à 3 qubits avec une instance de découpe possible.
Nous constatons que trois découpes sont nécessaires, et que la découpe est plus verticale qu'horizontale. Cela signifie que le nombre de découpes devrait croître linéairement avec le nombre de qubits, ce qui n'est pas favorable à la découpe.
Références
[1] Peng, T., Harrow, A. W., Ozols, M., & Wu, X. (2020). Simulating large quantum circuits on a small quantum computer. Physical review letters, 125(15), 150504.
[2] Tang, W., Tomesh, T., Suchara, M., Larson, J., & Martonosi, M. (2021, April). Cutqc: using small quantum computers for large quantum circuit evaluations. In Proceedings of the 26th ACM International conference on architectural support for programming languages and operating systems (pp. 473-486).
[3] Perlin, M. A., Saleem, Z. H., Suchara, M., & Osborn, J. C. (2021). Quantum circuit cutting with maximum-likelihood tomography. npj Quantum Information, 7(1), 64.
[4] Majumdar, R., & Wood, C. J. (2022). Error mitigated quantum circuit cutting. arXiv preprint arXiv:2211.13431.
[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V., & Simmhan, Y. (2023). Parallelizing Quantum-Classical Workloads: Profiling the Impact of Splitting Techniques. In 2023 IEEE International Conference on Quantum Computing and Engineering (QCE) (Vol. 1, pp. 990-1000). IEEE.
[6] Bhoumik, D., Majumdar, R., Saha, A., & Sur-Kolay, S. (2023). Distributed Scheduling of Quantum Circuits with Noise and Time Optimization. arXiv preprint arXiv:2309.06005.
Enquête sur le tutoriel
Réponds à cette courte enquête pour nous faire part de tes commentaires sur ce tutoriel. Tes retours nous aideront à améliorer nos contenus et l'expérience utilisateur.