Découpe de fils pour l'estimation des valeurs d'espérance
Estimation d'utilisation : 22 secondes sur un processeur Heron (REMARQUE : Il s'agit d'une estimation uniquement. Ton temps d'exécution peut varier.)
Objectifs d'apprentissage
Après ce tutoriel, les utilisateurs devraient comprendre :
- Comment utiliser
qiskit-addon-cuttingpour partitionner un grand circuit en sous-circuits plus petits, réduisant ainsi l'effet du bruit
Prérequis
Nous suggérons aux utilisateurs d'être familiers avec le sujet suivant avant de suivre ce tutoriel :
- Utilisation de la primitive Sampler, qui est utilisée dans ce workflow
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 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 ; consulte la documentation ainsi que d'autres ressources d'introduction pour une explication détaillée de la technique.
Ce tutoriel 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 : [7]) 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 de portes, ils sont censés être moins sensibles au bruit. Ce tutoriel présente un exemple où cette méthode peut être utilisée pour supprimer efficacement le bruit dans le système.
Configuration requise
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.10.0 ou ultérieur (
pip install qiskit-addon-cutting) - Qiskit addon utils 0.3 ou ultérieur (
pip install qiskit-addon-utils) - Qiskit Aer (
pip install qiskit-aer)
Configuration
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-aer 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_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, Batch
Exemple à petite échelle avec simulateur
Ce tutoriel implémente un pattern Qiskit pour simuler un circuit de Localisation à Plusieurs Corps (MBL) unidimensionnel (1D). 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 , quelle que soit la valeur de . Plus de détails sur ce circuit sont disponibles dans cet article.
Note que dans un simulateur sans bruit, la valeur d'espérance obtenue avec et sans découpe de circuit sera identique.
Étape 1 : Transposer les entrées classiques en un problème quantique
Construire le circuit MBL 1D
Tout d'abord, nous présentons une fonction pour construire le circuit MBL 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)
Nous calculons la valeur d'espérance moyenne sur tous les qubits pour . Comme la valeur d'espérance idéale de , la valeur d'espérance idéale de est également . Les paramètres sont sélectionnés aléatoirement.
np.random.seed(42)
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
Le circuit doit être annoté en insérant des CutWire aux emplacements souhaités pour le partitionner. Pour ce tutoriel, nous optons pour une partition égale. Le circuit MBL est conçu de sorte que définir use_cut=True dans la fonction insère l'annotation correctement après qubits, étant le nombre de qubits dans le circuit original. Nous avons également assigné 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)
Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique
Découper le circuit en sous-circuits plus petits
Maintenant, nous partitionnons le circuit en deux sous-circuits plus petits à l'aide de qiskit-addon-cutting. qiskit-addon-cutting ajoute une porte virtuelle Move pour séparer l'emplacement de découpe de fil en ajustant appropriement le nombre de qubits. Nous créons maintenant le circuit avec cette porte virtuelle. Puisqu'il y a une découpe de fil, le nombre de qubits associés sera augmenté de 1.
mbl_move = cut_wires(mbl_cut)
mbl_move.draw("mpl", fold=-1)
Construire et étendre les observables
L'observable, tel que défini précédemment, sera la moyenne de sur chaque qubit. Cependant, après l'insertion de la porte virtuelle Move, le nombre effectif de qubits dans le circuit augmente. L'observable doit également être étendu en conséquence pour tenir compte de ce changement du nombre de qubits. Note que l'observable agit toujours trivialement (comme ) 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)]
)
observable
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
Maintenant, le circuit peut être partitionné le long de la porte Move et nous obtenons les sous-circuits, ainsi que le sous-observable, qui est la partie de l'observable original associée à chaque sous-circuit.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
Voici la visualisation des deux sous-circuits :
subcircuits[0].draw("mpl", fold=-1)
subcircuits[1].draw("mpl", fold=-1)
L'extension de l'observable à l'aide de l'opération Move nécessite une structure de données PauliList. Pour reconstruire la valeur d'espérance du circuit non découpé, nous avons besoin de l'observable au format SparsePauliOp.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
Comme discuté précédemment, pour chaque découpe, 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 la base. La fonction generate_cutting_experiments crée tous ces circuits nécessaires et les coefficients associés à chaque circuit requis pour la reconstruction. Plus de détails dans cet article.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
Transpiler les circuits sur le backend
Pour ce premier exemple n'impliquant que de la simulation, nous transpilons le circuit dans l'ensemble de portes de base du backend :
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)
<IBMBackend('ibm_fez')>
Étape 3 : Exécuter à l'aide des primitives Qiskit
Maintenant, exécutons 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-traiter et renvoyer le résultat dans le format classique souhaité
Nous récupérons maintenant le résultat de chaque sous-expérience exécutée et reconstruisons la valeur d'espérance 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_expval
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)
Text(0, 0.5, '$M_Z$')
Exemple à grande échelle sur du matériel réel
Nous démontrons maintenant la découpe de fils pour un circuit MBL à 60 qubits. Le circuit non découpé, ainsi que le circuit découpé, seront exécutés sur du matériel 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()
uncut_expval
0.9202473958333336
Prochaines étapes
Si tu as trouvé ce travail intéressant, tu pourrais être intéressé par le matériel suivant :
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.
[7] Majumdar, R. (2024). Efficient Reduction of Resources and Noise in Discrete Quantum Computing Circuits (Doctoral dissertation, Indian Statistical Institute - Kolkata). https://www.proquest.com/openview/b481def90b1cc80e6b58a77c99e8385c/1?pq-origsite=gscholar&cbl=2026366&diss=y