Aller au contenu principal

Introduction à Qiskit

Dans ce notebook, nous allons explorer comment programmer des portes quantiques et des circuits quantiques avec Qiskit, et même comment les exécuter sur des simulateurs et de vrais ordinateurs quantiques grâce aux Qiskit patterns. Nous présenterons ensuite différentes façons d'encoder l'information et terminerons avec un exemple bonus de téléportation quantique.

Avant de commencer

Suis les instructions d'installation et de configuration si tu ne l'as pas encore fait, y compris les étapes pour configurer l'utilisation d'IBM Quantum™ Platform.

Il est recommandé d'utiliser l'environnement de développement Jupyter pour interagir avec les ordinateurs quantiques. N'oublie pas d'installer le support de visualisation supplémentaire recommandé ('qiskit[visualization]'). Tu auras également besoin du package matplotlib pour la deuxième partie de cet exemple.

Pour en savoir plus sur l'informatique quantique en général, consulte le cours sur les bases de l'information quantique sur IBM Quantum Learning

Imports

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
# Import necessary modules for this notebook
import time
import qiskit

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_bloch_multivector, plot_state_qsphere
from qiskit_aer import AerSimulator
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.visualization import plot_histogram
print(qiskit.__version__)
2.3.1

Pour exécuter tes circuits quantiques sur du matériel, tu dois d'abord configurer ton compte. Tu peux le faire comme suit :

  1. Va sur la plateforme IBM Quantum® mise à jour.
  2. Va dans le coin supérieur droit (comme indiqué sur l'image ci-dessus), crée ton jeton API et copie-le dans un endroit sécurisé.
  3. Dans la cellule suivante, remplace deleteThisAndPasteYourAPIKeyHere par ta clé API.
  4. Va dans le coin inférieur gauche (comme indiqué sur l'image ci-dessus) et crée ton instance. Assure-toi de choisir le plan gratuit.
  5. Une fois l'instance créée, copie le code CRN associé. Il se peut que tu doives actualiser la page pour voir l'instance.
  6. Dans la cellule ci-dessous, remplace deleteThisAndPasteYourCRNHere par ton code CRN.

Consulte ce guide pour plus de détails sur la façon de configurer ton compte IBM Cloud®.

⚠️ Note : Traite ta clé API comme tu le ferais avec un mot de passe sécurisé. Consulte le guide Configuration Cloud pour plus d'informations sur l'utilisation de ta clé API dans des environnements sécurisés et non sécurisés.

#your_api_key = "deleteThisAndPasteYourAPIKeyHere"
#your_crn = "deleteThisAndPasteYourCRNHere"

QiskitRuntimeService.save_account(
channel="ibm_quantum_platform",
token=your_api_key,
instance=your_crn,
overwrite=True
)

1. Portes Quantiques et Circuits Quantiques

Les circuits quantiques sont des modèles de calcul quantique dans lesquels un calcul est une séquence de portes quantiques. Regardons quelques-unes des portes quantiques les plus populaires.

Gate X

Une Gate X équivaut à une rotation autour de l'axe X de la sphère de Bloch de π\pi radians. Elle mappe 0|0\rangle vers 1|1\rangle et 1|1\rangle vers 0|0\rangle. C'est l'équivalent quantique de la porte NOT pour les ordinateurs classiques et elle est parfois appelée bit-flip.

X=(0110)X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \\ \end{pmatrix}

# Let's apply an X-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.draw(output='mpl')

Quantum circuit diagram

# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Code output

Gate H

Une Gate Hadamard représente une rotation de π\pi autour de l'axe situé au milieu de l'axe XX et de l'axe ZZ. Elle mappe l'état de base 0|0\rangle vers 0+12\frac{|0\rangle + |1\rangle}{\sqrt{2}}, ce qui signifie qu'une mesure aura des probabilités égales d'être 1 ou 0, créant une 'superposition' d'états. Cet état est également noté +|+\rangle.

H=12(1111)H = \frac{1}{\sqrt{2}}\begin{pmatrix} 1 & 1 \\ 1 & -1 \\ \end{pmatrix}

# Let's apply an H-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.h(0)
qc.draw(output='mpl')

Quantum circuit diagram

# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Code output

Gate CX (Gate CNOT)

La porte NOT contrôlée (ou CNOT ou CX) agit sur deux Qubits. Elle effectue l'opération NOT (équivalent à l'application d'une Gate X) sur le second Qubit uniquement lorsque le premier Qubit est 1|1\rangle et le laisse sinon inchangé. Remarque : Qiskit numérote les bits dans une chaîne de droite à gauche.

CX=(1000010000010010)CX = \begin{pmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 0 & 1\\ 0 & 0 & 1 & 0\\ \end{pmatrix}

# Let's apply a CX-gate on |11>
qc = QuantumCircuit(2)
qc.x(0)
qc.x(1)
qc.cx(0,1)
qc.draw(output='mpl')

Quantum circuit diagram

sv=Statevector(qc)
plot_state_qsphere(sv)

Code output

Crée le premier état de Bell

ϕ+=12(00+11)|\phi^+ \rangle = \frac{1}{\sqrt 2}(|00 \rangle + |11 \rangle)

# Create a Bell state circuit

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0,1)

# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

# Plot the state using q-sphere visualization
sv = Statevector(qc)
plot_state_qsphere(sv)
# q-sphere is useful for visualizing states when Bloch sphere fails to

Code output

Crée le deuxième état de Bell

ϕ=12(0011)|\phi^- \rangle = \frac{1}{\sqrt 2}(|00 \rangle - |11 \rangle)

# Create a circuit with the second Bell state

qc = QuantumCircuit(2)
qc.x(0)
qc.h(0)
qc.cx(0,1)

qc.draw("mpl")

Quantum circuit diagram

L'explication est que :

H1=12(01)=H|1\rangle=\frac{1}{\sqrt{2} }(|0\rangle-|1\rangle) = |-\rangle
# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

Crée l'état GHZ à 3 Qubits

GHZ=12(000+111)|GHZ \rangle = \frac{1}{\sqrt 2}(|000 \rangle + |111 \rangle)

# Create a circuit with 3-qubit GHZ state

qc= QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)

qc.draw("mpl")

Quantum circuit diagram

# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

Crée l'état du logo Qiskit

Qiskit=12(0010+1101)|Qiskit \rangle = \frac{1}{\sqrt 2}(|0010 \rangle + |1101 \rangle)

Centered Image
# Create a circuit with the Qiskit logo state

qc = QuantumCircuit(4)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
qc.cx(0,3)
qc.x(1)

# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

2. Créer et exécuter un programme quantique simple

Les quatre étapes pour écrire un programme quantique avec les Qiskit patterns sont :

  1. Mapper le problème dans un format natif quantique.

  2. Optimiser les circuits et les opérateurs.

  3. Exécuter à l'aide d'une fonction primitive quantique.

  4. Analyser les résultats.

2.1 Map the problem to a quantum-native format

Dans un programme quantique, les circuits quantiques sont le format natif pour représenter les instructions quantiques, et les opérateurs représentent les observables à mesurer. Quand tu crées un circuit, tu vas généralement créer un nouvel objet QuantumCircuit, puis y ajouter des instructions en séquence.

La cellule de code suivante crée un circuit qui produit l'état GHZ, qui est un état dans lequel trois qubits sont totalement intriqués les uns avec les autres.

Le SDK Qiskit utilise la numérotation de bits LSb 0 où le nthn^{th} chiffre a la valeur 1n1 \ll n ou 2n2^n. Pour plus de détails, consulte le sujet Bit-ordering in the Qiskit SDK.

# Create a GHZ state circuit

qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

Consulte QuantumCircuit dans la documentation pour toutes les opérations disponibles.

Quand tu crées des circuits quantiques, tu dois aussi réfléchir au type de données que tu veux récupérer après l'exécution. Qiskit offre deux façons de retourner des données : tu peux obtenir une distribution de probabilité pour un ensemble de qubits que tu choisis de mesurer, ou tu peux obtenir la valeur d'espérance d'un observable. Prépare ta charge de travail pour mesurer ton circuit de l'une de ces deux façons avec les primitives Qiskit (expliquées en détail à l'Étape 3).

Cet exemple mesure des valeurs d'espérance en utilisant le sous-module qiskit.quantum_info, qui est spécifié à l'aide d'opérateurs (objets mathématiques utilisés pour représenter une action ou un processus qui modifie un état quantique). La cellule de code suivante crée six opérateurs de Pauli à trois qubits : ZZZ, ZZX, ZII, XXI, ZZI et III.

# Set up six different observables.

observables_labels = ["ZZZ", "ZZX", "ZII", "XXI", "ZZI", "III"]

observables = [SparsePauliOp(label) for label in observables_labels]
print(observables)
[SparsePauliOp(['ZZZ'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZX'],
coeffs=[1.+0.j]), SparsePauliOp(['ZII'],
coeffs=[1.+0.j]), SparsePauliOp(['XXI'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZI'],
coeffs=[1.+0.j]), SparsePauliOp(['III'],
coeffs=[1.+0.j])]

Ici, un opérateur comme ZZI est une notation abrégée pour le produit tensoriel ZZIZ\otimes Z\otimes I, ce qui signifie mesurer Z sur le qubit 2 et Z sur le qubit 1 ensemble, et obtenir des informations sur la corrélation entre le qubit 2 et le qubit 1. Les valeurs d'espérance comme celle-ci sont aussi typiquement notées Z2Z1\langle Z_2 Z_1 \rangle.

Si l'état que l'on observe est l'état GHZ à trois qubits, alors la mesure de Z2Z1\langle Z_2 Z_1 \rangle devrait être 1.

2.2 Optimize the circuits and operators

Quand tu exécutes des circuits sur un appareil, il est important d'optimiser l'ensemble des instructions que le circuit contient et de minimiser la profondeur globale (approximativement le nombre d'instructions) du circuit. Cela garantit que tu obtiens les meilleurs résultats possibles en réduisant les effets des erreurs et du bruit. De plus, les instructions du circuit doivent être conformes à l'architecture du jeu d'instructions (ISA) d'un Backend et doivent prendre en compte les portes de base et la connectivité des qubits de l'appareil.

Le code suivant instancie un vrai appareil auquel soumettre une tâche, et transforme le circuit et les observables pour correspondre à l'ISA de ce Backend. Si tu n'as pas encore enregistré tes identifiants, suis les instructions ici pour t'authentifier avec ton jeton API.

# Choose a real backend
service = QiskitRuntimeService(channel='ibm_quantum_platform',)
backend = service.least_busy(min_num_qubits=156)
# print backend details
print(
f"Name: {backend.name}\n"
f"Version: {backend.backend_version}\n"
f"No. of qubits: {backend.num_qubits}\n"
f"Processor type: {backend.processor_type}\n"
)
Name: ibm_marrakesh
Version: 1.0.21
No. of qubits: 156
Processor type: {'family': 'Heron', 'revision': '2'}
# option to use the AerSimulator instead of a real quantum device
seed_sim=42
backend=AerSimulator.from_backend(backend,seed_simulator=seed_sim)

Transpile le circuit en circuit ISA

# Convert to an ISA circuit and layout-mapped observables.

pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc)

isa_circuit.draw("mpl", idle_wires=False)

Quantum circuit diagram

mapped_observables = [
observable.apply_layout(isa_circuit.layout) for observable in observables
]
print(mapped_observables)
[SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIXIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])]

2.3 Execute using the quantum primitives

Les ordinateurs quantiques peuvent produire des résultats aléatoires, donc tu collectes généralement un échantillon des sorties en exécutant le circuit plusieurs fois. Tu peux estimer la valeur de l'observable en utilisant la classe Estimator. Estimator est l'une des deux primitives ; l'autre est Sampler, qui peut être utilisé pour obtenir des données d'un ordinateur quantique. Ces objets possèdent une méthode run() qui exécute la sélection de circuits, d'observables et de paramètres (le cas échéant), en utilisant un bloc unifié de primitives (PUB). Quand tu exécutes ce code sur du vrai matériel quantique, pense à appliquer des techniques d'atténuation et de suppression d'erreurs pour réduire le bruit intrinsèque de l'ordinateur quantique.

# Construct the Estimator instance.
estimator = Estimator(mode=backend)
estimator.options.resilience_level = 1
estimator.options.default_shots = 5000

Soumet une tâche en utilisant la primitive Estimator.

# One pub, with one circuit to run against six different observables.
job = estimator.run([(isa_circuit, mapped_observables)])

# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job.job_id()}")
>>> Job ID: 97ecd036-1767-49b0-a1dc-c71638c3c3c4
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")

Après avoir soumis une tâche, tu peux attendre que la tâche soit terminée dans ton instance Python courante, ou utiliser le job_id pour récupérer les données ultérieurement. (Consulte la section sur la récupération des tâches pour plus de détails.)

Après la fin de la tâche, examine sa sortie via l'attribut result() de la tâche.

# This is the result of the entire submission.  You submitted one Pub,
# so this contains one inner result (and some metadata of its own).
job_result = job.result()

# This is the result from our single pub, which had six observables,
# so contains information on all six.
pub_result = job.result()[0]

On peut maintenant aussi exécuter le circuit en utilisant la primitive Sampler

# We include the measurements in the circuit
qc.measure_all()
sampler = Sampler(mode=backend)
qc.draw(output="mpl")

Quantum circuit diagram

Soumet une tâche en utilisant la primitive Sampler.

job_sampler = sampler.run(pm.run([qc]))

# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job_sampler.job_id()}")
# Get the results
results_sampler = job_sampler.result()
>>> Job ID: a6ee4d2f-c80d-4a86-9a76-e4b1a74502e7

2.4 Analyser les résultats

L'étape d'analyse est généralement celle où tu peux post-traiter tes résultats en utilisant, par exemple, la mitigation des erreurs de mesure ou l'extrapolation à zéro bruit (ZNE). Tu peux injecter ces résultats dans un autre flux de travail pour une analyse plus approfondie ou préparer un graphique des valeurs et données clés. En général, cette étape est spécifique à ton problème. Pour cet exemple, trace chacune des valeurs d'espérance qui ont été mesurées pour notre Circuit.

Les valeurs d'espérance et les écarts types pour les observables que tu as spécifiés à Estimator sont accessibles via les attributs PubResult.data.evs et PubResult.data.stds du résultat du job. Pour obtenir les résultats du Sampler, utilise la fonction PubResult.data.meas.get_counts(), qui retourne un dict de mesures sous la forme de chaînes de bits comme clés et de comptes comme valeurs correspondantes. Pour plus d'informations, voir Get started with Sampler.

# Plot the result
from matplotlib import pyplot as plt
values = pub_result.data.evs
errors = pub_result.data.stds
# plotting graph
# Plotting with error bars
plt.errorbar(observables_labels, values, yerr=errors, fmt='-o', capsize=5)
plt.xlabel("Observables")
plt.ylabel("Values")
plt.title("Plot of Observables vs Values with Error Bars")
plt.grid(True)
plt.tight_layout()
plt.show()

Plot output

On constate que les observables ZZIZZI et IIIIII ont une valeur d'espérance de 1, puisque ZZIZZI introduit deux signes moins qui s'annulent, et IIIIII agit comme l'identité, laissant l'état GHZ inchangé. Les autres observables ont une valeur d'espérance de 0, car leurs opérateurs ZZ introduisent un nombre impair de signes moins, ou les opérateurs XX retournent un certain nombre de Qubits qui rendent les états se superposant orthogonaux.

Maintenant on trace les résultats pour le Sampler

counts_list = results_sampler[0].data.meas.get_counts()
print(counts_list)
print(f"Outcomes : {counts_list}")
display(plot_histogram(counts_list, title="GHZ state"))
{'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}
Outcomes : {'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}

Code output

2.5 Passer à grande échelle avec un grand nombre de Qubits

En informatique quantique, le travail à l'échelle utilitaire est crucial pour progresser dans le domaine. Un tel travail nécessite des calculs effectués à une bien plus grande échelle ; travailler avec des Circuits pouvant utiliser plus de 100 Qubits et plus de 1000 Gates. Cet exemple fait un petit pas dans cette direction en mettant à l'échelle le problème GHZ à n=10n=10 Qubits. Il utilise le flux de travail Qiskit patterns et se termine en mesurant la valeur d'espérance Z0Zi\langle Z_0 Z_i \rangle .

Étape 1. Modéliser le problème

Écris une fonction qui retourne un QuantumCircuit préparant un état GHZ à nn Qubits (essentiellement un état de Bell étendu), puis utilise cette fonction pour préparer un état GHZ à 10 Qubits et collecter les observables à mesurer.

def get_qc_for_n_qubit_GHZ_state(n: int) -> QuantumCircuit:

qc = QuantumCircuit(n)
qc.h(0)
for i in range(n-1):
qc.cx(i, i+1)
return qc
n = 10
qc_n_GHZ = get_qc_for_n_qubit_GHZ_state(n)
qc_n_GHZ.draw("mpl")

Quantum circuit diagram

Ensuite, associe aux opérateurs d'intérêt. Cet exemple utilise les opérateurs ZZ entre les Qubits pour examiner le comportement à mesure qu'ils s'éloignent les uns des autres. Des valeurs d'espérance de plus en plus inexactes (corrompues) entre les Qubits distants révèleraient le niveau de bruit présent.

# ZZII...II, ZIZI...II, ... , ZIII...IZ
operator_strings = [
"Z" + i * "I" + "Z" + "I" * (n-i-2) for i in range(n-1)
]
print(operator_strings)
print(len(operator_strings))

operators = [SparsePauliOp(operator) for operator in operator_strings]
['ZZIIIIIIII', 'ZIZIIIIIII', 'ZIIZIIIIII', 'ZIIIZIIIII', 'ZIIIIZIIII', 'ZIIIIIZIII', 'ZIIIIIIZII', 'ZIIIIIIIZI', 'ZIIIIIIIIZ']
9

Étape 2. Optimiser le problème pour l'exécution sur un Backend quantique

Transforme le Circuit et les observables pour correspondre à l'ISA du Backend.

# Convert to an ISA circuit and layout-mapped observables.
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc_n_GHZ)
isa_operators_list = [operator.apply_layout(isa_circuit.layout) for operator in operators]

Étape 3. Exécuter sur le Backend

Soumets le job et si tu l'exécutes sur du matériel réel, active la suppression des erreurs en utilisant une technique pour réduire les erreurs appelée découplage dynamique. Le niveau de résilience spécifie la résistance aux erreurs à construire. Des niveaux plus élevés génèrent des résultats plus précis, au détriment de temps de traitement plus longs. Pour une explication plus détaillée des options définies dans le code suivant, voir Configure error mitigation for Qiskit Runtime.

# Submit the circuit to Estimator
job = estimator.run([(isa_circuit, isa_operators_list)])
job_id = job.job_id()
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")

Étape 4. Post-traiter les résultats

Pour mieux comprendre le comportement des états quantiques intriqués sur du matériel réel, on analyse les corrélations par paires entre les Qubits dans la base Z. Plus précisément, on examine les valeurs d'espérance ⟨Z₀Zᵢ⟩, qui mesurent la force de la corrélation entre le Qubit 0 et chaque autre Qubit i. En particulier, on va tracer :

ZiZ0/Z1Z0\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle

Quelles valeurs de ZiZ0/Z1Z0\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle t'attends-tu à voir dans le graphique ?

Options :

a) Décroissant à mesure que ii augmente

b) Constant à 1

c) De légères déviations autour de 1

