Aller au contenu principal

Coupe de circuits pour la réduction de profondeur

Estimation d'utilisation : huit minutes sur un processeur Eagle (REMARQUE : il s'agit uniquement d'une estimation. Ton temps d'exécution peut varier.)

Contexte

Ce tutoriel montre comment construire un Qiskit pattern pour couper des portes dans un circuit quantique afin de réduire la profondeur du circuit. Pour une discussion plus approfondie sur la coupe de circuits, consulte la documentation de l'addon Qiskit pour la coupe de circuits.

Prérequis

Avant de commencer ce tutoriel, assure-toi d'avoir installé les éléments suivants :

  • Qiskit SDK v2.0 ou ultérieur, avec prise en charge de la visualisation
  • Qiskit Runtime v0.22 ou ultérieur (pip install qiskit-ibm-runtime)
  • Addon Qiskit pour la coupe de circuits v0.9.0 ou ultérieur (pip install qiskit-addon-cutting)

Configuration

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
import numpy as np

from qiskit.circuit.library import EfficientSU2
from qiskit.quantum_info import PauliList, Statevector, SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

from qiskit_addon_cutting import (
cut_gates,
generate_cutting_experiments,
reconstruct_expectation_values,
)

from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2

Étape 1 : Convertir les entrées classiques en un problème quantique

Nous allons implémenter notre patron Qiskit en suivant les quatre étapes décrites dans la documentation. Dans ce cas, nous allons simuler des valeurs d'espérance sur un circuit d'une certaine profondeur en coupant des portes générant des portes swap et en exécutant des sous-expériences sur des circuits moins profonds. La coupe de portes est pertinente pour les étapes 2 (optimiser le circuit pour l'exécution quantique en décomposant les portes distantes) et 4 (post-traitement pour reconstruire les valeurs d'espérance sur le circuit original). Dans la première étape, nous allons générer un circuit à partir de la bibliothèque de circuits Qiskit et définir quelques observables.

  • Entrée : Paramètres classiques pour définir un circuit
  • Sortie : Circuit abstrait et observables
circuit = EfficientSU2(num_qubits=4, entanglement="circular").decompose()
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
observables = PauliList(["ZZII", "IZZI", "IIZZ", "XIXI", "ZIZZ", "IXIX"])
circuit.draw("mpl", scale=0.8, style="iqp")

Sortie de la cellule de code précédente

Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique

  • Entrée : Circuit abstrait et observables
  • Sortie : Circuit cible et observables produits en coupant les portes distantes pour réduire la profondeur du circuit transpilé

Nous choisissons une disposition initiale qui nécessite deux permutations (swaps) pour exécuter les portes entre les qubits 3 et 0 et deux autres permutations pour ramener les qubits à leurs positions initiales. Nous choisissons optimization_level=3, qui est le niveau d'optimisation le plus élevé disponible avec un gestionnaire de passes prédéfini.

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=circuit.num_qubits, simulator=False
)

pm = generate_preset_pass_manager(
optimization_level=3, initial_layout=[0, 1, 2, 3], backend=backend
)
transpiled_qc = pm.run(circuit)

Carte de couplage montrant les qubits qui devront être permutés

print(f"Transpiled circuit depth: {transpiled_qc.depth()}")
transpiled_qc.draw("mpl", scale=0.4, idle_wires=False, style="iqp", fold=-1)
Transpiled circuit depth: 103

Sortie de la cellule de code précédente

Trouver et couper les portes distantes : Nous allons remplacer les portes distantes (portes connectant des qubits non locaux, 0 et 3) par des objets TwoQubitQPDGate en spécifiant leurs indices. cut_gates remplacera les portes aux indices spécifiés par des objets TwoQubitQPDGate et retournera également une liste d'instances QPDBasis -- une pour chaque décomposition de porte. L'objet QPDBasis contient des informations sur la façon de décomposer les portes coupées en opérations à un seul qubit.

