Goals for this homework

  • Generalize operations to n bits
  • Combining and splitting qubits mathematically
  • Using these classes to write algorithms

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.)

Set Up

We recommend beginning with a code skeleton (including comments!) and even a set of tests before implementing your code.

Step 1: ParentQubit

The ParentQubit is an abstract class, i.e., it is not implemented itself and simply defines functionality for subclasses. Instead, the SingleQubit and NQubit classes will inherit from it. You should decide the data that the ParentQubit will contain for the state (that is common across both SingleQubit and NQubit.) Notice that, for all methods, indexing is 0-based. That is, the first qubit is qubit 0, the second qubit is qubit 1, etc.
  • ParentQubit(int numqubits)
    # Constructor: initialize all bits to 0
  • set_value(v, i)
    # v is a float
    # i is an integer
    # set the ith value to v
    # Combinations are always ordered in increasing order from 0 to 2^num_qubits-1.
    # Values are negative if the phase should be negative.
  • set_values(v)
    # v is an array of length 2^num_qubits
    # set the amplitudes to v
    # Combinations are always ordered in increasing order from 0 to 2^num_qubits-1.
  • get_value(i)
    # i is an integer
    # return the amplitude of the ith configuration
  • get_values()
    # return the amplitudes of all configurations
  • set_phase(p, i)
    # p is a float
    # i is an integer
    # set the phase of the ith configuration to p
    # Values are negative if the phase should be negative.
  • set_phases(p)
    # p is an array of length 2^num_qubits
    # set the phases to p
    # Values are negative if the phase should be negative.
  • get_phase(i)
    # i is an integer
    # return the phase of the ith configuration
    # return +1 for positive phase and -1 for negative phase
  • get_num_qubits()
    # return the number of qubits
  • merge_qubits(pq)
    # pq is a ParentQubit
    # this merges two sets of qubits and returns a new one that has
    # a number of qubits that is the sum of the two
    # the new qubits are ordered from the first qubit of the first set
    # to the last qubit of the second set
  • to_bra_ket()
    # return a string representation of the state in bra-ket notation
    # e.g. 0.5|00> + 0.5|01> - 0.5|10> + 0.5|11>
    # Note that we did '-' here; we want the formatting to look nice, e.g. we would not want "|00> + -|01>"
  • apply_not_gate(i=None)
    # i is an integer
    # apply the NOT gate to the ith qubit
    # if i is None, apply the NOT gate to all qubits
  • apply_hadamard_gate(i=None)
    # i is an integer
    # apply the Hadamard gate to the ith qubit
    # if i is None, apply the Hadamard gate to all qubits
  • apply_z_gate(i=None)
    # i is an integer
    # apply Z to the ith qubit
    # if i is None, apply the Z gate to all qubits
  • apply_cnot_gate(i, j)
    # i and j are integers
    # apply the CNOT gate with i as the control and j as the target
  • apply_swap_gate(i, j)
    # i and j are integers
    # apply the SWAP gate to the ith and jth qubits
  • measure()
    # measure all qubits in the register
    # return the value measured in the register (big endian)

Step 2: SingleQubit

The SingleQubit class inherits from ParentQubit. It is a single qubit, so it should have a single qubit. Implement the methods described in ParentQubit for this qubit.

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. :)

Step 3: NQubit

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.
  • NQubit(int numqubits);
    # Constructor: initialize all bits to 0
  • public ParentQubit merge_qubits(ParentQubit pq);
    # this merges two sets of qubits and returns a new one that has
    # a number of qubits that is the sum of the two.
  • public String to_bra_ket();
    # this prints out the state in bra-ket notation, like last week
  • public void apply_not_gate();
    # apply a not gate to every qubit
  • public void apply_not_gate(int qb);
    # apply a not gate to the qubit in position qb, where numbering starts at 0
  • public void apply_cnot_gate(int ctrl, int targ);
    # apply a CNOT gate from control to the targ, where numbering starts at 0
  • public void apply_hadamard_gate();
    # apply an H gate to each qubit
  • public void apply_hadamard_gate(int qb);
    # apply an H gate to the qubit in position qb, where numbering starts at 0
  • public void apply_swap_gate(int qubit1, int qubit2);
    # apply a swap gate between qubit1 and qubit2, where numbering starts at 0

Step 4: Circuits

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().

  • same_entangle(qa, i, j)
    # qa is a qubit array and i and j are integers.
    # no return value is necessary because you modify the qubit array qubits.
  • bernvaz(qa, qo)
    # qa is a qubit array and qo is a QOracle.
    # You can assume that nq has 4 bits and has been initialized as expected
    # for the bernvaz algorithm (3 white, 1 black). qo has already been initialized.
    # implement the algorithm.
    # no return value is necessary because you modify the input nq.
  • archimedes(qa, qo)
    # qa is a qubit array and qo is a QOracle.
    # You can assume that nq has 4 bits and has been initialized as expected
    # for the archimedes algorithm (3 white, 1 black). qo has already been initialized.
    # implement the algorithm.
    # no return value is necessary because you modify the input nq.

Step 5: Oracles

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.)

  • set_bernvaz(code)
    # code is a 3-bit integer (0-7) that represents the secret code.
    # 3-bit code, construct the oracle. For each 1 in the 3-bit code.
    # a C-NOT is connected to the response, where the qubit corresponding to
    # the 1 is the control, and the response is the target.
    This shows both the algorithm and the oracle. For secret code 101, you can see the C-NOTs that are connected between the response (bottom line) and the top and bottom inputs. This is how quantum circuits are typically depicted (just lines, no solid, colored parts).
  • probe_bernvaz(nq)
    # nq is a qubit array
    # nq is a 4-qubit input, with the first three qubits being the "guess" and
    # the last qubit being the response. Apply the oracle. No return value is
    # necessary because you modify the state of the input.
  • set_archimedes(codes)
    # codes is an array of 3-bit integers (0-7) that represents the secret codes.
    # receives a set of 3-bit codes (number from 0-7). Based on that
    # 3-bit code, construct the oracle. For each 3-bit code, the last
    # bit of the input gets flipped. Think carefully about what the matrix looks
    # like in the absence of any codes, and then think about how each individual
    # code modifies that starting matrix. Once you have figured it out on paper,
    # then you can work on how to implement that in code.
  • probe_archimedes(nq)
    # nq is a qubit array
    # NQubit is a 4-qubit input, with the first three qubits being the "guess" and
    # the last qubit being the response. Apply the oracle. No return value is
    # necessary because you modify the state of the input.

Testing and Submit

We will not require a certain format for testing. Make sure that you test your code thoroughly. Submit once the autograder comes on line.