Source code for qiskit_nature.second_q.problems.electronic_structure_problem

# This code is part of a Qiskit project.
# (C) Copyright IBM 2021, 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
# 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.

"""The Electronic Structure Problem class."""

from __future__ import annotations

from functools import partial
from typing import cast, Callable, List, Optional, Union, TYPE_CHECKING

import numpy as np

from qiskit_algorithms import EigensolverResult, MinimumEigensolverResult
from qiskit.quantum_info.analysis.z2_symmetries import Z2Symmetries

from qiskit_nature.exceptions import QiskitNatureError
from qiskit_nature.second_q.circuit.library.initial_states.hartree_fock import (
from qiskit_nature.second_q.mappers import QubitMapper
from qiskit_nature.second_q.hamiltonians import ElectronicEnergy
from import Interpretable

from .electronic_structure_result import ElectronicStructureResult
from .electronic_properties_container import ElectronicPropertiesContainer
from .eigenstate_result import EigenstateResult

from .base_problem import BaseProblem
from .electronic_basis import ElectronicBasis

    from qiskit_nature.second_q.formats.molecule_info import MoleculeInfo

[docs]class ElectronicStructureProblem(BaseProblem): r"""The Electronic Structure Problem. This class represents the problem of the electronic Schrödinger equation: .. math:: \hat{H_{el}}|\Psi\rangle = E_{el}|\Psi\rangle, where :math:`\hat{H_{el}}` is the :class:`qiskit_nature.second_q.hamiltonians.ElectronicEnergy` hamiltonian, :math:`\Psi` is the wave function of the system and :math:`E_{el}` is the eigenvalue. When passed to a :class:`qiskit_nature.second_q.algorithms.GroundStateSolver`, you will be solving for the ground-state energy, :math:`E_0`. This class has various attributes (see below) which allow you to add additional information about the problem which you are trying to solve, which can be used by various modules in the stack. For example, specifying the number of particles in the system :attr:`num_particles` is useful (and even required) for many components that interact with this problem instance to make your life easier (for example the :class:`qiskit_nature.second_q.transformers.ActiveSpaceTransformer`). In the fermionic case the default filter ensures that the number of particles is being preserved. .. note:: The default filter_criterion assumes a singlet spin configuration. This means, that the number of alpha-spin electrons is equal to the number of beta-spin electrons. If the :class:`` property is available, one can correctly filter a non-singlet spin configuration with a custom `filter_criterion` similar to the following: .. code-block:: python import numpy as np from qiskit_algorithms import NumPyMinimumEigensolver expected_spin = 2 expected_num_electrons = 6 def filter_criterion_custom(eigenstate, eigenvalue, aux_values): num_particles_aux = aux_values["ParticleNumber"][0] total_angular_momentum_aux = aux_values["AngularMomentum"][0] return ( np.isclose(total_angular_momentum_aux, expected_spin) and np.isclose(num_particles_aux, expected_num_electrons) ) solver = NumPyEigensolver() solver.filter_criterion = filter_criterion_custom The following attributes can be read and updated once the ``ElectronicStructureProblem`` object has been constructed. Attributes: properties (ElectronicStructureProblem): a container for additional observable operator factories. molecule (MoleculeInfo | None): a container for molecular system data. basis (ElectronicBasis | None): the electronic basis of all contained orbital coefficients. num_spatial_orbitals (int | tuple[int, int] | None): the number of spatial orbitals in the system. reference_energy (float | None): a reference energy for the ground state of the problem. orbital_energies (np.ndarray | None): the energy values of the alpha-spin orbitals. orbital_energies_b (np.ndarray | None): the energy values of the beta-spin orbitals. """ def __init__(self, hamiltonian: ElectronicEnergy) -> None: """ Args: hamiltonian: the Hamiltonian of this problem. """ super().__init__(hamiltonian) ElectronicPropertiesContainer = ElectronicPropertiesContainer() self.molecule: "MoleculeInfo" | None = None self.basis: ElectronicBasis | None = None self._num_particles: int | tuple[int, int] | None = None self.num_spatial_orbitals: int | None = hamiltonian.register_length self._orbital_occupations: np.ndarray | None = None self._orbital_occupations_b: np.ndarray | None = None self.reference_energy: float | None = None self.orbital_energies: np.ndarray | None = None self.orbital_energies_b: np.ndarray | None = None @property def hamiltonian(self) -> ElectronicEnergy: return cast(ElectronicEnergy, self._hamiltonian) @property def nuclear_repulsion_energy(self) -> float | None: """The nuclear repulsion energy. See :attr:`qiskit_nature.second_q.hamiltonians.ElectronicEnergy.nuclear_repulsion_energy` for more details. """ return self.hamiltonian.nuclear_repulsion_energy @property def num_particles(self) -> tuple[int, int] | None: """The number of particles in alpha- and beta-spin.""" if self._num_particles is None: return None if isinstance(self._num_particles, tuple): return self._num_particles num_beta = self._num_particles // 2 num_alpha = self._num_particles - num_beta return (num_alpha, num_beta) @num_particles.setter def num_particles(self, num_particles: int | tuple[int, int] | None) -> None: self._num_particles = num_particles @property def num_alpha(self) -> int | None: """The number of alpha-spin particles.""" if self.num_particles is None: return None return self.num_particles[0] @property def num_beta(self) -> int | None: """The number of beta-spin particles.""" if self.num_particles is None: return None return self.num_particles[1] @property def num_spin_orbitals(self) -> int | None: """The total number of spin orbitals.""" if self.num_spatial_orbitals is None: return None return 2 * self.num_spatial_orbitals @property def orbital_occupations(self) -> np.ndarray | None: """The occupations of the alpha-spin orbitals.""" if self._orbital_occupations is not None: return self._orbital_occupations if self.basis != ElectronicBasis.MO: return None num_orbs = self.num_spatial_orbitals if num_orbs is None: return None num_alpha = self.num_alpha if num_alpha is None: return None return np.asarray([1.0] * num_alpha + [0.0] * (num_orbs - num_alpha)) @orbital_occupations.setter def orbital_occupations(self, occ: np.ndarray | None) -> None: self._orbital_occupations = occ @property def orbital_occupations_b(self) -> np.ndarray | None: """The occupations of the beta-spin orbitals.""" if self._orbital_occupations_b is not None: return self._orbital_occupations_b if self.basis != ElectronicBasis.MO: return None num_orbs = self.num_spatial_orbitals if num_orbs is None: return None num_beta = self.num_beta if num_beta is None: return None return np.asarray([1.0] * num_beta + [0.0] * (num_orbs - num_beta)) @orbital_occupations_b.setter def orbital_occupations_b(self, occ: np.ndarray | None) -> None: self._orbital_occupations_b = occ
[docs] def interpret( self, raw_result: Union[EigenstateResult, EigensolverResult, MinimumEigensolverResult], ) -> ElectronicStructureResult: """Interprets an EigenstateResult in the context of this problem. Args: raw_result: an eigenstate result object. Returns: An electronic structure result. """ eigenstate_result = super().interpret(raw_result) result = ElectronicStructureResult() result.combine(eigenstate_result) if isinstance(self.hamiltonian, Interpretable): self.hamiltonian.interpret(result) for prop in if isinstance(prop, Interpretable): prop.interpret(result) result.computed_energies = np.asarray([e.real for e in eigenstate_result.eigenvalues]) if self.reference_energy is not None: result.hartree_fock_energy = self.reference_energy return result
[docs] def get_default_filter_criterion( self, ) -> Optional[Callable[[Union[List, np.ndarray], float, Optional[List[float]]], bool]]: """Returns a default filter criterion method to filter the eigenvalues computed by the eigensolver. For more information see also :meth:`~qiskit_algorithms.NumPyEigensolver.filter_criterion`. This particular default ensures that the total number of particles is conserved and that the angular momentum (if computed) evaluates to 0. """ # pylint: disable=unused-argument def filter_criterion(self, eigenstate, eigenvalue, aux_values): eval_num_particles = aux_values.get("ParticleNumber", None) if eval_num_particles is None: return True num_particles_close = np.isclose(eval_num_particles[0], self.num_alpha + self.num_beta) eval_angular_momentum = aux_values.get("AngularMomentum", None) if eval_angular_momentum is None: return num_particles_close angular_momentum_close = np.isclose(eval_angular_momentum[0], 0.0) return num_particles_close and angular_momentum_close return partial(filter_criterion, self)
def _symmetry_sector_locator( self, z2_symmetries: Z2Symmetries, mapper: QubitMapper, ) -> Optional[List[int]]: """Given the detected Z2Symmetries this determines the correct sector of the tapered operator that contains the ground state we need and returns that information. Args: z2_symmetries: the z2 symmetries object. mapper: the ``QubitMapper`` used for the operator conversion that symmetries are to be determined for. Raises: QiskitNatureError: if the :attr:`num_particles` attribute is ``None``. Returns: The sector of the tapered operators with the problem solution. """ if self.num_particles is None: raise QiskitNatureError( "Determining the correct symmetry sector for Z2 symmetry reduction requires the " "number of particles to be set on the problem instance. Please set " "ElectronicStructureProblem.num_particles or disable the use of Z2Symmetries to " "fix this." ) num_particles = self.num_particles if not isinstance(num_particles, tuple): num_particles = (self.num_alpha, self.num_beta) hf_bitstr = hartree_fock_bitstring_mapped( num_spatial_orbitals=self.num_spatial_orbitals, num_particles=num_particles, qubit_mapper=mapper, ) sector = ElectronicStructureProblem._pick_sector(z2_symmetries, hf_bitstr) return sector @staticmethod def _pick_sector(z2_symmetries: Z2Symmetries, hf_str: List[bool]) -> List[int]: # Finding all the symmetries using the find_Z2_symmetries: taper_coeff: List[int] = [] for sym in z2_symmetries.symmetries: coeff = -1 if np.logical_xor.reduce(np.logical_and(sym.z, hf_str)) else 1 taper_coeff.append(coeff) return taper_coeff