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
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
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)
def test_bad_constraints(self): n = 3 ones_mat = numpy.ones((n, 2 * n)) with self.assertRaises(ValueError): _ = fermionic_gaussian_decomposition(ones_mat)
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])