d) Alternant 1 et 0 pour les valeurs impaires et paires de ii

data = list(range(1, len(operators) + 1))  # Distance between the Z operators
result = job.result()[0]
values = result.data.evs # Expectation value at each Z operator.
values = [
v / values[0] for v in values
] # Normalize the expectation values to evaluate how they decay with distance.

plt.plot(data, values, marker="o", label=f"{n}-qubit GHZ state")
plt.xlabel("Distance between qubits $i$")
plt.ylabel(r"$\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle $")
plt.legend()
plt.show()

Plot output

Dans ce graphique, on remarque que Z0Zi\langle Z_0 Z_i \rangle fluctue autour de la valeur 1, même si dans une simulation idéale tous les Z0Zi\langle Z_0 Z_i \rangle devraient être 1.

Comme tu peux le constater, les résultats des expériences à 10 Qubits sont bons mais présentent encore quelques erreurs. Une façon d'améliorer les résultats est d'implémenter l'état GHZ plus efficacement.

Habituellement, on implémente l'état GHZ avec une séquence de gates CNOT en escalier. Cependant, tu peux implémenter l'état GHZ plus efficacement, réduisant la profondeur à 2 Qubits de n à n/2 ou moins.

Une métrique importante pour évaluer la précision des résultats, ou le niveau de bruit d'un Circuit, est la profondeur des gates à 2 Qubits. Cela est dû au fait que les taux d'erreur des gates à 2 Qubits (~10 fois plus élevés que les gates à Qubit unique) dominent les erreurs de l'ensemble du Circuit. Utilise le code suivant pour obtenir la profondeur des gates à 2 Qubits d'un Circuit.

