Skip to content

Commit

Permalink
HDF5-Property framework integration (qiskit-community#461)
Browse files Browse the repository at this point in the history
* WIP: HDF5-integration for property framework

This is an initial proof-of-concept implementation.
This needs to be reviewed extensively.

* Clean up interface

* refactor: let ElectronicIntegrals derive from PseudoProperty

* refactor: let Molecule derive from PseudoProperty

* feat: implement DriverMetadata HDF5 methods

* feat: add toggle to include PseudoProperty objects in GroupedProperty iteration

* feat: implement ParticleNumber HDF5 methods

* feat: implement AngularMomentum and Magnetization HDF5 methods

* feat: implement ElectronicBasisTransform HDF5 methods

* fix: lint

* fix: fix unittests

* remove comment

* fix: spell

* feat: also store Qiskit Nature version

* handle potential error cases in Property.import_and_build_from_hdf5

* Fix copyright

* feat: introduce individual version numbers per Property class

* WIP: HDF5 integration into vibrational properties

* Fix copyright

* fix: property tutorial

* Update typehints

* Run black

* Remove @AbstractMethod from Property.from_hdf5

This needs to be removed in order to ensure that the stable tutorials
remain working.

* Add more missing typehints

* refactor: introduce HDF5Storable Protocol

* refactor: remove PseudoProperty in favor of Interpretable Protocol

The PseudoProperty class effectively removes everything which defines
the Property class (the interpret method). So instead of having such a
pseudo-class, all previous PseudoProperty subclass are now directly
Property subclasses and the `interpret()` method existence is handled
via the `Interpretable` Protocol.

* Fix linters

* Fix ASTransformer caught error type

* Fix copyright

* Fix imports

* More guards against Property type

* More import fixes

* fix: property tutorial

* fix: ElectronicStructureDriverResult.__str__

* refactor: remove Property base class where not needed

* test: basic hdf5 method unittests

* test: *StructureDriverResult from_hdf5 methods

* docs: HDF5 documentation

* refactor: formally deprecate PseudoProperty class

* docs: actual HDF5 save and load examples

* Fix spell

* fix: avoid name clash with multiple atoms of same kind

* fix: ElectronicEnergy.from_hdf5 group access

* Update unittest HDF5 resource

* fix: ParticleNumber.from_hdf5 occupation dataset access

* Fix qiskit-community#519

This is actually required for the matrices loaded during
ElectronicIntegrals.from_hdf5 to be in the correct order!

* Update qiskit_nature/properties/property.py

Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com>

* Update docs

* Rename save_to_hdf5(..., force -> replace)

* refactor: fix DriverMetadata HDF5 attribute names

* refactor: make from_hdf5 a staticmethod

* feat: store Molecule.units in HDF5

* fix: update expected HDF5 result

* docs: include backwards compatibility expectations

* feat: add skip_unreadable_data toggle to HDF5 loading methods

* Fix spell

Apparently Sphinx can now use `kwds` instead of `kwargs`

* docs: explicitly request error raising

* docs: use `:func:` instead of `:class:`

* docs: ensure *StructureDriverResults are documented

* refactor: enforce keyword arguments in hdf5 module

* Update driver return types

While the previous return types were not wrong, for documentation
purposes using these concrete implementations is a bit nicer.

* Run black

* Add reno

* feat: use Molecule.units during from_hdf5

* test: refactor _hdf5 method tests

Instead of hard-coding the expected HDF5 file structure, we test that
`to_hdf5` and `from_hdf5` work consistently with each other.
The `test_to_hdf5` tests ensure that this method executes correctly
(i.e. without errors). The `from_hdf5` tests ensure that first writing
and subsequently reading a property from a file produces an identical
instance.

In the future, once version numbers of certain properties may increase,
we should store HDF5 files and compare those against expected instances.

* fix: README test

The README test previously failed because the iteration over the
auxiliary operator observables in the ElectronicStructureResult is
currently unable to handle the lack of certain properties which have
always been evaluated for legacy reasons (AngularMomentum,
Magnetization).
Even if we were to default them to an empty list instead of None, while
the zip command would execute normally, no results would be printed
since zip stops after the shortest length.

That being said, fixing ElectronicStructureResult is not the solution
right now in any case, since a user would be unable to manually request
the computation of AngularMomentum and Magnetization before we resolve
the issue ý¿¿¼£�qiskit-community#312
Thus, this commit reverts the exclusion of these auxiliary operators in
the case of `settings.dict_aux_operators`.

* refactor: use only public API in PropertyTest

* refactor: update type hints

* fix: update TestVibrationalStructureDriverResult to G16 Rev.C01

Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com>
  • Loading branch information
mrossinek and woodsp-ibm authored Feb 10, 2022
1 parent 8682d39 commit 26d9721
Show file tree
Hide file tree
Showing 57 changed files with 2,172 additions and 275 deletions.
3 changes: 3 additions & 0 deletions .pylintdict
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ hardcoded
hartree
hartrees
hcore
hdf
heidelberg
heisenberg
hermite
Expand All @@ -192,6 +193,7 @@ intel
intelvem
interatomic
internuclear
interpretable
ints
ising
iso
Expand Down Expand Up @@ -221,6 +223,7 @@ knowles
kohn
kwarg
kwargs
kwds
labelled
lda
len
Expand Down
17 changes: 17 additions & 0 deletions docs/apidocs/qiskit_nature.hdf5.rst
Original file line number Diff line number Diff line change
@@ -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
36 changes: 28 additions & 8 deletions docs/tutorials/08_property_framework.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -1217,7 +1229,7 @@
{
"data": {
"text/html": [
"<h3>Version Information</h3><table><tr><th>Qiskit Software</th><th>Version</th></tr><tr><td><code>qiskit-terra</code></td><td>0.20.0.dev0+7af16a3</td></tr><tr><td><code>qiskit-aer</code></td><td>0.10.2</td></tr><tr><td><code>qiskit-ignis</code></td><td>0.7.0</td></tr><tr><td><code>qiskit-nature</code></td><td>0.4.0</td></tr><tr><td><code>qiskit-finance</code></td><td>0.4.0</td></tr><tr><td><code>qiskit-optimization</code></td><td>0.4.0</td></tr><tr><td><code>qiskit-machine-learning</code></td><td>0.3.0</td></tr><tr><th>System information</th></tr><tr><td>Python version</td><td>3.8.12</td></tr><tr><td>Python compiler</td><td>Clang 10.0.0 </td></tr><tr><td>Python build</td><td>default, Oct 12 2021 06:23:56</td></tr><tr><td>OS</td><td>Darwin</td></tr><tr><td>CPUs</td><td>2</td></tr><tr><td>Memory (Gb)</td><td>12.0</td></tr><tr><td colspan='2'>Fri Feb 04 13:55:22 2022 EST</td></tr></table>"
"<h3>Version Information</h3><table><tr><th>Qiskit Software</th><th>Version</th></tr><tr><td><code>qiskit-terra</code></td><td>0.20.0.dev0+9a743fb</td></tr><tr><td><code>qiskit-aer</code></td><td>0.11.0</td></tr><tr><td><code>qiskit-ignis</code></td><td>0.7.0</td></tr><tr><td><code>qiskit-ibmq-provider</code></td><td>0.19.0.dev0+8455b01</td></tr><tr><td><code>qiskit-nature</code></td><td>0.4.0</td></tr><tr><th>System information</th></tr><tr><td>Python version</td><td>3.9.9</td></tr><tr><td>Python compiler</td><td>GCC 11.2.1 20210728 (Red Hat 11.2.1-1)</td></tr><tr><td>Python build</td><td>main, Nov 19 2021 00:00:00</td></tr><tr><td>OS</td><td>Linux</td></tr><tr><td>CPUs</td><td>4</td></tr><tr><td>Memory (Gb)</td><td>14.842281341552734</td></tr><tr><td colspan='2'>Mon Jan 24 14:34:24 2022 CET</td></tr></table>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
Expand Down Expand Up @@ -1245,6 +1257,14 @@
"%qiskit_version_table\n",
"%qiskit_copyright"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "889fbac9",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand All @@ -1263,7 +1283,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.12"
"version": "3.9.9"
}
},
"nbformat": 4,
Expand Down
3 changes: 2 additions & 1 deletion qiskit_nature/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -39,6 +39,7 @@
circuit
converters
drivers
hdf5
mappers
operators
problems
Expand Down
68 changes: 68 additions & 0 deletions qiskit_nature/drivers/molecule.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from typing import Callable, Tuple, List, Optional, cast
import copy

import h5py
import numpy as np
import scipy.linalg

Expand All @@ -36,6 +37,8 @@ class Molecule:
directly if its needed.
"""

VERSION = 1

def __init__(
self,
geometry: List[Tuple[str, List[float]]],
Expand Down Expand Up @@ -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}"]
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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


Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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))

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 26d9721

Please sign in to comment.