示例#1
0
    def diagonalizing_circuit(self):
        r"""Get a circuit for a unitary that diagonalizes this Hamiltonian

        This circuit performs the transformation to a basis in which the
        Hamiltonian takes the diagonal form

        .. math::

            \sum_{j} \varepsilon_j b^\dagger_j b_j + \text{constant}.

        Returns
        -------
            circuit_description (list[tuple]):
                A list of operations describing the circuit. Each operation
                is a tuple of objects describing elementary operations that
                can be performed in parallel. Each elementary operation
                is either the string 'pht' indicating a particle-hole
                transformation on the last fermionic mode, or a tuple of
                the form :math:`(i, j, \theta, \varphi)`,
                indicating a Givens rotation
                of modes :math:`i` and :math:`j` by angles :math:`\theta`
                and :math:`\varphi`.
        """
        _, transformation_matrix, _ = self.diagonalizing_bogoliubov_transform()

        if self.conserves_particle_number:
            # The Hamiltonian conserves particle number, so we don't need
            # to use the most general procedure.
            decomposition, _ = givens_decomposition_square(
                transformation_matrix)
            circuit_description = list(reversed(decomposition))
        else:
            # The Hamiltonian does not conserve particle number, so we
            # need to use the most general procedure.
            # Rearrange the transformation matrix because the circuit
            # generation routine expects it to describe annihilation
            # operators rather than creation operators.
            left_block = transformation_matrix[:, :self.n_qubits]
            right_block = transformation_matrix[:, self.n_qubits:]

            # Can't use numpy.block because that requires numpy>=1.13.0
            new_transformation_matrix = numpy.empty(
                (self.n_qubits, 2 * self.n_qubits), dtype=complex)
            new_transformation_matrix[:, :self.n_qubits] = numpy.conjugate(
                right_block)
            new_transformation_matrix[:, self.n_qubits:] = numpy.conjugate(
                left_block)

            # Get the circuit description
            decomposition, left_decomposition, _, _ = (
                fermionic_gaussian_decomposition(new_transformation_matrix))

            # need to use left_diagonal too
            circuit_description = list(
                reversed(decomposition + left_decomposition))

        return circuit_description
示例#2
0
def gaussian_state_preparation_circuit(quadratic_hamiltonian,
                                       occupied_orbitals=None,
                                       spin_sector=None):
    r"""Obtain the description of a circuit which prepares a fermionic Gaussian
    state.

    Fermionic Gaussian states can be regarded as eigenstates of quadratic
    Hamiltonians. If the Hamiltonian conserves particle number, then these are
    just Slater determinants. See arXiv:1711.05395 for a detailed description
    of how this procedure works.

    The circuit description is returned as a sequence of elementary
    operations; operations that can be performed in parallel are grouped
    together. Each elementary operation is either

    - the string 'pht', indicating the particle-hole transformation
      on the last fermionic mode, which is the operator :math:`\mathcal{B}`
      such that

      .. math::

          \begin{align}
              \mathcal{B} a_N \mathcal{B}^\dagger &= a_N^\dagger,\\
              \mathcal{B} a_j \mathcal{B}^\dagger &= a_j, \quad
                  j = 1, \ldots, N-1,
          \end{align}

      or

    - a tuple :math:`(i, j, \theta, \varphi)`, indicating the operation

      .. math::
          \exp[i \varphi a_j^\dagger a_j]
          \exp[\theta (a_i^\dagger a_j - a_j^\dagger a_i)],

      a Givens rotation of modes :math:`i` and :math:`j` by angles
      :math:`\theta` and :math:`\varphi`.

    Args:
        quadratic_hamiltonian(QuadraticHamiltonian):
            The Hamiltonian whose eigenstate is desired.
        occupied_orbitals(list):
            A list of integers representing the indices of the occupied
            orbitals in the desired Gaussian state. If this is None
            (the default), then it is assumed that the ground state is
            desired, i.e., the orbitals with negative energies are filled.
        spin_sector (optional str): An optional integer specifying
            a spin sector to restrict to: 0 for spin-up and 1 for
            spin-down. If specified, the returned circuit acts on modes
            indexed by spatial indices (rather than spin indices).
            Should only be specified if the Hamiltonian
            includes a spin degree of freedom and spin-up modes
            do not interact with spin-down modes.

    Returns
    -------
        circuit_description (list[tuple]):
            A list of operations describing the circuit. Each operation
            is a tuple of objects describing elementary operations that
            can be performed in parallel. Each elementary operation
            is either the string 'pht', indicating a particle-hole
            transformation on the last fermionic mode, or a tuple of
            the form :math:`(i, j, \theta, \varphi)`,
            indicating a Givens rotation
            of modes :math:`i` and :math:`j` by angles :math:`\theta`
            and :math:`\varphi`.
        start_orbitals (list):
            The occupied orbitals to start with. This describes the
            initial state that the circuit should be applied to: it should
            be a Slater determinant (in the computational basis) with these
            orbitals filled.
    """
    if not isinstance(quadratic_hamiltonian, QuadraticHamiltonian):
        raise ValueError('Input must be an instance of QuadraticHamiltonian.')

    orbital_energies, transformation_matrix, _ = (
        quadratic_hamiltonian.diagonalizing_bogoliubov_transform(
            spin_sector=spin_sector))

    if quadratic_hamiltonian.conserves_particle_number:
        if occupied_orbitals is None:
            # The ground state is desired, so we fill the orbitals that have
            # negative energy
            occupied_orbitals = numpy.where(orbital_energies < 0.0)[0]

        # Get the unitary rows which represent the Slater determinant
        slater_determinant_matrix = transformation_matrix[occupied_orbitals]

        # Get the circuit description
        circuit_description = slater_determinant_preparation_circuit(
            slater_determinant_matrix)
        start_orbitals = range(len(occupied_orbitals))
    else:
        # TODO implement this
        if spin_sector is not None:
            raise NotImplementedError("Not yet supported")
        # Rearrange the transformation matrix because the circuit generation
        # routine expects it to describe annihilation operators rather than
        # creation operators
        n_qubits = quadratic_hamiltonian.n_qubits
        left_block = transformation_matrix[:, :n_qubits]
        right_block = transformation_matrix[:, n_qubits:]
        # Can't use numpy.block because that requires numpy>=1.13.0
        new_transformation_matrix = numpy.empty((n_qubits, 2 * n_qubits),
                                                dtype=complex)
        new_transformation_matrix[:, :n_qubits] = numpy.conjugate(right_block)
        new_transformation_matrix[:, n_qubits:] = numpy.conjugate(left_block)

        # Get the circuit description
        decomposition, left_decomposition, _, _ = (
            fermionic_gaussian_decomposition(new_transformation_matrix))
        if occupied_orbitals is None:
            # The ground state is desired, so the circuit should be applied
            # to the vaccuum state
            start_orbitals = []
            circuit_description = list(reversed(decomposition))
        else:
            start_orbitals = occupied_orbitals
            # The circuit won't be applied to the ground state, so we need to
            # use left_decomposition
            circuit_description = list(
                reversed(decomposition + left_decomposition))

    return circuit_description, start_orbitals