# Find the indices of the distant gates
cut_indices = [
i
for i, instruction in enumerate(circuit.data)
if {circuit.find_bit(q)[0] for q in instruction.qubits} == {0, 3}
]

# Decompose distant CNOTs into TwoQubitQPDGate instances
qpd_circuit, bases = cut_gates(circuit, cut_indices)

qpd_circuit.draw("mpl", scale=0.8)

Sortie de la cellule de code précédente

Générer les sous-expériences à exécuter sur le backend : generate_cutting_experiments accepte un circuit contenant des instances TwoQubitQPDGate et des observables sous forme de PauliList.

Pour simuler la valeur d'espérance du circuit complet, de nombreuses sous-expériences sont générées à partir de la distribution de quasi-probabilité conjointe des portes décomposées, puis exécutées sur un ou plusieurs backends. Le nombre d'échantillons prélevés dans la distribution est contrôlé par num_samples, et un coefficient combiné est attribué pour chaque échantillon unique. Pour plus d'informations sur le calcul des coefficients, consulte la documentation explicative.

# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observables, num_samples=np.inf
)

À titre de comparaison, nous constatons que les sous-expériences QPD seront moins profondes après la coupe des portes distantes : Voici un exemple d'une sous-expérience choisie arbitrairement, générée à partir du circuit QPD. Sa profondeur a été réduite de plus de la moitié. De nombreuses sous-expériences probabilistes de ce type doivent être générées et évaluées afin de reconstruire une valeur d'espérance du circuit plus profond.

# Transpile the decomposed circuit to the same layout
transpiled_qpd_circuit = pm.run(subexperiments[100])

print(f"Original circuit depth after transpile: {transpiled_qc.depth()}")
print(
f"QPD subexperiment depth after transpile: {transpiled_qpd_circuit.depth()}"
)
transpiled_qpd_circuit.draw(
"mpl", scale=0.6, style="iqp", idle_wires=False, fold=-1
)
Original circuit depth after transpile: 103
QPD subexperiment depth after transpile: 46

Sortie de la cellule de code précédente

En revanche, la coupe entraîne un besoin d'échantillonnage supplémentaire. Ici, nous coupons trois portes CNOT, ce qui résulte en un surcoût d'échantillonnage de 939^3. Pour en savoir plus sur le surcoût d'échantillonnage engendré par la coupe de circuits, consulte la documentation du Circuit Knitting Toolbox.

print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 729.0

Étape 3 : Exécuter à l'aide des primitives Qiskit

Exécute les circuits cibles (les "sous-expériences") avec la primitive Sampler.

  • Entrée : Circuits cibles
  • Sortie : Distributions de quasi-probabilités
# Transpile the subexperiments to the backend's instruction set architecture (ISA)
isa_subexperiments = pm.run(subexperiments)

# Set up the Qiskit Runtime Sampler primitive. For a fake backend, this will use a local simulator.
sampler = SamplerV2(backend)

# Submit the subexperiments
job = sampler.run(isa_subexperiments)
# Retrieve the results
results = job.result()
print(job.job_id())
czypg1r6rr3g008mgp6g

Étape 4 : Post-traitement et restitution du résultat dans le format classique souhaité

Utilise les résultats des sous-expériences, les sous-observables et les coefficients d'échantillonnage pour reconstruire la valeur d'espérance du circuit original.

Entrée : Distributions de quasi-probabilités Sortie : Valeurs d'espérance reconstruites

reconstructed_expvals = reconstruct_expectation_values(
results,
coefficients,
observables,
)
# Reconstruct final expectation value
final_expval = np.dot(reconstructed_expvals, [1] * len(observables))
print("Final reconstructed expectation value")
print(final_expval)
Final reconstructed expectation value
1.0751342773437473
ideal_expvals = [
Statevector(circuit).expectation_value(SparsePauliOp(observable))
for observable in observables
]
print("Ideal expectation value")
print(np.dot(ideal_expvals, [1] * len(observables)).real)
Ideal expectation value
1.2283177520039992

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 notre contenu et l'expérience utilisateur.

Lien vers l'enquête