diff --git a/.pylintdict b/.pylintdict index 9b6fd46701..3f3679f8b9 100644 --- a/.pylintdict +++ b/.pylintdict @@ -166,6 +166,7 @@ hardcoded hartree hartrees hcore +hdf heidelberg heisenberg hermite @@ -192,6 +193,7 @@ intel intelvem interatomic internuclear +interpretable ints ising iso @@ -221,6 +223,7 @@ knowles kohn kwarg kwargs +kwds labelled lda len diff --git a/docs/apidocs/qiskit_nature.hdf5.rst b/docs/apidocs/qiskit_nature.hdf5.rst new file mode 100644 index 0000000000..42c139623c --- /dev/null +++ b/docs/apidocs/qiskit_nature.hdf5.rst @@ -0,0 +1,17 @@ +HDF5 +================== + +.. _qiskit_nature-hdf5: + +.. automodule:: qiskit_nature.hdf5 + + .. rubric:: Functions + + .. autosummary:: + load_from_hdf5 + save_to_hdf5 + + .. rubric:: Classes + + .. autosummary:: + HDF5Storable diff --git a/docs/tutorials/08_property_framework.ipynb b/docs/tutorials/08_property_framework.ipynb index 2ea9f6b3e9..3dd5eee2c0 100644 --- a/docs/tutorials/08_property_framework.ipynb +++ b/docs/tutorials/08_property_framework.ipynb @@ -708,11 +708,11 @@ "\t\t\tAlpha\n", "\t\t\t<(2, 2) matrix with 2 non-zero entries>\n", "\t\t\t[0, 0] = -1.2563390730032498\n", - "\t\t\t[1, 1] = -0.4718960072811426\n", + "\t\t\t[1, 1] = -0.47189600728114245\n", "\t\t\tBeta\n", "\t\t\t<(2, 2) matrix with 2 non-zero entries>\n", "\t\t\t[0, 0] = -1.2563390730032498\n", - "\t\t\t[1, 1] = -0.4718960072811426\n", + "\t\t\t[1, 1] = -0.47189600728114245\n", "\t\t(MO) 2-Body Terms:\n", "\t\t\tAlpha-Alpha\n", "\t\t\t<(2, 2, 2, 2) matrix with 8 non-zero entries>\n", @@ -785,15 +785,15 @@ "\t\t\t\tAlpha\n", "\t\t\t\t<(2, 2) matrix with 4 non-zero entries>\n", "\t\t\t\t[0, 0] = 0.6944743507776598\n", - "\t\t\t\t[0, 1] = -0.9278334704592321\n", + "\t\t\t\t[0, 1] = -0.927833470459232\n", "\t\t\t\t[1, 0] = -0.9278334704592321\n", - "\t\t\t\t[1, 1] = 0.6944743507776601\n", + "\t\t\t\t[1, 1] = 0.6944743507776604\n", "\t\t\t\tBeta\n", "\t\t\t\t<(2, 2) matrix with 4 non-zero entries>\n", "\t\t\t\t[0, 0] = 0.6944743507776598\n", - "\t\t\t\t[0, 1] = -0.9278334704592321\n", + "\t\t\t\t[0, 1] = -0.927833470459232\n", "\t\t\t\t[1, 0] = -0.9278334704592321\n", - "\t\t\t\t[1, 1] = 0.6944743507776601\n", + "\t\t\t\t[1, 1] = 0.6944743507776604\n", "\tAngularMomentum:\n", "\t\t4 SOs\n", "\tMagnetization:\n", @@ -1047,6 +1047,8 @@ "from itertools import product\n", "from typing import List\n", "\n", + "import h5py\n", + "\n", "from qiskit_nature.drivers import QMolecule\n", "from qiskit_nature.operators.second_quantization import FermionicOp\n", "from qiskit_nature.properties.second_quantization.electronic.bases import ElectronicBasis\n", @@ -1078,6 +1080,16 @@ " string += [f\"\\t{self._num_molecular_orbitals} MOs\"]\n", " return \"\\n\".join(string)\n", "\n", + " def to_hdf5(self, parent: h5py.Group) -> None:\n", + " super().to_hdf5(parent)\n", + " group = parent.require_group(self.name)\n", + "\n", + " group.attrs[\"num_molecular_orbitals\"] = self._num_molecular_orbitals\n", + "\n", + " @classmethod\n", + " def from_hdf5(cls, h5py_group: h5py.Group) -> \"ElectronicDensity\":\n", + " return ElectronicDensity(h5py_group.attrs[\"num_molecular_orbitals\"])\n", + "\n", " @classmethod\n", " def from_legacy_driver_result(cls, result) -> \"ElectronicDensity\":\n", " cls._validate_input_type(result, QMolecule)\n", @@ -1217,7 +1229,7 @@ { "data": { "text/html": [ - "

Version Information

Qiskit SoftwareVersion
qiskit-terra0.20.0.dev0+7af16a3
qiskit-aer0.10.2
qiskit-ignis0.7.0
qiskit-nature0.4.0
qiskit-finance0.4.0
qiskit-optimization0.4.0
qiskit-machine-learning0.3.0
System information
Python version3.8.12
Python compilerClang 10.0.0
Python builddefault, Oct 12 2021 06:23:56
OSDarwin
CPUs2
Memory (Gb)12.0
Fri Feb 04 13:55:22 2022 EST
" + "

Version Information

Qiskit SoftwareVersion
qiskit-terra0.20.0.dev0+9a743fb
qiskit-aer0.11.0
qiskit-ignis0.7.0
qiskit-ibmq-provider0.19.0.dev0+8455b01
qiskit-nature0.4.0
System information
Python version3.9.9
Python compilerGCC 11.2.1 20210728 (Red Hat 11.2.1-1)
Python buildmain, Nov 19 2021 00:00:00
OSLinux
CPUs4
Memory (Gb)14.842281341552734
Mon Jan 24 14:34:24 2022 CET
" ], "text/plain": [ "" @@ -1245,6 +1257,14 @@ "%qiskit_version_table\n", "%qiskit_copyright" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "889fbac9", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -1263,7 +1283,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.12" + "version": "3.9.9" } }, "nbformat": 4, diff --git a/qiskit_nature/__init__.py b/qiskit_nature/__init__.py index f10ba2dd6e..7b3c0c0b38 100644 --- a/qiskit_nature/__init__.py +++ b/qiskit_nature/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 2022. # # 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 @@ -39,6 +39,7 @@ circuit converters drivers + hdf5 mappers operators problems diff --git a/qiskit_nature/drivers/molecule.py b/qiskit_nature/drivers/molecule.py index 3e264d21cd..a2f8d2387a 100644 --- a/qiskit_nature/drivers/molecule.py +++ b/qiskit_nature/drivers/molecule.py @@ -17,6 +17,7 @@ from typing import Callable, Tuple, List, Optional, cast import copy +import h5py import numpy as np import scipy.linalg @@ -36,6 +37,8 @@ class Molecule: directly if its needed. """ + VERSION = 1 + def __init__( self, geometry: List[Tuple[str, List[float]]], @@ -76,6 +79,71 @@ def __init__( self._perturbations = None # type: Optional[List[float]] + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + group = parent.require_group(self.__class__.__name__) + group.attrs["__class__"] = self.__class__.__name__ + group.attrs["__module__"] = self.__class__.__module__ + group.attrs["__version__"] = self.VERSION + + geometry_group = group.create_group("geometry", track_order=True) + for idx, geom in enumerate(self._geometry): + symbol, coords = geom + geometry_group.create_dataset(str(idx), data=coords) + geometry_group[str(idx)].attrs["symbol"] = symbol + + group.attrs["units"] = self.units.value + group.attrs["multiplicity"] = self.multiplicity + group.attrs["charge"] = self.charge + + if self._masses: + group.create_dataset("masses", data=self._masses) + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> Molecule: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + geometry = [] + for atom in h5py_group["geometry"].values(): + geometry.append((atom.attrs["symbol"], list(atom[...]))) + + units: UnitsType + for unit in UnitsType: + if unit.value == h5py_group.attrs["units"]: + units = unit + break + else: + units = UnitsType.ANGSTROM + + multiplicity = h5py_group.attrs["multiplicity"] + charge = h5py_group.attrs["charge"] + + masses = None + if "masses" in h5py_group.keys(): + masses = list(h5py_group["masses"]) + + return Molecule( + geometry, + multiplicity=multiplicity, + charge=charge, + units=units, + masses=masses, + ) + def __str__(self) -> str: string = ["Molecule:"] string += [f"\tMultiplicity: {self._multiplicity}"] diff --git a/qiskit_nature/drivers/second_quantization/electronic_structure_driver.py b/qiskit_nature/drivers/second_quantization/electronic_structure_driver.py index 68d32b1486..7f7cd3d330 100644 --- a/qiskit_nature/drivers/second_quantization/electronic_structure_driver.py +++ b/qiskit_nature/drivers/second_quantization/electronic_structure_driver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -17,7 +17,7 @@ from abc import abstractmethod from enum import Enum -from qiskit_nature.properties.second_quantization.electronic.types import GroupedElectronicProperty +from qiskit_nature.properties.second_quantization.electronic import ElectronicStructureDriverResult from .base_driver import BaseDriver @@ -43,6 +43,6 @@ class ElectronicStructureDriver(BaseDriver): """ @abstractmethod - def run(self) -> GroupedElectronicProperty: - """Returns a GroupedElectronicProperty output as produced by the driver.""" + def run(self) -> ElectronicStructureDriverResult: + """Returns a ElectronicStructureDriverResult output as produced by the driver.""" pass diff --git a/qiskit_nature/drivers/second_quantization/electronic_structure_molecule_driver.py b/qiskit_nature/drivers/second_quantization/electronic_structure_molecule_driver.py index 0c64d07b1e..841258fe1b 100644 --- a/qiskit_nature/drivers/second_quantization/electronic_structure_molecule_driver.py +++ b/qiskit_nature/drivers/second_quantization/electronic_structure_molecule_driver.py @@ -20,7 +20,7 @@ from enum import Enum from qiskit.exceptions import MissingOptionalLibraryError -from qiskit_nature.properties.second_quantization.electronic.types import GroupedElectronicProperty +from qiskit_nature.properties.second_quantization.electronic import ElectronicStructureDriverResult from .electronic_structure_driver import ElectronicStructureDriver, MethodType from ..molecule import Molecule from ...exceptions import UnsupportMethodError @@ -169,7 +169,7 @@ def driver_kwargs(self, value: Optional[Dict[str, Any]]) -> None: """set driver kwargs""" self._driver_kwargs = value - def run(self) -> GroupedElectronicProperty: + def run(self) -> ElectronicStructureDriverResult: driver_class = ElectronicStructureDriverType.driver_class_from_type( self.driver_type, self.method ) diff --git a/qiskit_nature/drivers/second_quantization/pyquanted/pyquantedriver.py b/qiskit_nature/drivers/second_quantization/pyquanted/pyquantedriver.py index 6c81876702..18fbfcae36 100644 --- a/qiskit_nature/drivers/second_quantization/pyquanted/pyquantedriver.py +++ b/qiskit_nature/drivers/second_quantization/pyquanted/pyquantedriver.py @@ -24,7 +24,6 @@ from qiskit_nature import QiskitNatureError from qiskit_nature.constants import BOHR, PERIODIC_TABLE -from qiskit_nature.settings import settings from qiskit_nature.properties.second_quantization.driver_metadata import DriverMetadata from qiskit_nature.properties.second_quantization.electronic import ( ElectronicStructureDriverResult, @@ -406,9 +405,11 @@ def _construct_driver_result(self) -> ElectronicStructureDriverResult: self._populate_driver_result_particle_number(driver_result) self._populate_driver_result_electronic_energy(driver_result) - if not settings.dict_aux_operators: - driver_result.add_property(AngularMomentum(self._nmo * 2)) - driver_result.add_property(Magnetization(self._nmo * 2)) + # TODO: once https://github.com/Qiskit/qiskit-nature/issues/312 is fixed we can stop adding + # these properties by default. + # if not settings.dict_aux_operators: + driver_result.add_property(AngularMomentum(self._nmo * 2)) + driver_result.add_property(Magnetization(self._nmo * 2)) return driver_result diff --git a/qiskit_nature/drivers/second_quantization/pyscfd/pyscfdriver.py b/qiskit_nature/drivers/second_quantization/pyscfd/pyscfdriver.py index 7b2295a028..ff6a3c37ef 100644 --- a/qiskit_nature/drivers/second_quantization/pyscfd/pyscfdriver.py +++ b/qiskit_nature/drivers/second_quantization/pyscfd/pyscfdriver.py @@ -22,6 +22,7 @@ import numpy as np from qiskit.utils.validation import validate_min + from qiskit_nature.properties.second_quantization.driver_metadata import DriverMetadata from qiskit_nature.properties.second_quantization.electronic import ( ElectronicStructureDriverResult, @@ -516,9 +517,9 @@ def _construct_driver_result(self) -> ElectronicStructureDriverResult: self._populate_driver_result_electronic_energy(driver_result) self._populate_driver_result_electronic_dipole_moment(driver_result) - # TODO: once https://github.com/Qiskit/qiskit-terra/issues/6772 is resolved, we no longer - # _have_ to add these properties. However, until then the interpret method relies on indices - # of the aux_operators which are incorrect if these properties are not added. + # TODO: once https://github.com/Qiskit/qiskit-nature/issues/312 is fixed we can stop adding + # these properties by default. + # if not settings.dict_aux_operators: driver_result.add_property(AngularMomentum(self._mol.nao * 2)) driver_result.add_property(Magnetization(self._mol.nao * 2)) diff --git a/qiskit_nature/drivers/second_quantization/vibrational_structure_driver.py b/qiskit_nature/drivers/second_quantization/vibrational_structure_driver.py index 592ac68ba3..e4460fe6bf 100644 --- a/qiskit_nature/drivers/second_quantization/vibrational_structure_driver.py +++ b/qiskit_nature/drivers/second_quantization/vibrational_structure_driver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 2022. # # 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 @@ -16,8 +16,8 @@ from abc import abstractmethod -from qiskit_nature.properties.second_quantization.vibrational.types import ( - GroupedVibrationalProperty, +from qiskit_nature.properties.second_quantization.vibrational import ( + VibrationalStructureDriverResult, ) from .base_driver import BaseDriver @@ -28,6 +28,6 @@ class VibrationalStructureDriver(BaseDriver): """ @abstractmethod - def run(self) -> GroupedVibrationalProperty: - """Returns a GroupedVibrationalProperty output as produced by the driver.""" + def run(self) -> VibrationalStructureDriverResult: + """Returns a VibrationalStructureDriverResult output as produced by the driver.""" pass diff --git a/qiskit_nature/drivers/second_quantization/vibrational_structure_molecule_driver.py b/qiskit_nature/drivers/second_quantization/vibrational_structure_molecule_driver.py index 420b3962f5..577b136c42 100644 --- a/qiskit_nature/drivers/second_quantization/vibrational_structure_molecule_driver.py +++ b/qiskit_nature/drivers/second_quantization/vibrational_structure_molecule_driver.py @@ -20,8 +20,8 @@ from enum import Enum from qiskit.exceptions import MissingOptionalLibraryError -from qiskit_nature.properties.second_quantization.vibrational.types import ( - GroupedVibrationalProperty, +from qiskit_nature.properties.second_quantization.vibrational import ( + VibrationalStructureDriverResult, ) from .vibrational_structure_driver import VibrationalStructureDriver from ..molecule import Molecule @@ -146,7 +146,7 @@ def driver_kwargs(self, value: Optional[Dict[str, Any]]) -> None: """set driver kwargs""" self._driver_kwargs = value - def run(self) -> GroupedVibrationalProperty: + def run(self) -> VibrationalStructureDriverResult: driver_class = VibrationalStructureDriverType.driver_class_from_type(self.driver_type) driver = driver_class.from_molecule( # type: ignore self.molecule, self.basis, self.driver_kwargs diff --git a/qiskit_nature/hdf5.py b/qiskit_nature/hdf5.py new file mode 100644 index 0000000000..92443951b2 --- /dev/null +++ b/qiskit_nature/hdf5.py @@ -0,0 +1,222 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Qiskit Nature HDF5 Integration.""" + +from __future__ import annotations + +import importlib +import logging +import sys +from pathlib import Path +from typing import Generator + +import h5py + +from qiskit_nature.exceptions import QiskitNatureError + +if sys.version_info >= (3, 8): + # pylint: disable=no-name-in-module + from typing import Protocol, runtime_checkable +else: + from typing_extensions import Protocol, runtime_checkable + + +LOGGER = logging.getLogger(__name__) + + +@runtime_checkable +class HDF5Storable(Protocol): + """A Protocol implemented by those classes which support conversion methods for HDF5.""" + + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + Qiskit Nature uses the convention of storing the `__module__` and `__class__` information as + attributes of an HDF5 group. Furthermore, a `__version__` should be stored in order to allow + version handling at runtime. + + The `__version__` attribute should be object-dependent and is not coupled to the version of + Qiskit Nature itself. Instead, each `HDF5Storable` has its own `VERSION` class attribute via + which the `from_hdf5` implementation should deal with changes to the serialization. + Backwards compatibility should be enforced for the duration of a classes lifetime (i.e. + until its potential deprecation and removal). + + Args: + parent: the parent HDF5 group. + """ + ... + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> HDF5Storable: + """Constructs a new instance from the data stored in the provided HDF5 group. + + This method is expected to handle backwards compatibility. This means that even if the + classes `VERSION` value has been incremented, this method should be able to construct an + instance from an older `__version__` encountered in the provided HDF5 group. + In scenarios where full backwards compatibility is impossible due to (for example) an entire + restructuring of the class, the function should raise a + :class:`~qiskit_nature.QiskitNatureError`. + + Furthermore, if this method encounters a `__version__` number greater than the classes + `VERSION` (i.e. an HDF5 group generated from a newer Qiskit Nature) it should _not_ attempt + to construct the class and instead raise a :class:`~qiskit_nature.QiskitNatureError`. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + ... + + +def save_to_hdf5(obj: HDF5Storable, filename: str, *, replace: bool = False) -> None: + """A utility to method to store an object to an HDF5 file. + + Below is an example of how you can store the result produced by any driver in an HDF5 file: + + .. code-block:: python + + property = driver.run() + save_to_hdf5(property, "my_driver_result.hdf5") + + Besides the ability to store :class:`~qiskit_nature.properties.GroupedProperty` objects (like + the driver result in the example above) you can also store single objects like so: + + .. code-block:: python + + integrals = OneBodyElectronicIntegrals(ElectronicBasis.AO, (np.random((4, 4)), None)) + electronic_energy = ElectronicEnergy([integrals], reference_energy=-1.0) + save_to_hdf5(electronic_energy, "my_electronic_energy.hdf5") + + Args: + obj: the `HDF5Storable` object to store in the file. + filename: the path to the HDF5 file. + replace: whether to forcefully replace an existing file. + + Raises: + FileExistsError: if the file at the given path already exists and forcefully overwriting is + not enabled. + """ + if not isinstance(obj, HDF5Storable): + LOGGER.error("%s is not an instance of %s", obj, HDF5Storable) + return + + if Path(filename).exists() and not replace: + raise FileExistsError( + f"The file at {filename} already exists! Specify `replace=True` if you want to " + "overwrite it!" + ) + + with h5py.File(filename, "w") as file: + obj.to_hdf5(file) + + +def load_from_hdf5( + filename: str, *, skip_unreadable_data: bool = False +) -> Generator[HDF5Storable, None, None]: + """Loads Qiskit Nature objects from an HDF5 file. + + .. code-block:: python + + my_driver_result = load_from_hdf5("my_driver_result.hdf5") + + Args: + filename: the path to the HDF5 file. + skip_unreadable_data: if set to True, unreadable data (which can be any of the errors + documented below) will be skipped instead of raising those errors. + + Yields: + The objects constructed from the HDF5 groups encountered in the h5py_group. + + Raises: + QiskitNatureError: if an object without a `__class__` attribute is encountered. + QiskitNatureError: if a non-native object (`__module__` outside of `qiskit_nature`) is + encountered. + QiskitNatureError: if an import failure occurs because `__class__` cannot be found inside of + `__module__` + QiskitNatureError: if `__class__` does not implement the + :class:`~qiskit_nature.hdf5.HDF5Storable` protocol + """ + with h5py.File(filename, "r") as file: + yield from _import_and_build_from_hdf5(file, skip_unreadable_data=skip_unreadable_data) + + +def _import_and_build_from_hdf5( + h5py_group: h5py.Group, *, skip_unreadable_data: bool = False +) -> Generator[HDF5Storable, None, None]: + """Imports and builds a Qiskit Nature object from an HDF5 group. + + Qiskit Nature uses the convention of storing the `__module__` and `__class__` information as + attributes of an HDF5 group. From these, this method will import the required class at runtime + and use its `form_hdf5` method to construct an instance of the encountered class. + + Args: + h5py_group: the HDF5 group from which to import and build Qiskit Nature objects. + skip_unreadable_data: if set to True, unreadable data (which can be any of the errors + documented below) will be skipped instead of raising those errors. + + Yields: + The objects constructed from the HDF5 groups encountered in the h5py_group. + + Raises: + QiskitNatureError: if an object without a `__class__` attribute is encountered. + QiskitNatureError: if a non-native object (`__module__` outside of `qiskit_nature`) is + encountered. + QiskitNatureError: if an import failure occurs because `__class__` cannot be found inside of + `__module__` + QiskitNatureError: if `__class__` does not implement the + :class:`~qiskit_nature.hdf5.HDF5Storable` protocol + """ + for group in h5py_group.values(): + module_path = group.attrs.get("__module__", "") + if not module_path: + # due to how HDF5 groups are being iterated we cannot raise an error here + continue + + class_name = group.attrs.get("__class__", "") + + if not class_name: + msg = "faulty object without a '__class__' attribute" + if skip_unreadable_data: + LOGGER.warning("Skipping %s", msg) + continue + raise QiskitNatureError("Encountered " + msg) + + if not module_path.startswith("qiskit_nature"): + msg = "non-native object" + if skip_unreadable_data: + LOGGER.warning("Skipping %s", msg) + continue + raise QiskitNatureError("Encountered " + msg) + + loaded_module = importlib.import_module(module_path) + loaded_class = getattr(loaded_module, class_name, None) + + if loaded_class is None: + msg = f"import failure of {class_name} from {module_path}" + if skip_unreadable_data: + LOGGER.warning("Skipping after %s", msg) + continue + raise QiskitNatureError("Encountered " + msg) + + if not issubclass(loaded_class, HDF5Storable): + msg = f"object of type {loaded_class} which is not an HDF5Storable" + if skip_unreadable_data: + LOGGER.warning("Skipping %s", msg) + continue + raise QiskitNatureError("Encountered " + msg) + + constructor = getattr(loaded_class, "from_hdf5") + instance = constructor(group) + yield instance diff --git a/qiskit_nature/properties/__init__.py b/qiskit_nature/properties/__init__.py index f633640738..ff36e01a53 100644 --- a/qiskit_nature/properties/__init__.py +++ b/qiskit_nature/properties/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -24,7 +24,6 @@ Property GroupedProperty - PseudoProperty .. autosummary:: :toctree: @@ -33,10 +32,9 @@ """ from .grouped_property import GroupedProperty -from .property import Property, PseudoProperty +from .property import Property __all__ = [ "Property", "GroupedProperty", - "PseudoProperty", ] diff --git a/qiskit_nature/properties/grouped_property.py b/qiskit_nature/properties/grouped_property.py index 14fb93e907..5644c733bd 100644 --- a/qiskit_nature/properties/grouped_property.py +++ b/qiskit_nature/properties/grouped_property.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,11 +12,16 @@ """A group of multiple properties.""" +from __future__ import annotations + from collections.abc import Iterable -from typing import Dict, Generator, Generic, Optional, Type, TypeVar, Union +from typing import Generator, Generic, Optional, Type, TypeVar, Union + +import h5py +from qiskit_nature.hdf5 import _import_and_build_from_hdf5 from qiskit_nature.results import EigenstateResult -from .property import Property, PseudoProperty +from .property import Interpretable, Property # pylint: disable=invalid-name T = TypeVar("T", bound=Property, covariant=True) @@ -43,7 +48,7 @@ def __init__(self, name: str) -> None: name: the name of the property group. """ super().__init__(name) - self._properties: Dict[str, T] = {} + self._properties: dict[str, T] = {} def __str__(self) -> str: string = [super().__str__() + ":"] @@ -59,7 +64,11 @@ def add_property(self, prop: Optional[T]) -> None: prop: the property to be added. """ if prop is not None: - self._properties[prop.name] = prop + try: + name = prop.name + except AttributeError: + name = prop.__class__.__name__ + self._properties[name] = prop def get_property(self, prop: Union[str, Type[Property]]) -> Optional[T]: """Gets a property from the group. @@ -84,14 +93,9 @@ def __iter__(self) -> Generator[T, T, None]: def _generator(self) -> Generator[T, T, None]: """A generator-iterator method [1] iterating over all internal properties. - :class:`~qiskit_nature.properties.property.PseudoProperty` objects are automatically - excluded. - [1]: https://docs.python.org/3/reference/expressions.html#generator-iterator-methods """ for prop in self._properties.values(): - if isinstance(prop, PseudoProperty): - continue new_property = yield prop if new_property is not None: self.add_property(new_property) @@ -103,4 +107,45 @@ def interpret(self, result: EigenstateResult) -> None: result: the result to add meaning to. """ for prop in self._properties.values(): - prop.interpret(result) + if isinstance(prop, Interpretable): + prop.interpret(result) + + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + This method also iterates all properties contained in this ``GroupProperty`` instance. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + for prop in self._properties.values(): + prop.to_hdf5(group) + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> GroupedProperty: + """Constructs a new instance from the data stored in the provided HDF5 group. + + More specifically this method will iterate all groups found within `h5py_group` and + constructs the corresponding objects from these groups. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + class_name = h5py_group.attrs.get("__class__", "") + + ret: GroupedProperty = GroupedProperty(class_name) + + for prop in _import_and_build_from_hdf5(h5py_group): + ret.add_property(prop) + + return ret diff --git a/qiskit_nature/properties/property.py b/qiskit_nature/properties/property.py index 0bc0246af8..a82f0319dc 100644 --- a/qiskit_nature/properties/property.py +++ b/qiskit_nature/properties/property.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,11 +12,25 @@ """The Property base class.""" -from abc import ABC, abstractmethod +from __future__ import annotations + +from abc import ABC import logging +import sys + +import h5py +from qiskit_nature.deprecation import warn_deprecated, DeprecatedType from qiskit_nature.results import EigenstateResult +if sys.version_info >= (3, 8): + # pylint: disable=no-name-in-module + from typing import runtime_checkable, Protocol +else: + from typing_extensions import runtime_checkable, Protocol + +LOGGER = logging.getLogger(__name__) + class Property(ABC): """The Property base class. @@ -28,6 +42,11 @@ class Property(ABC): operator out of a given set of raw data). """ + VERSION = 1 + """Each Property has its own version number. Although initially only defined on the base class, + a subclass can increment its version number in order to handle changes during `load` and `save` + operations.""" + def __init__(self, name: str) -> None: """ Args: @@ -55,22 +74,56 @@ def log(self) -> None: return logger.info(self.__str__()) - @abstractmethod - def interpret(self, result: EigenstateResult) -> None: - """Interprets an :class:`~qiskit_nature.results.EigenstateResult` in this property's context. + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. Args: - result: the result to add meaning to. + parent: the parent HDF5 group. """ - raise NotImplementedError() + group = parent.require_group(self.name) + group.attrs["__class__"] = self.__class__.__name__ + group.attrs["__module__"] = self.__class__.__module__ + group.attrs["__version__"] = self.VERSION class PseudoProperty(Property, ABC): - """The PseudoProperty type. + """**DEPRECATED**: The PseudoProperty type. A pseudo-property is a type derived by auxiliary property-related meta data. """ + def __init__(self): + super().__init__(self.__class__.__name__) + warn_deprecated( + "0.4.0", + DeprecatedType.CLASS, + "PseudoProperty", + DeprecatedType.CLASS, + "Interpretable", + additional_msg=( + "The PseudoProperty class is deprecated. Instead, of requiring an `interpret()` " + "method on the Property base-class, this is now handled via the `Interpretable` " + "protocol." + ), + ) + def interpret(self, result: EigenstateResult) -> None: """A PseudoProperty cannot interpret anything.""" pass + + +@runtime_checkable +class Interpretable(Protocol): + """A protocol determining whether or not an object is interpretable. + + An object is considered interpretable if it implements an `interpret` method. + """ + + def interpret(self, result: EigenstateResult) -> None: + """Interprets an :class:`~qiskit_nature.results.EigenstateResult` in the object's context. + + Args: + result: the result to add meaning to. + """ diff --git a/qiskit_nature/properties/second_quantization/__init__.py b/qiskit_nature/properties/second_quantization/__init__.py index 59f218077a..d57260a7d9 100644 --- a/qiskit_nature/properties/second_quantization/__init__.py +++ b/qiskit_nature/properties/second_quantization/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 diff --git a/qiskit_nature/properties/second_quantization/driver_metadata.py b/qiskit_nature/properties/second_quantization/driver_metadata.py index c5bc8e4ac3..25cdb08203 100644 --- a/qiskit_nature/properties/second_quantization/driver_metadata.py +++ b/qiskit_nature/properties/second_quantization/driver_metadata.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,12 +12,20 @@ """The DriverMetadata class.""" -from ..property import PseudoProperty +from __future__ import annotations +import h5py -class DriverMetadata(PseudoProperty): +from ..property import Property + + +class DriverMetadata(Property): """A meta-data storage container for driver information.""" + _HDF5_ATTR_PROGRAM = "program" + _HDF5_ATTR_VERSION = "version" + _HDF5_ATTR_CONFIG = "config" + def __init__(self, program: str, version: str, config: str) -> None: """ Args: @@ -37,3 +45,36 @@ def __str__(self) -> str: string += ["\tConfig:"] string += ["\t\t" + s for s in self.config.split("\n")] return "\n".join(string) + + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + group.attrs[DriverMetadata._HDF5_ATTR_PROGRAM] = self.program + group.attrs[DriverMetadata._HDF5_ATTR_VERSION] = self.version + group.attrs[DriverMetadata._HDF5_ATTR_CONFIG] = self.config + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> DriverMetadata: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + return DriverMetadata( + h5py_group.attrs[DriverMetadata._HDF5_ATTR_PROGRAM], + h5py_group.attrs[DriverMetadata._HDF5_ATTR_VERSION], + h5py_group.attrs[DriverMetadata._HDF5_ATTR_CONFIG], + ) diff --git a/qiskit_nature/properties/second_quantization/electronic/__init__.py b/qiskit_nature/properties/second_quantization/electronic/__init__.py index 2aa9647e28..639a77289f 100644 --- a/qiskit_nature/properties/second_quantization/electronic/__init__.py +++ b/qiskit_nature/properties/second_quantization/electronic/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -16,6 +16,13 @@ .. currentmodule:: qiskit_nature.properties.second_quantization.electronic This module provides commonly evaluated properties for *electronic* problems. +It also includes the default return object for the electronic structure drivers: + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + ElectronicStructureDriverResult The main :class:`~qiskit_nature.properties.Property` of this module is the diff --git a/qiskit_nature/properties/second_quantization/electronic/angular_momentum.py b/qiskit_nature/properties/second_quantization/electronic/angular_momentum.py index 19574f2702..0d5b80d0d0 100644 --- a/qiskit_nature/properties/second_quantization/electronic/angular_momentum.py +++ b/qiskit_nature/properties/second_quantization/electronic/angular_momentum.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,11 +12,14 @@ """The AngularMomentum property.""" +from __future__ import annotations + import logging -from typing import cast, List, Optional, Tuple +from typing import cast, Optional import itertools +import h5py import numpy as np from qiskit_nature import ListOrDictType, settings @@ -64,6 +67,16 @@ def __init__( self._absolute_tolerance = absolute_tolerance self._relative_tolerance = relative_tolerance + @property + def num_spin_orbitals(self) -> int: + """Returns the number of spin orbitals.""" + return self._num_spin_orbitals + + @num_spin_orbitals.setter + def num_spin_orbitals(self, num_spin_orbitals: int) -> None: + """Sets the number of spin orbitals.""" + self._num_spin_orbitals = num_spin_orbitals + @property def spin(self) -> Optional[float]: """Returns the expected spin.""" @@ -74,6 +87,26 @@ def spin(self, spin: Optional[float]) -> None: """Sets the expected spin.""" self._spin = spin + @property + def absolute_tolerance(self) -> float: + """Returns the absolute tolerance.""" + return self._absolute_tolerance + + @absolute_tolerance.setter + def absolute_tolerance(self, absolute_tolerance: float) -> None: + """Sets the absolute tolerance.""" + self._absolute_tolerance = absolute_tolerance + + @property + def relative_tolerance(self) -> float: + """Returns the relative tolerance.""" + return self._relative_tolerance + + @relative_tolerance.setter + def relative_tolerance(self, relative_tolerance: float) -> None: + """Sets the relative tolerance.""" + self._relative_tolerance = relative_tolerance + def __str__(self) -> str: string = [super().__str__() + ":"] string += [f"\t{self._num_spin_orbitals} SOs"] @@ -81,8 +114,44 @@ def __str__(self) -> str: string += [f"\tExpected spin: {self.spin}"] return "\n".join(string) + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + group.attrs["num_spin_orbitals"] = self._num_spin_orbitals + if self._spin: + group.attrs["spin"] = self._spin + group.attrs["absolute_tolerance"] = self._absolute_tolerance + group.attrs["relative_tolerance"] = self._relative_tolerance + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> AngularMomentum: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + return AngularMomentum( + h5py_group.attrs["num_spin_orbitals"], + h5py_group.attrs.get("spin", None), + h5py_group.attrs["absolute_tolerance"], + h5py_group.attrs["relative_tolerance"], + ) + @classmethod - def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "AngularMomentum": + def from_legacy_driver_result(cls, result: LegacyDriverResult) -> AngularMomentum: """Construct an AngularMomentum instance from a :class:`~qiskit_nature.drivers.QMolecule`. Args: @@ -167,19 +236,19 @@ def interpret(self, result: EigenstateResult) -> None: result.total_angular_momentum.append(None) -def _calc_s_x_squared_ints(num_modes: int) -> Tuple[np.ndarray, np.ndarray]: +def _calc_s_x_squared_ints(num_modes: int) -> tuple[np.ndarray, np.ndarray]: return _calc_squared_ints(num_modes, _modify_s_x_squared_ints_neq, _modify_s_x_squared_ints_eq) -def _calc_s_y_squared_ints(num_modes: int) -> Tuple[np.ndarray, np.ndarray]: +def _calc_s_y_squared_ints(num_modes: int) -> tuple[np.ndarray, np.ndarray]: return _calc_squared_ints(num_modes, _modify_s_y_squared_ints_neq, _modify_s_y_squared_ints_eq) -def _calc_s_z_squared_ints(num_modes: int) -> Tuple[np.ndarray, np.ndarray]: +def _calc_s_z_squared_ints(num_modes: int) -> tuple[np.ndarray, np.ndarray]: return _calc_squared_ints(num_modes, _modify_s_z_squared_ints_neq, _modify_s_z_squared_ints_eq) -def _calc_squared_ints(num_modes: int, func_neq, func_eq) -> Tuple[np.ndarray, np.ndarray]: +def _calc_squared_ints(num_modes: int, func_neq, func_eq) -> tuple[np.ndarray, np.ndarray]: # calculates 1- and 2-body integrals for a given angular momentum axis (x or y or z, # specified by func_neq and func_eq) num_modes_2 = num_modes // 2 @@ -287,7 +356,7 @@ def _modify_s_z_squared_ints_eq(h_2: np.ndarray, p_ind: int, num_modes_2: int) - def _add_values_to_s_squared_ints( - h_2: np.ndarray, indices: List[Tuple[int, int, int, int]], values: List[int] + h_2: np.ndarray, indices: list[tuple[int, int, int, int]], values: list[int] ) -> np.ndarray: for index, value in zip(indices, values): h_2[index] += value diff --git a/qiskit_nature/properties/second_quantization/electronic/bases/electronic_basis_transform.py b/qiskit_nature/properties/second_quantization/electronic/bases/electronic_basis_transform.py index ad6d8bf6b0..a851082d98 100644 --- a/qiskit_nature/properties/second_quantization/electronic/bases/electronic_basis_transform.py +++ b/qiskit_nature/properties/second_quantization/electronic/bases/electronic_basis_transform.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,15 +12,19 @@ """The ElectronicBasisTransform provides a container of bases transformation data.""" -from typing import List, Optional +from __future__ import annotations + +from typing import Optional + +import h5py import numpy as np -from ....property import PseudoProperty from .electronic_basis import ElectronicBasis +from ....property import Property -class ElectronicBasisTransform(PseudoProperty): +class ElectronicBasisTransform(Property): """This class contains the coefficients required to map from one basis into another.""" def __init__( @@ -56,7 +60,43 @@ def __str__(self) -> str: string += self._render_coefficients(self.coeff_beta) return "\n".join(string) + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + group.attrs["initial_basis"] = self.initial_basis.name + group.attrs["final_basis"] = self.final_basis.name + + group.create_dataset("Alpha coefficients", data=self.coeff_alpha) + group.create_dataset("Beta coefficients", data=self.coeff_beta) + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> ElectronicBasisTransform: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + return ElectronicBasisTransform( + getattr(ElectronicBasis, h5py_group.attrs["initial_basis"]), + getattr(ElectronicBasis, h5py_group.attrs["final_basis"]), + h5py_group["Alpha coefficients"][...], + h5py_group["Beta coefficients"][...], + ) + @staticmethod - def _render_coefficients(coeffs) -> List[str]: + def _render_coefficients(coeffs) -> list[str]: nonzero = coeffs.nonzero() return [f"\t{indices} = {value}" for value, *indices in zip(coeffs[nonzero], *nonzero)] diff --git a/qiskit_nature/properties/second_quantization/electronic/dipole_moment.py b/qiskit_nature/properties/second_quantization/electronic/dipole_moment.py index cde487bebf..d6dac89896 100644 --- a/qiskit_nature/properties/second_quantization/electronic/dipole_moment.py +++ b/qiskit_nature/properties/second_quantization/electronic/dipole_moment.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,7 +12,11 @@ """The ElectronicDipoleMoment property.""" -from typing import Dict, List, Optional, Tuple, cast +from __future__ import annotations + +from typing import Optional, Tuple, cast + +import h5py from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import QMolecule @@ -41,8 +45,8 @@ class DipoleMoment(IntegralProperty): def __init__( self, axis: str, - electronic_integrals: List[ElectronicIntegrals], - shift: Optional[Dict[str, complex]] = None, + electronic_integrals: list[ElectronicIntegrals], + shift: Optional[dict[str, complex]] = None, ) -> None: """ Args: @@ -54,6 +58,47 @@ def __init__( name = self.__class__.__name__ + axis.upper() super().__init__(name, electronic_integrals, shift=shift) + @property + def axis(self) -> str: + """Returns the axis.""" + return self._axis + + @axis.setter + def axis(self, axis: str) -> None: + """Sets the axis.""" + self._axis = axis + + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + group.attrs["axis"] = self._axis + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> DipoleMoment: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + integral_property = IntegralProperty.from_hdf5(h5py_group) + + axis = h5py_group.attrs["axis"] + + return DipoleMoment(axis, list(integral_property), shift=integral_property._shift) + def integral_operator(self, density: OneBodyElectronicIntegrals) -> OneBodyElectronicIntegrals: """Returns the AO 1-electron integrals. @@ -98,8 +143,8 @@ class ElectronicDipoleMoment(GroupedProperty[DipoleMoment], ElectronicProperty): def __init__( self, - dipole_axes: List[DipoleMoment], - dipole_shift: Optional[Dict[str, DipoleTuple]] = None, + dipole_axes: Optional[list[DipoleMoment]] = None, + dipole_shift: Optional[dict[str, DipoleTuple]] = None, nuclear_dipole_moment: Optional[DipoleTuple] = None, reverse_dipole_sign: bool = False, ) -> None: @@ -115,8 +160,55 @@ def __init__( self._dipole_shift = dipole_shift self._nuclear_dipole_moment = nuclear_dipole_moment self._reverse_dipole_sign = reverse_dipole_sign - for dipole in dipole_axes: - self.add_property(dipole) + if dipole_axes is not None: + for dipole in dipole_axes: + self.add_property(dipole) + + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + + group = parent.require_group(self.name) + + group.attrs["reverse_dipole_sign"] = self._reverse_dipole_sign + + if self._nuclear_dipole_moment is not None: + group.attrs["nuclear_dipole_moment"] = self._nuclear_dipole_moment + + dipole_shift_group = group.create_group("dipole_shift") + if self._dipole_shift is not None: + for name, shift in self._dipole_shift.items(): + dipole_shift_group.attrs[name] = shift + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> ElectronicDipoleMoment: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + grouped_property = GroupedProperty.from_hdf5(h5py_group) + + ret = ElectronicDipoleMoment(list(grouped_property)) + + ret.reverse_dipole_sign = h5py_group.attrs["reverse_dipole_sign"] + ret.nuclear_dipole_moment = h5py_group.attrs.get("nuclear_dipole_moment", None) + + for name, shift in h5py_group["dipole_shift"].attrs.items(): + ret._dipole_shift[name] = shift + + return ret @property def nuclear_dipole_moment(self) -> Optional[DipoleTuple]: @@ -143,7 +235,7 @@ def reverse_dipole_sign(self, reverse_dipole_sign: bool) -> None: @classmethod def from_legacy_driver_result( cls, result: LegacyDriverResult - ) -> Optional["ElectronicDipoleMoment"]: + ) -> Optional[ElectronicDipoleMoment]: """Construct an ElectronicDipoleMoment instance from a :class:`~qiskit_nature.drivers.QMolecule`. @@ -179,30 +271,37 @@ def dipole_along_axis(axis, ao_ints, mo_ints, energy_shift): DipoleTuple, tuple(d_m for d_m in qmol.nuclear_dipole_moment) ) - return cls( - [ - dipole_along_axis( - "x", - (qmol.x_dip_ints, None), - (qmol.x_dip_mo_ints, qmol.x_dip_mo_ints_b), - qmol.x_dip_energy_shift, - ), - dipole_along_axis( - "y", - (qmol.y_dip_ints, None), - (qmol.y_dip_mo_ints, qmol.y_dip_mo_ints_b), - qmol.y_dip_energy_shift, - ), - dipole_along_axis( - "z", - (qmol.z_dip_ints, None), - (qmol.z_dip_mo_ints, qmol.z_dip_mo_ints_b), - qmol.z_dip_energy_shift, - ), - ], - nuclear_dipole_moment=nuclear_dipole_moment, - reverse_dipole_sign=qmol.reverse_dipole_sign, + ret = cls() + + ret.add_property( + dipole_along_axis( + "x", + (qmol.x_dip_ints, None), + (qmol.x_dip_mo_ints, qmol.x_dip_mo_ints_b), + qmol.x_dip_energy_shift, + ) ) + ret.add_property( + dipole_along_axis( + "y", + (qmol.y_dip_ints, None), + (qmol.y_dip_mo_ints, qmol.y_dip_mo_ints_b), + qmol.y_dip_energy_shift, + ) + ) + ret.add_property( + dipole_along_axis( + "z", + (qmol.z_dip_ints, None), + (qmol.z_dip_mo_ints, qmol.z_dip_mo_ints_b), + qmol.z_dip_energy_shift, + ) + ) + + ret.nuclear_dipole_moment = nuclear_dipole_moment + ret.reverse_dipole_sign = qmol.reverse_dipole_sign + + return ret def second_q_ops(self) -> ListOrDictType[FermionicOp]: """Returns the second quantized dipole moment operators. @@ -248,7 +347,7 @@ def interpret(self, result: EigenstateResult) -> None: axes_order = {"x": 0, "y": 1, "z": 2} dipole_moment = [None] * 3 for prop in iter(self): - moment: Optional[Tuple[complex, complex]] + moment: Optional[tuple[complex, complex]] try: moment = aux_op_eigenvalues[axes_order[prop._axis] + 3] except KeyError: @@ -257,7 +356,7 @@ def interpret(self, result: EigenstateResult) -> None: dipole_moment[axes_order[prop._axis]] = moment[0].real # type: ignore result.computed_dipole_moment.append(cast(DipoleTuple, tuple(dipole_moment))) - dipole_shifts: Dict[str, Dict[str, complex]] = {} + dipole_shifts: dict[str, dict[str, complex]] = {} for prop in self._properties.values(): for name, shift in prop._shift.items(): if name not in dipole_shifts: diff --git a/qiskit_nature/properties/second_quantization/electronic/electronic_energy.py b/qiskit_nature/properties/second_quantization/electronic/electronic_energy.py index 00e32ea71b..25fa2d1549 100644 --- a/qiskit_nature/properties/second_quantization/electronic/electronic_energy.py +++ b/qiskit_nature/properties/second_quantization/electronic/electronic_energy.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,8 +12,11 @@ """The ElectronicEnergy property.""" -from typing import Dict, List, Optional, cast +from __future__ import annotations +from typing import Optional, cast + +import h5py import numpy as np from qiskit_nature.drivers import QMolecule @@ -44,8 +47,8 @@ class ElectronicEnergy(IntegralProperty): def __init__( self, - electronic_integrals: List[ElectronicIntegrals], - energy_shift: Optional[Dict[str, complex]] = None, + electronic_integrals: list[ElectronicIntegrals], + energy_shift: Optional[dict[str, complex]] = None, nuclear_repulsion_energy: Optional[float] = None, reference_energy: Optional[float] = None, ) -> None: @@ -67,6 +70,66 @@ def __init__( self._kinetic: ElectronicIntegrals = None self._overlap: ElectronicIntegrals = None + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + if self.nuclear_repulsion_energy is not None: + group.attrs["nuclear_repulsion_energy"] = self.nuclear_repulsion_energy + + if self.reference_energy is not None: + group.attrs["reference_energy"] = self.reference_energy + + if self.orbital_energies is not None: + group.attrs["orbital_energies"] = self.orbital_energies + + if self.kinetic is not None: + kinetic_group = group.create_group("kinetic") + self.kinetic.to_hdf5(kinetic_group) + + if self.overlap is not None: + overlap_group = group.create_group("overlap") + self.overlap.to_hdf5(overlap_group) + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> ElectronicEnergy: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + integral_property = IntegralProperty.from_hdf5(h5py_group) + + ret = ElectronicEnergy(list(integral_property), energy_shift=integral_property._shift) + + ret.nuclear_repulsion_energy = h5py_group.attrs.get("nuclear_repulsion_energy", None) + ret.reference_energy = h5py_group.attrs.get("reference_energy", None) + ret.orbital_energies = h5py_group.attrs.get("orbital_energies", None) + + if "kinetic" in h5py_group.keys(): + ret.kinetic = ElectronicIntegrals.from_hdf5( + h5py_group["kinetic"]["OneBodyElectronicIntegrals"] + ) + + if "overlap" in h5py_group.keys(): + ret.overlap = ElectronicIntegrals.from_hdf5( + h5py_group["overlap"]["OneBodyElectronicIntegrals"] + ) + + return ret + @property def nuclear_repulsion_energy(self) -> Optional[float]: """Returns the nuclear repulsion energy.""" @@ -121,7 +184,7 @@ def overlap(self, overlap: Optional[ElectronicIntegrals]) -> None: self._overlap = overlap @classmethod - def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "ElectronicEnergy": + def from_legacy_driver_result(cls, result: LegacyDriverResult) -> ElectronicEnergy: """Construct an ``ElectronicEnergy`` instance from a :class:`~qiskit_nature.drivers.QMolecule`. Args: @@ -140,7 +203,7 @@ def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "ElectronicEne energy_shift = qmol.energy_shift.copy() - integrals: List[ElectronicIntegrals] = [] + integrals: list[ElectronicIntegrals] = [] if qmol.hcore is not None: integrals.append( OneBodyElectronicIntegrals(ElectronicBasis.AO, (qmol.hcore, qmol.hcore_b)) @@ -194,7 +257,7 @@ def from_raw_integrals( h2_bb: Optional[np.ndarray] = None, h2_ba: Optional[np.ndarray] = None, threshold: float = ElectronicIntegrals.INTEGRAL_TRUNCATION_LEVEL, - ) -> "ElectronicEnergy": + ) -> ElectronicEnergy: """Construct an ``ElectronicEnergy`` from raw integrals in a given basis. When setting the basis to diff --git a/qiskit_nature/properties/second_quantization/electronic/electronic_structure_driver_result.py b/qiskit_nature/properties/second_quantization/electronic/electronic_structure_driver_result.py index 720fe6e0e0..6027e9542d 100644 --- a/qiskit_nature/properties/second_quantization/electronic/electronic_structure_driver_result.py +++ b/qiskit_nature/properties/second_quantization/electronic/electronic_structure_driver_result.py @@ -12,7 +12,11 @@ """The ElectronicStructureDriverResult class.""" -from typing import List, Tuple, cast +from __future__ import annotations + +from typing import cast + +import h5py from qiskit_nature import ListOrDictType, settings from qiskit_nature.constants import BOHR @@ -20,7 +24,7 @@ from qiskit_nature.drivers import QMolecule from qiskit_nature.operators.second_quantization import FermionicOp -from ..second_quantized_property import LegacyDriverResult +from ..second_quantized_property import LegacyDriverResult, SecondQuantizedProperty from ..driver_metadata import DriverMetadata from .angular_momentum import AngularMomentum from .bases import ElectronicBasis, ElectronicBasisTransform @@ -43,17 +47,52 @@ def __init__(self) -> None: Property objects should be added via ``add_property`` rather than via the initializer. """ super().__init__(self.__class__.__name__) - self.molecule: "Molecule" = None + self.molecule: Molecule = None def __str__(self) -> str: string = [super().__str__()] string += [str(self.molecule)] return "\n".join(string) + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + self.molecule.to_hdf5(group) + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> ElectronicStructureDriverResult: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + grouped_property = GroupedElectronicProperty.from_hdf5(h5py_group) + + ret = ElectronicStructureDriverResult() + for prop in grouped_property: + if isinstance(prop, Molecule): + ret.molecule = prop + else: + ret.add_property(prop) + + return ret + @classmethod def from_legacy_driver_result( cls, result: LegacyDriverResult - ) -> "ElectronicStructureDriverResult": + ) -> ElectronicStructureDriverResult: """Converts a :class:`~qiskit_nature.drivers.QMolecule` into an ``ElectronicStructureDriverResult``. @@ -84,7 +123,7 @@ def from_legacy_driver_result( ) ) - geometry: List[Tuple[str, List[float]]] = [] + geometry: list[tuple[str, list[float]]] = [] for atom, xyz in zip(qmol.atom_symbol, qmol.atom_xyz): # QMolecule XYZ defaults to Bohr but Molecule requires Angstrom geometry.append((atom, xyz * BOHR)) @@ -121,12 +160,14 @@ def second_q_ops(self) -> ListOrDictType[FermionicOp]: ElectronicDipoleMoment, ]: prop = self.get_property(cls) # type: ignore - if prop is None: + if prop is None or not isinstance(prop, SecondQuantizedProperty): continue ops.extend(prop.second_q_ops()) return ops ops = {} for prop in iter(self): + if not isinstance(prop, SecondQuantizedProperty): + continue ops.update(prop.second_q_ops()) return ops diff --git a/qiskit_nature/properties/second_quantization/electronic/integrals/electronic_integrals.py b/qiskit_nature/properties/second_quantization/electronic/integrals/electronic_integrals.py index 5debaaae9b..ee0dcc98be 100644 --- a/qiskit_nature/properties/second_quantization/electronic/integrals/electronic_integrals.py +++ b/qiskit_nature/properties/second_quantization/electronic/integrals/electronic_integrals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,11 +12,15 @@ """A base class for raw electronic integrals.""" +from __future__ import annotations + +import importlib import itertools from abc import ABC, abstractmethod from copy import deepcopy -from typing import List, Optional, Tuple, Union +from typing import Generator, Optional, Union +import h5py import numpy as np from qiskit_nature.operators.second_quantization import FermionicOp @@ -41,15 +45,19 @@ class ElectronicIntegrals(ABC): ``ElectronicIntegrals.set_truncation`` to change this value. """ + VERSION = 1 + INTEGRAL_TRUNCATION_LEVEL = 1e-12 + _MATRIX_REPRESENTATIONS: list[str] = [] + _truncate = 5 def __init__( self, num_body_terms: int, basis: ElectronicBasis, - matrices: Union[np.ndarray, Tuple[Optional[np.ndarray], ...]], + matrices: Union[np.ndarray, tuple[Optional[np.ndarray], ...]], threshold: float = INTEGRAL_TRUNCATION_LEVEL, ) -> None: # pylint: disable=line-too-long @@ -75,11 +83,11 @@ def __init__( """ self._validate_num_body_terms(num_body_terms) self._validate_matrices(matrices, basis, num_body_terms) + self.name = self.__class__.__name__ self._basis = basis self._num_body_terms = num_body_terms self._threshold = threshold - self._matrix_representations: List[str] = [""] * len(matrices) - self._matrices: Union[np.ndarray, Tuple[Optional[np.ndarray], ...]] + self._matrices: Union[np.ndarray, tuple[Optional[np.ndarray], ...]] if basis == ElectronicBasis.SO: self._matrices = np.where(np.abs(matrices) > self._threshold, matrices, 0.0) else: @@ -91,12 +99,92 @@ def __init__( if basis != ElectronicBasis.SO: self._fill_matrices() + @property + def basis(self) -> ElectronicBasis: + """Returns the basis.""" + return self._basis + + @basis.setter + def basis(self, basis: ElectronicBasis) -> None: + """Sets the basis.""" + self._basis = basis + + @property + def num_body_terms(self) -> int: + """Returns the num_body_terms.""" + return self._num_body_terms + + @num_body_terms.setter + def num_body_terms(self, num_body_terms: int) -> None: + """Sets the num_body_terms.""" + self._num_body_terms = num_body_terms + + @property + def threshold(self) -> float: + """Returns the threshold.""" + return self._threshold + + @threshold.setter + def threshold(self, threshold: float) -> None: + """Sets the threshold.""" + self._threshold = threshold + + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + group = parent.require_group(self.name) + group.attrs["__class__"] = self.__class__.__name__ + group.attrs["__module__"] = self.__class__.__module__ + group.attrs["__version__"] = self.VERSION + + group.attrs["basis"] = self._basis.name + group.attrs["threshold"] = self._threshold + + if self._basis == ElectronicBasis.SO: + group.create_dataset("Spin", data=self._matrices) + else: + for name, mat in zip(self._MATRIX_REPRESENTATIONS, self._matrices): + group.create_dataset(name, data=mat) + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> ElectronicIntegrals: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + class_name = h5py_group.attrs["__class__"] + module_path = h5py_group.attrs["__module__"] + + loaded_module = importlib.import_module(module_path) + loaded_class = getattr(loaded_module, class_name, None) + + basis = getattr(ElectronicBasis, h5py_group.attrs["basis"]) + threshold = h5py_group.attrs["threshold"] + matrices = tuple(h5py_group[rep][...] for rep in loaded_class._MATRIX_REPRESENTATIONS) + + return loaded_class( + basis=basis, + matrices=matrices, + threshold=threshold, + ) + def __str__(self) -> str: string = [f"({self._basis.name}) {self._num_body_terms}-Body Terms:"] if self._basis == ElectronicBasis.SO: string += self._render_matrix_as_sparse_list(self._matrices) else: - for title, mat in zip(self._matrix_representations, self._matrices): + for title, mat in zip(self._MATRIX_REPRESENTATIONS, self._matrices): rendered_matrix = self._render_matrix_as_sparse_list(mat) string += [f"\t{title}"] if not rendered_matrix: @@ -106,7 +194,7 @@ def __str__(self) -> str: return "\n".join(string) @staticmethod - def _render_matrix_as_sparse_list(matrix) -> List[str]: + def _render_matrix_as_sparse_list(matrix) -> list[str]: string = [] nonzero = matrix.nonzero() nonzero_count = len(nonzero[0]) @@ -122,6 +210,14 @@ def _render_matrix_as_sparse_list(matrix) -> List[str]: count += 1 return string + def __iter__(self) -> Generator[np.ndarray, None, None]: + """An iterator over the internal matrices.""" + if isinstance(self._matrices, np.ndarray): + yield self._matrices + else: + for mat in self._matrices: + yield mat + @staticmethod def set_truncation(max_num_entries: int) -> None: """Set the maximum number of integral values to display before truncation. @@ -144,7 +240,7 @@ def _validate_num_body_terms(num_body_terms: int) -> None: @staticmethod def _validate_matrices( - matrices: Union[np.ndarray, Tuple[Optional[np.ndarray], ...]], + matrices: Union[np.ndarray, tuple[Optional[np.ndarray], ...]], basis: ElectronicBasis, num_body_terms: int, ) -> None: @@ -185,7 +281,7 @@ def _fill_matrices(self) -> None: self._matrices = tuple(filled_matrices) @abstractmethod - def transform_basis(self, transform: ElectronicBasisTransform) -> "ElectronicIntegrals": + def transform_basis(self, transform: ElectronicBasisTransform) -> ElectronicIntegrals: # pylint: disable=line-too-long """Transforms the integrals according to the given transform object. @@ -237,7 +333,7 @@ def to_second_q_op(self) -> FermionicOp: if spin_matrix[indices] ) - def _create_base_op(self, indices: Tuple[int, ...], coeff: complex, length: int) -> FermionicOp: + def _create_base_op(self, indices: tuple[int, ...], coeff: complex, length: int) -> FermionicOp: """Creates a single base operator for the given coefficient. Args: @@ -254,7 +350,7 @@ def _create_base_op(self, indices: Tuple[int, ...], coeff: complex, length: int) return base_op @abstractmethod - def _calc_coeffs_with_ops(self, indices: Tuple[int, ...]) -> List[Tuple[int, str]]: + def _calc_coeffs_with_ops(self, indices: tuple[int, ...]) -> list[tuple[int, str]]: """Maps indices to creation/annihilation operator symbols. Args: @@ -264,7 +360,7 @@ def _calc_coeffs_with_ops(self, indices: Tuple[int, ...]) -> List[Tuple[int, str A list of tuples associating each index with a creation/annihilation operator symbol. """ - def add(self, other: "ElectronicIntegrals") -> "ElectronicIntegrals": + def add(self, other: ElectronicIntegrals) -> ElectronicIntegrals: """Adds two ElectronicIntegrals instances. Args: @@ -281,8 +377,8 @@ def add(self, other: "ElectronicIntegrals") -> "ElectronicIntegrals": return ret def compose( - self, other: "ElectronicIntegrals", einsum_subscript: Optional[str] = None - ) -> Union[complex, "ElectronicIntegrals"]: + self, other: ElectronicIntegrals, einsum_subscript: Optional[str] = None + ) -> Union[complex, ElectronicIntegrals]: """Composes two ``ElectronicIntegrals`` instances. Args: @@ -294,7 +390,7 @@ def compose( """ raise NotImplementedError() - def __rmul__(self, other: complex) -> "ElectronicIntegrals": + def __rmul__(self, other: complex) -> ElectronicIntegrals: ret = deepcopy(self) if isinstance(self._matrices, np.ndarray): ret._matrices = other * self._matrices @@ -302,7 +398,7 @@ def __rmul__(self, other: complex) -> "ElectronicIntegrals": ret._matrices = [other * mat for mat in self._matrices] # type: ignore return ret - def __add__(self, other: "ElectronicIntegrals") -> "ElectronicIntegrals": + def __add__(self, other: ElectronicIntegrals) -> ElectronicIntegrals: if self._basis != other._basis: raise ValueError( f"The basis of self, {self._basis.value}, does not match the basis of other, " @@ -310,5 +406,5 @@ def __add__(self, other: "ElectronicIntegrals") -> "ElectronicIntegrals": ) return self.add(other) - def __sub__(self, other: "ElectronicIntegrals") -> "ElectronicIntegrals": + def __sub__(self, other: ElectronicIntegrals) -> ElectronicIntegrals: return self + (-1.0) * other diff --git a/qiskit_nature/properties/second_quantization/electronic/integrals/integral_property.py b/qiskit_nature/properties/second_quantization/electronic/integrals/integral_property.py index c0445b88b7..c7fdb586ad 100644 --- a/qiskit_nature/properties/second_quantization/electronic/integrals/integral_property.py +++ b/qiskit_nature/properties/second_quantization/electronic/integrals/integral_property.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,7 +12,11 @@ """The IntegralProperty property.""" -from typing import Dict, List, Optional +from __future__ import annotations + +from typing import Generator, Optional + +import h5py from qiskit_nature import ListOrDictType, settings from qiskit_nature.operators.second_quantization import FermionicOp @@ -39,8 +43,8 @@ class IntegralProperty(ElectronicProperty): def __init__( self, name: str, - electronic_integrals: List[ElectronicIntegrals], - shift: Optional[Dict[str, complex]] = None, + electronic_integrals: list[ElectronicIntegrals], + shift: Optional[dict[str, complex]] = None, ) -> None: # pylint: disable=line-too-long """ @@ -51,22 +55,34 @@ def __init__( shift: an optional dictionary of value shifts. """ super().__init__(name) - self._electronic_integrals: Dict[ElectronicBasis, Dict[int, ElectronicIntegrals]] = {} + self._electronic_integrals: dict[ElectronicBasis, dict[int, ElectronicIntegrals]] = {} for integral in electronic_integrals: self.add_electronic_integral(integral) self._shift = shift or {} def __str__(self) -> str: string = [super().__str__()] - for basis_ints in self._electronic_integrals.values(): - for ints in basis_ints.values(): - string += ["\t" + "\n\t".join(str(ints).split("\n"))] + for ints in self: + string += ["\t" + "\n\t".join(str(ints).split("\n"))] if self._shift: string += ["\tEnergy Shifts:"] for name, shift in self._shift.items(): string += [f"\t\t{name} = {shift}"] return "\n".join(string) + def __iter__(self) -> Generator[ElectronicIntegrals, None, None]: + """Returns the generator-iterator method.""" + return self._generator() + + def _generator(self) -> Generator[ElectronicIntegrals, None, None]: + """A generator-iterator method [1] iterating over all internal ``ElectronicIntegrals``. + + [1]: https://docs.python.org/3/reference/expressions.html#generator-iterator-methods + """ + for basis_ints in self._electronic_integrals.values(): + for ints in basis_ints.values(): + yield ints + def add_electronic_integral(self, integral: ElectronicIntegrals) -> None: """Adds an ElectronicIntegrals instance to the internal storage. @@ -152,7 +168,7 @@ def second_q_ops(self) -> ListOrDictType[FermionicOp]: return {self.name: op} @classmethod - def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "IntegralProperty": + def from_legacy_driver_result(cls, result: LegacyDriverResult) -> IntegralProperty: """This property does not support construction from a legacy driver result (yet). Args: @@ -173,3 +189,49 @@ def interpret(self, result: EigenstateResult) -> None: NotImplementedError """ raise NotImplementedError() + + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + ints_group = group.create_group("electronic_integrals") + for basis, integrals in self._electronic_integrals.items(): + basis_group = ints_group.create_group(basis.name) + for integral in integrals.values(): + integral.to_hdf5(basis_group) + + shift_group = group.create_group("shift") + for name, shift in self._shift.items(): + shift_group.attrs[name] = shift + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> IntegralProperty: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + ints = [] + for basis_group in h5py_group["electronic_integrals"].values(): + for int_group in basis_group.values(): + ints.append(ElectronicIntegrals.from_hdf5(int_group)) + + shifts = {} + for name, shift in h5py_group["shift"].attrs.items(): + shifts[name] = shift + + class_name = h5py_group.attrs.get("__class__", "") + + return IntegralProperty(class_name, ints, shifts) diff --git a/qiskit_nature/properties/second_quantization/electronic/integrals/one_body_electronic_integrals.py b/qiskit_nature/properties/second_quantization/electronic/integrals/one_body_electronic_integrals.py index 21646aab85..70bfcc88b2 100644 --- a/qiskit_nature/properties/second_quantization/electronic/integrals/one_body_electronic_integrals.py +++ b/qiskit_nature/properties/second_quantization/electronic/integrals/one_body_electronic_integrals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,7 +12,9 @@ """The 1-body electronic integrals.""" -from typing import List, Optional, Tuple, Union +from __future__ import annotations + +from typing import Optional, Union import numpy as np @@ -25,10 +27,12 @@ class OneBodyElectronicIntegrals(ElectronicIntegrals): """The 1-body electronic integrals.""" + _MATRIX_REPRESENTATIONS = ["Alpha", "Beta"] + def __init__( self, basis: ElectronicBasis, - matrices: Union[np.ndarray, Tuple[Optional[np.ndarray], ...]], + matrices: Union[np.ndarray, tuple[Optional[np.ndarray], ...]], threshold: float = ElectronicIntegrals.INTEGRAL_TRUNCATION_LEVEL, ) -> None: # pylint: disable=line-too-long @@ -48,9 +52,8 @@ def __init__( """ num_body_terms = 1 super().__init__(num_body_terms, basis, matrices, threshold) - self._matrix_representations = ["Alpha", "Beta"] - def transform_basis(self, transform: ElectronicBasisTransform) -> "OneBodyElectronicIntegrals": + def transform_basis(self, transform: ElectronicBasisTransform) -> OneBodyElectronicIntegrals: # pylint: disable=line-too-long """Transforms the integrals according to the given transform object. @@ -105,7 +108,7 @@ def to_spin(self) -> np.ndarray: return np.where(np.abs(so_matrix) > self._threshold, so_matrix, 0.0) - def _calc_coeffs_with_ops(self, indices: Tuple[int, ...]) -> List[Tuple[int, str]]: + def _calc_coeffs_with_ops(self, indices: tuple[int, ...]) -> list[tuple[int, str]]: return [(indices[0], "+"), (indices[1], "-")] def compose(self, other: ElectronicIntegrals, einsum_subscript: str = "ij,ji") -> complex: diff --git a/qiskit_nature/properties/second_quantization/electronic/integrals/two_body_electronic_integrals.py b/qiskit_nature/properties/second_quantization/electronic/integrals/two_body_electronic_integrals.py index 03836013ea..42be611dfd 100644 --- a/qiskit_nature/properties/second_quantization/electronic/integrals/two_body_electronic_integrals.py +++ b/qiskit_nature/properties/second_quantization/electronic/integrals/two_body_electronic_integrals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,7 +12,9 @@ """The 2-body electronic integrals.""" -from typing import List, Optional, Tuple, Union +from __future__ import annotations + +from typing import Optional, Union import numpy as np @@ -29,12 +31,14 @@ class TwoBodyElectronicIntegrals(ElectronicIntegrals): EINSUM_AO_TO_MO = "pqrs,pi,qj,rk,sl->ijkl" EINSUM_CHEM_TO_PHYS = "ijkl->ljik" + _MATRIX_REPRESENTATIONS = ["Alpha-Alpha", "Beta-Alpha", "Beta-Beta", "Alpha-Beta"] + # TODO: provide symmetry testing functionality? def __init__( self, basis: ElectronicBasis, - matrices: Union[np.ndarray, Tuple[Optional[np.ndarray], ...]], + matrices: Union[np.ndarray, tuple[Optional[np.ndarray], ...]], threshold: float = ElectronicIntegrals.INTEGRAL_TRUNCATION_LEVEL, ) -> None: # pylint: disable=line-too-long @@ -58,7 +62,6 @@ def __init__( """ num_body_terms = 2 super().__init__(num_body_terms, basis, matrices, threshold) - self._matrix_representations = ["Alpha-Alpha", "Beta-Alpha", "Beta-Beta", "Alpha-Beta"] def _fill_matrices(self) -> None: """Fills the internal matrices where ``None`` placeholders were inserted. @@ -84,7 +87,7 @@ def _fill_matrices(self) -> None: filled_matrices.append(self._matrices[0]) self._matrices = tuple(filled_matrices) - def transform_basis(self, transform: ElectronicBasisTransform) -> "TwoBodyElectronicIntegrals": + def transform_basis(self, transform: ElectronicBasisTransform) -> TwoBodyElectronicIntegrals: # pylint: disable=line-too-long """Transforms the integrals according to the given transform object. @@ -119,7 +122,7 @@ def transform_basis(self, transform: ElectronicBasisTransform) -> "TwoBodyElectr (coeff_beta, coeff_beta, coeff_beta, coeff_beta), (coeff_alpha, coeff_alpha, coeff_beta, coeff_beta), ] - matrices: List[Optional[np.ndarray]] = [] + matrices: list[Optional[np.ndarray]] = [] for mat, coeffs in zip(self._matrices, coeff_list): if mat is None: matrices.append(None) @@ -160,7 +163,7 @@ def to_spin(self) -> np.ndarray: return np.where(np.abs(so_matrix) > self._threshold, so_matrix, 0.0) - def _calc_coeffs_with_ops(self, indices: Tuple[int, ...]) -> List[Tuple[int, str]]: + def _calc_coeffs_with_ops(self, indices: tuple[int, ...]) -> list[tuple[int, str]]: return [(indices[0], "+"), (indices[2], "+"), (indices[3], "-"), (indices[1], "-")] def compose( diff --git a/qiskit_nature/properties/second_quantization/electronic/magnetization.py b/qiskit_nature/properties/second_quantization/electronic/magnetization.py index b1c0eb80b2..19ad472c29 100644 --- a/qiskit_nature/properties/second_quantization/electronic/magnetization.py +++ b/qiskit_nature/properties/second_quantization/electronic/magnetization.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,8 +12,12 @@ """The Magnetization property.""" +from __future__ import annotations + from typing import cast +import h5py + from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import QMolecule from qiskit_nature.operators.second_quantization import FermionicOp @@ -34,13 +38,50 @@ def __init__(self, num_spin_orbitals: int) -> None: super().__init__(self.__class__.__name__) self._num_spin_orbitals = num_spin_orbitals + @property + def num_spin_orbitals(self) -> int: + """Returns the number of spin orbitals.""" + return self._num_spin_orbitals + + @num_spin_orbitals.setter + def num_spin_orbitals(self, num_spin_orbitals: int) -> None: + """Sets the number of spin orbitals.""" + self._num_spin_orbitals = num_spin_orbitals + def __str__(self) -> str: string = [super().__str__() + ":"] string += [f"\t{self._num_spin_orbitals} SOs"] return "\n".join(string) + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + group.attrs["num_spin_orbitals"] = self._num_spin_orbitals + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> Magnetization: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + return Magnetization(h5py_group.attrs["num_spin_orbitals"]) + @classmethod - def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "Magnetization": + def from_legacy_driver_result(cls, result: LegacyDriverResult) -> Magnetization: """Construct a Magnetization instance from a :class:`~qiskit_nature.drivers.QMolecule`. Args: diff --git a/qiskit_nature/properties/second_quantization/electronic/particle_number.py b/qiskit_nature/properties/second_quantization/electronic/particle_number.py index 355957e063..b31da7d005 100644 --- a/qiskit_nature/properties/second_quantization/electronic/particle_number.py +++ b/qiskit_nature/properties/second_quantization/electronic/particle_number.py @@ -12,9 +12,12 @@ """The ParticleNumber property.""" +from __future__ import annotations + import logging -from typing import List, Optional, Tuple, Union, cast +from typing import Optional, Union, cast +import h5py import numpy as np from qiskit_nature import ListOrDictType, settings @@ -44,9 +47,9 @@ class ParticleNumber(ElectronicProperty): def __init__( self, num_spin_orbitals: int, - num_particles: Union[int, Tuple[int, int]], - occupation: Optional[Union[np.ndarray, List[float]]] = None, - occupation_beta: Optional[Union[np.ndarray, List[float]]] = None, + num_particles: Union[int, tuple[int, int]], + occupation: Optional[Union[np.ndarray, list[float]]] = None, + occupation_beta: Optional[Union[np.ndarray, list[float]]] = None, absolute_tolerance: float = ABSOLUTE_TOLERANCE, relative_tolerance: float = RELATIVE_TOLERANCE, ) -> None: @@ -72,6 +75,8 @@ def __init__( else: self._num_alpha, self._num_beta = num_particles + self._occupation_alpha: Union[np.ndarray, list[float]] + self._occupation_beta: Union[np.ndarray, list[float]] if occupation is None: self._occupation_alpha = [1.0 for _ in range(self._num_alpha)] self._occupation_alpha += [0.0] * (num_spin_orbitals // 2 - len(self._occupation_alpha)) @@ -81,8 +86,8 @@ def __init__( self._occupation_alpha = [np.ceil(o / 2) for o in occupation] self._occupation_beta = [np.floor(o / 2) for o in occupation] else: - self._occupation_alpha = occupation # type: ignore - self._occupation_beta = occupation_beta # type: ignore + self._occupation_alpha = occupation + self._occupation_beta = occupation_beta self._absolute_tolerance = absolute_tolerance self._relative_tolerance = relative_tolerance @@ -92,18 +97,33 @@ def num_spin_orbitals(self) -> int: """Returns the num_spin_orbitals.""" return self._num_spin_orbitals + @num_spin_orbitals.setter + def num_spin_orbitals(self, num_spin_orbitals: int) -> None: + """Sets the number of spin orbitals.""" + self._num_spin_orbitals = num_spin_orbitals + @property def num_alpha(self) -> int: """Returns the number of alpha electrons.""" return self._num_alpha + @num_alpha.setter + def num_alpha(self, num_alpha: int) -> None: + """Sets the number of alpha electrons.""" + self._num_alpha = num_alpha + @property def num_beta(self) -> int: """Returns the number of beta electrons.""" return self._num_beta + @num_beta.setter + def num_beta(self, num_beta: int) -> None: + """Sets the number of beta electrons.""" + self._num_beta = num_beta + @property - def num_particles(self) -> Tuple[int, int]: + def num_particles(self) -> tuple[int, int]: """Returns the number of electrons.""" return (self.num_alpha, self.num_beta) @@ -116,6 +136,11 @@ def occupation_alpha(self) -> np.ndarray: """ return np.asarray(self._occupation_alpha) + @occupation_alpha.setter + def occupation_alpha(self, occ_alpha: Union[np.ndarray, list[float]]) -> None: + """Sets the occupation numbers of the alpha-spin orbitals.""" + self._occupation_alpha = occ_alpha + @property def occupation_beta(self) -> np.ndarray: """Returns the occupation numbers of the beta-spin orbitals. @@ -125,6 +150,31 @@ def occupation_beta(self) -> np.ndarray: """ return np.asarray(self._occupation_beta) + @occupation_beta.setter + def occupation_beta(self, occ_beta: Union[np.ndarray, list[float]]) -> None: + """Sets the occupation numbers of the beta-spin orbitals.""" + self._occupation_beta = occ_beta + + @property + def absolute_tolerance(self) -> float: + """Returns the absolute tolerance.""" + return self._absolute_tolerance + + @absolute_tolerance.setter + def absolute_tolerance(self, absolute_tolerance: float) -> None: + """Sets the absolute tolerance.""" + self._absolute_tolerance = absolute_tolerance + + @property + def relative_tolerance(self) -> float: + """Returns the relative tolerance.""" + return self._relative_tolerance + + @relative_tolerance.setter + def relative_tolerance(self, relative_tolerance: float) -> None: + """Sets the relative tolerance.""" + self._relative_tolerance = relative_tolerance + def __str__(self) -> str: string = [super().__str__() + ":"] string += [f"\t{self._num_spin_orbitals} SOs"] @@ -134,8 +184,49 @@ def __str__(self) -> str: string += [f"\t\torbital occupation: {self.occupation_beta}"] return "\n".join(string) + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + group.attrs["num_spin_orbitals"] = self._num_spin_orbitals + group.attrs["num_alpha"] = self._num_alpha + group.attrs["num_beta"] = self._num_beta + group.attrs["absolute_tolerance"] = self._absolute_tolerance + group.attrs["relative_tolerance"] = self._relative_tolerance + + group.create_dataset("occupation_alpha", data=self.occupation_alpha) + group.create_dataset("occupation_beta", data=self.occupation_beta) + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> ParticleNumber: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + return ParticleNumber( + h5py_group.attrs["num_spin_orbitals"], + (h5py_group.attrs["num_alpha"], h5py_group.attrs["num_beta"]), + h5py_group["occupation_alpha"][...], + h5py_group["occupation_beta"][...], + h5py_group.attrs["absolute_tolerance"], + h5py_group.attrs["relative_tolerance"], + ) + @classmethod - def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "ParticleNumber": + def from_legacy_driver_result(cls, result: LegacyDriverResult) -> ParticleNumber: """Construct a ParticleNumber instance from a :class:`~qiskit_nature.drivers.QMolecule`. Args: diff --git a/qiskit_nature/properties/second_quantization/electronic/types.py b/qiskit_nature/properties/second_quantization/electronic/types.py index 8efb9e3435..4b972476cf 100644 --- a/qiskit_nature/properties/second_quantization/electronic/types.py +++ b/qiskit_nature/properties/second_quantization/electronic/types.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,10 +12,8 @@ """Electronic property types.""" -from typing import Optional, TypeVar +from typing import TypeVar -from qiskit_nature import QiskitNatureError -from ...property import PseudoProperty from ..second_quantized_property import SecondQuantizedProperty, GroupedSecondQuantizedProperty @@ -29,20 +27,3 @@ class ElectronicProperty(SecondQuantizedProperty): class GroupedElectronicProperty(GroupedSecondQuantizedProperty[T], ElectronicProperty): """A GroupedProperty subtype containing purely electronic properties.""" - - def add_property(self, prop: Optional[T]) -> None: - """Adds a property to the group. - - Args: - prop: the property to be added. - - Raises: - QiskitNatureError: if the added property is not an electronic one. - """ - if prop is not None: - if not isinstance(prop, (ElectronicProperty, PseudoProperty)): - raise QiskitNatureError( - f"{prop.__class__.__name__} is not an instance of `ElectronicProperty`, which " - "it must be in order to be added to an `GroupedElectronicProperty`!" - ) - self._properties[prop.name] = prop diff --git a/qiskit_nature/properties/second_quantization/second_quantized_property.py b/qiskit_nature/properties/second_quantization/second_quantized_property.py index 99533224fe..dbbfae9a13 100644 --- a/qiskit_nature/properties/second_quantization/second_quantized_property.py +++ b/qiskit_nature/properties/second_quantization/second_quantized_property.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,6 +12,8 @@ """The SecondQuantizedProperty base class.""" +from __future__ import annotations + from abc import abstractmethod from typing import Any, Type, TypeVar, Union @@ -45,7 +47,7 @@ def second_q_ops(self) -> ListOrDictType[SecondQuantizedOp]: @classmethod @abstractmethod - def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "Property": + def from_legacy_driver_result(cls, result: LegacyDriverResult) -> Property: """Construct a :class:`~qiskit_nature.properties.Property` instance from a legacy driver result. diff --git a/qiskit_nature/properties/second_quantization/vibrational/__init__.py b/qiskit_nature/properties/second_quantization/vibrational/__init__.py index 84f29833e7..61880b9036 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/__init__.py +++ b/qiskit_nature/properties/second_quantization/vibrational/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -16,6 +16,13 @@ .. currentmodule:: qiskit_nature.properties.second_quantization.vibrational This module provides commonly evaluated properties for *vibrational* problems. +It also includes the default return object for the vibrational structure drivers: + +.. autosummary:: + :toctree: ../stubs/ + :nosignatures: + + VibrationalStructureDriverResult The main :class:`~qiskit_nature.properties.Property` of this module is the diff --git a/qiskit_nature/properties/second_quantization/vibrational/bases/vibrational_basis.py b/qiskit_nature/properties/second_quantization/vibrational/bases/vibrational_basis.py index c25a468968..756a786666 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/bases/vibrational_basis.py +++ b/qiskit_nature/properties/second_quantization/vibrational/bases/vibrational_basis.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,8 +12,13 @@ """The Vibrational basis base class.""" +from __future__ import annotations + +import importlib from abc import ABC, abstractmethod -from typing import List, Optional +from typing import Optional + +import h5py class VibrationalBasis(ABC): @@ -24,9 +29,11 @@ class VibrationalBasis(ABC): to the documentation of :class:`~qiskit_nature.properties.vibrational.integrals` for more details. """ + VERSION = 1 + def __init__( self, - num_modals_per_mode: List[int], + num_modals_per_mode: list[int], threshold: float = 1e-6, ) -> None: """ @@ -34,11 +41,12 @@ def __init__( num_modals_per_mode: the number of modals to be used for each mode. threshold: the threshold value below which an integral coefficient gets neglected. """ + self.name = self.__class__.__name__ self._num_modals_per_mode = num_modals_per_mode self._threshold = threshold @property - def num_modals_per_mode(self) -> List[int]: + def num_modals_per_mode(self) -> list[int]: """Returns the number of modals per mode.""" return self._num_modals_per_mode @@ -47,6 +55,44 @@ def __str__(self) -> str: string += [f"\tModals: {self._num_modals_per_mode}"] return "\n".join(string) + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + group = parent.require_group(self.name) + group.attrs["__class__"] = self.__class__.__name__ + group.attrs["__module__"] = self.__class__.__module__ + group.attrs["__version__"] = self.VERSION + + group.attrs["threshold"] = self._threshold + group.attrs["num_modals_per_mode"] = self.num_modals_per_mode + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> VibrationalBasis: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + class_name = h5py_group.attrs["__class__"] + module_path = h5py_group.attrs["__module__"] + + loaded_module = importlib.import_module(module_path) + loaded_class = getattr(loaded_module, class_name, None) + + return loaded_class( + h5py_group.attrs["num_modals_per_mode"], h5py_group.attrs.get("threshold", None) + ) + @abstractmethod def eval_integral( self, diff --git a/qiskit_nature/properties/second_quantization/vibrational/integrals/vibrational_integrals.py b/qiskit_nature/properties/second_quantization/vibrational/integrals/vibrational_integrals.py index af6fdeb4d5..bd80559809 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/integrals/vibrational_integrals.py +++ b/qiskit_nature/properties/second_quantization/vibrational/integrals/vibrational_integrals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,11 +12,14 @@ """A container for arbitrary ``n-body`` vibrational integrals.""" +from __future__ import annotations + from abc import ABC from collections import Counter from itertools import chain, cycle, permutations, product, tee -from typing import Dict, List, Optional, Tuple +from typing import Optional +import h5py import numpy as np from qiskit_nature import QiskitNatureError @@ -33,12 +36,14 @@ class VibrationalIntegrals(ABC): ``VibrationalIntegrals.set_truncation`` to change this value. """ + VERSION = 1 + _truncate = 5 def __init__( self, num_body_terms: int, - integrals: List[Tuple[float, Tuple[int, ...]]], + integrals: list[tuple[float, tuple[int, ...]]], ) -> None: """ Args: @@ -52,10 +57,8 @@ def __init__( Raises: ValueError: if the number of body terms is less than 1. """ - if num_body_terms < 1: - raise ValueError( - f"The number of body terms must be greater than 0, not '{num_body_terms}'." - ) + self._validate_num_body_terms(num_body_terms) + self.name = f"{num_body_terms}Body{self.__class__.__name__}" self._num_body_terms = num_body_terms self._integrals = integrals self._basis: VibrationalBasis = None @@ -71,12 +74,22 @@ def basis(self, basis: VibrationalBasis) -> None: self._basis = basis @property - def integrals(self) -> List[Tuple[float, Tuple[int, ...]]]: + def num_body_terms(self) -> int: + """Returns the num_body_terms.""" + return self._num_body_terms + + @num_body_terms.setter + def num_body_terms(self, num_body_terms: int) -> None: + """Sets the num_body_terms.""" + self._num_body_terms = num_body_terms + + @property + def integrals(self) -> list[tuple[float, tuple[int, ...]]]: """Returns the integrals.""" return self._integrals @integrals.setter - def integrals(self, integrals: List[Tuple[float, Tuple[int, ...]]]) -> None: + def integrals(self, integrals: list[tuple[float, tuple[int, ...]]]) -> None: """Sets the integrals.""" self._integrals = integrals @@ -95,6 +108,49 @@ def __str__(self) -> str: count += 1 return "\n".join(string) + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + group = parent.require_group(self.name) + group.attrs["__class__"] = self.__class__.__name__ + group.attrs["__module__"] = self.__class__.__module__ + group.attrs["__version__"] = self.VERSION + + group.attrs["num_body_terms"] = self._num_body_terms + + dtype = h5py.vlen_dtype(np.dtype("int32")) + integrals_dset = group.create_dataset("integrals", (len(self.integrals),), dtype=dtype) + coeffs_dset = group.create_dataset("coefficients", (len(self.integrals),), dtype=float) + + for idx, ints in enumerate(self.integrals): + coeffs_dset[idx] = ints[0] + integrals_dset[idx] = list(ints[1]) + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> VibrationalIntegrals: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + integrals = [] + for coeff, ints in zip(h5py_group["coefficients"][...], h5py_group["integrals"][...]): + integrals.append((coeff, tuple(ints))) + + ret = VibrationalIntegrals(h5py_group.attrs["num_body_terms"], integrals) + + return ret + @staticmethod def set_truncation(max_num_entries: int) -> None: """Set the maximum number of integral values to display before truncation. @@ -107,6 +163,14 @@ def set_truncation(max_num_entries: int) -> None: """ VibrationalIntegrals._truncate = max_num_entries + @staticmethod + def _validate_num_body_terms(num_body_terms: int) -> None: + """Validates the number of body terms.""" + if num_body_terms < 1: + raise ValueError( + f"The number of body terms must be greater than 0, not '{num_body_terms}'." + ) + def to_basis(self) -> np.ndarray: """Maps the integrals into a basis which permits mapping into second-quantization. @@ -128,7 +192,7 @@ def to_basis(self) -> np.ndarray: # we can cache already evaluated integrals to improve cases in which a basis is very # expensive to compute - coeff_cache: Dict[Tuple[int, int, int, int, bool], Optional[float]] = {} + coeff_cache: dict[tuple[int, int, int, int, bool], Optional[float]] = {} for coeff0, indices in self._integrals: if len(set(indices)) != self._num_body_terms: @@ -145,7 +209,7 @@ def to_basis(self) -> np.ndarray: indices_np = np.absolute(indices_np) # the number of times which an index occurs corresponds to the power of the operator - powers: Dict[int, int] = Counter(indices_np) + powers: dict[int, int] = Counter(indices_np) index_list = [] @@ -226,7 +290,7 @@ def to_second_q_op(self) -> VibrationalOp: return VibrationalOp(labels, num_modes, num_modals_per_mode) @staticmethod - def _create_label_for_coeff(indices: List[Tuple[int, ...]]) -> str: + def _create_label_for_coeff(indices: list[tuple[int, ...]]) -> str: """Generates the operator label for the given indices. Args: diff --git a/qiskit_nature/properties/second_quantization/vibrational/occupied_modals.py b/qiskit_nature/properties/second_quantization/vibrational/occupied_modals.py index 0437a8da24..c0de80572e 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/occupied_modals.py +++ b/qiskit_nature/properties/second_quantization/vibrational/occupied_modals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,7 +12,11 @@ """The OccupiedModals property.""" -from typing import List, Optional, Tuple +from __future__ import annotations + +from typing import Optional + +import h5py from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import WatsonHamiltonian @@ -40,8 +44,23 @@ def __init__( """ super().__init__(self.__class__.__name__, basis) + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> OccupiedModals: + # pylint: disable=unused-argument + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + return OccupiedModals() + @classmethod - def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "OccupiedModals": + def from_legacy_driver_result(cls, result: LegacyDriverResult) -> OccupiedModals: """Construct an OccupiedModals instance from a :class:`~qiskit_nature.drivers.WatsonHamiltonian`. @@ -86,14 +105,13 @@ def _get_mode_op(self, mode: int) -> VibrationalOp: """ num_modals_per_mode = self.basis._num_modals_per_mode - labels: List[Tuple[str, complex]] = [] + labels: list[tuple[str, complex]] = [] for modal in range(num_modals_per_mode[mode]): labels.append((f"+_{mode}*{modal} -_{mode}*{modal}", 1.0)) return VibrationalOp(labels, len(num_modals_per_mode), num_modals_per_mode) - # TODO: refactor after closing https://github.com/Qiskit/qiskit-terra/issues/6772 def interpret(self, result: EigenstateResult) -> None: """Interprets an :class:`~qiskit_nature.results.EigenstateResult` in this property's context. diff --git a/qiskit_nature/properties/second_quantization/vibrational/types.py b/qiskit_nature/properties/second_quantization/vibrational/types.py index 45faf19f65..0da1e674df 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/types.py +++ b/qiskit_nature/properties/second_quantization/vibrational/types.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -14,9 +14,7 @@ from typing import Optional, TypeVar -from qiskit_nature import QiskitNatureError from .bases import VibrationalBasis -from ...property import PseudoProperty from ..second_quantized_property import SecondQuantizedProperty, GroupedSecondQuantizedProperty @@ -63,29 +61,14 @@ class GroupedVibrationalProperty(GroupedSecondQuantizedProperty[T], VibrationalP """A GroupedProperty subtype containing purely vibrational properties.""" @property - def basis(self) -> VibrationalBasis: + def basis(self) -> Optional[VibrationalBasis]: """Returns the basis.""" - return list(self._properties.values())[0].basis + for prop in self._properties.values(): + return prop.basis + return None @basis.setter def basis(self, basis: VibrationalBasis) -> None: """Sets the basis.""" for prop in self._properties.values(): prop.basis = basis - - def add_property(self, prop: Optional[T]) -> None: - """Adds a property to the group. - - Args: - prop: the property to be added. - - Raises: - QiskitNatureError: if the added property is not a vibrational one. - """ - if prop is not None: - if not isinstance(prop, (VibrationalProperty, PseudoProperty)): - raise QiskitNatureError( - f"{prop.__class__.__name__} is not an instance of `VibrationalProperty`, which " - "it must be in order to be added to an `GroupedVibrationalProperty`!" - ) - self._properties[prop.name] = prop diff --git a/qiskit_nature/properties/second_quantization/vibrational/vibrational_energy.py b/qiskit_nature/properties/second_quantization/vibrational/vibrational_energy.py index 1696bbbfee..429d4d30c0 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/vibrational_energy.py +++ b/qiskit_nature/properties/second_quantization/vibrational/vibrational_energy.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,7 +12,11 @@ """The VibrationalEnergy property.""" -from typing import cast, Dict, List, Optional, Tuple +from __future__ import annotations + +from typing import cast, Generator, Optional + +import h5py from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import WatsonHamiltonian @@ -34,7 +38,7 @@ class VibrationalEnergy(VibrationalProperty): def __init__( self, - vibrational_integrals: List[VibrationalIntegrals], + vibrational_integrals: list[VibrationalIntegrals], truncation_order: Optional[int] = None, basis: Optional[VibrationalBasis] = None, ) -> None: @@ -51,7 +55,7 @@ def __init__( be set before the second-quantized operator can be constructed. """ super().__init__(self.__class__.__name__, basis) - self._vibrational_integrals: Dict[int, VibrationalIntegrals] = {} + self._vibrational_integrals: dict[int, VibrationalIntegrals] = {} for integral in vibrational_integrals: self.add_vibrational_integral(integral) self._truncation_order = truncation_order @@ -72,8 +76,44 @@ def __str__(self) -> str: string += [f"\t{ints}"] return "\n".join(string) + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + ints_group = group.create_group("vibrational_integrals") + for integral in self._vibrational_integrals.values(): + integral.to_hdf5(ints_group) + + if self.truncation_order: + group.attrs["truncation_order"] = self.truncation_order + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> VibrationalEnergy: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + ints = [] + for int_group in h5py_group["vibrational_integrals"].values(): + ints.append(VibrationalIntegrals.from_hdf5(int_group)) + + return VibrationalEnergy(ints, h5py_group.attrs.get("truncation_order", None)) + @classmethod - def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "VibrationalEnergy": + def from_legacy_driver_result(cls, result: LegacyDriverResult) -> VibrationalEnergy: """Construct a VibrationalEnergy instance from a :class:`~qiskit_nature.drivers.WatsonHamiltonian`. @@ -91,7 +131,7 @@ def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "VibrationalEn w_h = cast(WatsonHamiltonian, result) - sorted_integrals: Dict[int, List[Tuple[float, Tuple[int, ...]]]] = {1: [], 2: [], 3: []} + sorted_integrals: dict[int, list[tuple[float, tuple[int, ...]]]] = {1: [], 2: [], 3: []} for coeff, *indices in w_h.data: ints = [int(i) for i in indices] num_body = len(set(ints)) @@ -101,6 +141,18 @@ def from_legacy_driver_result(cls, result: LegacyDriverResult) -> "VibrationalEn [VibrationalIntegrals(num_body, ints) for num_body, ints in sorted_integrals.items()] ) + def __iter__(self) -> Generator[VibrationalIntegrals, None, None]: + """Returns the generator-iterator method.""" + return self._generator() + + def _generator(self) -> Generator[VibrationalIntegrals, None, None]: + """A generator-iterator method [1] iterating over all internal ``VibrationalIntegrals``. + + [1]: https://docs.python.org/3/reference/expressions.html#generator-iterator-methods + """ + for ints in self._vibrational_integrals.values(): + yield ints + def add_vibrational_integral(self, integral: VibrationalIntegrals) -> None: # pylint: disable=line-too-long """Adds a diff --git a/qiskit_nature/properties/second_quantization/vibrational/vibrational_structure_driver_result.py b/qiskit_nature/properties/second_quantization/vibrational/vibrational_structure_driver_result.py index ba6c85b3d8..9e2a4f99fe 100644 --- a/qiskit_nature/properties/second_quantization/vibrational/vibrational_structure_driver_result.py +++ b/qiskit_nature/properties/second_quantization/vibrational/vibrational_structure_driver_result.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,13 +12,18 @@ """The VibrationalStructureDriverResult class.""" +from __future__ import annotations + from typing import cast +import h5py + from qiskit_nature import ListOrDictType, settings from qiskit_nature.drivers import WatsonHamiltonian from qiskit_nature.operators.second_quantization import VibrationalOp from ..second_quantized_property import LegacyDriverResult +from .bases import VibrationalBasis from .occupied_modals import OccupiedModals from .vibrational_energy import VibrationalEnergy from .types import GroupedVibrationalProperty @@ -48,10 +53,55 @@ def num_modes(self, num_modes: int) -> None: """Sets the number of modes.""" self._num_modes = num_modes + def to_hdf5(self, parent: h5py.Group) -> None: + """Stores this instance in an HDF5 group inside of the provided parent group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.to_hdf5` for more details. + + Args: + parent: the parent HDF5 group. + """ + super().to_hdf5(parent) + group = parent.require_group(self.name) + + group.attrs["num_modes"] = self.num_modes + + if self.basis is not None: + self.basis.to_hdf5(group) + + @staticmethod + def from_hdf5(h5py_group: h5py.Group) -> VibrationalStructureDriverResult: + """Constructs a new instance from the data stored in the provided HDF5 group. + + See also :func:`~qiskit_nature.hdf5.HDF5Storable.from_hdf5` for more details. + + Args: + h5py_group: the HDF5 group from which to load the data. + + Returns: + A new instance of this class. + """ + grouped_property = GroupedVibrationalProperty.from_hdf5(h5py_group) + + basis: VibrationalBasis = None + ret = VibrationalStructureDriverResult() + for prop in grouped_property: + if isinstance(prop, VibrationalBasis): + basis = prop + continue + ret.add_property(prop) + + ret.num_modes = h5py_group.attrs["num_modes"] + + if basis is not None: + ret.basis = basis + + return ret + @classmethod def from_legacy_driver_result( cls, result: LegacyDriverResult - ) -> "VibrationalStructureDriverResult": + ) -> VibrationalStructureDriverResult: """Converts a :class:`~qiskit_nature.drivers.WatsonHamiltonian` into an ``VibrationalStructureDriverResult``. diff --git a/qiskit_nature/transformers/second_quantization/electronic/active_space_transformer.py b/qiskit_nature/transformers/second_quantization/electronic/active_space_transformer.py index 051e189dfe..a6047e6474 100644 --- a/qiskit_nature/transformers/second_quantization/electronic/active_space_transformer.py +++ b/qiskit_nature/transformers/second_quantization/electronic/active_space_transformer.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -384,7 +384,7 @@ def _transform_property(self, prop: Property) -> Property: try: transformed_internal_property = self._transform_property(internal_property) - except NotImplementedError: + except TypeError: logger.warning( "The Property %s of type %s could not be transformed! Thus, it will not be " "included in the simulation from here onwards.", diff --git a/releasenotes/notes/hdf5-integration-b08c31426d1fdf9e.yaml b/releasenotes/notes/hdf5-integration-b08c31426d1fdf9e.yaml new file mode 100644 index 0000000000..388e24dddc --- /dev/null +++ b/releasenotes/notes/hdf5-integration-b08c31426d1fdf9e.yaml @@ -0,0 +1,25 @@ +--- +features: + - | + Adds a new HDF5-integration to support storing and loading of (mostly) + Property objects using HDF5 files. A similar feature existed in the legacy + QMolecule object but the new implementation is handled more general to + enable leveraging this integration throughout more parts of the stack in the + future. + + To store a driver result of the new drivers in a file you can do: + + .. code-block:: python + + from qiskit_nature.hdf5 import save_to_hdf5 + + my_driver_result = driver.run() + save_to_hdf5(my_driver_result, "my_driver_result.hdf5") + + and to load it again you would do: + + .. code-block:: python + + from qiskit_nature.hdf5 import load_from_hdf5 + + my_driver_result = load_from_hdf5("my_driver_result.hdf5") diff --git a/requirements.txt b/requirements.txt index 6da25594d1..5a83555407 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,6 @@ numpy>=1.17 psutil>=5 scikit-learn>=0.20.0 setuptools>=40.1.0 +typing_extensions h5py retworkx>=0.10.1 diff --git a/test/properties/property_test.py b/test/properties/property_test.py new file mode 100644 index 0000000000..c60aafdde4 --- /dev/null +++ b/test/properties/property_test.py @@ -0,0 +1,212 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""PropertyTest class""" + +from test import QiskitNatureTestCase + +import numpy as np + +from qiskit_nature.properties.second_quantization.driver_metadata import DriverMetadata +from qiskit_nature.properties.second_quantization.electronic import ( + AngularMomentum, + DipoleMoment, + ElectronicDipoleMoment, + ElectronicEnergy, + Magnetization, + ParticleNumber, +) +from qiskit_nature.properties.second_quantization.electronic.bases import ElectronicBasisTransform +from qiskit_nature.properties.second_quantization.electronic.integrals import ( + ElectronicIntegrals, + OneBodyElectronicIntegrals, + TwoBodyElectronicIntegrals, +) +from qiskit_nature.properties.second_quantization.vibrational import ( + OccupiedModals, + VibrationalEnergy, +) +from qiskit_nature.properties.second_quantization.vibrational.integrals import VibrationalIntegrals + + +class PropertyTest(QiskitNatureTestCase): + """Property instance tester""" + + def compare_angular_momentum( + self, first: AngularMomentum, second: AngularMomentum, msg: str = None + ) -> None: + """Compares two AngularMomentum instances.""" + if first.num_spin_orbitals != second.num_spin_orbitals: + raise self.failureException(msg) + if first.spin != second.spin: + raise self.failureException(msg) + if not np.isclose(first.absolute_tolerance, second.absolute_tolerance): + raise self.failureException(msg) + if not np.isclose(first.relative_tolerance, second.relative_tolerance): + raise self.failureException(msg) + + def compare_electronic_dipole_moment( + self, first: ElectronicDipoleMoment, second: ElectronicDipoleMoment, msg: str = None + ) -> None: + """Compares two ElectronicDipoleMoment instances.""" + for f_axis in iter(first): + s_axis = second.get_property(f_axis.name) + self.assertEqual(f_axis, s_axis) + + if first.reverse_dipole_sign != second.reverse_dipole_sign: + raise self.failureException(msg) + + if not np.allclose(first.nuclear_dipole_moment, second.nuclear_dipole_moment): + raise self.failureException(msg) + + def compare_dipole_moment( + self, first: DipoleMoment, second: DipoleMoment, msg: str = None + ) -> None: + """Compares two DipoleMoment instances.""" + if first.axis != second.axis: + raise self.failureException(msg) + + for f_ints, s_ints in zip(first, second): + self.compare_electronic_integral(f_ints, s_ints) + + def compare_electronic_energy( + self, first: ElectronicEnergy, second: ElectronicEnergy, msg: str = None + ) -> None: + """Compares two ElectronicEnergy instances.""" + for f_ints, s_ints in zip(first, second): + self.compare_electronic_integral(f_ints, s_ints) + + if not np.isclose(first.nuclear_repulsion_energy, second.nuclear_repulsion_energy): + raise self.failureException(msg) + if not np.isclose(first.reference_energy, second.reference_energy): + raise self.failureException(msg) + if not np.allclose(first.orbital_energies, second.orbital_energies): + raise self.failureException(msg) + + self.assertEqual(first.overlap, second.overlap) + self.assertEqual(first.kinetic, second.kinetic) + + def compare_magnetization( + self, first: Magnetization, second: Magnetization, msg: str = None + ) -> None: + """Compares two Magnetization instances.""" + if first.num_spin_orbitals != second.num_spin_orbitals: + raise self.failureException(msg) + + def compare_particle_number( + self, first: ParticleNumber, second: ParticleNumber, msg: str = None + ) -> None: + """Compares two ParticleNumber instances.""" + if first.num_spin_orbitals != second.num_spin_orbitals: + raise self.failureException(msg) + if first.num_alpha != second.num_alpha: + raise self.failureException(msg) + if first.num_beta != second.num_beta: + raise self.failureException(msg) + if not np.allclose(first.occupation_alpha, second.occupation_alpha): + raise self.failureException(msg) + if not np.allclose(first.occupation_beta, second.occupation_beta): + raise self.failureException(msg) + if not np.isclose(first.absolute_tolerance, second.absolute_tolerance): + raise self.failureException(msg) + if not np.isclose(first.relative_tolerance, second.relative_tolerance): + raise self.failureException(msg) + + def compare_driver_metadata( + self, first: DriverMetadata, second: DriverMetadata, msg: str = None + ) -> None: + """Compares two DriverMetadata instances.""" + if first.program != second.program: + raise self.failureException(msg) + if first.version != second.version: + raise self.failureException(msg) + if first.config != second.config: + raise self.failureException(msg) + + def compare_electronic_basis_transform( + self, first: ElectronicBasisTransform, second: ElectronicBasisTransform, msg: str = None + ) -> None: + """Compares two ElectronicBasisTransform instances.""" + if first.initial_basis != second.initial_basis: + raise self.failureException(msg) + if first.final_basis != second.final_basis: + raise self.failureException(msg) + if not np.allclose(first.coeff_alpha, second.coeff_alpha): + raise self.failureException(msg) + if not np.allclose(first.coeff_beta, second.coeff_beta): + raise self.failureException(msg) + + def compare_electronic_integral( + self, first: ElectronicIntegrals, second: ElectronicIntegrals, msg: str = None + ) -> None: + """Compares two ElectronicIntegrals instances.""" + if first.name != second.name: + raise self.failureException(msg) + if first.basis != second.basis: + raise self.failureException(msg) + if first.num_body_terms != second.num_body_terms: + raise self.failureException(msg) + if not np.isclose(first.threshold, second.threshold): + raise self.failureException(msg) + for f_mat, s_mat in zip(first, second): + if not np.allclose(f_mat, s_mat): + raise self.failureException(msg) + + def compare_vibrational_integral( + self, first: VibrationalIntegrals, second: VibrationalIntegrals, msg: str = None + ) -> None: + """Compares two VibrationalIntegral instances.""" + if first.name != second.name: + raise self.failureException(msg) + + if first.num_body_terms != second.num_body_terms: + raise self.failureException(msg) + + for f_int, s_int in zip(first.integrals, second.integrals): + if not np.isclose(f_int[0], s_int[0]): + raise self.failureException(msg) + + if not all(f == s for f, s in zip(f_int[1:], s_int[1:])): + raise self.failureException(msg) + + def compare_vibrational_energy( + self, first: VibrationalEnergy, second: VibrationalEnergy, msg: str = None + ) -> None: + # pylint: disable=unused-argument + """Compares two VibrationalEnergy instances.""" + for f_ints, s_ints in zip(first, second): + self.compare_vibrational_integral(f_ints, s_ints) + + def compare_occupied_modals( + self, first: OccupiedModals, second: OccupiedModals, msg: str = None + ) -> None: + # pylint: disable=unused-argument + """Compares two OccupiedModals instances.""" + pass + + def setUp(self) -> None: + """Setup expected object.""" + super().setUp() + self.addTypeEqualityFunc(AngularMomentum, self.compare_angular_momentum) + self.addTypeEqualityFunc(DipoleMoment, self.compare_dipole_moment) + self.addTypeEqualityFunc(ElectronicDipoleMoment, self.compare_electronic_dipole_moment) + self.addTypeEqualityFunc(ElectronicEnergy, self.compare_electronic_energy) + self.addTypeEqualityFunc(Magnetization, self.compare_magnetization) + self.addTypeEqualityFunc(ParticleNumber, self.compare_particle_number) + self.addTypeEqualityFunc(DriverMetadata, self.compare_driver_metadata) + self.addTypeEqualityFunc(ElectronicBasisTransform, self.compare_electronic_basis_transform) + self.addTypeEqualityFunc(ElectronicIntegrals, self.compare_electronic_integral) + self.addTypeEqualityFunc(OneBodyElectronicIntegrals, self.compare_electronic_integral) + self.addTypeEqualityFunc(TwoBodyElectronicIntegrals, self.compare_electronic_integral) + self.addTypeEqualityFunc(VibrationalIntegrals, self.compare_vibrational_integral) + self.addTypeEqualityFunc(VibrationalEnergy, self.compare_vibrational_energy) + self.addTypeEqualityFunc(OccupiedModals, self.compare_occupied_modals) diff --git a/test/properties/second_quantization/electronic/integrals/test_integral_property.py b/test/properties/second_quantization/electronic/integrals/test_integral_property.py index bc2d4da519..1682a67164 100644 --- a/test/properties/second_quantization/electronic/integrals/test_integral_property.py +++ b/test/properties/second_quantization/electronic/integrals/test_integral_property.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,8 +12,10 @@ """Test IntegralProperty""" -from test import QiskitNatureTestCase +import tempfile +from test.properties.property_test import PropertyTest +import h5py import numpy as np from qiskit_nature.properties.second_quantization.electronic.bases import ( @@ -27,7 +29,7 @@ ) -class TestIntegralProperty(QiskitNatureTestCase): +class TestIntegralProperty(PropertyTest): """Test IntegralProperty Property""" def setUp(self): @@ -106,3 +108,23 @@ def test_second_q_ops(self): ("+_0 -_0 +_2 -_2", (1 + 0j)), ] self.assertEqual(second_q_ops[0].to_list(), expected) + + def test_to_hdf5(self): + """Test to_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = IntegralProperty.from_hdf5(file["test"]) + + self.assertDictEqual(self.prop._shift, read_prop._shift) + + for f_int, s_int in zip(iter(self.prop), iter(read_prop)): + self.assertEqual(f_int, s_int) diff --git a/test/properties/second_quantization/electronic/integrals/test_one_body_electronic_integrals.py b/test/properties/second_quantization/electronic/integrals/test_one_body_electronic_integrals.py index 493f148905..cbc8749d99 100644 --- a/test/properties/second_quantization/electronic/integrals/test_one_body_electronic_integrals.py +++ b/test/properties/second_quantization/electronic/integrals/test_one_body_electronic_integrals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,8 +12,10 @@ """Test OneBodyElectronicIntegrals.""" -from test import QiskitNatureTestCase +import tempfile +from test.properties.property_test import PropertyTest +import h5py import numpy as np from qiskit_nature import QiskitNatureError @@ -26,7 +28,7 @@ ) -class TestOneBodyElectronicIntegrals(QiskitNatureTestCase): +class TestOneBodyElectronicIntegrals(PropertyTest): """Test OneBodyElectronicIntegrals.""" def test_init(self): @@ -207,3 +209,28 @@ def test_compose(self): self.assertTrue(isinstance(composition, complex)) self.assertAlmostEqual(composition, expected) + + def test_to_hdf5(self): + """Test to_hdf5.""" + random = np.random.rand(2, 2) + + ints = OneBodyElectronicIntegrals(ElectronicBasis.MO, (random, random)) + + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + ints.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + random = np.random.rand(2, 2) + + ints = OneBodyElectronicIntegrals(ElectronicBasis.MO, (random, random)) + + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + ints.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + new_ints = OneBodyElectronicIntegrals.from_hdf5(file["OneBodyElectronicIntegrals"]) + + self.assertEqual(ints, new_ints) diff --git a/test/properties/second_quantization/electronic/integrals/test_two_body_electronic_integrals.py b/test/properties/second_quantization/electronic/integrals/test_two_body_electronic_integrals.py index b6ef1b27ca..3ab349e9d5 100644 --- a/test/properties/second_quantization/electronic/integrals/test_two_body_electronic_integrals.py +++ b/test/properties/second_quantization/electronic/integrals/test_two_body_electronic_integrals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -13,8 +13,10 @@ """Test TwoBodyElectronicIntegrals.""" import json -from test import QiskitNatureTestCase +import tempfile +from test.properties.property_test import PropertyTest +import h5py import numpy as np from qiskit_nature import QiskitNatureError @@ -28,7 +30,7 @@ ) -class TestTwoBodyElectronicIntegrals(QiskitNatureTestCase): +class TestTwoBodyElectronicIntegrals(PropertyTest): """Test TwoBodyElectronicIntegrals.""" def test_init(self): @@ -229,3 +231,28 @@ def test_compose(self): self.assertTrue(isinstance(composition, OneBodyElectronicIntegrals)) self.assertTrue(composition._basis, ElectronicBasis.AO) self.assertTrue(np.allclose(composition._matrices[0], expected)) + + def test_to_hdf5(self): + """Test to_hdf5.""" + random = np.random.rand(2, 2, 2, 2) + + ints = TwoBodyElectronicIntegrals(ElectronicBasis.MO, (random, random, random, random)) + + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + ints.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + random = np.random.rand(2, 2, 2, 2) + + ints = TwoBodyElectronicIntegrals(ElectronicBasis.MO, (random, random, random, random)) + + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + ints.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + new_ints = TwoBodyElectronicIntegrals.from_hdf5(file["TwoBodyElectronicIntegrals"]) + + self.assertEqual(ints, new_ints) diff --git a/test/properties/second_quantization/electronic/resources/electronic_structure_driver_result.hdf5 b/test/properties/second_quantization/electronic/resources/electronic_structure_driver_result.hdf5 new file mode 100644 index 0000000000..54461d7df8 Binary files /dev/null and b/test/properties/second_quantization/electronic/resources/electronic_structure_driver_result.hdf5 differ diff --git a/test/properties/second_quantization/electronic/test_angular_momentum.py b/test/properties/second_quantization/electronic/test_angular_momentum.py index 4f8f475dfc..dcddf23e24 100644 --- a/test/properties/second_quantization/electronic/test_angular_momentum.py +++ b/test/properties/second_quantization/electronic/test_angular_momentum.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -13,16 +13,18 @@ """Test AngularMomentum Property""" import json +import tempfile import warnings -from test import QiskitNatureTestCase +from test.properties.property_test import PropertyTest +import h5py import numpy as np from qiskit_nature.drivers import QMolecule from qiskit_nature.properties.second_quantization.electronic import AngularMomentum -class TestAngularMomentum(QiskitNatureTestCase): +class TestAngularMomentum(PropertyTest): """Test AngularMomentum Property""" def setUp(self): @@ -49,3 +51,20 @@ def test_second_q_ops(self): for op, expected_op in zip(ops[0].to_list(), expected): self.assertEqual(op[0], expected_op[0]) self.assertTrue(np.isclose(op[1], expected_op[1])) + + def test_to_hdf5(self): + """Test to_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = AngularMomentum.from_hdf5(file["AngularMomentum"]) + + self.assertEqual(self.prop, read_prop) diff --git a/test/properties/second_quantization/electronic/test_dipole_moment.py b/test/properties/second_quantization/electronic/test_dipole_moment.py index 518fa309cf..6f392137ba 100644 --- a/test/properties/second_quantization/electronic/test_dipole_moment.py +++ b/test/properties/second_quantization/electronic/test_dipole_moment.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -13,8 +13,10 @@ """Test DipoleMoment Property""" import json -from test import QiskitNatureTestCase +import tempfile +from test.properties.property_test import PropertyTest +import h5py import numpy as np from qiskit_nature.drivers.second_quantization import HDF5Driver @@ -26,7 +28,7 @@ ) -class TestElectronicDipoleMoment(QiskitNatureTestCase): +class TestElectronicDipoleMoment(PropertyTest): """Test ElectronicDipoleMoment Property""" def setUp(self): @@ -56,8 +58,25 @@ def test_second_q_ops(self): self.assertEqual(truth[0], exp[0]) self.assertTrue(np.isclose(truth[1], exp[1])) + def test_to_hdf5(self): + """Test to_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) -class TestDipoleMoment(QiskitNatureTestCase): + def test_from_hdf5(self): + """Test from_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = ElectronicDipoleMoment.from_hdf5(file["ElectronicDipoleMoment"]) + + self.assertEqual(self.prop, read_prop) + + +class TestDipoleMoment(PropertyTest): """Test DipoleMoment Property""" def test_integral_operator(self): @@ -67,3 +86,26 @@ def test_integral_operator(self): matrix_op = prop.integral_operator(None) # the matrix-operator of the dipole moment is unaffected by the density! self.assertTrue(np.allclose(random, matrix_op._matrices[0])) + + def test_to_hdf5(self): + """Test to_hdf5.""" + random = np.random.random((4, 4)) + prop = DipoleMoment("x", [OneBodyElectronicIntegrals(ElectronicBasis.AO, (random, None))]) + + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + prop.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + random = np.random.random((4, 4)) + prop = DipoleMoment("x", [OneBodyElectronicIntegrals(ElectronicBasis.AO, (random, None))]) + + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + prop.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = DipoleMoment.from_hdf5(file["DipoleMomentX"]) + + self.assertEqual(prop, read_prop) diff --git a/test/properties/second_quantization/electronic/test_electronic_energy.py b/test/properties/second_quantization/electronic/test_electronic_energy.py index 2e3d6b645d..7c5a5a60b3 100644 --- a/test/properties/second_quantization/electronic/test_electronic_energy.py +++ b/test/properties/second_quantization/electronic/test_electronic_energy.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -13,9 +13,11 @@ """Test ElectronicEnergy Property""" import json -from test import QiskitNatureTestCase +import tempfile +from test.properties.property_test import PropertyTest from typing import cast +import h5py import numpy as np from qiskit_nature.drivers.second_quantization import HDF5Driver @@ -29,7 +31,7 @@ ) -class TestElectronicEnergy(QiskitNatureTestCase): +class TestElectronicEnergy(PropertyTest): """Test ElectronicEnergy Property""" def setUp(self): @@ -144,3 +146,20 @@ def test_from_raw_integrals(self): prop.get_electronic_integral(ElectronicBasis.MO, 2)._matrices[3], two_body_ba.T ) ) + + def test_to_hdf5(self): + """Test to_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = ElectronicEnergy.from_hdf5(file["ElectronicEnergy"]) + + self.assertEqual(self.prop, read_prop) diff --git a/test/properties/second_quantization/electronic/test_electronic_structure_driver_result.py b/test/properties/second_quantization/electronic/test_electronic_structure_driver_result.py new file mode 100644 index 0000000000..ef0585d19c --- /dev/null +++ b/test/properties/second_quantization/electronic/test_electronic_structure_driver_result.py @@ -0,0 +1,52 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Test ElectronicStructureDriverResult Property""" + +from test.properties.property_test import PropertyTest + +import h5py + +from qiskit_nature.drivers.second_quantization import HDF5Driver +from qiskit_nature.properties.second_quantization.electronic import ( + ElectronicStructureDriverResult, +) + + +class TestElectronicStructureDriverResult(PropertyTest): + """Test ElectronicStructureDriverResult Property""" + + def setUp(self) -> None: + """Setup expected object.""" + super().setUp() + + driver = HDF5Driver( + self.get_resource_path( + "BeH_sto3g_reduced.hdf5", "transformers/second_quantization/electronic" + ) + ) + self.expected = driver.run() + + def test_from_hdf5(self): + """Test from_hdf5.""" + with h5py.File( + self.get_resource_path( + "electronic_structure_driver_result.hdf5", + "properties/second_quantization/electronic/resources", + ), + "r", + ) as file: + for group in file.values(): + prop = ElectronicStructureDriverResult.from_hdf5(group) + for inner_prop in iter(prop): + expected = self.expected.get_property(type(inner_prop)) + self.assertEqual(inner_prop, expected) diff --git a/test/properties/second_quantization/electronic/test_magnetization.py b/test/properties/second_quantization/electronic/test_magnetization.py index d4dfdb647c..a671a9d5cb 100644 --- a/test/properties/second_quantization/electronic/test_magnetization.py +++ b/test/properties/second_quantization/electronic/test_magnetization.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,14 +12,17 @@ """Test Magnetization Property""" +import tempfile import warnings -from test import QiskitNatureTestCase +from test.properties.property_test import PropertyTest + +import h5py from qiskit_nature.drivers import QMolecule from qiskit_nature.properties.second_quantization.electronic import Magnetization -class TestMagnetization(QiskitNatureTestCase): +class TestMagnetization(PropertyTest): """Test Magnetization Property""" def setUp(self): @@ -46,3 +49,20 @@ def test_second_q_ops(self): ("+_7 -_7", -0.5), ] self.assertEqual(ops[0].to_list(), expected) + + def test_to_hdf5(self): + """Test to_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = Magnetization.from_hdf5(file["Magnetization"]) + + self.assertEqual(self.prop, read_prop) diff --git a/test/properties/second_quantization/electronic/test_particle_number.py b/test/properties/second_quantization/electronic/test_particle_number.py index eea27f63b7..d59aaa1521 100644 --- a/test/properties/second_quantization/electronic/test_particle_number.py +++ b/test/properties/second_quantization/electronic/test_particle_number.py @@ -12,16 +12,18 @@ """Test ParticleNumber Property""" +import tempfile import warnings -from test import QiskitNatureTestCase +from test.properties.property_test import PropertyTest +import h5py import numpy as np from qiskit_nature.drivers import QMolecule from qiskit_nature.properties.second_quantization.electronic import ParticleNumber -class TestParticleNumber(QiskitNatureTestCase): +class TestParticleNumber(PropertyTest): """Test ParticleNumber Property""" def setUp(self): @@ -56,3 +58,20 @@ def test_non_singlet_occupation(self): prop = ParticleNumber(4, (2, 1), [2.0, 1.0]) self.assertTrue(np.allclose(prop.occupation_alpha, [1.0, 1.0])) self.assertTrue(np.allclose(prop.occupation_beta, [1.0, 0.0])) + + def test_to_hdf5(self): + """Test to_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = ParticleNumber.from_hdf5(file["ParticleNumber"]) + + self.assertEqual(self.prop, read_prop) diff --git a/test/properties/second_quantization/vibrational/bases/test_harmonic_basis.py b/test/properties/second_quantization/vibrational/bases/test_harmonic_basis.py index e33cd65cca..b3b9580c29 100644 --- a/test/properties/second_quantization/vibrational/bases/test_harmonic_basis.py +++ b/test/properties/second_quantization/vibrational/bases/test_harmonic_basis.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -13,9 +13,11 @@ """Test HarmonicBasis.""" import json +import tempfile from test import QiskitNatureTestCase from ddt import ddt, data, unpack +import h5py import numpy as np from qiskit_nature.properties.second_quantization.vibrational.bases import HarmonicBasis @@ -114,3 +116,32 @@ def test_harmonic_basis(self, num_body, integrals): for (real_label, real_coeff), (exp_label, exp_coeff) in zip(op.to_list(), operator): self.assertEqual(real_label, exp_label) self.assertTrue(np.isclose(real_coeff, exp_coeff)) + + def test_to_hdf5(self): + """Test to_hdf5.""" + num_modes = 4 + num_modals = 2 + num_modals_per_mode = [num_modals] * num_modes + basis = HarmonicBasis(num_modals_per_mode) + + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + basis.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + num_modes = 4 + num_modals = 2 + num_modals_per_mode = [num_modals] * num_modes + basis = HarmonicBasis(num_modals_per_mode) + + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + basis.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = HarmonicBasis.from_hdf5(file["HarmonicBasis"]) + + self.assertTrue( + np.allclose(basis.num_modals_per_mode, read_prop.num_modals_per_mode) + ) diff --git a/test/properties/second_quantization/vibrational/resources/vibrational_structure_driver_result.hdf5 b/test/properties/second_quantization/vibrational/resources/vibrational_structure_driver_result.hdf5 new file mode 100644 index 0000000000..0925ae2571 Binary files /dev/null and b/test/properties/second_quantization/vibrational/resources/vibrational_structure_driver_result.hdf5 differ diff --git a/test/properties/second_quantization/vibrational/test_occupied_modals.py b/test/properties/second_quantization/vibrational/test_occupied_modals.py index 0fe2571914..6781420456 100644 --- a/test/properties/second_quantization/vibrational/test_occupied_modals.py +++ b/test/properties/second_quantization/vibrational/test_occupied_modals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -12,15 +12,18 @@ """Test OccupiedModals Property""" +import tempfile import warnings -from test import QiskitNatureTestCase +from test.properties.property_test import PropertyTest + +import h5py from qiskit_nature.drivers import WatsonHamiltonian from qiskit_nature.properties.second_quantization.vibrational import OccupiedModals from qiskit_nature.properties.second_quantization.vibrational.bases import HarmonicBasis -class TestOccupiedModals(QiskitNatureTestCase): +class TestOccupiedModals(PropertyTest): """Test OccupiedModals Property""" def setUp(self): @@ -48,3 +51,20 @@ def test_second_q_ops(self): ] for op, expected_op_list in zip(ops, expected): self.assertEqual(op.to_list(), expected_op_list) + + def test_to_hdf5(self): + """Test to_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = OccupiedModals.from_hdf5(file["OccupiedModals"]) + + self.assertEqual(self.prop, read_prop) diff --git a/test/properties/second_quantization/vibrational/test_vibrational_energy.py b/test/properties/second_quantization/vibrational/test_vibrational_energy.py index 5fe2725567..296a5ab367 100644 --- a/test/properties/second_quantization/vibrational/test_vibrational_energy.py +++ b/test/properties/second_quantization/vibrational/test_vibrational_energy.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 2022. # # 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 @@ -13,9 +13,11 @@ """Test VibrationalEnergy Property""" import json +import tempfile import warnings -from test import QiskitNatureTestCase +from test.properties.property_test import PropertyTest +import h5py import numpy as np from qiskit_nature.drivers import WatsonHamiltonian @@ -23,7 +25,7 @@ from qiskit_nature.properties.second_quantization.vibrational.bases import HarmonicBasis -class TestVibrationalEnergy(QiskitNatureTestCase): +class TestVibrationalEnergy(PropertyTest): """Test VibrationalEnergy Property""" def setUp(self): @@ -80,3 +82,20 @@ def test_second_q_ops(self): for op, expected_op in zip(ops[0].to_list(), expected): self.assertEqual(op[0], expected_op[0]) self.assertTrue(np.isclose(op[1], expected_op[1])) + + def test_to_hdf5(self): + """Test to_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + def test_from_hdf5(self): + """Test from_hdf5.""" + with tempfile.TemporaryFile() as tmp_file: + with h5py.File(tmp_file, "w") as file: + self.prop.to_hdf5(file) + + with h5py.File(tmp_file, "r") as file: + read_prop = VibrationalEnergy.from_hdf5(file["VibrationalEnergy"]) + + self.assertEqual(self.prop, read_prop) diff --git a/test/properties/second_quantization/vibrational/test_vibrational_structure_driver_result.py b/test/properties/second_quantization/vibrational/test_vibrational_structure_driver_result.py new file mode 100644 index 0000000000..2db0f50cfc --- /dev/null +++ b/test/properties/second_quantization/vibrational/test_vibrational_structure_driver_result.py @@ -0,0 +1,54 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Test VibrationalStructureDriverResult Property""" + +from test.properties.property_test import PropertyTest + +import h5py + +from qiskit_nature.drivers.second_quantization import GaussianForcesDriver +from qiskit_nature.properties.second_quantization.vibrational import ( + VibrationalStructureDriverResult, +) +from qiskit_nature.properties.second_quantization.vibrational.bases import HarmonicBasis + + +class TestVibrationalStructureDriverResult(PropertyTest): + """Test VibrationalStructureDriverResult Property""" + + def setUp(self) -> None: + """Setup expected object.""" + super().setUp() + + driver = GaussianForcesDriver( + logfile=self.get_resource_path( + "test_driver_gaussian_log_C01.txt", "drivers/second_quantization/gaussiand" + ) + ) + self.expected = driver.run() + self.expected.basis = HarmonicBasis([3]) + + def test_from_hdf5(self): + """Test from_hdf5.""" + with h5py.File( + self.get_resource_path( + "vibrational_structure_driver_result.hdf5", + "properties/second_quantization/vibrational/resources", + ), + "r", + ) as file: + for group in file.values(): + prop = VibrationalStructureDriverResult.from_hdf5(group) + for inner_prop in iter(prop): + expected = self.expected.get_property(type(inner_prop)) + self.assertEqual(inner_prop, expected)