示例#3
0
 def test_bad_dimensions(self):
     n, p = (3, 7)
     rand_mat = numpy.random.randn(n, p)
     with self.assertRaises(ValueError):
         _ = fermionic_gaussian_decomposition(rand_mat)
示例#4
0
 def test_bad_constraints(self):
     n = 3
     ones_mat = numpy.ones((n, 2 * n))
     with self.assertRaises(ValueError):
         _ = fermionic_gaussian_decomposition(ones_mat)
示例#5
0
    def test_main_procedure(self):
        for n in self.test_dimensions:
            # Obtain a random quadratic Hamiltonian
            quadratic_hamiltonian = random_quadratic_hamiltonian(n)

            # Get the diagonalizing transformation
            _, transformation_matrix, _ = (
                quadratic_hamiltonian.diagonalizing_bogoliubov_transform())
            left_block = transformation_matrix[:, :n]
            right_block = transformation_matrix[:, n:]
            lower_unitary = numpy.empty((n, 2 * n), dtype=complex)
            lower_unitary[:, :n] = numpy.conjugate(right_block)
            lower_unitary[:, n:] = numpy.conjugate(left_block)

            # Get fermionic Gaussian decomposition of lower_unitary
            decomposition, left_decomposition, diagonal, left_diagonal = (
                fermionic_gaussian_decomposition(lower_unitary))

            # Compute left_unitary
            left_unitary = numpy.eye(n, dtype=complex)
            for parallel_set in left_decomposition:
                combined_op = numpy.eye(n, dtype=complex)
                for op in reversed(parallel_set):
                    i, j, theta, phi = op
                    c = numpy.cos(theta)
                    s = numpy.sin(theta)
                    phase = numpy.exp(1.j * phi)
                    givens_rotation = numpy.array(
                        [[c, -phase * s], [s, phase * c]], dtype=complex)
                    givens_rotate(combined_op, givens_rotation, i, j)
                left_unitary = combined_op.dot(left_unitary)
            for i in range(n):
                left_unitary[i] *= left_diagonal[i]
            left_unitary = left_unitary.T
            for i in range(n):
                left_unitary[i] *= diagonal[i]

            # Check that left_unitary zeroes out the correct entries of
            # lower_unitary
            product = left_unitary.dot(lower_unitary)
            for i in range(n - 1):
                for j in range(n - 1 - i):
                    self.assertAlmostEqual(product[i, j], 0.)

            # Compute right_unitary
            right_unitary = numpy.eye(2 * n, dtype=complex)
            for parallel_set in decomposition:
                combined_op = numpy.eye(2 * n, dtype=complex)
                for op in reversed(parallel_set):
                    if op == 'pht':
                        swap_rows(combined_op, n - 1, 2 * n - 1)
                    else:
                        i, j, theta, phi = op
                        c = numpy.cos(theta)
                        s = numpy.sin(theta)
                        phase = numpy.exp(1.j * phi)
                        givens_rotation = numpy.array(
                            [[c, -phase * s], [s, phase * c]], dtype=complex)
                        double_givens_rotate(combined_op, givens_rotation, i,
                                             j)
                right_unitary = combined_op.dot(right_unitary)

            # Compute left_unitary * lower_unitary * right_unitary^\dagger
            product = left_unitary.dot(
                lower_unitary.dot(right_unitary.T.conj()))

            # Construct the diagonal matrix
            diag = numpy.zeros((n, 2 * n), dtype=complex)
            diag[range(n), range(n, 2 * n)] = diagonal

            # Assert that W and D are the same
            for i in numpy.ndindex((n, 2 * n)):
                self.assertAlmostEqual(diag[i], product[i])