Skip to main content
IBM Quantum Platform

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-cutting pour 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, MjM_j désigne un ensemble de bases (généralement Pauli X, Y et Z), et PiP_i désigne un ensemble d'états propres (généralement 0|0\rangle, 1|1\rangle, +|+\rangle et +i|+i\rangle ).

wc-1.png wc-2.png

É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, Batch

Exemple 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 : θ\theta et ϕ\vec{\phi}. Lorsque θ\theta est défini sur 00 et que l'état initial est préparé dans 0|0\rangle pour tous les qubits, la valeur attendue idéale de Zi\langle Z_i \rangle est +1+1 pour chaque site de qubit ii, quelles que soient les valeurs de ϕ\vec{\phi}. 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:

Output of the previous code cell

Nous calculons la valeur attendue moyenne O=1niZiO = \frac{1}{n} \sum_i Z_i sur l'ensemble des qubits pour θ=0\theta = 0. Étant donné que la valeur attendue idéale de Zi=1\langle Z_i \rangle = 1 \forall ii, la valeur attendue idéale de OO est également 11. Les paramètres ϕ\phi sont choisis au hasard.

np.random.seed(42)
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis

Pour 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 n2\frac{n}{2} qubits, où nn 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:

Output of the previous code cell

É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:

Output of the previous code cell

Construire et étendre l'observable

L'observable, telle que définie précédemment, correspondra à la moyenne de l' ZZ 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 II ) 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

Output:

PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
           'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
           'IIIIIIIIZI', 'IIIIIIIIIZ'])
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs

Output:

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.subobservables

Nous représentons ici les deux sous-circuits :

subcircuits[0].draw("mpl", fold=-1)

Output:

Output of the previous code cell
subcircuits[1].draw("mpl", fold=-1)

Output:

Output of the previous code cell

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_expval

Output:

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$')
Output of the previous code cell

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:

Output of the previous code cell
uncut_expval

Output:

0.9202473958333336

Etapes suivantes

Recommandations

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

Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.