Repository: theochem/AtomDB (language: python)

Issue:
Improve slater database
Hello, @PaulWAyers  @gabrielasd This is the branch with the later database with the added functionalities (all the properties can be partitioned in orbitals). Sorry that it took me more than I thought (I had to code tests to look for problems). So far everything works (at least everything I have tested). I will continue to create tests for this database (but I think the code can be used).


Discussion / hints:

Fix patch:
diff --git a/atomdb/__init__.py b/atomdb/__init__.py
index e04c7dfb..0507eb9b 100644
--- a/atomdb/__init__.py
+++ b/atomdb/__init__.py
@@ -15,14 +15,34 @@
 
 r"""AtomDB, a database of atomic and ionic properties."""
 
+from atomdb.periodic import Element
 
-from atomdb.version import *
+from atomdb.species import Species
 
-from atomdb.api import *
+from atomdb.promolecule import Promolecule
 
-from atomdb.promolecule import *
+from atomdb.periodic import element_number, element_symbol, element_name
 
-from atomdb.periodic import *
+from atomdb.species import compile, load, dump, raw_datafile
+
+from atomdb.promolecule import make_promolecule
+
+from atomdb.version import version
+
+
+__all__ = [
+    "Element",
+    "Species",
+    "Promolecule",
+    "element_number",
+    "element_symbol",
+    "element_name",
+    "compile",
+    "load",
+    "dump",
+    "raw_datafile",
+    "make_promolecule" "version",
+]
 
 
 __version__ = version
diff --git a/atomdb/__main__.py b/atomdb/__main__.py
index 3d195bb7..729cd21a 100644
--- a/atomdb/__main__.py
+++ b/atomdb/__main__.py
@@ -15,43 +15,55 @@
 
 r"""AtomDB console script."""
 
-
 from argparse import ArgumentParser
 
-from sys import stderr
-
-import atomdb
+from atomdb import compile, load
 
 
 # Initialize command line argument parser
-parser = ArgumentParser(prog="python -m atomdb", description="Compile and/or query an AtomDB entry")
+#
+parser = ArgumentParser(prog="atomdb", description="Compile or query an AtomDB entry.")
+
+# Define subcommands
+#
+command = parser.add_argument_group("commands")
+
+command_group = command.add_mutually_exclusive_group(required=True)
+
+command_group.add_argument(
+    "-c", "--compile", action="store_true", help="compile a species into the database"
+)
+
+command_group.add_argument(
+    "-q", "--query", action="store_true", help="query a species from the database"
+)
 
 
-# Specify positional arguments and options
-parser.add_argument("-c", action="store_true", default=False, help="compile the specified entry")
-parser.add_argument("-q", action="store_true", default=False, help="query the specified entry")
-parser.add_argument("dataset", type=str, help="name of dataset")
-parser.add_argument("elem", type=str, help="element symbol")
-parser.add_argument("charge", type=int, help="charge")
-parser.add_argument("mult", type=int, help="multiplicity")
-parser.add_argument("-e", type=int, default=0, help="excitation level")
+# Add arguments
+#
+arg_group = parser.add_argument_group("arguments")
+
+arg_group.add_argument("dataset", type=str, help="name of dataset")
+
+arg_group.add_argument("elem", type=str, help="element symbol")
+
+arg_group.add_argument("charge", type=int, help="charge")
+
+arg_group.add_argument("mult", type=int, help="multiplicity")
+
+arg_group.add_argument("-e", "--exc", type=int, default=0, help="excitation level")
 
 
 if __name__ == "__main__":
 
-    # Parse arguments
     args = parser.parse_args()
 
-    # Exit if no commands are specified
-    if not (args.c or args.q):
-        print("No command specified. Exiting...", file=stderr)
-        exit(1)
+    if args.compile:
+
+        compile(args.elem, args.charge, args.mult, args.exc, args.dataset)
+
+    elif args.query:
 
-    # Run specified command(s)
-    if args.c:
-        atomdb.compile(args.elem, args.charge, args.mult, args.e, args.dataset)
-    if args.q:
-        print(atomdb.load(args.elem, args.charge, args.mult, args.e, args.dataset).to_json())
+        species = load(args.elem, args.charge, args.mult, args.exc, args.dataset)
 
-    # Exit successfully
-    exit(0)
+        print(species.to_json())
diff --git a/atomdb/api.py b/atomdb/api.py
deleted file mode 100644
index d43f45b2..00000000
--- a/atomdb/api.py
+++ /dev/null
@@ -1,776 +0,0 @@
-# This file is part of AtomDB.
-#
-# AtomDB is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# AtomDB is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
-# for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
-
-r"""AtomDB, a database of atomic and ionic properties."""
-
-
-from dataclasses import dataclass, field, asdict
-
-from glob import glob
-
-from importlib import import_module
-
-from json import JSONEncoder, dumps
-
-from os import environ, makedirs
-
-from os.path import dirname, join
-
-from sys import platform
-
-from msgpack import Packer, Unpacker
-
-from numbers import Integral
-
-from numpy import ndarray, frombuffer, exp, log, sum
-
-from scipy import constants
-from scipy.interpolate import CubicSpline
-
-from csv import reader
-
-
-__all__ = [
-    "DEFAULT_DATASET",
-    "DEFAULT_DATAPATH",
-    "Species",
-    "load",
-    "load_all",
-    "compile",
-    "datafile",
-    "element_number",
-    "element_symbol",
-    "get_element_data",
-]
-
-
-DEFAULT_DATASET = "slater"
-r"""Default dataset to query."""
-
-
-DEFAULT_DATAPATH = environ.get("ATOMDB_DATAPATH", join(dirname(__file__), "datasets/"))
-r"""The path for raw and compiled AtomDB data files."""
-
-
-ELEMENTS = (
-    # fmt: off
-    "\0", "H",  "He", "Li", "Be", "B",  "C",  "N",  "O",  "F",  "Ne", "Na",
-    "Mg", "Al", "Si", "P",  "S",  "Cl", "Ar", "K",  "Ca", "Sc", "Ti", "V",
-    "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br",
-    "Kr", "Rb", "Sr", "Y",  "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag",
-    "Cd", "In", "Sn", "Sb", "Te", "I",  "Xe", "Cs", "Ba", "La", "Ce", "Pr",
-    "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu",
-    "Hf", "Ta", "W",  "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi",
-    "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U",  "Np", "Pu", "Am",
-    "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh",
-    "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og",
-    # fmt: on
-)
-r"""Tuple of the symbols for each of the 118 elements. The zeroth element is a placeholder."""
-
-
-MULTIPLICITIES = (
-    # fmt: off
-    0, 2, 1, 2, 1, 2, 3, 4, 3, 2, 1, 2, 1, 2, 3, 4, 3, 2, 1, 2, 1, 2, 3, 4,
-    7, 6, 5, 4, 3, 2, 1, 2, 3, 4, 3, 2, 1, 2, 1, 2, 3, 6, 7, 6, 5, 4, 1, 2,
-    1, 2, 3, 4, 3, 2, 1, 2, 1, 2, 1, 4, 5, 6, 7, 8, 9, 6, 5, 4, 3, 2, 1, 2,
-    3, 4, 5, 6, 5, 4, 3, 2, 1, 2, 3, 4, 3, 2, 1, 2, 1, 2, 3, 4, 5, 6, 7, 8,
-    9, 6, 5, 4, 3,
-    # fmt: on
-)
-r"""Tuple of the multiplicities for each isoelectronic series (up to 100 electrons).
-The zeroth element is a placeholder."""
-
-
-# The correct way to convert numpy arrays to bytes is different on Mac/"Darwin"
-if platform == "darwin":
-    # Mac
-    def _array_to_bytes(array):
-        r"""Convert a numpy.ndarray instance to bytes."""
-        return array.tobytes()
-
-else:
-    # Linux and friends
-    def _array_to_bytes(array):
-        r"""Convert a numpy.ndarray instance to bytes."""
-        return array.data if array.flags["C_CONTIGUOUS"] else array.tobytes()
-
-
-@dataclass(eq=False, order=False)
-class SpeciesData:
-    r"""Properties of atomic and ionic species corresponding to fields in MessagePack files."""
-
-    #
-    # Species info
-    #
-    dataset: str = field()
-    elem: str = field()
-    atnum: int = field()
-    obasis_name: str = field()
-    nelec: int = field()
-    nspin: int = field()
-    nexc: int = field()
-    #
-    # Element properties
-    #
-    atmass: float = field()
-    cov_radius: dict = field()
-    vdw_radius: dict = field()
-    at_radius: dict = field()
-    polarizability: dict = field()
-    dispersion_c6: dict = field()
-    #
-    # Electronic and molecular orbital energies
-    #
-    energy: float = field(default=None)
-    mo_energy_a: ndarray = field(default=None)
-    mo_energy_b: ndarray = field(default=None)
-    mo_occs_a: ndarray = field(default=None)
-    mo_occs_b: ndarray = field(default=None)
-    #
-    # Conceptual DFT related properties
-    #
-    ip: float = field(default=None)
-    mu: float = field(default=None)
-    eta: float = field(default=None)
-    #
-    # Radial grid
-    #
-    rs: ndarray = field(default=None)
-    #
-    # Density
-    #
-    mo_dens_a: ndarray = field(default=None)
-    mo_dens_b: ndarray = field(default=None)
-    dens_tot: ndarray = field(default=None)
-    #
-    # Density gradient
-    #
-    mo_d_dens_a: ndarray = field(default=None)
-    mo_d_dens_b: ndarray = field(default=None)
-    d_dens_tot: ndarray = field(default=None)
-    #
-    # Density laplacian
-    #
-    mo_dd_dens_a: ndarray = field(default=None)
-    mo_dd_dens_b: ndarray = field(default=None)
-    dd_dens_tot: ndarray = field(default=None)
-    #
-    # Kinetic energy density
-    #
-    mo_ked_a: ndarray = field(default=None)
-    mo_ked_b: ndarray = field(default=None)
-    ked_tot: ndarray = field(default=None)
-
-
-class Species(SpeciesData):
-    r"""Properties of atomic and ionic species."""
-
-    def __init__(self, *args, **kwargs):
-        r"""Initialize a Species Instance."""
-        # Initialize superclass
-        SpeciesData.__init__(self, *args, **kwargs)
-        self.ao = _AtomicOrbitals(
-            self.mo_occs_a, self.mo_occs_b, self.mo_energy_a, self.mo_energy_b
-        )
-        # Reshape the orbital densities and kinetic energy densities as 2D arrays
-        self.mo_dens_a = self._to_ndarray(self.mo_dens_a, self.ao.norba)
-        self.mo_dens_b = self._to_ndarray(self.mo_dens_b, self.ao.norba)
-        self.mo_d_dens_a = self._to_ndarray(self.mo_d_dens_a, self.ao.norba)
-        self.mo_d_dens_b = self._to_ndarray(self.mo_d_dens_b, self.ao.norba)
-        self.mo_dd_dens_a = self._to_ndarray(self.mo_dd_dens_a, self.ao.norba)
-        self.mo_dd_dens_b = self._to_ndarray(self.mo_dd_dens_b, self.ao.norba)
-        self.mo_ked_a = self._to_ndarray(self.mo_ked_a, self.ao.norba)
-        self.mo_ked_b = self._to_ndarray(self.mo_ked_b, self.ao.norba)
-        #
-        # Attributes declared here are not considered as part of the dataclasses interface,
-        # and therefore are not included in the output of dataclasses.asdict(species_instance)
-        #
-        # Charge and multiplicity
-        #
-        self.charge = self.atnum - self.nelec
-        self.mult = self.nspin + 1
-        self.spinpol = int(kwargs.get("spinpol", 1))
-        self.doc = get_docstring(self.dataset)
-
-    @property
-    def spinpol(self):
-        r"""
-        The spin polarization direction (±1).
-
-        """
-        return self._spinpol
-
-    @spinpol.setter
-    def spinpol(self, p):
-        if not isinstance(p, Integral):
-            raise TypeError("`spinpol` attribute must be an integral type")
-        p = int(p)
-        if p not in (+1, -1):
-            raise ValueError("`spinpol` must be +1 or -1")
-        self._spinpol = p
-
-    def _to_ndarray(self, array1d, n):
-        return array1d.reshape(n, -1) if array1d is not None else None
-
-    #
-    # Density splines
-    #
-    def density_func(self, spin="ab", index=None, log=False):
-        """Compute electron density
-
-        Parameters
-        ----------
-        spin : str, optional
-            Type of occupied spin orbitals which can be either "a" (for alpha), "b" (for
-            beta), "ab" (for alpha + beta), and, "m" (for alpha - beta), by default 'ab'
-        index : sequence of int, optional
-            Sequence of integers representing the spin orbitals which are indexed
-            from 1 to the number basis functions. If ``None``, all orbitals of the given
-            spin(s) are included, by default None.
-        log : bool, optional
-            Whether the logarithm of the density is used for interpolation, by default False
-
-        Returns
-        -------
-        Callable[[np.ndarray(N,), int] -> np.ndarray(N,)]
-            a callable function evaluating the density given a set of radial points (1-D array).
-
-        Examples
-        --------
-        # Generate the interpolator for the atomic density
-        >>> dens_spline = density_func(log=True)
-        # Define a radial set of points to be interpolated
-        >>> x = np.arange(0, 5)
-        >>> dens = dens_spline(x)            # interpolated density
-        """
-        if spin not in ["a", "b", "ab", "m"]:
-            raise ValueError(
-                f"Incorrect `spin` parameter {spin}, choose one of  `a`, `b`, `ab` or `m`."
-            )
-        if spin in ["a", "b", "m"] and (self.mo_dens_a is None):
-            raise ValueError(f"Density values for `{spin}` spin-orbitals unavailable.")
-        if index is not None and (self.mo_dens_a is None):
-            raise ValueError(
-                "Can not perform indexing since densities per orbital is missing in this dataset."
-            )
-
-        # Assign cases that require spin-densitiy data. Since these are always
-        # stored as density per orbital they work for any `index` parameter case.
-        if spin == "a":
-            orbs_dens = self.mo_dens_a if self.spinpol == 1 else self.mo_dens_b
-        elif spin == "b":
-            orbs_dens = self.mo_dens_b if self.spinpol == 1 else self.mo_dens_a
-        elif spin == "m":
-            orbs_dens = self.spinpol * (self.mo_dens_a - self.mo_dens_b)
-
-        # Evaluate property values for interpolation.
-        # Total density (ab) is evaluated from spin components when indexing is required
-        if index is None:
-            if spin == "ab":
-                value_array = self.dens_tot
-            else:
-                value_array = sum(orbs_dens, axis=0)
-        else:
-            if spin == "ab":
-                orbs_dens = self.mo_dens_a + self.mo_dens_b
-            orbs_dens = orbs_dens[index]  # M(K_orb,N)
-            value_array = sum(orbs_dens, axis=0)  # (N,)
-
-        return cubic_interp(self.rs, value_array, log=log)
-
-    def ddens_func(self, spin="ab", index=None, log=False):
-        """Compute the first derivative of the atomic density along a 1-D grid.
-
-        The derivarive of the density as a function of the distance to the atomic center
-        (a set of points along a 1-D grid) is modeled by a cubic spline. The property can
-        be computed for the alpha, beta, alpha + beta, and alpha - beta components of the
-        electron density.
-
-        Parameters
-        ----------
-        spin : str, optional
-            Type of occupied spin orbitals which can be either "a" (for alpha), "b" (for
-            beta), "ab" (for alpha + beta), and, "m" (for alpha - beta), by default 'ab'
-        index : sequence of int, optional
-            Sequence of integers representing the spin orbitals which are indexed
-            from 1 to the number basis functions. If ``None``, all orbitals of the given spin(s) are included
-        log : bool, optional
-            Whether the logarithm of the density property is used for interpolation
-
-        Returns
-        -------
-        Callable[[np.ndarray(N,), int] -> np.ndarray(N,)]
-            a callable function evaluating the derivative of the density given a set of radial
-            points (1-D array).
-
-        Example
-        -------
-        # Generate the interpolator for the derivative of the density
-        >>> d_dens_spline = ddens_func()
-        # Define a 1-D set of points to be interpolated
-        >>> x = np.arange(0, 5)
-        >>> d_dens = d_dens_spline(x)  # interpolated derivative of the density
-        """
-        if spin not in ["a", "b", "ab", "m"]:
-            raise ValueError(
-                f"Incorrect `spin` parameter {spin}, choose one of  `a`, `b`, `ab` or `m`."
-            )
-        if spin in ["a", "b", "m"] and (self.mo_d_dens_a is None):
-            raise ValueError(f"Density property values for `{spin}` spin-orbitals unavailable.")
-        if index is not None and (self.mo_dens_a is None):
-            raise ValueError(
-                "Can not perform indexing since densities per orbital is missing in this dataset."
-            )
-        if log:
-            raise ValueError("Logarithmic interpolation is not supported for gradients.")
-
-        # Assign cases that require spin-densitiy data. Since these are always
-        # stored as density per orbital they work for any `index` parameter case.
-        if spin == "a":
-            orbs_d_dens = self.mo_d_dens_a
-        elif spin == "b":
-            orbs_d_dens = self.mo_d_dens_b
-        elif spin == "m":
-            orbs_d_dens = self.mo_d_dens_a - self.mo_d_dens_b
-
-        # Get the property values for interpolation.
-        # 1) If index is not provided, the gradient is summed along the axis of the molecular
-        # orbital components to give the total, alpha, beta or magnetic values of the property.
-        # 2) When indexing is required, the property is evaluated for the specified spin-orbitals
-        # for the alpha, beta, alpha + beta, and alpha - beta components of the density.
-        if index is None:
-            if spin == "ab":
-                value_array = self.d_dens_tot
-            else:
-                value_array = sum(orbs_d_dens, axis=0)
-        else:
-            if spin == "ab":
-                orbs_d_dens = self.mo_d_dens_a + self.mo_d_dens_b
-            orbs_d_dens = orbs_d_dens[index]  # M(K_orb,N)
-            value_array = sum(orbs_d_dens, axis=0)  # (N,)
-
-        return cubic_interp(self.rs, value_array, log=log)
-
-    def d2dens_func(self, spin="ab", index=None, log=False):
-        """Compute the second derivative of the atomic density along a 1-D grid.
-
-        The second derivarive of the density is modeled by a cubic spline. Using this function,
-        the property can be interpolated at a set of points (along a 1-D grid). It can be evaluated
-        for the alpha, beta, alpha + beta, and alpha - beta components of the electron density.
-
-        Parameters
-        ----------
-        spin : str, optional
-            Type of occupied spin orbitals which can be either "a" (for alpha), "b" (for
-            beta), "ab" (for alpha + beta), and, "m" (for alpha - beta), by default 'ab'
-        index : sequence of int, optional
-            Sequence of integers representing the spin orbitals which are indexed
-            from 1 to the number basis functions. If ``None``, all orbitals of the given spin(s) are included
-        log : bool, optional
-            Whether the logarithm of the density property is used for interpolation
-
-        Returns
-        -------
-        Callable[[np.ndarray(N,), int] -> np.ndarray(N,)]
-            a callable function evaluating the second derivative of the density given
-            a set of radial points (1-D array).
-
-        Example
-        -------
-        # Generate the interpolator for the derivative of the density of order 2
-        >>> dd_dens_spline = d2dens_func()
-        # Define a 1-D set of points to be interpolated
-        >>> x = np.arange(0, 5)
-        >>> dd_dens = dd_dens_spline(x)  # interpolated second derivative of density
-        """
-        if spin not in ["a", "b", "ab", "m"]:
-            raise ValueError(
-                f"Incorrect `spin` parameter {spin}, choose one of  `a`, `b`, `ab` or `m`."
-            )
-        if spin in ["a", "b", "m"] and (self.mo_d_dens_a is None):
-            raise ValueError(f"Density property values for `{spin}` spin-orbitals unavailable.")
-        if index is not None and (self.mo_d_dens_a is None):
-            raise ValueError(
-                "Can not perform indexing since densities per orbital are missing in this dataset."
-            )
-        if log:
-            raise ValueError("Logarithmic interpolation is not supported for the Laplacian.")
-
-        # Assign cases that require spin-densitiy data. Since these are always
-        # stored as density per orbital they work for any `index` parameter case.
-        if spin == "a":
-            orbs_dd_dens = self.mo_dd_dens_a
-        elif spin == "b":
-            orbs_dd_dens = self.mo_dd_dens_b
-        elif spin == "m":
-            orbs_dd_dens = self.mo_dd_dens_a - self.mo_dd_dens_b
-
-        # Get the property values for interpolation.
-        # 1) If index is not provided, the gradient is summed along the axis of the molecular
-        # orbital components to give the total, alpha, beta or magnetic values of the property.
-        # 2) When indexing is required, the property is evaluated for the specified spin-orbitals
-        # for the alpha, beta, alpha + beta, and alpha - beta components of the density.
-        if index is None:
-            if spin == "ab":
-                value_array = self.dd_dens_tot
-            else:
-                value_array = sum(orbs_dd_dens, axis=0)
-        else:
-            if spin == "ab":
-                orbs_dd_dens = self.mo_dd_dens_a + self.mo_dd_dens_b
-            orbs_dd_dens = orbs_dd_dens[index]  # M(K_orb,N)
-            value_array = sum(orbs_dd_dens, axis=0)  # (N,)
-
-        return cubic_interp(self.rs, value_array, log=log)
-
-    def ked_func(self, spin="ab", index=None, log=False):
-        r"""Compute the positive definite kinetic energy density (KED).
-
-        Parameters
-        ----------
-        spin : str, optional
-            Type of occupied spin orbitals which can be either "a" (for alpha), "b" (for
-            beta), "ab" (for alpha + beta), and, "m" (for alpha - beta)
-        index : sequence of int, optional
-            Sequence of integers representing the spin orbitals which are indexed
-            from 1 to the number basis functions. If ``None``, all orbitals of the given spin(s) are included
-        log : bool, optional
-            Whether the logarithm of the KED is used for interpolation
-
-        Returns
-        -------
-        Callable[[np.ndarray(N,), int] -> np.ndarray(N,)]
-            a callable function evaluating the KED given a set of radial points (1-D array).
-
-        """
-        if spin not in ["a", "b", "ab", "m"]:
-            raise ValueError(
-                f"Incorrect `spin` parameter {spin}, choose one of  `a`, `b`, `ab` or `m`."
-            )
-        if spin in ["a", "b", "m"] and (self.mo_ked_a is None):
-            raise ValueError(f"Kinetic energy density for `{spin}` spin-orbitals unavailable.")
-        if index is not None and (self.mo_ked_a is None):
-            raise ValueError(
-                "Can not perform indexing since orbital densities are missing in this dataset."
-            )
-
-        # Assign cases that require spin-densitiy data. Since these are stored as density
-        # per orbital they work for any `index` parameter case.
-        if spin == "a":
-            orbs_ked = self.mo_ked_a if self.spinpol == 1 else self.mo_ked_b
-        elif spin == "b":
-            orbs_ked = self.mo_ked_b if self.spinpol == 1 else self.mo_ked_a
-        elif spin == "m":
-            orbs_ked = self.spinpol * (self.mo_ked_a - self.mo_ked_b)
-
-        # Evaluate property values for interpolation.
-        # Total density (ab) is evaluated from spin components when indexing is required
-        if index is None:
-            if spin == "ab":
-                value_array = self.ked_tot
-            else:
-                value_array = sum(orbs_ked, axis=0)
-        else:
-            if spin == "ab":
-                orbs_ked = self.mo_ked_a + self.mo_ked_b
-            orbs_ked = orbs_ked[index]  # M(K_orb,N)
-            value_array = sum(orbs_ked, axis=0)  # (N,)
-
-        return cubic_interp(self.rs, value_array, log=log)
-
-    def to_dict(self):
-        r"""Return the dictionary representation of the Species instance."""
-        return asdict(self)
-
-    def to_json(self):
-        r"""Return the JSON string representation of the Species instance."""
-        return dumps(asdict(self), cls=Species._JSONEncoder)
-
-    class _JSONEncoder(JSONEncoder):
-        r"""JSON encoder handling simple `numpy.ndarray` objects (for `Species.dump`)."""
-
-        def default(self, obj):
-            r"""Default encode function."""
-            return obj.tolist() if isinstance(obj, ndarray) else JSONEncoder.default(self, obj)
-
-    @staticmethod
-    def _msgfile(elem, charge, mult, nexc, dataset, datapath):
-        r"""Return the filename of a database entry MessagePack file."""
-        return join(datapath, f"{dataset.lower()}/db/{elem}_{charge}_{mult}_{nexc}.msg")
-
-    def _dump(self, datapath):
-        r"""Dump the Species instance to a MessagePack file in the database."""
-        # Get database entry filename
-        fn = Species._msgfile(self.elem, self.charge, self.mult, self.nexc, self.dataset, datapath)
-        # Convert numpy arrays to raw bytes for dumping as msgpack
-        msg = {
-            k: _array_to_bytes(v) if isinstance(v, ndarray) else v for k, v in asdict(self).items()
-        }
-        # Dump msgpack entry to database
-        with open(fn, "wb") as f:
-            f.write(pack_msg(msg))
-
-
-def load(elem, charge, mult, nexc=0, dataset=DEFAULT_DATASET, datapath=DEFAULT_DATAPATH):
-    r"""Load an atomic or ionic species from the AtomDB database."""
-    # Load database msgpack entry
-    with open(Species._msgfile(elem, charge, mult, nexc, dataset, datapath), "rb") as f:
-        msg = unpack_msg(f)
-    # Convert raw bytes back to numpy arrays, initialize the Species instance, return it
-    return Species(**{k: frombuffer(v) if isinstance(v, bytes) else v for k, v in msg.items()})
-
-
-def load_all(elem, nexc=0, dataset=DEFAULT_DATASET, datapath=DEFAULT_DATAPATH):
-    r"""Load an atomic or ionic species from the AtomDB database."""
-    species = []
-    # Find all matching msgpack entries
-    for file in glob(join(datapath, f"{dataset.lower()}/db/{element_symbol(elem)}_*_{nexc}.msg")):
-        # Load database msgpack entry
-        with open(file, "rb") as f:
-            msg = unpack_msg(f)
-        # Convert raw bytes back to numpy arrays, initialize the Species instance, store it
-        species.append(
-            Species(**{k: frombuffer(v) if isinstance(v, bytes) else v for k, v in msg.items()})
-        )
-    return species
-
-
-def compile(elem, charge, mult, nexc=0, dataset=DEFAULT_DATASET, datapath=DEFAULT_DATAPATH):
-    r"""Compile an atomic or ionic species into the AtomDB database."""
-    # Ensure directories exist
-    makedirs(join(datapath, f"{dataset}/db"), exist_ok=True)
-    makedirs(join(datapath, f"{dataset}/raw"), exist_ok=True)
-    # Import the compile script for the appropriate dataset
-    submodule = import_module(f"atomdb.datasets.{dataset}")
-    # Compile the Species instance and dump the database entry
-    submodule.run(elem, charge, mult, nexc, dataset, datapath)._dump(datapath)
-
-
-def datafile(suffix, elem, charge, mult, nexc=0, dataset=None, datapath=DEFAULT_DATAPATH):
-    r"""Return the filename of a raw data file."""
-    # Check that all non-optional arguments are specified
-    if dataset is None:
-        raise ValueError("Argument `dataset` cannot be unspecified")
-    # Format the filename specified and return it
-    suffix = f"{'' if suffix.startswith('.') else '_'}{suffix.lower()}"
-    if dataset.split("_")[0] == "hci":
-        prefix = f"{str(element_number(elem)).zfill(3)}"
-        tag = f"q{str(charge).zfill(3)}_m{mult:02d}_k{nexc:02}_sp_{dataset}"
-    else:
-        prefix = f"{str(element_number(elem)).zfill(3)}"
-        tag = f"q{str(charge).zfill(3)}_m{mult:02d}_k{nexc:02}_sp_{dataset}"
-    return join(datapath, f"{dataset.lower()}/raw/{prefix}_{tag}{suffix}")
-
-
-def element_number(elem):
-    r"""Return the element number from the given element symbol."""
-    return ELEMENTS.index(elem) if isinstance(elem, str) else elem
-
-
-def element_symbol(elem):
-    r"""Return the element symbol from the given element number."""
-    return elem if isinstance(elem, str) else ELEMENTS[elem]
-
-
-def pack_msg(msg):
-    r"""Pack an object to MessagePack binary format."""
-    return Packer(use_bin_type=True).pack(msg)
-
-
-def unpack_msg(msg):
-    r"""Unpack an object from MessagePack binary format."""
-    return Unpacker(msg, use_list=False, strict_map_key=True).unpack()
-
-
-class cubicspline_log(CubicSpline):
-    r"""Interpolate over a 1-D grid."""
-
-    def __init__(self, x, y, **kwargs):
-        r"""Initialize the CubicSpline instance."""
-        CubicSpline.__init__(self, x, log(y), **kwargs)
-
-    def __call__(self, x, deriv=0):
-        r"""Compute the interpolation at some x-values.
-
-        Parameters
-        ----------
-        x : ndarray(M,)
-            points to be interpolated.
-        deriv: int, optional
-            order of spline derivative to evaluate. Must be one of 0, 1 and 2, default is 0.
-
-        Returns
-        -------
-        ndarray(M,)
-            Interpolated values (1-D array).
-        """
-        if deriv not in [0, 1, 2]:
-            raise NotImplementedError
-
-        y = exp(CubicSpline.__call__(self, x))
-        if deriv == 1:
-            # d(rho(r)) = d(log(rho(r))) * rho(r)
-            dlogy = CubicSpline.__call__(self, x, nu=1)
-            y = dlogy.flatten() * y
-        elif deriv == 2:
-            # d^2(rho(r)) = d^2(log(rho(r))) * rho(r) + [d(rho(r))]^2/rho(r)
-            dlogy = CubicSpline.__call__(self, x, nu=1)
-            d2logy = CubicSpline.__call__(self, x, nu=2)
-            y = d2logy.flatten() * y + dlogy.flatten() ** 2 * y
-
-        return y
-
-
-class cubicspline_(CubicSpline):
-    r"""Interpolate over a 1-D grid."""
-
-    def __init__(self, x, y, **kwargs):
-        r"""Initialize the CubicSpline instance."""
-        CubicSpline.__init__(self, x, y, **kwargs)
-
-    def __call__(self, x, deriv=0):
-        r"""Compute the interpolation at some x-values.
-
-        Parameters
-        ----------
-        x : ndarray(M,)
-            points to be interpolated.
-        deriv: int, optional
-            order of spline derivative to evaluate, default is 0.
-
-        Returns
-        -------
-        ndarray(M,)
-            Interpolated values (1-D array).
-        """
-        return CubicSpline.__call__(self, x, nu=deriv)
-
-
-def cubic_interp(x, y, log=False):
-    r"""Create an interpolated cubic spline for the given data."""
-    cls = cubicspline_log if log else cubicspline_
-    return cls(x, y, axis=0, bc_type="not-a-knot", extrapolate=True)
-
-
-class _AtomicOrbitals(object):
-    """Atomic orbitals class."""
-
-    def __init__(self, occs_a, occs_b, energy_a, energy_b) -> None:
-        self.occs_a = occs_a
-        self.occs_b = occs_b
-        self.energy_a = energy_a
-        self.energy_b = energy_b
-        self.norba = len(energy_a) if energy_a is not None else None
-        self.norbb = len(energy_b) if energy_b is not None else None
-
-
-def get_element_data(elem):
-    r"""Get element properties from elements.csv.
-
-    The following attributes are present for some elements. When a parameter
-    is not known for a given element, the attribute is set to `None`.
-    mass
-        The IUPAC atomic masses (wieghts) of 2013.
-        T.B. Coplen, W.A. Brand, J. Meija, M. Gröning, N.E. Holden, M.
-        Berglund, P. De Bièvre, R.D. Loss, T. Prohaska, and T. Walczyk.
-        http://ciaaw.org, http://www.ciaaw.org/pubs/TSAW2013_xls.xls,
-        When ranges are provided, the middle of the range is used.
-    cov_radius_cordero
-        Covalent radius. B. Cordero, V. Gomez, A. E. Platero-Prats, M.
-        Reves, J. Echeverria, E. Cremades, F. Barragan, and S. Alvarez,
-        Dalton Trans. pp. 2832--2838 (2008), URL
-        http://dx.doi.org/10.1039/b801115j
-    cov_radius_bragg
-        Covalent radius. W. L. Bragg, Phil. Mag. 40, 169 (1920), URL
-        http://dx.doi.org/10.1080/14786440808636111
-    cov_radius_slater
-        Covalent radius. J. C. Slater, J. Chem. Phys. 41, 3199 (1964), URL
-        http://dx.doi.org/10.1063/1.1725697
-    vdw_radius_bondi
-        van der Waals radius. A. Bondi, J. Phys. Chem. 68, 441 (1964), URL
-        http://dx.doi.org/10.1021/j100785a001
-    vdw_radius_truhlar
-        van der Waals radius. M. Mantina A. C. Chamberlin R. Valero C. J.
-        Cramer D. G. Truhlar J. Phys. Chem. A 113 5806 (2009), URL
-        http://dx.doi.org/10.1021/jp8111556
-    vdw_radius_rt
-        van der Waals radius. R. S. Rowland and R. Taylor, J. Phys. Chem.
-        100, 7384 (1996), URL http://dx.doi.org/10.1021/jp953141+
-    vdw_radius_batsanov
-        van der Waals radius. S. S. Batsanov Inorganic Materials 37 871
-        (2001), URL http://dx.doi.org/10.1023/a%3a1011625728803
-    vdw_radius_dreiding
-        van der Waals radius. Stephen L. Mayo, Barry D. Olafson, and William
-        A. Goddard III J. Phys. Chem. 94 8897 (1990), URL
-        http://dx.doi.org/10.1021/j100389a010
-    vdw_radius_uff
-        van der Waals radius. A. K. Rappi, C. J. Casewit, K. S. Colwell, W.
-        A. Goddard III, and W. M. Skid J. Am. Chem. Soc. 114 10024 (1992),
-        URL http://dx.doi.org/10.1021/ja00051a040
-    vdw_radius_mm3
-        van der Waals radius. N. L. Allinger, X. Zhou, and J. Bergsma,
-        Journal of Molecular Structure: THEOCHEM 312, 69 (1994),
-        http://dx.doi.org/10.1016/s0166-1280(09)80008-0
-    """
-
-    angstrom = 100 * constants.pico * constants.m_e * constants.c * constants.alpha / constants.hbar
-    amu = constants.gram / (constants.Avogadro * constants.m_e)
-
-    z = element_number(elem)
-    convertor_types = {
-        "int": (lambda s: int(s)),
-        "float": (lambda s: float(s)),
-        "au": (lambda s: float(s)),  # just for clarity, atomic units
-        "str": (lambda s: s.strip()),
-        "angstrom": (lambda s: float(s) * angstrom),
-        "2angstrom": (lambda s: float(s) * angstrom / 2),
-        "angstrom**3": (lambda s: float(s) * angstrom**3),
-        "amu": (lambda s: float(s) * amu),
-    }
-
-    with open(join(dirname(__file__), "data/elements.csv"), "r") as f:
-        rows = reader(f)
-        # Skip information about data provenance
-        for row in rows:
-            if len(row[1]) > 0:
-                break
-        # parse the first two header rows
-        names = row
-        convertors = [convertor_types[unit] for unit in next(rows)]
-        data = list(rows)
-
-        cov_radii = {}
-        vdw_radii = {}
-        mass = 0.0  # Initialize mass so it's never unbound
-        for idx, (name, val) in enumerate(zip(names, data[z - 1])):
-            if "cov_radius" in name:
-                kval = name.split("_")[-1]
-                cov_radii[kval] = convertors[idx](val) if val != "" else None
-            elif "vdw_radius" in name:
-                kval = name.split("_")[-1]
-                vdw_radii[kval] = convertors[idx](val) if val != "" else None
-            elif name == "mass":
-                mass = float(val) if val != "" else None
-    return cov_radii, vdw_radii, mass
-
-
-def get_docstring(dataset):
-    """Retrieve the dataset doctring."""
-    # FIXME: This function breacks the inicialization of the Species class.
-    # The databases docstrings need to be separated from the compilation scripts files.
-    # submodule = import_module(f"atomdb.datasets.{dataset}")
-    # return submodule.DOCSTRING
-    return "No documentation available."
diff --git a/atomdb/data/data_info.csv b/atomdb/data/data_info.csv
index e63edfdf..47f4bcd9 100644
--- a/atomdb/data/data_info.csv
+++ b/atomdb/data/data_info.csv
@@ -1,4 +1,7 @@
 #Property key,Property name,Source key,Property description,Reference,doi,Notes
+group,Group,,int,Element group on the periodic table,,
+period,Period,,int,Element period on the periodic table,,
+mult,Multiplicity,,int,Ground state multiplicity,,
 cov_radius,Covalent radius,cordero,Covalent radius (derived from crystallographic data),"B. Cordero, V. Gomez, A. E. Platero-Prats, M. Reves, J. Echeverria,E. Cremades,\nF. Barragan, and S. Alvarez, Dalton Trans. pp. 2832--2838 (2008)",http://dx.doi.org/10.1039/b801115j,"For carbon the number is a weighted average over the data for different hybridizations.\nFor Mn, Fe, and Co, the number is a weighted average over the data for different spins\n(high spin and low spin)"
 cov_radius,Covalent radius,bragg,"Bragg radius (covalent, metallic and ionic bonds, derived from crystallographic data)","W. L. Bragg, Phil. Mag. 40, 169 (1920)",http://dx.doi.org/10.1080/14786440808636111,
 cov_radius,Covalent radius,slater,"Slater radius (covalent, metallic and ionic bonds, derived from crystallographic data)","J. C. Slater, J. Chem. Phys. 41, 3199 (1964)",http://dx.doi.org/10.1063/1.1725697,
diff --git a/atomdb/data/elements_data.csv b/atomdb/data/elements_data.csv
index d8621dda..67acd447 100644
--- a/atomdb/data/elements_data.csv
+++ b/atomdb/data/elements_data.csv
@@ -3,7 +3,7 @@
 # Second row indicates  the source/alternative for a given property (can be empty) it is used to link the references with the values,,,,,,,,,,,,,,,,,,,,,,
 # Third row indicates the units,,,,,,,,,,,,,,,,,,,,,,
 ,,,,,,,,,,,,,,,,,,,,,,
-atnum,symbol,name,group,period,multiplicity,cov_radius,cov_radius,cov_radius,vdw_radius,vdw_radius,vdw_radius,vdw_radius,vdw_radius,vdw_radius,vdw_radius,at_radius,at_radius,eneg,pold,pold,c6,mass
+atnum,symbol,name,group,period,mult,cov_radius,cov_radius,cov_radius,vdw_radius,vdw_radius,vdw_radius,vdw_radius,vdw_radius,vdw_radius,vdw_radius,at_radius,at_radius,eneg,pold,pold,c6,mass
 ,,,,,,cordero,bragg,slater,bondi,truhlar,rt,batsanov,dreiding,uff,mm3,wc,cr,pauling,crc,chu,chu,stb
 int,str,str,int,int,int,angstrom,angstrom,angstrom,angstrom,angstrom,angstrom,angstrom,2angstrom,2angstrom,angstrom,angstrom,angstrom,float,angstrom**3,au,au,amu
 1,H,Hydrogen,1,1,2,0.31,,0.25,1.2,,1.1,,3.195,2.886,1.62,0.529192875,0.53,2.2,0.666793,4.5,6.499026705,1.007975
@@ -118,8 +118,9 @@ int,str,str,int,int,int,angstrom,angstrom,angstrom,angstrom,angstrom,angstrom,an
 110,Ds,Darmstadtium,10,7,3,,,,,,,,,,,,,,,,,
 111,Rg,Roentgenium,11,7,2,,,,,,,,,,,,,,,,,
 112,Cn,Copernicium,12,7,1,,,,,,,,,,,,,,,,,
-113,Uut,Ununtrium,13,7,2,,,,,,,,,,,,,,,,,
+113,Nh,Nihonium,13,7,2,,,,,,,,,,,,,,,,,
 114,Fl,Flerovium,14,7,3,,,,,,,,,,,,,,,,,
-115,Uup,Ununpentium,15,7,4,,,,,,,,,,,,,,,,,
+115,Mc,Moscovium,15,7,4,,,,,,,,,,,,,,,,,
 116,Lv,Livermorium,16,7,3,,,,,,,,,,,,,,,,,
-117,Uus,Ununseptium,17,7,2,,,,,,,,,,,,,,,,,
+117,Ts,Tennessine,17,7,2,,,,,,,,,,,,,,,,,
+118,Og,Oganesson,18,7,1,,,,,,,,,,,,,,,,,
diff --git a/atomdb/datasets/gaussian/__init__.py b/atomdb/datasets/gaussian/__init__.py
index d5790940..1148a24e 100644
--- a/atomdb/datasets/gaussian/__init__.py
+++ b/atomdb/datasets/gaussian/__init__.py
@@ -13,473 +13,4 @@
 # You should have received a copy of the GNU General Public License
 # along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
 
-r"""Gaussian density compile function."""
-
-import os
-
-import numpy as np
-
-from gbasis.wrappers import from_iodata
-
-from gbasis.evals.density import evaluate_density as eval_dens
-from gbasis.evals.density import evaluate_density_gradient
-from gbasis.evals.density import evaluate_density_hessian
-from gbasis.evals.density import evaluate_posdef_kinetic_energy_density as eval_pd_ked
-from gbasis.evals.eval_deriv import evaluate_deriv_basis
-from gbasis.evals.eval import evaluate_basis
-
-from grid.onedgrid import UniformInteger
-from grid.rtransform import ExpRTransform
-from grid.atomgrid import AtomGrid
-
-from iodata import load_one
-
-import atomdb
-
-from atomdb.periodic import Element
-
-
-__all__ = [
-    "run",
-]
-
-
-# Parameters to generate an atomic grid from uniform radial grid
-# Use 170 points, lmax = 21 for the Lebedev grid since our basis
-# don't go beyond l=10 in the spherical harmonics.
-BOUND = (1e-30, 2e1)  # (r_min, r_max)
-
-NPOINTS = 1000
-
-SIZE = 170  # Lebedev grid sizes
-
-DEGREE = 21  #  Lebedev grid degrees
-
-
-DOCSTRING = """Gaussian basis densities (UHF) Dataset
-
-Electronic structure and density properties evaluated with def2-svpd basis set
-
-"""
-
-
-def _load_fchk(n_atom, element, n_elec, multi, basis_name, data_path):
-    r"""Load Gaussian fchk file and return the iodata object
-
-    This function finds the fchk file in the data directory corresponding to the given parameters,
-    loads it and returns the iodata object.
-
-    Parameters
-    ----------
-    n_atom : int
-        Atomic number
-    element : str
-        Chemical symbol of the species
-    n_elec : int
-        Number of electrons
-    multi : int
-        Multiplicity
-    basis_name : str
-        Basis set name
-    data_path : str
-        Path to the data directory
-
-    Returns
-    -------
-    iodata : iodata.IOData
-        Iodata object containing the data from the fchk file
-    """
-    bname = basis_name.lower().replace("-", "").replace("*", "p").replace("+", "d")
-    prefix = f"atom_{str(n_atom).zfill(3)}_{element}"
-    tag = f"N{str(n_elec).zfill(2)}_M{multi}"
-    method = f"uhf_{bname}_g09"
-    # fchkpath = os.path.join(os.path.dirname(__file__), f"raw/{prefix}_{tag}_{method}.fchk")
-    fchkpath = os.path.join(data_path, f"gaussian/raw/{prefix}_{tag}_{method}.fchk")
-    return load_one(fchkpath)
-
-
-def eval_orbs_density(one_density_matrix, orb_eval):
-    r"""Return each orbital density evaluated at a set of points
-
-    rho_i(r) = \sum_j P_ij \phi_i(r) \phi_j(r)
-
-    Parameters
-    ----------
-    one_density_matrix : np.ndarray(K_orb, K_orb)
-        One-electron density matrix (1DM) from K orbitals
-    orb_eval : np.ndarray(K_orb, N)
-        orbitals evaluated at a set of grid points (N).
-        These orbitals must be the basis used to evaluate the 1DM.
-
-    Returns
-    -------
-    orb_dens : np.ndarray(K_orb, N)
-        orbitals density at a set of grid points (N)
-    """
-    #
-    # Following lines were taken from Gbasis eval.py module (L60-L61)
-    #
-    density = one_density_matrix.dot(orb_eval)
-    density *= orb_eval
-    return density
-
-
-def eval_orbs_radial_d_density(one_density_matrix, basis, points, transform=None):
-    """Compute the radial derivative of the density orbital components.
-
-    For a set of points, compute the radial derivative of the density component for each orbital
-    given the basis set and the basis transformation matrix.
-
-    Parameters
-    ----------
-    one_density_matrix : np.ndarray(K_orb, K_orb)
-        One-electron density matrix in terms of the given basis set.
-    basis : gbasis.basis.Basis
-        Basis set used to evaluate the radial derivative of the density
-    points : np.ndarray(N, 3)
-        Cartesian coordinates of the points in space (in atomic units) where the derivatives
-        are evaluated.
-        Rows correspond to the points and columns correspond to the :math:`x, y, \text{and} z`
-        components.
-
-    Returns
-    -------
-    radial_orb_d_dens : np.ndarray(K, N)
-        Radial derivative of the density at the set of points for each orbital component.
-    """
-    # compute the basis values for the points output shape (K, N)
-    basis_val = evaluate_basis(basis, points, transform=transform)
-
-    # compute unitary vectors for the directions of the points
-    unitvect_pts = points / np.linalg.norm(points, axis=1)[:, None]
-
-    # array to store the radial derivative of the density (orbital components)
-    output = np.zeros_like(basis_val)
-
-    # orders for the cartesian directions
-    orders_one = np.array(([1, 0, 0], [0, 1, 0], [0, 0, 1]))
-
-    # for each cartesian direction
-    for ind, orders in enumerate(orders_one):
-        # compute the derivative of each orbital for the cartesian direction
-        deriv_comp = evaluate_deriv_basis(basis, points, orders, transform)
-        # compute matrix product of 1RDM and d|phi_i|^2/dx(or y or z) for each point
-        density = 2 * one_density_matrix @ basis_val * deriv_comp
-        # project derivative components to the radial component
-        density *= unitvect_pts[:, ind]
-
-        output += density
-    return output
-
-
-def eval_orbs_radial_dd_density(one_density_matrix, basis, points, transform=None):
-    """Compute the radial second derivative of the density orbital components.
-
-    For a set of points, compute the radial second derivative of the density component for each
-    orbital given the basis set and the basis transformation matrix.
-
-    Parameters
-    ----------
-    one_density_matrix : np.ndarray(K_orb, K_orb)
-        One-electron density matrix in terms of the given basis set.
-    basis : gbasis.basis.Basis
-        Basis set used to evaluate the radial derivative of the density
-    points : np.ndarray(N, 3)
-        Cartesian coordinates of the points in space (in atomic units) where the derivatives
-        are evaluated.
-        Rows correspond to the points and columns correspond to the :math:`x, y, \text{and} z`
-        components.
-
-    Returns
-    -------
-    radial_dd_orb_dens : np.ndarray(K, N)
-        Radial second derivative of the density at the set of points for each orbital component.
-    """
-    # compute unitary vectors for the directions of the points
-    unitvect_pts = points / np.linalg.norm(points, axis=1)[:, None]
-
-    # compute the basis values for the points output shape (K, N)
-    orb_val = evaluate_basis(basis, points, transform=transform)
-
-    # compute first order derivatives of the basis for the points
-    orb_d_x = evaluate_deriv_basis(basis, points, np.array([1, 0, 0]), transform)
-    orb_d_y = evaluate_deriv_basis(basis, points, np.array([0, 1, 0]), transform)
-    orb_d_z = evaluate_deriv_basis(basis, points, np.array([0, 0, 1]), transform)
-
-    # assemble the gradient of basis functions for the points
-    orb_1d = np.array([orb_d_x, orb_d_y, orb_d_z])
-
-    # array to store the radial second derivative of the orbital components of density, shape (K, N)
-    output = np.zeros((one_density_matrix.shape[0], points.shape[0]))
-
-    # for each distinct element of the Hessian
-    for i in range(3):
-        for j in range(i + 1):
-            # cartesian orders for the hessian element [i, j]
-            hess_order = np.array([0, 0, 0])
-            hess_order[i] += 1
-            hess_order[j] += 1
-
-            # derivative of the basis for the points with order hess_order
-            orb_dd_ij = evaluate_deriv_basis(basis, points, hess_order, transform)
-
-            # compute hessian of orbital contributions to the density
-            #  2 * (dphi/di * 1RDM @ dphi/dj +  phi * 1RDM @ Hij)
-            dd_rho_orb_ij = 2 * (
-                np.einsum("il,ij,jl -> jl", orb_dd_ij, one_density_matrix, orb_val)
-                + np.einsum("il,ij,jl -> jl", orb_1d[i], one_density_matrix, orb_1d[j])
-            )
-
-            # project the hessian of the orbital contributions to the density to the radial component
-            increment = np.einsum(
-                "i,ji,i -> ji", unitvect_pts[:, i], dd_rho_orb_ij, unitvect_pts[:, j]
-            )
-            # add the contribution to the output
-            output += increment
-            # if element not in the diagonal, add the symmetric contribution
-            if i != j:
-                output += increment
-    return output
-
-
-def eval_radial_d_density(one_density_matrix, basis, points):
-    """Compute the radial derivative of the density.
-
-    For a set of points, compute the radial derivative of the density
-    given the one-electron density matrix and the basis set.
-
-    Parameters
-    ----------
-    one_density_matrix : np.ndarray(K_orb, K_orb)
-        One-electron density matrix (1DM) from K orbitals
-    basis : gbasis.basis.Basis
-        Basis set used to evaluate the radial derivative of the density
-    points : np.ndarray(N, 3)
-        Set of points where the radial derivative of the density is evaluated
-
-    Returns
-    -------
-    radial_d_density : np.ndarray(N)
-        Radial derivative of the density at the set of points
-    """
-    rho_grad = evaluate_density_gradient(one_density_matrix, basis, points)
-    # compute unitary vectors for the directions of the points
-    unitvect_pts = points / np.linalg.norm(points, axis=1)[:, None]
-    # compute the radial derivative of the density
-    return np.einsum("ij,ij->i", unitvect_pts, rho_grad)
-
-
-def eval_radial_dd_density(one_density_matrix, basis, points):
-    """Compute the radial derivative of the density.
-
-    For a set of points, compute the radial derivative of the density
-    given the one-electron density matrix and the basis set.
-
-    Parameters
-    ----------
-    one_density_matrix : np.ndarray(K_orb, K_orb)
-        One-electron density matrix (1DM) from K orbitals
-    basis : gbasis.basis.Basis
-        Basis set used to evaluate the radial derivative of the density
-    points : np.ndarray(N, 3)
-        Set of points where the radial derivative of the density is evaluated
-
-    Returns
-    -------
-    radial_dd_density : np.ndarray(N)
-        Radial derivative of the density at the set of points
-    """
-    rho_hess = evaluate_density_hessian(one_density_matrix, basis, points)
-    # compute unitary vectors for the directions of the points
-    unitvect_pts = points / np.linalg.norm(points, axis=1)[:, None]
-    # compute the radial second derivative of the density
-    return np.einsum("ij,ijk,ik->i", unitvect_pts, rho_hess, unitvect_pts)
-
-
-def eval_orb_ked(one_density_matrix, basis, points, transform=None):
-    "Adapted from Gbasis"
-    orbt_ked = 0
-    for orders in np.identity(3, dtype=int):
-        deriv_orb_eval_one = evaluate_deriv_basis(basis, points, orders, transform=transform)
-        deriv_orb_eval_two = deriv_orb_eval_one  # if orders_one == orders_two
-        density = one_density_matrix.dot(deriv_orb_eval_two)
-        density *= deriv_orb_eval_one
-        orbt_ked += density
-    return 0.5 * orbt_ked
-
-
-def run(elem, charge, mult, nexc, dataset, datapath):
-    r"""Compile the AtomDB database entry for densities from Gaussian wfn."""
-    # Check arguments
-    if nexc != 0:
-        raise ValueError("Nonzero value of `nexc` is not currently supported")
-
-    # Set up internal variables
-    elem = atomdb.element_symbol(elem)
-    atnum = atomdb.element_number(elem)
-    nelec = atnum - charge
-    nspin = mult - 1
-    n_up = (nelec + nspin) // 2
-    n_dn = (nelec - nspin) // 2
-
-    # Load data from fchk
-    obasis_name = "def2-svpd"
-    data = _load_fchk(atnum, elem, nelec, mult, obasis_name, datapath)
-
-    # Unrestricted Hartree-Fock SCF results
-    energy = data.energy
-    norba = data.mo.norba
-    mo_e_up = data.mo.energies[:norba]
-    mo_e_dn = data.mo.energies[norba:]
-    occs_up = data.mo.occs[:norba]
-    occs_dn = data.mo.occs[norba:]
-    mo_coeffs = data.mo.coeffs  # ndarray(nbasis, norba + norbb)
-    coeffs_a = mo_coeffs[:, :norba]
-    coeffs_b = mo_coeffs[:, norba:]
-
-    # check for inconsistencies in filenames
-    if not np.allclose(np.array([n_up, n_dn]), np.array([sum(occs_up), sum(occs_dn)])):
-        raise ValueError(f"Inconsistent data in fchk file for N: {atnum}, M: {mult} CH: {charge}")
-
-    # Prepare data for computing Species properties
-    # density matrix in AO basis
-    dm1_up = np.dot(coeffs_a * occs_up, coeffs_a.T)
-    dm1_dn = np.dot(coeffs_b * occs_dn, coeffs_b.T)
-    dm1_tot = data.one_rdms["scf"]
-
-    # Make grid
-    onedg = UniformInteger(NPOINTS)  # number of uniform grid points.
-    rgrid = ExpRTransform(*BOUND).transform_1d_grid(onedg)  # radial grid
-    atgrid = AtomGrid(rgrid, degrees=[DEGREE], sizes=[SIZE], center=np.array([0.0, 0.0, 0.0]))
-
-    # Evaluate properties on the grid:
-    # --------------------------------
-    # total and spin-up orbital, and spin-down orbital densities
-    obasis = from_iodata(data)
-    orb_eval = evaluate_basis(obasis, atgrid.points, transform=None)
-    orb_dens_up = eval_orbs_density(dm1_up, orb_eval)
-    orb_dens_dn = eval_orbs_density(dm1_dn, orb_eval)
-    dens_tot = eval_dens(dm1_tot, obasis, atgrid.points, transform=None)
-
-    # total, spin-up orbital, and spin-down orbital first (radial) derivatives of the density
-    d_dens_tot = eval_radial_d_density(dm1_tot, obasis, atgrid.points)
-    orb_d_dens_up = eval_orbs_radial_d_density(dm1_up, obasis, atgrid.points, transform=None)
-    orb_d_dens_dn = eval_orbs_radial_d_density(dm1_dn, obasis, atgrid.points, transform=None)
-
-    # total, spin-up orbital, and spin-down orbital second (radial) derivatives of the density
-    dd_dens_tot = eval_radial_dd_density(dm1_tot, obasis, atgrid.points)
-    orb_dd_dens_up = eval_orbs_radial_dd_density(dm1_up, obasis, atgrid.points, transform=None)
-    orb_dd_dens_dn = eval_orbs_radial_dd_density(dm1_dn, obasis, atgrid.points, transform=None)
-
-    # total, spin-up orbital, and spin-down orbital kinetic energy densities
-    ked_tot = eval_pd_ked(dm1_tot, obasis, atgrid.points, transform=None)
-    orb_ked_up = eval_orb_ked(dm1_up, obasis, atgrid.points, transform=None)
-    orb_ked_dn = eval_orb_ked(dm1_dn, obasis, atgrid.points, transform=None)
-
-    # Spherically average properties:
-    # --------------------------------
-    # total, spin-up orbital, and spin-down orbital densities
-    dens_spherical_avg = atgrid.spherical_average(dens_tot)
-    dens_splines_up = [atgrid.spherical_average(dens) for dens in orb_dens_up]
-    dens_splines_dn = [atgrid.spherical_average(dens) for dens in orb_dens_dn]
-
-    # total, spin-up orbital, and spin-down orbital radial derivatives of the density
-    d_dens_spherical_avg = atgrid.spherical_average(d_dens_tot)
-    d_dens_splines_up = [atgrid.spherical_average(d_dens) for d_dens in orb_d_dens_up]
-    d_dens_splines_dn = [atgrid.spherical_average(d_dens) for d_dens in orb_d_dens_dn]
-
-    # total, spin-up orbital, and spin-down orbital radial second derivatives of the density
-    dd_dens_spherical_avg = atgrid.spherical_average(dd_dens_tot)
-    dd_dens_splines_up = [atgrid.spherical_average(dd_dens) for dd_dens in orb_dd_dens_up]
-    dd_dens_splines_dn = [atgrid.spherical_average(dd_dens) for dd_dens in orb_dd_dens_dn]
-
-    # total, spin-up orbital, and spin-down orbital kinetic energy densities
-    ked_spherical_avg = atgrid.spherical_average(ked_tot)
-    ked_splines_up = [atgrid.spherical_average(dens) for dens in orb_ked_up]
-    ked_splines_dn = [atgrid.spherical_average(dens) for dens in orb_ked_dn]
-
-    # Evaluate interpolated densities in uniform radial grid:
-    # -------------------------------------------------------
-    rs = rgrid.points
-    # total, spin-up orbital, and spin-down orbital densities
-    dens_avg_tot = dens_spherical_avg(rs)
-    orb_dens_avg_up = np.array([spline(rs) for spline in dens_splines_up])
-    orb_dens_avg_dn = np.array([spline(rs) for spline in dens_splines_dn])
-
-    # total, spin-up orbital, and spin-down orbital radial derivatives of the density
-    d_dens_avg_tot = d_dens_spherical_avg(rs)
-    orb_d_dens_avg_up = np.array([spline(rs) for spline in d_dens_splines_up])
-    orb_d_dens_avg_dn = np.array([spline(rs) for spline in d_dens_splines_dn])
-
-    # total, spin-up orbital, and spin-down orbital radial second derivatives of the density
-    dd_dens_avg_tot = dd_dens_spherical_avg(rs)
-    orb_dd_dens_avg_up = np.array([spline(rs) for spline in dd_dens_splines_up])
-    orb_dd_dens_avg_dn = np.array([spline(rs) for spline in dd_dens_splines_dn])
-
-    # total, spin-up orbital, and spin-down orbital kinetic energy densities
-    ked_avg_tot = ked_spherical_avg(rs)
-    orb_ked_avg_up = np.array([spline(rs) for spline in ked_splines_up])
-    orb_ked_avg_dn = np.array([spline(rs) for spline in ked_splines_dn])
-
-    # Get information about the element
-    atom = Element(elem)
-    atmass = atom.mass["stb"]
-    cov_radius, vdw_radius, at_radius, polarizability, dispersion_c6 = [
-        None,
-    ] * 5
-    # overwrite values for neutral atomic species
-    if charge == 0:
-        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
-        polarizability = atom.pold
-        dispersion_c6 = atom.c6
-
-    # Conceptual-DFT properties (WIP)
-    # NOTE: Only the alpha component of the MOs is used bellow
-    mo_energy_occ_up = mo_e_up[:n_up]
-    mo_energy_virt_up = mo_e_up[n_up:]
-    ip = -mo_energy_occ_up[-1]  # - energy_HOMO_alpha
-    ea = -mo_energy_virt_up[0]  # - energy_LUMO_alpha
-    mu = None
-    eta = None
-
-    # Return Species instance
-    return atomdb.Species(
-        dataset,
-        elem,
-        atnum,
-        obasis_name,
-        nelec,
-        nspin,
-        nexc,
-        atmass,
-        cov_radius,
-        vdw_radius,
-        at_radius,
-        polarizability,
-        dispersion_c6,
-        energy,
-        mo_e_up,
-        mo_e_dn,
-        occs_up,
-        occs_dn,
-        ip,
-        mu,
-        eta,
-        rs=rs,
-        # Density
-        mo_dens_a=orb_dens_avg_up.flatten(),
-        mo_dens_b=orb_dens_avg_dn.flatten(),
-        dens_tot=dens_avg_tot,
-        # Density gradient
-        mo_d_dens_a=orb_d_dens_avg_up.flatten(),
-        mo_d_dens_b=orb_d_dens_avg_dn.flatten(),
-        d_dens_tot=d_dens_avg_tot,
-        # Density laplacian
-        mo_dd_dens_a=orb_dd_dens_avg_up.flatten(),
-        mo_dd_dens_b=orb_dd_dens_avg_dn.flatten(),
-        dd_dens_tot=dd_dens_avg_tot,
-        # KED
-        mo_ked_a=orb_ked_avg_up.flatten(),
-        mo_ked_b=orb_ked_avg_dn.flatten(),
-        ked_tot=ked_avg_tot,
-    )
+r"""Gaussian dataset."""
diff --git a/atomdb/datasets/gaussian/run.py b/atomdb/datasets/gaussian/run.py
new file mode 100644
index 00000000..2f165957
--- /dev/null
+++ b/atomdb/datasets/gaussian/run.py
@@ -0,0 +1,485 @@
+# This file is part of AtomDB.
+#
+# AtomDB is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# AtomDB is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
+
+r"""Gaussian density compile function."""
+
+import os
+
+import numpy as np
+
+from gbasis.wrappers import from_iodata
+
+from gbasis.evals.density import evaluate_density as eval_dens
+from gbasis.evals.density import evaluate_density_gradient
+from gbasis.evals.density import evaluate_density_hessian
+from gbasis.evals.density import evaluate_posdef_kinetic_energy_density as eval_pd_ked
+from gbasis.evals.eval_deriv import evaluate_deriv_basis
+from gbasis.evals.eval import evaluate_basis
+
+from grid.onedgrid import UniformInteger
+from grid.rtransform import ExpRTransform
+from grid.atomgrid import AtomGrid
+
+from iodata import load_one
+
+import atomdb
+
+from atomdb.periodic import Element
+
+
+__all__ = [
+    "run",
+]
+
+
+# Parameters to generate an atomic grid from uniform radial grid
+# Use 170 points, lmax = 21 for the Lebedev grid since our basis
+# don't go beyond l=10 in the spherical harmonics.
+BOUND = (1e-30, 2e1)  # (r_min, r_max)
+
+NPOINTS = 1000
+
+SIZE = 170  # Lebedev grid sizes
+
+DEGREE = 21  #  Lebedev grid degrees
+
+
+DOCSTRING = """Gaussian basis densities (UHF) Dataset
+
+Electronic structure and density properties evaluated with def2-svpd basis set
+
+"""
+
+
+def _load_fchk(n_atom, element, n_elec, multi, basis_name, data_path):
+    r"""Load Gaussian fchk file and return the iodata object
+
+    This function finds the fchk file in the data directory corresponding to the given parameters,
+    loads it and returns the iodata object.
+
+    Parameters
+    ----------
+    n_atom : int
+        Atomic number
+    element : str
+        Chemical symbol of the species
+    n_elec : int
+        Number of electrons
+    multi : int
+        Multiplicity
+    basis_name : str
+        Basis set name
+    data_path : str
+        Path to the data directory
+
+    Returns
+    -------
+    iodata : iodata.IOData
+        Iodata object containing the data from the fchk file
+    """
+    bname = basis_name.lower().replace("-", "").replace("*", "p").replace("+", "d")
+    prefix = f"atom_{str(n_atom).zfill(3)}_{element}"
+    tag = f"N{str(n_elec).zfill(2)}_M{multi}"
+    method = f"uhf_{bname}_g09"
+    # fchkpath = os.path.join(os.path.dirname(__file__), f"raw/{prefix}_{tag}_{method}.fchk")
+    fchkpath = os.path.join(data_path, f"gaussian/raw/{prefix}_{tag}_{method}.fchk")
+    return load_one(fchkpath)
+
+
+def eval_orbs_density(one_density_matrix, orb_eval):
+    r"""Return each orbital density evaluated at a set of points
+
+    rho_i(r) = \sum_j P_ij \phi_i(r) \phi_j(r)
+
+    Parameters
+    ----------
+    one_density_matrix : np.ndarray(K_orb, K_orb)
+        One-electron density matrix (1DM) from K orbitals
+    orb_eval : np.ndarray(K_orb, N)
+        orbitals evaluated at a set of grid points (N).
+        These orbitals must be the basis used to evaluate the 1DM.
+
+    Returns
+    -------
+    orb_dens : np.ndarray(K_orb, N)
+        orbitals density at a set of grid points (N)
+    """
+    #
+    # Following lines were taken from Gbasis eval.py module (L60-L61)
+    #
+    density = one_density_matrix.dot(orb_eval)
+    density *= orb_eval
+    return density
+
+
+def eval_orbs_radial_d_density(one_density_matrix, basis, points, transform=None):
+    """Compute the radial derivative of the density orbital components.
+
+    For a set of points, compute the radial derivative of the density component for each orbital
+    given the basis set and the basis transformation matrix.
+
+    Parameters
+    ----------
+    one_density_matrix : np.ndarray(K_orb, K_orb)
+        One-electron density matrix in terms of the given basis set.
+    basis : gbasis.basis.Basis
+        Basis set used to evaluate the radial derivative of the density
+    points : np.ndarray(N, 3)
+        Cartesian coordinates of the points in space (in atomic units) where the derivatives
+        are evaluated.
+        Rows correspond to the points and columns correspond to the :math:`x, y, \text{and} z`
+        components.
+
+    Returns
+    -------
+    radial_orb_d_dens : np.ndarray(K, N)
+        Radial derivative of the density at the set of points for each orbital component.
+    """
+    # compute the basis values for the points output shape (K, N)
+    basis_val = evaluate_basis(basis, points, transform=transform)
+
+    # compute unitary vectors for the directions of the points
+    unitvect_pts = points / np.linalg.norm(points, axis=1)[:, None]
+
+    # array to store the radial derivative of the density (orbital components)
+    output = np.zeros_like(basis_val)
+
+    # orders for the cartesian directions
+    orders_one = np.array(([1, 0, 0], [0, 1, 0], [0, 0, 1]))
+
+    # for each cartesian direction
+    for ind, orders in enumerate(orders_one):
+        # compute the derivative of each orbital for the cartesian direction
+        deriv_comp = evaluate_deriv_basis(basis, points, orders, transform)
+        # compute matrix product of 1RDM and d|phi_i|^2/dx(or y or z) for each point
+        density = 2 * one_density_matrix @ basis_val * deriv_comp
+        # project derivative components to the radial component
+        density *= unitvect_pts[:, ind]
+
+        output += density
+    return output
+
+
+def eval_orbs_radial_dd_density(one_density_matrix, basis, points, transform=None):
+    """Compute the radial second derivative of the density orbital components.
+
+    For a set of points, compute the radial second derivative of the density component for each
+    orbital given the basis set and the basis transformation matrix.
+
+    Parameters
+    ----------
+    one_density_matrix : np.ndarray(K_orb, K_orb)
+        One-electron density matrix in terms of the given basis set.
+    basis : gbasis.basis.Basis
+        Basis set used to evaluate the radial derivative of the density
+    points : np.ndarray(N, 3)
+        Cartesian coordinates of the points in space (in atomic units) where the derivatives
+        are evaluated.
+        Rows correspond to the points and columns correspond to the :math:`x, y, \text{and} z`
+        components.
+
+    Returns
+    -------
+    radial_dd_orb_dens : np.ndarray(K, N)
+        Radial second derivative of the density at the set of points for each orbital component.
+    """
+    # compute unitary vectors for the directions of the points
+    unitvect_pts = points / np.linalg.norm(points, axis=1)[:, None]
+
+    # compute the basis values for the points output shape (K, N)
+    orb_val = evaluate_basis(basis, points, transform=transform)
+
+    # compute first order derivatives of the basis for the points
+    orb_d_x = evaluate_deriv_basis(basis, points, np.array([1, 0, 0]), transform)
+    orb_d_y = evaluate_deriv_basis(basis, points, np.array([0, 1, 0]), transform)
+    orb_d_z = evaluate_deriv_basis(basis, points, np.array([0, 0, 1]), transform)
+
+    # assemble the gradient of basis functions for the points
+    orb_1d = np.array([orb_d_x, orb_d_y, orb_d_z])
+
+    # array to store the radial second derivative of the orbital components of density, shape (K, N)
+    output = np.zeros((one_density_matrix.shape[0], points.shape[0]))
+
+    # for each distinct element of the Hessian
+    for i in range(3):
+        for j in range(i + 1):
+            # cartesian orders for the hessian element [i, j]
+            hess_order = np.array([0, 0, 0])
+            hess_order[i] += 1
+            hess_order[j] += 1
+
+            # derivative of the basis for the points with order hess_order
+            orb_dd_ij = evaluate_deriv_basis(basis, points, hess_order, transform)
+
+            # compute hessian of orbital contributions to the density
+            #  2 * (dphi/di * 1RDM @ dphi/dj +  phi * 1RDM @ Hij)
+            dd_rho_orb_ij = 2 * (
+                np.einsum("il,ij,jl -> jl", orb_dd_ij, one_density_matrix, orb_val)
+                + np.einsum("il,ij,jl -> jl", orb_1d[i], one_density_matrix, orb_1d[j])
+            )
+
+            # project the hessian of the orbital contributions to the density to the radial component
+            increment = np.einsum(
+                "i,ji,i -> ji", unitvect_pts[:, i], dd_rho_orb_ij, unitvect_pts[:, j]
+            )
+            # add the contribution to the output
+            output += increment
+            # if element not in the diagonal, add the symmetric contribution
+            if i != j:
+                output += increment
+    return output
+
+
+def eval_radial_d_density(one_density_matrix, basis, points):
+    """Compute the radial derivative of the density.
+
+    For a set of points, compute the radial derivative of the density
+    given the one-electron density matrix and the basis set.
+
+    Parameters
+    ----------
+    one_density_matrix : np.ndarray(K_orb, K_orb)
+        One-electron density matrix (1DM) from K orbitals
+    basis : gbasis.basis.Basis
+        Basis set used to evaluate the radial derivative of the density
+    points : np.ndarray(N, 3)
+        Set of points where the radial derivative of the density is evaluated
+
+    Returns
+    -------
+    radial_d_density : np.ndarray(N)
+        Radial derivative of the density at the set of points
+    """
+    rho_grad = evaluate_density_gradient(one_density_matrix, basis, points)
+    # compute unitary vectors for the directions of the points
+    unitvect_pts = points / np.linalg.norm(points, axis=1)[:, None]
+    # compute the radial derivative of the density
+    return np.einsum("ij,ij->i", unitvect_pts, rho_grad)
+
+
+def eval_radial_dd_density(one_density_matrix, basis, points):
+    """Compute the radial derivative of the density.
+
+    For a set of points, compute the radial derivative of the density
+    given the one-electron density matrix and the basis set.
+
+    Parameters
+    ----------
+    one_density_matrix : np.ndarray(K_orb, K_orb)
+        One-electron density matrix (1DM) from K orbitals
+    basis : gbasis.basis.Basis
+        Basis set used to evaluate the radial derivative of the density
+    points : np.ndarray(N, 3)
+        Set of points where the radial derivative of the density is evaluated
+
+    Returns
+    -------
+    radial_dd_density : np.ndarray(N)
+        Radial derivative of the density at the set of points
+    """
+    rho_hess = evaluate_density_hessian(one_density_matrix, basis, points)
+    # compute unitary vectors for the directions of the points
+    unitvect_pts = points / np.linalg.norm(points, axis=1)[:, None]
+    # compute the radial second derivative of the density
+    return np.einsum("ij,ijk,ik->i", unitvect_pts, rho_hess, unitvect_pts)
+
+
+def eval_orb_ked(one_density_matrix, basis, points, transform=None):
+    "Adapted from Gbasis"
+    orbt_ked = 0
+    for orders in np.identity(3, dtype=int):
+        deriv_orb_eval_one = evaluate_deriv_basis(basis, points, orders, transform=transform)
+        deriv_orb_eval_two = deriv_orb_eval_one  # if orders_one == orders_two
+        density = one_density_matrix.dot(deriv_orb_eval_two)
+        density *= deriv_orb_eval_one
+        orbt_ked += density
+    return 0.5 * orbt_ked
+
+
+def run(elem, charge, mult, nexc, dataset, datapath):
+    r"""Compile the AtomDB database entry for densities from Gaussian wfn."""
+    # Check arguments
+    if nexc != 0:
+        raise ValueError("Nonzero value of `nexc` is not currently supported")
+
+    # Set up internal variables
+    elem = atomdb.element_symbol(elem)
+    atnum = atomdb.element_number(elem)
+    nelec = atnum - charge
+    nspin = mult - 1
+    n_up = (nelec + nspin) // 2
+    n_dn = (nelec - nspin) // 2
+
+    # Load data from fchk
+    obasis_name = "def2-svpd"
+    data = _load_fchk(atnum, elem, nelec, mult, obasis_name, datapath)
+
+    # Unrestricted Hartree-Fock SCF results
+    energy = data.energy
+    norba = data.mo.norba
+    mo_e_up = data.mo.energies[:norba]
+    mo_e_dn = data.mo.energies[norba:]
+    occs_up = data.mo.occs[:norba]
+    occs_dn = data.mo.occs[norba:]
+    mo_coeffs = data.mo.coeffs  # ndarray(nbasis, norba + norbb)
+    coeffs_a = mo_coeffs[:, :norba]
+    coeffs_b = mo_coeffs[:, norba:]
+
+    # check for inconsistencies in filenames
+    if not np.allclose(np.array([n_up, n_dn]), np.array([sum(occs_up), sum(occs_dn)])):
+        raise ValueError(f"Inconsistent data in fchk file for N: {atnum}, M: {mult} CH: {charge}")
+
+    # Prepare data for computing Species properties
+    # density matrix in AO basis
+    dm1_up = np.dot(coeffs_a * occs_up, coeffs_a.T)
+    dm1_dn = np.dot(coeffs_b * occs_dn, coeffs_b.T)
+    dm1_tot = data.one_rdms["scf"]
+
+    # Make grid
+    onedg = UniformInteger(NPOINTS)  # number of uniform grid points.
+    rgrid = ExpRTransform(*BOUND).transform_1d_grid(onedg)  # radial grid
+    atgrid = AtomGrid(rgrid, degrees=[DEGREE], sizes=[SIZE], center=np.array([0.0, 0.0, 0.0]))
+
+    # Evaluate properties on the grid:
+    # --------------------------------
+    # total and spin-up orbital, and spin-down orbital densities
+    obasis = from_iodata(data)
+    orb_eval = evaluate_basis(obasis, atgrid.points, transform=None)
+    orb_dens_up = eval_orbs_density(dm1_up, orb_eval)
+    orb_dens_dn = eval_orbs_density(dm1_dn, orb_eval)
+    dens_tot = eval_dens(dm1_tot, obasis, atgrid.points, transform=None)
+
+    # total, spin-up orbital, and spin-down orbital first (radial) derivatives of the density
+    d_dens_tot = eval_radial_d_density(dm1_tot, obasis, atgrid.points)
+    orb_d_dens_up = eval_orbs_radial_d_density(dm1_up, obasis, atgrid.points, transform=None)
+    orb_d_dens_dn = eval_orbs_radial_d_density(dm1_dn, obasis, atgrid.points, transform=None)
+
+    # total, spin-up orbital, and spin-down orbital second (radial) derivatives of the density
+    dd_dens_tot = eval_radial_dd_density(dm1_tot, obasis, atgrid.points)
+    orb_dd_dens_up = eval_orbs_radial_dd_density(dm1_up, obasis, atgrid.points, transform=None)
+    orb_dd_dens_dn = eval_orbs_radial_dd_density(dm1_dn, obasis, atgrid.points, transform=None)
+
+    # total, spin-up orbital, and spin-down orbital kinetic energy densities
+    ked_tot = eval_pd_ked(dm1_tot, obasis, atgrid.points, transform=None)
+    orb_ked_up = eval_orb_ked(dm1_up, obasis, atgrid.points, transform=None)
+    orb_ked_dn = eval_orb_ked(dm1_dn, obasis, atgrid.points, transform=None)
+
+    # Spherically average properties:
+    # --------------------------------
+    # total, spin-up orbital, and spin-down orbital densities
+    dens_spherical_avg = atgrid.spherical_average(dens_tot)
+    dens_splines_up = [atgrid.spherical_average(dens) for dens in orb_dens_up]
+    dens_splines_dn = [atgrid.spherical_average(dens) for dens in orb_dens_dn]
+
+    # total, spin-up orbital, and spin-down orbital radial derivatives of the density
+    d_dens_spherical_avg = atgrid.spherical_average(d_dens_tot)
+    d_dens_splines_up = [atgrid.spherical_average(d_dens) for d_dens in orb_d_dens_up]
+    d_dens_splines_dn = [atgrid.spherical_average(d_dens) for d_dens in orb_d_dens_dn]
+
+    # total, spin-up orbital, and spin-down orbital radial second derivatives of the density
+    dd_dens_spherical_avg = atgrid.spherical_average(dd_dens_tot)
+    dd_dens_splines_up = [atgrid.spherical_average(dd_dens) for dd_dens in orb_dd_dens_up]
+    dd_dens_splines_dn = [atgrid.spherical_average(dd_dens) for dd_dens in orb_dd_dens_dn]
+
+    # total, spin-up orbital, and spin-down orbital kinetic energy densities
+    ked_spherical_avg = atgrid.spherical_average(ked_tot)
+    ked_splines_up = [atgrid.spherical_average(dens) for dens in orb_ked_up]
+    ked_splines_dn = [atgrid.spherical_average(dens) for dens in orb_ked_dn]
+
+    # Evaluate interpolated densities in uniform radial grid:
+    # -------------------------------------------------------
+    rs = rgrid.points
+    # total, spin-up orbital, and spin-down orbital densities
+    dens_avg_tot = dens_spherical_avg(rs)
+    orb_dens_avg_up = np.array([spline(rs) for spline in dens_splines_up])
+    orb_dens_avg_dn = np.array([spline(rs) for spline in dens_splines_dn])
+
+    # total, spin-up orbital, and spin-down orbital radial derivatives of the density
+    d_dens_avg_tot = d_dens_spherical_avg(rs)
+    orb_d_dens_avg_up = np.array([spline(rs) for spline in d_dens_splines_up])
+    orb_d_dens_avg_dn = np.array([spline(rs) for spline in d_dens_splines_dn])
+
+    # total, spin-up orbital, and spin-down orbital radial second derivatives of the density
+    dd_dens_avg_tot = dd_dens_spherical_avg(rs)
+    orb_dd_dens_avg_up = np.array([spline(rs) for spline in dd_dens_splines_up])
+    orb_dd_dens_avg_dn = np.array([spline(rs) for spline in dd_dens_splines_dn])
+
+    # total, spin-up orbital, and spin-down orbital kinetic energy densities
+    ked_avg_tot = ked_spherical_avg(rs)
+    orb_ked_avg_up = np.array([spline(rs) for spline in ked_splines_up])
+    orb_ked_avg_dn = np.array([spline(rs) for spline in ked_splines_dn])
+
+    # Get information about the element
+    atom = Element(elem)
+    atmass = atom.mass["stb"]
+    cov_radius, vdw_radius, at_radius, polarizability, dispersion = [
+        None,
+    ] * 5
+    # overwrite values for neutral atomic species
+    if charge == 0:
+        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
+        polarizability = atom.pold
+        dispersion = {"C6": atom.c6}
+
+    # Conceptual-DFT properties (WIP)
+    # NOTE: Only the alpha component of the MOs is used bellow
+    mo_energy_occ_up = mo_e_up[:n_up]
+    mo_energy_virt_up = mo_e_up[n_up:]
+    ip = -mo_energy_occ_up[-1]  # - energy_HOMO_alpha
+    ea = -mo_energy_virt_up[0]  # - energy_LUMO_alpha
+    mu = None
+    eta = None
+
+    # Return Species instance
+    fields = dict(
+        elem=elem,
+        atnum=atnum,
+        obasis_name=obasis_name,
+        nelec=nelec,
+        nspin=nspin,
+        nexc=nexc,
+        atmass=atmass,
+        cov_radius=cov_radius,
+        vdw_radius=vdw_radius,
+        at_radius=at_radius,
+        polarizability=polarizability,
+        dispersion=dispersion,
+        energy=energy,
+        mo_energy_a=mo_e_up,
+        mo_energy_b=mo_e_dn,
+        mo_occs_a=occs_up,
+        mo_occs_b=occs_dn,
+        ip=ip,
+        mu=mu,
+        eta=eta,
+        rs=rs,
+        # Density
+        mo_dens_a=orb_dens_avg_up.flatten(),
+        mo_dens_b=orb_dens_avg_dn.flatten(),
+        dens_tot=dens_avg_tot,
+        # Density gradient
+        mo_d_dens_a=orb_d_dens_avg_up.flatten(),
+        mo_d_dens_b=orb_d_dens_avg_dn.flatten(),
+        d_dens_tot=d_dens_avg_tot,
+        # Density laplacian
+        mo_dd_dens_a=orb_dd_dens_avg_up.flatten(),
+        mo_dd_dens_b=orb_dd_dens_avg_dn.flatten(),
+        dd_dens_tot=dd_dens_avg_tot,
+        # KED
+        mo_ked_a=orb_ked_avg_up.flatten(),
+        mo_ked_b=orb_ked_avg_dn.flatten(),
+        ked_tot=ked_avg_tot,
+    )
+    return atomdb.Species(dataset, fields)
diff --git a/atomdb/datasets/hci_augccpwcvqz/__init__.py b/atomdb/datasets/hci_augccpwcvqz/__init__.py
index ceaaab1b..6a07392c 100644
--- a/atomdb/datasets/hci_augccpwcvqz/__init__.py
+++ b/atomdb/datasets/hci_augccpwcvqz/__init__.py
@@ -13,207 +13,4 @@
 # You should have received a copy of the GNU General Public License
 # along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
 
-r"""HCI compile function."""
-
-import numpy as np
-
-from iodata import load_one
-
-from gbasis.wrappers import from_iodata
-
-from gbasis.evals.density import evaluate_density as eval_dens
-from gbasis.evals.density import evaluate_deriv_density as eval_d_dens
-from gbasis.evals.density import evaluate_posdef_kinetic_energy_density as eval_pd_ked
-from gbasis.evals.density import evaluate_basis
-from gbasis.evals.eval_deriv import evaluate_deriv_basis
-
-from grid.onedgrid import UniformInteger
-from grid.rtransform import ExpRTransform
-from grid.atomgrid import AtomGrid
-
-import atomdb
-
-
-__all__ = [
-    "run",
-]
-
-
-# Parameters to generate an atomic grid from uniform radial grid
-# Use 170 points, lmax = 21 for the Lebedev grid since our basis
-# don't go beyond l=10 in the spherical harmonics.
-BOUND = (1e-5, 2e1)  # (r_min, r_max)
-
-NPOINTS = 100
-
-SIZE = 170  # Lebedev grid sizes
-
-DEGREE = 21  #  Lebedev grid degrees
-
-
-BASIS = "aug-ccpwCVQZ"
-
-
-DOCSTRING = """Heat-bath Configuration Interaction (HCI) Dataset
-
-Electronic structure and density properties evaluated with aug-ccpwCVQZ basis set
-
-"""
-
-
-def eval_orbs_density(one_density_matrix, orb_eval):
-    r"""Return each orbital density evaluated at a set of points
-
-    rho_i(r) = \sum_j P_ij \phi_i(r) \phi_j(r)
-
-    Parameters
-    ----------
-    one_density_matrix : np.ndarray(K_orb, K_orb)
-        One-electron density matrix (1DM) from K orbitals
-    orb_eval : np.ndarray(K_orb, N)
-        orbitals evaluated at a set of grid points (N).
-        These orbitals must be the basis used to evaluate the 1DM.
-
-    Returns
-    -------
-    orb_dens : np.ndarray(K_orb, N)
-        orbitals density at a set of grid points (N)
-    """
-    #
-    # Following lines were taken from Gbasis eval.py module (L60-L61)
-    #
-    density = one_density_matrix.dot(orb_eval)
-    density *= orb_eval
-    return density
-
-
-def eval_orb_ked(one_density_matrix, basis, points, transform=None, coord_type="spherical"):
-    "Adapted from Gbasis"
-    orbt_ked = 0
-    for orders in np.identity(3, dtype=int):
-        deriv_orb_eval_one = evaluate_deriv_basis(
-            basis, points, orders, transform=transform, coord_type=coord_type
-        )
-        deriv_orb_eval_two = deriv_orb_eval_one  # orders_one == orders_two
-        density = one_density_matrix.dot(deriv_orb_eval_two)
-        density *= deriv_orb_eval_one
-        orbt_ked += density
-    return 0.5 * orbt_ked
-
-
-def run(elem, charge, mult, nexc, dataset, datapath):
-    r"""Run an HCI computation and compile the AtomDB database entry."""
-    # Check arguments
-    if nexc != 0:
-        raise ValueError("Nonzero value of `nexc` is not currently supported")
-
-    # Set up internal variables
-    elem = atomdb.element_symbol(elem)
-    natom = atomdb.element_number(elem)
-    nelec = natom - charge
-    nspin = mult - 1
-    n_up = (nelec + nspin) // 2
-    n_dn = (nelec - nspin) // 2
-    basis = BASIS
-
-    # Load restricted Hartree-Fock SCF
-    scfdata = load_one(atomdb.datafile(".molden", elem, charge, mult, nexc, dataset, datapath))
-    norba = scfdata.mo.norba
-    mo_e_up = scfdata.mo.energies[:norba]
-    mo_e_dn = mo_e_up  # since only alpha MO information in .molden
-    occs_up = scfdata.mo.occs  # ndarray(nbasis, nmos)
-    occs_dn = None
-    mo_coeff = scfdata.mo.coeffs
-
-    # Load HCI data
-    data = np.load(atomdb.datafile(".ci.npz", elem, charge, mult, nexc, dataset, datapath))
-    energy = data["energy"][0]
-
-    # Prepare data for computing Species properties
-    dm1_up, dm1_dn = data["rdm1"]
-    dm1_tot = dm1_up + dm1_dn
-
-    # Make grid
-    onedg = UniformInteger(NPOINTS)  # number of uniform grid points.
-    rgrid = ExpRTransform(*BOUND).transform_1d_grid(onedg)  # radial grid
-    atgrid = AtomGrid(rgrid, degrees=[DEGREE], sizes=[SIZE], center=np.array([0.0, 0.0, 0.0]))
-
-    # Compute densities
-    obasis, coord_types = from_iodata(scfdata)
-    orb_eval = evaluate_basis(obasis, atgrid.points, coord_type=coord_types, transform=mo_coeff.T)
-    orb_dens_up = eval_orbs_density(dm1_up, orb_eval)
-    orb_dens_dn = eval_orbs_density(dm1_dn, orb_eval)
-    # orb_dens_tot = orb_dens_up + orb_dens_dn
-    # dens_tot = np.sum(orb_dens_tot, axis=0)
-    dens_tot = eval_dens(
-        dm1_tot, obasis, atgrid.points, coord_type=coord_types, transform=mo_coeff.T
-    )
-
-    # Compute kinetic energy density
-    orb_ked_up = eval_orb_ked(
-        dm1_up, obasis, atgrid.points, transform=mo_coeff.T, coord_type=coord_types
-    )
-    orb_ked_dn = eval_orb_ked(
-        dm1_dn, obasis, atgrid.points, transform=mo_coeff.T, coord_type=coord_types
-    )
-    ked_tot = eval_pd_ked(
-        dm1_tot, obasis, atgrid.points, coord_type=coord_types, transform=mo_coeff.T
-    )
-
-    # Density and KED spherical average
-    dens_spherical_avg = atgrid.spherical_average(dens_tot)
-    ked_spherical_avg = atgrid.spherical_average(ked_tot)
-    dens_splines_up = [atgrid.spherical_average(dens) for dens in orb_dens_up]
-    dens_splines_dn = [atgrid.spherical_average(dens) for dens in orb_dens_dn]
-    ked_splines_up = [atgrid.spherical_average(dens) for dens in orb_ked_up]
-    ked_splines_dn = [atgrid.spherical_average(dens) for dens in orb_ked_dn]
-    # Evaluate interpolated densities in a uniform radial grid
-    rs = rgrid.points
-    dens_avg_tot = dens_spherical_avg(rs)
-    orb_dens_avg_up = np.array([spline(rs) for spline in dens_splines_up])
-    orb_dens_avg_dn = np.array([spline(rs) for spline in dens_splines_dn])
-    ked_avg_tot = ked_spherical_avg(rs)
-    orb_ked_avg_up = np.array([spline(rs) for spline in ked_splines_up])
-    orb_ked_avg_dn = np.array([spline(rs) for spline in ked_splines_dn])
-
-    #
-    # Element properties
-    #
-    cov_radii, vdw_radii, mass = atomdb.get_element_data(elem)
-    if charge != 0:
-        cov_radii, vdw_radii = [None, None]  # overwrite values for charged species
-    #
-    # Conceptual-DFT properties (TODO)
-    #
-    ip = None
-    mu = None
-    eta = None
-
-    # Return Species instance
-    return atomdb.Species(
-        dataset,
-        elem,
-        natom,
-        basis,
-        nelec,
-        nspin,
-        nexc,
-        cov_radii,
-        vdw_radii,
-        mass,
-        energy,
-        mo_e_up,
-        mo_e_dn,
-        occs_up,
-        occs_dn,
-        ip,
-        mu,
-        eta,
-        rs=rs,
-        _orb_dens_up=orb_dens_avg_up.flatten(),
-        _orb_dens_dn=orb_dens_avg_dn.flatten(),
-        dens_tot=dens_avg_tot,
-        _orb_ked_up=orb_ked_avg_up.flatten(),
-        _orb_ked_dn=orb_ked_avg_dn.flatten(),
-        ked_tot=ked_avg_tot,
-    )
+r"""HCI dataset."""
diff --git a/atomdb/datasets/hci_augccpwcvqz/run.py b/atomdb/datasets/hci_augccpwcvqz/run.py
new file mode 100644
index 00000000..049f110e
--- /dev/null
+++ b/atomdb/datasets/hci_augccpwcvqz/run.py
@@ -0,0 +1,219 @@
+# This file is part of AtomDB.
+#
+# AtomDB is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# AtomDB is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
+
+r"""HCI compile function."""
+
+import numpy as np
+
+from iodata import load_one
+
+from gbasis.wrappers import from_iodata
+
+from gbasis.evals.density import evaluate_density as eval_dens
+from gbasis.evals.density import evaluate_deriv_density as eval_d_dens
+from gbasis.evals.density import evaluate_posdef_kinetic_energy_density as eval_pd_ked
+from gbasis.evals.density import evaluate_basis
+from gbasis.evals.eval_deriv import evaluate_deriv_basis
+
+from grid.onedgrid import UniformInteger
+from grid.rtransform import ExpRTransform
+from grid.atomgrid import AtomGrid
+
+import atomdb
+
+
+__all__ = [
+    "run",
+]
+
+
+# Parameters to generate an atomic grid from uniform radial grid
+# Use 170 points, lmax = 21 for the Lebedev grid since our basis
+# don't go beyond l=10 in the spherical harmonics.
+BOUND = (1e-5, 2e1)  # (r_min, r_max)
+
+NPOINTS = 100
+
+SIZE = 170  # Lebedev grid sizes
+
+DEGREE = 21  #  Lebedev grid degrees
+
+
+BASIS = "aug-ccpwCVQZ"
+
+
+DOCSTRING = """Heat-bath Configuration Interaction (HCI) Dataset
+
+Electronic structure and density properties evaluated with aug-ccpwCVQZ basis set
+
+"""
+
+
+def eval_orbs_density(one_density_matrix, orb_eval):
+    r"""Return each orbital density evaluated at a set of points
+
+    rho_i(r) = \sum_j P_ij \phi_i(r) \phi_j(r)
+
+    Parameters
+    ----------
+    one_density_matrix : np.ndarray(K_orb, K_orb)
+        One-electron density matrix (1DM) from K orbitals
+    orb_eval : np.ndarray(K_orb, N)
+        orbitals evaluated at a set of grid points (N).
+        These orbitals must be the basis used to evaluate the 1DM.
+
+    Returns
+    -------
+    orb_dens : np.ndarray(K_orb, N)
+        orbitals density at a set of grid points (N)
+    """
+    #
+    # Following lines were taken from Gbasis eval.py module (L60-L61)
+    #
+    density = one_density_matrix.dot(orb_eval)
+    density *= orb_eval
+    return density
+
+
+def eval_orb_ked(one_density_matrix, basis, points, transform=None, coord_type="spherical"):
+    "Adapted from Gbasis"
+    orbt_ked = 0
+    for orders in np.identity(3, dtype=int):
+        deriv_orb_eval_one = evaluate_deriv_basis(
+            basis, points, orders, transform=transform, coord_type=coord_type
+        )
+        deriv_orb_eval_two = deriv_orb_eval_one  # orders_one == orders_two
+        density = one_density_matrix.dot(deriv_orb_eval_two)
+        density *= deriv_orb_eval_one
+        orbt_ked += density
+    return 0.5 * orbt_ked
+
+
+def run(elem, charge, mult, nexc, dataset, datapath):
+    r"""Run an HCI computation and compile the AtomDB database entry."""
+    # Check arguments
+    if nexc != 0:
+        raise ValueError("Nonzero value of `nexc` is not currently supported")
+
+    # Set up internal variables
+    elem = atomdb.element_symbol(elem)
+    natom = atomdb.element_number(elem)
+    nelec = natom - charge
+    nspin = mult - 1
+    n_up = (nelec + nspin) // 2
+    n_dn = (nelec - nspin) // 2
+    basis = BASIS
+
+    # Load restricted Hartree-Fock SCF
+    scfdata = load_one(atomdb.datafile(".molden", elem, charge, mult, nexc, dataset, datapath))
+    norba = scfdata.mo.norba
+    mo_e_up = scfdata.mo.energies[:norba]
+    mo_e_dn = mo_e_up  # since only alpha MO information in .molden
+    occs_up = scfdata.mo.occs  # ndarray(nbasis, nmos)
+    occs_dn = None
+    mo_coeff = scfdata.mo.coeffs
+
+    # Load HCI data
+    data = np.load(atomdb.datafile(".ci.npz", elem, charge, mult, nexc, dataset, datapath))
+    energy = data["energy"][0]
+
+    # Prepare data for computing Species properties
+    dm1_up, dm1_dn = data["rdm1"]
+    dm1_tot = dm1_up + dm1_dn
+
+    # Make grid
+    onedg = UniformInteger(NPOINTS)  # number of uniform grid points.
+    rgrid = ExpRTransform(*BOUND).transform_1d_grid(onedg)  # radial grid
+    atgrid = AtomGrid(rgrid, degrees=[DEGREE], sizes=[SIZE], center=np.array([0.0, 0.0, 0.0]))
+
+    # Compute densities
+    obasis, coord_types = from_iodata(scfdata)
+    orb_eval = evaluate_basis(obasis, atgrid.points, coord_type=coord_types, transform=mo_coeff.T)
+    orb_dens_up = eval_orbs_density(dm1_up, orb_eval)
+    orb_dens_dn = eval_orbs_density(dm1_dn, orb_eval)
+    # orb_dens_tot = orb_dens_up + orb_dens_dn
+    # dens_tot = np.sum(orb_dens_tot, axis=0)
+    dens_tot = eval_dens(
+        dm1_tot, obasis, atgrid.points, coord_type=coord_types, transform=mo_coeff.T
+    )
+
+    # Compute kinetic energy density
+    orb_ked_up = eval_orb_ked(
+        dm1_up, obasis, atgrid.points, transform=mo_coeff.T, coord_type=coord_types
+    )
+    orb_ked_dn = eval_orb_ked(
+        dm1_dn, obasis, atgrid.points, transform=mo_coeff.T, coord_type=coord_types
+    )
+    ked_tot = eval_pd_ked(
+        dm1_tot, obasis, atgrid.points, coord_type=coord_types, transform=mo_coeff.T
+    )
+
+    # Density and KED spherical average
+    dens_spherical_avg = atgrid.spherical_average(dens_tot)
+    ked_spherical_avg = atgrid.spherical_average(ked_tot)
+    dens_splines_up = [atgrid.spherical_average(dens) for dens in orb_dens_up]
+    dens_splines_dn = [atgrid.spherical_average(dens) for dens in orb_dens_dn]
+    ked_splines_up = [atgrid.spherical_average(dens) for dens in orb_ked_up]
+    ked_splines_dn = [atgrid.spherical_average(dens) for dens in orb_ked_dn]
+    # Evaluate interpolated densities in a uniform radial grid
+    rs = rgrid.points
+    dens_avg_tot = dens_spherical_avg(rs)
+    orb_dens_avg_up = np.array([spline(rs) for spline in dens_splines_up])
+    orb_dens_avg_dn = np.array([spline(rs) for spline in dens_splines_dn])
+    ked_avg_tot = ked_spherical_avg(rs)
+    orb_ked_avg_up = np.array([spline(rs) for spline in ked_splines_up])
+    orb_ked_avg_dn = np.array([spline(rs) for spline in ked_splines_dn])
+
+    #
+    # Element properties
+    #
+    cov_radii, vdw_radii, mass = atomdb.get_element_data(elem)
+    if charge != 0:
+        cov_radii, vdw_radii = [None, None]  # overwrite values for charged species
+    #
+    # Conceptual-DFT properties (TODO)
+    #
+    ip = None
+    mu = None
+    eta = None
+
+    # Return Species instance
+    fields = dict(
+        elem=elem,
+        natom=natom,
+        basis=basis,
+        nelec=nelec,
+        nspin=nspin,
+        nexc=nexc,
+        # cov_radii=cov_radii,
+        # vdw_radii=vdw_radii,
+        # mass=mass,
+        energy=energy,
+        mo_e_up=mo_e_up,
+        mo_e_dn=mo_e_dn,
+        occs_up=occs_up,
+        occs_dn=occs_dn,
+        ip=ip,
+        mu=mu,
+        eta=eta,
+        rs=rs,
+        _orb_dens_up=orb_dens_avg_up.flatten(),
+        _orb_dens_dn=orb_dens_avg_dn.flatten(),
+        dens_tot=dens_avg_tot,
+        _orb_ked_up=orb_ked_avg_up.flatten(),
+        _orb_ked_dn=orb_ked_avg_dn.flatten(),
+        ked_tot=ked_avg_tot,
+    )
+    return atomdb.Species(dataset, fields)
diff --git a/atomdb/datasets/nist/__init__.py b/atomdb/datasets/nist/__init__.py
index 57965d79..bb1e07f2 100644
--- a/atomdb/datasets/nist/__init__.py
+++ b/atomdb/datasets/nist/__init__.py
@@ -13,132 +13,4 @@
 # You should have received a copy of the GNU General Public License
 # along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
 
-r"""NIST compile function."""
-
-import os
-
-import numpy as np
-
-from scipy import constants
-
-import h5py as h5
-
-import csv
-
-import atomdb
-from atomdb.periodic import Element
-from atomdb.utils import _gs_mult_energy
-from atomdb.api import DEFAULT_DATAPATH
-
-
-__all__ = [
-    "run",
-]
-
-
-DOCSTRING = """Conceptual DFT Dataset
-
-The following neutral and ionic species are available
-
-`neutrals` H to Lr
-`cations` H to Lr
-`anions` H to Lr (up to charge -2)
-
-The values were obtained from the paper, `Phys. Chem. Chem. Phys., 2016,18, 25721-25734 <https://doi.org/10.1039/C6CP04533B>`_.
-For each element/charge pair the values correspond to the most stable electronic configuration.
-
-"""
-
-
-def run(elem, charge, mult, nexc, dataset, datapath):
-    r"""Parse NIST related data and compile the AtomDB database entry."""
-    # Check arguments
-    if nexc != 0:
-        raise ValueError("Nonzero value of `nexc` is not currently supported")
-
-    # Set up internal variables
-    elem = atomdb.element_symbol(elem)
-    atnum = atomdb.element_number(elem)
-    nelec = atnum - charge
-    nspin = mult - 1
-    obasis_name = None
-
-    # Check that the input charge is valid
-    if charge < -2 or charge > atnum:
-        raise ValueError(f"{elem} with {charge} not available.")
-
-    #
-    # Element properties
-    #
-    atom = Element(elem)
-    atmass = atom.mass["stb"]
-    cov_radius, vdw_radius, at_radius, polarizability, dispersion_c6 = [
-        None,
-    ] * 5
-    if charge == 0:
-        # overwrite values for neutral atomic species
-        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
-        polarizability = atom.pold
-        dispersion_c6 = atom.c6
-
-    #
-    # Get the energy for the most stable electronic configuration from database_beta_1.3.0.h5.
-    # Check that the input multiplicity corresponds to this configuration.
-    #
-    datapath = f"{DEFAULT_DATAPATH}/{dataset.lower()}/raw/database_beta_1.3.0.h5"
-    # case 1: neutral or cationic species
-    if charge >= 0:
-        expected_mult, energy = _gs_mult_energy(atnum, nelec, datapath)
-    # case 2: anionic species, read multiplicity from neutral isoelectronic species
-    else:
-        expected_mult, energy = _gs_mult_energy(nelec, nelec, datapath)
-        # There is no data for anions in database_beta_1.3.0.h5, therefore:
-        energy = None
-
-    if not mult == expected_mult:
-        raise ValueError(f"{elem} with {charge} and multiplicity {mult} not available.")
-
-    # Get conceptual-DFT related properties from c6cp04533b1.csv
-    # Locate where each table starts: search for "Element" columns
-    datapath = f"{DEFAULT_DATAPATH}/{dataset.lower()}/raw/c6cp04533b1.csv"
-    data = list(csv.reader(open(datapath, "r")))
-    tabid = [i for i, row in enumerate(data) if "Element" in row]
-    # Assign each conceptual-DFT data table to a variable.
-    # Remove empty and header rows
-    table_ips = data[tabid[0] : tabid[1]]
-    table_ips = [row for row in table_ips if len(row[1]) > 0]
-    table_mus = data[tabid[1] : tabid[2]]
-    table_mus = [row for row in table_mus if len(row[1]) > 0]
-    table_etas = data[tabid[2] :]
-    table_etas = [row for row in table_etas if len(row[1]) > 0]
-    # Get property at table(atnum, charge); convert to Hartree
-    colid = table_ips[0].index(str(charge))
-    ip = float(table_ips[atnum][colid]) if len(table_ips[atnum][colid]) > 1 else None
-    ip *= constants.eV / (2 * constants.Rydberg * constants.h * constants.c)
-    colid = table_mus[0].index(str(charge))
-    mu = float(table_mus[atnum][colid]) if len(table_mus[atnum][colid]) > 1 else None
-    mu *= constants.eV / (2 * constants.Rydberg * constants.h * constants.c)
-    colid = table_etas[0].index(str(charge))
-    eta = float(table_etas[atnum][colid]) if len(table_etas[atnum][colid]) > 1 else None
-    eta *= constants.eV / (2 * constants.Rydberg * constants.h * constants.c)
-
-    # Return Species instance
-    return atomdb.Species(
-        dataset,
-        elem,
-        atnum,
-        obasis_name,
-        nelec,
-        nspin,
-        nexc,
-        atmass,
-        cov_radius,
-        vdw_radius,
-        at_radius,
-        polarizability,
-        dispersion_c6,
-        energy,
-        ip=ip,
-        mu=mu,
-        eta=eta,
-    )
+r"""NIST dataset."""
diff --git a/atomdb/datasets/nist/run.py b/atomdb/datasets/nist/run.py
new file mode 100644
index 00000000..5940e961
--- /dev/null
+++ b/atomdb/datasets/nist/run.py
@@ -0,0 +1,183 @@
+# This file is part of AtomDB.
+#
+# AtomDB is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# AtomDB is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
+
+r"""NIST compile function."""
+
+import os
+
+import numpy as np
+
+from scipy import constants
+
+import h5py as h5
+
+import csv
+
+import atomdb
+from atomdb.utils import MODULE_DATAPATH, MULTIPLICITIES, CMINV, EV
+from atomdb.periodic import Element
+
+
+__all__ = [
+    "run",
+]
+
+
+DOCSTRING = """Conceptual DFT Dataset
+
+The following neutral and ionic species are available
+
+`neutrals` H to Lr
+`cations` H to Lr
+`anions` H to Lr (up to charge -2)
+
+The values were obtained from the paper, `Phys. Chem. Chem. Phys., 2016,18, 25721-25734 <https://doi.org/10.1039/C6CP04533B>`_.
+For each element/charge pair the values correspond to the most stable electronic configuration.
+
+"""
+
+
+def load_nist_spectra_data(atnum, nelec, datafile):
+    """Load data from database_beta_1.3.0.h5 file into a `SpeciesTable`.
+
+    Note: function based on spectra.py module from old master
+    https://github.com/theochem/AtomDB/blob/oldmaster/atomdb/io/spectra.py
+    """
+
+    # set keys for the atomic number and number of electrons
+    z = str(atnum).zfill(3)
+    ne = str(nelec).zfill(3)
+
+    # in database_beta_1.3.0.h5
+    with h5.File(datafile, "r") as f:
+        # for specie with atomic number z and ne electrons get mults, energies, configurations & J values
+        mults = np.array(list(f[z][ne]["Multi"][...]), dtype=int)
+        energy = f[z][ne]["Energy"][...]
+        config = f[z][ne]["Config"][...]
+        j_vals = f[z][ne]["J"][...]
+        assert len(mults) == len(energy) == len(config) == len(j_vals)
+
+    # found violations in Derick"s data (they should be mult ordered!)
+    if all(mults != sorted(mults)):
+        print((mults, sorted(mults)))
+        print((energy, config, j_vals))
+        print("WARN number={0}, elec={1}, {2}, {3}".format(z, ne, mults, sorted(mults)))
+
+    # sort based on energy
+    index_sorting = sorted(list(range(len(energy))), key=lambda k: energy[k])
+    # mults = list(mults[index_sorting])
+    # energy = list(energy[index_sorting])
+
+    # sort and store the mults, energies, configurations & J values in ascending order of energy
+    output = {
+        "mult": list(mults[index_sorting]),
+        "energy": list(energy[index_sorting]),
+        "config": list(config[index_sorting]),
+        "j_vals": list(j_vals[index_sorting]),
+    }
+
+    return output
+
+
+def run(elem, charge, mult, nexc, dataset, datapath):
+    r"""Parse NIST related data and compile the AtomDB database entry."""
+    # Check arguments
+    if nexc != 0:
+        raise ValueError("Nonzero value of `nexc` is not currently supported")
+
+    # Set up internal variables
+    elem = atomdb.element_symbol(elem)
+    atnum = atomdb.element_number(elem)
+    nelec = atnum - charge
+    nspin = mult - 1
+    obasis_name = None
+
+    # Check that the input charge is valid
+    if charge < -2 or charge > atnum:
+        raise ValueError(f"{elem} with {charge} not available.")
+
+    # Check that the input multiplicity corresponds to this configuration.
+    if not mult == MULTIPLICITIES[(atnum, charge)]:
+        raise ValueError(f"{elem} with charge {charge} and multiplicity {mult} not available.")
+
+    print(f"Generating {elem} with charge {charge} and multiplicity {mult}.")
+    #
+    # Element properties
+    #
+    atom = Element(elem)
+    atmass = atom.mass["stb"]
+    cov_radius, vdw_radius, at_radius, polarizability, dispersion = [
+        None,
+    ] * 5
+    if charge == 0:
+        # overwrite values for neutral atomic species
+        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
+        polarizability = atom.pold
+        dispersion = {"C6": atom.c6}
+    # print(atmass)
+
+    #
+    # Get the ground state energy from database_beta_1.3.0.h5.
+    #
+    # Set an energy default value since there is no data for anions in database_beta_1.3.0.h5.
+    energy = None
+    h5path = os.path.join(MODULE_DATAPATH, "database_beta_1.3.0.h5")
+    if charge >= 0:  # neutral or cationic species
+        spectra_data = load_nist_spectra_data(atnum, nelec, h5path)
+        energies = spectra_data["energy"]
+        # Convert energy to Hartree from cm^{-1} if available
+        energy = energies[0] * CMINV if len(energies) != 0 else energy
+
+    # Get conceptual-DFT related properties from c6cp04533b1.csv
+    # Locate where each table starts: search for "Element" columns
+    csvpath = os.path.join(MODULE_DATAPATH, "c6cp04533b1.csv")
+    data = list(csv.reader(open(csvpath, "r")))
+    tabid = [i for i, row in enumerate(data) if "Element" in row]
+    # Assign each conceptual-DFT data table to a variable.
+    # Remove empty and header rows
+    table_ips = data[tabid[0] : tabid[1]]
+    table_ips = [row for row in table_ips if len(row[1]) > 0]
+    table_mus = data[tabid[1] : tabid[2]]
+    table_mus = [row for row in table_mus if len(row[1]) > 0]
+    table_etas = data[tabid[2] :]
+    table_etas = [row for row in table_etas if len(row[1]) > 0]
+    # Get property at table(atnum, charge); convert to Hartree
+    colid = table_ips[0].index(str(charge))
+    ip = float(table_ips[atnum][colid]) * EV if len(table_ips[atnum][colid]) > 1 else None
+    colid = table_mus[0].index(str(charge))
+    mu = float(table_mus[atnum][colid]) * EV if len(table_mus[atnum][colid]) > 1 else None
+    colid = table_etas[0].index(str(charge))
+    eta = float(table_etas[atnum][colid]) * EV if len(table_etas[atnum][colid]) > 1 else None
+
+    # Return Species instance
+    fields = dict(
+        elem=elem,
+        atnum=atnum,
+        obasis_name=obasis_name,
+        nelec=nelec,
+        nspin=nspin,
+        nexc=nexc,
+        atmass=atmass,
+        cov_radius=cov_radius,
+        vdw_radius=vdw_radius,
+        at_radius=at_radius,
+        polarizability=polarizability,
+        dispersion=dispersion,
+        energy=energy,
+        ip=ip,
+        mu=mu,
+        eta=eta,
+    )
+    return atomdb.Species(dataset, fields)
diff --git a/atomdb/datasets/numeric/__init__.py b/atomdb/datasets/numeric/__init__.py
index 8e525b0b..f2ae4bf0 100644
--- a/atomdb/datasets/numeric/__init__.py
+++ b/atomdb/datasets/numeric/__init__.py
@@ -22,166 +22,5 @@
 #
 # --
 #
-"""Numerical Hartree-Fock Data on a Grid."""
 
-
-import os
-
-import numpy as np
-
-import atomdb
-
-from atomdb.utils import multiplicities
-
-from atomdb.periodic import Element
-
-
-def load_numerical_hf_data():
-    """Load data from desnity.out file into a `SpeciesTable`."""
-
-    from io import StringIO
-
-    def helper_skip():
-        """Skip the header for each species."""
-        for _ in range(4):
-            f.readline()
-
-    def helper_data():
-        """Read the grid, density, gradient, laplacian values into arrays."""
-        data = ""
-        for i in range(number_points):
-            data += f.readline()
-        data = np.loadtxt(StringIO(data))
-        assert data.shape == (number_points, 4)
-        return data[:, 0], data[:, 1], data[:, 2], data[:, 3]
-
-    species = {}
-    with open(os.path.join(os.path.dirname(__file__), "raw/density.out"), "r") as f:
-        line = f.readline()
-        while line:
-            if line.startswith(" 1st line is atomic no"):
-                helper_skip()
-                line = f.readline()
-            if line.startswith("     "):
-                # dictionary to store attributes of the new atomic species
-                kwargs = {}
-
-                atomic_number, number_electrons, number_points = [
-                    int(item) for item in line.split()
-                ]
-                energy = [float(item) for item in f.readline().split()]
-                assert len(energy) == 5
-                assert abs(sum(energy[:-1]) - energy[-1]) < 1.0e-8  # 1.e-12 didn"t work?!
-
-                # store energy component values
-                kwargs["energy_components"] = dict(
-                    [
-                        ("T", energy[0]),
-                        ("Vne", energy[1]),
-                        ("J", energy[2]),
-                        ("Ex", energy[3]),
-                        ("E", energy[4]),
-                    ]
-                )
-
-                grid, density, gradient, laplacian = helper_data()
-                assert grid.shape == density.shape == gradient.shape == laplacian.shape
-
-                # store grid dependent values
-                kwargs.update(
-                    {"grid": grid, "density": density, "gradient": gradient, "laplacian": laplacian}
-                )
-
-                # add the new atomic species
-                species[(atomic_number, number_electrons)] = kwargs
-                line = f.readline()
-
-    return species
-
-
-DOCSTRING = """Numeric Dataset
-
-Load data from desnity.out file into a `SpeciesTable`.
-
-"""
-
-
-def run(elem, charge, mult, nexc, dataset, datapath):
-    r"""Compile the densities from Slater orbitals database entry."""
-    # Check arguments
-    if nexc != 0:
-        raise ValueError("Nonzero value of `nexc` is not currently supported")
-    if charge < -1:
-        raise ValueError("The charge must be greater than or equal to -1.")
-    if charge == elem:
-        raise ValueError("The atomic number and charge cannot be the same.")
-
-    # Set up internal variables
-    elem = atomdb.element_symbol(elem)
-    atnum = atomdb.element_number(elem)
-    nelec = atnum - charge
-    nspin = mult - 1
-    n_up = (nelec + nspin) // 2
-    n_dn = (nelec - nspin) // 2
-    obasis_name = None
-
-    # Check that the input multiplicity corresponds to the most stable electronic configuration.
-    # For charged species take the multiplicity from the neutral isoelectronic species.
-    expected_mult = multiplicities[(atnum, charge)]
-    if mult != expected_mult:
-        raise ValueError(
-            f"Multiplicity {mult} not available for {elem} with charge = {charge}. "
-            f"Expected multiplicity is {expected_mult}."
-        )
-
-    species_table = load_numerical_hf_data()
-    data = species_table[(atnum, nelec)]
-
-    #
-    # Element periodic properties
-    #
-    atom = Element(elem)
-    atmass = atom.mass["stb"]
-    cov_radius, vdw_radius, at_radius, polarizability, dispersion_c6 = [
-        None,
-    ] * 5
-    if charge == 0:
-        # overwrite values for neutral atomic species
-        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
-        polarizability = atom.pold
-        dispersion_c6 = atom.c6
-
-    # Get electronic structure data
-    energy = data["energy_components"]["E"]
-
-    # Make grid
-    points = data["grid"]
-
-    # Compute densities and derivatives
-    dens_tot = data["density"]
-    # d_dens_tot = data['gradient']
-
-    # Compute laplacian and kinetic energy density
-    # lapl_tot = data['laplacian']
-    ked_tot = None
-
-    # Return Species instance
-    return atomdb.Species(
-        dataset,
-        elem,
-        atnum,
-        obasis_name,
-        nelec,
-        nspin,
-        nexc,
-        atmass,
-        cov_radius,
-        vdw_radius,
-        at_radius,
-        polarizability,
-        dispersion_c6,
-        energy,
-        rs=points,
-        dens_tot=dens_tot,
-        ked_tot=ked_tot,
-    )
+"""Numerical Hartree-Fock dataset."""
diff --git a/atomdb/datasets/numeric/run.py b/atomdb/datasets/numeric/run.py
new file mode 100644
index 00000000..df39d37f
--- /dev/null
+++ b/atomdb/datasets/numeric/run.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+# AtomDB is an extended periodic table database containing experimental
+# and/or computational information on stable ground state
+# and/or excited states of neutral and charged atomic species.
+#
+# Copyright (C) 2014-2015 The AtomDB Development Team
+#
+# This file is part of AtomDB.
+#
+# AtomDB is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 3
+# of the License, or (at your option) any later version.
+#
+# AtomDB is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <http://www.gnu.org/licenses/>
+#
+# --
+#
+"""Numerical Hartree-Fock Data on a Grid."""
+
+
+import os
+
+import numpy as np
+
+import atomdb
+
+from atomdb.utils import MULTIPLICITIES
+
+from atomdb.periodic import Element
+
+
+def load_numerical_hf_data():
+    """Load data from desnity.out file into a `SpeciesTable`."""
+
+    from io import StringIO
+
+    def helper_skip():
+        """Skip the header for each species."""
+        for _ in range(4):
+            f.readline()
+
+    def helper_data():
+        """Read the grid, density, gradient, laplacian values into arrays."""
+        data = ""
+        for i in range(number_points):
+            data += f.readline()
+        data = np.loadtxt(StringIO(data))
+        assert data.shape == (number_points, 4)
+        return data[:, 0], data[:, 1], data[:, 2], data[:, 3]
+
+    species = {}
+    with open(os.path.join(os.path.dirname(__file__), "raw/density.out"), "r") as f:
+        line = f.readline()
+        while line:
+            if line.startswith(" 1st line is atomic no"):
+                helper_skip()
+                line = f.readline()
+            if line.startswith("     "):
+                # dictionary to store attributes of the new atomic species
+                kwargs = {}
+
+                atomic_number, number_electrons, number_points = [
+                    int(item) for item in line.split()
+                ]
+                energy = [float(item) for item in f.readline().split()]
+                assert len(energy) == 5
+                assert abs(sum(energy[:-1]) - energy[-1]) < 1.0e-8  # 1.e-12 didn"t work?!
+
+                # store energy component values
+                kwargs["energy_components"] = dict(
+                    [
+                        ("T", energy[0]),
+                        ("Vne", energy[1]),
+                        ("J", energy[2]),
+                        ("Ex", energy[3]),
+                        ("E", energy[4]),
+                    ]
+                )
+
+                grid, density, gradient, laplacian = helper_data()
+                assert grid.shape == density.shape == gradient.shape == laplacian.shape
+
+                # store grid dependent values
+                kwargs.update(
+                    {"grid": grid, "density": density, "gradient": gradient, "laplacian": laplacian}
+                )
+
+                # add the new atomic species
+                species[(atomic_number, number_electrons)] = kwargs
+                line = f.readline()
+
+    return species
+
+
+DOCSTRING = """Numeric Dataset
+
+Load data from desnity.out file into a `SpeciesTable`.
+
+"""
+
+
+def run(elem, charge, mult, nexc, dataset, datapath):
+    r"""Compile the densities from Slater orbitals database entry."""
+    # Check arguments
+    if nexc != 0:
+        raise ValueError("Nonzero value of `nexc` is not currently supported")
+    if charge < -1:
+        raise ValueError("The charge must be greater than or equal to -1.")
+    if charge == elem:
+        raise ValueError("The atomic number and charge cannot be the same.")
+
+    # Set up internal variables
+    elem = atomdb.element_symbol(elem)
+    atnum = atomdb.element_number(elem)
+    nelec = atnum - charge
+    nspin = mult - 1
+    n_up = (nelec + nspin) // 2
+    n_dn = (nelec - nspin) // 2
+    obasis_name = None
+
+    # Check that the input multiplicity corresponds to the most stable electronic configuration.
+    # For charged species take the multiplicity from the neutral isoelectronic species.
+    expected_mult = MULTIPLICITIES[(atnum, charge)]
+    if mult != expected_mult:
+        raise ValueError(
+            f"Multiplicity {mult} not available for {elem} with charge = {charge}. "
+            f"Expected multiplicity is {expected_mult}."
+        )
+
+    species_table = load_numerical_hf_data()
+    data = species_table[(atnum, nelec)]
+
+    #
+    # Element periodic properties
+    #
+    atom = Element(elem)
+    atmass = atom.mass["stb"]
+    cov_radius, vdw_radius, at_radius, polarizability, dispersion = [
+        None,
+    ] * 5
+    if charge == 0:
+        # overwrite values for neutral atomic species
+        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
+        polarizability = atom.pold
+        dispersion = {"C6": atom.c6}
+
+    # Get electronic structure data
+    energy = data["energy_components"]["E"]
+
+    # Make grid
+    points = data["grid"]
+
+    # Compute densities and derivatives
+    dens_tot = data["density"]
+    d_dens_tot = data["gradient"]
+
+    # Compute laplacian and kinetic energy density
+    lapl_tot = data["laplacian"]
+    ked_tot = None
+
+    # Return Species instance
+    fields = dict(
+        elem=elem,
+        atnum=atnum,
+        obasis_name=obasis_name,
+        nelec=nelec,
+        nspin=nspin,
+        nexc=nexc,
+        atmass=atmass,
+        cov_radius=cov_radius,
+        vdw_radius=vdw_radius,
+        at_radius=at_radius,
+        polarizability=polarizability,
+        dispersion=dispersion,
+        energy=energy,
+        rs=points,
+        dens_tot=dens_tot,
+        d_dens_tot=d_dens_tot,
+        dd_dens_tot=lapl_tot,
+        ked_tot=ked_tot,
+    )
+    return atomdb.Species(dataset, fields)
diff --git a/atomdb/datasets/slater/__init__.py b/atomdb/datasets/slater/__init__.py
index 8329cd91..ebdc90b9 100644
--- a/atomdb/datasets/slater/__init__.py
+++ b/atomdb/datasets/slater/__init__.py
@@ -13,7 +13,8 @@
 # You should have received a copy of the GNU General Public License
 # along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
 
-r"""
+r"""Slater dataset.
+
 Module responsible for reading and storing atomic wave-function information from '.slater' files and
     computing electron density, and kinetic energy from them.
 
@@ -26,931 +27,3 @@
     of anion, cation and neutral atomic wave-function information.
 
 """
-
-
-import numpy as np
-import os
-import re
-from scipy.special import factorial
-import atomdb
-from atomdb.periodic import Element
-
-
-__all__ = ["AtomicDensity", "load_slater_wfn", "run"]
-
-
-BOUND = (1e-5, 15.0)
-
-NPOINTS = 10000
-
-
-class AtomicDensity:
-    r"""
-    Atomic Density Class.
-
-    Reads and Parses information from the .slater file of a atom and stores it inside this class.
-    It is then able to construct the (total, core and valence) electron density based
-    on linear combination of orbitals where each orbital is a linear combination of
-    Slater-type orbitals.
-    Elements supported by default from "./atomdb/data/examples/" range from Hydrogen to Xenon.
-
-    Attributes
-    ----------
-    Attributes relating to the standard electron configuration.
-
-    energy : list
-        Energy of that atom.
-    configuration : str
-        Return the electron configuration of the element.
-    orbitals : list, (M,)
-        List of strings representing each of the orbitals in the electron configuration.
-        For example, Beryllium has ["1S", "2S"] in its electron configuration.
-        Ordered based on "S", "P", "D", etc.
-    orbitals_occupation : ndarray, (M, 1)
-        Returns the number of electrons in each of the orbitals in the electron configuration.
-        e.g. Beryllium has two electrons in "1S" and two electrons in "2S".
-
-    Attributes relating to representing orbitals as linear combination of Slater-type orbitals.
-
-    orbital_energy : list, (N, 1)
-        Energy of each of the N Slater-type orbital.
-    orbitals_cusp : list, (N, 1)
-        Cusp of each of the N Slater-type orbital. Same ordering as `orbitals`. Does not exist
-        for Heavy atoms past Xenon.
-    orbitals_basis : dict
-        Keys are the orbitals in the electron configuration. Each orbital has N Slater-type orbital
-        attached to them.
-    orbitals_exp : dict (str : ndarray(N, 1))
-        Key is the orbital in the electron configuration and the item of that key is the Slater
-         exponents attached
-         to each N Slater-type orbital.
-    orbitals_coeff : dict (str : ndarray(N, 1))
-        Key is the orbital in the electron configuration (e. 1S, 2S or 2P) and the item is the
-        Slater coefficients attached to each of the N Slater-type orbital.
-    basis_numbers : dict (str : ndarray(N, 1))
-        Key is orbital in electron configuration and the item is the basis number of each of
-        the N Slater-type orbital. These are the principal quantum number to each Slater-type
-        orbital.
-
-    Methods
-    -------
-    atomic_density(mode="total") :
-        Construct the atomic density from the linear combinations of slater-type orbitals.
-        Can compute the total (default), core and valence atomic density.
-    lagrangian_kinetic_energy :
-        Construct the Positive Definite Kinetic energy.
-
-    Examples
-    --------
-    Grab information about Beryllium.
-    >> be =  AtomicDensity("be")
-
-    Some of the attributes are the following.
-    >> print(be.configuration) #  Should "1S(2)2S(2)".
-    >> print(be.orbitals)  # ['1S', '2S'].
-    >> print(be.orbitals_occupation) # [[2], [2]] Two electrons in each orbital.
-    >> print(be.orbitals_cusp)  # [1.0001235, 0.9998774].
-
-    The Slatar coefficients and exponents of the 1S orbital can be obtained as:
-    >> print(be.orbital_coeff["1S"])
-    >> print(be.orbitals_exp["1S"])
-
-    The total, core and valence electron density can be obtained as:
-    >> points = np.arange(0., 25., 0.01)
-    >> total_density = be.atomic_density(points, "total")
-    >> core_density = be.atomic_density(points, "core")
-    >> valence_density = be.atomic_density(points, "valence")
-
-    References
-    ----------
-    [1] "Analytical Hartree–Fock wave functions subject to cusp and asymptotic constraints:
-        He to Xe, Li+ to Cs+, H− to I−" by T. Koga, K. Kanayama, S. Watanabe and A.J. Thakkar.
-
-    """
-
-    def __init__(self, element, anion=False, cation=False):
-        r"""
-        Construct AtomicDensity object.
-
-        Parameters
-        ----------
-        element : str
-            Symbol of element.
-        anion : bool
-            If true, then the anion of element is used.
-        cation : bool
-            If true, then the cation of element is used.
-
-        """
-        if not isinstance(element, str) or not element.isalpha():
-            raise TypeError("The element argument should be all letters string.")
-
-        data = load_slater_wfn(element, anion, cation)
-        for key, value in data.items():
-            setattr(self, key, value)
-
-    @staticmethod
-    def slater_orbital(exponent, number, points):
-        r"""
-        Compute the Slater-type orbitals on the given points.
-
-        A Slater-type orbital is defined as:
-        .. math::
-            R(r) = N r^{n-1} e^{- C r)
-
-        where,
-            :math:`n` is the principal quantum number of that orbital.
-            :math:`N` is the normalizing constant.
-            :math:`r` is the radial point, distance to the origin.
-            :math:`C` is the zeta exponent of that orbital.
-
-        Parameters
-        ----------
-        exponent : ndarray, (M, 1)
-            The zeta exponents of Slater orbitals.
-        number : ndarray, (M, 1)
-            The principle quantum numbers of Slater orbitals.
-        points : ndarray, (N,)
-            The radial grid points.
-
-        Returns
-        -------
-        slater : ndarray, (N, M)
-            The Slater-type orbitals evaluated on the grid points.
-
-        See Also
-        --------
-        The principal quantum number of all of the orbital are stored in `basis_numbers`.
-        The zeta exponents of all of the orbitals are stored in the attribute `orbitals_exp`.
-
-        """
-        if points.ndim != 1:
-            raise ValueError("The argument point should be a 1D array.")
-        # compute norm & pre-factor
-        norm = np.power(2.0 * exponent, number) * np.sqrt(
-            (2.0 * exponent) / factorial(2.0 * number)
-        )
-        pref = np.power(points, number - 1).T
-        # compute slater function
-        slater = norm.T * pref * np.exp(-exponent * points).T
-        return slater
-
-    def phi_matrix(self, points, deriv=False):
-        r"""
-        Compute the linear combination of Slater-type atomic orbitals on the given points.
-
-        Each row corresponds to a point on the grid, represented as :math:`r` and
-         each column is represented as a linear combination of Slater-type atomic orbitals
-         of the form:
-
-        .. math::
-            \sum c_i R(r, n_i, C_i)
-
-        where,
-            :math:`c_i` is the coefficient of the Slater-type orbital.
-            :math:`C_i` is the zeta exponent attached to the Slater-type orbital.
-            :math:`n_i` is the principal quantum number attached to the Slater-type orbital.
-            :math:`R(r, n_i, C_i)` is the Slater-type orbital.
-            i ranges from 0 to K-1 where K is the number of orbitals in electron configuration.
-
-        Parameters
-        ----------
-        points : ndarray, (N,)
-            The radial grid points.
-        deriv : bool
-            If true, use the derivative of the slater-orbitals.
-
-        Returns
-        -------
-        phi_matrix : ndarray(N, K)
-            The linear combination of Slater-type orbitals evaluated on the grid points, where K is
-            the number of orbitals. The order is S orbitals, then P then D.
-
-        Notes
-        -----
-        - At r = 0, the derivative of slater-orbital is undefined and this function returns
-            zero instead. See "derivative_slater_type_orbital".
-
-        """
-        # compute orbital composed of a linear combination of Slater
-        phi_matrix = np.zeros((len(points), len(self.orbitals)))
-        for index, orbital in enumerate(self.orbitals):
-            exps, number = self.orbitals_exp[orbital[1]], self.basis_numbers[orbital[1]]
-            if deriv:
-                slater = self.derivative_slater_type_orbital(exps, number, points)
-            else:
-                slater = self.slater_orbital(exps, number, points)
-            phi_matrix[:, index] = np.dot(slater, self.orbitals_coeff[orbital]).ravel()
-        return phi_matrix
-
-    def atomic_density(self, points, mode="total"):
-        r"""
-        Compute atomic density on the given points.
-
-        The total density is written as a linear combination of Slater-type orbital
-        whose coefficients is the orbital occupation number of the electron configuration:
-        .. math::
-            \sum n_i |P(r, n_i, C_i)|^2
-
-        where,
-            :math:`n_i` is the number of electrons in orbital i.
-            :math:`P(r, n_i, C_i)` is a linear combination of Slater-type orbitals evaluated
-                on the point :math:`r`.
-
-        For core and valence density, please see More Info below.
-
-        Parameters
-        ----------
-        points : ndarray, (N,)
-            The radial grid points.
-        mode : str
-            The type of atomic density, which can be "total", "valence" or "core".
-
-        Returns
-        -------
-        dens : ndarray, (N,)
-            The atomic density on the grid points.
-
-        Notes
-        -----
-        The core density and valence density is respectively written as:
-        .. math::
-            \sum n_i (1 - e^{-|e_i - e_{homo}|^2}) |P(r, n_i, C_i)|
-            \sum n_i e^{-|e_i - e_{homo}|^2}) |P(r, n_i. C_i)|
-
-        where,
-            :math:`e_i` is the energy of the orbital i.
-            :math:`e_{homo}` is the energy of the highest occupying orbital.
-
-        """
-        if mode not in ["total", "valence", "core"]:
-            raise ValueError("Argument mode not recognized!")
-
-        # compute orbital occupation numbers
-        orb_occs = self.orbitals_occupation
-        if mode == "valence":
-            orb_homo = self.orbitals_energy[len(self.orbitals_occupation) - 1]
-            orb_occs = orb_occs * np.exp(-((self.orbitals_energy - orb_homo) ** 2))
-        elif mode == "core":
-            orb_homo = self.orbitals_energy[len(self.orbitals_occupation) - 1]
-            orb_occs = orb_occs * (1.0 - np.exp(-((self.orbitals_energy - orb_homo) ** 2)))
-        # compute density
-        dens = np.dot(self.phi_matrix(points) ** 2, orb_occs).ravel() / (4 * np.pi)
-        return dens
-
-    @staticmethod
-    def derivative_slater_type_orbital(exponent, number, points):
-        r"""
-        Compute the derivative of Slater-type orbitals on the given points.
-
-        A Slater-type orbital is defined as:
-        .. math::
-            \frac{d R(r)}{dr} = \bigg(\frac{n-1}{r} - C \bigg) N r^{n-1} e^{- C r),
-
-        where,
-            :math:`n` is the principal quantum number of that orbital.
-            :math:`N` is the normalizing constant.
-            :math:`r` is the radial point, distance to the origin.
-            :math:`C` is the zeta exponent of that orbital.
-
-        Parameters
-        ----------
-        exponent : ndarray, (M, 1)
-            The zeta exponents of Slater orbitals.
-        number : ndarray, (M, 1)
-            The principle quantum numbers of Slater orbitals.
-        points : ndarray, (N,)
-            The radial grid points. If points contain zero, then it is undefined at those
-            points and set to zero.
-
-        Returns
-        -------
-        slater : ndarray, (N, M)
-            The Slater-type orbitals evaluated on the grid points.
-
-        Notes
-        -----
-        - At r = 0, the derivative is undefined and this function returns zero instead.
-
-        References
-        ----------
-        See wikipedia page on "Slater-Type orbitals".
-
-        """
-        slater = AtomicDensity.slater_orbital(exponent, number, points)
-        # derivative
-        deriv_pref = (number.T - 1.0) / np.reshape(points, (points.shape[0], 1)) - exponent.T
-        deriv = deriv_pref * slater
-        return deriv
-
-    def lagrangian_kinetic_energy(self, points):
-        r"""
-        Positive definite or Lagrangian kinectic energy density.
-
-        Parameters
-        ----------
-        points : ndarray,(N,)
-            The radial grid points.
-
-        Returns
-        -------
-        energy : ndarray, (N,)
-            The kinetic energy on the grid points.
-
-        Notes
-        -----
-        - To integrate this to get kinetic energy in .slater files, it should not be done in
-            spherically, ie the integral is exactly :math:`\int_0^{\infty} T(r) dr`.
-
-        """
-
-        phi_matrix = np.zeros((len(points), len(self.orbitals)))
-        for index, orbital in enumerate(self.orbitals):
-            exps, number = self.orbitals_exp[orbital[1]], self.basis_numbers[orbital[1]]
-            slater = AtomicDensity.slater_orbital(exps, number, points)
-            # derivative
-            deriv_pref = (number.T - 1.0) - exps.T * np.reshape(points, (points.shape[0], 1))
-            deriv = deriv_pref * slater
-            phi_matrix[:, index] = np.dot(deriv, self.orbitals_coeff[orbital]).ravel()
-
-        angular = []  # Angular numbers are l(l + 1)
-        for index, orbital in enumerate(self.orbitals):
-            if "S" in orbital:
-                angular.append(0.0)
-            elif "P" in orbital:
-                angular.append(2.0)
-            elif "D" in orbital:
-                angular.append(6.0)
-            elif "F" in orbital:
-                angular.append(12.0)
-
-        orb_occs = self.orbitals_occupation
-        energy = np.dot(phi_matrix**2.0, orb_occs).ravel() / 2.0
-        # Add other term
-        molecular = self.phi_matrix(points) ** 2.0 * np.array(angular)
-        energy += np.dot(molecular, orb_occs).ravel() / 2.0
-        return energy
-
-    def derivative_density(self, points):
-        r"""
-        Return the derivative of the atomic density on a set of points.
-
-        Parameters
-        ----------
-        points : ndarray,(N,)
-            The radial grid points.
-
-        Returns
-        -------
-        deriv : ndarray, (N,)
-            The derivative of atomic density on the grid points.
-        """
-        factor = self.phi_matrix(points) * self.phi_matrix(points, deriv=True)
-        derivative = np.dot(2.0 * factor, self.orbitals_occupation).ravel() / (4 * np.pi)
-        return derivative
-
-
-def load_slater_wfn(element, anion=False, cation=False):
-    """
-    Return the data recorded in the atomic Slater wave-function file as a dictionary.
-
-    Parameters
-    ----------
-    file_name : str
-        The path to the Slater atomic file.
-    anion : bool
-        If true, then the anion of element is used.
-    cation : bool
-        If true, then the cation of element is used.
-
-    """
-    # Heavy atoms from atom cs to lr.
-    heavy_atoms = [
-        "cs",
-        "ba",
-        "la",
-        "ce",
-        "pr",
-        "nd",
-        "pm",
-        "sm",
-        "eu",
-        "gd",
-        "tb",
-        "dy",
-        "ho",
-        "er",
-        "tm",
-        "yb",
-        "lu",
-        "hf",
-        "ta",
-        "w",
-        "re",
-        "os",
-        "ir",
-        "pt",
-        "au",
-        "hg",
-        "tl",
-        "pb",
-        "bi",
-        "po",
-        "at",
-        "rn",
-        "fr",
-        "ra",
-        "ac",
-        "th",
-        "pa",
-        "u",
-        "np",
-        "pu",
-        "am",
-        "cm",
-        "bk",
-        "cf",
-        "es",
-        "fm",
-        "md",
-        "no",
-        "lr",
-    ]
-
-    anion_atoms = [
-        "ag",
-        "al",
-        "as",
-        "b",
-        "br",
-        "c",
-        "cl",
-        "co",
-        "cr",
-        "cu",
-        "f",
-        "fe",
-        "ga",
-        "ge",
-        "h",
-        "i",
-        "in",
-        "k",
-        "li",
-        "mn",
-        "mo",
-        "n",
-        "na",
-        "nb",
-        "ni",
-        "o",
-        "p",
-        "pd",
-        "rb",
-        "rh",
-        "ru",
-        "s",
-        "sb",
-        "sc",
-        "se",
-        "si",
-        "sn",
-        "tc",
-        "te",
-        "ti",
-        "v",
-        "y",
-        "zr",
-    ]
-
-    cation_atoms = [
-        "ag",
-        "al",
-        "ar",
-        "as",
-        "b",
-        "be",
-        "br",
-        "c",
-        "ca",
-        "cd",
-        "cl",
-        "co",
-        "cr",
-        "cs",
-        "cu",
-        "f",
-        "fe",
-        "ga",
-        "ge",
-        "i",
-        "in",
-        "k",
-        "kr",
-        "li",
-        "mg",
-        "mn",
-        "mo",
-        "n",
-        "na",
-        "nb",
-        "ne",
-        "ni",
-        "o",
-        "p",
-        "pd",
-        "rb",
-        "rh",
-        "ru",
-        "s",
-        "sb",
-        "sc",
-        "se",
-        "si",
-        "sn",
-        "sr",
-        "tc",
-        "te",
-        "ti",
-        "v",
-        "xe",
-        "y",
-        "zn",
-        "zr",
-    ]
-
-    is_heavy_element = element.lower() in heavy_atoms
-    if (anion or cation) and is_heavy_element:
-        raise ValueError("Both Anion & Cation Slater File for element %s does not exist." % element)
-    if anion:
-        if element.lower() in anion_atoms:
-            # file_path = "./atomdb/data/anion/%s.an" % element.lower()
-            file_path = "raw/anion/%s.an" % element.lower()
-        else:
-            raise ValueError("Anion Slater File for element %s does not exist." % element)
-    elif cation:
-        if element.lower() in cation_atoms:
-            # file_path = "./atomdb/data/cation/%s.cat" % element.lower()
-            file_path = "raw/cation/%s.cat" % element.lower()
-        else:
-            raise ValueError("Cation Slater File for element %s does not exist." % element)
-    else:
-        # file_path = "./atomdb/data/neutral/%s.slater" % element.lower()
-        file_path = "raw/neutral/%s.slater" % element.lower()
-
-    file_name = os.path.join(os.path.dirname(__file__), file_path)
-
-    def get_number_of_electrons_per_orbital(configuration):
-        """
-        Get the Occupation Number for all orbitals of an _element returing an dictionary.
-
-        Parameters
-        ----------
-        configuration : str
-            The electron configuration.
-
-        Returns
-        --------
-        dict
-            a dict containing the number and orbital.
-
-        """
-        electron_config_list = configuration
-
-        shells = ["K", "L", "M", "N"]
-
-        out = {}
-        orbitals = (
-            [str(x) + "S" for x in range(1, 8)]
-            + [str(x) + "P" for x in range(2, 8)]
-            + [str(x) + "D" for x in range(3, 8)]
-            + [str(x) + "F" for x in range(4, 8)]
-        )
-        for orb in orbitals:
-            # Initialize all atomic orbitals to zero electrons
-            out[orb] = 0
-
-        for x in shells:
-            if x in electron_config_list:
-                if x == "K":
-                    out["1S"] = 2
-                elif x == "L":
-                    out["2S"] = 2
-                    out["2P"] = 6
-                elif x == "M":
-                    out["3S"] = 2
-                    out["3P"] = 6
-                    out["3D"] = 10
-                elif x == "N":
-                    out["4S"] = 2
-                    out["4P"] = 6
-                    out["4D"] = 10
-                    out["4F"] = 14
-
-        for x in orbitals:
-            if x in electron_config_list:
-                index = electron_config_list.index(x)
-                orbital = electron_config_list[index : index + 2]
-
-                if orbital[1] == "D" or orbital[1] == "F":
-                    # num_electrons = re.sub('[(){}<>,]', "", electron_config_list.split(orbital)[1])
-                    num_electrons = re.search(orbital + r"\((.*?)\)", electron_config_list).group(1)
-                    out[orbital] = int(num_electrons)
-                else:
-                    out[orbital] = int(electron_config_list[index + 3 : index + 4])
-
-        return {key: value for key, value in out.items() if value != 0}
-
-    def get_column(t_orbital):
-        """
-        Correct the error in order to retrieve the correct column.
-
-        The Columns are harder to parse as the orbitals start with one while p orbitals start at
-            energy.
-
-        Parameters
-        ----------
-        t_orbital : str
-            orbital i.e. "1S" or "2P" or "3D"
-
-        Returns
-        -------
-        int :
-            Retrieve the right column index depending on whether it is "S", "P" or "D" orbital.
-
-        """
-        if t_orbital[1] == "S":
-            return int(t_orbital[0]) + 1
-        elif t_orbital[1] == "P":
-            return int(t_orbital[0])
-        elif t_orbital[1] == "D":
-            return int(t_orbital[0]) - 1
-        elif t_orbital[1] == "F":
-            return int(t_orbital[0]) - 2
-        else:
-            raise ValueError("Did not recognize orbital %s " % t_orbital)
-
-    def configuration_exact_for_heavy_elements(configuration):
-        r"""later file for heavy elements does not contain the configuration in right format."""
-        true_configuration = ""
-        if "[XE]" in configuration:
-            true_configuration += "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)"
-            true_configuration += configuration.split("[XE]")[1]
-        elif "[RN]" in configuration:
-            # Add Xenon
-            true_configuration += "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)"
-            # Add Rn
-            true_configuration += "4F(14)6S(2)5D(10)6P(6)"
-            # Add rest
-            true_configuration += configuration.split("[RN]")[1]
-        else:
-            raise ValueError("Heavy element is not the right format for parsing. ")
-        return true_configuration
-
-    with open(file_name, "r") as f:
-        line = f.readline()
-        configuration = line.split()[1].replace(",", "")
-        if is_heavy_element:
-            configuration = configuration_exact_for_heavy_elements(configuration)
-
-        next_line = f.readline()
-        # Sometimes there are blank lin es.
-        while len(next_line.strip()) == 0:
-            next_line = f.readline()
-
-        if is_heavy_element:
-            # Heavy element slater files has extra redundant information of 5 lines.
-            for i in range(0, 6):
-                f.readline()
-
-        next_line = f.readline()
-        energy = [float(next_line.split()[2])] + [
-            float(x) for x in (re.findall(r"[= -]\d+.\d+", f.readline()))[:-1]
-        ]
-
-        orbitals = []
-        orbitals_basis = {"S": [], "P": [], "D": [], "F": []}
-        orbitals_cusp = []
-        orbitals_energy = []
-        orbitals_exp = {"S": [], "P": [], "D": [], "F": []}
-        orbitals_coeff = {}
-
-        line = f.readline()
-        while line.strip() != "":
-            # If line has ___S___ or P or D where _ = " ".
-            if re.search(r"  [S|P|D|F]  ", line):
-                # Get All The Orbitals
-                subshell = line.split()[0]
-                list_of_orbitals = line.split()[1:]
-                orbitals += list_of_orbitals
-                for x in list_of_orbitals:
-                    orbitals_coeff[x] = []  # Initilize orbitals inside coefficient dictionary
-
-                # Get Energy, Cusp Levels
-                line = f.readline()
-                orbitals_energy.extend([float(x) for x in line.split()[1:]])
-                if not is_heavy_element:
-                    # Heavy atoms slater files, doesn't have cusp values,.
-                    line = f.readline()
-                    orbitals_cusp.extend([float(x) for x in line.split()[1:]])
-                line = f.readline()
-
-                # Get Exponents, Coefficients, Orbital Basis
-                while re.match(r"\A^\d" + subshell, line.lstrip()):
-
-                    list_words = line.split()
-                    orbitals_exp[subshell] += [float(list_words[1])]
-                    orbitals_basis[subshell] += [list_words[0]]
-
-                    for x in list_of_orbitals:
-                        orbitals_coeff[x] += [float(list_words[get_column(x)])]
-                    line = f.readline()
-            else:
-                line = f.readline()
-
-    data = {
-        "configuration": configuration,
-        "energy": energy,
-        "orbitals": orbitals,
-        "orbitals_energy": np.array(orbitals_energy)[:, None],
-        "orbitals_cusp": np.array(orbitals_cusp)[:, None],
-        "orbitals_basis": orbitals_basis,
-        "orbitals_exp": {
-            key: np.asarray(value).reshape(len(value), 1)
-            for key, value in orbitals_exp.items()
-            if value != []
-        },
-        "orbitals_coeff": {
-            key: np.asarray(value).reshape(len(value), 1)
-            for key, value in orbitals_coeff.items()
-            if value != []
-        },
-        "orbitals_occupation": np.array(
-            [get_number_of_electrons_per_orbital(configuration)[k] for k in orbitals]
-        )[:, None],
-        "basis_numbers": {
-            key: np.asarray([[int(x[0])] for x in value])
-            for key, value in orbitals_basis.items()
-            if len(value) != 0
-        },
-    }
-
-    return data
-
-
-def split_configuration(orbitals, occupations):
-    r"""Split electronic configuration into alpha and beta components following Hund's rule
-
-    Returns
-    -------
-    occs_a : list
-        alpha electronic configuration for each orbital (M values)
-    occs_b : list
-        beta electronic configuration for each orbital (M values)
-    """
-    subshell_alphas = {"S": 1, "P": 3, "D": 5, "F": 7}
-    occs_a = []
-    occs_b = []
-    for i, orb in enumerate(orbitals):
-        n_el = occupations[i] - 1  # N electrons in sub shell - 1
-        na_sshell = subshell_alphas[orb[-1]]  # N alpha electrons in sub shell
-        row = n_el // na_sshell
-        col = n_el % na_sshell
-        if row == 0:
-            na = (row * na_sshell + col) + 1
-            nb = 0
-        else:
-            na = na_sshell
-            nb = (row * na_sshell + col) - na + 1
-        occs_a.append(na)
-        occs_b.append(nb)
-
-    return np.array(occs_a, dtype=float), np.array(occs_b, dtype=float)
-
-
-def eval_multiplicity(orbitals, occupations):
-    r"""Evaluate multiplicity
-
-    Parameters
-    ----------
-    orbitals : list, (M,)
-        List of strings representing each of the orbitals in the electron configuration.
-        Ordered based on "S", "P", "D", etc. For example, Beryllium has ["1S", "2S"] in its electron
-        configuration.
-    occupations : ndarray, (M,)
-        Number of electrons in each of the orbitals in the electron configuration.
-
-    Returns
-    -------
-    Spin multiplicity : int
-
-    """
-    occs_a, occs_b = split_configuration(orbitals, occupations)
-    na = sum(occs_a)
-    nb = sum(occs_b)
-    return int((na - nb)) + 1
-
-
-DOCSTRING = """Slater Dataset
-
-The following neutral and ionic (+/- 1 charge) species are available:
-
-`neutrals` He to Xe, Cs to Lr
-`cations` Li to Cs
-`anions` H to I
-
-The Slater basis set information for both anion, cation and neutral, was obtained from the paper:
-`(1999), Int. J. Quantum Chem., 71: 491-497 <https://onlinelibrary.wiley.com/doi/abs/10.1002/(SICI)1097-461X(1999)71:6%3C491::AID-QUA6%3E3.0.CO;2-T>`_
-
-The neutral heavy elements were obtained from:
-`Theor Chem Acc 104, 411–413 (2000) <https://link.springer.com/article/10.1007/s002140000150>`_
-"""
-
-
-def run(elem, charge, mult, nexc, dataset, datapath):
-    r"""Compile the densities from Slater orbitals database entry."""
-    # Check arguments
-    if nexc != 0:
-        raise ValueError("Nonzero value of `nexc` is not currently supported")
-    if charge != 0 and abs(charge) > 1:
-        raise ValueError(f"`charge` must be one of -1, 0 or 1")
-
-    # Get information about the element
-    atom = Element(elem)
-    atmass = atom.mass["stb"]
-    cov_radius, vdw_radius, at_radius, polarizability, dispersion_c6 = [
-        None,
-    ] * 5
-    if charge == 0:
-        # overwrite values for neutral atomic species
-        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
-        polarizability = atom.pold
-        dispersion_c6 = atom.c6
-
-    # Set up internal variables
-    elem = atomdb.element_symbol(elem)
-    atnum = atomdb.element_number(elem)
-    nelec = atnum - charge
-    nspin = mult - 1
-    obasis_name = None
-
-    # Retrieve Slater data
-    if charge == 0:
-        species = AtomicDensity(elem, anion=False, cation=False)
-    elif charge > 0:
-        species = AtomicDensity(elem, anion=False, cation=True)
-    else:
-        species = AtomicDensity(elem, anion=True, cation=False)
-
-    # Check multiplicity value
-    mo_occ = species.orbitals_occupation.ravel()  # these are configurations not occupations
-    multiplicity = eval_multiplicity(species.orbitals, mo_occ)
-    if mult != multiplicity:
-        raise ValueError(f"Multiplicity {mult} is not available for {elem} with charge {charge}")
-
-    # Get electronic structure data
-    # FIXME:sign error in parsed energy value (looks like T value instead of E was parsed from raw file).
-    # This is a temporal fix until the parsing code for Slater's data gets updated from BFit.
-    energy = -species.energy[0]
-    mo_energies_a = species.orbitals_energy.ravel()  # assuming same alpha and beta energies
-    mo_occ_a, mo_occ_b = split_configuration(species.orbitals, mo_occ)
-
-    # Make grid
-    points = np.linspace(*BOUND, NPOINTS)
-
-    # Compute densities and derivatives
-    dens_orbs = species.phi_matrix(points) ** 2 / (4 * np.pi)
-    dens_tot = species.atomic_density(points, "total")
-    # dens_core = species.atomic_density(points, "core")
-    # dens_valence = species.atomic_density(points, "valence")
-    d_dens_tot = species.derivative_density(points)
-
-    # Compute laplacian and kinetic energy density
-    # lapl_tot = None
-    ked_tot = species.lagrangian_kinetic_energy(points)
-
-    # Return Species instance
-    return atomdb.Species(
-        dataset,
-        elem,
-        atnum,
-        obasis_name,
-        nelec,
-        nspin,
-        nexc,
-        atmass,
-        cov_radius,
-        vdw_radius,
-        at_radius,
-        polarizability,
-        dispersion_c6,
-        energy,
-        mo_energy_a=mo_energies_a,
-        mo_energy_b=mo_energies_a,
-        mo_occs_a=mo_occ_a,
-        mo_occs_b=mo_occ_b,
-        rs=points,
-        dens_tot=dens_tot,
-        d_dens_tot=d_dens_tot,
-        ked_tot=ked_tot,
-    )
diff --git a/atomdb/datasets/slater/run.py b/atomdb/datasets/slater/run.py
new file mode 100644
index 00000000..7ad3fcfb
--- /dev/null
+++ b/atomdb/datasets/slater/run.py
@@ -0,0 +1,1194 @@
+# This file is part of AtomDB.
+#
+# AtomDB is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# AtomDB is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
+
+
+import numpy as np
+import os
+import re
+import atomdb
+
+from atomdb.periodic import Element
+from grid.onedgrid import UniformInteger
+from grid.rtransform import ExpRTransform
+
+# from importlib_resources import files
+from atomdb.utils import DEFAULT_DATAPATH
+from scipy.special import factorial
+
+
+__all__ = ["AtomicDensity", "load_slater_wfn", "run"]
+
+
+BOUND = (1e-5, 15.0)
+
+NPOINTS = 10000
+
+# DATAPATH = files("atomdb.datasets.slater.raw")
+# DATAPATH = os.path.abspath(DATAPATH._paths[0])
+
+
+class AtomicDensity:
+    r"""
+    Atomic Density Class.
+
+    Reads and Parses information from the .slater file of a atom and stores it inside this class.
+    It is then able to construct the (total, core and valence) electron density based
+    on linear combination of orbitals where each orbital is a linear combination of
+    Slater-type orbitals.
+    Elements supported by default from "./atomdb/data/examples/" range from Hydrogen to Xenon.
+
+    Attributes
+    ----------
+    Attributes relating to the standard electron configuration.
+
+    energy : list
+        Energy of that atom.
+    configuration : str
+        Return the electron configuration of the element.
+    orbitals : list, (M,)
+        List of strings representing each of the orbitals in the electron configuration.
+        For example, Beryllium has ["1S", "2S"] in its electron configuration.
+        Ordered based on orbital energy.
+    orbitals_occupation : ndarray, (M, 1)
+        Returns the number of electrons in each of the orbitals in the electron configuration.
+        e.g. Beryllium has two electrons in "1S" and two electrons in "2S".
+
+    Attributes relating to representing orbitals as linear combination of Slater-type orbitals.
+
+    orbital_energy : list, (N, 1)
+        Energy of each of the N Slater-type orbital.
+    orbitals_cusp : list, (N, 1)
+        Cusp of each of the N Slater-type orbital. Same ordering as `orbitals`. Does not exist
+        for Heavy atoms past Xenon.
+    orbitals_basis : dict
+        Keys are the orbitals in the electron configuration. Each orbital has N Slater-type orbital
+        attached to them.
+    orbitals_exp : dict (str : ndarray(N, 1))
+        Key is the orbital in the electron configuration and the item of that key is the Slater
+         exponents attached
+         to each N Slater-type orbital.
+    orbitals_coeff : dict (str : ndarray(N, 1))
+        Key is the orbital in the electron configuration (e. 1S, 2S or 2P) and the item is the
+        Slater coefficients attached to each of the N Slater-type orbital.
+    basis_numbers : dict (str : ndarray(N, 1))
+        Key is orbital in electron configuration and the item is the basis number of each of
+        the N Slater-type orbital. These are the principal quantum number to each Slater-type
+        orbital.
+
+    Methods
+    -------
+    atomic_density(mode="total") :
+        Construct the atomic density from the linear combinations of slater-type orbitals.
+        Can compute the total (default), core and valence atomic density.
+    lagrangian_kinetic_energy :
+        Construct the Positive Definite Kinetic energy.
+
+    Examples
+    --------
+    Grab information about Beryllium.
+    >> be =  AtomicDensity("be")
+
+    Some of the attributes are the following.
+    >> print(be.configuration) #  Should "1S(2)2S(2)".
+    >> print(be.orbitals)  # ['1S', '2S'].
+    >> print(be.orbitals_occupation) # [[2], [2]] Two electrons in each orbital.
+    >> print(be.orbitals_cusp)  # [1.0001235, 0.9998774].
+
+    The Slatar coefficients and exponents of the 1S orbital can be obtained as:
+    >> print(be.orbital_coeff["1S"])
+    >> print(be.orbitals_exp["1S"])
+
+    The total, core and valence electron density can be obtained as:
+    >> points = np.arange(0., 25., 0.01)
+    >> total_density = be.atomic_density(points, "total")
+    >> core_density = be.atomic_density(points, "core")
+    >> valence_density = be.atomic_density(points, "valence")
+
+    References
+    ----------
+    [1] "Analytical Hartree–Fock wave functions subject to cusp and asymptotic constraints:
+        He to Xe, Li+ to Cs+, H− to I−" by T. Koga, K. Kanayama, S. Watanabe and A.J. Thakkar.
+
+    """
+
+    def __init__(self, element, anion=False, cation=False, data_path=None):
+        r"""
+        Construct AtomicDensity object.
+
+        Parameters
+        ----------
+        element : str
+            Symbol of element.
+        anion : bool
+            If true, then the anion of element is used.
+        cation : bool
+            If true, then the cation of element is used.
+        data_path : str or Path or None
+            The path to the data folder if not the default.
+
+        """
+        if not isinstance(element, str) or not element.isalpha():
+            raise TypeError("The element argument should be all letters string.")
+
+        data = load_slater_wfn(element, anion, cation, data_path)
+        for key, value in data.items():
+            setattr(self, key, value)
+
+    @staticmethod
+    def slater_orbital(exponent, number, points):
+        r"""
+        Compute the Slater-type orbitals on the given points.
+
+        A Slater-type orbital is defined as:
+        .. math::
+            R(r) = N r^{n-1} e^{- C r)
+
+        where,
+            :math:`n` is the principal quantum number of that orbital.
+            :math:`N` is the normalizing constant.
+            :math:`r` is the radial point, distance to the origin.
+            :math:`C` is the zeta exponent of that orbital.
+
+        Parameters
+        ----------
+        exponent : ndarray, (M, 1)
+            The zeta exponents of Slater orbitals.
+        number : ndarray, (M, 1)
+            The principle quantum numbers of Slater orbitals.
+        points : ndarray, (N,)
+            The radial grid points.
+
+        Returns
+        -------
+        slater : ndarray, (N, M)
+            The Slater-type orbitals evaluated on the grid points.
+
+        See Also
+        --------
+        The principal quantum number of all of the orbital are stored in `basis_numbers`.
+        The zeta exponents of all of the orbitals are stored in the attribute `orbitals_exp`.
+
+        """
+        if points.ndim != 1:
+            raise ValueError("The argument point should be a 1D array.")
+        # compute norm & pre-factor
+        norm = np.power(2.0 * exponent, number) * np.sqrt(
+            (2.0 * exponent) / factorial(2.0 * number)
+        )
+        pref = np.power(points, number - 1).T
+        # compute slater function
+        slater = norm.T * pref * np.exp(-exponent * points).T
+        return slater
+
+    def phi_matrix(self, points, deriv=0):
+        r"""
+        Compute the linear combination of Slater-type atomic orbitals on the given points.
+
+        Each row corresponds to a point on the grid, represented as :math:`r` and
+        each column is represented as a linear combination of Slater-type atomic orbitals
+        of the form:
+
+        .. math::
+            \sum c_i R(r, n_i, C_i)
+
+        where,
+            :math:`c_i` is the coefficient of the Slater-type orbital.
+            :math:`C_i` is the zeta exponent attached to the Slater-type orbital.
+            :math:`n_i` is the principal quantum number attached to the Slater-type orbital.
+            :math:`R(r, n_i, C_i)` is the Slater-type orbital.
+            i ranges from 0 to K-1 where K is the number of orbitals in electron configuration.
+
+        Parameters
+        ----------
+        points : ndarray, (N,)
+            The radial grid points.
+        deriv : int
+            Order of the derivative. Default is 0 and can also be 1 or 2.
+
+        Returns
+        -------
+        phi_matrix : ndarray(N, K)
+            The linear combination of Slater-type orbitals evaluated on the grid points, where K is
+            the number of orbitals. The order is S orbitals, then P then D.
+
+        Notes
+        -----
+        - At r = 0, the derivative of slater-orbital is undefined and this function returns
+            zero instead. See "derivative_slater_type_orbital".
+
+        """
+
+        # compute orbital composed of a linear combination of Slater
+        phi_matrix = np.zeros((len(points), len(self.orbitals)))
+        for index, orbital in enumerate(self.orbitals):
+            # get exponent and number of the orbital type (s, p, d, f)
+            exps, numbers = self.orbitals_exp[orbital[1]], self.basis_numbers[orbital[1]]
+            if deriv == 0:
+                slater = self.slater_orbital(exps, numbers, points)
+            elif deriv == 1:
+                slater = self.derivative_slater_type_orbital(exps, numbers, points)
+            elif deriv == 2:
+                slater = self.second_derivative_slater_type_orbital(exps, numbers, points)
+            else:
+                raise ValueError("Derivative order can only be 0, 1 or 2.")
+            phi_matrix[:, index] = np.dot(slater, self.orbitals_coeff[orbital]).ravel()
+        return phi_matrix
+
+    def eval_density(self, points, mode="total"):
+        r"""
+        Compute atomic density on the given points.
+
+        The total density is written as a linear combination of Slater-type orbital
+        whose coefficients is the orbital occupation number of the electron configuration:
+        .. math::
+            \sum n_i |P(r, n_i, C_i)|^2
+
+        where,
+            :math:`n_i` is the number of electrons in orbital i.
+            :math:`P(r, n_i, C_i)` is a linear combination of Slater-type orbitals evaluated
+                on the point :math:`r`.
+
+        For core and valence density, please see More Info below.
+
+        Parameters
+        ----------
+        points : ndarray, (N,)
+            The radial grid points.
+        mode : str
+            The type of atomic density, which can be "total", "valence" or "core".
+
+        Returns
+        -------
+        dens : ndarray, (N,)
+            The atomic density on the grid points.
+
+        Notes
+        -----
+        The core density and valence density is respectively written as:
+        .. math::
+            \sum n_i (1 - e^{-|e_i - e_{homo}|^2}) |P(r, n_i, C_i)|
+            \sum n_i e^{-|e_i - e_{homo}|^2}) |P(r, n_i. C_i)|
+
+        where,
+            :math:`e_i` is the energy of the orbital i.
+            :math:`e_{homo}` is the energy of the highest occupying orbital.
+
+        """
+        if mode not in ["total", "valence", "core"]:
+            raise ValueError("Argument mode not recognized!")
+
+        # compute orbital occupation numbers
+        orb_occs = self.orbitals_occupation
+        if mode == "valence":
+            # get index of homo (equal to sum of occupied alpha orbitals)
+            orb_homo = sum(orb_occs[: len(orb_occs) // 2 - 1])
+            e_orb_homo = self.orbitals_energy[orb_homo]
+            orb_occs = orb_occs * np.exp(-((self.orbitals_energy - e_orb_homo) ** 2))
+        elif mode == "core":
+            orb_homo = self.orbitals_energy[len(self.orbitals_occupation) - 1]
+            orb_occs = orb_occs * (1.0 - np.exp(-((self.orbitals_energy - orb_homo) ** 2)))
+        # compute density
+        dens = np.dot(self.phi_matrix(points) ** 2, orb_occs).ravel() / (4 * np.pi)
+        return dens
+
+    def eval_orbs_density(self, points):
+        r"""Return each orbital density evaluated at a set of points
+
+        rho_i(r) = n_i |P(r, n_i, C_i)|^2
+
+        where,
+        :math:`n_i` is the number of electrons in orbital i.
+        :math:`P(r, n_i, C_i)` is a linear combination of Slater-type orbitals evaluated
+            on the point :math:`r`.
+
+        Parameters
+        ----------
+        points: np.ndarray(N)
+            radial grid points
+
+        Returns
+        -------
+        orb_dens : np.ndarray(K_orb, N)
+            orbitals density at a set of grid points (N)
+        """
+        orb_occs = self.orbitals_occupation
+        orb_dens = self.phi_matrix(points) ** 2 * orb_occs.ravel() / (4 * np.pi)
+        return orb_dens.T
+
+    def eval_orbs_radial_d_density(self, points):
+        r"""Return each orbital density evaluated at a set of points
+
+        math::
+        \frac{d \rho_i(r)}{dr} = n_i \frac{d}{dr} |P(r, n_i, C_i)|^2
+
+        where,
+        :math:`n_i` is the number of electrons in orbital i.
+        :math:`P(r, n_i, C_i)` is a linear combination of Slater-type orbitals evaluated
+            on the point :math:`r`.
+
+        Parameters
+        ----------
+        points: np.ndarray(N)
+            radial grid points
+
+        Returns
+        -------
+        orb_dens : np.ndarray(K_orb, N)
+            orbitals density at a set of grid points (N)
+        """
+
+        factor = self.phi_matrix(points) * self.phi_matrix(points, deriv=1)
+        orb_derivative = 2.0 * factor * self.orbitals_occupation.ravel() / (4 * np.pi)
+        return orb_derivative
+
+    def eval_orbs_radial_dd_density(self, points):
+        r"""Return each orbital density evaluated at a set of points
+
+        math::
+        \frac{d^{2} \rho_i(r)}{dr^{2}} = n_i \frac{d^{2}}{dr^{2}} |P(r, n_i, C_i)|^2
+
+        where,
+        :math:`n_i` is the number of electrons in orbital i.
+        :math:`P(r, n_i, C_i)` is a linear combination of Slater-type orbitals evaluated
+            on the point :math:`r`.
+
+        Parameters
+        ----------
+        points: np.ndarray(N)
+            radial grid points
+
+        Returns
+        -------
+        orb_dens : np.ndarray(K_orb, N)
+            orbitals density at a set of grid points (N)
+        """
+
+        factor = (
+            self.phi_matrix(points) * self.phi_matrix(points, deriv=2)
+            + self.phi_matrix(points, deriv=1) ** 2
+        )
+        orb_derivative = 2.0 * factor * self.orbitals_occupation.ravel() / (4 * np.pi)
+        return orb_derivative.T
+
+    @staticmethod
+    def derivative_slater_type_orbital(exponent, number, points):
+        r"""
+        Compute the derivative of Slater-type orbitals on the given points.
+
+        A Slater-type orbital is defined as:
+        .. math::
+            \frac{d R(r)}{dr} = \bigg(\frac{n-1}{r} - C \bigg) N r^{n-1} e^{- C r),
+
+        where,
+            :math:`n` is the principal quantum number of that orbital.
+            :math:`N` is the normalizing constant.
+            :math:`r` is the radial point, distance to the origin.
+            :math:`C` is the zeta exponent of that orbital.
+
+        Parameters
+        ----------
+        exponent : ndarray, (M, 1)
+            The zeta exponents of Slater orbitals.
+        number : ndarray, (M, 1)
+            The principle quantum numbers of Slater orbitals.
+        points : ndarray, (N,)
+            The radial grid points. If points contain zero, then it is undefined at those
+            points and set to zero.
+
+        Returns
+        -------
+        slater : ndarray, (N, M)
+            The derivative of Slater-type orbitals evaluated on the grid points.
+
+        Notes
+        -----
+        - At r = 0, the derivative is undefined and this function returns zero instead.
+
+        References
+        ----------
+        See wikipedia page on "Slater-Type orbitals".
+
+        """
+        slater = AtomicDensity.slater_orbital(exponent, number, points)
+        # derivative
+        deriv_pref = (number.T - 1.0) / np.reshape(points, (points.shape[0], 1)) - exponent.T
+        deriv = deriv_pref * slater
+        return deriv
+
+    @staticmethod
+    def second_derivative_slater_type_orbital(exponent, number, points):
+        r"""
+        Compute the derivative of Slater-type orbitals on the given points.
+
+        A Slater-type orbital is defined as:
+        .. math::
+            \frac{d R(r)}{dr} = \bigg(\frac{n-1}{r} - C \bigg) N r^{n-1} e^{- C r),
+
+        where,
+            :math:`n` is the principal quantum number of that orbital.
+            :math:`N` is the normalizing constant.
+            :math:`r` is the radial point, distance to the origin.
+            :math:`C` is the zeta exponent of that orbital.
+
+        Parameters
+        ----------
+        exponent : ndarray, (M, 1)
+            The zeta exponents of Slater orbitals.
+        number : ndarray, (M, 1)
+            The principle quantum numbers of Slater orbitals.
+        points : ndarray, (N,)
+            The radial grid points. If points contain zero, then it is undefined at those
+            points and set to zero.
+
+        Returns
+        -------
+        slater : ndarray, (N, M)
+            The second derivative of Slater-type orbitals evaluated on the grid points.
+
+        Notes
+        -----
+        - At r = 0, the derivative is undefined and this function returns zero instead.
+
+        References
+        ----------
+        See wikipedia page on "Slater-Type orbitals".
+
+        """
+        slater = AtomicDensity.slater_orbital(exponent, number, points)
+        # derivative
+        deriv_pref = ((number.T - 1.0) / np.reshape(points, (points.shape[0], 1)) - exponent.T) ** 2
+        deriv_pref -= (number.T - 1.0) / np.reshape(points, (points.shape[0], 1)) ** 2
+        deriv2 = deriv_pref * slater
+        return deriv2
+
+    def eval_orbs_ked_positive_definite(self, points):
+        r"""Return each the kinetic energy density of each orbital evaluated at a set of points
+
+        math::
+            \tau_{\text{PD}}^{i} \left(\mathbf{r}\right) = \tfrac{1}{2} n_i \rvert \nabla \phi_i \left(\mathbf{r}\right) \lvert^2
+
+
+        Parameters
+        ----------
+        points: np.ndarray(N)
+            radial grid points
+
+        Returns
+        -------
+        orb_ked : np.ndarray(K_orb, N)
+            orbitals kinetic energy density values at a set of grid points (N).
+        """
+        phi_matrix = np.zeros((len(points), len(self.orbitals)))
+        for index, orbital in enumerate(self.orbitals):
+            exps, number = self.orbitals_exp[orbital[1]], self.basis_numbers[orbital[1]]
+            slater = AtomicDensity.slater_orbital(exps, number, points)
+            # derivative
+            deriv_pref = (number.T - 1.0) - exps.T * np.reshape(points, (points.shape[0], 1))
+            deriv = deriv_pref * slater
+            phi_matrix[:, index] = np.dot(deriv, self.orbitals_coeff[orbital]).ravel()
+
+        angular = []  # Angular numbers are l(l + 1)
+        for index, orbital in enumerate(self.orbitals):
+            if "S" in orbital:
+                angular.append(0.0)
+            elif "P" in orbital:
+                angular.append(2.0)
+            elif "D" in orbital:
+                angular.append(6.0)
+            elif "F" in orbital:
+                angular.append(12.0)
+
+        orb_occs = self.orbitals_occupation
+        orbs_ked = phi_matrix**2.0 * orb_occs.ravel() / 2.0
+        # Add other term
+        molecular = self.phi_matrix(points) ** 2.0 * np.array(angular)
+        orbs_ked += molecular * orb_occs.ravel() / 2.0
+        return orbs_ked.T
+
+    def eval_ked_positive_definite(self, points):
+        r"""
+        Positive definite or Lagrangian kinetic energy density.
+
+        Parameters
+        ----------
+        points : ndarray,(N,)
+            The radial grid points.
+
+        Returns
+        -------
+        energy : ndarray, (N,)
+            The kinetic energy on the grid points.
+
+        Notes
+        -----
+        - To integrate this to get kinetic energy in .slater files, it should not be done in
+            spherically, ie the integral is exactly :math:`\int_0^{\infty} T(r) dr`.
+
+        """
+
+        phi_matrix = np.zeros((len(points), len(self.orbitals)))
+        for index, orbital in enumerate(self.orbitals):
+            exps, number = self.orbitals_exp[orbital[1]], self.basis_numbers[orbital[1]]
+            slater = AtomicDensity.slater_orbital(exps, number, points)
+            # derivative
+            deriv_pref = (number.T - 1.0) - exps.T * np.reshape(points, (points.shape[0], 1))
+            deriv = deriv_pref * slater
+            phi_matrix[:, index] = np.dot(deriv, self.orbitals_coeff[orbital]).ravel()
+
+        angular = []  # Angular numbers are l(l + 1)
+        for index, orbital in enumerate(self.orbitals):
+            if "S" in orbital:
+                angular.append(0.0)
+            elif "P" in orbital:
+                angular.append(2.0)
+            elif "D" in orbital:
+                angular.append(6.0)
+            elif "F" in orbital:
+                angular.append(12.0)
+
+        orb_occs = self.orbitals_occupation
+        energy = np.dot(phi_matrix**2.0, orb_occs).ravel() / 2.0
+        # Add other term
+        molecular = self.phi_matrix(points) ** 2.0 * np.array(angular)
+        energy += np.dot(molecular, orb_occs).ravel() / 2.0
+        return energy
+
+    def eval_radial_d_density(self, points):
+        r"""
+        Return the derivative of the atomic density on a set of points.
+
+        Parameters
+        ----------
+        points : ndarray,(N,)
+            The radial grid points.
+
+        Returns
+        -------
+        deriv : ndarray, (N,)
+            The derivative of atomic density on the grid points.
+        """
+        factor = self.phi_matrix(points) * self.phi_matrix(points, deriv=1)
+        derivative = np.dot(2.0 * factor, self.orbitals_occupation).ravel() / (4 * np.pi)
+        return derivative
+
+    def eval_radial_dd_density(self, points):
+        r"""
+        Return the second derivative of the atomic density on a set of points.
+
+        Parameters
+        ----------
+        points : ndarray,(N,)
+            The radial grid points.
+
+        Returns
+        -------
+        deriv : ndarray, (N,)
+            The derivative of atomic density on the grid points.
+        """
+        factor = (
+            self.phi_matrix(points) * self.phi_matrix(points, deriv=2)
+            + self.phi_matrix(points, deriv=1) ** 2
+        )
+        dderivative = np.dot(2.0 * factor, self.orbitals_occupation).ravel() / (4 * np.pi)
+        return dderivative
+
+
+def get_cs_occupations(configuration):
+    """
+    Get the alpha and beta occupation numbers for each contracted shell.
+
+    Parameters
+    ----------
+    configuration : str
+        The electron configuration.
+
+    Returns
+    --------
+    a_occ, b_occ, max_occ : dict, dict, dict
+        Dictionaries containing the alpha, beta, and maximum occupation numbers for each
+        contracted shell. Keys are the contracted shells (e.g. 1S, 2S, 2P, 3D) and values are
+        the occupation numbers.
+
+    """
+    # alpha and beta occupation numbers for each contracted shell
+    a_occ = {}
+    b_occ = {}
+
+    # fill in the occupation numbers for the K, L, M, N shells if they are present
+    for cs in ["K", "L", "M", "N"]:
+        if cs in configuration:
+            if cs == "K":
+                a_occ["1S"], b_occ["1S"] = 1, 1
+            elif cs == "L":
+                a_occ["2S"], b_occ["2S"] = 1, 1
+                a_occ["2P"], b_occ["2P"] = 3, 3
+            elif cs == "M":
+                a_occ["3S"], b_occ["3S"] = 1, 1
+                a_occ["3P"], b_occ["3P"] = 3, 3
+                a_occ["3D"], b_occ["3D"] = 5, 5
+            elif cs == "N":
+                a_occ["4S"], b_occ["4S"] = 1, 1
+                a_occ["4P"], b_occ["4P"] = 3, 3
+                a_occ["4D"], b_occ["4D"] = 5, 5
+                a_occ["4F"], b_occ["4F"] = 7, 7
+
+    # create possible contracted shells and their max occupation numbers for alpha or beta
+    contractions = [
+        *[str(x) + "S" for x in range(1, 8)],
+        *[str(x) + "P" for x in range(2, 8)],
+        *[str(x) + "D" for x in range(3, 8)],
+        *[str(x) + "F" for x in range(4, 8)],
+    ]
+
+    # maximum occupation numbers for each contracted shell (alpha or beta only)
+    max_occ = {"S": 1, "P": 3, "D": 5, "F": 7}
+
+    # for each possible contracted shell
+    for cs in contractions:
+        # check if the contracted shell is the configuration
+        if cs in configuration:
+            # get the total number of electrons in the contracted shell
+            n_elec = re.search(cs + r"\((.*?)\)", configuration).group(1)
+            n_elec = int(n_elec)
+
+            # fill alpha and beta occupations for the contracted shell following Hund's rule
+            if n_elec > max_occ[cs[-1]]:
+                a_occ[cs], b_occ[cs] = max_occ[cs[-1]], n_elec - max_occ[cs[-1]]
+            else:
+                a_occ[cs], b_occ[cs] = n_elec, 0
+    return a_occ, b_occ, max_occ
+
+
+def load_slater_wfn(element, anion=False, cation=False, data_path=None):
+    """
+    Return the data recorded in the atomic Slater wave-function file as a dictionary.
+
+    Parameters
+    ----------
+    element : str
+        The element symbol.
+    anion : bool
+        If true, then the anion of element is used.
+    cation : bool
+        If true, then the cation of element is used.
+    data_path : str or Path or None
+        The path to the data folder.
+
+    """
+    # set the data path
+    if data_path is None:
+        data_path = DEFAULT_DATAPATH
+    data_path = os.path.join(data_path, "slater", "raw")
+
+    # Heavy atoms from atom cs to lr.
+    heavy_atoms = [
+        "cs",
+        "ba",
+        "la",
+        "ce",
+        "pr",
+        "nd",
+        "pm",
+        "sm",
+        "eu",
+        "gd",
+        "tb",
+        "dy",
+        "ho",
+        "er",
+        "tm",
+        "yb",
+        "lu",
+        "hf",
+        "ta",
+        "w",
+        "re",
+        "os",
+        "ir",
+        "pt",
+        "au",
+        "hg",
+        "tl",
+        "pb",
+        "bi",
+        "po",
+        "at",
+        "rn",
+        "fr",
+        "ra",
+        "ac",
+        "th",
+        "pa",
+        "u",
+        "np",
+        "pu",
+        "am",
+        "cm",
+        "bk",
+        "cf",
+        "es",
+        "fm",
+        "md",
+        "no",
+        "lr",
+    ]
+
+    anion_atoms = [
+        "ag",
+        "al",
+        "as",
+        "b",
+        "br",
+        "c",
+        "cl",
+        "co",
+        "cr",
+        "cu",
+        "f",
+        "fe",
+        "ga",
+        "ge",
+        "h",
+        "i",
+        "in",
+        "k",
+        "li",
+        "mn",
+        "mo",
+        "n",
+        "na",
+        "nb",
+        "ni",
+        "o",
+        "p",
+        "pd",
+        "rb",
+        "rh",
+        "ru",
+        "s",
+        "sb",
+        "sc",
+        "se",
+        "si",
+        "sn",
+        "tc",
+        "te",
+        "ti",
+        "v",
+        "y",
+        "zr",
+    ]
+
+    cation_atoms = [
+        "ag",
+        "al",
+        "ar",
+        "as",
+        "b",
+        "be",
+        "br",
+        "c",
+        "ca",
+        "cd",
+        "cl",
+        "co",
+        "cr",
+        "cs",
+        "cu",
+        "f",
+        "fe",
+        "ga",
+        "ge",
+        "i",
+        "in",
+        "k",
+        "kr",
+        "li",
+        "mg",
+        "mn",
+        "mo",
+        "n",
+        "na",
+        "nb",
+        "ne",
+        "ni",
+        "o",
+        "p",
+        "pd",
+        "rb",
+        "rh",
+        "ru",
+        "s",
+        "sb",
+        "sc",
+        "se",
+        "si",
+        "sn",
+        "sr",
+        "tc",
+        "te",
+        "ti",
+        "v",
+        "xe",
+        "y",
+        "zn",
+        "zr",
+    ]
+
+    # select and open file containing the slater data
+    is_heavy_element = element.lower() in heavy_atoms
+    if (anion or cation) and is_heavy_element:
+        raise ValueError("Both Anion & Cation Slater File for element %s does not exist." % element)
+    if anion:
+        if element.lower() in anion_atoms:
+            # file_path = "./atomdb/data/anion/%s.an" % element.lower()
+            file_path = "anion/%s.an" % element.lower()
+        else:
+            raise ValueError("Anion Slater File for element %s does not exist." % element)
+    elif cation:
+        if element.lower() in cation_atoms:
+            # file_path = "./atomdb/data/cation/%s.cat" % element.lower()
+            file_path = "cation/%s.cat" % element.lower()
+        else:
+            raise ValueError("Cation Slater File for element %s does not exist." % element)
+    else:
+        # file_path = "./atomdb/data/neutral/%s.slater" % element.lower()
+        file_path = "neutral/%s.slater" % element.lower()
+
+    file_name = os.path.join(data_path, file_path)
+
+    def get_column(t_orbital):
+        """
+        Correct the error in order to retrieve the correct column.
+
+        The Columns are harder to parse as the orbitals start with one while p orbitals start at
+            energy.
+
+        Parameters
+        ----------
+        t_orbital : str
+            orbital i.e. "1S" or "2P" or "3D"
+
+        Returns
+        -------
+        int :
+            Retrieve the right column index depending on whether it is "S", "P" or "D" orbital.
+
+        """
+        if t_orbital[1] == "S":
+            return int(t_orbital[0]) + 1
+        elif t_orbital[1] == "P":
+            return int(t_orbital[0])
+        elif t_orbital[1] == "D":
+            return int(t_orbital[0]) - 1
+        elif t_orbital[1] == "F":
+            return int(t_orbital[0]) - 2
+        else:
+            raise ValueError("Did not recognize orbital %s " % t_orbital)
+
+    def configuration_exact_for_heavy_elements(configuration):
+        r"""later file for heavy elements does not contain the configuration in right format."""
+        true_configuration = ""
+        if "[XE]" in configuration:
+            true_configuration += "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)"
+            true_configuration += configuration.split("[XE]")[1]
+        elif "[RN]" in configuration:
+            # Add Xenon
+            true_configuration += "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)"
+            # Add Rn
+            true_configuration += "4F(14)6S(2)5D(10)6P(6)"
+            # Add rest
+            true_configuration += configuration.split("[RN]")[1]
+        else:
+            raise ValueError("Heavy element is not the right format for parsing. ")
+        return true_configuration
+
+    with open(file_name, "r") as f:
+        next_line = f.readline()
+        configuration = next_line.split()[1].replace(",", "")
+        if is_heavy_element:
+            configuration = configuration_exact_for_heavy_elements(configuration)
+
+        # in light atoms, the energy is the next line with data
+        next_line = f.readline().strip()
+        # some cases have empty lines before the energy (remove them)
+        while not next_line:
+            next_line = f.readline().strip()
+
+        # heavy atoms have 6 lines of redundant data before the energy (skip them)
+        if is_heavy_element:
+            for _ in range(0, 6):
+                next_line = f.readline().strip()
+        # read total energy
+        energy = [float(next_line.split("=")[1])]
+
+        # declare containers for orbitals, orbital basis, cusp, energy, exponents, and coefficients
+        cs = []
+        cs_basis = {}
+        cs_cusp = []
+        cs_energy = []
+        cs_exp = {}
+        cs_coeff = {}
+
+        # ignore next line, has no useful information T, V and V/T
+        f.readline()
+        # some files have empty lines before the first sub-shell
+        while not f.readline().strip():
+            pass
+
+        while next_line:
+            # If line has ___S___ or P or D where _ = " ". This is the start of a new sub-shell
+            if re.search(r"  [S|P|D|F]  ", next_line):
+                # read sub-shell (e.g S) and contractions (e.g 1S, 2S, 3S) for that sub-shell
+                subshell = next_line.split()[0]
+                list_of_orbitals = next_line.split()[1:]
+                # add sub-shell contractions to the list of contractions
+                cs += list_of_orbitals
+
+                # read contractions energies from next line and store them following the
+                # contractions list order
+                next_line = f.readline().strip()
+                cs_energy.extend([float(x) for x in next_line.split()[1:]])
+
+                # save cusp values, for heavy atoms this is not present
+                if not is_heavy_element:
+                    next_line = f.readline()
+                    cs_cusp.extend([float(x) for x in next_line.split()[1:]])
+                # read lines until next sub-shell and get the exponents and coefficients
+
+                for next_line in f:
+                    # break if next line is not an integer followed by a sub-shell (e.g 1S, 2S)
+                    if not re.match(r"\A^\d" + subshell, next_line.lstrip()):
+                        break
+
+                    list_words = next_line.split()
+                    # read orbital basis from first column, save basis list for each sub-shell
+                    cs_basis.setdefault(subshell, []).append(list_words[0])
+                    # read basis exponents from second column and save as basis list (same order)
+                    cs_exp.setdefault(subshell, []).append(float(list_words[1]))
+
+                    # read orbital basis coefficients from the correct column.
+                    # for each orbital, the coefficients are saved in same order as basis list
+                    for x in list_of_orbitals:
+                        coeff = float(list_words[get_column(x)])
+                        cs_coeff.setdefault(x, []).append(coeff)
+            else:
+                next_line = f.readline()
+
+    # create dictionaries with energy and cusps for each contracted shell
+    cs_energy_dict = {i: j for i, j in zip(cs, cs_energy)}
+    cs_cusp_dict = {i: j for i, j in zip(cs, cs_cusp)}
+
+    # compute alpha, beta and max occupation numbers for each contracted shell
+    a_occ_dict, b_occ_dict, max_occ_dict = get_cs_occupations(configuration)
+
+    # list of orbital occupations
+    a_occ = []
+    b_occ = []
+    # list of orbitals energies and cusps
+    orbitals = []
+    orb_energy = []
+    orb_cusp = []
+
+    # for each contracted shell, in order of increasing energy
+    for _, contraction in sorted(zip(cs_energy, cs)):
+        # for each orbital in the contracted shell
+        for i in range(max_occ_dict[contraction[-1]]):
+            # append the orbital to the list
+            orbitals.append(contraction)
+            # compute alpha and beta occupation numbers for the orbital
+            a_occ_val = 1 if i < a_occ_dict[contraction] else 0
+            b_occ_val = 1 if i < b_occ_dict[contraction] else 0
+            # add orbital occupation numbers to the list
+            a_occ.append(a_occ_val)
+            b_occ.append(b_occ_val)
+            # get orbital energy, exponent, and coefficient
+            orb_energy.append(cs_energy_dict[contraction])
+            # get orbital cusp
+            if cs_cusp_dict:
+                orb_cusp.append(cs_cusp_dict[contraction])
+
+    # construct the total orbital list, occupation list, energy list, cusp list
+    orbitals += orbitals
+    orb_occ = np.array(a_occ + b_occ)
+    orb_energy = np.array(orb_energy + orb_energy)
+    orb_cusp = np.array(orb_cusp + orb_cusp)
+
+    data = {
+        "configuration": configuration,
+        "energy": energy,
+        "orbitals": orbitals,
+        "orbitals_energy": orb_energy[:, None],
+        "orbitals_cusp": orb_cusp[:, None],
+        "orbitals_basis": cs_basis,
+        "orbitals_exp": {
+            key: np.asarray(value).reshape(len(value), 1)
+            for key, value in cs_exp.items()
+            if value != []
+        },
+        "orbitals_coeff": {
+            key: np.asarray(value).reshape(len(value), 1)
+            for key, value in cs_coeff.items()
+            if value != []
+        },
+        "orbitals_occupation": orb_occ[:, None],
+        "basis_numbers": {
+            key: np.asarray([[int(x[0])] for x in value])
+            for key, value in cs_basis.items()
+            if len(value) != 0
+        },
+    }
+    return data
+
+
+DOCSTRING = """Slater Dataset
+
+The following neutral and ionic (+/- 1 charge) species are available:
+
+`neutrals` He to Xe, Cs to Lr
+`cations` Li to Cs
+`anions` H to I
+
+The Slater basis set information for both anion, cation and neutral, was obtained from the paper:
+`(1999), Int. J. Quantum Chem., 71: 491-497 <https://onlinelibrary.wiley.com/doi/abs/10.1002/(SICI)1097-461X(1999)71:6%3C491::AID-QUA6%3E3.0.CO;2-T>`_
+
+The neutral heavy elements were obtained from:
+`Theor Chem Acc 104, 411–413 (2000) <https://link.springer.com/article/10.1007/s002140000150>`_
+"""
+
+
+def run(elem, charge, mult, nexc, dataset, datapath):
+    r"""Compile the densities from Slater orbitals database entry."""
+    # Check arguments
+    if nexc != 0:
+        raise ValueError("Nonzero value of `nexc` is not currently supported")
+    if charge != 0 and abs(charge) > 1:
+        raise ValueError(f"`charge` must be one of -1, 0 or 1")
+
+    # Set up internal variables
+    elem = atomdb.element_symbol(elem)
+    atnum = atomdb.element_number(elem)
+    nelec = atnum - charge
+    nspin = mult - 1
+
+    # Retrieve Slater data
+    if charge == 0:
+        species = AtomicDensity(elem, anion=False, cation=False, data_path=datapath)
+    elif charge > 0:
+        species = AtomicDensity(elem, anion=False, cation=True, data_path=datapath)
+    else:
+        species = AtomicDensity(elem, anion=True, cation=False, data_path=datapath)
+
+    # Check multiplicity value
+    mo_occ = species.orbitals_occupation.ravel()
+    multiplicity = int(np.sum(mo_occ[: len(mo_occ) // 2] - mo_occ[len(mo_occ) // 2 :])) + 1
+    if mult != multiplicity:
+        raise ValueError(f"Multiplicity {mult} is not available for {elem} with charge {charge}")
+
+    # Get electronic structure data
+    energy = species.energy[0]  # get energy from list
+    norba = len(mo_occ) // 2
+    # Get MO energies and occupations
+    mo_e_up = species.orbitals_energy.ravel()[:norba]
+    mo_e_dn = species.orbitals_energy.ravel()[norba:]
+    occs_up, occs_dn = mo_occ[:norba], mo_occ[norba:]
+
+    # Make grid
+    onedg = UniformInteger(NPOINTS)  # number of uniform grid points.
+    rgrid = ExpRTransform(*BOUND).transform_1d_grid(onedg)  # radial grid
+
+    # Evaluate properties on the grid:
+    # --------------------------------
+    rs = rgrid.points
+
+    # total and spin-up orbital, and spin-down orbital densities
+    orb_dens_up = species.eval_orbs_density(rs)[:norba, :]
+    orb_dens_dn = species.eval_orbs_density(rs)[norba:, :]
+    dens_tot = species.eval_density(rs, mode="total")
+
+    # total, spin-up orbital, and spin-down orbital first (radial) derivatives of the density
+    d_dens_tot = species.eval_radial_d_density(rs)
+    orb_d_dens_up = species.eval_orbs_radial_d_density(rs)[:norba, :]
+    orb_d_dens_dn = species.eval_orbs_radial_d_density(rs)[norba:, :]
+
+    # total, spin-up orbital, and spin-down orbital second (radial) derivatives of the density
+    dd_dens_tot = species.eval_radial_dd_density(rs)
+    orb_dd_dens_up = species.eval_orbs_radial_dd_density(rs)[:norba, :]
+    orb_dd_dens_dn = species.eval_orbs_radial_dd_density(rs)[norba:, :]
+
+    # total, spin-up orbital, and spin-down orbital kinetic energy densities
+    ked_tot = species.eval_ked_positive_definite(rs)
+    mo_ked_a = species.eval_orbs_ked_positive_definite(rs)[:norba, :]
+    mo_ked_b = species.eval_orbs_ked_positive_definite(rs)[:norba, :]
+
+    # Get information about the element
+    atom = Element(elem)
+    atmass = atom.mass["stb"]
+    cov_radius, vdw_radius, at_radius, polarizability, dispersion = [
+        None,
+    ] * 5
+    # overwrite values for neutral atomic species
+    if charge == 0:
+        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
+        polarizability = atom.pold
+        dispersion = {"C6": atom.c6}
+
+    # Conceptual-DFT properties (WIP)
+    ip = -mo_e_up[np.sum(occs_up) - 1]  # - energy of HOMO
+    # ea = -mo_e_dn[np.sum(occs_dn)] if np.sum(occs_dn) < np.sum(occs_up) else None  # - LUMO energy
+    mu = None
+    eta = None
+
+    # Return Species instance
+    fields = dict(
+        elem=elem,
+        atnum=atnum,
+        obasis_name="Slater",
+        nelec=nelec,
+        nspin=nspin,
+        nexc=nexc,
+        atmass=atmass,
+        cov_radius=cov_radius,
+        vdw_radius=vdw_radius,
+        at_radius=at_radius,
+        polarizability=polarizability,
+        dispersion=dispersion,
+        energy=energy,
+        mo_energy_a=mo_e_up,
+        mo_energy_b=mo_e_dn,
+        mo_occs_a=occs_up,
+        mo_occs_b=occs_dn,
+        ip=ip,
+        mu=mu,
+        eta=eta,
+        rs=rs,
+        # Density
+        mo_dens_a=orb_dens_up.flatten(),
+        mo_dens_b=orb_dens_dn.flatten(),
+        dens_tot=dens_tot,
+        # Density gradient
+        mo_d_dens_a=orb_d_dens_up.flatten(),
+        mo_d_dens_b=orb_d_dens_dn.flatten(),
+        d_dens_tot=d_dens_tot,
+        # Density laplacian
+        mo_dd_dens_a=orb_dd_dens_up.flatten(),
+        mo_dd_dens_b=orb_dd_dens_dn.flatten(),
+        dd_dens_tot=dd_dens_tot,
+        # KED
+        mo_ked_a=mo_ked_a.flatten(),
+        mo_ked_b=mo_ked_b.flatten(),
+        ked_tot=ked_tot,
+    )
+    return atomdb.Species(dataset, fields)
diff --git a/atomdb/datasets/uhf_augccpvdz/__init__.py b/atomdb/datasets/uhf_augccpvdz/__init__.py
index 1cb76a0a..7bbccd5d 100644
--- a/atomdb/datasets/uhf_augccpvdz/__init__.py
+++ b/atomdb/datasets/uhf_augccpvdz/__init__.py
@@ -13,227 +13,4 @@
 # You should have received a copy of the GNU General Public License
 # along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
 
-r"""UHF compile function."""
-
-import numpy as np
-
-from pyscf import gto, scf
-from pyscf.tools import molden
-
-from iodata import load_one
-
-from gbasis.wrappers import from_iodata
-
-from gbasis.evals.density import evaluate_density as eval_dens
-from gbasis.evals.density import evaluate_deriv_density as eval_d_dens
-from gbasis.evals.density import evaluate_posdef_kinetic_energy_density as eval_pd_ked
-from gbasis.evals.density import evaluate_basis
-from gbasis.evals.eval_deriv import evaluate_deriv_basis
-
-from grid.onedgrid import UniformInteger
-from grid.rtransform import ExpRTransform
-from grid.atomgrid import AtomGrid
-
-import atomdb
-from atomdb.periodic import Element
-
-
-__all__ = [
-    "run",
-]
-
-
-# Parameters to generate an atomic grid from uniform radial grid
-# Use 170 points, lmax = 21 for the Lebedev grid since our basis
-# don't go beyond l=10 in the spherical harmonics.
-BOUND = (1e-5, 2e1)  # (r_min, r_max)
-
-NPOINTS = 100
-
-SIZE = 170  # Lebedev grid sizes
-
-DEGREE = 21  #  Lebedev grid degrees
-
-
-BASIS = "aug-cc-pVDZ"
-
-
-DOCSTRING = """UHF Dataset
-
-Electronic structure and density properties evaluated with aug-cc-pVDZ basis set
-
-"""
-
-
-def eval_orbs_density(one_density_matrix, orb_eval):
-    r"""Return each orbital density evaluated at a set of points
-
-    rho_i(r) = \sum_j P_ij \phi_i(r) \phi_j(r)
-
-    Parameters
-    ----------
-    one_density_matrix : np.ndarray(K_orb, K_orb)
-        One-electron density matrix (1DM) from K orbitals
-    orb_eval : np.ndarray(K_orb, N)
-        orbitals evaluated at a set of grid points (N).
-        These orbitals must be the basis used to evaluate the 1DM.
-
-    Returns
-    -------
-    orb_dens : np.ndarray(K_orb, N)
-        orbitals density at a set of grid points (N)
-    """
-    #
-    # Following lines were taken from Gbasis eval.py module (L60-L61)
-    #
-    density = one_density_matrix.dot(orb_eval)
-    density *= orb_eval
-    return density
-
-
-def eval_orb_ked(one_density_matrix, basis, points, transform=None):
-    "Adapted from Gbasis"
-    orbt_ked = 0
-    for orders in np.identity(3, dtype=int):
-        deriv_orb_eval_one = evaluate_deriv_basis(basis, points, orders, transform=transform)
-        deriv_orb_eval_two = deriv_orb_eval_one  # orders_one == orders_two
-        density = one_density_matrix.dot(deriv_orb_eval_two)
-        density *= deriv_orb_eval_one
-        orbt_ked += density
-    return 0.5 * orbt_ked
-
-
-def run(elem, charge, mult, nexc, dataset, datapath):
-    r"""Run an HCI computation and compile the AtomDB database entry."""
-    # Check arguments
-    if nexc != 0:
-        raise ValueError("Nonzero value of `nexc` is not currently supported")
-
-    # Set up internal variables
-    elem = atomdb.element_symbol(elem)
-    atnum = atomdb.element_number(elem)
-    nelec = atnum - charge
-    nspin = mult - 1
-    n_up = (nelec + nspin) // 2
-    n_dn = (nelec - nspin) // 2
-    obasis_name = BASIS
-
-    # Load restricted Hartree-Fock SCF
-    mol = gto.Mole()
-    mol.build(
-        atom=[[elem, (0, 0, 0)]],
-        basis=obasis_name,
-        charge=charge,
-        spin=nspin,
-    )
-    mf = scf.UHF(mol)
-    mf.kernel()
-    molden.from_scf(
-        mf,
-        atomdb.datafile(".molden", elem, charge, mult, nexc, dataset, datapath),
-        ignore_h=False,
-    )
-
-    scfdata = load_one(atomdb.datafile(".molden", elem, charge, mult, nexc, dataset, datapath))
-    norba = scfdata.mo.norba
-    mo_e_up = scfdata.mo.energies[:norba]
-    mo_e_dn = scfdata.mo.energies[norba:]
-    occs_up = scfdata.mo.occs[:norba]
-    occs_dn = scfdata.mo.occs[norba:]
-    mo_coeff = scfdata.mo.coeffs
-
-    energy = mf.e_tot
-
-    # Prepare data for computing Species properties
-    dm1_up, dm1_dn = mf.make_rdm1()
-    dm1_tot = dm1_up + dm1_dn
-
-    # Make grid
-    onedg = UniformInteger(NPOINTS)  # number of uniform grid points.
-    rgrid = ExpRTransform(*BOUND).transform_1d_grid(onedg)  # radial grid
-    atgrid = AtomGrid(rgrid, degrees=[DEGREE], sizes=[SIZE], center=np.array([0.0, 0.0, 0.0]))
-
-    # Compute densities
-    obasis = from_iodata(scfdata)
-    orb_eval = evaluate_basis(obasis, atgrid.points)
-    orb_dens_up = eval_orbs_density(dm1_up, orb_eval)
-    orb_dens_dn = eval_orbs_density(dm1_dn, orb_eval)
-    # orb_dens_tot = orb_dens_up + orb_dens_dn
-    # dens_tot = np.sum(orb_dens_tot, axis=0)
-    dens_tot = eval_dens(dm1_tot, obasis, atgrid.points)
-
-    # Compute kinetic energy density
-    orb_ked_up = eval_orb_ked(dm1_up, obasis, atgrid.points)
-    orb_ked_dn = eval_orb_ked(dm1_dn, obasis, atgrid.points)
-    ked_tot = eval_pd_ked(dm1_tot, obasis, atgrid.points)
-
-    # Density and KED spherical average
-    dens_spherical_avg = atgrid.spherical_average(dens_tot)
-    ked_spherical_avg = atgrid.spherical_average(ked_tot)
-    dens_splines_up = [atgrid.spherical_average(dens) for dens in orb_dens_up]
-    dens_splines_dn = [atgrid.spherical_average(dens) for dens in orb_dens_dn]
-    ked_splines_up = [atgrid.spherical_average(dens) for dens in orb_ked_up]
-    ked_splines_dn = [atgrid.spherical_average(dens) for dens in orb_ked_dn]
-    # Evaluate interpolated densities in a uniform radial grid
-    rs = rgrid.points
-    dens_avg_tot = dens_spherical_avg(rs)
-    orb_dens_avg_up = np.array([spline(rs) for spline in dens_splines_up])
-    orb_dens_avg_dn = np.array([spline(rs) for spline in dens_splines_dn])
-    ked_avg_tot = ked_spherical_avg(rs)
-    orb_ked_avg_up = np.array([spline(rs) for spline in ked_splines_up])
-    orb_ked_avg_dn = np.array([spline(rs) for spline in ked_splines_dn])
-
-    #
-    # Element properties
-    #
-    atom = Element(elem)
-    atmass = atom.mass["stb"]
-    cov_radius, vdw_radius, at_radius, polarizability, dispersion_c6 = [
-        None,
-    ] * 5
-    # overwrite values for neutral atomic species
-    if charge == 0:
-        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
-        polarizability = atom.pold
-        dispersion_c6 = atom.c6
-
-    #
-    # Conceptual-DFT properties (TODO)
-    #
-    ip = None
-    mu = None
-    eta = None
-
-    # Return Species instance
-    return atomdb.Species(
-        dataset,
-        elem,
-        atnum,
-        obasis_name,
-        nelec,
-        nspin,
-        nexc,
-        atmass,
-        cov_radius,
-        vdw_radius,
-        at_radius,
-        polarizability,
-        dispersion_c6,
-        energy,
-        mo_e_up,
-        mo_e_dn,
-        occs_up,
-        occs_dn,
-        ip,
-        mu,
-        eta,
-        rs=rs,
-        # Density
-        mo_dens_a=orb_dens_avg_up.flatten(),
-        mo_dens_b=orb_dens_avg_dn.flatten(),
-        dens_tot=dens_avg_tot,
-        # KED
-        mo_ked_a=orb_ked_avg_up.flatten(),
-        mo_ked_b=orb_ked_avg_dn.flatten(),
-        ked_tot=ked_avg_tot,
-    )
+r"""UHF dataset."""
diff --git a/atomdb/datasets/uhf_augccpvdz/run.py b/atomdb/datasets/uhf_augccpvdz/run.py
new file mode 100644
index 00000000..3a784e50
--- /dev/null
+++ b/atomdb/datasets/uhf_augccpvdz/run.py
@@ -0,0 +1,239 @@
+# This file is part of AtomDB.
+#
+# AtomDB is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# AtomDB is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
+
+r"""UHF compile function."""
+
+import numpy as np
+
+from pyscf import gto, scf
+from pyscf.tools import molden
+
+from iodata import load_one
+
+from gbasis.wrappers import from_iodata
+
+from gbasis.evals.density import evaluate_density as eval_dens
+from gbasis.evals.density import evaluate_deriv_density as eval_d_dens
+from gbasis.evals.density import evaluate_posdef_kinetic_energy_density as eval_pd_ked
+from gbasis.evals.density import evaluate_basis
+from gbasis.evals.eval_deriv import evaluate_deriv_basis
+
+from grid.onedgrid import UniformInteger
+from grid.rtransform import ExpRTransform
+from grid.atomgrid import AtomGrid
+
+import atomdb
+from atomdb.periodic import Element
+
+
+__all__ = [
+    "run",
+]
+
+
+# Parameters to generate an atomic grid from uniform radial grid
+# Use 170 points, lmax = 21 for the Lebedev grid since our basis
+# don't go beyond l=10 in the spherical harmonics.
+BOUND = (1e-5, 2e1)  # (r_min, r_max)
+
+NPOINTS = 100
+
+SIZE = 170  # Lebedev grid sizes
+
+DEGREE = 21  #  Lebedev grid degrees
+
+
+BASIS = "aug-cc-pVDZ"
+
+
+DOCSTRING = """UHF Dataset
+
+Electronic structure and density properties evaluated with aug-cc-pVDZ basis set
+
+"""
+
+
+def eval_orbs_density(one_density_matrix, orb_eval):
+    r"""Return each orbital density evaluated at a set of points
+
+    rho_i(r) = \sum_j P_ij \phi_i(r) \phi_j(r)
+
+    Parameters
+    ----------
+    one_density_matrix : np.ndarray(K_orb, K_orb)
+        One-electron density matrix (1DM) from K orbitals
+    orb_eval : np.ndarray(K_orb, N)
+        orbitals evaluated at a set of grid points (N).
+        These orbitals must be the basis used to evaluate the 1DM.
+
+    Returns
+    -------
+    orb_dens : np.ndarray(K_orb, N)
+        orbitals density at a set of grid points (N)
+    """
+    #
+    # Following lines were taken from Gbasis eval.py module (L60-L61)
+    #
+    density = one_density_matrix.dot(orb_eval)
+    density *= orb_eval
+    return density
+
+
+def eval_orb_ked(one_density_matrix, basis, points, transform=None):
+    "Adapted from Gbasis"
+    orbt_ked = 0
+    for orders in np.identity(3, dtype=int):
+        deriv_orb_eval_one = evaluate_deriv_basis(basis, points, orders, transform=transform)
+        deriv_orb_eval_two = deriv_orb_eval_one  # orders_one == orders_two
+        density = one_density_matrix.dot(deriv_orb_eval_two)
+        density *= deriv_orb_eval_one
+        orbt_ked += density
+    return 0.5 * orbt_ked
+
+
+def run(elem, charge, mult, nexc, dataset, datapath):
+    r"""Run an HCI computation and compile the AtomDB database entry."""
+    # Check arguments
+    if nexc != 0:
+        raise ValueError("Nonzero value of `nexc` is not currently supported")
+
+    # Set up internal variables
+    elem = atomdb.element_symbol(elem)
+    atnum = atomdb.element_number(elem)
+    nelec = atnum - charge
+    nspin = mult - 1
+    n_up = (nelec + nspin) // 2
+    n_dn = (nelec - nspin) // 2
+    obasis_name = BASIS
+
+    # Load restricted Hartree-Fock SCF
+    mol = gto.Mole()
+    mol.build(
+        atom=[[elem, (0, 0, 0)]],
+        basis=obasis_name,
+        charge=charge,
+        spin=nspin,
+    )
+    mf = scf.UHF(mol)
+    mf.kernel()
+    molden.from_scf(
+        mf,
+        atomdb.datafile(".molden", elem, charge, mult, nexc, dataset, datapath),
+        ignore_h=False,
+    )
+
+    scfdata = load_one(atomdb.datafile(".molden", elem, charge, mult, nexc, dataset, datapath))
+    norba = scfdata.mo.norba
+    mo_e_up = scfdata.mo.energies[:norba]
+    mo_e_dn = scfdata.mo.energies[norba:]
+    occs_up = scfdata.mo.occs[:norba]
+    occs_dn = scfdata.mo.occs[norba:]
+    mo_coeff = scfdata.mo.coeffs
+
+    energy = mf.e_tot
+
+    # Prepare data for computing Species properties
+    dm1_up, dm1_dn = mf.make_rdm1()
+    dm1_tot = dm1_up + dm1_dn
+
+    # Make grid
+    onedg = UniformInteger(NPOINTS)  # number of uniform grid points.
+    rgrid = ExpRTransform(*BOUND).transform_1d_grid(onedg)  # radial grid
+    atgrid = AtomGrid(rgrid, degrees=[DEGREE], sizes=[SIZE], center=np.array([0.0, 0.0, 0.0]))
+
+    # Compute densities
+    obasis = from_iodata(scfdata)
+    orb_eval = evaluate_basis(obasis, atgrid.points)
+    orb_dens_up = eval_orbs_density(dm1_up, orb_eval)
+    orb_dens_dn = eval_orbs_density(dm1_dn, orb_eval)
+    # orb_dens_tot = orb_dens_up + orb_dens_dn
+    # dens_tot = np.sum(orb_dens_tot, axis=0)
+    dens_tot = eval_dens(dm1_tot, obasis, atgrid.points)
+
+    # Compute kinetic energy density
+    orb_ked_up = eval_orb_ked(dm1_up, obasis, atgrid.points)
+    orb_ked_dn = eval_orb_ked(dm1_dn, obasis, atgrid.points)
+    ked_tot = eval_pd_ked(dm1_tot, obasis, atgrid.points)
+
+    # Density and KED spherical average
+    dens_spherical_avg = atgrid.spherical_average(dens_tot)
+    ked_spherical_avg = atgrid.spherical_average(ked_tot)
+    dens_splines_up = [atgrid.spherical_average(dens) for dens in orb_dens_up]
+    dens_splines_dn = [atgrid.spherical_average(dens) for dens in orb_dens_dn]
+    ked_splines_up = [atgrid.spherical_average(dens) for dens in orb_ked_up]
+    ked_splines_dn = [atgrid.spherical_average(dens) for dens in orb_ked_dn]
+    # Evaluate interpolated densities in a uniform radial grid
+    rs = rgrid.points
+    dens_avg_tot = dens_spherical_avg(rs)
+    orb_dens_avg_up = np.array([spline(rs) for spline in dens_splines_up])
+    orb_dens_avg_dn = np.array([spline(rs) for spline in dens_splines_dn])
+    ked_avg_tot = ked_spherical_avg(rs)
+    orb_ked_avg_up = np.array([spline(rs) for spline in ked_splines_up])
+    orb_ked_avg_dn = np.array([spline(rs) for spline in ked_splines_dn])
+
+    #
+    # Element properties
+    #
+    atom = Element(elem)
+    atmass = atom.mass["stb"]
+    cov_radius, vdw_radius, at_radius, polarizability, dispersion_c6 = [
+        None,
+    ] * 5
+    # overwrite values for neutral atomic species
+    if charge == 0:
+        cov_radius, vdw_radius, at_radius = (atom.cov_radius, atom.vdw_radius, atom.at_radius)
+        polarizability = atom.pold
+        dispersion_c6 = atom.c6
+
+    #
+    # Conceptual-DFT properties (TODO)
+    #
+    ip = None
+    mu = None
+    eta = None
+
+    # Return Species instance
+    fields = dict(
+        elem=elem,
+        atnum=atnum,
+        obasis_name=obasis_name,
+        nelec=nelec,
+        nspin=nspin,
+        nexc=nexc,
+        # atmass=atmass,
+        # cov_radius=cov_radius,
+        # vdw_radius=vdw_radius,
+        # at_radius=at_radius,
+        # polarizability=polarizability,
+        # dispersion_c6=dispersion_c6,
+        energy=energy,
+        mo_e_up=mo_e_up,
+        mo_e_dn=mo_e_dn,
+        occs_up=occs_up,
+        occs_dn=occs_dn,
+        ip=ip,
+        mu=mu,
+        eta=eta,
+        rs=rs,
+        # Density
+        mo_dens_a=orb_dens_avg_up.flatten(),
+        mo_dens_b=orb_dens_avg_dn.flatten(),
+        dens_tot=dens_avg_tot,
+        # KED
+        mo_ked_a=orb_ked_avg_up.flatten(),
+        mo_ked_b=orb_ked_avg_dn.flatten(),
+        ked_tot=ked_avg_tot,
+    )
+    return atomdb.Species(dataset, fields)
diff --git a/atomdb/periodic.py b/atomdb/periodic.py
index 643071b3..f968928c 100644
--- a/atomdb/periodic.py
+++ b/atomdb/periodic.py
@@ -1,210 +1,247 @@
-import csv
-import numpy as np
+from csv import reader
 
 from importlib_resources import files
-from atomdb.utils import convertor_types
 
-__all__ = ["_sym2num", "_name2num", "_num2sym", "_num2name", "Element"]
-
-
-if __name__ == "__main__":
-    print("This module is not meant to be run directly.")
-    print("Use 'from atomdb.periodic import Element' instead.")
-    raise SystemExit
-
-
-ATOM_DOCSTRING_BASE = (
-    "Class to store the properties of the elements.\n\n"
-    + "Attributes\n----------\n"
-    + "atnum : int\n    Atomic number of the element.\n"
-    + "symbol : str\n    Chemical symbol of the element.\n"
-    + "name : str\n    Name of the element.\n"
-)
-
-
-# auxiliary functions
-def _read_data_csv(file):
-    """Reads a csv file and returns a list of lists with the data.
-
-    The data is read from the file and stored in a list of lists. The first two rows are the column
-    headers and the key sources, respectively. From the third row onwards are the data values.
-
-
-    Parameters
-    ----------
-    file : file
-        The file to read.
-
-    Returns
-    -------
-    list of lists
-        The data in the file.
-    """
-    data = []
-    for row in csv.reader(file):
-        # ignore comments and empty lines
-        if row and not row[0].startswith("#") and not all(c in ", \n" for c in row):
-            # Replace \n with new line in each element
-            data.append([element.replace("\\n", "\n") for element in row])
-
-    # pop units from data (third row)
-    units = data.pop(2)
+from atomdb.utils import CONVERTOR_TYPES
+
+__all__ = [
+    "Element",
+    "element_number",
+    "element_symbol",
+    "element_name",
+]
+
+
+def setup_element():
+    r"""Generate the ``Element`` class and helper functions."""
+    data, props, srcs, units, prop2col, num2str, str2num = get_data()
+    prop2name, prop2desc, prop2src, prop2url, prop2note = get_info()
+
+    def element_number(elem):
+        (
+            "Return the element number from a string or int.\n"
+            "\n"
+            "Parameters\n"
+            "----------\n"
+            "elem: (str | int)\n"
+            "    Symbol, name, or number of an element.\n"
+            "\n"
+            "Returns\n"
+            "-------\n"
+            "atnum : int\n"
+            "    Atomic number.\n"
+        )
+        return str2num[elem] if isinstance(elem, str) else int(elem)
+
+    def element_symbol(elem):
+        (
+            "Return the element symbol from a string or int.\n"
+            "\n"
+            "Parameters\n"
+            "----------\n"
+            "elem: (str | int)\n"
+            "    Symbol, name, or number of an element.\n"
+            "\n"
+            "Returns\n"
+            "-------\n"
+            "symbol : str\n"
+            "    Element symbol.\n"
+        )
+        return num2str[element_number(elem) if isinstance(elem, str) else int(elem)][0]
+
+    def element_name(elem):
+        (
+            "Return the element name from a string or int.\n"
+            "\n"
+            "Parameters\n"
+            "----------\n"
+            "elem: (str | int)\n"
+            "    Symbol, name, or number of an element.\n"
+            "\n"
+            "Returns\n"
+            "-------\n"
+            "name : str\n"
+            "    Element name.\n"
+        )
+        return num2str[element_number(elem) if isinstance(elem, str) else int(elem)][1]
+
+    def init(self, elem):
+        (
+            "Initialize an ``Element`` instance.\n"
+            "\n"
+            "Parameters\n"
+            "----------\n"
+            "elem : (str | int)\n"
+            "    Symbol, name, or number of an element.\n"
+        )
+        self._atnum = element_number(elem)
+
+    @property
+    def atnum(self):
+        return self._atnum
+
+    atnum.__doc__ = "Atomic number of the element.\n" "\n" "Returns\n" "-------\n" "atnum : int\n"
+
+    @property
+    def symbol(self):
+        return element_symbol(self._atnum)
+
+    symbol.__doc__ = "Symbol of the element.\n" "\n" "Returns\n" "-------\n" "symbol : str\n"
+
+    @property
+    def name(self):
+        return element_name(self._atnum)
+
+    name.__doc__ = "Name of the element.\n" "\n" "Returns\n" "-------\n" "name : str\n"
+
+    # Element attributes; add __init__ method
+    attrs = {
+        "__init__": init,
+        "atnum": atnum,
+        "symbol": symbol,
+        "name": name,
+    }
+
+    # ELement class docstring header
+    class_doc = (
+        "Element properties.\n"
+        "\n"
+        "Attributes\n"
+        "----------\n"
+        "atnum : int\n"
+        "    Atomic number of the element.\n"
+        "symbol : str\n"
+        "    Symbol of the element.\n"
+        "name : str\n"
+        "    Name of the element.\n"
+    )
+
+    # Autocomplete class docstring with data from the CSV files
+    for prop, name in prop2name.items():
+
+        # Add signature, description, sources, units, urls, and notes
+        short = f"{name} of the element."
+        if len(prop2col[prop]) == 1 and "" in prop2col[prop]:
+            # Only one default source
+            t = type(data[0][prop2col[prop][""]]).__name__
+            sig = f"{prop} : {t}"
+            long = ""
+        else:
+            # Multiple or non-default sources
+            t = type(data[0][next(iter(prop2col[prop].values()))]).__name__
+            sig = f"{prop} : Dict[{t}]"
+            long = "\n" "Notes\n" "-----\n" "This property is a dictionary with the following keys:"
+            # Add unit, url, note for each source
+            for src in prop2col[prop].keys():
+                long += f'\n    * "{src}"'
+                if prop2src[prop][src] != "":
+                    long += "\n        * Source\n" f"{indent_lines(prop2src[prop][src], 12)}"
+                if units[prop2col[prop][src]] != "":
+                    long += "\n        * Units\n" f"{indent_lines(units[prop2col[prop][src]], 12)}"
+                if prop2url[prop][src] != "":
+                    long += "\n        * URL\n" f"{indent_lines(prop2url[prop][src], 12)}"
+                if prop2note[prop][src] != "":
+                    long += "\n        * Notes\n" f"{indent_lines(prop2note[prop][src], 12)}"
+            long += "\n"
+
+        # Add property to class docstring
+        class_doc += f"{sig}\n" f"    {short}\n"
+
+        # Make property method for Element class with docstring
+        f = make_property(data, prop, prop2col)
+        f.__doc__ = f"{short}\n" "\n" "Returns\n" "-------\n" f"{sig}\n" f"{long}"
+
+        # Add property method to attributes
+        attrs[prop] = f
+
+    # Add class docstring to attributes
+    attrs["__doc__"] = class_doc
+
+    # Construct Element class
+    Element = type("Element", (object,), attrs)
+
+    # Return constructed class and functions
+    return Element, element_number, element_symbol, element_name
+
+
+def read_csv(file):
+    r"""Read a CSV file into a list of lists."""
+    lines = []
+    with open(file) as f:
+        for row in reader(f):
+            # ignore comments and empty lines
+            if row and not row[0].lstrip().startswith("#") and any(c not in ", \n" for c in row):
+                # Replace \n with new line in each element
+                lines.append([i.replace("\\n", "\n") for i in row])
+    return lines
+
+
+def get_data():
+    r"""Extract the contents of ``data/elements_data.csv``."""
+    data = read_csv(files("atomdb.data").joinpath("elements_data.csv"))
+    # Get property keys/source keys/units
+    props = data.pop(0)[3:]
+    srcs = data.pop(0)[3:]
+    units = data.pop(0)[3:]
+    # Create utility dictionary to locate the columns of the properties
+    prop2col = {}
+    for col, (prop, key) in enumerate(zip(props, srcs)):
+        prop2col.setdefault(prop, {})[key] = col
+    # Create maps from symbol/name to element number
+    num2str, str2num = {}, {}
+    for row in data:
+        atnum = int(row.pop(0))
+        symbol = row.pop(0).title()
+        name = row.pop(0).title()
+        num2str[atnum] = (symbol, name)
+        str2num[symbol] = atnum
+        str2num[name] = atnum
+        # Convert the rest of the data to numbers
+        for i, (unit, val) in enumerate(zip(units, row)):
+            row[i] = CONVERTOR_TYPES[unit](val) if val else None
+    return data, props, srcs, units, prop2col, num2str, str2num
+
+
+def get_info():
+    r"""Extract the contents of ``data/data_info.csv``."""
+    info = read_csv(files("atomdb.data").joinpath("data_info.csv"))
+    prop2name = {}
+    prop2desc = {}
+    prop2src = {}
+    prop2url = {}
+    prop2note = {}
+    for prop, name, key, desc, src, url, note in info:
+        prop2name[prop] = name
+        prop2desc.setdefault(prop, {})[key] = desc
+        prop2src.setdefault(prop, {})[key] = src
+        prop2url.setdefault(prop, {})[key] = url
+        prop2note.setdefault(prop, {})[key] = note
+    return prop2name, prop2desc, prop2src, prop2url, prop2note
+
+
+def indent_lines(input_string, indent):
+    r"""Indent each line of a string by a given number of spaces."""
+    lines = input_string.splitlines()
+    indented_lines = [(indent * " ") + line for line in lines]
+    return "\n".join(indented_lines)
 
-    # convert data to the appropriate type (int, float, str, or amu)
-    for row in data[2:]:
-        row[:] = [convertor_types[unit](i) if i else np.nan for unit, i in zip(units, row)]
-    return data
 
+def make_property(data, prop, prop2col):
+    r"""Construct a property method for the Element class."""
 
-# auxiliary functions
-def _read_csv(file):
-    """Reads a csv file and returns a list of lists with the data.
+    if len(prop2col[prop]) == 1 and "" in prop2col[prop]:
 
-    Parameters
-    ----------
-    file : file
-        The file to read.
+        def f(self):
+            return data[self._atnum - 1][prop2col[prop][""]]
 
-    Returns
-    -------
-    list of lists
-        The data in the file.
-    """
-    data = []
-    for row in csv.reader(file):
-        # ignore comments and empty lines
-        if row and not row[0].startswith("#") and not all(c in ", \n" for c in row):
-            # Replace \n with new line in each element
-            data.append([cell.replace("\\n", "\n") for cell in row])
-    return data
+    else:
 
+        def f(self):
+            row = data[self._atnum - 1]
+            return {k: row[v] for k, v in prop2col[prop].items() if row[v] is not None}
 
-def _indent_lines(input_string, indent):
-    """Indent each line of a string by a given number of spaces."""
-    lines = input_string.splitlines()
-    indented_lines = [(indent * " ") + line for line in lines]
-    return "\n".join(indented_lines)
+    return property(f)
 
 
-def _gendoc(info_file):
-    """Generate a docstring for the Element class.
-
-    Parameters
-    ----------
-    info_file : str
-        The name of the csv file with the data for constructing the docstring.
-    """
-    # create a dictionary to store the metadata of the properties
-    _data_src = _read_csv(open(info_file))
-    _prop2name = {}
-    _prop2desc = {}
-    _prop2source = {}
-    _prop2url = {}
-    _prop2notes = {}
-    for prop, prop_name, key, description, source, url, notes in _data_src:
-        _prop2name[prop] = prop_name
-        _prop2desc.setdefault(prop, {})[key] = description
-        _prop2source.setdefault(prop, {})[key] = source
-        _prop2url.setdefault(prop, {})[key] = url
-        _prop2notes.setdefault(prop, {})[key] = notes
-
-    docstring = ATOM_DOCSTRING_BASE
-
-    # autocomplete the docstring with data from the csv files
-    # for each property
-    for i in _prop2name:
-        # if only one source is available
-        if len(_prop2desc[i]) == 1:
-            # add line to docstring with property name, type, description, unit, source, and url
-            docstring += f"{i} : float\n    {_prop2name[i]} of the element.\n"
-            if list(_prop2desc[i].values())[0] != "":
-                docstring += _indent_lines(f"{list(_prop2desc[i].values())[0]}", 4) + "\n"
-            if list(_prop2source[i].values())[0] != "":
-                docstring += _indent_lines(f"{list(_prop2source[i].values())[0]}", 4) + "\n"
-            if list(_prop2url[i].values())[0] != "":
-                docstring += _indent_lines(f"{list(_prop2url[i].values())[0]}", 4) + "\n"
-            if list(_prop2notes[i].values())[0] != "":
-                docstring += _indent_lines(f"Notes:\n{list(_prop2notes[i].values())[0]}", 4) + "\n"
-        # if multiple sources are available
-        else:
-            docstring += f"{i} : dict\n    Dictionary with the {_prop2name[i]} of the element.\n"
-            # add a line docstring with the description, source, and url for each source
-            for j in _prop2col[i]:
-                if _prop2desc[i][j] != "":
-                    docstring += _indent_lines(f"{j} : {_prop2desc[i][j]}", 4) + "\n"
-    return docstring
-
-
-# Code to run when the module is imported
-data_file = files("atomdb.data").joinpath("elements_data.csv")
-info_file = files("atomdb.data").joinpath("data_info.csv")
-
-
-# work with data from "elements_data.csv"
-data = _read_data_csv(open(data_file))
-
-# get properties and key sources
-properties = data.pop(0)[3:]
-key_sources = data.pop(0)[3:]
-
-# convert data to the appropriate type
-atnums, symbols, names = [], [], []
-
-# set the atomic numbers, symbols, and names of the elements from the data
-atnums = [row[0] for row in data]
-symbols = [row[1] for row in data]
-names = [row[2] for row in data]
-
-# create utility dictionaries to convert between different element identifiers
-_sym2num = dict(zip(symbols, atnums))
-_name2num = dict(zip(names, atnums))
-_num2sym = dict(zip(atnums, symbols))
-_num2name = dict(zip(atnums, names))
-
-# create utility dictionary to locate the columns of the properties
-_prop2col = {}
-for column, (prop, key) in enumerate(zip(properties, key_sources)):
-    _prop2col.setdefault(prop, {})[key] = {"ncol": column + 3}
-
-
-class Element:
-    def __init__(self, id):
-        """Create an Element object.
-
-        Parameters
-        ----------
-        id : str or int
-            The atomic number, symbol, or name of the element.
-        """
-        # check if the input is a valid element identifier and get the atomic number
-        if isinstance(id, str):
-            # if it is a is a name
-            if len(id) > 3:
-                self.atnum = _name2num[id.lower()]
-            # if it is a symbol
-            else:
-                self.atnum = _sym2num[id]
-        elif isinstance(id, int) and id in _sym2num.values():
-            self.atnum = id
-        else:
-            raise ValueError(
-                "Invalid input for element identifier, must be Z, symbol, or name of element"
-            )
-        # set the symbol and name of the element
-        self.atsym = _num2sym[self.atnum]
-        self.atname = _num2name[self.atnum].capitalize()
-
-        # create a dictionary to store the properties of the element
-        tmp_dict = _prop2col.copy()
-        for i in tmp_dict:
-            # select the multiplicity of isoelectronic element
-            if len(tmp_dict[i]) == 1 and "" in tmp_dict[i]:
-                tmp_dict[i] = data[self.atnum - 1][tmp_dict[i][""]["ncol"]]
-            else:
-                tmp_dict[i] = {j: data[self.atnum - 1][tmp_dict[i][j]["ncol"]] for j in tmp_dict[i]}
-        # set the class attributes to the properties of the element
-        self.__dict__.update(tmp_dict)
-
-
-# set the docstring of the Element class
-Element.__doc__ = _gendoc(info_file)
+# Generate class and functions to export
+Element, element_number, element_symbol, element_name = setup_element()
diff --git a/atomdb/promolecule.py b/atomdb/promolecule.py
index be529ff3..529d5bb0 100644
--- a/atomdb/promolecule.py
+++ b/atomdb/promolecule.py
@@ -24,15 +24,9 @@
 import numpy as np
 from scipy.optimize import linprog
 
-from .api import (
-    DEFAULT_DATAPATH,
-    DEFAULT_DATASET,
-    MULTIPLICITIES,
-    element_number,
-    element_symbol,
-    load,
-    load_all,
-)
+from atomdb.utils import DEFAULT_DATAPATH, DEFAULT_DATASET, MULTIPLICITIES
+from atomdb.periodic import element_number, element_symbol
+from atomdb.species import load
 
 __all__ = [
     "Promolecule",
@@ -44,9 +38,10 @@ class Promolecule:
     r"""
     Promolecule class.
 
-    A promolecule is an approximation of a molecule constructed from a linear combination of atomic
-    and/or ionic species. Properties of this promolecule can be computed from those of the atomic
-    and/or ionic species, depending on whether the property is an extensive one or an intensive one.
+    A promolecule is an approximation of a molecule constructed from a linear
+    combination of atomic and/or ionic species. Properties of this promolecule
+    can be computed from those of the atomic and/or ionic species, depending
+    on whether the property is an extensive one or an intensive one.
 
     For an extensive property, the formula is a linear combination:
 
@@ -60,9 +55,12 @@ class Promolecule:
     .. math::
 
         \text{prop.}_{\text{mol;intensive}}
-          = {\left\langle \left\{ \text{prop.}_A \right\}_{A=1}^{N_{\text{atoms}}} \right\rangle}_p
+          = {\left\langle
+               \left\{ \text{prop.}_A \right\}_{A=1}^{N_{\text{atoms}}}
+             \right\rangle}_p
 
-    where the parameter ``p`` defines the type of mean used (1 = linear, 2 = geometric, etc.).
+    where the parameter ``p`` defines the type of mean used
+    (1 = linear, 2 = geometric, etc.).
 
     Attributes
     ----------
@@ -78,7 +76,8 @@ class Promolecule:
     density(points, spin='ab', log=False)
         Compute the electron density of the promolecule at the desired points.
     ked(points, spin='ab', log=False)
-        Compute the kinetic energy density of the promolecule at the desired points.
+        Compute the kinetic energy density of the promolecule at the desired
+        points.
     energy()
         Compute the energy of the promolecule.
     mass()
@@ -133,20 +132,28 @@ def density(self, points, spin="ab", log=False):
             Type of density to compute; either total, alpha-spin, beta-spin,
             or magnetization density.
         log: bool, default=False
-            Whether to compute the log of the density instead of the density itself.
+            Whether to compute the log of the density instead of the density.
             May be slightly more accurate.
 
         """
 
-        # Define the property as a function, and call `_extensive_local_property` on it
         def f(atom):
             return atom.density_func(spin=spin, log=log)
 
-        return sum(_extensive_local_property(self.atoms, self.coords, self.coeffs, points, f))
+        return sum(
+            _extensive_local_property(
+                self.atoms,
+                self.coords,
+                self.coeffs,
+                points,
+                f,
+            )
+        )
 
     def ked(self, points, spin="ab", log=False):
         r"""
-        Compute the kinetic energy density of the promolecule at the desired points.
+        Compute the kinetic energy density of the promolecule at the desired
+        points.
 
         points: np.ndarray((N, 3), dtype=float)
             Points at which to compute the density.
@@ -154,7 +161,7 @@ def ked(self, points, spin="ab", log=False):
             Type of density to compute; either total, alpha-spin, beta-spin,
             or magnetization density.
         log: bool, default=False
-            Whether to compute the log of the density instead of the density itself.
+            Whether to compute the log of the density instead of the density.
             May be slightly more accurate.
 
         """
@@ -162,7 +169,15 @@ def ked(self, points, spin="ab", log=False):
         def f(atom):
             return atom.ked_func(spin=spin, log=log)
 
-        return sum(_extensive_local_property(self.atoms, self.coords, self.coeffs, points, f))
+        return sum(
+            _extensive_local_property(
+                self.atoms,
+                self.coords,
+                self.coeffs,
+                points,
+                f,
+            )
+        )
 
     def nelec(self):
         r"""Compute the electron number of the promolecule."""
@@ -175,7 +190,6 @@ def f(atom):
     def charge(self):
         r"""Compute the charge of the promolecule."""
 
-        # return sum(atom.atnum for atom in self.atoms) - self.nelec()
         def f(atom):
             return atom.charge
 
@@ -201,7 +215,7 @@ def nspin(self, p=1):
         r"""Compute the spin number of the promolecule."""
 
         def f(atom):
-            return atom.nspin * atom.spinpol
+            return atom.nspin
 
         return _intensive_property(self.atoms, self.coeffs, f, p=1)
 
@@ -216,11 +230,10 @@ def ip(self, p=1):
         Parameters
         ----------
         p: int, default=1
-            Value of ``p`` for the p-mean computation of this intensive property.
+            Value of ``p`` for the p-mean computation.
 
         """
 
-        # Define the property as a function, and call `_intensive_property` on it
         def f(atom):
             return atom.ip
 
@@ -233,11 +246,10 @@ def mu(self, p=1):
         Parameters
         ----------
         p: int, default=1
-            Value of ``p`` for the p-mean computation of this intensive property.
+            Value of ``p`` for the p-mean computation.
 
         """
 
-        # Define the property as a function, and call `_intensive_property` on it
         def f(atom):
             return atom.mu
 
@@ -250,11 +262,10 @@ def eta(self, p=1):
         Parameters
         ----------
         p: int, default=1
-            Value of ``p`` for the p-mean computation of this intensive property.
+            Value of ``p`` for the p-mean computation.
 
         """
 
-        # Define the property as a function, and call `_intensive_property` on it
         def f(atom):
             return atom.eta
 
@@ -262,18 +273,24 @@ def f(atom):
 
     def gradient(self, points, spin="ab", log=False):
         r"""
-        Compute the electron density gradient of the promolecule at the desired points.
+        Compute the electron density gradient of the promolecule at the
+        desired points.
 
         Promolecular gradient:
         .. math::
             \nabla \rho_{\text{mol}}^{(0)} (\mathbf{R})
-                = \sum_{A=1}^{N_{\text{atoms}}} c_A \nabla \rho_A^{(0)}(\mathbf{R})
+                = \sum_{A=1}^{N_{\text{atoms}}}
+                    c_A \nabla \rho_A^{(0)}(\mathbf{R})
+
+        where
+
+        :math:`N_{\text{atoms}}` is the number of atoms in the molecule,
+
+        :math:`c_A` are the coefficients of the species,
 
-        where,
-            :math:`N_{\text{atoms}}` is the number of atoms in the molecule.
-            :math:`c_A` are the coefficients of the specie.
-            :math:`R` are points in 3D cartesian coordinates.
-            :math:`\nabla \rho_A^{(0)}(\mathbf{R})` is the gradient of specie A.
+        :math:`R` are points in 3D cartesian coordinates,
+
+        :math:`\nabla \rho_A^{(0)}(\mathbf{R})` is the gradient of the species.
 
         Parameters
         ----------
@@ -283,7 +300,7 @@ def gradient(self, points, spin="ab", log=False):
             Type of density to compute; either total, alpha-spin, beta-spin,
             or magnetization density.
         log: bool, default=False
-            Whether to compute the log of the density instead of the density itself.
+            Whether to compute the log of the density instead of the density.
             May be slightly more accurate.
 
         Returns
@@ -292,17 +309,26 @@ def gradient(self, points, spin="ab", log=False):
 
         """
 
-        # Define the property as a function, and call `_extensive_local_property` on it
         def f(atom):
             return atom.ddens_func(spin=spin, log=log)
 
-        atoms_ddens = _extensive_local_property(self.atoms, self.coords, self.coeffs, points, f)
+        atoms_ddens = _extensive_local_property(
+            self.atoms,
+            self.coords,
+            self.coeffs,
+            points,
+            f,
+        )
 
-        # The cartesian gradient (\nabla \rho_A) has to be evaluated using the chain rule:
-        # \nabla \rho_A(x,y,z) = \frac{\partial \rho_A}{\partial r} *  \hat{r}
-        # where `\frac{\partial \rho_A}{\partial r}` is the gradient in spherical coordinates
-        # and   \hat{r} = [dr/dx, dr/dy, dr/dz] is the radial unit vector:
-        # dr/dx = (x - x_A) / |r-R_A|
+        # The cartesian gradient (\nabla \rho_A) has to be evaluated using
+        # the chain rule:
+        #   \nabla \rho_A(x,y,z) = \frac{\partial \rho_A}{\partial r} * \hat{r}
+        # where
+        #   \frac{\partial \rho_A}{\partial r}
+        # is the gradient in spherical coordinates and
+        #   \hat{r} = [dr/dx, dr/dy, dr/dz]`
+        # is the radial unit vector:
+        #   dr/dx = (x - x_A) / |r-R_A|
         #
         # Define a unit vector function
         def unit_v(vector):
@@ -316,7 +342,8 @@ def unit_v(vector):
 
     def hessian(self, points, spin="ab", log=False):
         r"""
-        Compute the promolecule's electron density Hessian at the desired points.
+        Compute the promolecule's electron density Hessian at the
+        desired points.
 
         Parameters
         ----------
@@ -326,33 +353,43 @@ def hessian(self, points, spin="ab", log=False):
             Type of density to compute; either total, alpha-spin, beta-spin,
             or magnetization density.
         log: bool, default=False
-            Whether to compute the log of the density instead of the density itself.
+            Whether to compute the log of the density instead of the density.
             May be slightly more accurate.
 
         """
 
-        # Define the property as a function, and call `_extensive_local_property` on it
         def df(atom):
             return atom.ddens_func(spin=spin, log=log)
 
         def d2f(atom):
             return atom.d2dens_func(spin=spin, log=log)
 
-        atoms_ddens = _extensive_local_property(self.atoms, self.coords, self.coeffs, points, df)
-        atoms_d2dens = _extensive_local_property(self.atoms, self.coords, self.coeffs, points, d2f)
+        atoms_ddens = _extensive_local_property(
+            self.atoms,
+            self.coords,
+            self.coeffs,
+            points,
+            df,
+        )
+        atoms_d2dens = _extensive_local_property(
+            self.atoms,
+            self.coords,
+            self.coeffs,
+            points,
+            d2f,
+        )
 
         # Evaluate the derivatives of the radii
-        atoms_router_triu = np.array(
-            [_radial_vector_outer_triu(points - coord) for coord in self.coords]
-        )
+        dradii = np.array([_radial_vector_outer_triu(points - coord) for coord in self.coords])
 
         # Get the unique elements of the Hessian
-        # \sum_A c_A [d^2f_A/dx^2, d^2f_A/dxdy, d^2f_A/dxdz, d^2f_A/dydz, d^2f_A/dz]
+        # \sum_A c_A
+        #   [d^2f_A/dx^2, d^2f_A/dxdy, d^2f_A/dxdz, d^2f_A/dydz, d^2f_A/dz]
         # where
-        # d^2f_A/dx^2 = (d^2f_A/dr^2 - df_A/dr) (dr_A/dx)^2 + shift
-        # d^2f_A/dxdy = (d^2f_A/dr^2 - df_A/dr) d^r_A/dx d^r_A/dy
+        #   d^2f_A/dx^2 = (d^2f_A/dr^2 - df_A/dr) (dr_A/dx)^2 + shift
+        #   d^2f_A/dxdy = (d^2f_A/dr^2 - df_A/dr) d^r_A/dx d^r_A/dy
         interm = 0
-        for d2dens, ddens, rhess in zip(atoms_d2dens, atoms_ddens, atoms_router_triu):
+        for d2dens, ddens, rhess in zip(atoms_d2dens, atoms_ddens, dradii):
             interm += (d2dens - ddens)[:, None] * rhess
 
         # Reconstruct the Hessian matrix
@@ -373,7 +410,8 @@ def d2f(atom):
 
     def laplacian(self, points, spin="ab", log=False):
         r"""
-        Compute the promolecule's electron density Laplacian at the desired points.
+        Compute the promolecule's electron density Laplacian at the
+        desired points.
 
         Parameters
         ----------
@@ -383,7 +421,7 @@ def laplacian(self, points, spin="ab", log=False):
             Type of density to compute; either total, alpha-spin, beta-spin,
             or magnetization density.
         log: bool, default=False
-            Whether to compute the log of the density instead of the density itself.
+            Whether to compute the log of the density instead of the density.
             May be slightly more accurate.
 
         """
@@ -398,8 +436,20 @@ def shift(dens, radii):
             return 3 * dens / np.linalg.norm(radii)
 
         # Radial derivatives of the density
-        atoms_ddens = _extensive_local_property(self.atoms, self.coords, self.coeffs, points, df)
-        atoms_d2dens = _extensive_local_property(self.atoms, self.coords, self.coeffs, points, d2f)
+        atoms_ddens = _extensive_local_property(
+            self.atoms,
+            self.coords,
+            self.coeffs,
+            points,
+            df,
+        )
+        atoms_d2dens = _extensive_local_property(
+            self.atoms,
+            self.coords,
+            self.coeffs,
+            points,
+            d2f,
+        )
 
         return sum(
             d2dens - ddens + shift(ddens, points - coord)
@@ -417,8 +467,8 @@ def make_promolecule(
     datapath=DEFAULT_DATAPATH,
 ):
     r"""
-    Construct a Promolecule instance from a set of atoms and their coordinates, charges,
-    and multiplicities.
+    Construct a Promolecule instance from a set of atoms and their coordinates,
+    charges, and multiplicities.
 
     Parameters
     ----------
@@ -444,7 +494,7 @@ def make_promolecule(
     elif units.lower() == "angstrom":
         coords = [coord / 0.52917721092 for coord in coords]
     else:
-        raise ValueError("Invalid `units` parameter; must be 'bohr' or 'angstrom'")
+        raise ValueError(f"Invalid `units` parameter '{units}'; " "must be 'bohr' or 'angstrom'")
 
     # Get atomic symbols/numbers from inputs
     atoms = [element_symbol(atom) for atom in atnums]
@@ -456,7 +506,7 @@ def make_promolecule(
 
     # Handle default multiplicity parameters
     if mults is None:
-        # Force non-int charge to be integer here; it will be overwritten below.
+        # Force non-int charge to be integer here; will be overwritten below.
         mults = [
             MULTIPLICITIES[max(1, int(np.round(atnum - charge)))]
             for (atnum, charge) in zip(atnums, charges)
@@ -479,7 +529,8 @@ def make_promolecule(
             except FileNotFoundError:
                 warn(
                     "Unable to load species corresponding to `charge, mult`; "
-                    "generating species via linear combination of other species'",
+                    "generating species via linear combination "
+                    "of other species",
                     stacklevel=1,
                 )
 
@@ -488,7 +539,7 @@ def make_promolecule(
         nelec = atnum - charge
         nspin = np.sign(mult) * (abs(mult) - 1)
         # Get all candidates for linear combination
-        species_list = load_all(atom, dataset=dataset, datapath=datapath)
+        species_list = load(atom, ..., ..., nexc=0, dataset=dataset, datapath=datapath)
         for specie in species_list[: len(species_list)]:
             if specie.nspin > 0:
                 specie_neg_spinpol = deepcopy(specie)
@@ -505,7 +556,7 @@ def make_promolecule(
                     [
                         [1 for t in ts],
                         [t.nelec for t in ts],
-                        [t.nspin * t.spinpol for t in ts],
+                        [t.nspin for t in ts],
                     ],
                     dtype=float,
                 ),
@@ -518,7 +569,7 @@ def make_promolecule(
             promol._extend(*(min(good_combs, key=itemgetter(0))[1:]))
         else:
             raise ValueError(
-                "Unable to construct species with non-integer charge/spin from database entries"
+                "Unable to construct species with non-integer" "charge/spin from database entries"
             )
 
     # Return Promolecule instance
diff --git a/atomdb/species.py b/atomdb/species.py
new file mode 100644
index 00000000..d06cc84e
--- /dev/null
+++ b/atomdb/species.py
@@ -0,0 +1,599 @@
+# This file is part of AtomDB.
+#
+# AtomDB is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# AtomDB is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
+
+r"""AtomDB, a database of atomic and ionic properties."""
+
+from dataclasses import dataclass, field, asdict
+
+from glob import glob
+
+from importlib import import_module
+
+import json
+
+from numbers import Integral
+
+from os import makedirs, path
+
+from msgpack import packb, unpackb
+
+from msgpack_numpy import encode, decode
+
+import numpy as np
+
+from numpy import ndarray
+
+from scipy.interpolate import CubicSpline
+
+from atomdb.utils import DEFAULT_DATASET, DEFAULT_DATAPATH
+from atomdb.periodic import element_symbol
+
+
+__all__ = [
+    "Species",
+    "compile",
+    "dump",
+    "load",
+    "raw_datafile",
+]
+
+
+def default_required(name, typeof):
+    r"""Default factory for required fields."""
+
+    def f():
+        raise KeyError(f"Field {name} of type {typeof} was not found")
+
+    return f
+
+
+def default_vector():
+    r"""Default factory for 1-dimensional ``np.ndarray``."""
+
+    return np.zeros(0).reshape(0)
+
+
+def default_matrix():
+    r"""Default factory for 2-dimensional ``np.ndarray``."""
+    return np.zeros(0).reshape(1, 0)
+
+
+def scalar(method):
+    r"""Expose a SpeciesData field."""
+    name = method.__name__
+
+    @property
+    def wrapper(self):
+        rf"""{method.__doc__}"""
+        return getattr(self._data, name)
+
+    return wrapper
+
+
+def _remove_suffix(input_string, suffix):
+    if suffix and input_string.endswith(suffix):
+        return input_string[: -len(suffix)]
+    return input_string
+
+
+def spline(method):
+    r"""Expose a SpeciesData field via the ``DensitySpline`` interface."""
+    name = _remove_suffix(method.__name__, "_func")
+
+    def wrapper(self, spin="t", index=None):
+        rf"""{method.__doc__}"""
+        # Validate `spin` variable
+        if spin not in ("t", "a", "b", "m"):
+            raise ValueError(
+                f"Invalid `spin` parameter '{spin}'; " "choose one of ('t'| 'a' | 'b' | 'm')"
+            )
+
+        # Set names for {a,b,tot} arrays
+        name_tot = f"{name}_tot"
+        if self.spinpol == 1:
+            name_a, name_b = f"mo_{name}_a", f"mo_{name}_b"
+        else:
+            name_a, name_b = f"mo_{name}_b", f"mo_{name}_a"
+
+        # Extract arrays
+        if index is None and spin == "t":
+            arr = getattr(self._data, name_tot)
+        elif spin == "t":
+            arr = getattr(self._data, name_a) + getattr(self._data, name_b)
+            # FIXME: This is a hack to change the array to the correct 2D shape
+            arr = arr.reshape(self.ao.nbasis, -1)
+        elif spin == "a":
+            arr = getattr(self._data, name_a)
+            arr = arr.reshape(self.ao.nbasis, -1)
+        elif spin == "b":
+            arr = getattr(self._data, name_b)
+            arr = arr.reshape(self.ao.nbasis, -1)
+        elif spin == "m":
+            arr = getattr(self._data, name_a) - getattr(self._data, name_b)
+            arr = arr.reshape(self.ao.nbasis, -1)
+        # Select specific orbitals
+        arr = arr[index]
+        # Colapse the orbital dimension to get total density values
+        if arr.ndim > 1:
+            arr = arr.sum(axis=0)  # (N,)
+
+        # Return cubic spline
+        return DensitySpline(self._data.rs, arr)
+
+    return wrapper
+
+
+class DensitySpline:
+    r"""Interpolate density using a cubic spline over a 1-D grid."""
+
+    def __init__(self, x, y, log=False):
+        r"""Initialize the CubicSpline instance."""
+        self._log = log
+        self._obj = CubicSpline(
+            x,
+            np.log(y) if log else y,
+            axis=0,
+            bc_type="not-a-knot",
+            extrapolate=True,
+        )
+
+    def __call__(self, x, deriv=0):
+        r"""
+        Compute the interpolation at some x-values.
+
+        Parameters
+        ----------
+        x: ndarray(M,)
+            Points to be interpolated.
+        deriv: int, default=0
+            Order of spline derivative to evaluate. Must be 0, 1, or 2.
+
+        Returns
+        -------
+        ndarray(M,)
+            Interpolated values (1-D array).
+
+        """
+        if not (0 <= deriv <= 2):
+            raise ValueError(f"Invalid derivative order {deriv}; must be 0 <= `deriv` <= 2")
+        elif self._log:
+            y = np.exp(self._obj(x))
+            if deriv == 1:
+                # d(ρ(r)) = d(log(ρ(r))) * ρ(r)
+                dlogy = self._obj(x, nu=1)
+                y = dlogy.flatten() * y
+            elif deriv == 2:
+                # d^2(ρ(r)) = d^2(log(ρ(r))) * ρ(r) + [d(ρ(r))]^2/ρ(r)
+                dlogy = self._obj(x, nu=1)
+                d2logy = self._obj(x, nu=2)
+                y = d2logy.flatten() * y + dlogy.flatten() ** 2 * y
+        else:
+            y = self._obj(x, nu=deriv)
+        return y
+
+
+class JSONEncoder(json.JSONEncoder):
+    r"""JSON encoder handling simple `numpy.ndarray` objects."""
+
+    def default(self, obj):
+        r"""Default encode function."""
+        if isinstance(obj, ndarray):
+            return obj.tolist()
+        else:
+            return JSONEncoder.default(self, obj)
+
+
+class _AtomicOrbitals(object):
+    """Atomic orbitals class."""
+
+    def __init__(self, data) -> None:
+        self.occs_a = data.mo_occs_a
+        self.occs_b = data.mo_occs_b
+        self.energy_a = data.mo_energy_a
+        self.energy_b = data.mo_energy_b
+        self.norba = len(self.energy_a) if self.energy_a is not None else None
+        self.norbb = len(self.energy_b) if self.energy_a is not None else None
+        self.nbasis = self.norba  # number of spatial basis functions
+
+
+@dataclass(eq=False, order=False)
+class SpeciesData:
+    r"""Database entry fields for atomic and ionic species."""
+
+    # Species info
+    elem: str = field(default_factory=default_required("elem", "str"))
+    atnum: int = field(default_factory=default_required("atnum", "int"))
+    nelec: int = field(default_factory=default_required("nelec", "int"))
+    nspin: int = field(default_factory=default_required("nspin", "int"))
+    nexc: int = field(default_factory=default_required("nexc", "int"))
+
+    # Scalar properties
+    atmass: float = field(default=None)
+    cov_radius: float = field(default=None)
+    vdw_radius: float = field(default=None)
+    at_radius: float = field(default=None)
+    polarizability: float = field(default=None)
+    dispersion: float = field(default=None)
+
+    # Scalar energy and CDFT-related properties
+    energy: float = field(default=None)
+    ip: float = field(default=None)
+    mu: float = field(default=None)
+    eta: float = field(default=None)
+
+    # Basis set name
+    obasis_name: str = field(default=None)
+
+    # Radial grid
+    rs: ndarray = field(default_factory=default_vector)
+
+    # Orbital energies
+    mo_energy_a: ndarray = field(default_factory=default_vector)
+    mo_energy_b: ndarray = field(default_factory=default_vector)
+
+    # Orbital occupations
+    mo_occs_a: ndarray = field(default_factory=default_vector)
+    mo_occs_b: ndarray = field(default_factory=default_vector)
+
+    # Orbital densities
+    mo_dens_a: ndarray = field(default_factory=default_matrix)
+    mo_dens_b: ndarray = field(default_factory=default_matrix)
+    dens_tot: ndarray = field(default_factory=default_matrix)
+
+    # Orbital density gradients
+    mo_d_dens_a: ndarray = field(default_factory=default_matrix)
+    mo_d_dens_b: ndarray = field(default_factory=default_matrix)
+    d_dens_tot: ndarray = field(default_factory=default_matrix)
+
+    # Orbital density Laplacian
+    mo_dd_dens_a: ndarray = field(default_factory=default_matrix)
+    mo_dd_dens_b: ndarray = field(default_factory=default_matrix)
+    dd_dens_tot: ndarray = field(default_factory=default_matrix)
+
+    # Orbital kinetic energy densities
+    mo_ked_a: ndarray = field(default_factory=default_matrix)
+    mo_ked_b: ndarray = field(default_factory=default_matrix)
+    ked_tot: ndarray = field(default_factory=default_matrix)
+
+
+class Species:
+    r"""Properties of atomic and ionic species."""
+
+    def __init__(self, dataset, fields, spinpol=1):
+        r"""Initialize a ``Species`` instance."""
+        self._dataset = dataset.lower()
+        self._data = SpeciesData(**fields)
+        self.spinpol = spinpol
+        self.ao = _AtomicOrbitals(self._data)
+
+    def get_docstring(self):
+        r"""Docstring of the species' dataset."""
+        return import_module(f"atomdb.datasets.{self._dataset}").__doc__
+
+    def to_dict(self):
+        r"""Return the dictionary representation of the Species instance."""
+        return asdict(self._data)
+
+    def to_json(self):
+        r"""Return the JSON string representation of the Species instance."""
+        return json.dumps(asdict(self._data), cls=JSONEncoder)
+
+    @property
+    def dataset(self):
+        r"""Dataset."""
+        return self._dataset
+
+    @property
+    def charge(self):
+        r"""Charge."""
+        return self._data.atnum - self._data.nelec
+
+    @property
+    def nspin(self):
+        r"""Spin number :math:`N_S = N_α - N_β`."""
+        return self._data.nspin * self._spinpol
+
+    @property
+    def mult(self):
+        r"""Multiplicity :math:`M = \left|N_S\right| + 1`."""
+        return self._data.nspin + 1
+
+    @property
+    def spinpol(self):
+        r"""Spin polarization direction (±1) of the species."""
+        return self._spinpol
+
+    @spinpol.setter
+    def spinpol(self, spinpol):
+        r"""Spin polarization direction setter."""
+        if not isinstance(spinpol, Integral):
+            raise TypeError("`spinpol` attribute must be an integral type")
+
+        spinpol = int(spinpol)
+
+        if abs(spinpol) != 1:
+            raise ValueError("`spinpol` must be +1 or -1")
+
+        self._spinpol = spinpol
+
+    @scalar
+    def atnum(self):
+        r"""Atomic mass."""
+        pass
+
+    @scalar
+    def nelec(self):
+        r"""Atomic mass."""
+        pass
+
+    @scalar
+    def atmass(self):
+        r"""Atomic mass."""
+        pass
+
+    @scalar
+    def cov_radius(self):
+        r"""Covalent radius (derived from crystallographic data)."""
+        pass
+
+    @scalar
+    def vdw_radius(self):
+        r"""Van der Waals radii."""
+        pass
+
+    @scalar
+    def at_radius(self):
+        r"""Atomic radius."""
+        pass
+
+    @scalar
+    def polarizability(self):
+        r"""Isolated atom dipole polarizability."""
+        pass
+
+    @property
+    def dispersion_c6(self):
+        r"""Isolated atom C6 dispersion coefficients."""
+        return self._data.dispersion["C6"]
+
+    @scalar
+    def nexc(self):
+        r"""Excitation number."""
+        pass
+
+    @scalar
+    def energy(self):
+        r"""Energy."""
+        pass
+
+    @scalar
+    def ip(self):
+        r"""Ionization potential."""
+        pass
+
+    @scalar
+    def mu(self):
+        r"""Chemical potential."""
+        pass
+
+    @scalar
+    def eta(self):
+        r"""Chemical hardness."""
+        pass
+
+    @spline
+    def dens_func(self):
+        r"""
+        Return a cubic spline of the electronic density.
+
+        Parameters
+        ----------
+        spin : str, default="ab"
+            Type of occupied spin orbitals.
+            Can be either "t" (for alpha + beta), "a" (for alpha),
+            "b" (for beta), or "m" (for alpha - beta).
+        index : sequence of int, optional
+            Sequence of integers representing the spin orbitals.
+            These are indexed from 0 to the number of basis functions.
+            By default, all orbitals of the given spin(s) are included.
+        log : bool, default=False
+            Whether the logarithm of the density is used for interpolation.
+
+        Returns
+        -------
+        DensitySpline
+            A DensitySpline instance for the density and its derivatives.
+            Given a set of radial points, it can evaluate densities and
+            derivatives up to order 2.
+
+        """
+        pass
+
+    @spline
+    def d_dens_func(self):
+        """
+        Return a cubic spline of the first derivative of the electronic density.
+
+        The derivarive of the density as a function of the distance to the atomic center
+        (a set of points along a 1-D grid) is modeled by a cubic spline. The property can
+        be computed for the alpha, beta, alpha + beta, and alpha - beta components of the
+        electron density.
+
+        Parameters
+        ----------
+        spin : str, optional
+            Type of occupied spin orbitals which can be either "a" (for alpha), "b" (for
+            beta), "ab" (for alpha + beta), and, "m" (for alpha - beta), by default 'ab'
+        index : sequence of int, optional
+            Sequence of integers representing the spin orbitals which are indexed
+            from 1 to the number basis functions. If ``None``, all orbitals of the given spin(s) are included
+        log : bool, optional
+            Whether the logarithm of the density property is used for interpolation
+
+        Returns
+        -------
+        Callable[[np.ndarray(N,), int] -> np.ndarray(N,)]
+            a callable function evaluating the derivative of the density given a set of radial
+            points (1-D array).
+
+        """
+        pass
+
+    @spline
+    def dd_dens_func(self):
+        r"""
+        Return a cubic spline of the electronic density Laplacian.
+
+        Parameters
+        ----------
+        spin : str, default="ab"
+            Type of occupied spin orbitals.
+            Can be either "t" (for alpha + beta), "a" (for alpha),
+            "b" (for beta), or "m" (for alpha - beta).
+        index : sequence of int, optional
+            Sequence of integers representing the spin orbitals.
+            These are indexed from 0 to the number of basis functions.
+            By default, all orbitals of the given spin(s) are included.
+        log : bool, default=False
+            Whether the logarithm of the density is used for interpolation.
+
+        Returns
+        -------
+        DensitySpline
+            A DensitySpline instance for the density and its derivatives.
+            Given a set of radial points, it can evaluate densities and
+            derivatives up to order 2.
+
+        """
+        pass
+
+    @spline
+    def ked_func(self):
+        r"""
+        Return a cubic spline of the kinetic energy density.
+
+        Parameters
+        ----------
+        spin : str, default="ab"
+            Type of occupied spin orbitals.
+            Can be either "t" (for alpha + beta), "a" (for alpha),
+            "b" (for beta), or "m" (for alpha - beta).
+        index : sequence of int, optional
+            Sequence of integers representing the spin orbitals.
+            These are indexed from 0 to the number of basis functions.
+            By default, all orbitals of the given spin(s) are included.
+        log : bool, default=False
+            Whether the logarithm of the density is used for interpolation.
+
+        Returns
+        -------
+        DensitySpline
+            A DensitySpline instance for the density and its derivatives.
+            Given a set of radial points, it can evaluate densities and
+            derivatives up to order 2.
+
+        """
+        pass
+
+
+def compile(
+    elem,
+    charge,
+    mult,
+    nexc=0,
+    dataset=DEFAULT_DATASET,
+    datapath=DEFAULT_DATAPATH,
+):
+    r"""Compile an atomic or ionic species into the AtomDB database."""
+    # Ensure directories exist
+    makedirs(path.join(datapath, dataset.lower(), "db"), exist_ok=True)
+    makedirs(path.join(datapath, dataset.lower(), "raw"), exist_ok=True)
+    # Import the compile script for the appropriate dataset
+    submodule = import_module(f"atomdb.datasets.{dataset}.run")
+    # Compile the Species instance and dump the database entry
+    species = submodule.run(elem, charge, mult, nexc, dataset, datapath)
+    dump(species, datapath=datapath)
+
+
+def dump(*species, datapath=DEFAULT_DATAPATH):
+    r"""Dump the Species instance(s) to a MessagePack file in the database."""
+    for s in species:
+        fn = datafile(
+            s._data.elem, s.charge, s.mult, nexc=s.nexc, dataset=s.dataset, datapath=datapath
+        )
+        with open(fn, "wb") as f:
+            f.write(packb(asdict(s._data), default=encode))
+
+
+def load(
+    elem,
+    charge,
+    mult,
+    nexc=0,
+    dataset=DEFAULT_DATASET,
+    datapath=DEFAULT_DATAPATH,
+):
+    r"""Load one or many atomic or ionic species from the AtomDB database."""
+    fn = datafile(
+        elem,
+        charge,
+        mult,
+        nexc=nexc,
+        dataset=dataset,
+        datapath=datapath,
+    )
+    print(fn)
+    if Ellipsis in (elem, charge, mult, nexc):
+        obj = []
+        for file in glob(fn):
+            with open(file, "rb") as f:
+                obj.append(Species(dataset, unpackb(f.read(), object_hook=decode)))
+    else:
+        with open(fn, "rb") as f:
+            obj = Species(dataset, unpackb(f.read(), object_hook=decode))
+    return obj
+
+
+def datafile(
+    elem,
+    charge,
+    mult,
+    nexc=0,
+    dataset=DEFAULT_DATASET,
+    datapath=DEFAULT_DATAPATH,
+):
+    r"""Return the name of the database file for a species."""
+    elem = "*" if elem is Ellipsis else element_symbol(elem)
+    charge = "*" if charge is Ellipsis else f"{charge:03d}"
+    mult = "*" if mult is Ellipsis else f"{mult:03d}"
+    nexc = "*" if nexc is Ellipsis else f"{nexc:03d}"
+    return path.join(datapath, dataset.lower(), "db", f"{elem}_{charge}_{mult}_{nexc}.msg")
+
+
+def raw_datafile(
+    suffix,
+    elem,
+    charge,
+    mult,
+    nexc=0,
+    dataset=DEFAULT_DATASET,
+    datapath=DEFAULT_DATAPATH,
+):
+    r"""Return the name of the database file for a species."""
+    elem = "*" if elem is Ellipsis else element_symbol(elem)
+    charge = "*" if charge is Ellipsis else f"{charge:03d}"
+    mult = "*" if mult is Ellipsis else f"{mult:03d}"
+    nexc = "*" if nexc is Ellipsis else f"{nexc:03d}"
+    return path.join(datapath, dataset.lower(), "raw", f"{elem}_{charge}_{mult}_{nexc}{suffix}")
diff --git a/atomdb/utils.py b/atomdb/utils.py
index 0ff1441e..2391addf 100644
--- a/atomdb/utils.py
+++ b/atomdb/utils.py
@@ -13,177 +13,105 @@
 # You should have received a copy of the GNU General Public License
 # along with AtomDB. If not, see <http://www.gnu.org/licenses/>.
 
-r"""Tool functions."""
+r"""Constants and utility functions."""
+
+import csv
+
+from os import path, environ
 
 import numpy as np
 
-import os
 import h5py as h5
-import csv
 
-from importlib_resources import files
 from scipy import constants
 
-# get data path
-TEST_DATAPATH = files("atomdb.data")
-TEST_DATAPATH = os.fspath(TEST_DATAPATH._paths[0])
 
+__all__ = [
+    "DEFAULT_DATASET",
+    "DEFAULT_DATAPATH",
+    "MODULE_DATAPATH",
+    "CONVERTOR_TYPES",
+    "MULTIPLICITIES",
+    "generate_mult_csv",
+]
 
-__all__ = ["multiplicities"]
 
+DEFAULT_DATASET = "slater"
+r"""Default dataset to query."""
 
-def _gs_mult_energy(atnum, nelec, datafile):
-    """Retrieve multiplicity and energy for the most stable electronic configuration of a species.
 
-    Parameters
-    ----------
-    atnum : int
-        Atomic number of the element
-    nelec : int
-        Number of electrons of the species
-    datafile : str
-        Path to the HDF5 file containing the data
+DEFAULT_DATAPATH = environ.get(
+    "ATOMDB_DATAPATH",
+    path.join(path.dirname(__file__), "datasets"),
+)
+r"""The path for AtomDB datasets."""
 
-    Returns
-    -------
-    mult : int
-        Multiplicity of the most stable electronic configuration
-    energy : float
-        Energy (in Hartree) of the most stable electronic configuration
-    """
-    # initialize multiplicity to zero and energy to a large value
-    default_mult = 0
-    default_energy = 1e6
 
-    # set keys for the atomic number and number of electrons
-    z = str(atnum).zfill(3)
-    ne = str(nelec).zfill(3)
+MODULE_DATAPATH = path.join(path.dirname(__file__), "data")
+r"""The path for AtomDB data files."""
 
-    # in database_beta_1.3.0.h5
-    with h5.File(datafile, "r") as f:
-        # for specie with atomic number z and ne electrons retrieve all multiplicities and energies
-        mults = np.array(list(f[z][ne]["Multi"][...]), dtype=int)
-        energy = f[z][ne]["Energy"][...]
 
-    # sort the multiplicities and energies in ascending of energy
-    index_sorting = sorted(list(range(len(energy))), key=lambda k: energy[k])
-    mults = list(mults[index_sorting])
-    energy = list(energy[index_sorting])
+HDF5_NIST_FILE = path.join(MODULE_DATAPATH, "database_beta_1.3.0.h5")
+r"""The HDF5 file containing the raw NIST data."""
 
-    # return multiplicity and energy of the most stable species or default values for missing data
-    mult = mults[0] if len(mults) != 0 else default_mult
-    energy = energy[0] if len(energy) != 0 else default_energy
-    # Convert energy to Hartree from cm^{-1}
-    energy *= 2 * constants.centi * constants.Rydberg
-    return mult, energy
 
+MULT_TABLE_CSV = path.join(MODULE_DATAPATH, "multiplicities_table.csv")
+r"""The CSV file containing the multiplicities of species."""
 
-def _make_mults_table(datafile, max_atnum=100):
-    """Creates Multiplicity Table for Neutral and Charged Species up to Atomic Number `max_atnum`
 
-    Values retrieved from 'database_beta_1.3.0.h5' are organized into a table, with rows
-    corresponding to atomic numbers and columns to charges. The maximum atomic number (`max_atnum`)
-    must not exceed 100, the database's limit. The charge range spans from -2 to `max_atnum`-1.
-    Missing combinations have multiplicities set to zero.
+ANGSTROM = 100 * constants.pico * constants.m_e * constants.c * constants.alpha / constants.hbar
+r"""Angstrom (:math:`\text{Å}`)."""
 
-    Parameters
-    ----------
-    datafile : str
-        Path to the HDF5 file containing the data
-    max_atnum : int, optional
-        Highest atomic number of the elements to be added to the table.
 
-    Returns
-    -------
-    mult_table : np.ndarray
-        Table of multiplicities
+AMU = constants.gram / (constants.Avogadro * constants.m_e)
+r"""Atomic mass unit (:math:`\text{a.m.u.}`)."""
 
-    Raises
-    ------
-    ValueError
-        If the maximum allowed atomic number is greater than 100.
-    """
-    # set maximum limits for charge, get possible charges and count them
-    neg_charge, pos_charge = -2, max_atnum
-    charge_range = range(neg_charge, pos_charge)
-    num_species = len(charge_range)
 
-    # create multiplicity table (number of elements x number of charged species) with zeros
-    mult_table = np.zeros((max_atnum, num_species), dtype=int)
+CMINV = 2 * constants.centi * constants.Rydberg
+r"""Inverse centimetre (:math:`\text{cm}^{-1}`)."""
 
-    # check if the maximum atomic number is within the database's limit
-    if max_atnum > 100:
-        raise ValueError("The maximum allowed atomic number is 100.")
 
-    # for each atomic number between 1 and the maximum atomic number
-    for atnum in range(1, max_atnum + 1):
-        # for each charge in the charge range
-        for charge in charge_range:
-            nelec = atnum - charge
-            # if number of electrons is non-positive, go to the next atomic number
-            if nelec <= 0:
-                break
-
-            # case 1: neutral or cationic species
-            if charge >= 0:
-                mult, _ = _gs_mult_energy(atnum, nelec, datafile)
-            # case 2: anionic species, read multiplicity from neutral isoelectronic species
-            else:
-                # check if the neutral isoelectronic species is in the database, if not, skip case
-                if nelec >= 100:
-                    continue
-                mult, _ = _gs_mult_energy(nelec, nelec, datafile)
-
-            # column 0 corresponds to charge -2, column 1 to charge -1, and so on
-            mult_table[atnum - 1, charge + 2] = mult
-    return mult_table
-
-
-def _write_mults_table_to_csv(mults_table, csv_file):
-    """Write the table of multiplicities to a csv file.
+EV = constants.eV / (2 * constants.Rydberg * constants.h * constants.c)
+r"""Electron volt (:math:`\text{eV}`)"""
 
-    Parameters
-    ----------
-    mults_table : np.ndarray
-        Table of multiplicities with dimension of number of elements and
-        number of charged species (Z x Q).
-    csv_file : str
-        Path to the csv file to save the table.
-    """
 
-    # get the maximum atomic number and maximum number of charged species
-    max_atnum, num_species = mults_table.shape
-    element_range = range(1, max_atnum + 1)
-    charge_range = range(-2, max_atnum)
+NEWLINE = "\n"
+r"""Newline character for use in f-strings."""
+
 
-    # first column is for atomic numbers, the other columns are for charges
-    mult_table_with_atnum = np.zeros((max_atnum, num_species + 1), dtype=int)
-    mult_table_with_atnum[:, 0] = list(element_range)
-    mult_table_with_atnum[:, 1:] = mults_table
+CONVERTOR_TYPES = {
+    "int": lambda s: int(s),
+    "float": lambda s: float(s),
+    "au": lambda s: float(s),  # atomic units,
+    "str": lambda s: s.strip(),
+    "angstrom": lambda s: float(s) * ANGSTROM,
+    "2angstrom": lambda s: float(s) * ANGSTROM / 2,
+    "angstrom**3": lambda s: float(s) * ANGSTROM**3,
+    "amu": lambda s: float(s) * AMU,
+}
+rf"""
+Unit conversion functions.
 
-    # Write the table to a csv file
-    with open(csv_file, "w", newline="") as file:
-        writer = csv.writer(file)
-        # write the header
-        charge_label = [f"Charge" if i == 3 else "" for i in range(num_species + 1)]
-        writer.writerow(charge_label)
+It has the following keys:
+{NEWLINE.join(" * " + key for key in CONVERTOR_TYPES.keys())}
 
-        # write column labels
-        colums_label = ["atnum"] + [f"{charge}" for charge in charge_range]
-        writer.writerow(colums_label)
+"""
 
-        # write table data
-        writer.writerows(mult_table_with_atnum)
 
+def make_mult_dict(max_atnum=100):
+    r"""
+    Create dictionary from the table of multiplicities.
 
-def _make_mults_dict(csv_file, max_atnum=100):
-    """Create dictionary from the table of multiplicities for neutral and charged atomic species.
+    The values are read from the table of multiplicities for neutral
+    and charged atomic species ``multiplicities_table.csv``.
 
-    The values are read from the table of multiplicities for neutral and charged atomic species
-    `multiplicities_table.csv`. The maximum atomic number supported is 100. The possible charges
-    for a given atom range from -2 to Z-1. The multiplicities are set to zero for cases where the
-    atomic numbers and charges were not present in the table. For the anions, the multiplicity is
-    taken from the neutral isoelectronic species.
+    The maximum atomic number supported is 100.
+
+    The possible charges for a given atom range from ``-2`` to ``Z-1``.
+
+    The multiplicities are set to zero for cases where the atomic numbers
+    and charges were not present in the table. For the anions,
+    the multiplicity is taken from the neutral isoelectronic species.
 
     Parameters
     ----------
@@ -194,61 +122,113 @@ def _make_mults_dict(csv_file, max_atnum=100):
     -------
     mults_dict : dict
         Dictionary with the multiplicities for each atomic species.
-        The keys are tuples of the form (atomic number, charge) and the values are the multiplicities.
+        The keys are tuples of the form (atomic number, charge) and
+        the values are the multiplicities.
 
     Examples
     --------
-    To get the multiplicity of the neutral Hidrogen atom (atomic number 1, charge 0) do:
+    To get the multiplicity of the neutral hydrogen atom (atomic number 1,
+    charge 0), do:
     >>> mults_dict = _make_mults_dict()
     >>> mults_dict[(1, 0)]
     2
     """
-    mults_dict = {}
-
-    with open(csv_file, "r") as file:
+    # Read CSV file
+    with open(MULT_TABLE_CSV, "r") as file:
         reader = csv.reader(file)
-        # skip the header
+        # Skip the row header
         next(reader)
-        # read column labels
-        labels = next(reader)
-        # read table data
+        # Read column header
+        col_header = next(reader)
+        # Read table data
         table = list(reader)
 
-    # check table columns format
-    if labels[0] != "atnum" and not all(labels[1:] == [str(i) for i in range(-2, max_atnum)]):
-        raise ValueError("The provided multiplicities table does not have the expected format")
-
-    # check table rows format
-    if len(table) != max_atnum:
-        raise ValueError(f"Expected {max_atnum} elements in multiplicities table, got {len(table)}")
+    # Read charges from the labels, skip the first element which is for atomic numbers
+    charges = [int(charge) for charge in col_header[1:]]
 
-    # read charges from the column labels, skip the first column which is for atomic numbers
-    charges = labels[1:]
-
-    # store multiplicities in a dictionary, keys are tuples (atnum, charge)
+    # Store multiplicities in a dictionary; keys are tuples (atnum, charge)
+    mult_dict = {}
     for row in table:
-        atnum, mults = int(row[0]), row[1:]
+        atnum, mults = int(row[0]), [int(mult) for mult in row[1:]]
         for charge, mult in zip(charges, mults):
-            mults_dict[(atnum, int(charge))] = int(mult)
-    return mults_dict
+            if mult != 0:
+                mult_dict[(atnum, charge)] = mult
 
+    return mult_dict
 
-multiplicities = _make_mults_dict(os.path.join(TEST_DATAPATH, "multiplicities_table.csv"))
 
+MULTIPLICITIES = make_mult_dict()
+r"""
+Dictionary of species' ground state multiplicities.
 
-# set constants and utility dictionary  for unit conversion
-angstrom = 100 * constants.pico * constants.m_e * constants.c * constants.alpha / constants.hbar
-amu = constants.gram / (constants.Avogadro * constants.m_e)
-cminv = 2 * constants.centi * constants.Rydberg
-ev = constants.eV / (2 * constants.Rydberg * constants.h * constants.c)
+Has items ``(atnum, charge): Tuple[int, int]``.
 
-convertor_types = {
-    "int": (lambda s: int(s)),
-    "float": (lambda s: float(s)),
-    "au": (lambda s: float(s)),  # just for clarity, atomic units
-    "str": (lambda s: s.strip()),
-    "angstrom": (lambda s: float(s) * angstrom),
-    "2angstrom": (lambda s: float(s) * angstrom / 2),
-    "angstrom**3": (lambda s: float(s) * angstrom**3),
-    "amu": (lambda s: float(s) * amu),
-}
+"""
+
+
+def generate_mult_csv(max_atnum=100):
+    r"""
+    Write a table of multiplicities to a CSV file.
+
+    Values retrieved from ``database_beta_1.3.0.h5`` are organized into a
+    table, with rows corresponding to atomic numbers and columns to charges.
+
+    The maximum atomic number (``max_atnum``) must not exceed 100,
+    which is the database's limit.
+
+    The charge range spans from ``-2`` to ``max_atnum - 1``.
+
+    Missing entries have multiplicities set to zero.
+
+    Parameters
+    ----------
+    max_atnum : int, default=100
+        Highest atomic number of the elements to be added to the table.
+
+    Raises
+    ------
+    ValueError
+        If the maximum allowed atomic number is greater than 100.
+
+    """
+    if max_atnum > 100:
+        raise ValueError("The maximum allowed atomic number is 100.")
+
+    # Set limits for charge
+    min_charge, max_charge = -2, max_atnum
+
+    # Create multiplicity table
+    table = np.zeros((max_atnum, 1 + max_charge - min_charge), dtype=int)
+    table[:, 0] = np.arange(1, max_atnum + 1)
+    with h5.File(HDF5_NIST_FILE, "r") as f:
+        # For each atnum
+        for atnum, row in enumerate(table[:, 1:]):
+            # For each charge
+            for col, charge in enumerate(range(min_charge, max_charge)):
+                nelec = atnum - charge
+                # Skip zero or negative electron number
+                if nelec <= 0:
+                    break
+                # Check if the data exists in the H5 file
+                if charge >= 0 or nelec < 100:
+                    # Read multiplicity and energy from H5 data file
+                    elem = f[f"{atnum:03d}"][f"{nelec:03d}"]
+                    mults = elem["Multi"][...].astype(int)
+                    energies = elem["Energy"][...].astype(float)
+                    # Ensure that the data is not empty
+                    if len(mults) == 0 or len(energies) == 0:
+                        row[col] = 0
+                    else:
+                        # Write multiplicity and energy of most stable species
+                        row[col] = mults[np.argmin[energies]]
+
+    # Make header lines for CSV file
+    row_header = ["", "charge"] + [""] * table[:, 2:].shape[1]
+    col_header = ["atnum"] + list(map(str, range(min_charge, max_charge)))
+
+    # Write multiplicity table to CSV file
+    with open(MULT_TABLE_CSV, "w", newline="") as f:
+        writer = csv.writer(f)
+        writer.writerow(row_header)
+        writer.writerow(col_header)
+        writer.writerows(table)
diff --git a/docs/notebooks/Getting_Started.ipynb b/docs/notebooks/Getting_Started.ipynb
index cf7bb6f4..b0898d9b 100644
--- a/docs/notebooks/Getting_Started.ipynb
+++ b/docs/notebooks/Getting_Started.ipynb
@@ -157,7 +157,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# Dumping Data to a JSON File\n",
+    "# Dumping Data to a JSON string\n",
     "atom.to_json()"
    ]
   },
diff --git a/docs/notebooks/Promolecule_NCI.ipynb b/docs/notebooks/Promolecule_NCI.ipynb
index 5370d55f..9b8a9948 100644
--- a/docs/notebooks/Promolecule_NCI.ipynb
+++ b/docs/notebooks/Promolecule_NCI.ipynb
@@ -107,7 +107,7 @@
     "from iodata import load_one\n",
     "from grid.cubic import UniformGrid\n",
     "# AtomDB's promolecule tool\n",
-    "from atomdb.promolecule import make_promolecule\n",
+    "from atomdb import make_promolecule\n",
     "\n",
     "\n",
     "# Function to evaluate the reduced density gradient\n",
diff --git a/docs/notebooks/Promolecule_Tools.ipynb b/docs/notebooks/Promolecule_Tools.ipynb
index 8085bc83..7313f5b6 100644
--- a/docs/notebooks/Promolecule_Tools.ipynb
+++ b/docs/notebooks/Promolecule_Tools.ipynb
@@ -32,7 +32,7 @@
    "outputs": [],
    "source": [
     "import numpy as np\n",
-    "from atomdb.promolecule import make_promolecule\n",
+    "from atomdb import make_promolecule\n",
     "\n",
     "\n",
     "# Atomic numbers of each center\n",
diff --git a/docs/notebooks/hello_atomdb.ipynb b/docs/notebooks/hello_atomdb.ipynb
index 8bb91d1d..fb916608 100644
--- a/docs/notebooks/hello_atomdb.ipynb
+++ b/docs/notebooks/hello_atomdb.ipynb
@@ -405,7 +405,7 @@
    "outputs": [],
    "source": [
     "# Import modules\n",
-    "from atomdb.promolecule import make_promolecule\n",
+    "from atomdb import make_promolecule\n",
     "\n",
     "# optional modules\n",
     "from iodata import load_one\n",
diff --git a/pyproject.toml b/pyproject.toml
index c8f9ee62..c032852d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,10 +44,9 @@ classifiers = [
 ]
 dependencies = [
     "numpy>=1.16",
-    "pytest >=2.6",
     "scipy>=1.4",
-    "importlib_resources>=5.7",
     "msgpack",
+    "msgpack-numpy",
     "h5py",
 ]
 dynamic = ["version"]
@@ -59,6 +58,18 @@ Source = "https://github.com/theochem/atomdb/"
 Changelog = "https://github.com/theochem/atomdb/blob/main/CHANGELOG.md"
 
 [project.optional-dependencies]
+dev = [
+    "pytest >=2.6",
+    "pyscf",
+    "qc-gbasis@git+https://github.com/theochem/gbasis.git@master",
+    "qc-grid@git+https://github.com/theochem/grid.git@master",
+    "qc-iodata@git+https://github.com/theochem/iodata.git@master",
+]
+test_extra = [
+    "pytest-md",
+    "pytest-emoji",
+    "pytest-cov",
+]
 doc = [
     "ipython",
     "numpydoc",
@@ -69,18 +80,7 @@ doc = [
     "sphinx_rtd_theme",
     "sphinx_autodoc_typehints",
     "docutils==0.16",  # Needed to show bullet points in sphinx_rtd_theme
-    "nbsphinx-link"
-]
-dev = [
-    "pyscf",
-    "qc-gbasis@git+https://github.com/theochem/gbasis.git@master",
-    "qc-grid@git+https://github.com/theochem/grid.git@master",
-    "qc-iodata@git+https://github.com/theochem/iodata.git@master"
-]
-test_extra = [
-    "pytest-md",
-    "pytest-emoji",
-    "pytest-cov",
+    "nbsphinx-link",
 ]
 
 [tool.black]


Test patch:
diff --git a/atomdb/test/data/gaussian/db/B_0_2_0.msg b/atomdb/test/data/gaussian/db/B_000_002_000.msg
similarity index 96%
rename from atomdb/test/data/gaussian/db/B_0_2_0.msg
rename to atomdb/test/data/gaussian/db/B_000_002_000.msg
index 57560808..c767e1d7 100644
Binary files a/atomdb/test/data/gaussian/db/B_0_2_0.msg and b/atomdb/test/data/gaussian/db/B_000_002_000.msg differ
diff --git a/atomdb/test/data/gaussian/db/Be_0_1_0.msg b/atomdb/test/data/gaussian/db/Be_000_001_000.msg
similarity index 92%
rename from atomdb/test/data/gaussian/db/Be_0_1_0.msg
rename to atomdb/test/data/gaussian/db/Be_000_001_000.msg
index 6842dd0d..5dfc9218 100644
Binary files a/atomdb/test/data/gaussian/db/Be_0_1_0.msg and b/atomdb/test/data/gaussian/db/Be_000_001_000.msg differ
diff --git a/atomdb/test/data/nist/db/C_-01_004_000.msg b/atomdb/test/data/nist/db/C_-01_004_000.msg
new file mode 100644
index 00000000..01fcf1d8
Binary files /dev/null and b/atomdb/test/data/nist/db/C_-01_004_000.msg differ
diff --git a/atomdb/test/data/nist/db/C_-1_4_0.msg b/atomdb/test/data/nist/db/C_-1_4_0.msg
deleted file mode 100644
index 2c06862e..00000000
Binary files a/atomdb/test/data/nist/db/C_-1_4_0.msg and /dev/null differ
diff --git a/atomdb/test/data/nist/db/C_000_003_000.msg b/atomdb/test/data/nist/db/C_000_003_000.msg
new file mode 100644
index 00000000..3b959fd9
Binary files /dev/null and b/atomdb/test/data/nist/db/C_000_003_000.msg differ
diff --git a/atomdb/test/data/nist/db/C_001_002_000.msg b/atomdb/test/data/nist/db/C_001_002_000.msg
new file mode 100644
index 00000000..73b1d283
Binary files /dev/null and b/atomdb/test/data/nist/db/C_001_002_000.msg differ
diff --git a/atomdb/test/data/nist/db/C_0_3_0.msg b/atomdb/test/data/nist/db/C_0_3_0.msg
deleted file mode 100644
index 44e60ce4..00000000
Binary files a/atomdb/test/data/nist/db/C_0_3_0.msg and /dev/null differ
diff --git a/atomdb/test/data/nist/db/C_1_2_0.msg b/atomdb/test/data/nist/db/C_1_2_0.msg
deleted file mode 100644
index 3f46570b..00000000
Binary files a/atomdb/test/data/nist/db/C_1_2_0.msg and /dev/null differ
diff --git a/atomdb/test/data/nist/db/H_000_002_000.msg b/atomdb/test/data/nist/db/H_000_002_000.msg
new file mode 100644
index 00000000..84eb8762
Binary files /dev/null and b/atomdb/test/data/nist/db/H_000_002_000.msg differ
diff --git a/atomdb/test/data/nist/db/H_0_2_0.msg b/atomdb/test/data/nist/db/H_0_2_0.msg
deleted file mode 100644
index 04427bfc..00000000
Binary files a/atomdb/test/data/nist/db/H_0_2_0.msg and /dev/null differ
diff --git a/atomdb/test/data/numeric/db/Be_000_001_000.msg b/atomdb/test/data/numeric/db/Be_000_001_000.msg
new file mode 100644
index 00000000..51b553be
Binary files /dev/null and b/atomdb/test/data/numeric/db/Be_000_001_000.msg differ
diff --git a/atomdb/test/data/numeric/db/Be_0_1_0.msg b/atomdb/test/data/numeric/db/Be_0_1_0.msg
deleted file mode 100644
index 73ac6ef2..00000000
Binary files a/atomdb/test/data/numeric/db/Be_0_1_0.msg and /dev/null differ
diff --git a/atomdb/test/data/numeric/db/Cl_000_002_000.msg b/atomdb/test/data/numeric/db/Cl_000_002_000.msg
new file mode 100644
index 00000000..086baf7b
Binary files /dev/null and b/atomdb/test/data/numeric/db/Cl_000_002_000.msg differ
diff --git a/atomdb/test/data/numeric/db/Cl_0_2_0.msg b/atomdb/test/data/numeric/db/Cl_0_2_0.msg
deleted file mode 100644
index 135ef04c..00000000
Binary files a/atomdb/test/data/numeric/db/Cl_0_2_0.msg and /dev/null differ
diff --git a/atomdb/test/data/numeric/db/H_-01_001_000.msg b/atomdb/test/data/numeric/db/H_-01_001_000.msg
new file mode 100644
index 00000000..b61afa8b
Binary files /dev/null and b/atomdb/test/data/numeric/db/H_-01_001_000.msg differ
diff --git a/atomdb/test/data/numeric/db/H_-1_1_0.msg b/atomdb/test/data/numeric/db/H_-1_1_0.msg
deleted file mode 100644
index c95e640b..00000000
Binary files a/atomdb/test/data/numeric/db/H_-1_1_0.msg and /dev/null differ
diff --git a/atomdb/test/data/numeric/db/H_000_002_000.msg b/atomdb/test/data/numeric/db/H_000_002_000.msg
new file mode 100644
index 00000000..308f63ab
Binary files /dev/null and b/atomdb/test/data/numeric/db/H_000_002_000.msg differ
diff --git a/atomdb/test/data/numeric/db/H_0_2_0.msg b/atomdb/test/data/numeric/db/H_0_2_0.msg
deleted file mode 100644
index 69535a06..00000000
Binary files a/atomdb/test/data/numeric/db/H_0_2_0.msg and /dev/null differ
diff --git a/atomdb/test/data/numeric/db/Ne_000_001_000.msg b/atomdb/test/data/numeric/db/Ne_000_001_000.msg
new file mode 100644
index 00000000..4973b63a
Binary files /dev/null and b/atomdb/test/data/numeric/db/Ne_000_001_000.msg differ
diff --git a/atomdb/test/data/numeric/db/Ne_0_1_0.msg b/atomdb/test/data/numeric/db/Ne_0_1_0.msg
deleted file mode 100644
index bc614d6b..00000000
Binary files a/atomdb/test/data/numeric/db/Ne_0_1_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/Ag_000_002_000.msg b/atomdb/test/data/slater/db/Ag_000_002_000.msg
new file mode 100644
index 00000000..2e6f9e83
Binary files /dev/null and b/atomdb/test/data/slater/db/Ag_000_002_000.msg differ
diff --git a/atomdb/test/data/slater/db/Ag_0_2_0.msg b/atomdb/test/data/slater/db/Ag_0_2_0.msg
deleted file mode 100644
index 3ac705f7..00000000
Binary files a/atomdb/test/data/slater/db/Ag_0_2_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/B_000_002_000.msg b/atomdb/test/data/slater/db/B_000_002_000.msg
new file mode 100644
index 00000000..4ac9cbce
Binary files /dev/null and b/atomdb/test/data/slater/db/B_000_002_000.msg differ
diff --git a/atomdb/test/data/slater/db/B_0_2_0.msg b/atomdb/test/data/slater/db/B_0_2_0.msg
deleted file mode 100644
index 2bcc76e5..00000000
Binary files a/atomdb/test/data/slater/db/B_0_2_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/Be_000_001_000.msg b/atomdb/test/data/slater/db/Be_000_001_000.msg
new file mode 100644
index 00000000..135c78ab
Binary files /dev/null and b/atomdb/test/data/slater/db/Be_000_001_000.msg differ
diff --git a/atomdb/test/data/slater/db/Be_0_1_0.msg b/atomdb/test/data/slater/db/Be_0_1_0.msg
deleted file mode 100644
index 34cf0457..00000000
Binary files a/atomdb/test/data/slater/db/Be_0_1_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/C_-01_004_000.msg b/atomdb/test/data/slater/db/C_-01_004_000.msg
new file mode 100644
index 00000000..842f6acc
Binary files /dev/null and b/atomdb/test/data/slater/db/C_-01_004_000.msg differ
diff --git a/atomdb/test/data/slater/db/C_-1_4_0.msg b/atomdb/test/data/slater/db/C_-1_4_0.msg
deleted file mode 100644
index 50ca15e2..00000000
Binary files a/atomdb/test/data/slater/db/C_-1_4_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/C_000_003_000.msg b/atomdb/test/data/slater/db/C_000_003_000.msg
new file mode 100644
index 00000000..ead1451d
Binary files /dev/null and b/atomdb/test/data/slater/db/C_000_003_000.msg differ
diff --git a/atomdb/test/data/slater/db/C_001_002_000.msg b/atomdb/test/data/slater/db/C_001_002_000.msg
new file mode 100644
index 00000000..7e354e73
Binary files /dev/null and b/atomdb/test/data/slater/db/C_001_002_000.msg differ
diff --git a/atomdb/test/data/slater/db/C_0_3_0.msg b/atomdb/test/data/slater/db/C_0_3_0.msg
deleted file mode 100644
index 37429e0d..00000000
Binary files a/atomdb/test/data/slater/db/C_0_3_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/C_1_2_0.msg b/atomdb/test/data/slater/db/C_1_2_0.msg
deleted file mode 100644
index 08e9d491..00000000
Binary files a/atomdb/test/data/slater/db/C_1_2_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/Cl_000_002_000.msg b/atomdb/test/data/slater/db/Cl_000_002_000.msg
new file mode 100644
index 00000000..97b74f93
Binary files /dev/null and b/atomdb/test/data/slater/db/Cl_000_002_000.msg differ
diff --git a/atomdb/test/data/slater/db/Cl_0_2_0.msg b/atomdb/test/data/slater/db/Cl_0_2_0.msg
deleted file mode 100644
index 47d6b383..00000000
Binary files a/atomdb/test/data/slater/db/Cl_0_2_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/H_-01_001_000.msg b/atomdb/test/data/slater/db/H_-01_001_000.msg
new file mode 100644
index 00000000..1f0e2e23
Binary files /dev/null and b/atomdb/test/data/slater/db/H_-01_001_000.msg differ
diff --git a/atomdb/test/data/slater/db/H_-1_1_0.msg b/atomdb/test/data/slater/db/H_-1_1_0.msg
deleted file mode 100644
index 35687039..00000000
Binary files a/atomdb/test/data/slater/db/H_-1_1_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/H_000_002_000.msg b/atomdb/test/data/slater/db/H_000_002_000.msg
new file mode 100644
index 00000000..f138559b
Binary files /dev/null and b/atomdb/test/data/slater/db/H_000_002_000.msg differ
diff --git a/atomdb/test/data/slater/db/H_0_2_0.msg b/atomdb/test/data/slater/db/H_0_2_0.msg
deleted file mode 100644
index 898cd00a..00000000
Binary files a/atomdb/test/data/slater/db/H_0_2_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/He_000_001_000.msg b/atomdb/test/data/slater/db/He_000_001_000.msg
new file mode 100644
index 00000000..c90865ac
Binary files /dev/null and b/atomdb/test/data/slater/db/He_000_001_000.msg differ
diff --git a/atomdb/test/data/slater/db/He_0_1_0.msg b/atomdb/test/data/slater/db/He_0_1_0.msg
deleted file mode 100644
index eb633649..00000000
Binary files a/atomdb/test/data/slater/db/He_0_1_0.msg and /dev/null differ
diff --git a/atomdb/test/data/slater/db/Ne_000_001_000.msg b/atomdb/test/data/slater/db/Ne_000_001_000.msg
new file mode 100644
index 00000000..26650b6f
Binary files /dev/null and b/atomdb/test/data/slater/db/Ne_000_001_000.msg differ
diff --git a/atomdb/test/data/slater/db/Ne_0_1_0.msg b/atomdb/test/data/slater/db/Ne_0_1_0.msg
deleted file mode 100644
index 15397a1c..00000000
Binary files a/atomdb/test/data/slater/db/Ne_0_1_0.msg and /dev/null differ
diff --git a/atomdb/test/test_api.py b/atomdb/test/test_api.py
index c656098c..5b5bc3b6 100644
--- a/atomdb/test/test_api.py
+++ b/atomdb/test/test_api.py
@@ -22,14 +22,17 @@
 #
 # --
 
+from importlib.resources import files
+
+import os
+
 import pytest
 
+import numpy as np
+
 from scipy.interpolate import CubicSpline
 
-import numpy as np
-from atomdb.api import load
-from importlib_resources import files
-import os
+from atomdb import load
 
 
 # get test data path
diff --git a/atomdb/test/test_gaussian.py b/atomdb/test/test_gaussian.py
index 36367eea..127176ee 100644
--- a/atomdb/test/test_gaussian.py
+++ b/atomdb/test/test_gaussian.py
@@ -22,13 +22,18 @@
 #
 # --
 
+from importlib_resources import files
+
+import os
+
 import pytest
 
-from atomdb.api import load
-from importlib_resources import files
 import numpy as np
+
 from numpy.testing import assert_almost_equal
-import os
+
+from atomdb import load
+
 
 # get test data path
 TEST_DATAPATH = files("atomdb.test.data")
@@ -44,16 +49,17 @@ def test_compiled_gaussian_hf_data():
     assert_almost_equal(sp.energy, answer, decimal=10)
 
     # load radial grid, total density, alpha and beta (radial) orbital densities, orbital energies
-    grid = sp.rs
-    dens = sp.dens_tot
-    orb_dens_a = sp.mo_dens_a
-    energy_a = sp.ao.energy_a
+    grid = sp._data.rs
+    dens = sp._data.dens_tot
+    orb_dens_a = sp._data.mo_dens_a
+    energy_a = sp._data.mo_energy_a
 
     # check array shapes
     assert energy_a.shape == (12,)
     assert grid.shape == (1000,)
     assert dens.shape == grid.shape
-    assert orb_dens_a.shape == (12, 1000)
+    ##FIXME: orbital density arrays must be to 2D arrays
+    assert orb_dens_a.reshape(len(energy_a), -1).shape == (12, 1000)
 
     # check array elements
     # all R and density values are positive
@@ -78,38 +84,39 @@ def test_compiled_gaussian_hf_data():
         ]
     )
     assert np.allclose(energy_a, energy, atol=1.0e-6)
-    assert np.allclose(sp.ao.energy_b, energy, atol=1.0e-6)
+    assert np.allclose(sp._data.mo_energy_b, energy_a, atol=1.0e-6)
 
 
 @pytest.mark.parametrize("atom, mult, nelec, nalpha", [("Be", 1, 4, 2), ("B", 2, 5, 3)])
-def test_gaussian_hf_density_be(atom, mult, nelec, nalpha):
-    # load Be atomic data and make density spline
+def test_gaussian_hf_density(atom, mult, nelec, nalpha):
+    # load the atomic data and make density spline
     sp = load(atom, 0, mult, dataset="gaussian", datapath=TEST_DATAPATH)
-    grid = sp.rs
-    orb_dens_a = sp.mo_dens_a
-    spline_dens = sp.density_func(spin="ab", log=True)
+    grid = sp._data.rs
+    ##FIXME: orbital density arrays must be 2D arrays
+    orb_dens_a = sp._data.mo_dens_a.reshape(len(sp._data.mo_energy_a), -1)
+    spline_dens = sp.dens_func(spin="t")
     dens_a = np.sum(orb_dens_a, axis=0)
 
     # check density values
     # check the total density integrates to the number of electrons
-    assert np.allclose(4 * np.pi * np.trapz(grid**2 * sp.dens_tot, grid), nelec, rtol=1e-3)
+    assert np.allclose(4 * np.pi * np.trapz(grid**2 * sp._data.dens_tot, grid), nelec, rtol=1e-3)
 
     # check the alpha density integrates to the number of alpha electrons
     assert np.allclose(4 * np.pi * np.trapz(grid**2 * dens_a, grid), nalpha, rtol=1e-3)
 
     # check interpolated densities
-    assert np.allclose(spline_dens(grid), sp.dens_tot, atol=1e-6)
+    assert np.allclose(spline_dens(grid), sp._data.dens_tot, atol=1e-6)
 
 
 @pytest.mark.xfail
 @pytest.mark.parametrize("atom, mult", [("Be", 1), ("B", 2)])
-def test_gaussian_hf_gradient_be(atom, mult):
-    # load Be atomic data, make density spline and evalaute 1st derivative of density
+def test_gaussian_hf_gradient(atom, mult):
+    # load the atomic data, make density spline and evalaute 1st derivative of density
     sp = load(atom, 0, mult, dataset="gaussian", datapath=TEST_DATAPATH)
-    grid = sp.rs
-    spline_dens = sp.density_func(spin="ab", log=True)
-    gradient = spline_dens(grid, deriv=1)
-    np_gradient = np.gradient(sp.dens_tot, grid)
+    grid = sp._data.rs
+    spline_dens = sp.d_dens_func(spin="t")
+    gradient = spline_dens(grid)
+    np_gradient = np.gradient(sp._data.dens_tot, grid)
 
     # check spline gradient against numpy gradient
     assert np.allclose(gradient, np_gradient, rtol=1e-3)
@@ -117,16 +124,16 @@ def test_gaussian_hf_gradient_be(atom, mult):
 
 @pytest.mark.xfail
 @pytest.mark.parametrize("atom, mult", [("Be", 1), ("B", 2)])
-def test_gaussian_hf_laplacian_be(atom, mult):
+def test_gaussian_hf_laplacian(atom, mult):
     # load the atomic data, make a spline of the density and evaluate the second derivative
     # of the density from it. Compare with gradient from numpy.
     sp = load(atom, 0, mult, dataset="gaussian", datapath=TEST_DATAPATH)
-    grid = sp.rs
-    spline_dens = sp.density_func(spin="ab", log=True)
-    d2dens = spline_dens(grid, deriv=2)
+    grid = sp._data.rs
+    spline_dens = sp.dd_dens_func(spin="t")
+    d2dens = spline_dens(grid)
 
     # load reference values
-    np_gradient = np.gradient(sp.dens_tot, grid)
+    np_gradient = np.gradient(sp._data.dens_tot, grid)
     np_d2dens = np.gradient(np_gradient, grid)
 
     # check interpolated laplacian values against reference
@@ -134,11 +141,11 @@ def test_gaussian_hf_laplacian_be(atom, mult):
 
 
 @pytest.mark.parametrize("atom, mult", [("Be", 1), ("B", 2)])
-def test_gaussian_hf_ked_be(atom, mult):
+def test_gaussian_hf_ked(atom, mult):
     # load the atomic data and make a spline of the kinetic energy density.
     sp = load(atom, 0, mult, dataset="gaussian", datapath=TEST_DATAPATH)
-    grid = sp.rs
-    spline_kdens = sp.ked_func(spin="ab", log=True)
+    grid = sp._data.rs
+    spline_kdens = sp.ked_func(spin="t")
 
     # check interpolated densities
-    assert np.allclose(spline_kdens(grid), sp.ked_tot, atol=1e-6)
+    assert np.allclose(spline_kdens(grid), sp._data.ked_tot, atol=1e-6)
diff --git a/atomdb/test/test_nist.py b/atomdb/test/test_nist.py
index 5586869b..85465aea 100644
--- a/atomdb/test/test_nist.py
+++ b/atomdb/test/test_nist.py
@@ -1,9 +1,11 @@
-import pytest
+from importlib.resources import files
+
 import os
-import numpy as np
-from importlib_resources import files
-from atomdb.api import load
-from atomdb.utils import angstrom, amu, cminv, ev
+
+import pytest
+
+from atomdb import load
+from atomdb.utils import ANGSTROM, AMU, CMINV, EV
 
 # get test data path
 TEST_DATAPATH = files("atomdb.test.data")
@@ -20,7 +22,7 @@
             "nelec": 1,
             "nspin": 1,
             "nexc": 0,
-            "atmass": 1.007975 * amu,
+            "atmass": 1.007975 * AMU,
             # "cov_radius": {"cordero": 0.31 * angstrom, "bragg": np.nan, "slater": 0.25 * angstrom},
             # "vdw_radius": {
             #     "bondi": 1.2 * angstrom,
@@ -31,13 +33,13 @@
             #     "uff": 2.886 * angstrom / 2,
             #     "mm3": 1.62 * angstrom,
             # },
-            "at_radius": {"wc": 0.529192875 * angstrom, "cr": 0.53 * angstrom},
-            "polarizability": {"crc": 0.666793 * angstrom**3, "chu": 4.5},
+            "at_radius": {"wc": 0.529192875 * ANGSTROM, "cr": 0.53 * ANGSTROM},
+            "polarizability": {"crc": 0.666793 * ANGSTROM**3, "chu": 4.5},
             "dispersion_c6": {"chu": 6.499026705},
-            "energy": -109678.77174307 * cminv,
-            "ip": 13.598443 * ev,
-            "mu": -7.18 * ev,
-            "eta": 12.84 * ev,
+            "energy": -109678.77174307 * CMINV,
+            "ip": 13.598443 * EV,
+            "mu": -7.18 * EV,
+            "eta": 12.84 * EV,
         },
         id="H neutral",
     ),
@@ -50,11 +52,11 @@
             "nelec": 6,
             "nspin": 2,
             "nexc": 0,
-            "atmass": 12.0106 * amu,
+            "atmass": 12.0106 * AMU,
             "cov_radius": {
-                "cordero": 0.7445337366 * angstrom,
-                "bragg": 0.77 * angstrom,
-                "slater": 0.7 * angstrom,
+                "cordero": 0.7445337366 * ANGSTROM,
+                "bragg": 0.77 * ANGSTROM,
+                "slater": 0.7 * ANGSTROM,
             },
             # "vdw_radius": {
             #     "bondi": 1.7 * angstrom,
@@ -65,13 +67,13 @@
             #     "uff": 3.851 * angstrom / 2,
             #     "mm3": 2.04 * angstrom,
             # },
-            "at_radius": {"wc": 0.62 * angstrom, "cr": 0.67 * angstrom},
-            "polarizability": {"crc": 1.76 * angstrom**3, "chu": 12.0},
+            "at_radius": {"wc": 0.62 * ANGSTROM, "cr": 0.67 * ANGSTROM},
+            "polarizability": {"crc": 1.76 * ANGSTROM**3, "chu": 12.0},
             "dispersion_c6": {"chu": 46.6},
-            "energy": -8308396.1899999995 * cminv,
-            "ip": 11.2603 * ev,
-            "mu": -6.26 * ev,
-            "eta": 10 * ev,
+            "energy": -8308396.1899999995 * CMINV,
+            "ip": 11.2603 * EV,
+            "mu": -6.26 * EV,
+            "eta": 10 * EV,
         },
         id="C neutral",
     ),
@@ -84,16 +86,16 @@
             "nelec": 5,
             "nspin": 1,
             "nexc": 0,
-            "atmass": 12.0106 * amu,
+            "atmass": 12.0106 * AMU,
             "cov_radius": None,
             "vdw_radius": None,
             "at_radius": None,
             "polarizability": None,
             "dispersion_c6": None,
-            "energy": -8217575.77 * cminv,
-            "ip": 24.3833 * ev,
-            "mu": -17.82 * ev,
-            "eta": 13.12 * ev,
+            "energy": -8217575.77 * CMINV,
+            "ip": 24.3833 * EV,
+            "mu": -17.82 * EV,
+            "eta": 13.12 * EV,
         },
         id="C cation",
     ),
@@ -106,16 +108,16 @@
             "nelec": 7,
             "nspin": 3,
             "nexc": 0,
-            "atmass": 12.0106 * amu,
+            "atmass": 12.0106 * AMU,
             "cov_radius": None,
             "vdw_radius": None,
             "at_radius": None,
             "polarizability": None,
             "dispersion_c6": None,
             "energy": None,
-            "ip": 1.262118 * ev,
-            "mu": 2.08 * ev,
-            "eta": 8.02 * ev,
+            "ip": 1.262118 * EV,
+            "mu": 2.08 * EV,
+            "eta": 8.02 * EV,
         },
         id="C anion",
     ),
diff --git a/atomdb/test/test_numeric.py b/atomdb/test/test_numeric.py
index 27428161..7bedf25d 100644
--- a/atomdb/test/test_numeric.py
+++ b/atomdb/test/test_numeric.py
@@ -22,14 +22,18 @@
 #
 # --
 
+from importlib.resources import files
+
+import os
 
 import pytest
 
-from numpy.testing import assert_equal, assert_almost_equal
-from importlib_resources import files
 import numpy as np
-from atomdb.api import load
-import os
+
+from numpy.testing import assert_almost_equal
+
+from atomdb import load
+
 
 # get test data path
 TEST_DATAPATH = files("atomdb.test.data")
diff --git a/atomdb/test/test_periodic.py b/atomdb/test/test_periodic.py
index 3f32a444..3e6b0ac3 100644
--- a/atomdb/test/test_periodic.py
+++ b/atomdb/test/test_periodic.py
@@ -1,35 +1,34 @@
 import pytest
-import numpy as np
-from atomdb.periodic import _num2sym, _sym2num, _name2num, _num2name, Element
-from atomdb.utils import angstrom, amu
 
+from atomdb import Element, element_number, element_symbol, element_name
 
-def test_num2sym():
-    assert _num2sym[1] == "H"
-    assert _num2sym[6] == "C"
-    assert _num2sym[26] == "Fe"
+from atomdb.utils import ANGSTROM, AMU
 
 
-def test_sym2num():
-    assert _sym2num["H"] == 1
-    assert _sym2num["C"] == 6
-    assert _sym2num["Fe"] == 26
+def testelement_symbol():
+    assert element_symbol(1) == "H"
+    assert element_symbol(6) == "C"
+    assert element_symbol(26) == "Fe"
+
 
+def test_sym2num():
+    assert element_number("H") == 1
+    assert element_number("C") == 6
+    assert element_number("Fe") == 26
 
-def _name2num():
-    assert _sym2num["hydrogen"] == 1
-    assert _sym2num["carbon"] == 6
-    assert _sym2num["iron"] == 26
+    assert element_number("hydrogen") == 1
+    assert element_number("carbon") == 6
+    assert element_number("iron") == 26
 
-    assert _sym2num["Hydrogen"] == 1
-    assert _sym2num["Carbon"] == 6
-    assert _sym2num["Iron"] == 26
+    assert element_number("Hydrogen") == 1
+    assert element_number("Carbon") == 6
+    assert element_number("Iron") == 26
 
 
-def _num2name():
-    assert _sym2num[1] == "Hydrogen"
-    assert _sym2num[6] == "Carbon"
-    assert _sym2num[26] == "Iron"
+def test__num2name():
+    assert element_name(1) == "Hydrogen"
+    assert element_name(6) == "Carbon"
+    assert element_name(26) == "Iron"
 
 
 def test_element_invalid_element():
@@ -59,21 +58,25 @@ def test_get_attributes():
         "atname": "Hydrogen",
         "group": 1,
         "period": 1,
-        "cov_radius": {"cordero": 0.31 * angstrom, "bragg": np.nan, "slater": 0.25 * angstrom},
+        "cov_radius": {
+            "cordero": 0.31 * ANGSTROM,
+            # "bragg": np.nan,
+            "slater": 0.25 * ANGSTROM,
+        },
         "vdw_radius": {
-            "bondi": 1.2 * angstrom,
-            "truhlar": np.nan,
-            "rt": 1.1 * angstrom,
-            "batsanov": np.nan,
-            "dreiding": 3.195 * angstrom / 2,
-            "uff": 2.886 * angstrom / 2,
-            "mm3": 1.62 * angstrom,
+            "bondi": 1.2 * ANGSTROM,
+            # "truhlar": np.nan,
+            "rt": 1.1 * ANGSTROM,
+            # "batsanov": np.nan,
+            "dreiding": 3.195 * ANGSTROM / 2,
+            "uff": 2.886 * ANGSTROM / 2,
+            "mm3": 1.62 * ANGSTROM,
         },
-        "at_radius": {"wc": 0.529192875 * angstrom, "cr": 0.53 * angstrom},
+        "at_radius": {"wc": 0.529192875 * ANGSTROM, "cr": 0.53 * ANGSTROM},
         "eneg": {"pauling": 2.2},
-        "pold": {"crc": 0.666793 * angstrom**3, "chu": 4.5},
+        "pold": {"crc": 0.666793 * ANGSTROM**3, "chu": 4.5},
         "c6": {"chu": 6.499026705},
-        "mass": {"stb": 1.007975 * amu},
+        "mass": {"stb": 1.007975 * AMU},
     }
 
     # Carbon data
@@ -84,24 +87,24 @@ def test_get_attributes():
         "group": 14,
         "period": 2,
         "cov_radius": {
-            "cordero": 0.7445337366 * angstrom,
-            "bragg": 0.77 * angstrom,
-            "slater": 0.7 * angstrom,
+            "cordero": 0.7445337366 * ANGSTROM,
+            "bragg": 0.77 * ANGSTROM,
+            "slater": 0.7 * ANGSTROM,
         },
         "vdw_radius": {
-            "bondi": 1.7 * angstrom,
-            "truhlar": np.nan,
-            "rt": 1.77 * angstrom,
-            "batsanov": 1.7 * angstrom,
-            "dreiding": 3.8983 * angstrom / 2,
-            "uff": 3.851 * angstrom / 2,
-            "mm3": 2.04 * angstrom,
+            "bondi": 1.7 * ANGSTROM,
+            # "truhlar": np.nan,
+            "rt": 1.77 * ANGSTROM,
+            "batsanov": 1.7 * ANGSTROM,
+            "dreiding": 3.8983 * ANGSTROM / 2,
+            "uff": 3.851 * ANGSTROM / 2,
+            "mm3": 2.04 * ANGSTROM,
         },
-        "at_radius": {"wc": 0.62 * angstrom, "cr": 0.67 * angstrom},
+        "at_radius": {"wc": 0.62 * ANGSTROM, "cr": 0.67 * ANGSTROM},
         "eneg": {"pauling": 2.55},
-        "pold": {"crc": 1.76 * angstrom**3, "chu": 12.0},
+        "pold": {"crc": 1.76 * ANGSTROM**3, "chu": 12.0},
         "c6": {"chu": 46.6},
-        "mass": {"stb": 12.0106 * amu},
+        "mass": {"stb": 12.0106 * AMU},
     }
 
     # for each element, check if the attributes are as expected
diff --git a/atomdb/test/test_promolecule.py b/atomdb/test/test_promolecule.py
index c29e4074..6d75c798 100644
--- a/atomdb/test/test_promolecule.py
+++ b/atomdb/test/test_promolecule.py
@@ -21,11 +21,14 @@
 #
 # --
 
+import pytest
+
 import numpy as np
+
 import numpy.testing as npt
-import pytest
 
-from atomdb.promolecule import make_promolecule
+from atomdb import make_promolecule
+
 
 TEST_CASES_MAKE_PROMOLECULE = [
     pytest.param(
diff --git a/atomdb/test/test_utils.py b/atomdb/test/test_utils.py
index 91a89aec..919b81fe 100644
--- a/atomdb/test/test_utils.py
+++ b/atomdb/test/test_utils.py
@@ -24,7 +24,9 @@
 #
 
 import pytest
-from atomdb.utils import multiplicities
+
+from atomdb.utils import MULTIPLICITIES
+
 
 # Mark all tests in this file with the 'dev' flag
 pytestmark = pytest.mark.dev
@@ -45,4 +47,4 @@ def test_mults_table():
         [30, -1, 2],  # Zn-
     ]
     for atnum, charge, mult in species:
-        assert multiplicities[(atnum, charge)] == mult
+        assert MULTIPLICITIES[(atnum, charge)] == mult
diff --git a/atomdb/test/test_wfn_slater.py b/atomdb/test/test_wfn_slater.py
index 1ceec436..646fa2c6 100644
--- a/atomdb/test/test_wfn_slater.py
+++ b/atomdb/test/test_wfn_slater.py
@@ -23,14 +23,18 @@
 # --
 #
 
+from importlib_resources import files
+
+import os
+
 import pytest
 
 import numpy as np
-from numpy.testing import assert_equal, assert_almost_equal, assert_raises
 
-from atomdb.api import load
-from importlib_resources import files
-import os
+from numpy.testing import assert_almost_equal
+
+from atomdb import load
+
 
 # get test data path
 TEST_DATAPATH = files("atomdb.test.data")
@@ -53,29 +57,27 @@ def test_slater_energy_especies(atom, mult, answer):
 
 
 @pytest.mark.parametrize(
-    "atom, charge, mult, tol",
+    "atom, charge, mult, answer, tol",
     [
-        ("He", 0, 1, 4),
-        ("Be", 0, 1, 5),
-        ("B", 0, 2, 3),
-        ("Cl", 0, 2, 3),
-        ("Ag", 0, 2, 2),
-        ("C", 1, 2, 6),
-        ("C", -1, 4, 5),
+        ("He", 0, 1, 2.861679997, 5),
+        ("Be", 0, 1, 14.573023130, 5),
+        ("B", 0, 2, 24.529060725, 5),
+        ("Cl", 0, 2, 459.482072308, 3),
+        ("Ag", 0, 2, 5197.698468984, 2),
+        ("C", 1, 2, 37.292223758, 5),
+        ("C", -1, 4, 37.708843614, 5),
     ],
 )
-def test_slater_positive_definite_kinetic_energy(atom, charge, mult, tol):
+def test_slater_positive_definite_kinetic_energy(atom, charge, mult, answer, tol):
     # load atomic and density data
     sp = load(atom, charge, mult, dataset="slater", datapath=TEST_DATAPATH)
     # get KED computed on an equally distant radial grid
-    grid = sp.rs
-    energ = sp.ked_tot
+    grid = sp._data.rs
+    energ = sp._data.ked_tot
     integral = np.trapz(energ, grid)
-    answer = -sp.energy  # KED is negative of total energy
-    # assert np.all(np.abs(integral - answer) < tol)
     assert_almost_equal(integral, answer, decimal=tol)
     # check interpolated density
-    spline = sp.ked_func(spin="ab", log=False)
+    spline = sp.ked_func(spin="t")
     assert np.allclose(spline(grid), energ, atol=1e-6)
 
 
@@ -87,9 +89,9 @@ def test_slater_atomic_density(atom, mult, num_elect):
     sp = load(atom, 0, mult, dataset="slater", datapath=TEST_DATAPATH)
 
     # get radial grid points, total density, and its spline interpolation
-    grid = sp.rs
-    dens = sp.dens_tot
-    spline = sp.density_func(spin="ab", log=True)
+    grid = sp._data.rs
+    dens = sp._data.dens_tot
+    spline = sp.dens_func(spin="t")
 
     # check shape of density and radial grid
     assert dens.shape == grid.shape
@@ -109,9 +111,9 @@ def test_slater_atomic_density_ions(atom, charge, mult, num_elect):
     sp = load(atom, charge, mult, dataset="slater", datapath=TEST_DATAPATH)
 
     # get radial grid points, total density, and its spline interpolation
-    grid = sp.rs
-    dens = sp.dens_tot
-    spline = sp.density_func(spin="ab", log=True)
+    grid = sp._data.rs
+    dens = sp._data.dens_tot
+    spline = sp.dens_func(spin="t")
 
     # check shape of density and radial grid
     assert dens.shape == grid.shape
@@ -129,9 +131,9 @@ def test_slater_atomic_density_ions(atom, charge, mult, num_elect):
 def test_slater_atomic_density_gradient(atom, charge, mult):
     # load atomic and density data and get density derivative evaluated on a radial grid
     sp = load(atom, charge, mult, dataset="slater", datapath=TEST_DATAPATH)
-    grid = sp.rs
-    spline = sp.density_func(spin="ab", log=True)
-    gradient = spline(grid, deriv=1)
+    grid = sp._data.rs
+    spline = sp.d_dens_func(spin="t")
+    gradient = spline(grid)
 
     # get reference values from Slater wfn raw files
     id = f"{str(sp.atnum).zfill(3)}_q{str(charge).zfill(3)}_m{mult:02d}"
@@ -151,26 +153,28 @@ def test_slater_h_anion_density_splines():
     charge = -1
     mult = 1
     sp = load("H", charge, mult, dataset="slater", datapath=TEST_DATAPATH)
-    grid = sp.rs
-    dens = sp.dens_tot
+    grid = sp._data.rs
+    dens = sp._data.dens_tot
     # check interpolated densities
-    spline_dens = sp.density_func(spin="ab", log=True)
+    spline_dens = sp.dens_func(spin="t")
     assert np.allclose(spline_dens(grid), dens, atol=1e-6)
     # check interpolated kinetic energy density
-    spline = sp.ked_func(spin="ab", log=False)
-    assert np.allclose(spline(grid), sp.ked_tot, atol=1e-6)
+    spline = sp.ked_func(spin="t")
+    assert np.allclose(spline(grid), sp._data.ked_tot, atol=1e-6)
 
     # load reference values for gradient
     id = f"{str(sp.atnum).zfill(3)}_q{str(charge).zfill(3)}_m{mult:02d}"
     fname = f"{id}_slater_gradient.npy"
     gradient = np.load(f"{TEST_DATAPATH}/slater/db/{fname}")
 
-    # check density gradient (spline derivative) vs reference values
-    assert np.allclose(spline_dens(grid, deriv=1), gradient, atol=1e-6)
+    # check interpolated density gradient vs reference values
+    spline = sp.d_dens_func(spin="t")
+    assert np.allclose(spline(grid), gradient, atol=1e-6)
 
     # check density spline second derivative vs derivative of reference gradient values
     # FIXME: second derivative has high error
     d2dens = np.gradient(gradient, sp.rs)
+    spline = sp.dd_dens_func(spin="t")
     assert np.allclose(spline_dens(grid, deriv=2), d2dens, atol=1e-2)
 
 
@@ -178,11 +182,8 @@ def test_slater_missing_attributes():
     # load He data
     sp = load("He", 0, 1, dataset="slater", datapath=TEST_DATAPATH)
     # check missing attributes default to None
-    assert sp.ip is None
     assert sp.mu is None
     assert sp.eta is None
-    assert sp.mo_dens_a is None
-    assert sp.mo_dens_b is None
 
 
 def test_slater_orbitals_be():
@@ -191,15 +192,15 @@ def test_slater_orbitals_be():
 
     # check mo energy and occupation arrays
     # check array shapes
-    assert len(sp.mo_energy_a) == 2
-    assert len(sp.mo_energy_a) == len(sp.mo_energy_b)
-    assert len(sp.mo_occs_a) == 2
-    assert len(sp.mo_occs_a) == len(sp.mo_occs_b)
+    assert len(sp._data.mo_energy_a) == 2
+    assert len(sp._data.mo_energy_a) == len(sp._data.mo_energy_b)
+    assert len(sp._data.mo_occs_a) == 2
+    assert len(sp._data.mo_occs_a) == len(sp._data.mo_occs_b)
     # check array elements
-    assert np.allclose(sp.mo_energy_a, np.array([-4.7326699, -0.3092695]), atol=1e-6)
-    assert np.allclose(sp.mo_energy_b, np.array([-4.7326699, -0.3092695]), atol=1e-6)
-    assert np.allclose(sp.mo_occs_a, np.array([1.0, 1.0]), atol=1e-6)
-    assert np.allclose(sp.mo_occs_b, np.array([1.0, 1.0]), atol=1e-6)
+    assert np.allclose(sp._data.mo_energy_a, np.array([-4.7326699, -0.3092695]), atol=1e-6)
+    assert np.allclose(sp._data.mo_energy_b, np.array([-4.7326699, -0.3092695]), atol=1e-6)
+    assert np.allclose(sp._data.mo_occs_a, np.array([1.0, 1.0]), atol=1e-6)
+    assert np.allclose(sp._data.mo_occs_b, np.array([1.0, 1.0]), atol=1e-6)
 
 
 def test_slater_orbitals_ne():
@@ -208,15 +209,19 @@ def test_slater_orbitals_ne():
 
     # check mo energy and occupation arrays
     # check array shapes
-    assert len(sp.mo_energy_a) == 3
-    assert len(sp.mo_energy_a) == len(sp.mo_energy_b)
-    assert len(sp.mo_occs_a) == 3
-    assert len(sp.mo_occs_a) == len(sp.mo_occs_b)
+    assert len(sp._data.mo_energy_a) == 5
+    assert len(sp._data.mo_energy_a) == len(sp._data.mo_energy_b)
+    assert len(sp._data.mo_occs_a) == 5
+    assert len(sp._data.mo_occs_a) == len(sp._data.mo_occs_b)
     # check array elements
-    assert np.allclose(sp.mo_energy_a, np.array([-32.7724425, -1.9303907, -0.8504095]), atol=1e-6)
-    assert np.allclose(sp.mo_energy_b, np.array([-32.7724425, -1.9303907, -0.8504095]), atol=1e-6)
-    assert np.allclose(sp.mo_occs_a, np.array([1.0, 1.0, 3.0]), atol=1e-6)
-    assert np.allclose(sp.mo_occs_b, np.array([1.0, 1.0, 3.0]), atol=1e-6)
+    assert np.allclose(
+        sp._data.mo_energy_a,
+        np.array([-32.7724425, -1.9303907, -0.8504095, -0.8504095, -0.8504095]),
+        atol=1e-6,
+    )
+    assert np.allclose(sp._data.mo_energy_a, sp._data.mo_energy_b, atol=1e-6)
+    assert np.allclose(sp._data.mo_occs_a, np.array([1.0, 1.0, 1.0, 1.0, 1.0]), atol=1e-6)
+    assert np.allclose(sp._data.mo_occs_a, sp._data.mo_occs_b, atol=1e-6)
 
 
 def test_slater_orbitals_h():
@@ -225,15 +230,15 @@ def test_slater_orbitals_h():
 
     # check mo energy and occupation arrays
     # check array shapes
-    assert len(sp.mo_energy_a) == 1
-    assert len(sp.mo_energy_a) == len(sp.mo_energy_b)
-    assert len(sp.mo_occs_a) == 1
-    assert len(sp.mo_occs_a) == len(sp.mo_occs_b)
+    assert len(sp._data.mo_energy_a) == 1
+    assert len(sp._data.mo_energy_a) == len(sp._data.mo_energy_b)
+    assert len(sp._data.mo_occs_a) == 1
+    assert len(sp._data.mo_occs_a) == len(sp._data.mo_occs_b)
     # check array elements
-    assert np.allclose(sp.mo_energy_a, np.array([-0.50]), atol=1e-6)
-    assert np.allclose(sp.mo_energy_b, np.array([-0.50]), atol=1e-6)
-    assert np.allclose(sp.mo_occs_a, np.array([1.0]), atol=1e-6)
-    assert np.allclose(sp.mo_occs_b, np.array([0.0]), atol=1e-6)
+    assert np.allclose(sp._data.mo_energy_a, np.array([-0.50]), atol=1e-6)
+    assert np.allclose(sp._data.mo_energy_b, np.array([-0.50]), atol=1e-6)
+    assert np.allclose(sp._data.mo_occs_a, np.array([1.0]), atol=1e-6)
+    assert np.allclose(sp._data.mo_occs_b, np.array([0.0]), atol=1e-6)
 
 
 def test_slater_orbitals_h_anion():
@@ -242,27 +247,28 @@ def test_slater_orbitals_h_anion():
 
     # check mo energy and occupation arrays
     # check array shapes
-    assert len(sp.mo_energy_a) == 1
-    assert len(sp.mo_energy_a) == len(sp.mo_energy_b)
-    assert len(sp.mo_occs_a) == 1
-    assert len(sp.mo_occs_a) == len(sp.mo_occs_b)
+    assert len(sp._data.mo_energy_a) == 1
+    assert len(sp._data.mo_energy_a) == len(sp._data.mo_energy_b)
+    assert len(sp._data.mo_occs_a) == 1
+    assert len(sp._data.mo_occs_a) == len(sp._data.mo_occs_b)
     # check array elements
-    assert np.allclose(sp.mo_energy_a, np.array([-0.0462224]), atol=1e-6)
-    assert np.allclose(sp.mo_energy_b, np.array([-0.0462224]), atol=1e-6)
-    assert np.allclose(sp.mo_occs_a, np.array([1.0]), atol=1e-6)
-    assert np.allclose(sp.mo_occs_b, np.array([1.0]), atol=1e-6)
+    assert np.allclose(sp._data.mo_energy_a, np.array([-0.0462224]), atol=1e-6)
+    assert np.allclose(sp._data.mo_energy_b, np.array([-0.0462224]), atol=1e-6)
+    assert np.allclose(sp._data.mo_occs_a, np.array([1.0]), atol=1e-6)
+    assert np.allclose(sp._data.mo_occs_b, np.array([1.0]), atol=1e-6)
 
 
+@pytest.mark.xfail(reason="Test for old version of Slater dataset code.")
 def test_slater_orbitals_ag():
     # Load the Ag file.
     sp = load("Ag", 0, 2, dataset="slater", datapath=TEST_DATAPATH)
 
     # check mo energy and occupation arrays
     # check array shapes
-    assert len(sp.mo_energy_a) == 10
-    assert len(sp.mo_energy_a) == len(sp.mo_energy_b)
-    assert len(sp.mo_occs_a) == 10
-    assert len(sp.mo_occs_a) == len(sp.mo_occs_b)
+    assert len(sp._data.mo_energy_a) == 10
+    assert len(sp._data.mo_energy_a) == len(sp._data.mo_energy_b)
+    assert len(sp._data.mo_occs_a) == 10
+    assert len(sp._data.mo_occs_a) == len(sp._data.mo_occs_b)
     # check array elements
     energy = np.array(
         [
@@ -278,202 +284,11 @@ def test_slater_orbitals_ag():
             -0.5374007,
         ]
     )
-    assert np.allclose(sp.mo_energy_a, energy, atol=1e-6)
-    assert np.allclose(sp.mo_energy_b, energy, atol=1e-6)
+    assert np.allclose(sp._data.mo_energy_a, energy, atol=1e-6)
+    assert np.allclose(sp._data.mo_energy_b, energy, atol=1e-6)
     assert np.allclose(
-        sp.mo_occs_a, np.array([1.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0, 5.0, 5.0]), atol=1e-6
+        sp._data.mo_occs_a, np.array([1.0, 1.0, 1.0, 1.0, 1.0, 3.0, 3.0, 3.0, 5.0, 5.0]), atol=1e-6
     )
     assert np.allclose(
-        sp.mo_occs_b, np.array([1.0, 1.0, 1.0, 1.0, 0.0, 3.0, 3.0, 3.0, 5.0, 5.0]), atol=1e-6
+        sp._data.mo_occs_b, np.array([1.0, 1.0, 1.0, 1.0, 0.0, 3.0, 3.0, 3.0, 5.0, 5.0]), atol=1e-6
     )
-
-
-#
-# TODO: Update tests bellow
-#
-# def test_atomic_density_heavy_cs():
-#     r"""Test integration of atomic density of carbon anion and cation."""
-#     # These files don't exist.
-#     assert_raises(ValueError, AtomicDensity, "cs", cation=True)
-#     assert_raises(ValueError, AtomicDensity, "cs", anion=True)
-
-#     cs = AtomicDensity("cs")
-#     grid = np.arange(0.0, 40.0, 0.0001)
-#     dens = cs.atomic_density(grid, mode="total")
-#     assert_almost_equal(4 * np.pi * np.trapz(dens * grid ** 2., grid), 55.0, decimal=5)
-
-
-# def test_atomic_density_heavy_rn():
-#     r"""Test integration of atomic density of carbon anion and cation."""
-#     # These files don't exist.
-#     assert_raises(ValueError, AtomicDensity, "rn", cation=True)
-#     assert_raises(ValueError, AtomicDensity, "rn", anion=True)
-
-#     rn = AtomicDensity("rn")
-#     grid = np.arange(0.0, 40.0, 0.0001)
-#     dens = rn.atomic_density(grid, mode="total")
-#     assert_almost_equal(4 * np.pi * np.trapz(dens * grid ** 2., grid), 86, decimal=5)
-
-
-# def test_kinetic_energy_heavy_element_ce():
-#     c = AtomicDensity("ce")
-#     grid = np.arange(0.0, 25.0, 0.0001)
-#     dens = c.lagrangian_kinetic_energy(grid)
-#     assert_almost_equal(np.trapz(dens, grid), c.energy[0], decimal=3)
-
-
-# def test_raises():
-#     assert_raises(TypeError, AtomicDensity, 25)
-#     assert_raises(TypeError, AtomicDensity, "be2")
-#     assert_raises(ValueError, AtomicDensity.slater_orbital, np.array([[1]]), np.array([[2]]),
-#                   np.array([[1.]]))
-#     c = AtomicDensity("c")
-#     assert_raises(ValueError, c.atomic_density, np.array([[1.]]), "not total")
-
-
-# def test_parsing_slater_density_k():
-#     # Load the K file
-#     k = load_slater_wfn("k")
-
-#     assert k["configuration"] == "K(2)L(8)3S(2)3P(6)4S(1)"
-#     assert k["energy"] == [599.164786943]
-
-#     assert k['orbitals'] == ["1S", "2S", "3S", "4S", "2P", "3P"]
-#     assert np.all(abs(k['orbitals_cusp'] - np.array([1.0003111, 0.9994803, 1.0005849, 1.0001341,
-#                                                      1.0007902, 0.9998975])[:, None]) < 1.e-6)
-#     assert np.all(abs(k['orbitals_energy'] - np.array([-133.5330493, -14.4899575, -1.7487797, -0.1474751, -11.5192795,
-#                                                        -0.9544227])[:, None]) < 1.e-6)
-#     assert k['orbitals_basis']['P'] == ['2P', '3P', '2P', '3P', '2P', '2P', '3P', '2P', '2P', '2P']
-
-#     basis_numbers = np.array([[2], [3], [2], [3], [2], [2], [3], [2], [2], [2]])
-#     assert np.all(np.abs(k['basis_numbers']['P'] - basis_numbers) < 1e-5)
-
-#     # Check coefficients of 3P orbital
-#     coeff_3P = np.array([0.0000354, 0.0011040, -0.0153622, 0.0620133, -0.1765320, -0.3537264,
-#                          -0.3401560, 1.3735350, 0.1055549, 0.0010773])
-#     assert (abs(k['orbitals_coeff']['3P'] - coeff_3P.reshape(10, 1)) < 1.e-6).all()
-
-
-# def test_parsing_slater_density_i():
-#     i = load_slater_wfn("i")
-#     assert i["configuration"] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(5)"
-#     assert i["orbitals"] == ["1S", "2S", "3S", "4S", "5S", "2P", "3P", "4P", "5P", "3D", "4D"]
-#     occupation = np.array([[2], [2], [2], [2], [2], [6], [6], [6], [5], [10], [10]])
-#     assert (i["orbitals_occupation"] == occupation).all()
-
-#     exponents = np.array([[58.400845, 45.117174, 24.132009, 20.588554, 12.624386, 10.217388,
-#                              8.680013, 4.627159, 3.093797, 1.795536, 0.897975]])
-#     assert np.all(np.abs(i["orbitals_exp"]["D"] - exponents.reshape((11, 1))) < 1e-4)
-
-
-# def test_parsing_slater_density_xe():
-#     # Load the Xe file
-#     xe = load_slater_wfn("xe")
-#     assert xe['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)"
-#     assert xe['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "2P", "3P", "4P", "5P", "3D", "4D"]
-#     occupation = np.array([[2], [2], [2], [2], [2], [6], [6], [6], [6], [10], [10]])
-#     assert (xe["orbitals_occupation"] == occupation).all()
-#     assert xe['energy'] == [7232.138367196]
-
-#     # Check coeffs of D orbitals.
-#     coeffs = np.array([-0.0006386, -0.0030974, 0.0445101, -0.1106186, -0.0924762, -0.4855794,
-#                      0.1699923, 0.7240230, 0.3718553, 0.0251152, 0.0001040])
-#     assert (abs(xe['orbitals_coeff']["4D"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
-
-
-# def test_parsing_slater_density_xe_cation():
-#     # Load the Xe file
-#     np.testing.assert_raises(ValueError, load_slater_wfn, "xe", anion=True)
-#     xe = load_slater_wfn("xe", cation=True)
-#     assert xe['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(5)"
-#     assert xe['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "2P", "3P", "4P", "5P", "3D", "4D"]
-#     occupation = np.array([[2], [2], [2], [2], [2], [6], [6], [6], [5], [10], [10]])
-#     assert (xe["orbitals_occupation"] == occupation).all()
-#     assert xe['energy'] == [7231.708943551]
-
-#     # Check coefficients of D orbitals.
-#     coeff = np.array([-0.0004316, -0.0016577, -0.0041398, -0.2183952, 0.0051908, -0.2953384,
-#                      -0.0095762, 0.6460145, 0.4573096, 0.0431928, -0.0000161])
-#     assert (abs(xe['orbitals_coeff']["4D"] - coeff.reshape((len(coeff), 1))) < 1e-10).all()
-
-
-# def test_parsing_slater_heavy_atom_cs():
-#     np.testing.assert_raises(ValueError, load_slater_wfn, "cs", cation=True)
-#     np.testing.assert_raises(ValueError, load_slater_wfn, "cs", anion=True)
-#     # Load the Xe file
-#     cs = load_slater_wfn("cs")
-#     assert cs['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)6S(1)"
-#     assert cs['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "6S", "2P", "3P", "4P", "5P", "3D",
-#                               "4D"]
-#     occupation = np.array([[2], [2], [2], [2], [2], [1], [6], [6], [6], [6], [10], [10]])
-#     assert (cs["orbitals_occupation"] == occupation).all()
-#     assert cs['energy'] == [7553.933539793]
-
-#     # Check coeffs of D orbitals.
-#     coeffs = np.array([-0.0025615, -0.1930536, -0.2057559, -0.1179374, 0.4341816, 0.6417053,
-#                        0.1309576])
-#     assert (abs(cs['orbitals_coeff']["4D"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
-
-#     # Check Exponents of D orbitals.
-#     exps = np.array([32.852137, 18.354403, 14.523221, 10.312620, 7.919345, 5.157647,
-#                      3.330606])
-#     assert (abs(cs['orbitals_exp']["D"] - exps.reshape((len(exps), 1))) < 1e-10).all()
-
-
-# def test_parsing_slater_heavy_atom_rn():
-#     # Load the Xe file
-#     rn = load_slater_wfn("rn")
-#     assert rn['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)4F(14)6S(2)5D(10)6P(6)"
-#     print(rn["orbitals"])
-#     assert rn['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "6S", "2P", "3P", "4P", "5P", "6P",
-#                               "3D", "4D", "5D", "4F"]
-#     occupation = np.array([[2], [2], [2], [2], [2], [2], [6], [6], [6], [6], [6], [10], [10],
-#                            [10], [14]])
-#     assert (rn["orbitals_occupation"] == occupation).all()
-#     assert rn['energy'] == [21866.772036482]
-
-#     # Check coeffs of 4F orbitals.
-#     coeffs = np.array([.0196357, .2541992, .4806186, -.2542278, .5847619, .0099519])
-#     assert (abs(rn['orbitals_coeff']["4F"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
-
-
-# def test_parsing_slater_heavy_atom_lr():
-#     # Load the lr file
-#     lr = load_slater_wfn("lr")
-#     assert lr['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)4F(14)6S(2)5D(10)6P(6)" \
-#                                   "7S(2)6D(1)5F(14)"
-#     assert lr['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "6S", "7S",
-#                               "2P", "3P", "4P", "5P", "6P",
-#                               "3D", "4D", "5D", "6D", "4F", "5F"]
-#     occupation = np.array([[2], [2], [2], [2], [2], [2], [2], [6], [6], [6], [6], [6], [10], [10],
-#                            [10], [1], [14], [14]])
-#     assert (lr["orbitals_occupation"] == occupation).all()
-#     assert lr['energy'] == [33557.949960623]
-
-#     # Check coeffs of 3D orbitals.
-#     coeffs = np.array([0.0017317, 0.4240510, 0.4821228, 0.1753365, -0.0207393, 0.0091584,
-#                        -0.0020913, 0.0005398, -0.0001604, 0.0000443, -0.0000124])
-#     assert (abs(lr['orbitals_coeff']["3D"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
-
-#     # Check coeffs of 2S orbitals.
-#     coeffs = np.array([0.0386739, -0.4000069, -0.2570804, 1.2022357, 0.0787866, 0.0002957,
-#                        0.0002277, 0.0002397, -0.0005650, 0.0000087, 0.0000031, -0.0000024,
-#                        0.0000008, -0.0000002, 0.0000001])
-#     assert (abs(lr['orbitals_coeff']["2S"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
-
-
-# def test_parsing_slater_heavy_atom_dy():
-#     # Load the dy file
-#     dy = load_slater_wfn("dy")
-#     assert dy['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)6S(2)5D(0)4F(10)"
-#     assert dy['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "6S",
-#                               "2P", "3P", "4P", "5P",
-#                               "3D", "4D", "4F"]
-#     occupation = np.array([[2], [2], [2], [2], [2], [2], [6], [6], [6], [6], [10], [10], [10]])
-#     assert (dy["orbitals_occupation"] == occupation).all()
-#     assert dy['energy'] == [11641.452478555]
-
-#     # Check coeffs of 4D orbitals.
-#     coeffs = np.array([-0.0016462, -0.2087639, -0.2407385, -0.1008913, 0.4844709, 0.6180159,
-#                        0.1070867])
-#     assert (abs(dy['orbitals_coeff']["4D"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
diff --git a/atomdb/test/test_wfn_slater_dev.py b/atomdb/test/test_wfn_slater_dev.py
new file mode 100644
index 00000000..05e37902
--- /dev/null
+++ b/atomdb/test/test_wfn_slater_dev.py
@@ -0,0 +1,559 @@
+# -*- coding: utf-8 -*-
+# AtomDB is an extended periodic table database containing experimental
+# and/or computational information on stable ground state
+# and/or excited states of neutral and charged atomic species.
+#
+# Copyright (C) 2014-2015 The AtomDB Development Team
+#
+# This file is part of AtomDB.
+#
+# AtomDB is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 3
+# of the License, or (at your option) any later version.
+#
+# AtomDB is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, see <http://www.gnu.org/licenses/>
+#
+# --
+#
+
+
+import numpy as np
+import pytest
+
+# avoid import error on 'not dev' tests, these statements are run even when the tests are skipped
+from importlib_resources import files
+from numpy.testing import assert_allclose, assert_array_equal, assert_almost_equal, assert_equal
+import os
+
+try:
+    from atomdb.datasets.slater.run import AtomicDensity
+    from atomdb.datasets.slater.run import get_cs_occupations
+    from atomdb.datasets.slater.run import load_slater_wfn
+
+    # get test data path
+    TEST_DATAPATH = files("atomdb.test.data")
+    TEST_DATAPATH = os.path.abspath(TEST_DATAPATH._paths[0])
+
+    # raw data path
+    RAW_DATAPATH = files("atomdb.datasets.slater.raw")
+    RAW_DATAPATH = os.path.abspath(RAW_DATAPATH._paths[0])
+except ImportError:
+    pass
+
+# mark all tests in this file as development tests
+pytestmark = pytest.mark.dev
+
+
+def test_get_cs_occupations():
+    configurations = ["1S(2)2S(2)2P(1)", "K(2)L(8)3S(2)3P(3)"]
+    results = [
+        [{"1S": 1, "2S": 1, "2P": 1}, {"1S": 1, "2S": 1, "2P": 0}],
+        [
+            {"1S": 1, "2S": 1, "2P": 3, "3S": 1, "3P": 3},
+            {"1S": 1, "2S": 1, "2P": 3, "3S": 1, "3P": 0},
+        ],
+    ]
+    for configuration, result in zip(configurations, results):
+        alpha_occ, beta_occ, _ = get_cs_occupations(configuration)
+        for key, a_occ in alpha_occ.items():
+            assert a_occ == result[0][key]
+            assert beta_occ[key] == result[1][key]
+
+
+@pytest.mark.parametrize("atom", ["al", "c", "cu"])
+def test_load_slater_wfn_orbitals_occupation_simple(atom):
+
+    cation = load_slater_wfn(atom, cation=True)
+    anion = load_slater_wfn(atom, anion=True)
+    neutral = load_slater_wfn(atom)
+
+    cation_occ = cation["orbitals_occupation"]
+    anion_occ = anion["orbitals_occupation"]
+    neutral_occ = neutral["orbitals_occupation"]
+
+    # check relation in the number of electrons between cation, anion and neutral
+    assert np.sum(cation_occ) + 1 == np.sum(neutral_occ)
+    assert np.sum(anion_occ) - 1 == np.sum(neutral_occ)
+
+
+@pytest.mark.parametrize("atom", ["al", "c", "cu"])
+def test_load_slater_wfn_orbitals_occupation_complex(atom):
+
+    cation = load_slater_wfn(atom, cation=True)
+    anion = load_slater_wfn(atom, anion=True)
+    neutral = load_slater_wfn(atom)
+
+    cation_occ = cation["orbitals_occupation"]
+    anion_occ = anion["orbitals_occupation"]
+    neutral_occ = neutral["orbitals_occupation"]
+
+    # check that unpaired electrons go to alpha orbitals
+    for occ in [cation_occ, neutral_occ, anion_occ]:
+        # split alpha and beta orbitals
+        a_occ = occ[: len(occ) // 2]
+        b_occ = occ[len(occ) // 2 :]
+        n_a_occ = np.sum(a_occ)
+        n_b_occ = np.sum(b_occ)
+
+        min_occ = np.min([n_a_occ, n_b_occ])
+        # more alpha electrons than beta electrons
+        assert np.sum(n_a_occ) >= np.sum(n_b_occ)
+        # occupied beta orbitals have occupied alpha counterparts
+        assert_array_equal(a_occ[:min_occ].ravel(), b_occ[:min_occ].ravel())
+
+
+@pytest.mark.parametrize("atom", ["al", "c", "cu"])
+def test_load_slater_wfn_orbitals(atom):
+
+    cation = load_slater_wfn(atom, cation=True)
+    anion = load_slater_wfn(atom, anion=True)
+    neutral = load_slater_wfn(atom)
+
+    cation_orbs = cation["orbitals"]
+    anion_orbs = anion["orbitals"]
+    neutral_orbs = neutral["orbitals"]
+
+    cat_orbs_alpha = cation_orbs[: len(cation_orbs) // 2]
+    cat_orbs_beta = cation_orbs[len(cation_orbs) // 2 :]
+
+    neu_orbs_alpha = neutral_orbs[: len(neutral_orbs) // 2]
+    neu_orbs_beta = neutral_orbs[len(neutral_orbs) // 2 :]
+
+    an_orbs_alpha = anion_orbs[: len(anion_orbs) // 2]
+    an_orbs_beta = anion_orbs[len(anion_orbs) // 2 :]
+
+    # check alpha orbitals are in the same order for cation, neutral and anion
+    for c_orb, ne_orb, a_orb in zip(cat_orbs_alpha, neu_orbs_alpha, an_orbs_alpha):
+        assert c_orb == ne_orb
+        assert c_orb == a_orb
+
+    # check beta orbitals are in the same order for cation, neutral and anion
+    for c_orb, ne_orb, a_orb in zip(cat_orbs_beta, neu_orbs_beta, an_orbs_beta):
+        assert c_orb == ne_orb
+        assert c_orb == a_orb
+
+
+@pytest.mark.parametrize("atom", ["al", "c", "cu"])
+def test_load_slater_wfn_orbitals_energy(atom):
+
+    cation = load_slater_wfn(atom, cation=True)
+    anion = load_slater_wfn(atom, anion=True)
+    neutral = load_slater_wfn(atom)
+
+    cation_e = cation["orbitals_energy"]
+    anion_e = anion["orbitals_energy"]
+    neutral_e = neutral["orbitals_energy"]
+
+    cat_orbs_alpha = cation_e[: len(cation_e) // 2]
+    cat_orbs_beta = cation_e[len(cation_e) // 2 :]
+
+    neu_orbs_alpha = neutral_e[: len(neutral_e) // 2]
+    neu_orbs_beta = neutral_e[len(neutral_e) // 2 :]
+
+    an_orbs_alpha = anion_e[: len(anion_e) // 2]
+    an_orbs_beta = anion_e[len(anion_e) // 2 :]
+
+    # check that each array is sorted in ascending order
+    a_orb_sets = [cat_orbs_alpha, neu_orbs_alpha, an_orbs_alpha]
+    b_orb_sets = [cat_orbs_beta, neu_orbs_beta, an_orbs_beta]
+    # for each set check that alpha and beta orbitals are sorted in ascending order
+    for a_orbs, b_orbs in zip(a_orb_sets, b_orb_sets):
+        for orbs in [a_orbs, b_orbs]:
+            assert np.all(np.diff(orbs) >= 0)
+
+
+def test_load_slater_wfn_orbitals_coefficients():
+
+    cation = load_slater_wfn("c", cation=True)
+    anion = load_slater_wfn("c", anion=True)
+    neutral = load_slater_wfn("c")
+
+    ca_1s_ref = np.array(
+        [
+            -0.0005984,
+            0.1852569,
+            0.1243627,
+            0.7131840,
+            0.0023454,
+            0.0009096,
+            -0.0001368,
+            0.0000628,
+        ]
+    )
+
+    neu_2p_ref = np.array(
+        [
+            0.0000552,
+            0.0137471,
+            0.0162926,
+            0.2011747,
+            0.3651183,
+            0.3811596,
+            0.0951923,
+        ]
+    )
+
+    an_2s_ref = np.array(
+        [
+            -0.0001251,
+            0.0128461,
+            0.0309079,
+            -0.1919809,
+            -0.3898360,
+            0.0663604,
+            0.7370742,
+            0.4387447,
+            0.0022083,
+        ]
+    )
+
+    assert np.allclose(ca_1s_ref, cation["orbitals_coeff"]["1S"].ravel())
+    assert np.allclose(neu_2p_ref, neutral["orbitals_coeff"]["2P"].ravel())
+    assert np.allclose(an_2s_ref, anion["orbitals_coeff"]["2S"].ravel())
+
+
+def test_load_slater_wfn_orbitals_exp():
+
+    cation = load_slater_wfn("cu", cation=True)
+    anion = load_slater_wfn("cu", anion=True)
+    neutral = load_slater_wfn("cu")
+
+    cation_exp = cation["orbitals_exp"]
+    anion_exp = anion["orbitals_exp"]
+    neutral_exp = neutral["orbitals_exp"]
+
+    cation_exp_P = np.array(
+        [
+            54.142279,
+            42.542133,
+            21.883549,
+            18.160695,
+            14.647103,
+            11.953711,
+            5.504517,
+            4.493556,
+            2.786438,
+            1.272903,
+        ]
+    )
+    anion_exp_S = np.array(
+        [
+            72.014363,
+            41.772406,
+            30.543805,
+            21.425299,
+            12.757196,
+            10.917885,
+            9.613085,
+            4.283572,
+            3.026154,
+            1.863101,
+            0.444541,
+            0.342667,
+            0.205342,
+        ]
+    )
+    neu_exp_D = np.array(
+        [26.287704, 20.461389, 8.922963, 5.814383, 3.626597, 2.200138, 1.349920, 0.690642]
+    )
+
+    assert np.allclose(cation_exp["P"].ravel(), cation_exp_P)
+    assert np.allclose(anion_exp["S"].ravel(), anion_exp_S)
+    assert np.allclose(neutral_exp["D"].ravel(), neu_exp_D)
+
+
+def test_load_slater_wfn_basis():
+
+    cation = load_slater_wfn("al", cation=True)
+    anion = load_slater_wfn("al", anion=True)
+    neutral = load_slater_wfn("al")
+
+    cation_basis = cation["orbitals_basis"]
+    anion_basis = anion["orbitals_basis"]
+    neutral_basis = neutral["orbitals_basis"]
+
+    ref_cat_S_basis = ["3S", "1S", "2S", "1S", "2S", "3S", "2S", "3S", "3S", "1S"]
+    ref_neu_P_basis = ["2P", "2P", "2P", "2P", "2P", "2P", "2P", "2P", "2P", "3P"]
+    ref_an_S_basis = ["2S", "1S", "2S", "2S", "1S", "2S", "2S", "1S", "1S", "1S", "1S"]
+
+    assert all([a == b for a, b in zip(cation_basis["S"], ref_cat_S_basis)])
+    assert all([a == b for a, b in zip(neutral_basis["P"], ref_neu_P_basis)])
+    assert all([a == b for a, b in zip(anion_basis["S"], ref_an_S_basis)])
+
+
+def test_load_slater_wfn_basis_numbers():
+
+    cation = load_slater_wfn("al", cation=True)
+    anion = load_slater_wfn("al", anion=True)
+    neutral = load_slater_wfn("al")
+
+    cation_basis = cation["basis_numbers"]
+    anion_basis = anion["basis_numbers"]
+    neutral_basis = neutral["basis_numbers"]
+
+    ref_cat_S_basis = ["3S", "1S", "2S", "1S", "2S", "3S", "2S", "3S", "3S", "1S"]
+    ref_neu_P_basis = ["2P", "2P", "2P", "2P", "2P", "2P", "2P", "2P", "2P", "3P"]
+    ref_an_S_basis = ["2S", "1S", "2S", "2S", "1S", "2S", "2S", "1S", "1S", "1S", "1S"]
+
+    assert all([a == int(b[0]) for a, b in zip(cation_basis["S"].ravel(), ref_cat_S_basis)])
+    assert all([a == int(b[0]) for a, b in zip(neutral_basis["P"].ravel(), ref_neu_P_basis)])
+    assert all([a == int(b[0]) for a, b in zip(anion_basis["S"].ravel(), ref_an_S_basis)])
+
+
+def test_eval_density_heavy_cs():
+    r"""Test integration of atomic density of carbon anion and cation."""
+    # These files don't exist.
+    with pytest.raises(ValueError):
+        AtomicDensity("cs", cation=True)
+    with pytest.raises(ValueError):
+        AtomicDensity("cs", anion=True)
+
+    cs = AtomicDensity("cs")
+    grid = np.arange(0.0, 40.0, 0.0001)
+    dens = cs.eval_density(grid, mode="total")
+    assert_almost_equal(4 * np.pi * np.trapz(dens * grid**2.0, grid), 55.0, decimal=5)
+
+
+def test_eval_density_heavy_rn():
+    r"""Test integration of atomic density of carbon anion and cation."""
+
+    # These files don't exist.
+    with pytest.raises(ValueError):
+        AtomicDensity("rn", cation=True)
+    with pytest.raises(ValueError):
+        AtomicDensity("rn", anion=True)
+    rn = AtomicDensity("rn")
+    grid = np.arange(0.0, 40.0, 0.0001)
+    dens = rn.eval_density(grid, mode="total")
+    assert_almost_equal(4 * np.pi * np.trapz(dens * grid**2.0, grid), 86, decimal=5)
+
+
+# def test_kinetic_energy_heavy_element_ce():
+#     c = AtomicDensity("ce")
+#     grid = np.arange(0.0, 25.0, 0.0001)
+#     dens = c.lagrangian_kinetic_energy(grid)
+#     assert_almost_equal(np.trapz(dens, grid), c.energy[0], decimal=3)
+
+
+def test_raises():
+    with pytest.raises(TypeError):
+        AtomicDensity(25)
+    with pytest.raises(TypeError):
+        AtomicDensity("be2")
+    with pytest.raises(ValueError):
+        AtomicDensity.slater_orbital(np.array([[1]]), np.array([[2]]), np.array([[1.0]]))
+    c = AtomicDensity("c")
+    with pytest.raises(ValueError):
+        c.eval_density(np.array([[1.0]]), "not total")
+
+
+def test_parsing_slater_density_k():
+    # Load the K file
+    k = load_slater_wfn("k")
+
+    assert k["configuration"] == "K(2)L(8)3S(2)3P(6)4S(1)"
+    assert k["energy"] == [-599.164786322]
+
+    assert k["orbitals"] == [
+        "1S",
+        "2S",
+        "2P",
+        "2P",
+        "2P",
+        "3S",
+        "3P",
+        "3P",
+        "3P",
+        "4S",
+        "1S",
+        "2S",
+        "2P",
+        "2P",
+        "2P",
+        "3S",
+        "3P",
+        "3P",
+        "3P",
+        "4S",
+    ]
+    cusps = [
+        1.0003111,
+        0.9994803,
+        1.0007902,
+        1.0007902,
+        1.0007902,
+        1.0005849,
+        0.9998975,
+        0.9998975,
+        0.9998975,
+        1.0001341,
+    ] * 2
+
+    assert np.allclose(k["orbitals_cusp"].ravel(), np.array(cusps), atol=1.0e-6)
+
+    ref_orb_energy = [
+        -133.5330493,
+        -14.4899575,
+        -11.5192795,
+        -11.5192795,
+        -11.5192795,
+        -1.7487797,
+        -0.9544227,
+        -0.9544227,
+        -0.9544227,
+        -0.1474751,
+    ] * 2
+    assert np.allclose(k["orbitals_energy"].ravel(), np.array(ref_orb_energy), atol=1.0e-6)
+    assert k["orbitals_basis"]["P"] == ["2P", "3P", "2P", "3P", "2P", "2P", "3P", "2P", "2P", "2P"]
+
+    basis_numbers = np.array([[2], [3], [2], [3], [2], [2], [3], [2], [2], [2]])
+    np.allclose(k["basis_numbers"]["P"], basis_numbers)
+
+    # Check coefficients of 3P orbital
+    coeff_3P = np.array(
+        [
+            0.0000354,
+            0.0011040,
+            -0.0153622,
+            0.0620133,
+            -0.1765320,
+            -0.3537264,
+            -0.3401560,
+            1.3735350,
+            0.1055549,
+            0.0010773,
+        ]
+    )
+    assert (abs(k["orbitals_coeff"]["3P"] - coeff_3P.reshape(10, 1)) < 1.0e-6).all()
+
+
+# def test_parsing_slater_density_i():
+#     i = load_slater_wfn("i")
+#     assert i["configuration"] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(5)"
+#     assert i["orbitals"] == ["1S", "2S", "3S", "4S", "5S", "2P", "3P", "4P", "5P", "3D", "4D"]
+#     occupation = np.array([[2], [2], [2], [2], [2], [6], [6], [6], [5], [10], [10]])
+#     assert (i["orbitals_occupation"] == occupation).all()
+
+#     exponents = np.array([[58.400845, 45.117174, 24.132009, 20.588554, 12.624386, 10.217388,
+#                              8.680013, 4.627159, 3.093797, 1.795536, 0.897975]])
+#     assert np.all(np.abs(i["orbitals_exp"]["D"] - exponents.reshape((11, 1))) < 1e-4)
+
+
+# def test_parsing_slater_density_xe():
+#     # Load the Xe file
+#     xe = load_slater_wfn("xe")
+#     assert xe['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)"
+#     assert xe['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "2P", "3P", "4P", "5P", "3D", "4D"]
+#     occupation = np.array([[2], [2], [2], [2], [2], [6], [6], [6], [6], [10], [10]])
+#     assert (xe["orbitals_occupation"] == occupation).all()
+#     assert xe['energy'] == [7232.138367196]
+
+#     # Check coeffs of D orbitals.
+#     coeffs = np.array([-0.0006386, -0.0030974, 0.0445101, -0.1106186, -0.0924762, -0.4855794,
+#                      0.1699923, 0.7240230, 0.3718553, 0.0251152, 0.0001040])
+#     assert (abs(xe['orbitals_coeff']["4D"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
+
+
+# def test_parsing_slater_density_xe_cation():
+#     # Load the Xe file
+#     np.testing.assert_raises(ValueError, load_slater_wfn, "xe", anion=True)
+#     xe = load_slater_wfn("xe", cation=True)
+#     assert xe['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(5)"
+#     assert xe['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "2P", "3P", "4P", "5P", "3D", "4D"]
+#     occupation = np.array([[2], [2], [2], [2], [2], [6], [6], [6], [5], [10], [10]])
+#     assert (xe["orbitals_occupation"] == occupation).all()
+#     assert xe['energy'] == [7231.708943551]
+
+#     # Check coefficients of D orbitals.
+#     coeff = np.array([-0.0004316, -0.0016577, -0.0041398, -0.2183952, 0.0051908, -0.2953384,
+#                      -0.0095762, 0.6460145, 0.4573096, 0.0431928, -0.0000161])
+#     assert (abs(xe['orbitals_coeff']["4D"] - coeff.reshape((len(coeff), 1))) < 1e-10).all()
+
+
+# def test_parsing_slater_heavy_atom_cs():
+#     np.testing.assert_raises(ValueError, load_slater_wfn, "cs", cation=True)
+#     np.testing.assert_raises(ValueError, load_slater_wfn, "cs", anion=True)
+#     # Load the Xe file
+#     cs = load_slater_wfn("cs")
+#     assert cs['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)6S(1)"
+#     assert cs['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "6S", "2P", "3P", "4P", "5P", "3D",
+#                               "4D"]
+#     occupation = np.array([[2], [2], [2], [2], [2], [1], [6], [6], [6], [6], [10], [10]])
+#     assert (cs["orbitals_occupation"] == occupation).all()
+#     assert cs['energy'] == [7553.933539793]
+
+#     # Check coeffs of D orbitals.
+#     coeffs = np.array([-0.0025615, -0.1930536, -0.2057559, -0.1179374, 0.4341816, 0.6417053,
+#                        0.1309576])
+#     assert (abs(cs['orbitals_coeff']["4D"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
+
+#     # Check Exponents of D orbitals.
+#     exps = np.array([32.852137, 18.354403, 14.523221, 10.312620, 7.919345, 5.157647,
+#                      3.330606])
+#     assert (abs(cs['orbitals_exp']["D"] - exps.reshape((len(exps), 1))) < 1e-10).all()
+
+
+# def test_parsing_slater_heavy_atom_rn():
+#     # Load the Xe file
+#     rn = load_slater_wfn("rn")
+#     assert rn['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)4F(14)6S(2)5D(10)6P(6)"
+#     print(rn["orbitals"])
+#     assert rn['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "6S", "2P", "3P", "4P", "5P", "6P",
+#                               "3D", "4D", "5D", "4F"]
+#     occupation = np.array([[2], [2], [2], [2], [2], [2], [6], [6], [6], [6], [6], [10], [10],
+#                            [10], [14]])
+#     assert (rn["orbitals_occupation"] == occupation).all()
+#     assert rn['energy'] == [21866.772036482]
+
+#     # Check coeffs of 4F orbitals.
+#     coeffs = np.array([.0196357, .2541992, .4806186, -.2542278, .5847619, .0099519])
+#     assert (abs(rn['orbitals_coeff']["4F"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
+
+
+# def test_parsing_slater_heavy_atom_lr():
+#     # Load the lr file
+#     lr = load_slater_wfn("lr")
+#     assert lr['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)4F(14)6S(2)5D(10)6P(6)" \
+#                                   "7S(2)6D(1)5F(14)"
+#     assert lr['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "6S", "7S",
+#                               "2P", "3P", "4P", "5P", "6P",
+#                               "3D", "4D", "5D", "6D", "4F", "5F"]
+#     occupation = np.array([[2], [2], [2], [2], [2], [2], [2], [6], [6], [6], [6], [6], [10], [10],
+#                            [10], [1], [14], [14]])
+#     assert (lr["orbitals_occupation"] == occupation).all()
+#     assert lr['energy'] == [33557.949960623]
+
+#     # Check coeffs of 3D orbitals.
+#     coeffs = np.array([0.0017317, 0.4240510, 0.4821228, 0.1753365, -0.0207393, 0.0091584,
+#                        -0.0020913, 0.0005398, -0.0001604, 0.0000443, -0.0000124])
+#     assert (abs(lr['orbitals_coeff']["3D"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
+
+#     # Check coeffs of 2S orbitals.
+#     coeffs = np.array([0.0386739, -0.4000069, -0.2570804, 1.2022357, 0.0787866, 0.0002957,
+#                        0.0002277, 0.0002397, -0.0005650, 0.0000087, 0.0000031, -0.0000024,
+#                        0.0000008, -0.0000002, 0.0000001])
+#     assert (abs(lr['orbitals_coeff']["2S"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()
+
+
+# def test_parsing_slater_heavy_atom_dy():
+#     # Load the dy file
+#     dy = load_slater_wfn("dy")
+#     assert dy['configuration'] == "K(2)L(8)M(18)4S(2)4P(6)5S(2)4D(10)5P(6)6S(2)5D(0)4F(10)"
+#     assert dy['orbitals'] == ["1S", "2S", "3S", "4S", "5S", "6S",
+#                               "2P", "3P", "4P", "5P",
+#                               "3D", "4D", "4F"]
+#     occupation = np.array([[2], [2], [2], [2], [2], [2], [6], [6], [6], [6], [10], [10], [10]])
+#     assert (dy["orbitals_occupation"] == occupation).all()
+#     assert dy['energy'] == [11641.452478555]
+
+#     # Check coeffs of 4D orbitals.
+#     coeffs = np.array([-0.0016462, -0.2087639, -0.2407385, -0.1008913, 0.4844709, 0.6180159,
+#                        0.1070867])
+#     assert (abs(dy['orbitals_coeff']["4D"] - coeffs.reshape((len(coeffs), 1))) < 1e-10).all()