src/qsim/gate.py
0a4b3d01
 """Quantum gate operations
346cf14e
 
0a4b3d01
 A quantum gate acting on :math:`n` qubits is a :math:`2^n×2^n` unitary
 matrix written in the computational basis.
 """
 
 import numpy as np
 
65919468
 from . import operator
 
0a4b3d01
 __all__ = [
1673b8dc
     "apply",
0a4b3d01
     "n_qubits",
     "controlled",
     # -- Single qubit gates --
     "id",
     "x",
     "y",
     "z",
     "not_",
     "sqrt_not",
     "phase_shift",
     # -- 2 qubit gates --
     "cnot",
     "swap",
     # -- 3 qubit gates --
     "toffoli",
     "cswap",
     "fredkin",
     "deutsch",
 ]  # type: ignore
 
 
1673b8dc
 def apply(gate, qubits, state):
0232cc30
     """Apply a gate to the specified qubits of a state
 
     Parameters
     ----------
65919468
     gate: ndarray[complex]
         The gate to apply.
0232cc30
     qubits : sequence of int
         The qubits on which to act. Qubit 0 is the least significant qubit.
     state : ndarray[complex]
 
     Returns
     -------
     new_state : ndarray[complex]
     """
65919468
     _check_valid_gate(gate)
     return operator.apply(gate, qubits, state)
 
 
 n_qubits = operator.n_qubits
1673b8dc
 
 
0a4b3d01
 def _check_valid_gate(gate):
65919468
     if not operator.is_unitary(gate):
         raise ValueError("Gate is invalid")
0a4b3d01
 
 
 def controlled(gate):
     """Return a controlled quantum gate, given a quantum gate.
 
     If 'gate' operates on :math:`n` qubits, then the controlled gate operates
     on :math:`n+1` qubits, where the most-significant qubit is the control.
 
     Parameters
     ----------
     gate : np.ndarray[complex]
         A quantum gate acting on :math:`n` qubits;
         a :math:`2^n×2^n` unitary matrix in the computational basis.
 
     Returns
     -------
     controlled_gate : np.ndarray[(2**(n+1), 2**(n+1)), complex]
     """
     _check_valid_gate(gate)
     n = gate.shape[0]
     zeros = np.zeros((n, n))
     return np.block([[np.identity(n), zeros], [zeros, gate]])
 
 
 # -- Single qubit gates --
 
 #: The identity gate on 1 qubit
 id = np.identity(2, complex)
 #: Pauli X gate
 x = np.array([[0, 1], [1, 0]], complex)
 #: NOT gate
 not_ = x
 #: Pauli Y gate
 y = np.array([[0, -1j], [1j, 0]], complex)
 #: Pauli Z gate
 z = np.array([[1, 0], [0, -1]], complex)
 #: SQRT(NOT) gate
 sqrt_not = 0.5 * (1 + 1j * id - 1j * x)
 #: Hadamard gate
 hadamard = np.sqrt(0.5) * (x + z)
 
 
 def phase_shift(phi):
     "Return a gate that shifts the phase of :math:`|1⟩` by :math:`φ`."
     return np.array([[1, 0], [0, np.exp(1j * phi)]])
 
 
 # -- Two qubit gates --
 
 #: Controlled NOT gate
6ec8f307
 cnot = controlled(not_)
0a4b3d01
 #: SWAP gate
 swap = np.identity(4, complex)[:, (0, 2, 1, 3)]
 
 # -- Three qubit gates --
 
 #: Toffoli (CCNOT) gate
 toffoli = controlled(cnot)
 #: Controlled SWAP gate
 cswap = controlled(swap)
 #: Fredkin gate
 fredkin = cswap
 
 
 def deutsch(phi):
     "Return a Deutsch gate for angle :math:`φ`."
     gate = np.identity(8, complex)
     gate[-2:, -2:] = np.array(
         [[1j * np.cos(phi), np.sin(phi)], [np.sin(phi), 1j * np.cos(phi)]]
     )
     return gate