Source code for qiskit_nature.second_q.circuit.library.initial_states.hartree_fock

# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2018, 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Hartree-Fock initial state."""

from __future__ import annotations
import numpy as np
from qiskit import QuantumRegister
from qiskit.circuit.library import BlueprintCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_algorithms.utils.validation import validate_min

from qiskit_nature.second_q.mappers import (
    BravyiKitaevSuperFastMapper,
    QubitMapper,
    TaperedQubitMapper,
)
from qiskit_nature.second_q.operators import FermionicOp


[docs]class HartreeFock(BlueprintCircuit): """A Hartree-Fock initial state.""" def __init__( self, num_spatial_orbitals: int | None = None, num_particles: tuple[int, int] | None = None, qubit_mapper: QubitMapper | None = None, ) -> None: # pylint: disable=unused-argument """ Args: num_spatial_orbitals: The number of spatial orbitals. num_particles: The number of particles as a tuple storing the number of alpha and beta-spin electrons in the first and second number, respectively. qubit_mapper: A :class:`~qiskit_nature.second_q.mappers.QubitMapper`. Raises: NotImplementedError: If ``qubit_mapper`` is (or uses) a :class:`~qiskit_nature.second_q.mappers.BravyiKitaevSuperFastMapper`. See https://github.com/Qiskit/qiskit-nature/issues/537 for more information. """ super().__init__() self._qubit_mapper = qubit_mapper self._num_spatial_orbitals = num_spatial_orbitals self._num_particles = num_particles self._bitstr: list[bool] | None = None self._reset_register() @property def qubit_mapper(self) -> QubitMapper | None: """The qubit mapper.""" return self._qubit_mapper @qubit_mapper.setter def qubit_mapper(self, mapper: QubitMapper | None) -> None: """Sets the qubit mapper.""" self._invalidate() self._qubit_mapper = mapper self._reset_register() @property def num_spatial_orbitals(self) -> int: """The number of spatial orbitals.""" return self._num_spatial_orbitals @num_spatial_orbitals.setter def num_spatial_orbitals(self, n: int) -> None: """Sets the number of spatial orbitals.""" self._invalidate() self._num_spatial_orbitals = n self._reset_register() @property def num_particles(self) -> tuple[int, int]: """The number of particles.""" return self._num_particles @num_particles.setter def num_particles(self, n: tuple[int, int]) -> None: """Sets the number of particles.""" self._invalidate() self._num_particles = n self._reset_register() # pylint: disable=too-many-return-statements def _check_configuration(self, raise_on_failure: bool = True) -> bool: """Check if the configuration of the HartreeFock class is valid. Args: raise_on_failure: Whether to raise on failure. Returns: True, if the configuration is valid and the circuit can be constructed. Otherwise returns False. Errors are only raised when raise_on_failure is set to True. Raises: ValueError: If the number of spatial orbitals is not specified or less than one. ValueError: If the number of particles is not specified or less than zero. ValueError: If the number of particles of any kind is less than zero. ValueError: If the number of spatial orbitals is smaller than the number of particles of any kind. ValueError: If the qubit mapper is not specified. NotImplementedError: If the specified qubit mapper is a :class:`~qiskit_nature.second_q.mappers.BravyiKitaevSuperFastMapper` instance. """ if self.num_spatial_orbitals is None: if raise_on_failure: raise ValueError("The number of spatial orbitals cannot be 'None'.") return False if self.num_spatial_orbitals <= 0: if raise_on_failure: raise ValueError( f"The number of spatial orbitals must be > 0 was {self.num_spatial_orbitals}." ) return False if self.num_particles is None: if raise_on_failure: raise ValueError("The number of particles cannot be 'None'.") return False if any(n < 0 for n in self.num_particles): if raise_on_failure: raise ValueError( f"The number of particles cannot be smaller than 0 was {self.num_particles}." ) return False if any(n > self.num_spatial_orbitals for n in self.num_particles): if raise_on_failure: raise ValueError( f"The number of spatial orbitals {self.num_spatial_orbitals}" f"must be greater than or equal to the number of particles of " f"any spin kind {self.num_particles}." ) return False if self.qubit_mapper is None: if raise_on_failure: raise ValueError("The qubit mapper cannot be `None`.") return False if isinstance(self.qubit_mapper, TaperedQubitMapper): # we also include the TaperedQubitMapper here, purely for the check done below mapper = self.qubit_mapper.mapper else: mapper = self.qubit_mapper if isinstance(mapper, BravyiKitaevSuperFastMapper): if raise_on_failure: raise NotImplementedError( "Unsupported mapper: ", type(mapper), ". See https://github.com/Qiskit/qiskit-nature/issues/537", ) return False return True def _reset_register(self): """Reset the register and recompute the mapped Hartree-Fock bitstring.""" self.qregs = [] self._bitstr = None if self._check_configuration(raise_on_failure=False): self._bitstr = hartree_fock_bitstring_mapped( self.num_spatial_orbitals, self.num_particles, self.qubit_mapper, ) self.qregs = [QuantumRegister(len(self._bitstr), name="q")] def _build(self) -> None: """ Construct the Hartree-Fock initial state given its parameters. Returns: QuantumCircuit: a quantum circuit preparing the Hartree-Fock initial state given a number of spatial orbitals, number of particles and a qubit mapper. """ if self._is_built: return super()._build() # Construct the circuit for bitstring. Since this is defined as an initial state # circuit its assumed that this is applied first to the qubits that are initialized to # the zero state. Hence we just need to account for all True entries and set those. if self._bitstr is not None: for i, bit in enumerate(self._bitstr): if bit: self.x(i)
def hartree_fock_bitstring_mapped( num_spatial_orbitals: int, num_particles: tuple[int, int], qubit_mapper: QubitMapper, ) -> list[bool]: # pylint: disable=unused-argument """Compute the bitstring representing the mapped Hartree-Fock state for the specified system. Args: num_spatial_orbitals: The number of spatial orbitals, has a min. value of 1. num_particles: The number of particles as a tuple (alpha, beta) containing the number of alpha- and beta-spin electrons, respectively. qubit_mapper: A QubitMapper. Returns: The bitstring representing the mapped state of the Hartree-Fock state as array of bools. """ # get the bitstring encoding the Hartree Fock state bitstr = hartree_fock_bitstring(num_spatial_orbitals, num_particles) # encode the bitstring as a `FermionicOp` bitstr_op = FermionicOp( {" ".join(f"+_{idx}" for idx, bit in enumerate(bitstr) if bit): 1.0}, num_spin_orbitals=2 * num_spatial_orbitals, ) # map the `FermionicOp` to a qubit operator qubit_op: SparsePauliOp if isinstance(qubit_mapper, TaperedQubitMapper): # To avoid checking commutativity, we call the two methods separately. qubit_op = qubit_mapper.map_clifford(bitstr_op) qubit_op = qubit_mapper.taper_clifford(qubit_op, check_commutes=False) else: qubit_op = qubit_mapper.map(bitstr_op) # We check the mapped operator `x` part of the paulis because we want to have particles # i.e. True, where the initial state introduced a creation (`+`) operator. bits = [] for bit in qubit_op.paulis.x[0]: bits.append(bit) return bits def hartree_fock_bitstring(num_spatial_orbitals: int, num_particles: tuple[int, int]) -> list[bool]: """Compute the bitstring representing the Hartree-Fock state for the specified system. Args: num_spatial_orbitals: The number of spatial orbitals, has a min. value of 1. num_particles: The number of particles as a tuple storing the number of alpha- and beta-spin electrons in the first and second number, respectively. Returns: The bitstring representing the state of the Hartree-Fock state as array of bools. Raises: ValueError: If the total number of particles is larger than the number of orbitals. """ # validate the input validate_min("num_spatial_orbitals", num_spatial_orbitals, 1) num_alpha, num_beta = num_particles if any(n > num_spatial_orbitals for n in num_particles): raise ValueError("# of particles must be less than or equal to # of orbitals.") half_orbitals = num_spatial_orbitals bitstr = np.zeros(2 * num_spatial_orbitals, bool) bitstr[:num_alpha] = True bitstr[half_orbitals : (half_orbitals + num_beta)] = True return bitstr.tolist()