You are expected to complete this assignment individually. If you need help, you are invited to come to office hours and/or ask questions on Canvas. Clarification questions about the assignments may be asked publicly. Once you have specific bugs related to your code, make the posts private.
In this lab, we are adding the functionality to perform arbitrary operations on multiple qubits using matrix multiplication. In particular, we are generalizing the one- and two-qubit implementations to n qubits.
If you want to add "helper" functions that aren't specified in the assignment, you are welcome to do so. If you want it to be used by all classes, place it in the parent class. If it is specific to a subclass, place it there. We will only test the functions specified, but you're always allowed to add private or protected methods.
Note that almost all methods will implicitly pass the "self" argument. It's been omitted here for brevity, but anything modifying internal state will need it.
Much of the single-qubit functionality will be similar to last week's lab. You may reuse the code, if desired, or rewrite it to better align with the new parent/child class of code. Then, you will create NQubit (which will need to start dealing with entanglement and arbitrary size registers).
While this looks like a lot of classes, each class should have a relatively restricted set of methods that need to be performed. Additionally, you may use libraries like numpy, which are capable of matrix multiplication and tensor products. (Be aware that you should still understand how to implement these operations yourself, as they may be on the final.)
We recommend beginning with a code skeleton (including comments!) and even a set of tests before implementing your code.
For this class, please also make the constructor so that it doesn't require any arguments when constructing (i.e., num_qubits=1 default). This is because we know the number of qubits. :)
The NQubit class inherits from ParentQubit. It is an arbitrary length array of qubits. Implement the methods described in ParentQubit.
You will implement NQubit, which applies operations with an arbitrary number of bits. We will only test your code on problems on fairly low numbers of bits, but the code needs to be written in a general way. Like SingleQubit and DoubleQubit, this class is derived from ParentQubit.
Note that all of these methods needed to start with a "self" input.The next step is to implement QCircuit. Each method of QCircuit implements a circuit (a sequence of quantum gates). Below, I'll give you the interface and, if the circuit wasn't taught in class, a picture of the circuit. You need to implement that circuit.
This class has only methods - it has no state. For each method, the state comes in as input arguments and is returned, and there is no reason for it to store anything. Therefore, we will only implement static methods. There is no data stored in this class. Because the methods are static, you can call them directly from the class without creating an instance of the class. For example, you can call QCircuit.potato_circuit(q) instead of having to create an instance of QCircuit and then calling q.potato_circuit().
The next step is to implement QOracle. This implements two oracles: Archimedes and BernVaz.
This class holds the state for two oracles: BernVaz and Archimedes. You need to figure out how, given a code(BernVaz) or set of codes (Archimedes) you can initialize the matrix to implement the desired functionality. (Remember -- numpy may be useful! But, you should still know how to implement the linear algebra yourself, because it may be on the final.)