Implémentation Qiskit
Dans cette leçon, nous mettons en œuvre quelques-unes des idées de la leçon sur l'intrication en action, à l'aide de Qiskit.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"qiskit": "qiskit", "qiskit_aer": "qiskit-aer"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
from qiskit import __version__
print(__version__)
2.1.1
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram, array_to_latex
from qiskit.result import marginal_distribution
from qiskit.circuit.library import UGate
from math import pi
import random
Voici une implémentation en circuit quantique du protocole de téléportation.
qubit = QuantumRegister(1, "Q")
ebit0 = QuantumRegister(1, "A")
ebit1 = QuantumRegister(1, "B")
a = ClassicalRegister(1, "a")
b = ClassicalRegister(1, "b")
protocol = QuantumCircuit(qubit, ebit0, ebit1, a, b)
# Prepare ebit used for teleportation
protocol.h(ebit0)
protocol.cx(ebit0, ebit1)
protocol.barrier()
# Alice's operations
protocol.cx(qubit, ebit0)
protocol.h(qubit)
protocol.barrier()
# Alice measures and sends classical bits to Bob
protocol.measure(ebit0, a)
protocol.measure(qubit, b)
protocol.barrier()
# Bob uses the classical bits to conditionally apply gates
with protocol.if_test((a, 1)):
protocol.x(ebit1)
with protocol.if_test((b, 1)):
protocol.z(ebit1)
display(protocol.draw(output="mpl"))
Le circuit utilise quelques fonctionnalités de Qiskit que nous n'avons pas encore vues dans les leçons précédentes, notamment les fonctions barrier et if_test.
La fonction barrier crée une séparation visuelle rendant le schéma du circuit plus lisible, et elle empêche également Qiskit d'effectuer diverses simplifications et optimisations à travers la barrière lors de la compilation, quand les circuits sont exécutés sur du vrai matériel.
La fonction if_test applique une opération de façon conditionnelle en fonction d'un bit ou d'un registre classique.
Le circuit initialise d'abord dans l'état (ce qui ne fait pas partie du protocole lui-même), puis viennent les opérations d'Alice, ses mesures, et enfin les opérations de Bob. Pour vérifier que le protocole fonctionne correctement, nous allons appliquer une porte à qubit unique générée aléatoirement à l'état initialisé de afin d'obtenir un vecteur d'état quantique aléatoire à téléporter. En appliquant l'inverse (c'est-à-dire la transposée conjuguée) de cette porte à après l'exécution du protocole, on peut vérifier que l'état a bien été téléporté en mesurant pour s'assurer qu'il est revenu à l'état .
D'abord, choisissons aléatoirement une porte unitaire à qubit.
random_gate = UGate(
theta=random.random() * 2 * pi,
phi=random.random() * 2 * pi,
lam=random.random() * 2 * pi,
)
display(array_to_latex(random_gate.to_matrix()))
Nous allons maintenant créer un nouveau circuit de test qui commence par appliquer notre porte aléatoire à , puis exécute le circuit de téléportation, et enfin applique l'inverse de notre porte aléatoire au qubit et mesure. Le résultat devrait être avec certitude.
# Create a new circuit including the same bits and qubits used in the
# teleportation protocol.
test = QuantumCircuit(qubit, ebit0, ebit1, a, b)
# Start with the randomly selected gate on Q
test.append(random_gate, qubit)
test.barrier()
# Append the entire teleportation protocol from above.
test = test.compose(protocol)
test.barrier()
# Finally, apply the inverse of the random unitary to B and measure.
test.append(random_gate.inverse(), ebit1)
result = ClassicalRegister(1, "Result")
test.add_register(result)
test.measure(ebit1, result)
display(test.draw(output="mpl"))
Enfin, exécutons le simulateur Aer sur ce circuit et affichons un histogramme des résultats. Nous verrons les statistiques pour les trois bits classiques : le bit du bas/de gauche devrait toujours valoir , ce qui indique que le qubit a bien été téléporté dans , tandis que les deux autres bits devraient être à peu près uniformes.
result = AerSimulator().run(test).result()
statistics = result.get_counts()
display(plot_histogram(statistics))
On peut aussi filtrer les statistiques pour se concentrer uniquement sur le qubit de résultat du test si on le souhaite, comme ceci :
filtered_statistics = marginal_distribution(statistics, [2])
display(plot_histogram(filtered_statistics))
Codage superdense
Le codage superdense est un protocole qui, en un certain sens, vise un objectif complémentaire à celui de la téléportation. Plutôt que de permettre la transmission d'un qubit en utilisant deux bits classiques de communication (au coût d'un e-bit d'intrication), il permet la transmission de deux bits classiques en utilisant un qubit de communication quantique (toujours au coût d'un e-bit d'intrication).
Plus en détail, nous avons un émetteur (Alice) et un récepteur (Bob) qui partagent un e-bit d'intrication. Selon les conventions établies pour la leçon, cela signifie qu'Alice détient un qubit , Bob détient un qubit , et ensemble la paire est dans l'état . Alice souhaite transmettre deux bits classiques à Bob, que nous noterons et , et elle y parviendra en lui envoyant un qubit.
Il est raisonnable de considérer cet exploit comme moins intéressant que celui accompli par la téléportation. L'envoi de qubits sera vraisemblablement bien plus difficile que celui de bits classiques dans un avenir prévisible, de sorte qu'échanger un qubit de communication quantique contre deux bits de communication classique, au coût d'un e-bit de surcroît, ne semble guère en valoir la peine. Cependant, cela ne signifie pas que le codage superdense ne soit pas intéressant, car il l'est assurément.
Dans le thème de la leçon, l'une des raisons pour lesquelles le codage superdense est intéressant est qu'il démontre une utilisation concrète et (dans le contexte de la théorie de l'information) plutôt frappante de l'intrication. Un théorème célèbre de la théorie de l'information quantique, connu sous le nom du théorème de Holevo, implique que sans l'utilisation d'un état intriqué partagé, il est impossible de communiquer plus d'un bit d'information classique en envoyant un seul qubit. (Le théorème de Holevo est plus général que cela. Son énoncé précis est technique et nécessite des explications, mais c'en est une conséquence.) Ainsi, grâce au codage superdense, l'intrication partagée permet effectivement de doubler la capacité de transport d'information classique lors de l'envoi de qubits.
Protocole
Le schéma de circuit quantique suivant décrit le protocole de codage superdense :

