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 Software | Version |
---|
qiskit-terra | 0.20.0.dev0+7af16a3 |
qiskit-aer | 0.10.2 |
qiskit-ignis | 0.7.0 |
qiskit-nature | 0.4.0 |
qiskit-finance | 0.4.0 |
qiskit-optimization | 0.4.0 |
qiskit-machine-learning | 0.3.0 |
System information |
---|
Python version | 3.8.12 |
Python compiler | Clang 10.0.0 |
Python build | default, Oct 12 2021 06:23:56 |
OS | Darwin |
CPUs | 2 |
Memory (Gb) | 12.0 |
Fri Feb 04 13:55:22 2022 EST |
"
+ "Version Information
Qiskit Software | Version |
---|
qiskit-terra | 0.20.0.dev0+9a743fb |
qiskit-aer | 0.11.0 |
qiskit-ignis | 0.7.0 |
qiskit-ibmq-provider | 0.19.0.dev0+8455b01 |
qiskit-nature | 0.4.0 |
System information |
---|
Python version | 3.9.9 |
Python compiler | GCC 11.2.1 20210728 (Red Hat 11.2.1-1) |
Python build | main, Nov 19 2021 00:00:00 |
OS | Linux |
CPUs | 4 |
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)