qc.depth(lambda x: x.operation.num_qubits == 2)
def better_ghz(n):
"fan out"
s = int(n / 2)
qc = QuantumCircuit(n)
qc.h(s)
for m in range(s, 0, -1):
qc.cx(m, m - 1)
if not (n % 2 == 0 and m == s):
qc.cx(n - m - 1, n - m)
return qc

better_ghz(n).draw("mpl")

Quantum circuit diagram

# Check 2-qubit gate depth before transpilation
qc_better_ghz = better_ghz(n)
qc_better_ghz.depth(lambda x: x.operation.num_qubits == 2)
5

Une chose intéressante à noter ici est qu'on a réussi à réduire la profondeur quantique du Circuit que l'on souhaite exécuter simplement en étant astucieux et en trouvant une façon différente de le programmer. Cependant, il y aura des situations et des algorithmes où on ne pourra pas compter sur ces astuces ingénieuses. C'est là que le Transpiler devient utile : il nous aide à optimiser tous ces aspects efficacement, pour qu'on n'ait pas à trop s'en préoccuper.

3. Encoder l'information

3.1 Amplitude encoding

Maintenant que tu as vu comment construire des circuits quantiques, il est intéressant d'explorer comment on peut encoder des informations classiques dans des états quantiques. Une méthode puissante est l'amplitude encoding, où les amplitudes d'un état quantique représentent les composantes d'un vecteur classique.

