def setUp(self):
        self.n_qubits = 5
        self.constant = 1.7
        self.chemical_potential = 2.

        # Obtain random Hermitian and antisymmetric matrices
        self.hermitian_mat = random_hermitian_matrix(self.n_qubits)
        self.antisymmetric_mat = random_antisymmetric_matrix(self.n_qubits)

        self.combined_hermitian = (
            self.hermitian_mat -
            self.chemical_potential * numpy.eye(self.n_qubits))

        # Initialize a particle-number-conserving Hamiltonian
        self.quad_ham_pc = QuadraticHamiltonian(self.hermitian_mat,
                                                constant=self.constant)

        # Initialize a non-particle-number-conserving Hamiltonian
        self.quad_ham_npc = QuadraticHamiltonian(self.hermitian_mat,
                                                 self.antisymmetric_mat,
                                                 self.constant,
                                                 self.chemical_potential)

        # Initialize the sparse operators and get their ground energies
        self.quad_ham_pc_sparse = get_sparse_operator(self.quad_ham_pc)
        self.quad_ham_npc_sparse = get_sparse_operator(self.quad_ham_npc)

        self.pc_ground_energy, self.pc_ground_state = get_ground_state(
            self.quad_ham_pc_sparse)
        self.npc_ground_energy, self.npc_ground_state = get_ground_state(
            self.quad_ham_npc_sparse)
    def test_diagonalizing_bogoliubov_transform(self):
        """Test diagonalizing Bogoliubov transform."""
        hermitian_part = numpy.array(
            [[0.0, 1.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 0.0]], dtype=complex)
        antisymmetric_part = numpy.array(
            [[0.0, 1.0j, 0.0], [-1.0j, 0.0, 1.0j], [0.0, -1.0j, 0.0]],
            dtype=complex)
        quad_ham = QuadraticHamiltonian(hermitian_part, antisymmetric_part)
        block_matrix = numpy.zeros((6, 6), dtype=complex)
        block_matrix[:3, :3] = antisymmetric_part
        block_matrix[:3, 3:] = hermitian_part
        block_matrix[3:, :3] = -hermitian_part.conj()
        block_matrix[3:, 3:] = -antisymmetric_part.conj()

        _, transformation_matrix, _ = (
            quad_ham.diagonalizing_bogoliubov_transform())
        left_block = transformation_matrix[:, :3]
        right_block = transformation_matrix[:, 3:]
        ferm_unitary = numpy.zeros((6, 6), dtype=complex)
        ferm_unitary[:3, :3] = left_block
        ferm_unitary[:3, 3:] = right_block
        ferm_unitary[3:, :3] = numpy.conjugate(right_block)
        ferm_unitary[3:, 3:] = numpy.conjugate(left_block)

        # Check that the transformation is diagonalizing
        majorana_matrix, _ = quad_ham.majorana_form()
        canonical, _ = antisymmetric_canonical_form(majorana_matrix)
        diagonalized = ferm_unitary.conj().dot(
            block_matrix.dot(ferm_unitary.T.conj()))
        for i in numpy.ndindex((6, 6)):
            self.assertAlmostEqual(diagonalized[i], canonical[i])
def random_quadratic_hamiltonian(n_orbitals,
                                 conserves_particle_number=False,
                                 real=False,
                                 expand_spin=False,
                                 seed=None):
    """Generate a random instance of QuadraticHamiltonian.

    Args:
        n_orbitals(int): the number of orbitals
        conserves_particle_number(bool): whether the returned Hamiltonian
            should conserve particle number
        real(bool): whether to use only real numbers
        expand_spin: Whether to expand each orbital symmetrically into two
            spin orbitals. Note that if this option is set to True, then
            the total number of orbitals will be doubled.

    Returns:
        QuadraticHamiltonian
    """
    if seed is not None:
        numpy.random.seed(seed)

    constant = numpy.random.randn()
    chemical_potential = numpy.random.randn()
    hermitian_mat = random_hermitian_matrix(n_orbitals, real)

    if conserves_particle_number:
        antisymmetric_mat = None
    else:
        antisymmetric_mat = random_antisymmetric_matrix(n_orbitals, real)

    if expand_spin:
        hermitian_mat = numpy.kron(hermitian_mat, numpy.eye(2))
        if antisymmetric_mat is not None:
            antisymmetric_mat = numpy.kron(antisymmetric_mat, numpy.eye(2))

    return QuadraticHamiltonian(hermitian_mat, antisymmetric_mat, constant,
                                chemical_potential)