Concrètement, voici ce que fait Alice :
-
Si , Alice applique une porte sur son qubit (et si elle ne fait rien).
-
Si , Alice applique une porte sur son qubit (et si elle ne fait rien).
Alice envoie ensuite son qubit à Bob.
Ce que fait Bob à la réception du qubit consiste d'abord à appliquer une porte CNOT, avec comme contrôle et comme cible, puis il applique une porte Hadamard à . Il mesure ensuite pour obtenir et pour obtenir , avec des mesures en base standard dans les deux cas.
Analyse
L'idée sous-jacente à ce protocole est simple : Alice choisit en pratique quel état de Bell elle souhaite partager avec Bob, elle lui envoie son qubit, et Bob mesure pour déterminer quel état de Bell Alice a choisi.
Autrement dit, ils partagent initialement et selon les bits et Alice laisse cet état tel quel ou le fait basculer vers l'un des autres états de Bell en appliquant ou à son qubit
Les actions de Bob ont les effets suivants sur les quatre états de Bell :
Cela peut être vérifié directement en calculant les résultats des opérations de Bob sur ces états un par un.
Ainsi, lorsque Bob effectue ses mesures, il est capable de déterminer quel état de Bell Alice a choisi. Vérifier que le protocole fonctionne correctement revient à examiner chaque cas :
-
Si , alors l'état de quand Bob reçoit est . Il transforme cet état en et obtient .
-
Si , alors l'état de quand Bob reçoit est . Il transforme cet état en et obtient .
-
Si , alors l'état de quand Bob reçoit est . Il transforme cet état en et obtient .
-
Si , alors l'état de quand Bob reçoit est . Il transforme cet état en et obtient . (Le facteur de phase moins un n'a aucun effet ici.)
Implémentation du codage superdense
Voici une implémentation simple du codage superdense où nous spécifions le circuit lui-même en fonction des bits à transmettre. Nous allons d'abord choisir deux bits à transmettre. (Plus tard, nous les choisirons aléatoirement, mais pour l'instant nous faisons simplement un choix arbitraire.)
c = "1"
d = "0"
Maintenant, nous allons construire le circuit en conséquence. Ici, nous laisserons Qiskit utiliser les noms par défaut pour les qubits : pour le qubit du haut et pour celui du bas.
protocol = QuantumCircuit(2)
# Prepare ebit used for superdense coding
protocol.h(0)
protocol.cx(0, 1)
protocol.barrier()
# Alice's operations
if d == "1":
protocol.z(0)
if c == "1":
protocol.x(0)
protocol.barrier()
# Bob's actions
protocol.cx(0, 1)
protocol.h(0)
protocol.measure_all()
display(protocol.draw(output="mpl"))
Il n'y a pas grand-chose de nouveau ici, si ce n'est la fonction measure_all, qui mesure tous les qubits et place les résultats dans un unique registre classique (ayant donc deux bits dans ce cas).
L'exécution du simulateur Aer produit le résultat attendu.
result = AerSimulator().run(protocol).result()
statistics = result.get_counts()
for outcome, frequency in statistics.items():
print(f"Measured {outcome} with frequency {frequency}")
display(plot_histogram(statistics))
Measured 10 with frequency 1024
Utilisons maintenant un qubit supplémentaire comme générateur de bits aléatoires — essentiellement pour lancer des pièces de monnaie équilibrées. Nous l'utiliserons pour choisir aléatoirement et , puis exécuterons le protocole de codage superdense.
rbg = QuantumRegister(1, "coin")
ebit0 = QuantumRegister(1, "A")
ebit1 = QuantumRegister(1, "B")
Alice_c = ClassicalRegister(1, "Alice c")
Alice_d = ClassicalRegister(1, "Alice d")
test = QuantumCircuit(rbg, ebit0, ebit1, Alice_d, Alice_c)
# Initialize the ebit
test.h(ebit0)
test.cx(ebit0, ebit1)
test.barrier()
# Use the 'coin' qubit twice to generate Alice's bits c and d.
test.h(rbg)
test.measure(rbg, Alice_c)
test.h(rbg)
test.measure(rbg, Alice_d)
test.barrier()
# Now the protocol runs, starting with Alice's actions, which depend
# on her bits.
with test.if_test((Alice_d, 1), label="Z"):
test.z(ebit0)
with test.if_test((Alice_c, 1), label="X"):
test.x(ebit0)
test.barrier()
# Bob's actions
test.cx(ebit0, ebit1)
test.h(ebit0)
test.barrier()
Bob_c = ClassicalRegister(1, "Bob c")
Bob_d = ClassicalRegister(1, "Bob d")
test.add_register(Bob_d)
test.add_register(Bob_c)
test.measure(ebit0, Bob_d)
test.measure(ebit1, Bob_c)
display(test.draw(output="mpl"))
L'exécution du simulateur Aer montre les résultats : les bits classiques d'Alice et de Bob sont toujours identiques.
result = AerSimulator().run(test).result()
statistics = result.get_counts()
display(plot_histogram(statistics))
Implémentation Qiskit
On peut implémenter le jeu CHSH, ainsi que la stratégie quantique définie ci-dessus, dans Qiskit comme suit.
Voici d'abord la définition du jeu lui-même, qui permet de brancher une stratégie arbitraire en argument.
def chsh_game(strategy):
# This function runs the CHSH game, using the strategy (a function
# from two bits to two bits), returning 1 for a win and 0 for a loss.
# Choose x and y randomly
x, y = random.randint(0, 1), random.randint(0, 1)
# Use the strategy to determine a and b
a, b = strategy(x, y)
# Decide if the strategy wins or loses
if (a != b) == (x & y):
return 1 # Win
return 0 # Lose
On va maintenant créer une fonction qui produit un circuit en fonction des questions posées à Alice et Bob. Pour simplifier, on laissera les qubits avec leurs noms par défaut, et on utilisera la porte intégrée pour les actions d'Alice et de Bob.
def chsh_circuit(x, y):
# This function creates a `QuantumCircuit` implementing the quantum
# strategy described above (including the e-bit preparation).
qc = QuantumCircuit(2, 2)
# Prepare an e-bit
qc.h(0)
qc.cx(0, 1)
qc.barrier()
# Alice's actions
if x == 0:
qc.ry(0, 0)
else:
qc.ry(-pi / 2, 0)
qc.measure(0, 0)
# Bob's actions
if y == 0:
qc.ry(-pi / 4, 1)
else:
qc.ry(pi / 4, 1)
qc.measure(1, 1)
return qc
Voici les quatre circuits possibles, selon les questions posées.
# Draw the four possible circuits
print("(x,y) = (0,0)")
display(chsh_circuit(0, 0).draw(output="mpl"))
print("(x,y) = (0,1)")
display(chsh_circuit(0, 1).draw(output="mpl"))
print("(x,y) = (1,0)")
display(chsh_circuit(1, 0).draw(output="mpl"))
print("(x,y) = (1,1)")
display(chsh_circuit(1, 1).draw(output="mpl"))
(x,y) = (0,0)
(x,y) = (0,1)
(x,y) = (1,0)
(x,y) = (1,1)
On va maintenant créer un job avec le simulateur Aer qui exécute le circuit une seule fois pour une paire d'entrées donnée.
def quantum_strategy(x, y):
# This function runs the appropriate quantum circuit defined above
# one time and returns the measurement results
# Setting `shots=1` to run the circuit once
result = AerSimulator().run(chsh_circuit(x, y), shots=1).result()
statistics = result.get_counts()
# Determine the output bits and return them
bits = list(statistics.keys())[0]
a, b = bits[0], bits[1]
return a, b
Enfin, on va jouer 1000 fois et calculer la fraction de parties gagnées par la stratégie.
NUM_GAMES = 1000
TOTAL_SCORE = 0
for _ in range(NUM_GAMES):
TOTAL_SCORE += chsh_game(quantum_strategy)
print("Fraction of games won:", TOTAL_SCORE / NUM_GAMES)
Fraction of games won: 0.867
On peut aussi définir une stratégie classique et voir comment elle se comporte. Ce n'est qu'une stratégie parmi d'autres — on peut en tester d'autres en modifiant le code — mais elle fait partie des stratégies classiques optimales.
def classical_strategy(x, y):
# This function implements just one example of an optimal classical
# strategy for the CHSH game. Other classical strategies can be
# implemented by changing the bit values assigned to a and b.
# Alice's answer
if x == 0:
a = 0
elif x == 1:
a = 1
# Bob's answer
if y == 0:
b = 1
elif y == 1:
b = 0
return a, b
Jouons à nouveau 1000 parties pour voir comment elle se comporte.
NUM_GAMES = 1000
TOTAL_SCORE = 0
for _ in range(NUM_GAMES):
TOTAL_SCORE += chsh_game(classical_strategy)
print("Fraction of games won:", TOTAL_SCORE / NUM_GAMES)
Fraction of games won: 0.747
Bien qu'il y ait de l'aléatoire en jeu, il est très improbable que les statistiques s'écartent beaucoup après 1000 exécutions. La stratégie quantique gagne environ 85 % du temps, tandis qu'une stratégie classique ne peut pas gagner plus d'environ 75 % du temps.