Considérons un exemple simple. Suppose que tu veuilles encoder le vecteur classique

x=[x0x1x2x3]\vec{x} = \begin{bmatrix} x_0 \\ x_1 \\ x_2 \\ x_3 \end{bmatrix}

dans un état quantique de deux qubits. L'objectif est de préparer l'état quantique :

ψ=x000+x101+x210+x311\ket{\psi} = x_0\ket{00} + x_1\ket{01} + x_2\ket{10} + x_3\ket{11}

x0,x1,x2,x3Rx_0, x_1, x_2, x_3 \in \mathbb{R} (ou C\mathbb{C}) et le vecteur est normalisé tel que :

x02+x12+x22+x32=1|x_0|^2 + |x_1|^2 + |x_2|^2 + |x_3|^2 = 1

On considère maintenant l'exemple particulier : x=[0.8924,0.3696,0.2391,0.0990]\vec{x} = [0.8924, 0.3696, 0.2391, 0.0990]

L'état quantique correspondant est alors :

ψ=0.892400+0.369601+0.239110+0.099011\begin{aligned} \ket{\psi} &= 0.8924\,\ket{00} + 0.3696\,\ket{01} + 0.2391\,\ket{10} + 0.0990\,\ket{11} \end{aligned}

Cet état peut être préparé en utilisant une combinaison de rotation gates RyR_y d'angles π/6\pi/6 et π/4\pi/4 pour les qubits 0 et 1 respectivement

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
import numpy as np

