Example #1
0
 def __init__(self, num_rails, initial_state=None):
     self.num_rails = num_rails
     self.state = []
     self.gates = []
     if initial_state is not None:
         if initial_state.shape == [num_rails, 1]:
             # Qubit-wise definition.
             state = None
             for qubit in initial_state:
                 if qubit == 0:
                     if state is None:
                         state = zero
                     else:
                         state = tensorprod(state, zero)
                 elif qubit == 1:
                     if state is None:
                         state = one
                     else:
                         state = tensorprod(state, one)
                 else:
                     raise ValueError('Qubit can only be either 0 or 1')
         else:
             if initial_state.shape != (2 ** num_rails, 1):
                 # Not a full description.
                 raise ValueError('Incomplete or improper definition of'
                                  ' initial_state')
     else:
         initial_state = tensor(zero, num_rails)
     self.state.append(initial_state)
Example #2
0
    def add_gate(self, gate, qubits, controls=[]):
        """
        Add a gate to the circuit.

        Can currently handle only single-qubit gates with one or more controls.

        Parameters
        ----------
        gate : np.ndarray
            2D matrix defining the transformation. Must be unitary. Size of the
            matrix must be (NxN) where N is 2^len(qubits)
        qubits : list
            List of rail or qubit numbers upon which this gate acts. Numbers
            must be between 1 and num_rails.
        controls : list
            Control qubits upon which this gate depends. Absolute value of
            qubit numbers must be between 1 and num_rails. Use a negative sign
            to indicate 0-control.

        Returns
        -------
        state : np.ndarray
            (num_rails x 1) dimensional state vector after successful
            application of the gate.
        """

        # Implementation notes:
        #
        # 1. Currently computing whole operator matrix for the entire state
        #    space from the single-qubit gate, the qubit being operated upon
        #    and the control qubits.
        # 2. Qubit-wise computations are not feasible, because we would then
        #    have to operate multi-qubit gates on some qubits *only*, which
        #    would be difficult because it is hard to extract qubits from the
        #    system state.
        # 3. Implementing control by looking at the probability of the control
        #    qubit being 0 or 1 and then applying the operation with that
        #    probability is harder than simply writing out the whole operation
        #    matrix.

        n = len(qubits)
        N = 2 ** n
        if gate.shape != (N, N):
            raise ValueError('Gate is of improper shape')
        if n != 1 or gate.shape != (2, 2):
            raise NotImplementedError('Cannot handle gates that are not single'
                                      '-qubit')
        controls = np.array(controls)

        # First, determine the x-operator, to invert zero-control rails.
        x_operator = np.array([[1,],])
        for i in xrange(1, self.num_rails+1):
            if i in abs(controls):
                if controls[np.where(abs(controls) == i)] < 0:
                    # Zero-control
                    x_operator = tensorprod(x_operator, X)
                else:
                    x_operator = tensorprod(x_operator, I)
            else:
                x_operator = tensorprod(x_operator, I)

        # Now, determine the actual operation of the gate itself.
        operator = np.array([[1,],])
        for i in xrange(1, self.num_rails+1):
            if i in qubits:
                if i in abs(controls):
                    raise ValueError('A qubit cannot be used for the gate and'
                                     ' also be a control')
                else:
                    operator = tensorprod(operator, gate)
            else:
                # Ignore control qubits for now. Treat them like identity.
                operator = tensorprod(operator, I)

        # Now go back and identify (pun intended) parts of the operator where
        # there is control.
        controls = abs(controls)
        controls.sort()
        # Identity is a big I block that will be used to wipe out those places
        # where there should have been a control, but we put a gate instead.
        identity = tensor(I, self.num_rails-1)
        for i in controls[::-1]:
            # Block size is the number of contiguous 0's that appear at the
            # i'th qubit when you write the binary numbers in order.
            block_size = 2 ** (self.num_rails - i)
            # We start with one square of dimension (block_size x block_size)
            # at the (0, 0) index and make it identity. We then skip one
            # block, go to the third block starting from (0, 0) and repeat.
            # We do this both sideways and downwards. That effectively adds
            # the requisite control to the entire operator.
            for j in xrange(0, 2**self.num_rails, 2*block_size):
                for k in xrange(0, 2**self.num_rails, 2*block_size):
                    # p and q are starting positions of blocks of dimension
                    # (block_size x block_size) in indentity. These are
                    # required because identity is only half as big in side
                    # length when compared with operator.
                    p = j / 2
                    q = k / 2
                    operator[j:j+block_size, k:k+block_size] = (
                        identity[p:p+block_size, q:q+block_size]
                    )

        # Compute the overall gate matrix
        gate_matrix = np.dot(x_operator, np.dot(operator, x_operator))
        # Compute and return the state after application of the gate
        state = np.dot(gate_matrix, self.state[-1])
        self.state.append(state)
        return state