Qiskit / Python Version des Bernstein-Vazirani-Algorithmus

Anlage zu Blog Q15.

Lesbar und ausführbar als Jupyter Notebook, z.B. in IBM Q Experience

1 Bereitstellen der notwendigen Bibliotheken und Packages

In [1]:
%matplotlib inline
# Importieren der Standard Qiskit Libraries 
from qiskit import QuantumCircuit, execute, Aer, IBMQ
from qiskit.compiler import transpile, assemble
from qiskit.tools.jupyter import *
from qiskit.visualization import *
from qiskit import QuantumRegister,ClassicalRegister
# Laden des persönlichen IBM Q Account
provider = IBMQ.load_account()
# Importieren der notwendigen Python Libraries für Numerik- und Zufallsfunktionen
import numpy as np
import random as rd

2 Der Qubit Circuit

Es wird das sog. Quantenregister, also das Qubit-System für diesen Algorithmus, mit n_qubits = 33 Qubits erzeugt. Im Composer also die Qubit-Linien. Ebenso eine sog. Classical(-Bit-)Register für die Aufnahme von n_quibts Messergebnissen. Im Composer die Mess-Bit-Schiene.

Wir wollen Geheimnisse in Form von 32-Bit Ketten bestimmen. Wie im Q15 Blog brauchen wir dazu ein zusätzliches Hilfs-Qubit. Daher n_qubits = 33

In [2]:
## Definition eines 33-qubit Quantum Circuit
n_qubits = 33                         # Anzahl der Qubits und Messwergebnisse
q = QuantumRegister(n_qubits, 'q')    # q wird eine Liste (Python-Struktur) von n_qubits Qubits
c = ClassicalRegister(n_qubits, 'c')  # Entsprechend c für die Mess-Bits
circ = QuantumCircuit(q, c)           # Der Q-Circuit mit Namen 'circ' wird definiert

tmp = q[n_qubits-1]    # Das Hilfs-Qubit 'tmp' ist das mit dem höchsten Index (das unterste im Composer)
c_tmp = c[n_qubits-1]  # Wir bennen das Hilfs-Qubit und das zugehörige Mess-Bit mit 'tmp' und 'c_tmp'

3 Zufallserzeugtes Orakel

Wir erzeugen die geheime Bit-Kette per Zufallszahlengenerator, eine Funktion aus dem Paket 'random'. Der Zufallszahlengenerator erzeugt nur "Pseudo-Zufallszahlen", da die Zahlen nach einem festen Algorithmus berechnet werden.

Mit der Bit-Kette implementieren wir das Orakel im Qubit-Circuit. Wir kapseln diesen Teilalgorithmus in einer PythonFunktion, die wir allgemein verwenden und an geeigneter Stelle im Circuit aufrufen können.

Beachte: Anders als beim Beispiel in Q15 kennen wir das Geheimnis hier nicht, da es erst durch die Zufallsprozedur erstellt wird.

In [3]:
# Implementierung des Orakels als Teilalgorithmus (Funktion)

## Die Parameter 'circuit' und 'n' werden später bei Aufruf durch unsern
## vordefinierten 'circ' und 'n_qubits' ersetzt.
def implement_oracle(circuit,n):                 
    oracle = [rd.randint(0,1) for _ in range(n)]   # Erzeugt eine Zufalls-Bit-Kette (eine *Liste* in Python) 
#  Das Geheimnis ist hier immer noch unbekannt
# Das Orakel wird ungesehen im circuit implementiert
    for i,s in enumerate(reversed(oracle)): # reversed wg der "Zählung" der von rechts nach links
        if s==1:                            # Ist das geheime Bit 1, wird das
            circuit.cx(q[i],tmp)            # CNOT-Gate zwischen dem Qubit und dem Hilfs-Qubit eingefügt
    return oracle                           
# Die Funktion liefert (return) das erzeugte Geheimnis, falls wir es zur Überprüfung verwenden wollen

4 Der Gesamt-Circuit wird definiert

In [4]:
# Alle Qubits sind automatisch mit Zustand |0> initialisiert 
# Nun wird der B-V-Algorithmus als Circuit "komponiert" 
# Die Qiskit-Namen für die die Gates sind einfach: x für das X-Gate, h für das Hadamerd usw.
circ.x(tmp)   # tmp wird zunächst in den Zustand |1> überführt mit dem X Operator (Gate)
for qu in q:  # Der H-Operator wir für alle Qubits (aus der Liste 'q') angewendet
    circ.h(qu)
    
# Jetzt wird das Orakel erzeugt, mit unbekanntem(!) Geheimnis
oracle = implement_oracle(circ,n_qubits-1)    
# Mit dieser Form des Aufrufs speichern wir aber das Geheimnis in der Variablen 'oracle'
# für evtl. späteren Vergleich, ob das Ergebnis des Algorithmus stimmt