qc = QuantumCircuit(2)

qc.ry(np.pi / 6, 0)
qc.ry(np.pi / 4, 1)

simulator = AerSimulator()
qc.save_statevector()
result = simulator.run(qc).result()
statevector = result.get_statevector()

print("Statevector:", statevector)
qc.draw(output="mpl")
Statevector: Statevector([0.8923991 +0.j, 0.23911762+0.j, 0.36964381+0.j,
0.09904576+0.j],
dims=(2, 2))

Quantum circuit diagram

from qiskit.quantum_info import Statevector

# Define our vector
v = np.array([0.8924, 0.3696, 0.2391, 0.0990])
v = v/np.linalg.norm(v)
# Create a statevector from the vector
state = Statevector(v)

# Initialize a quantum circuit with 2 qubits
qc = QuantumCircuit(2)
qc.initialize(state.data, [0, 1])

# Optional: simulate the state
print("Statevector:", state)

# Visualize the circuit
qc.decompose().decompose().decompose().decompose().decompose().draw("mpl")
Statevector: Statevector([0.89242154+0.j, 0.36960892+0.j, 0.23910577+0.j,
0.09900239+0.j],
dims=(2, 2))

Quantum circuit diagram

On a donc vu comment encoder des informations à l'aide de rotation gates.

