Coupe de fil pour l'estimation des valeurs attendues
Estimation du temps d'exécution : 22 secondes sur un processeur Heron (REMARQUE : il s'agit uniquement d'une estimation. (Votre temps d'exécution peut varier.)
Résultats d'apprentissage
À l'issue de ce tutoriel, les utilisateurs devraient être en mesure de :
- Comment utiliser
qiskit-addon-cuttingpour diviser un grand circuit en sous-circuits plus petits, réduisant ainsi l'effet du bruit
Prérequis
Nous recommandons aux utilisateurs de se familiariser avec le sujet suivant avant de suivre ce tutoriel :
- Utilisation de la primitive « Sampler », qui est utilisée dans ce flux de travail
Arrière-plan
Le « tricotage de circuits » est un terme générique qui désigne diverses méthodes permettant de diviser un circuit en plusieurs sous-circuits plus petits, comportant moins de portes logiques ou de qubits. Chacun des sous-circuits peut être exécuté indépendamment, et le résultat final est obtenu grâce à un post-traitement classique appliqué aux résultats de chaque sous-circuit. Cette technique est disponible dans l 'extension « Circuit cutting » de Qiskit; consultez la documentation ainsi que d'autres ressources d'introduction pour obtenir une explication détaillée de cette technique.
Ce tutoriel porte sur une méthode appelée « coupure de fil », qui consiste à diviser le circuit le long du fil [1], [2]. Il convient de noter que le partitionnement est simple dans les circuits classiques, car le résultat au point de partition peut être déterminé de manière déterministe et prend la valeur 0 ou 1. Cependant, l'état du qubit au moment de la coupure est, en général, un état mixte. Par conséquent, chaque sous-circuit doit être mesuré à plusieurs reprises dans différentes bases (généralement une base tomographiquement complète, telle que la base de Pauli [3], [4] ) et préparé en conséquence dans son état propre. La figure ci-dessous (source : [7] ) illustre un exemple de découpage d'un état GHZ à quatre 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 comporte moins de qubits et de portes logiques, on s'attend à ce qu'ils soient moins sensibles au bruit. Ce tutoriel présente un exemple illustrant comment cette méthode peut être utilisée pour réduire efficacement le bruit dans le système.
Exigences
Avant de commencer ce tutoriel, assurez-vous que les éléments suivants sont installés :
- Qiskit SDK v2.0 ou plus tard, avec prise en charge de la visualisation
- Qiskit Runtime v0.22 ou plus tard (
pip install qiskit-ibm-runtime) - Module complémentaire Qiskit « Circuit cutting » v0.10.0 ou version ultérieure (
pip install qiskit-addon-cutting) - Module complémentaire Qiskit utils 0.3 ou version ultérieure (
pip install qiskit-addon-utils) - Qiskit Aer (
pip install qiskit-aer)
Configuration
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_aer import AerSimulator
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, BatchExemple de simulateur à petite échelle
Ce tutoriel met en œuvre un modèle Qiskit permettant de simuler un circuit de localisation à N corps (MBL) unidimensionnel (en 1D ). Le circuit MBL est un circuit efficace sur le plan matériel et est paramétré par deux paramètres : et . Lorsque est défini sur et que l'état initial est préparé dans pour tous les qubits, la valeur attendue idéale de est pour chaque site de qubit , quelles que soient les valeurs de . Vous trouverez plus de détails sur ce circuit dans cet article.
Il convient de noter que, dans un simulateur sans bruit, la valeur attendue obtenue avec ou sans coupure du circuit sera la même.
Étape 1 : Mettre en correspondance les entrées classiques avec un problème quantique
Construire le circuit MBL de l' 1D
Nous présentons tout d'abord une fonction permettant de construire le circuit MBL d' 1D.
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)num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)Output:
Nous calculons la valeur attendue moyenne sur l'ensemble des qubits pour . Étant donné que la valeur attendue idéale de , la valeur attendue idéale de est également . Les paramètres sont choisis au hasard.
np.random.seed(42)
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phisPour diviser le circuit, il faut y insérer des annotations « CutWire » aux emplacements souhaités. Pour ce tutoriel, nous avons choisi un partage en parts égales. Le circuit MBL est conçu de telle sorte que le réglage use_cut=True dans la fonction insère correctement l'annotation après qubits, où correspond au nombre de qubits dans le circuit d'origine. Nous avons également attribué les paramètres générés aléatoirement au circuit.
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)Output:
Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique
Divisez le circuit en sous-circuits plus petits
Nous allons maintenant diviser le circuit en deux sous-circuits plus petits à l'aide de qiskit-addon-cutting. qiskit-addon-cutting ajoute une porte virtuelle Move pour diviser l'emplacement de la coupure du fil en ajustant de manière appropriée le nombre de qubits. Nous allons maintenant créer le circuit à l'aide de cette porte logique virtuelle. Comme un fil a été coupé, le nombre de qubits associés augmentera d'une unité.
mbl_move = cut_wires(mbl_cut)
mbl_move.draw("mpl", fold=-1)Output:
Construire et étendre l'observable
L'observable, telle que définie précédemment, correspondra à la moyenne de l' e sur chaque qubit. Cependant, dès lors que l'on insère la porte virtuelle Move , le nombre effectif de qubits dans le circuit augmente. L'observable doit également être étendue en conséquence pour tenir compte de cette variation du nombre de qubits. Notez que l'observable agit toujours de manière triviale (comme dans ) sur le qubit supplémentaire ajouté pour la porte virtuelle Move .
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observableOutput:
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])
new_obs = expand_observables(observable, mbl, mbl_move)
new_obsOutput:
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
On peut désormais diviser le circuit le long de Move la porte et obtenir ainsi les sous-circuits, ainsi que la sous-observable, qui correspond à la partie de l'observable d'origine associée à chaque sous-circuit.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservablesNous représentons ici les deux sous-circuits :
subcircuits[0].draw("mpl", fold=-1)Output:
subcircuits[1].draw("mpl", fold=-1)Output:
Pour étendre l'observable à l'aide de l'opération Move , il faut une PauliList structure de données. Pour calculer la valeur attendue du circuit d'origine, nous avons besoin de l'observable sous la SparsePauliOp forme
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)Comme nous l'avons vu précédemment, pour chaque coupure, le circuit en amont doit être mesuré dans une base de Pauli, et le circuit en aval doit être préparé dans l'état propre de cette base. La fonction generate_cutting_experiments crée tous les circuits nécessaires ainsi que les coefficients associés à chaque circuit requis pour la reconstruction. Pour plus de détails, consultez cet article.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)Transpiler les circuits vers le backend
Pour le premier exemple, qui ne concerne que la simulation, nous transpilons le circuit vers l'ensemble de portes de base du backend :
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)Output:
<IBMBackend('ibm_fez')>
Étape 3 : Exécutez à l'aide d' Qiskit primitives
Maintenant, lancez chaque sous-expérience :
pm_basis = generate_preset_pass_manager(
optimization_level=2, basis_gates=backend.configuration().basis_gates
)
basis_subexperiments = {
label: pm_basis.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}sampler = SamplerV2(mode=AerSimulator())
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in basis_subexperiments.items()
}Étape 4 : Post-traitement et restitution du résultat dans le format classique souhaité
Nous récupérons maintenant le résultat de chaque exécution de sous-expérience et reconstituons la valeur attendue du circuit non découpé :
# 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_expvalOutput:
np.float64(0.9953821063041687)
methods = [
"Uncut",
"Wire cut",
]
values = [
1,
reconstructed_expval,
] # since the ideal expectation value in noiseless simulation is +1
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylabel(r"$M_Z$", fontsize=12)Output:
Text(0, 0.5, '$M_Z$')
Exemple de matériel à grande échelle
Nous allons maintenant présenter la technique de « wire cutting » pour un circuit MBL de 60 qubits. Les circuits non découpés, tout comme les circuits découpés, seront réalisés sur du matériel d' IBM Quantum® :
num_qubits = 60
depth = 2
# construct the circuit
mbl = MBLChainCircuit(num_qubits, depth)
# create parameters
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
# construct the cut circuit
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_move = cut_wires(mbl_cut)
# Define observable and expand to account for the wire cut
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)
# Construct a SparsePauliOp version of the observable for later use in reconstruction
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
# Partition the circuit and get subcircuits and subobservables
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
# Obtain subexperiments and coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
# Transpile the subexperiments to the backend
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
# Execute the subexperiments and retrieve results
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
sampler.options.environment.job_tags = ["TUT_WC"]
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
results = {label: job.result() for label, job in jobs.items()}
# Reconstruct the expectation value of the original observable
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
# Compute the uncut circuit to obtain the noisy expectation value for comparison
sampler = SamplerV2(mode=backend)
sampler.options.environment.job_tags = ["TUT_WC"]
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)
# visualize the results
ax = plt.gca()
methods = ["uncut", "cut"]
values = [uncut_expval, reconstructed_expval]
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
plt.text(0.3, 0.95, "Exact result")
plt.show()Output:
uncut_expvalOutput:
0.9202473958333336
Etapes suivantes
Si ce travail vous a paru intéressant, les documents suivants pourraient vous intéresser :
Références
[1] Peng, T., Harrow, A. W., Ozols, M. et Wu, X. (2020). Simulation de grands circuits quantiques sur un petit ordinateur quantique. Physical review letters, 125(15), 150504.
[2] Tang, W., Tomesh, T., Suchara, M., Larson, J. et Martonosi, M. (2021, avril). Cutqc : utilisation de petits ordinateurs quantiques pour l'évaluation de circuits quantiques de grande taille. 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., et Osborn, J. C. (2021). Découpage de circuits quantiques par tomographie à maximum de vraisemblance. npj Quantum Information, 7(1), 64.
[4] Majumdar, R., & Wood, C. J. (2022). Découpage de circuits quantiques avec atténuation des erreurs. arXiv preprint arXiv:2211.13431.
[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V. et Simmhan, Y. (2023). Paralléliser les charges de travail classiques et quantiques : Profilage de l'impact des techniques de fractionnement. In 2023 IEEE International Conference on Quantum Computing and Engineering (QCE) (Vol. 1, pp. 990-1000). IEEE.
[6] Bhoumik, D., Majumdar, R., Saha, A. et Sur-Kolay, S. (2023). Ordonnancement distribué de circuits quantiques avec optimisation du bruit et du temps. arXiv preprint arXiv:2309.06005.
[7] Majumdar, R. (2024). Réduction efficace des ressources et du bruit dans les circuits d'informatique quantique discrète (Thèse de doctorat, Institut indien de statistique - Kolkata). https://www.proquest.com/openview/b481def90b1cc80e6b58a77c99e8385c/1?pq-origsite=gscholar&cbl=2026366&diss=y