diff --git a/exercises/grover_exercise.py b/exercises/grover_exercise.py new file mode 100644 index 0000000000000000000000000000000000000000..32c6939510d294a4c17f5c8ba032520f664ef9a9 --- /dev/null +++ b/exercises/grover_exercise.py @@ -0,0 +1,217 @@ +#!/bin/env python3 + +''' +Exercise with Qiskit: Grover algorithm. + +Task: Implement the three missing parts marked by "TODO" in the function grover(). + +This script requires qiskit. Further information about Qiskit can be found +here: https://qiskit.org + +Installation instructions for Qiskit and links to cloud environments +with Qiskit pre-installed can be found here: +https://qiskit.org/documentation/getting_started.html + +Running this script does not require a full installation of Qiskit. +Installing qiskit-terra should be sufficient: +https://github.com/Qiskit/qiskit-terra +However, with qiskit-aer also installed the simulation will be much faster: +https://github.com/Qiskit/qiskit-aer +''' + +############################################################################### +### LOAD MODULES +############################################################################### + +from traceback import print_exc +from sys import stderr +import numpy as np # numpy is a dependency of qiskit and should be installed. + +# Import basic functions and classes of Qiskit. +# The functions/classes are imported explicitly to obtain more useful error +# messages if this fails. +try: + from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, execute + from qiskit.circuit.library import XGate +except: + print("Failed to load qiskit:", file=stderr) + print_exc(file=stderr) + print(__doc__) + exit(1) + +# Load a simulation backend. Depending on the Qiskit installation, either +# a simulator from qiskit-aer or a basic simulator from qiskit-terra is used. +try: + # If qiskit-aer is installed: use the backends from this package. + from qiskit import Aer + backend = Aer.get_backend('qasm_simulator') + MAX_QUBITS = 14 +except: + # If qiskit-aer is can not be loaded: use the basic backends provided by qiskit-terra. + from qiskit import BasicAer + backend = BasicAer.get_backend('qasm_simulator') + # The basic backend is slow compared to the backend in Aer. + print('WARNING: using the slow simulation backend from qiskit-terra.\nFaster simulations with more qubits and shots are possible if qiskit-aer is installed.') + MAX_QUBITS = 9 + +### SIMULATE REAL DEVICE +# If you want to simulate a real quantum device, you can use this backend: +# <https://qiskit.org/documentation/tutorials/simulators/2_device_noise_simulation.html> +#from qiskit.test.mock import FakeVigo +#backend = FakeVigo() +#MAX_QUBITS = 5 + +############################################################################### + + +def random_bitstring(n): + ''' + Create a random string of length n containing only 0s and 1s. + ''' + bitstr = bin(np.random.randint(2**n))[2:] + bitstr = (n-len(bitstr))*'0' + bitstr + return bitstr + + +def grover( + n : 'number of bits' = 3, + shots : 'number of simulation runs' = 1000, + show_circuit : 'print quantum circuits to stdout' = True, + ): + ''' + Simulate the Grover search algorithm. + ''' + # Don't try to use too many qubits. + if n >= MAX_QUBITS: + raise ValueError("Circuit is too large") + + # Create a random bit string <solution>. + solution = random_bitstring(n) + # The following string may be useful later on. + zeros = n*'0' + + print('Generated random bit string: %s'%solution) + + + ### INITIALIZATION ### + + # Create a register of n qubits. + qubit_register = QuantumRegister(n, name='q') + # Create one ancilla qubit. + ancilla = QuantumRegister(1, name='a') + # Create a circuit containing the previously created registers. + full_circuit = QuantumCircuit(qubit_register, ancilla) + + # All qubits are now initialized in the |0> state. + + # Apply a Hadamard gate on each of the qubits in <qubit_register>. + full_circuit.h(qubit_register) + + # TODO: bring the ancilla qubit in the |-> state. + # HINT: You can use Pauli gates (full_circuit.x, full_circuit.y, + # full_circuit.z) and the Hadamard gate (full_circuit.h). + + if show_circuit: + print('Initialization before the Grover steps:') + print(full_circuit.draw('text')) + + + ### GROVER STEP ### + + # Create a new quantum circuit with the same qubits, in which we implement + # a single Grover step. We will later include this circuit multiple times + # in <full_circuit>. + step_circuit = QuantumCircuit(qubit_register, ancilla, name='G') + + # The oracle is defined as follows: + # If and only if the qubits in <qubit_register> are in the state + # <solution>, a Pauli X gate will be applied to the ancilla qubit. + # The following command creates such a gate (<n> is the number of + # control qubits): + oracle_gate = XGate().control(n, ctrl_state=solution) + + # Apply the oracle gate to the Grover step circuit. + # Here the second argument specifies on which qubits the gate acts. + # In this case, these are all qubits (qubit_register and ancilla). + all_qubits = (*qubit_register, *ancilla) + step_circuit.append(oracle_gate, all_qubits) + + # TODO: Implement the remaining part of the Grover step. + # HINT: You can ignore global phases. The previously introduced functions + # and variables are sufficient to solve the task. + + # Show the quantum circuit for a single Grover step. + if show_circuit: + print('Circuit for a single Grover step G:') + print(step_circuit.draw('text')) + + # TODO: Calculate how many Grover steps (iterations) should be applied and + # correct the following line. + # HINT: You can use round(...) to obtain an integer. + # "2 to the power n" is implemented as 2**n in python. + iterations = 1 + # Add the grover step circuit <iteration> times to the full circuit. + full_circuit.compose( step_circuit.power(iterations), inplace=True ) + + + ### MEASUREMENT ### + + # Create a classical register of n bits. + cbit_register = ClassicalRegister(n, 'c') + # Add <cbit_register> to the quantum circuit. + full_circuit.add_register(cbit_register) + # Measure the qubits in <qubit_register> and save the values to + # <cbit_register>. + full_circuit.measure(qubit_register, cbit_register) + + + # Show the (now complete) quantum circuit. + if show_circuit: + print('Full quantum circuit:') + print(full_circuit.draw('text')) + + + ### SIMULATION ### + + # Create a simulation job for this circuit. + job = execute(full_circuit, backend=backend, shots=shots) + result = job.result() + # Get the outputs of the simulation. + # counts is a dictionary mapping the classical outputs to the number + # how often this output occurred. The classical output is given as a + # bit string with bits ordered from bit n to 0. + counts = result.get_counts() + + + ### SHOW RESULTS ### + + # Show the output of the simulation. + for key, value in counts.items(): + print('Measured %s in%7d of %d simulation runs.'%(key, value, shots)) + # Show the input bit string a for comparison. + print('Check: %s is the correct solution.'%(''.join(str(i) for i in solution))) + + # Check if the circuit solved the problem of finding the right solution + # with high probability. + if counts[solution] > 0.9 * shots: + print('It works: correct result obtained in {}% of the simulation runs.'\ + .format(100*counts[solution]/shots)) + elif counts[solution] < 0.6 * shots: + print('Improvement required: correct result obtained in only {}% of the simulation runs.'\ + .format(100*counts[solution]/shots)) + print('Implement the three parts marked by "TODO" in the function grover().') + + # Analyze the success probability. + # Instead of p_success we print p_fail, because for large n and small p_fail + # that gives a better overview. + p_fail_theory = np.cos((2*iterations + 1)*np.arccos((1-2**-n)**.5))**2 + p_fail_simulation = (shots - counts[solution])/shots + print('Probability of failure: theory = %g, simulation = %g'\ + %(p_fail_theory, p_fail_simulation)) + + + +# This is how a python script should be structured: +# In the end the main function is called if the script is executed directly. +if __name__ == '__main__': + grover() diff --git a/exercises/grover_mpl.py b/exercises/grover_mpl.py new file mode 100644 index 0000000000000000000000000000000000000000..5d86de774543a6946e45288b3301b951a1eb84b8 --- /dev/null +++ b/exercises/grover_mpl.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 + +''' +Exercise with Qiskit: Grover algorithm. + +Task: Implement the three missing parts marked by "TODO" in the function grover(). + +This script requires qiskit. Further information about Qiskit can be found +here: https://qiskit.org + +Installation instructions for Qiskit and links to cloud environments +with Qiskit pre-installed can be found here: +https://qiskit.org/documentation/getting_started.html + +Running this script does not require a full installation of Qiskit. +Installing qiskit-terra should be sufficient: +https://github.com/Qiskit/qiskit-terra +However, with qiskit-aer also installed the simulation will be much faster: +https://github.com/Qiskit/qiskit-aer +''' + +############################################################################### +### LOAD MODULES +############################################################################### + +from traceback import print_exc +from sys import stderr +import numpy as np +import matplotlib.pyplot as plt + +from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, execute +from qiskit.circuit.library import XGate, ZGate + +# Load a simulation backend. Depending on the Qiskit installation, either +# a simulator from qiskit-aer or a basic simulator from qiskit-terra is used. +try: + # If qiskit-aer is installed: use the backends from this package. + from qiskit import Aer + default_backend = Aer.get_backend('qasm_simulator') + MAX_QUBITS = 14 +except: + # If qiskit-aer is can not be loaded: use the basic backends provided by qiskit-terra. + from qiskit import BasicAer + default_backend = BasicAer.get_backend('qasm_simulator') + # The basic backend is slow compared to the backend in Aer. + print('WARNING: using the slow simulation backend from qiskit-terra.\nFaster simulations with more qubits and shots are possible if qiskit-aer is installed.') + MAX_QUBITS = 9 + +############################################################################### + +def to_bit_string(x, n): + bitstr = bin(x)[2:] + return (n-len(bitstr))*'0' + bitstr + +def random_bitstring(n): + ''' + Create a random string of length n containing only 0s and 1s. + ''' + return to_bit_string(np.random.randint(2**n), n) + +def visualize(circuit, title, fmt): + try: + visualization = circuit.draw(fmt) + except: + print('Failed to visualize circuit') + return + if fmt == 'mpl': + visualization.text(0.5, 0.9, title, horizontalalignment='center') + else: + print(title) + print(visualization) + + +def grover( + n : 'number of bits' = 3, + shots : 'number of simulation runs' = 1000, + iterations : 'number of Grover steps or None' = None, + show_circuit : 'format for showng quantum circuits' = 'text', + solution = None, + backend = default_backend, + ): + ''' + Simulate the Grover search algorithm. + ''' + # Don't try to use too many qubits. + if n >= MAX_QUBITS: + raise ValueError("Circuit is too large") + + # Create a random bit string <solution>. + if solution is None: + solution = random_bitstring(n) + elif type(solution) == int: + solution = to_bit_string(solution, n) + # The following string may be useful later on. + zeros = n*'0' + + print('Generated random bit string: %s'%solution) + + + ### INITIALIZATION ### + + # Create a register of n qubits. + qubit_register = QuantumRegister(n, name='q') + # Create one ancilla qubit. + ancilla = QuantumRegister(1, name='a') + # Create a circuit containing the previously created registers. + full_circuit = QuantumCircuit(qubit_register, ancilla) + + # All qubits are now initialized in the |0> state. + + # Apply a Hadamard gate on each of the qubits in <qubit_register>. + full_circuit.h(qubit_register) + + # TODO: bring the ancilla qubit in the |-> state. + # BEGIN SOLUTION + full_circuit.x(ancilla) # X|0> = |1> + full_circuit.h(ancilla) # H|1> = |-> + # Alternative solution: + #full_circuit.h(ancilla) # H|0> = |+> + #full_circuit.z(ancilla) # Z|+> = |-> + # END SOLUTION + + if show_circuit: + visualize(full_circuit, 'Initialization before the Grover steps', show_circuit) + + + ### GROVER STEP ### + + # Create a new quantum circuit with the same qubits, in which we implement + # a single Grover step. We will later include this circuit multiple times + # in <full_circuit>. + step_circuit = QuantumCircuit(qubit_register, ancilla, name='G') + + # The oracle is defined as follows: + # If and only if the qubits in <qubit_register> are in the state + # <solution>, a Pauli X gate will be applied to the ancilla qubit. + # The following command creates such a gate (<n> is the number of + # control qubits): + oracle_gate = XGate().control(n, ctrl_state=solution) + + # Apply the oracle gate to the Grover step circuit. + # Here the second argument specifies on which qubits the gate acts. + # In this case, these are all qubits (qubit_register and ancilla). + all_qubits = (*qubit_register, *ancilla) + step_circuit.append(oracle_gate, all_qubits) + + # TODO: Implement the remaining part of the Grover step. + # HINT: You can ignore global phases. The previously introduced functions + # and variables are sufficient to solve the task. + # BEGIN SOLUTION + step_circuit.h(qubit_register[:-1]) + step_circuit.z(qubit_register[-1]) + step_circuit.append(XGate().control(n-1, ctrl_state=(n-1)*'0'), qubit_register) + step_circuit.z(qubit_register[-1]) + step_circuit.h(qubit_register[:-1]) + # Alternative solution: + #step_circuit.h(qubit_register) + #step_circuit.barrier() + #step_circuit.x(qubit_register) + #step_circuit.mcrz(np.pi, qubit_register[1:], qubit_register[0]) + #step_circuit.x(qubit_register) + #step_circuit.barrier() + #step_circuit.h(qubit_register) + # END SOLUTION + + # Show the quantum circuit for a single Grover step. + if show_circuit: + visualize(step_circuit, 'Circuit for a single Grover step G', show_circuit) + + # TODO: Calculate how many Grover steps (iterations) should be applied and + # correct the following line. + # BEGIN SOLUTION + if (iterations is None): + #iterations = int(np.pi/(2*np.arcsin(2**(-n/2)))) + iterations = int(np.pi*2**(n/2-2)) + # END SOLUTION + # Add the grover step circuit <iteration> times to the full circuit. + full_circuit.append(step_circuit.power(iterations), all_qubits) + + + ### MEASUREMENT ### + + # Create a classical register of n bits. + cbit_register = ClassicalRegister(n, 'c') + # Add <cbit_register> to the quantum circuit. + full_circuit.add_register(cbit_register) + # Measure the qubits in <qubit_register> and save the values to + # <cbit_register>. + full_circuit.measure(qubit_register, cbit_register) + + + # Show the (now complete) quantum circuit. + if show_circuit: + visualize(full_circuit, 'Full quantum circuit', show_circuit) + + + ### SIMULATION ### + + # Create a simulation job for this circuit. + job = execute(full_circuit, backend=backend, shots=shots) + result = job.result() + # Get the outputs of the simulation. + # counts is a dictionary mapping the classical outputs to the number + # how often this output occurred. The classical output is given as a + # bit string with bits ordered from bit n to 0. + counts = result.get_counts() + + + ### SHOW RESULTS ### + + # Show the output of the simulation. + for key, value in counts.items(): + print('Measured %s in%7d of %d simulation runs.'%(key, value, shots)) + # Show the input bit string a for comparison. + print('Check: %s is the correct solution.'%(''.join(str(i) for i in solution))) + + # Check if the circuit solved the problem of finding the right solution + # with high probability. + if counts[solution] > 0.9 * shots: + print('It works: correct result obtained in {}% of the simulation runs.'\ + .format(100*counts[solution]/shots)) + elif counts[solution] < 0.6 * shots: + print('Improvement required: correct result obtained in only {}% of the simulation runs.'\ + .format(100*counts[solution]/shots)) + print('Implement the three parts marked by "TODO" in the function grover().') + + # Analyze the success probability. + # Instead of p_success we print p_fail, because for large n and small p_fail + # that gives a better overview. + p_fail_theory = np.cos((2*iterations + 1)*np.arcsin(2**(-n/2)))**2 + p_fail_simulation = (shots - counts[solution])/shots + print('Probability of failure: theory = %g, simulation = %g'\ + %(p_fail_theory, p_fail_simulation)) + + return counts, solution + + +def groverGUI(n=3, shots=1000, noisy=False, **kwargs): + if noisy: + from qiskit.test.mock import FakeVigo + backend = FakeVigo() + else: + backend = default_backend + counts, solution = grover(n=n, shots=shots, show_circuit='mpl', backend=backend, **kwargs) + solution = int(solution, 2) + fig, ax = plt.subplots() + ax.bar(range(2**n), [counts.get(to_bit_string(x, n), 0)/shots for x in range(2**n)], log=True) + ax.set_title('Simulation results') + ax.vlines(solution, 0, 1, color='red') + plt.show() + + +# This is how a python script should be structured: +# In the end the main function is called if the script is executed directly. +if __name__ == '__main__': + from sys import argv + args = {} + for text in argv[1:]: + try: + key, value = text.split('=') + args[key] = int(value) + except: + print('Argument not understood:', text) + groverGUI(**args)