3.2 Angle encoding et circuits paramétrés

Une façon particulièrement intéressante d'encoder des informations dans un ordinateur quantique consiste à concevoir un Circuit quantique contenant des angles de rotation θ\vec{\theta} ou des paramètres qui peuvent être ajustés afin de représenter une famille de fonctions f(θ)f(\vec{\theta}). Considérons par exemple le Circuit quantique paramétré suivant :

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

# Define a symbolic parameter
theta = Parameter("θ")

qc = QuantumCircuit(2)
# We applied a parametrized RX gate
qc.rx(theta, 0)
qc.cx(0, 1)
qc.draw("mpl")

Quantum circuit diagram

Mathématiquement, on peut analyser quelle est la famille de fonctions que l'on peut représenter avec ce Circuit :

CNOT01Rx{0}(θ)00=CNOT01(cos(θ/2)00isin(θ/2)10)=cos(θ/2)00isin(θ/2)11\text{CNOT}_{01} \, R_x^{\{0\}}(\theta) |00\rangle = \text{CNOT}_{01} \left( \cos(\theta/2)\ket{00} - i\sin(\theta/2)\ket{10} \right) = \cos(\theta/2)\ket{00} - i\sin(\theta/2)\ket{11}

Il est assez clair que le nombre d'états que l'on peut représenter avec ce Circuit quantique est limité, car on ne peut pas représenter les états 10\ket{10} ou 01\ket{01} par exemple. Cependant, la famille d'états que l'on peut représenter commence à s'élargir lorsqu'on introduit davantage de rotations aux endroits adéquats :

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

# Define a symbolic parameter
theta1 = Parameter("θ1")
theta2 = Parameter("θ2")

qc = QuantumCircuit(2)
qc.rx(theta1, 0)
qc.rx(theta2, 1)
qc.cx(0, 1)
qc.draw("mpl")

Quantum circuit diagram

Dans ce cas, les états quantiques que l'on représentera sont :