for qu in q:  # Am Ende wird der Hadamard-Operator auf alle Qubits angewendet
    circ.h(qu)
    

4a Wir könnten uns den kompletten Circuit als Composer Grafik anzeigen lassen

Was wir aber nicht tun, da das Geheimnis damit "ausserhalb" des Algorithmus offengelegt würde. Die Programmzeile dazu wäre:

circ.draw(output="mpl")

5 Messen zum Abschluss des Circuits

Und die gefundenen Bit-Kette in 'c' speichern.

In [8]:
# Die Qubits aus der Liste 'q' werden alle gemessen, das Ergebnis wird auf
# die Bit-Kette (genauer: die Liste 'c' der Bits) übertragen.
# Eigentlich interessieren nur die ersten 32 Ergebnisse, das des Hilfs-Qubits ist irrelevant
circ.measure(q,c)

print('Algorithmus definiert für ',n_qubits-1,'Qubits')
Algorithmus definiert für  32 Qubits

6 Der Circuit wird ausgeführt

Auf dem Q-Simulator oder einem der realen IBM Quanten-Computer. Hier der 'qasm_simulator', den wir auch bei der grafischen Programmierung (Composer) verwendet haben.

Die Anzahl Wiederholungen (Variable 'n_shots') kann beim Simulator auf 1 gesetzt werden, da wir erwarten können, dass das Ergebnis zu 100% eine Bit-Kette liefert. Wird stattdessen ein Quantum-Device verwendet, muss man damit rechnen, dass aufgrund von Dekohärenz-Effekten das Ergebnis eines einzelnen Durchlaufs "gestört" ist. Mit z.B. n_shots=1024 wird das Programm 1024 Mal wiederholt und man kann an der Häufigkeit der Ergebnisse die richtige Antwort erschliessen. Es ist, als müssten wir das Orakel mehrfach befragen, weil es die Antwort immer "leicht verwirrt" gibt. (Ein typischer Diskussionspunkt, wenn es um die (theoretische) "Quanten-Beschleunigung" geht.)

Achtung: Ergebnisse für Qubits q0 bis q32 werden in der Bit-Kette von rechts nach links gespeichert. Der Messwert des Hilfs-Qubits steht also ganz links (und gehört nicht zum Geheimnis)

In [11]:
# Ausführen des Circuits auf Simulator

n_shots = 1                                           # Reicht aus für den Simulator
simulator = Aer.get_backend('qasm_simulator')         # Der 'simulator' wird festgelegt 
job = execute(circ, backend=simulator, shots=n_shots) # Der 'job' wird ausgeführt auf dem
                                                      # 'simulator' als Backend
result = job.result()                                 # Das Ergebnis wird im Objekt 'result' gespeichert

# Wir verzichten auf das Anzeigen des Histogramms
# Stattdessen speichern wir das Ergebnis in 'counts'

print('Anm.: Ergebnisse für Qubits q0 bis q32 werden in der Bit-Kette von rechts nach links geschrieben')
print('      Qubits, die nicht gemessen werden, haben den Messwert 0 per Default')
counts = result.get_counts(circ)
print()
print('counts: ',counts)                     # Ausgabe des 'counts'-Ergebnis (in Form eines Python Dictionary)

# 'counts# hat bei n_shots=1 nur einen Eintrag: das gefundene Geheimnis
# Das erste (= linke) Bit stammt vom Hilfs-Qubit und wird weggelassen.
secret = list(counts.keys())[0]
secret = secret[1:]                # Python-Ausdrucksweise dafür, dass das erste Zeichen wegfallen soll
print('Gefundenes Geheimnis:',secret)
print('Das Orakel war:',oracle)
Anm.: Ergebnisse für Qubits q0 bis q32 werden in der Bit-Kette von rechts nach links geschrieben
      Qubits, die nicht gemessen werden, haben den Messwert 0 per Default

counts:  {'100101100111110100101111010110000': 1}
Gefundenes Geheimnis: 00101100111110100101111010110000
Das Orakel war: [0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0]

7 Erklärung des Outputs

Der Output ist hier minimalistisch aber übersichtlich. Die Zeile 'counts' zeigt einfach das Python-Dicitionary (Lexikon) 'counts'. Es hat hier nur einen Eintrag: das Messergebnis und deren Häufigkeit, die hier natürlich 1 ist wegen n_shots=1.

Darunter das vom Algorithmus gefundene Geheimnis als Bit-Kette.

Das wäre alles.

Na, vielleicht möchte man doch wissen .... Darunter also noch, als Liste, die geheimen Bits des Orakels.

In [ ]: