def test_diagonalizing_bogoliubov_transform_particle_conserving(self):
        """Test particle-conserving diagonalizing Bogoliubov transform."""

        # Spin-symmetric
        quad_ham = random_quadratic_hamiltonian(5,
                                                conserves_particle_number=True,
                                                expand_spin=True)
        quad_ham = get_quadratic_hamiltonian(
            reorder(get_fermion_operator(quad_ham), up_then_down))

        orbital_energies, transformation_matrix, _ = (
            quad_ham.diagonalizing_bogoliubov_transform())
        max_upper_right = numpy.max(numpy.abs(transformation_matrix[:5, 5:]))
        max_lower_left = numpy.max(numpy.abs(transformation_matrix[5:, :5]))

        numpy.testing.assert_allclose(orbital_energies[:5],
                                      orbital_energies[5:])
        numpy.testing.assert_allclose(transformation_matrix.dot(
            quad_ham.combined_hermitian_part.T.dot(
                transformation_matrix.T.conj())),
                                      numpy.diag(orbital_energies),
                                      atol=1e-7)
        numpy.testing.assert_allclose(max_upper_right, 0.0)
        numpy.testing.assert_allclose(max_lower_left, 0.0)

        # Specific spin sector
        quad_ham = random_quadratic_hamiltonian(5,
                                                conserves_particle_number=True,
                                                expand_spin=True)
        quad_ham = get_quadratic_hamiltonian(
            reorder(get_fermion_operator(quad_ham), up_then_down))

        for spin_sector in range(2):
            orbital_energies, transformation_matrix, _ = (
                quad_ham.diagonalizing_bogoliubov_transform(
                    spin_sector=spin_sector))

            def index_map(i):
                return i + spin_sector * 5

            spin_indices = [index_map(i) for i in range(5)]
            spin_matrix = quad_ham.combined_hermitian_part[numpy.ix_(
                spin_indices, spin_indices)]
            numpy.testing.assert_allclose(transformation_matrix.dot(
                spin_matrix.T.dot(transformation_matrix.T.conj())),
                                          numpy.diag(orbital_energies),
                                          atol=1e-7)

        # Not spin-symmetric
        quad_ham = random_quadratic_hamiltonian(5,
                                                conserves_particle_number=True,
                                                expand_spin=False)
        orbital_energies, _ = quad_ham.orbital_energies()

        _, transformation_matrix, _ = (
            quad_ham.diagonalizing_bogoliubov_transform())
        numpy.testing.assert_allclose(transformation_matrix.dot(
            quad_ham.combined_hermitian_part.T.dot(
                transformation_matrix.T.conj())),
                                      numpy.diag(orbital_energies),
                                      atol=1e-7)
def symmetry_conserving_bravyi_kitaev(fermion_hamiltonian, active_orbitals,
                                      active_fermions):
    """ Returns the qubit Hamiltonian for the fermionic Hamiltonian
        supplied, with two qubits removed using conservation of electron
        spin and number, as described in arXiv:1701.08213.

        Args:
            fermion_hamiltonian: A fermionic hamiltonian obtained
                                 using OpenFermion. An instance
                                 of the FermionOperator class.

            active_orbitals: Int type object. The number of active orbitals
                             being considered for the system.

            active_fermions: Int type object. The number of active fermions
                              being considered for the system (note, this
                              is less than the number of electrons in a
                              molecule if some orbitals have been assumed
                              filled).
        Returns:
                qubit_hamiltonian: The qubit Hamiltonian corresponding to
                                   the supplied fermionic Hamiltonian, with
                                   two qubits removed using spin symmetries.
        WARNING:
                Reorders orbitals from the default even-odd ordering to all
                spin-up orbitals, then all spin-down orbitals.
        Raises:
                ValueError if fermion_hamiltonian isn't of the type
                FermionOperator, or active_orbitals isn't an integer,
                or active_fermions isn't an integer.

        Notes: This function reorders the spin orbitals as all spin-up, then
               all spin-down. It uses the OpenFermion bravyi_kitaev_tree
               mapping, rather than the bravyi-kitaev mapping.
               Caution advised when using with a Fermi-Hubbard Hamiltonian;
               this technique correctly reduces the Hamiltonian only for the
               lowest energy even and odd fermion number states, not states
               with an arbitrary number of fermions.
    """
    # Catch errors if inputs are of wrong type.
    if type(fermion_hamiltonian) is not FermionOperator:
        raise ValueError(
            "Supplied operator should be an instance of FermionOperator class")
    if type(active_orbitals) is not int:
        raise ValueError("Number of active orbitals should be an integer.")
    if type(active_fermions) is not int:
        raise ValueError("Number of active fermions should be an integer.")

    # Arrange spins up then down, then BK map to qubit Hamiltonian.
    fermion_hamiltonian_reorder = reorder(fermion_hamiltonian,
                                          up_then_down,
                                          num_modes=active_orbitals)
    qubit_hamiltonian = bravyi_kitaev_tree(fermion_hamiltonian_reorder,
                                           n_qubits=active_orbitals)
    qubit_hamiltonian.compress()

    # Allocates the parity factors for the orbitals as in arXiv:1704.05018.
    remainder = active_fermions % 4
    if remainder == 0:
        parity_final_orb = 1
        parity_middle_orb = 1
    elif remainder == 1:
        parity_final_orb = -1
        parity_middle_orb = -1
    elif remainder == 2:
        parity_final_orb = 1
        parity_middle_orb = -1
    else:
        parity_final_orb = -1
        parity_middle_orb = 1

    # Removes the final qubit, then the middle qubit.
    qubit_hamiltonian = edit_hamiltonian_for_spin(qubit_hamiltonian,
                                                  active_orbitals,
                                                  parity_final_orb)
    qubit_hamiltonian = edit_hamiltonian_for_spin(qubit_hamiltonian,
                                                  active_orbitals / 2,
                                                  parity_middle_orb)
    qubit_hamiltonian = remove_indices(qubit_hamiltonian,
                                       (active_orbitals / 2, active_orbitals))

    return qubit_hamiltonian