\begin{align*} \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2) R_x^{\{0}}(\theta_1) \ket{00} &= \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2)\left( \cos(\theta_1/2)\ket{00} - i\sin(\theta_1/2)\ket{10} \right) \\ &= \text{CNOT}_{01}\left( \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \right. \\ &\quad \left. - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{10} + \sin(\theta_1/2)\sin(\theta_2/2)\ket{11} \right) \\ &= \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \\ &\quad + \sin(\theta_1/2)\sin(\theta_2/2)\ket{10} - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{11} \end{align*}

On peut voir que ce Circuit génère une famille plus large d'états quantiques par rapport au précédent. En particulier, il peut maintenant produire des états avec des amplitudes non nulles pour 01\ket{01} ou 10\ket{10}, ce qui n'était pas possible avec le Circuit ci-dessus. Cependant, ce Circuit n'est toujours pas un générateur universel d'états quantiques, bien qu'il puisse être suffisamment expressif pour concevoir des circuits avec une certaine flexibilité permettant de représenter certaines fonctions. En général, plus on introduit de paramètres (angles) indépendants, plus le Circuit a d'expressivité pour approximer des états quantiques arbitraires.

Ansatzes et bibliothèque de Circuits

Ce type de Circuit quantique paramétré peut être utilisé pour construire des Ansatzes, des états quantiques d'essai qui visent à approximer la solution d'un problème. Ces Ansatzes sont un composant central des Variational Quantum Algorithms, une classe d'algorithmes hybrides quantiques-classiques qui utilisent un ordinateur quantique pour évaluer une fonction de coût et un optimiseur classique pour la minimiser. Nous entrerons dans les détails de ces sujets dans une unité ultérieure, mais pour l'instant, nous allons montrer comment construire un Ansatz simple en utilisant la bibliothèque de Circuits dans Qiskit.

from qiskit.circuit.library import efficient_su2

SU2_ansatz = efficient_su2(4, su2_gates=["rx", "y"], entanglement="linear", reps=1)
SU2_ansatz.decompose().draw(output="mpl")

Quantum circuit diagram

On a vu comment construire un Ansatz simple en utilisant la fonction efficient_su2 de la bibliothèque qiskit.circuit.library, qui sera capable de générer une large gamme d'états quantiques en ajustant ses paramètres θ\vec{\theta}.

Conclusion

Dans ce notebook, tu as appris comment construire des circuits quantiques, depuis la création de Gates quantiques jusqu'à la définition et la mesure d'observables, et comment exécuter ces circuits efficacement sur des simulateurs et de vrais dispositifs quantiques. Tu as également vu l'importance d'une conception soigneuse du Circuit afin de minimiser les erreurs lors du travail avec de vrais dispositifs quantiques, ainsi que des stratégies pour passer à l'échelle des circuits vers un plus grand nombre de qubits, notamment à travers l'exemple de l'état GHZ. De plus, tu as exploré différentes techniques pour encoder des informations classiques dans des états quantiques, notamment l'amplitude encoding et l'angle encoding. Avec tout cela, tu es pleinement équipé pour passer à la session suivante et commencer à travailler avec des algorithmes quantiques.

Installer l'assistant de code Qiskit dans VSCode

Clique sur le lien et suis les instructions.

Bonus : Téléportation quantique

Quand tu entends le terme téléportation quantique, tu pourrais imaginer une technologie futuriste de science-fiction qui désintègre un objet à un endroit et le fait réapparaître ailleurs. Mais la téléportation quantique n'est pas du tout comme ça. En réalité, ce qui est téléporté n'est pas de la matière, c'est de l'information.

La téléportation quantique permet le transfert de l'état quantique d'un Qubit d'un endroit à un autre. Bien que ce transfert semble instantané, il ne viole pas les lois de la physique. Comment est-ce possible ? Creusons le sujet !

La téléportation quantique est un protocole qui permet à un émetteur (Alice) de transmettre l'état ψ|\psi\rangle d'un Qubit q à un récepteur (Bob) en utilisant deux ressources clés : une paire de qubits intriqués partagée a et b, et deux bits de communication classique c0 et c1.

Voici essentiellement ce dont le protocole a besoin :

  • q : le Qubit d'Alice, initialement dans l'état ψ|\psi\rangle que l'on veut téléporter.
  • a : la moitié d'Alice d'une paire intriquée partagée.
  • b : la moitié de Bob de la paire intriquée partagée.
  • c0, c1 : des bits classiques pour stocker les résultats des mesures d'Alice.