class QuadraticHamiltonianTest(unittest.TestCase):
    def setUp(self):
        self.n_qubits = 5
        self.constant = 1.7
        self.chemical_potential = 2.

        # Obtain random Hermitian and antisymmetric matrices
        self.hermitian_mat = random_hermitian_matrix(self.n_qubits)
        self.antisymmetric_mat = random_antisymmetric_matrix(self.n_qubits)

        self.combined_hermitian = (
            self.hermitian_mat -
            self.chemical_potential * numpy.eye(self.n_qubits))

        # Initialize a particle-number-conserving Hamiltonian
        self.quad_ham_pc = QuadraticHamiltonian(self.hermitian_mat,
                                                constant=self.constant)

        # Initialize a non-particle-number-conserving Hamiltonian
        self.quad_ham_npc = QuadraticHamiltonian(self.hermitian_mat,
                                                 self.antisymmetric_mat,
                                                 self.constant,
                                                 self.chemical_potential)

        # Initialize the sparse operators and get their ground energies
        self.quad_ham_pc_sparse = get_sparse_operator(self.quad_ham_pc)
        self.quad_ham_npc_sparse = get_sparse_operator(self.quad_ham_npc)

        self.pc_ground_energy, self.pc_ground_state = get_ground_state(
            self.quad_ham_pc_sparse)
        self.npc_ground_energy, self.npc_ground_state = get_ground_state(
            self.quad_ham_npc_sparse)

    def test_combined_hermitian_part(self):
        """Test getting the combined Hermitian part."""
        combined_hermitian_part = self.quad_ham_pc.combined_hermitian_part
        for i in numpy.ndindex(combined_hermitian_part.shape):
            self.assertAlmostEqual(self.hermitian_mat[i],
                                   combined_hermitian_part[i])

        combined_hermitian_part = self.quad_ham_npc.combined_hermitian_part
        for i in numpy.ndindex(combined_hermitian_part.shape):
            self.assertAlmostEqual(self.combined_hermitian[i],
                                   combined_hermitian_part[i])

    def test_hermitian_part(self):
        """Test getting the Hermitian part."""
        hermitian_part = self.quad_ham_pc.hermitian_part
        for i in numpy.ndindex(hermitian_part.shape):
            self.assertAlmostEqual(self.hermitian_mat[i], hermitian_part[i])

        hermitian_part = self.quad_ham_npc.hermitian_part
        for i in numpy.ndindex(hermitian_part.shape):
            self.assertAlmostEqual(self.hermitian_mat[i], hermitian_part[i])

    def test_antisymmetric_part(self):
        """Test getting the antisymmetric part."""
        antisymmetric_part = self.quad_ham_pc.antisymmetric_part
        for i in numpy.ndindex(antisymmetric_part.shape):
            self.assertAlmostEqual(0., antisymmetric_part[i])

        antisymmetric_part = self.quad_ham_npc.antisymmetric_part
        for i in numpy.ndindex(antisymmetric_part.shape):
            self.assertAlmostEqual(self.antisymmetric_mat[i],
                                   antisymmetric_part[i])

    def test_conserves_particle_number(self):
        """Test checking whether Hamiltonian conserves particle number."""
        self.assertTrue(self.quad_ham_pc.conserves_particle_number)
        self.assertFalse(self.quad_ham_npc.conserves_particle_number)

    def test_add_chemical_potential(self):
        """Test adding a chemical potential."""
        self.quad_ham_npc.add_chemical_potential(2.4)

        combined_hermitian_part = self.quad_ham_npc.combined_hermitian_part
        hermitian_part = self.quad_ham_npc.hermitian_part

        want_combined = (self.combined_hermitian -
                         2.4 * numpy.eye(self.n_qubits))
        want_hermitian = self.hermitian_mat

        for i in numpy.ndindex(combined_hermitian_part.shape):
            self.assertAlmostEqual(combined_hermitian_part[i],
                                   want_combined[i])

        for i in numpy.ndindex(hermitian_part.shape):
            self.assertAlmostEqual(hermitian_part[i], want_hermitian[i])

        self.assertAlmostEqual(2.4 + self.chemical_potential,
                               self.quad_ham_npc.chemical_potential)

    def test_orbital_energies(self):
        """Test getting the orbital energies."""
        # Test the particle-number-conserving case
        orbital_energies, constant = self.quad_ham_pc.orbital_energies()
        # Test the ground energy
        energy = numpy.sum(orbital_energies[orbital_energies < 0.0]) + constant
        self.assertAlmostEqual(energy, self.pc_ground_energy)

        # Test the non-particle-number-conserving case
        orbital_energies, constant = self.quad_ham_npc.orbital_energies()
        # Test the ground energy
        energy = constant
        self.assertAlmostEqual(energy, self.npc_ground_energy)

    def test_ground_energy(self):
        """Test getting the ground energy."""
        # Test particle-number-conserving case
        energy = self.quad_ham_pc.ground_energy()
        self.assertAlmostEqual(energy, self.pc_ground_energy)
        # Test non-particle-number-conserving case
        energy = self.quad_ham_npc.ground_energy()
        self.assertAlmostEqual(energy, self.npc_ground_energy)

    def test_majorana_form(self):
        """Test getting the Majorana form."""
        majorana_matrix, majorana_constant = self.quad_ham_npc.majorana_form()
        # Convert the Majorana form to a FermionOperator
        majorana_op = FermionOperator((), majorana_constant)
        normalization = 1. / numpy.sqrt(2.)
        for i in range(2 * self.n_qubits):
            if i < self.n_qubits:
                left_op = majorana_operator((i, 0), normalization)
            else:
                left_op = majorana_operator((i - self.n_qubits, 1),
                                            normalization)
            for j in range(2 * self.n_qubits):
                if j < self.n_qubits:
                    right_op = majorana_operator(
                        (j, 0), majorana_matrix[i, j] * normalization)
                else:
                    right_op = majorana_operator(
                        (j - self.n_qubits, 1),
                        majorana_matrix[i, j] * normalization)
                majorana_op += .5j * left_op * right_op
        # Get FermionOperator for original Hamiltonian
        fermion_operator = normal_ordered(
            get_fermion_operator(self.quad_ham_npc))
        self.assertTrue(normal_ordered(majorana_op) == fermion_operator)

    def test_diagonalizing_bogoliubov_transform(self):
        """Test diagonalizing Bogoliubov transform."""
        hermitian_part = numpy.array(
            [[0.0, 1.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 0.0]], dtype=complex)
        antisymmetric_part = numpy.array(
            [[0.0, 1.0j, 0.0], [-1.0j, 0.0, 1.0j], [0.0, -1.0j, 0.0]],
            dtype=complex)
        quad_ham = QuadraticHamiltonian(hermitian_part, antisymmetric_part)
        block_matrix = numpy.zeros((6, 6), dtype=complex)
        block_matrix[:3, :3] = antisymmetric_part
        block_matrix[:3, 3:] = hermitian_part
        block_matrix[3:, :3] = -hermitian_part.conj()
        block_matrix[3:, 3:] = -antisymmetric_part.conj()

        _, transformation_matrix, _ = (
            quad_ham.diagonalizing_bogoliubov_transform())
        left_block = transformation_matrix[:, :3]
        right_block = transformation_matrix[:, 3:]
        ferm_unitary = numpy.zeros((6, 6), dtype=complex)
        ferm_unitary[:3, :3] = left_block
        ferm_unitary[:3, 3:] = right_block
        ferm_unitary[3:, :3] = numpy.conjugate(right_block)
        ferm_unitary[3:, 3:] = numpy.conjugate(left_block)

        # Check that the transformation is diagonalizing
        majorana_matrix, _ = quad_ham.majorana_form()
        canonical, _ = antisymmetric_canonical_form(majorana_matrix)
        diagonalized = ferm_unitary.conj().dot(
            block_matrix.dot(ferm_unitary.T.conj()))
        for i in numpy.ndindex((6, 6)):
            self.assertAlmostEqual(diagonalized[i], canonical[i])

    def test_diagonalizing_bogoliubov_transform_non_particle_conserving(self):
        """Test non-particle-conserving diagonalizing Bogoliubov transform."""
        hermitian_part = self.quad_ham_npc.combined_hermitian_part
        antisymmetric_part = self.quad_ham_npc.antisymmetric_part
        block_matrix = numpy.zeros((2 * self.n_qubits, 2 * self.n_qubits),
                                   dtype=complex)
        block_matrix[:self.n_qubits, :self.n_qubits] = antisymmetric_part
        block_matrix[:self.n_qubits, self.n_qubits:] = hermitian_part
        block_matrix[self.n_qubits:, :self.n_qubits] = -hermitian_part.conj()
        block_matrix[self.n_qubits:,
                     self.n_qubits:] = (-antisymmetric_part.conj())

        _, transformation_matrix, _ = (
            self.quad_ham_npc.diagonalizing_bogoliubov_transform())
        left_block = transformation_matrix[:, :self.n_qubits]
        right_block = transformation_matrix[:, self.n_qubits:]
        ferm_unitary = numpy.zeros((2 * self.n_qubits, 2 * self.n_qubits),
                                   dtype=complex)
        ferm_unitary[:self.n_qubits, :self.n_qubits] = left_block
        ferm_unitary[:self.n_qubits, self.n_qubits:] = right_block
        ferm_unitary[self.n_qubits:, :self.n_qubits] = numpy.conjugate(
            right_block)
        ferm_unitary[self.n_qubits:,
                     self.n_qubits:] = numpy.conjugate(left_block)

        # Check that the transformation is diagonalizing
        majorana_matrix, _ = self.quad_ham_npc.majorana_form()
        canonical, _ = antisymmetric_canonical_form(majorana_matrix)
        diagonalized = ferm_unitary.conj().dot(
            block_matrix.dot(ferm_unitary.T.conj()))
        for i in numpy.ndindex((2 * self.n_qubits, 2 * self.n_qubits)):
            self.assertAlmostEqual(diagonalized[i], canonical[i])

        lower_unitary = ferm_unitary[self.n_qubits:]
        lower_left = lower_unitary[:, :self.n_qubits]
        lower_right = lower_unitary[:, self.n_qubits:]

        # Check that lower_left and lower_right satisfy the constraints
        # necessary for the transformed fermionic operators to satisfy
        # the fermionic anticommutation relations
        constraint_matrix_1 = (lower_left.dot(lower_left.T.conj()) +
                               lower_right.dot(lower_right.T.conj()))
        constraint_matrix_2 = (lower_left.dot(lower_right.T) +
                               lower_right.dot(lower_left.T))

        identity = numpy.eye(self.n_qubits, dtype=complex)
        for i in numpy.ndindex((self.n_qubits, self.n_qubits)):
            self.assertAlmostEqual(identity[i], constraint_matrix_1[i])
            self.assertAlmostEqual(0., constraint_matrix_2[i])

    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 test_diagonalizing_bogoliubov_transform_exceptions(self):
        quad_ham = random_quadratic_hamiltonian(5)
        with self.assertRaises(ValueError):
            _ = quad_ham.diagonalizing_bogoliubov_transform(spin_sector=0)

        quad_ham = random_quadratic_hamiltonian(
            5, conserves_particle_number=False, expand_spin=True)
        with self.assertRaises(NotImplementedError):
            _ = quad_ham.diagonalizing_bogoliubov_transform(spin_sector=0)