Et comment ça fonctionne ? Le flux de travail est le suivant

  1. Préparer l'état ψ|\psi\rangle d'Alice sur q. On créera un état spécifique comme +|+\rangle pour la vérification.
  2. Créer l'intrication : Générer une paire de Bell entre a et b.
  3. Opérations d'Alice : Alice effectue une « mesure de Bell » sur ses deux qubits (q et a) et stocke les résultats classiques dans c0 et c1.
  4. Communication classique : Alice envoie ses deux bits classiques (c0, c1) à Bob.
  5. Corrections de Bob : Bob applique des Gates quantiques spécifiques (X et/ou Z) à son Qubit (b), conditionnellement aux valeurs de c0 et c1 qu'il a reçues.

Si tout est fait correctement, le Qubit b de Bob se retrouvera dans l'état ψ|\psi\rangle, l'état original du q d'Alice !

Pour une explication et une exploration plus approfondies de la téléportation quantique, incluant l'explication mathématique du fonctionnement de ce protocole, tu peux consulter les ressources IBM Quantum Learning : Quantum Teleportation. Cela fait partie du cours Basics of Quantum Information.


import matplotlib.pyplot as plt
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram, plot_bloch_multivector

# Define individual quantum registers for each qubit
q = QuantumRegister(1, name='q') # message qubit
a = QuantumRegister(1, name='a') # Alice's entangled qubit
b = QuantumRegister(1, name='b') # Bob's entangled qubit

# Classical register for Alice's measurements
cr_alice = ClassicalRegister(2, name='c_alice')

# Create quantum circuit
teleport_qc = QuantumCircuit(q, a, b, cr_alice, name='Teleportation')

# Step 1: Prepare message state |+⟩ on q
teleport_qc.h(q[0])
teleport_qc.barrier()

# Step 2: Create entanglement between a and b
teleport_qc.h(a[0])
teleport_qc.cx(a[0], b[0])
teleport_qc.barrier()

# Step 3: Alice's Bell measurement
teleport_qc.cx(q[0], a[0])
teleport_qc.h(q[0])
teleport_qc.barrier()

# Step 4: Alice measures q and a
teleport_qc.measure(q[0], cr_alice[0])
teleport_qc.measure(a[0], cr_alice[1])
teleport_qc.barrier()

# Step 5: Bob's conditional measurements
with teleport_qc.if_test((cr_alice[1], 1)):
teleport_qc.x(b[0])
with teleport_qc.if_test((cr_alice[0], 1)):
teleport_qc.z(b[0])

# Draw the circuit
teleport_qc.draw(output='mpl')

Quantum circuit diagram

Après l'exécution du protocole, une question clé se pose : comment vérifier que la téléportation a fonctionné ? On ne peut pas directement « voir » l'état du Qubit de Bob après le protocole. Cependant, puisque nous avons préparé l'état initial ψ|\psi\rangle d'Alice (on a choisi +|+\rangle), on peut utiliser un type spécial de simulation pour vérifier si le Qubit b de Bob s'est retrouvé dans ce même état.

On utilisera AerSimulator avec save_statevector pour vérifier si le Qubit b de Bob se retrouve dans l'état original d'Alice (+|+\rangle). Ce simulateur calcule le vecteur d'état quantique final. et le représente ensuite en utilisant plot_bloch_multivector pour visualiser le Qubit de Bob (b) comparé à l'état initial d'Alice (q).

# Simulate the teleportation circuit
sv_simulator = AerSimulator(method='statevector')
teleport_qc_sv = teleport_qc.copy()
teleport_qc_sv.save_statevector()

# Execute the circuit on the statevector simulator
job_sv = sv_simulator.run(teleport_qc_sv)
result_sv = job_sv.result()

# Get the final statevector
final_statevector = result_sv.get_statevector()
print("Visualizing final qubit states:")
display(plot_bloch_multivector(final_statevector))
print("Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.")
Visualizing final qubit states:

Quantum circuit diagram

Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.

Comme on peut le voir dans la visualisation, les deux premiers qubits (appartenant à Alice) se sont effondrés vers 0 ou 1. Pendant ce temps, le troisième Qubit (appartenant à Bob), représenté dans la troisième sphère de Bloch, pointe le long de l'axe x, indiquant qu'il est dans l'état +|+\rangle, donc nous avons bien implémenté avec succès le protocole de téléportation quantique !

Résumé

À ce stade, il est opportun de faire un bref résumé de ce que nous avons accompli :

  • Alice a transmis un état quantique inconnu à Bob.
  • Aucune particule physique n'a été transférée.
  • L'état original sur le Qubit d'Alice est détruit, conformément au théorème de non-clonage.

Cependant, la téléportation quantique nécessite toujours une communication classique (les résultats de mesure d'Alice envoyés à Bob), et c'est ce qui explique pourquoi ce processus ne permet pas un transfert d'information plus rapide que la lumière et est pleinement cohérent avec toutes les lois connues de la physique.