Repository: zincware/MDSuite (language: python)

Issue:
Inconsistent units with the RDF/ADF/SDF calculators



Discussion / hints:

Fix patch:
diff --git a/dev-requirements.txt b/dev-requirements.txt
new file mode 100644
index 00000000..991660c1
--- /dev/null
+++ b/dev-requirements.txt
@@ -0,0 +1,11 @@
+sphinxcontrib.bibtex
+pybtex
+pytest
+nbsphinx
+sphinx_rtd_theme
+numpydoc
+sphinx-copybutton
+IPython
+pandoc
+pytest
+git+git://github.com/zincware/ZincHub.git#egg=zinchub
\ No newline at end of file
diff --git a/docs/source/_developer_docs/implementing_a_post_analysis_calculator.rst b/docs/source/_developer_docs/implementing_a_post_analysis_calculator.rst
new file mode 100644
index 00000000..e69de29b
diff --git a/docs/source/_developer_docs/implementing_a_trajectory_calculator.rst b/docs/source/_developer_docs/implementing_a_trajectory_calculator.rst
new file mode 100644
index 00000000..016f0dfc
--- /dev/null
+++ b/docs/source/_developer_docs/implementing_a_trajectory_calculator.rst
@@ -0,0 +1,509 @@
+Implementing a Trajectory Calculator
+------------------------------------
+The more involved calculator is certainly the trajectory calculator.
+This is due to the interfacing with the MDSuite database as well as considering memory
+safety and data handling.
+This has all been wrapped up into the parent class but certain steps must be taken to
+make the most of the infrastructure.
+
+
+Imports
+=======
+The first step to many python programs are the import statements.
+In the case of an MDSuite calculator the important ones are as follows:
+
+.. code-block:: python
+
+   import logging
+   from mdsuite.calculators import call
+   from dataclasses import dataclass
+   from mdsuite.database import simulation_properties
+   from mdsuite.calculators import TrajectoryCalculator
+
+   log = logging.getLogger(__name__)
+
+Let's go through them one by one.
+
+* logging is the library you will want to use to log information. Typically, when you
+  want to output some information you might use a print statement. However, by using
+  for example :code:`log.info` or :code:`log.warning` you can better utilize Python
+  software frameworks.
+* call is a decorator method we have implemented which helps to take care of some
+  commonly called methods between calculators such as saving data into the SQL database.
+* dataclass will be used to define a dataclass for the properties that are saved into
+  the MDSuite project database.
+* simulation_properties is a dataclass of all the different properties that can be
+  stored in an MDSuite simulation database. If your calculator is using data that you
+  have specially made and added, you will need to include the name of the HDF5 database
+  group to this dataclass. This will all be covered in the
+  'Implementing a Transformation' and 'Implementing a File Reader' tutorials.
+* TrajectoryCalculator is the parent class for all of our trajectory calculators.
+  It contains within it all the necessary methods for memory and data management and
+  also inherits from the main Calculator parent class.
+
+Dataclass
+=========
+One of the great things about MDSuite is that it stores the outcome of all computations
+in an SQL database along with the meta-data of the calculation.
+In order for this to happen, the calculator needs to know what should be saved.
+We do this using a dataclass for each calculator, the contents of which will end up
+in the project database.
+
+.. code-block:: python
+
+   @dataclass
+   class Args:
+       data_range: int
+       correlation_time: int
+       atom_selection: np.s_
+       tau_values: np.s_
+       species: list
+
+Here we have shown the dataclass from our Einstein diffusion coefficients.
+In this calculator, we want to store the data_range over which the function was measured,
+the correlation time used, the atoms selected for the computation, the time steps used,
+and the species studied in the analysis.
+Consider now the dataclass for the radial distribution function:
+
+.. code-block:: python
+
+   @dataclass
+   class Args:
+       number_of_bins: int
+       number_of_configurations: int
+       atom_selection: np.s_
+       cutoff: float
+       start: int
+       stop: int
+       species: list
+
+In this case, any of the information related with data ranges or correlation times is
+removed in favour of knowing how many configurations and bins were used in the analysis.
+This isn't the end of the story for the SQL database, there is one more part that we
+will discuss shortly.
+
+Declaration and __init__
+========================
+We can now define the class.
+In reality, the :code:`__init__` method of MDSuite calculators is not overly useful as we rely
+mostly on the :code:`__call__` method.
+However, as best practice dictates, we must still declare our class variables in the
+:code:`__init__` and for your own benefit, should document them there.
+
+.. code-block:: python
+
+   class NewTrajectoryCalculator(TrajectoryCalculator):
+       """
+       Class for the Einstein diffusion coefficient implementation
+
+       Notes
+       -----
+
+       Attributes
+       ----------
+
+       See Also
+       --------
+
+       Examples
+       --------
+       """
+
+       def __init__(self, **kwargs):
+           """
+
+           Parameters
+           ----------
+
+           """
+
+           super().__init__(**kwargs)
+           self.scale_function = {"linear": {"scale_factor": 150}}
+           self.loaded_property = simulation_properties.unwrapped_positions
+           self.x_label = r"$ x label $"
+           self.y_label = r"$ y label $"
+           self.result_keys = ["diffusion_coefficient", "uncertainty"]
+           self.result_series_keys = ["time", "msd"]
+           self.analysis_name = "Name of Analysis"
+           self.data_array = None
+           self.tau_values = None
+           self._dtype = tf.float64
+
+           log.info("starting ... Computation")
+
+This barebones example was taken from the einstein diffusion coefficient calculator
+where we have stripped out a lot of doc-strings and removed some string in place of a
+more descriptive option.
+So now let's go through these attributes one by one and discuss briefly what they are
+doing and if they are necessary.
+In the following list, all of the highlighted attributes must be defined.
+
+* :code:`scale_function`: This is how the calculator scales with the data input and is
+  essential for the memory management.
+  A list of scale functions can be found in mdsuite.utils.scale_functions
+* :code:`loaded_property`: This is the data that will be loaded from the database.
+  It references a tuple of the form ("Positions", (None, None, 3)).
+  The first string is the name of the HDF5 database group that will be loaded and the
+  second tuple is shape of the data.
+  Note that None contained here is correct.
+  You should NOT put an actual value here.
+  The only important thing is that the number of elements is correct.
+  Given that each atom has a position for all time-steps, there are three elements.
+  If something like ionic_current is being studied, which has only one value for each
+  configuration, the parameter will look like ("Ionic_Current", (None, 3)).
+* :code:`x_label`: Name used on the x axis of the plot.
+* :code:`y_label`: Name used on the y axis of the plot.
+* :code:`result_keys`: This is what the single value data will be stored as. In the case
+  of an einstein diffusion coefficient, both the actual diffusion coefficient and the
+  msd will be plotted. In this case, we want to store the diffusion coefficient and the
+  uncertainty and so that is how we label the data.
+* :code:`results_series_keys`: These keys are the names of the series data. Following
+  the previous example, they are called time and msd.
+* :code:`analysis_name`: This is the name of the analysis so that it can be labelled
+  correctly. Could be radial distribution function or einstein diffusion coefficient.
+* :code:`_dtype`: Type required in the analysis e.g. :code:`tf.float64`.
+* data_array: For most analysis you will loop over batches or ensembles or both.
+  In this case, it is easiest at each iteration to update a class attribute than it
+  is to handle returns.
+  Therefore, I have a data array here of, in the diffusion case, my msd.
+* tau_values: When you run an msd over 500 time steps sometimes you will want to use
+  only every n steps.
+  Tau_values is the parameter you wil set if you do not want to use every time step
+  between 0 and your data range.
+
+The __call__ method
+===================
+Python has a nice call method which allows a class to be called as ClassName().
+MDSuite makes use of this to allow for things like autocomplete as well as streamlined
+execution through a parent class.
+The call method in an MDSuite calculator takes on the form of a standard :code:`__init`
+and is where user inputs are processed.
+
+.. code-block:: python
+
+   @call
+   def __call__(
+       self,
+       plot: bool = True,
+       species: list = None,
+       data_range: int = 100,
+       correlation_time: int = 1,
+       atom_selection: np.s_ = np.s_[:],
+       molecules: bool = False,
+       tau_values: Union[int, List, Any] = np.s_[:],
+       gpu: bool = False,
+   ):
+       """
+
+       Parameters
+       ----------
+
+       Returns
+       -------
+       """
+       if species is None:
+           if molecules:
+               species = list(self.experiment.molecules)
+           else:
+               species = list(self.experiment.species)
+       # set args that will affect the computation result
+       self.args = Args(
+           data_range=data_range,
+           correlation_time=correlation_time,
+           atom_selection=atom_selection,
+           tau_values=tau_values,
+           molecules=molecules,
+           species=species,
+       )
+       self.gpu = gpu
+       self.plot = plot
+       self.system_property = False
+       self.time = self._handle_tau_values()
+       self.msd_array = np.zeros(self.data_resolution)
+
+Again we have removed all of the doc-strings for clarity.
+In the call method, not only do we populate some of the declared methods from the
+:code:`__init__` but we also define some new ones and populate the database attributes
+that we defined earlier.
+In the call itself the user arguments should be passed.
+The first real distinction between system and species-wise calculators is present here.
+Because these calculators run for a specific species, you have to handle if the user
+has not added the data themselves.
+In this case, we take all species or molecules in the system as the default.
+Considering the :code:`__call__` argument from the ionic conductivity below we can
+see the difference.
+
+.. code-block:: python
+
+   @call
+   def __call__(
+       self,
+       plot=True,
+       data_range=500,
+       correlation_time=1,
+       tau_values: np.s_ = np.s_[:],
+       gpu: bool = False,
+   ):
+       """
+       Python constructor
+
+       Parameters
+       ----------
+       plot : bool
+               if true, plot the tensor_values
+       data_range :
+               Number of configurations to use in each ensemble
+       correlation_time : int
+               Correlation time to use in the analysis.
+       gpu : bool
+               If true, reduce memory usage to the maximum GPU capability.
+       """
+       # set args that will affect the computation result
+       self.args = Args(
+           data_range=data_range,
+           correlation_time=correlation_time,
+           tau_values=tau_values,
+           atom_selection=np.s_[:],
+       )
+
+       self.gpu = gpu
+       self.plot = plot
+       self.time = self._handle_tau_values()
+       self.msd_array = np.zeros(self.data_resolution)
+
+The next important part is the updating of the :code:`self.args` data class.
+Remember that whatever is added here will be recorded in the SQL database and used for
+querying later.
+Beyond this, when you need to call one of these properties during the calculator, you
+must call it through the data class by :code:`self.args.data-range`.
+The remaining attributes are those that are either used only within the class such as
+:code:`self.msd_array` and :code:`self.time` or are used in the parent class such as
+:code:`self.gpu` and :code:`self.plot`.
+These last 4 lines are quite important and so we will discuss them in more detail.
+
+* :code:`self.gpu`: At the moment the :code:`self.gpu` command only reduces the max
+  memory allowed to that of the biggest GPU.
+* :code:`self.plot`: If this is set to false, no plots of the analysis will be
+  generated.
+* :code:`self.time`: This attribute is set by calling the
+  :code:`self._handle_tau_values` method which will allow for the use of custom time
+  steps within a data range.
+  This then correlates directly to the :code:`self.msd_array` which must be instantiated
+  with the :code:`self.data_resolution` attribute.
+  This attribute is an integer describing how many points are loaded between 0 and the
+  data_range.
+
+The run_calculator Method
+=========================
+Finally we can discuss the running of the calculator including the loading of data and
+the implementation of the operation.
+
+In this section we will break down three different examples.
+The first two will be very similar whereas the third will be an alternative to
+implementing calculators.
+
+Species calculators
+^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: python
+
+   def run_calculator(self):
+       """
+       Run analysis.
+
+       Returns
+       -------
+
+       """
+       self.check_input()
+       # Loop over species
+       for species in self.args.species:
+           dict_ref = str.encode("/".join([species, self.loaded_property[0]]))
+           self.calculate_prefactor(species)
+
+           batch_ds = self.get_batch_dataset([species])
+           for batch in tqdm(
+               batch_ds,
+               ncols=70,
+               desc=species,
+               total=self.n_batches,
+               disable=self.memory_manager.minibatch,
+           ):
+               ensemble_ds = self.get_ensemble_dataset(batch, species, split=True)
+
+               for ensemble in ensemble_ds:
+                   self.ensemble_operation(ensemble[dict_ref])
+
+           # Scale, save, and plot the data.
+           self.postprocessing(species)
+
+The first line in this calculator runs a check on the user input.
+This is optional but advisable if inputs that can kill the analysis are possible.
+The second part, and the main difference between system-wise and species-wise
+calculators, is the loop over the chosen species.
+What follows can be broken into four components.
+
+1. Get a batch dataset. This is the first step in memory management. A batch is N
+   configurations that can fit into memory keeping in mind what kind of inflation the
+   operation will cause.
+2. Get an ensemble dataset for that batch. An ensemble is a subset of the N loaded
+   configuration over which you actually perform a computation. Consider the MSD on
+   a data_range of 500. It may be faster and possible to load 1000 configurations in and
+   loop over ensembles of 500 configurations sliding along in a window in steps of
+   correlation time.
+3. Perform an operation on each ensemble. This can include computing the msd or
+   performing auto-correlation.
+4. Run some post-processing on the analysis including plotting.
+
+The most important point here is what do you have to pass to the batch data set method
+and what you get out.
+The argument it takes is a list of species from which data should be loaded.
+The dataset will know what data is to be loaded based on the :code:`loaded_property`
+attribute in the class.
+What you get back is a TensorFlow Dataset.
+This is a generator object which is also capable of pre-fetching data between loops for
+maximum performance.
+When you loop over ths dataset you will get a dict object back with byte-string encoded
+keys.
+This is due to some back-end TensorFlow processes and is the reason we defined a
+dict_ref variable at the start of the loop.
+For example, this is what would happen if you wished to load position data for
+:code:`Na` and :code:`Cl`:
+
+.. code-block:: python
+
+   ds = self.get_batch_dataset(["Na", "Cl"])
+   for batch in ds:
+       print(ds)
+
+   >>> ds = {b'Na/Positions': tf.Tensor(shape=(n_atoms, n_confs, 3)),
+             b'Cl/Positions': tf.Tensor(shape=(n_atoms, n_confs, 3)),
+             b'data_size': tf.int32}
+
+Typically the data_size is set to None but will be returned with the loading.
+It is important to remember this if you plan on looping over this dict.
+
+System Calculators
+^^^^^^^^^^^^^^^^^^
+Now we will look at the run method of a system calculator.
+You will notice that the only difference ins the species loop as well as what is passed
+to the batch generator.
+
+.. code-block:: python
+
+   def run_calculator(self):
+       """
+
+       Run analysis.
+
+       Returns
+       -------
+
+       """
+       self.check_input()
+       # Compute the pre-factor early.
+       self._calculate_prefactor()
+
+       dict_ref = str.encode(
+           "/".join([self.loaded_property[0], self.loaded_property[0]])
+       )
+
+       batch_ds = self.get_batch_dataset([self.loaded_property[0]])
+
+       for batch in tqdm(
+           batch_ds,
+           ncols=70,
+           total=self.n_batches,
+           disable=self.memory_manager.minibatch,
+       ):
+           ensemble_ds = self.get_ensemble_dataset(batch, self.loaded_property[0])
+
+           for ensemble in ensemble_ds:
+               self.ensemble_operation(ensemble[dict_ref])
+
+       # Scale, save, and plot the data.
+       self._apply_averaging_factor()
+       self._post_operation_processes()
+
+So, as mentioned, there is no species loop.
+Beyond this,the loaded property is what is passed to the batch generator.
+This is because what is stored in the HDF5 database is under a group with the same name.
+For example, ionic current is stored as :code:`db["Ionic_Current"/"Ionic_Current"]`.
+
+None-ensemble calculators.
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+In cases where one wants to load several species at the same time the ensemble ds will
+likely not suffice.
+All of the MDSuite structural calculators have this problem and therefore look slightly
+different.
+Let's look at the RDF to see what is going on.
+
+.. code-block:: python
+
+   def run_calculator(self):
+       """
+       Run the analysis.
+
+       Returns
+       -------
+
+       """
+       self.check_input()
+
+       dict_keys, split_arr, batch_tqm = self.prepare_computation()
+
+       # Get the batch dataset
+       batch_ds = self.get_batch_dataset(
+           subject_list=self.args.species, loop_array=split_arr, correct=True
+       )
+
+       # Loop over the batches.
+       for idx, batch in tqdm(enumerate(batch_ds), ncols=70, disable=batch_tqm):
+
+           # Reformat the data.
+           log.debug("Reformatting data.")
+           positions_tensor = self._format_data(batch=batch, keys=dict_keys)
+
+           # Create a new dataset to loop over.
+           log.debug("Creating dataset.")
+           per_atoms_ds = tf.data.Dataset.from_tensor_slices(positions_tensor)
+           n_atoms = tf.shape(positions_tensor)[0]
+
+           # Start the computation.
+           log.debug("Beginning calculation.")
+           minibatch_start = tf.constant(0)
+           stop = tf.constant(0)
+           rdf = {
+               name: tf.zeros(self.args.number_of_bins, dtype=tf.int32)
+               for name in self.key_list
+           }
+
+           for atoms in tqdm(
+               per_atoms_ds.batch(self.minibatch).prefetch(tf.data.AUTOTUNE),
+               ncols=70,
+               disable=not batch_tqm,
+               desc=f"Running mini batch loop {idx + 1} / {self.n_batches}",
+           ):
+               # Compute the minibatch update
+               minibatch_rdf, minibatch_start, stop = self.run_minibatch_loop(
+                   atoms, stop, n_atoms, minibatch_start, positions_tensor
+               )
+
+               # Update the rdf.
+               start_time = timer()
+               rdf = self.combine_dictionaries(rdf, minibatch_rdf)
+               log.debug(f"Updating dictionaries took {timer() - start_time} s")
+
+           # Update the class before the next batch.
+           for key in self.rdf:
+               self.rdf[key] += rdf[key]
+
+       self._calculate_radial_distribution_functions()
+
+In these cases after the batch is generated some alternative mini-batching is applied
+to ensure memory safety.
+In principle however, once the batch is being looped over you can apply whatever
+operations you want.
+
+Post-processing
+---------------
diff --git a/docs/source/_developer_docs/implementing_calculators.rst b/docs/source/_developer_docs/implementing_calculators.rst
new file mode 100644
index 00000000..3615fba2
--- /dev/null
+++ b/docs/source/_developer_docs/implementing_calculators.rst
@@ -0,0 +1,28 @@
+Implementing your own Calculator
+--------------------------------
+Whilst MDSuite offers a large selection of calculators, we understand that it is
+quite likely we have missed at least one.
+For this reason, we wrote the calculator class and the actual calculators in a modular,
+transferable way such that you can easily implement your own while getting the most out
+of the memory safety and data-set capabilities that make MDSuite a high performance
+engine.
+
+In this documentation series we will walk through the main stages of a calculator
+and highlight differences you may face using real examples.
+
+In MDSuite there are two types of calculators.
+Trajectory calculators, which take data directly from the HDF5 database and compute a
+property on the trajectory and the, for lack of a better name, post-analysis
+calculators, which take data generated by a trajectory calculator and compute something
+on that data. Trajectory calculators include things like radial distribution functions
+or diffusion coefficients whereas coordination numbers or potential of mean-force are
+post-analysis calculators.
+
+Due to their differences in implementation they warrant separate guidelines.
+
+.. toctree::
+   :maxdepth: 1
+   :caption: Calculator Tutorials:
+
+   implementing_a_trajectory_calculator
+   implementing_a_post_analysis_calculator
diff --git a/docs/source/conf.py b/docs/source/conf.py
index cf832169..3b4f04e2 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -27,10 +27,11 @@
 import os
 import sys
 import sphinx_rtd_theme
-sys.path.insert(0, '..')
-#sys.path.insert(0, '../..')
 
-#for x in os.walk('../..'):
+sys.path.insert(0, "..")
+# sys.path.insert(0, '../..')
+
+# for x in os.walk('../..'):
 #  sys.path.insert(0, os.path.abspath(x[0]))
 
 html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
diff --git a/docs/source/developer.rst b/docs/source/developer.rst
index 93e1a181..0649bc08 100644
--- a/docs/source/developer.rst
+++ b/docs/source/developer.rst
@@ -6,4 +6,5 @@ some resources to help you get started and avoid making some of the same mistake
 .. toctree::
     :maxdepth: 1
 
-    _developer_docs/faq
\ No newline at end of file
+    _developer_docs/faq
+    _developer_docs/implementing_calculators
\ No newline at end of file
diff --git a/examples/scripts/green_kubo_diffusion.py b/examples/scripts/green_kubo_diffusion.py
index 3379e59c..3615139c 100644
--- a/examples/scripts/green_kubo_diffusion.py
+++ b/examples/scripts/green_kubo_diffusion.py
@@ -69,7 +69,7 @@ def run_example():
         data_range=100, plot=True, correlation_time=10
     )
     project.run.EinsteinDiffusionCoefficients(
-        data_range=100, correlation_time=10, plot=True
+        data_range=200, correlation_time=10, plot=True
     )
     project.run.EinsteinHelfandIonicConductivity(
         data_range=100, correlation_time=10, plot=True
@@ -78,9 +78,12 @@ def run_example():
     project.run.RadialDistributionFunction(
         number_of_configurations=100, start=0, stop=101, plot=True
     )
+    project.run.GreenKuboIonicConductivity(data_range=100)
+
     project.run.CoordinationNumbers(plot=True)
     project.run.PotentialOfMeanForce(plot=True)
-    project.experiments.NaCl_GK.run_visualization()
+    project.run.AngularDistributionFunction(number_of_configurations=3)
+    # project.experiments.NaCl_GK.run_visualization()
     print("Tutorial complete....... Files being deleted now.")
 
 
diff --git a/examples/scripts/spatial_distribution_function.py b/examples/scripts/spatial_distribution_function.py
index eff377c4..8b9cd61e 100644
--- a/examples/scripts/spatial_distribution_function.py
+++ b/examples/scripts/spatial_distribution_function.py
@@ -41,7 +41,7 @@ def load_data():
     -------
     Will store simulation data locally for the example.
     """
-    base_url = "https://github.com/zincware/ExampleData/raw/main/bmim_bf4.lammpstrj"
+    base_url = "https://github.com/zincware/Datahub/raw/main/bmim_bf4.lammpstrj"
     filename, headers = urllib.request.urlretrieve(
         f"{base_url}.gz", filename="bmim_bf4.lammpstrj.gz"
     )
@@ -80,13 +80,14 @@ def run_example():
     project.run.SpatialDistributionFunction(
         species=["bmim", "bf4"],
         r_min=1.0,
-        r_max=3.0,
-        number_of_configurations=5,
+        r_max=9.0,
+        number_of_configurations=50,
+        n_bins=1000,
         start=0,
-        stop=40,
+        stop=80,
     )
 
-    project.experiments.bmim_bf4.run_visualization(molecules=True)
+    # project.experiments.bmim_bf4.run_visualization(molecules=True)
 
     print("Tutorial complete....... Files being deleted now.")
 
diff --git a/mdsuite/__init__.py b/mdsuite/__init__.py
index 02a3a31c..f15a5add 100644
--- a/mdsuite/__init__.py
+++ b/mdsuite/__init__.py
@@ -34,7 +34,7 @@
 
 __all__ = ["Project", "Experiment", "adjacency_matrix", "Report", "config"]
 
-logger = logging.getLogger("mdsuite")
+logger = logging.getLogger(__name__)
 logger.setLevel(logging.INFO)
 
 # Formatter for advanced logging
diff --git a/mdsuite/calculators/__init__.py b/mdsuite/calculators/__init__.py
index cebab0ac..fe31c215 100644
--- a/mdsuite/calculators/__init__.py
+++ b/mdsuite/calculators/__init__.py
@@ -27,6 +27,7 @@
 from __future__ import annotations
 
 from .calculator import Calculator
+from .trajectory_calculator import TrajectoryCalculator
 from .angular_distribution_function import AngularDistributionFunction
 from .coordination_number_calculation import CoordinationNumbers
 from .einstein_diffusion_coefficients import EinsteinDiffusionCoefficients
@@ -58,6 +59,7 @@
 
 __all__ = [
     "Calculator",
+    "TrajectoryCalculator",
     "AngularDistributionFunction",
     "CoordinationNumbers",
     "EinsteinDiffusionCoefficients",
@@ -87,7 +89,6 @@ def __init__(
         self,
         experiment: Experiment = None,
         experiments: List[Experiment] = None,
-        load_data: bool = False,
     ):
         """Collection of all calculators
 
@@ -107,7 +108,6 @@ def __init__(
         self.kwargs = {
             "experiment": experiment,
             "experiments": experiments,
-            "load_data": load_data,
         }
 
     @property
diff --git a/mdsuite/calculators/angular_distribution_function.py b/mdsuite/calculators/angular_distribution_function.py
index de4beb9d..0bfe96ba 100644
--- a/mdsuite/calculators/angular_distribution_function.py
+++ b/mdsuite/calculators/angular_distribution_function.py
@@ -23,6 +23,9 @@
 
 Summary
 -------
+Module to perform the calculation of the angular distribution function (ADF). The ADF
+describes the average distribution of angles between three particles of species a, b,
+and c. Note that a, b, and c may all be the same species, e.g. Na-Na-Na.
 """
 from abc import ABC
 import logging
@@ -30,7 +33,8 @@
 import itertools
 import numpy as np
 from tqdm import tqdm
-from mdsuite.calculators.calculator import Calculator, call
+from typing import Union
+from mdsuite.calculators.calculator import call
 from mdsuite.utils.neighbour_list import (
     get_neighbour_list,
     get_triu_indicies,
@@ -38,12 +42,34 @@
 )
 from mdsuite.utils.linalg import get_angles
 from mdsuite.utils.meta_functions import join_path
-from mdsuite.database.scheme import Computation
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+from mdsuite.calculators import TrajectoryCalculator
+
 
 log = logging.getLogger(__name__)
 
 
-class AngularDistributionFunction(Calculator, ABC):
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
+
+    number_of_bins: int
+    number_of_configurations: int
+    correlation_time: int
+    atom_selection: np.s_
+    data_range: int
+    cutoff: float
+    start: int
+    norm_power: Union[int, float]
+    stop: int
+    species: list
+    molecules: bool
+
+
+class AngularDistributionFunction(TrajectoryCalculator, ABC):
     """
     Compute the Angular Distribution Function for all species combinations
 
@@ -96,29 +122,20 @@ def __init__(self, **kwargs):
         """
         super().__init__(**kwargs)
         self.scale_function = {"quadratic": {"outer_scale_factor": 10}}
-        self.loaded_property = "Positions"
+        self.loaded_property = simulation_properties.positions
 
         self.use_tf_function = None
-        self.r_cut = None
-        self.start = None
         self.molecules = None
-        self.experimental = True
-        self.stop = None
-        self.n_confs = None
-        self.bins = None
         self.bin_range = None
-        self._batch_size = None  # memory management for all batches
-        self.species = None
         self.number_of_atoms = None
         self.norm_power = None
+        self.sample_configurations = None
         self.result_series_keys = ["angle", "adf"]
+        self._dtype = tf.float32
 
-        # TODO _n_batches is used instead of n_batches because the memory management is
-        #  not yet implemented correctly
-        self.n_minibatches = None  # memory management for triples generation per batch.
+        self.minibatch = None  # memory management for triples generation per batch.
 
         self.analysis_name = "Angular_Distribution_Function"
-        self.database_group = "Angular_Distribution_Function"
         self.x_label = r"$$\text{Angle} / \theta $$"
         self.y_label = r"$$\text{ADF} / a.u.$$"
 
@@ -126,12 +143,12 @@ def __init__(self, **kwargs):
     def __call__(
         self,
         batch_size: int = 1,
-        n_minibatches: int = 50,
-        n_confs: int = 5,
-        r_cut: int = 6.0,
+        minibatch: int = 50,
+        number_of_configurations: int = 5,
+        cutoff: int = 6.0,
         start: int = 1,
         stop: int = None,
-        bins: int = 500,
+        number_of_bins: int = 500,
         species: list = None,
         use_tf_function: bool = False,
         molecules: bool = False,
@@ -139,23 +156,23 @@ def __call__(
         plot: bool = True,
         norm_power: int = 4,
         **kwargs,
-    ) -> Computation:
+    ):
         """
         Parameters
         ----------
         batch_size : int
             Number of batches, to split the configurations into.
-        n_minibatches: int
+        minibatch: int
             Number of minibatches for computing the angles to split each batch into
-        n_confs: int
+        number_of_configurations: int
             Number of configurations to analyse
-        r_cut: float
+        cutoff: float
             cutoff radius for the ADF
         start: int
             Index of the first configuration
         stop: int
             Index of the last configuration
-        bins: int
+        number_of_bins: int
             bins for the ADF
         use_tf_function: bool, default False
             activate the tf.function decorator for the minibatches. Can speed
@@ -169,51 +186,42 @@ def __call__(
             The power of the normalization factor applied to the ADF histogram.
             If set to zero no distance normalization will be applied.
         molecules : bool
-                if true, perform the anlaysis on molecules.
+                if true, perform the analysis on molecules.
         gpu : bool
                 if true, scale the memory requirements to that of the biggest
                 GPU on the machine.
         plot : bool
                 If true, plot the result of the analysis.
-
-        Notes
-        -----
-        # TODO _n_batches is used instead of n_batches because the memory
-        management is not yet implemented correctly
-
         """
-        # Parse the parent class arguments.
-        self.update_user_args(data_range=1, gpu=gpu, plot=plot)
+        # set args that will affect the computation result
+        self.args = Args(
+            number_of_bins=number_of_bins,
+            cutoff=cutoff,
+            start=start,
+            stop=stop,
+            atom_selection=np.s_[:],
+            data_range=1,
+            correlation_time=1,
+            molecules=molecules,
+            species=species,
+            number_of_configurations=number_of_configurations,
+            norm_power=norm_power,
+        )
 
         # Parse the user arguments.
         self.use_tf_function = use_tf_function
-        self.r_cut = r_cut
-        self.start = start
-        self.stop = stop
-        self.molecules = molecules
-        self.n_confs = n_confs
-        self.bins = bins
+        self.cutoff = cutoff
+        self.gpu = gpu
+        self.plot = plot
         self._batch_size = batch_size  # memory management for all batches
-        self.n_minibatches = (
-            n_minibatches  # memory management for triples generation per batch.
+        self.minibatch = (
+            minibatch  # memory management for triples generation per batch.
         )
-        self.species = species
-        self._check_inputs()
         self.bin_range = [0.0, 3.15]  # from 0 to a chemists pi
         self.norm_power = norm_power
+        self.override_n_batches = kwargs.get("batches")
 
-        return self.update_db_entry_with_kwargs(
-            r_cut=r_cut,
-            start=start,
-            stop=stop,
-            molecules=molecules,
-            n_confs=n_confs,
-            bins=bins,
-            species=species,
-            norm_power=norm_power,
-        )
-
-    def _check_inputs(self):
+    def check_input(self):
         """
         Check the inputs and set defaults if necessary.
 
@@ -221,20 +229,22 @@ def _check_inputs(self):
         -------
         Updates the class attributes.
         """
-        if self.stop is None:
-            self.stop = self.experiment.number_of_configurations - 1
-
-        if self.species is None:
-            self.species = list(self.experiment.species)
-            self._compute_number_of_atoms(self.experiment.species)
+        self._run_dependency_check()
+        if self.args.stop is None:
+            self.args.stop = self.experiment.number_of_configurations - 1
+
+        # Get the correct species out.
+        if self.args.species is None:
+            if self.args.molecules:
+                self.args.species = list(self.experiment.molecules)
+                self._compute_number_of_atoms(self.experiment.molecules)
+            else:
+                self.args.species = list(self.experiment.species)
+                self._compute_number_of_atoms(self.experiment.species)
 
         else:
             self._compute_number_of_atoms(self.experiment.species)
 
-        if self.molecules:
-            self.species = list(self.experiment.molecules)
-            self._compute_number_of_atoms(self.experiment.molecules)
-
     def _compute_number_of_atoms(self, reference: dict):
         """
         Compute the number of atoms total in the selected set of species.
@@ -249,35 +259,11 @@ def _compute_number_of_atoms(self, reference: dict):
         Updates the number of atoms attribute.
         """
         number_of_atoms = 0
-        for item in self.species:
+        for item in self.args.species:
             number_of_atoms += len(reference[item]["indices"])
 
         self.number_of_atoms = number_of_atoms
 
-    def _load_positions(self, indices: list) -> tf.Tensor:
-        """
-        Load the positions matrix
-
-        This function is here to optimize calculation speed
-
-        Parameters
-        ----------
-        indices : list
-                List of indices to take from the database_path
-        Returns
-        -------
-        loaded_data : tf.Tensor
-                tf.Tensor of tensor_values loaded from the hdf5 database_path
-        """
-        path_list = [join_path(species, "Positions") for species in self.species]
-        data = self.experiment.load_matrix(
-            "Positions", path=path_list, select_slice=np.s_[:, indices]
-        )
-        if len(self.species) == 1:
-            return data
-        else:
-            return tf.concat(data, axis=0)
-
     def _prepare_data_structure(self):
         """
         Prepare variables and dicts for the analysis.
@@ -285,33 +271,23 @@ def _prepare_data_structure(self):
         -------
 
         """
-        sample_configs = np.linspace(self.start, self.stop, self.n_confs, dtype=np.int)
+        sample_configs = np.linspace(
+            self.args.start,
+            self.args.stop,
+            self.args.number_of_configurations,
+            dtype=np.int,
+        )
 
         species_indices = []
         start_index = 0
         stop_index = 0
-        for species in self.species:
+        for species in self.args.species:
             stop_index += len(self.experiment.species.get(species).get("indices"))
             species_indices.append((species, start_index, stop_index))
             start_index = stop_index
 
         return sample_configs, species_indices
 
-    def _prepare_generators(self, sample_configs):
-        """
-        Prepare the generators and compiled functions for the analysis.
-
-        Returns
-        -------
-
-        """
-
-        def generator():
-            for idx in sample_configs:
-                yield self._load_positions(idx)
-
-        return generator
-
     def _prepare_triples_generator(self):
         """
         Prepare the triples generators including tf.function
@@ -325,9 +301,9 @@ def _prepare_triples_generator(self):
             def _get_triplets(x):
                 return get_triplets(
                     x,
-                    r_cut=self.r_cut,
+                    r_cut=self.cutoff,
                     n_atoms=self.number_of_atoms,
-                    n_batches=self.n_minibatches,
+                    n_batches=self.minibatch,
                     disable_tqdm=True,
                 )
 
@@ -336,9 +312,9 @@ def _get_triplets(x):
             def _get_triplets(x):
                 return get_triplets(
                     x,
-                    r_cut=self.r_cut,
+                    r_cut=self.cutoff,
                     n_atoms=self.number_of_atoms,
-                    n_batches=self.n_minibatches,
+                    n_batches=self.minibatch,
                     disable_tqdm=True,
                 )
 
@@ -357,8 +333,8 @@ def _compute_rijk_matrices(self, tmp: tf.Tensor, timesteps: int):
         r_ij_flat = next(
             get_neighbour_list(tmp, cell=self.experiment.box_array, batch_size=1)
         )
-        r_ij_indices = get_triu_indicies(self.number_of_atoms)
 
+        r_ij_indices = get_triu_indicies(self.number_of_atoms)
         # Shape is now (n_atoms, n_atoms, 3, n_timesteps)
         r_ij_mat = tf.scatter_nd(
             indices=tf.transpose(r_ij_indices),
@@ -373,7 +349,8 @@ def _compute_rijk_matrices(self, tmp: tf.Tensor, timesteps: int):
 
         return r_ij_mat, r_ijk_indices
 
-    def _compute_angles(self, species, r_ijk_indices):
+    @staticmethod
+    def _compute_angles(species, r_ijk_indices):
         """
         Compute the angles between indices in triangle.
 
@@ -406,7 +383,7 @@ def _compute_angles(self, species, r_ijk_indices):
 
         return condition, name
 
-    def _build_histograms(self, sample_configs, species_indices):
+    def _build_histograms(self, positions, species_indices, angles):
         """
         Build the adf histograms.
 
@@ -415,48 +392,34 @@ def _build_histograms(self, sample_configs, species_indices):
         angles : dict
                 A dictionary of the triples references and their histogram values.
         """
-        angles = {}
 
-        # Prepare the generators and trplets functions -- handle tf.function calls.
-        generator = self._prepare_generators(sample_configs)
+        tmp = tf.transpose(tf.concat(positions, axis=0), (1, 0, 2))
 
-        # Prepare the dataset generators.
-        dataset = tf.data.Dataset.from_generator(
-            generator,
-            output_signature=(tf.TensorSpec(shape=(None, None), dtype=tf.float32)),
-        )
-        dataset = dataset.batch(self._batch_size).prefetch(tf.data.AUTOTUNE)
-
-        log.debug(f"batch_size: {self._batch_size}")
-
-        for positions in tqdm(
-            dataset, total=self.n_confs, ncols=70, desc="Building histograms"
-        ):
-            timesteps, atoms, _ = tf.shape(positions)
-            tmp = tf.concat(positions, axis=0)
-            r_ij_mat, r_ijk_indices = self._compute_rijk_matrices(tmp, timesteps)
-
-            for species in itertools.combinations_with_replacement(species_indices, 3):
-                # Select the part of the r_ijk indices, where the selected species
-                # triple occurs.
-                condition, name = self._compute_angles(species, r_ijk_indices)
-                tmp = tf.gather_nd(r_ijk_indices, tf.where(condition))
-
-                # Get the indices required.
-                angle_vals, pre_factor = get_angles(r_ij_mat, tmp)
-                pre_factor = 1 / pre_factor ** self.norm_power
-                histogram, _ = np.histogram(
-                    angle_vals,
-                    bins=self.bins,
-                    range=self.bin_range,
-                    weights=pre_factor,
-                    density=True,
-                )
-                histogram = tf.cast(histogram, dtype=tf.float32)
-                if angles.get(name) is not None:
-                    angles.update({name: angles.get(name) + histogram})
-                else:
-                    angles.update({name: histogram})
+        timesteps, atoms, _ = tf.shape(tmp)
+
+        r_ij_mat, r_ijk_indices = self._compute_rijk_matrices(tmp, timesteps)
+
+        for species in itertools.combinations_with_replacement(species_indices, 3):
+            # Select the part of the r_ijk indices, where the selected species
+            # triple occurs.
+            condition, name = self._compute_angles(species, r_ijk_indices)
+            tmp = tf.gather_nd(r_ijk_indices, tf.where(condition))
+
+            # Get the indices required.
+            angle_vals, pre_factor = get_angles(r_ij_mat, tmp)
+            pre_factor = 1 / pre_factor ** self.norm_power
+            histogram, _ = np.histogram(
+                angle_vals,
+                bins=self.args.number_of_bins,
+                range=self.bin_range,
+                weights=pre_factor,
+                density=True,
+            )
+            histogram = tf.cast(histogram, dtype=tf.float32)
+            if angles.get(name) is not None:
+                angles.update({name: angles.get(name) + histogram})
+            else:
+                angles.update({name: histogram})
 
         return angles
 
@@ -482,12 +445,12 @@ def _compute_adfs(self, angles, species_indices):
             bin_range_to_angles = np.linspace(
                 self.bin_range[0] * (180 / 3.14159),
                 self.bin_range[1] * (180 / 3.14159),
-                self.bins,
+                self.args.number_of_bins,
             )
 
             self.selected_species = [_species[0] for _species in species]
 
-            self.data_range = self.n_confs
+            self.data_range = self.args.number_of_configurations
             log.debug(f"species are {species}")
 
             data = {
@@ -497,20 +460,6 @@ def _compute_adfs(self, angles, species_indices):
 
             self.queue_data(data=data, subjects=self.selected_species)
 
-    def run_experimental_analysis(self):
-        """
-        Perform the ADF analysis.
-
-        Notes
-        -----
-        # TODO select when to enable tqdm of get_triplets
-        # TODO batch_size!
-        # TODO allow for delete_duplicates with more then 2 species!
-        """
-        sample_configs, species_indices = self._prepare_data_structure()
-        angles = self._build_histograms(sample_configs, species_indices)
-        self._compute_adfs(angles, species_indices)
-
     def plot_data(self, data):
         """Plot data"""
         for selected_species, val in data.items():
@@ -526,5 +475,108 @@ def plot_data(self, data):
             self.run_visualization(
                 x_data=np.array(val[self.result_series_keys[0]]),
                 y_data=np.array(val[self.result_series_keys[1]]),
-                title=(f"{selected_species} - Max:" f" {title_value:.3f} degrees "),
+                title=f"{selected_species} - Max:" f" {title_value:.3f} degrees ",
             )
+
+    def _format_data(self, batch: tf.Tensor, keys: list) -> tf.Tensor:
+        """
+        Format the loaded data for use in the rdf calculator.
+
+        The RDF requires a reshaped dataset. The generator will load a default
+        dict oriented type. This method restructures the data to be used in the
+        calculator.
+
+        Parameters
+        ----------
+        batch : tf.Tensor
+                A batch of data to transform.
+        keys : list
+                Dict keys to extract from the data.
+
+        Returns
+        -------
+
+        """
+        formatted_data = []
+        for item in keys:
+            formatted_data.append(batch[item])
+
+        if len(self.args.species) == 1:
+            return tf.cast(formatted_data[0], dtype=self.dtype)
+        else:
+            return tf.cast(tf.concat(formatted_data, axis=0), dtype=self.dtype)
+
+    def _correct_batch_properties(self):
+        """
+        We must fix the batch size parameters set by the parent class.
+
+        Returns
+        -------
+        Updates the parent class.
+        """
+        if self.batch_size > self.args.number_of_configurations:
+            self.batch_size = self.args.number_of_configurations
+            self.n_batches = 1
+        else:
+            self.n_batches = int(self.args.number_of_configurations / self.batch_size)
+
+        if self.override_n_batches is not None:
+            self.n_batches = self.override_n_batches
+
+    def prepare_computation(self):
+        """
+        Run steps necessary to prepare the computation for running.
+
+        Returns
+        -------
+
+        """
+        path_list = [
+            join_path(item, self.loaded_property[0]) for item in self.args.species
+        ]
+        self._prepare_managers(path_list)
+
+        # batch loop correction
+        self._correct_batch_properties()
+
+        # Get the correct dict keys.
+        dict_keys = []
+        for item in self.args.species:
+            dict_keys.append(str.encode(join_path(item, self.loaded_property[0])))
+
+        # Split the configurations into batches.
+        split_arr = np.array_split(self.sample_configurations, self.n_batches)
+
+        return dict_keys, split_arr
+
+    def run_calculator(self):
+        """
+        Run the analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        self.sample_configurations, species_indices = self._prepare_data_structure()
+
+        dict_keys, split_arr = self.prepare_computation()
+
+        # Get the batch dataset
+        batch_ds = self.get_batch_dataset(
+            subject_list=self.args.species, loop_array=split_arr, correct=True
+        )
+
+        angles = {}
+
+        # Loop over the batches.
+        for idx, batch in tqdm(enumerate(batch_ds), ncols=70):
+            positions_tensor = self._format_data(batch=batch, keys=dict_keys)
+
+            angles = self._build_histograms(
+                positions=positions_tensor,
+                species_indices=species_indices,
+                angles=angles,
+            )
+
+        self._compute_adfs(angles, species_indices)
diff --git a/mdsuite/calculators/calculator.py b/mdsuite/calculators/calculator.py
index ed2d04be..f9ec1b57 100644
--- a/mdsuite/calculators/calculator.py
+++ b/mdsuite/calculators/calculator.py
@@ -23,26 +23,19 @@
 
 Summary
 -------
+Parent class for the calculators.
 """
 from __future__ import annotations
 
 import logging
 import numpy as np
-import random
-from pathlib import Path
 import tensorflow as tf
-import pandas as pd
-from scipy.optimize import curve_fit
 from mdsuite.visualizer.d2_data_visualization import DataVisualizer2D
-from mdsuite.utils.exceptions import RangeExceeded
-from mdsuite.utils.meta_functions import join_path
-from mdsuite.memory_management.memory_manager import MemoryManager
-from mdsuite.database.data_manager import DataManager
-from mdsuite.database.simulation_database import Database
-from mdsuite.calculators.transformations_reference import switcher_transformations
 from mdsuite.database.calculator_database import CalculatorDatabase
+import mdsuite.database.scheme as db
 from tqdm import tqdm
-from typing import Union, List, Any
+from typing import Union, List, Dict
+import warnings
 
 import functools
 
@@ -51,18 +44,44 @@
 if TYPE_CHECKING:
     from mdsuite import Experiment
 
+tqdm.monitor_interval = 0
+warnings.filterwarnings("ignore")
+
 log = logging.getLogger(__name__)
 
 
 def call(func):
-    """Decorator for the calculator call method
+    """
+    Decorator for the calculator call method
 
     This decorator provides a unified approach for handling run_computation and
     load_data for a single or multiple experiments.
-    It handles the `run_computation()` method, iterates over experiments and
+    It handles the `run.<calc>()` method, iterates over experiments and
     loads data if requested! Therefore, the __call__ method does not and can
     not return any values anymore!
 
+
+    Notes
+    -----
+    When calling the calculator it will check if a computation with the given
+    user arguments was already performed:
+    >>> Calculator.get_computation_data() is not None
+
+    if no computations are available it will
+    1. prepare a database entry
+    >>> Calculator.prepare_db_entry()
+    2. save the user arguments
+    >>> Calculator.save_computation_args()
+    3. Run the analysis
+    >>> Calculator.run_analysis()
+    4. Save all the data to the database
+    >>> Calculator.save_db_data()
+    5. Finally query the the data from the database and pass them to the user / plotting
+    >>> data = Calculator.get_computation_data()
+
+
+
+
     Parameters
     ----------
     func: Calculator.__call__ method
@@ -74,50 +93,53 @@ def call(func):
     """
 
     @functools.wraps(func)
-    def inner(self, *args, **kwargs):
+    def inner(
+        self, *args, **kwargs
+    ) -> Union[db.Computation, Dict[str, db.Computation]]:
         """Manage the call method
 
         Parameters
         ----------
         self: Calculator
 
-
         Returns
         -------
         data:
             A dictionary of shape {name: data} when called from the project class
             A list of [data] when called directly from the experiment class
         """
-        # This is only true, when called via project.experiments.Exp.run_computation,
+        # This is only true, when called via project.experiments.<exp>.run,
         #  otherwise the experiment will be None
         return_dict = self.experiment is None
 
         out = {}
         for experiment in self.experiments:
-            self.experiment = experiment
-            self.clean_cache()
-            data = func(self, *args, **kwargs)
+            CLS = self.__class__
+            # NOTE: if the calculator accepts more than just experiment/experiments
+            #  as init, this has to be changed!
+            cls = CLS(experiment=experiment)
+            # pass the user args to the calculator
+            func(cls, *args, **kwargs)
+            data = cls.get_computation_data()
             if data is None:
                 # new calculation will be performed
-                self.prepare_db_entry()
-                self.run_analysis()
-                self.save_db_data()
-                data = func(self, *args, **kwargs)
-            elif self.load_data:
-                raise NotImplementedError(
-                    "Please user <exp/proj>.run.<method> instead of load!"
-                )
-            else:
-                # Calculation already performed
-                pass
-
-            out[self.experiment.name] = data
-
-            if self.plot:
+                cls.prepare_db_entry()
+                cls.save_computation_args()
+                cls.run_analysis()
+                cls.save_db_data()
+                # Need to reset the user args, if they got change
+                # or set to defaults, e.g. n_configurations = - 1 so
+                # that they match the query
+                func(cls, *args, **kwargs)
+                data = cls.get_computation_data()
+
+            if cls.plot:
                 """Plot the data"""
-                self.plotter = DataVisualizer2D(title=self.analysis_name)
-                self.plot_data(data.data_dict)
-                self.plotter.grid_show(self.plot_array)
+                cls.plotter = DataVisualizer2D(title=cls.analysis_name)
+                cls.plot_data(data.data_dict)
+                cls.plotter.grid_show(cls.plot_array)
+
+            out[cls.experiment.name] = data
 
         if return_dict:
             return out
@@ -134,39 +156,48 @@ class Calculator(CalculatorDatabase):
     Attributes
     ----------
     experiment : Experiment
-            Class object of the experiment.
-    plot : bool (default=True)
-            Decision to plot the analysis.
-    save : bool (default=True)
-            Decision to save the generated tensor_values arrays.
-
-    data_range : int (default=500)
-            Range over which the property should be evaluated. This is not
-            applicable to the current analysis as the full rdf will be
-            calculated.
-    batch_size : dict
-            Size of batches to use in the analysis separated into parallel and
-             serial components, i.e {'Serial': 100, 'Parallel': 50} for a two
-             component, symmetric experiment.
-    n_batches : dict
-            Number of barthes to use as a dictionary for both serial and
-            parallel implementations
+                Experiment for which the calculator will be run.
+    experiments : List[Experiment]
+            List of experiments on which to run the calculator.
+    plot : bool
+            If true, the results will be plotted.
+    gpu : bool
+            If true, the memory will be scaled down to the best GPU on the system.
+    system_property: bool (default = False)
+            If the calculator returns a value for the whole system such as ionic
+            conductivity or viscosity as opposed to a species-specific number.
+    experimental : bool (default = False)
+            If true, a warning is raised upon calling this calcalator with more
+            information about why it is experimental.
+    selected_species: tuple
+            Species currently being studied in a specific loop.
+    analysis_name: str
+            Name of the analysis to store in the database.
+    time : np.ndarray
+            Time array over which to integrate and plot.
+    plotter : DataVisualizer2D
+            Data visualizer class for use in the plotting.
+    result_keys : list
+            keys to use when storing the results. e.g.
+            ["diffusion_coefficient", "uncertainty"]
+    result_series_keys : list
+            keys to use when storing series results e.g.
+            ["time", "msd"]
+    prefactor : float (optional)
+            can be set if the same pre-factor is required many times.
+    x_label : str
+            x-label for the plots.
+    y_label : str
+            y-label for the plots.
+    _dtype : object = tf.float64
+            dtype required by the analysis.
     plot_array : list
             A list of plot objects to be show together at the end of the
             species loop.
     """
 
     def __init__(
-        self,
-        experiment: Experiment = None,
-        experiments: List[Experiment] = None,
-        plot: bool = True,
-        save: bool = True,
-        data_range: int = 500,
-        correlation_time: int = 1,
-        atom_selection: object = np.s_[:],
-        gpu: bool = False,
-        load_data: bool = False,
+        self, experiment: Experiment = None, experiments: List[Experiment] = None
     ):
         """
         Constructor for the calculator class.
@@ -174,440 +205,71 @@ def __init__(
         Parameters
         ----------
         experiment : Experiment
-                Experiment object to update.
-        experiments: List[Experiment]:
-                List of experiments, for that the calculation should be executed
-        plot : bool
-                If true, analysis is plotted.
-        save : bool
-                If true, the analysis is saved.
-        data_range : int
-                Data range over which to compute.
-        correlation_time : int
-                Correlation time to use in the analysis.
-        atom_selection : np.s_
-                Atoms to perform the analysis on.
-        gpu : bool
-                If true, reduce memory usage to what is allowed on the system
-                GPU.
+                Experiment for which the calculator will be run.
+        experiments : List[Experiment]
+                List of experiments on which to run the calculator.
         """
         # Set upon instantiation of parent class
         super().__init__(experiment)
+        # NOTE: if the calculator accepts more than just experiment/experiments
+        #  in the init the @call decorator has to be changed!
         self.experiment: Experiment = experiment
         self.experiments: List[Experiment] = experiments
-
         # Setting the experiment value supersedes setting experiments
         if self.experiment is not None:
             self.experiments = [self.experiment]
 
-        self.data_range = data_range
-        self.plot = plot
-        self.save = save
-        self.atom_selection = atom_selection
-        self.correlation_time = correlation_time
-        self.database = None
-        self.gpu = gpu
-        self.load_data = load_data
+        self.plot = False
+        self.gpu = False
 
-        # Set to default by class, over-written in child classes or during operations
-        self.system_property = False
-        self.multi_species = False
-        self.post_generation = False
-        self.experimental = False
-        self.optimize = False
-        self.loaded_property = None
-        self.dependency = None
-        self.scale_function = None
-        self.batch_output_signature = None
-        self.ensemble_output_signature = None
-        self.species = None
-        # all species that are used in the calculation
-        self.selected_species = None
-        # the selected species which the current calculation iteration is performed on
-        self.database_group = None
-        self.analysis_name = None
-        self.tau_values = None
-        self.time = None
-        self.data_resolution = None
-        self.plotter = None
-        # e.g. [diffusion_coefficient, uncertainty]
+        # SQL data attributes.
         self.result_keys = None
-        # e.g., [time, msd]
         self.result_series_keys = None
-
-        # Set during operation or by child class
-        self.batch_size: int
-        self.n_batches: int
-        self.remainder: int
-        self.prefactor: float
-        self.memory_manager: MemoryManager
-        self.data_manager: DataManager
-        self.x_label: str
-        self.y_label: str
-        self.analysis_name: str
-        self.minibatch: bool
-
-        self.x_label: str
-        self.y_label: str
-        self.analysis_name: str
-        self.plot_array = []
-
-        self.database_group = None
         self.analysis_name = None
+        self.selected_species = None
 
-        # Properties
-        self._dtype = tf.float64
-
-    def update_user_args(
-        self,
-        plot: bool,
-        data_range: int = 500,
-        correlation_time: int = 1,
-        atom_selection: object = np.s_[:],
-        tau_values: Union[int, List, Any] = np.s_[:],
-        gpu: bool = False,
-        *args,
-        **kwargs,
-    ):
-        """
-        Update the user args that are given by the __call__ method of the
-        calculator.
-
-        Parameters
-        ----------
-        plot : bool
-                If true, analysis is plotted.
-        save : bool
-                If true, the analysis is saved.
-        data_range : int
-                Data range over which to compute.
-        correlation_time : int
-                Correlation time to use in the analysis.
-        atom_selection : object
-                Atoms to perform the analysis on.
-        gpu : bool
-                If true, reduce memory usage to what is allowed on the system
-                GPU.
-        """
-        self.database = Database(
-            name=Path(self.experiment.database_path, "database.hdf5").as_posix()
-        )
-
-        # Prevent $DISPLAY warnings on clusters.
-        if self.experiment.cluster_mode:
-            import matplotlib
-
-            matplotlib.use("Agg")
-
-        self.data_range = data_range
-        self.plot = plot
-        self.gpu = gpu
-        self.tau_values = tau_values
-        self.correlation_time = correlation_time
-        self.atom_selection = atom_selection
-
-        # attributes based on user args
-        self.time = self._handle_tau_values()  # process selected tau values.
-
-    def _calculate_prefactor(self, species: Union[str, tuple] = None):
-        """
-        calculate the calculator pre-factor.
-
-        Parameters
-        ----------
-        species : str
-                Species property if required.
-        Returns
-        -------
-
-        """
-        raise NotImplementedError
-
-    def _apply_operation(self, data, index):
-        """
-        Perform operation on an ensemble.
-
-        Parameters
-        ----------
-        One tensor_values range of tensor_values to operate on.
-
-        Returns
-        -------
-
-        """
-        raise NotImplementedError
-
-    def _apply_averaging_factor(self):
-        """
-        Apply an averaging factor to the tensor_values.
-        Returns
-        -------
-        averaged copy of the tensor_values
-        """
-        raise NotImplementedError
-
-    def _post_operation_processes(self, species: Union[str, tuple] = None):
-        """
-        call the post-op processes
-
-        Parameters
-        ----------
-        species : Union[str, tuple]
-                List or tuple of species fo which this post-operation process
-                is being applied.
-        Returns
-        -------
-
-        """
-        raise NotImplementedError
-
-    def _update_output_signatures(self):
-        """
-        After having run _prepare managers, update the output signatures.
-
-        Returns
-        -------
-
-        """
-        pass
-
-    @staticmethod
-    def _fit_einstein_curve(data: list):
-        """
-        Fit operation for Einstein calculations
-
-        Parameters
-        ----------
-        data : list
-                x and y tensor_values for the fitting [np.array, np.array] of
-                (2, data_range)
-
-        Returns
-        -------
-        fit results : list
-                A tuple list with the fit value along with the error of the fit
-        """
-
-        fits = []  # define an empty fit array so errors may be extracted
-
-        def func(x, m, a):
-            """
-            Standard linear function for fitting.
-
-            Parameters
-            ----------
-            x : list/np.array
-                    x axis tensor_values for the fit
-            m : float
-                    gradient of the line
-            a : float
-                    scalar offset, also the y-intercept for those who did not
-                    get much maths in school.
-
-            Returns
-            -------
-
-            """
-            return m * x + a
-
-        # get the logarithmic dataset
-        log_y = np.log10(data[1][1:])
-        log_x = np.log10(data[0][1:])
-
-        min_end_index, max_end_index = int(0.8 * len(log_y)), int(len(log_y) - 1)
-        min_start_index, max_start_index = int(0.3 * len(log_y)), int(0.5 * len(log_y))
-
-        for _ in range(100):
-            end_index = random.randint(min_end_index, max_end_index)
-            start_index = random.randint(min_start_index, max_start_index)
-
-            popt, pcov = curve_fit(
-                func, log_x[start_index:end_index], log_y[start_index:end_index]
-            )
-            fits.append(10 ** popt[1])
-
-        return [np.mean(fits), np.std(fits)]
-
-    @staticmethod
-    def _update_species_type_dict(dictionary: dict, path_list: list, dimension: int):
-        """
-        Update a type spec dictionary for a species input.
-
-        Parameters
-        ----------
-        dictionary : dict
-                Dictionary to append
-        path_list : list
-                List of paths for the dictionary
-        dimension : int
-                Dimension of the property
-        Returns
-        -------
-        type dict : dict
-                Dictionary for the type spec.
-        """
-        for item in path_list:
-            dictionary[str.encode(item)] = tf.TensorSpec(
-                shape=(None, None, dimension), dtype=tf.float64
-            )
-
-        return dictionary
-
-    @staticmethod
-    def _build_pandas_dataframe(x: np.array, y: np.array) -> pd.DataFrame:
-        """
-        Build a pandas dataframe with x and y data.
-
-        Parameters
-        ----------
-        x : np.array
-                x data to go into the data frame
-        y : np.array
-                y data to go into the data frame
-
-        Returns
-        -------
-        data : pd.DataFrame
-                Pandas data frame of the data
-        """
-
-        return pd.DataFrame({"x": x, "y": y})
-
-    def _msd_operation(self, ensemble: tf.Tensor, square: bool = True):
-        """
-        Perform a simple msd operation.
-
-        Parameters
-        ----------
-        ensemble : tf.Tensor
-            Trajectory over which to compute the msd.
-        square : bool
-            If true, square the result, else just return the difference.
-        Returns
-        -------
-        msd : tf.Tensor
-                Mean square displacement.
-        """
-        if square:
-            return tf.math.squared_difference(
-                tf.gather(ensemble, self.tau_values, axis=1), ensemble[:, None, 0]
-            )
-        else:
-            return tf.math.subtract(ensemble, ensemble[:, None, 0])
-
-    def _handle_tau_values(self) -> np.array:
-        """
-        Handle the parsing of custom tau values.
-
-        Returns
-        -------
-        times : np.array
-            The time values corresponding to the selected tau values
-        """
-        if isinstance(self.tau_values, int):
-            self.data_resolution = self.tau_values
-            self.tau_values = np.linspace(
-                0, self.data_range - 1, self.tau_values, dtype=int
-            )
-        if isinstance(self.tau_values, list) or isinstance(self.tau_values, np.ndarray):
-            self.data_resolution = len(self.tau_values)
-            self.data_range = self.tau_values[-1] + 1
-        if isinstance(self.tau_values, slice):
-            self.tau_values = np.linspace(
-                0, self.data_range - 1, self.data_range, dtype=int
-            )[self.tau_values]
-            self.data_resolution = len(self.tau_values)
-
-        times = (
-            np.asarray(self.tau_values)
-            * self.experiment.time_step
-            * self.experiment.sample_rate
-        )
-
-        return times
-
-    def _prepare_managers(self, data_path: list):
-        """
-        Prepare the memory and tensor_values monitors for calculation.
-
-        Parameters
-        ----------
-        data_path : list
-                List of tensor_values paths to load from the hdf5
-                database_path.
+        # Calculator attributes
+        self.system_property = False
+        self.experimental = False
+        self.time = None
+        self.prefactor = None
 
-        Returns
-        -------
-        Updates the calculator class
-        """
-        self.memory_manager = MemoryManager(
-            data_path=data_path,
-            database=self.database,
-            memory_fraction=0.8,
-            scale_function=self.scale_function,
-            gpu=self.gpu,
-        )
-        (
-            self.batch_size,
-            self.n_batches,
-            self.remainder,
-        ) = self.memory_manager.get_batch_size(system=self.system_property)
-
-        self.ensemble_loop, minibatch = self.memory_manager.get_ensemble_loop(
-            self.data_range, self.correlation_time
-        )
-        if minibatch:
-            self.batch_size = self.memory_manager.batch_size
-            self.n_batches = self.memory_manager.n_batches
-            self.remainder = self.memory_manager.remainder
-
-        self.data_manager = DataManager(
-            data_path=data_path,
-            database=self.database,
-            data_range=self.data_range,
-            batch_size=self.batch_size,
-            n_batches=self.n_batches,
-            ensemble_loop=self.ensemble_loop,
-            correlation_time=self.correlation_time,
-            remainder=self.remainder,
-            atom_selection=self.atom_selection,
-            minibatch=minibatch,
-            atom_batch_size=self.memory_manager.atom_batch_size,
-            n_atom_batches=self.memory_manager.n_atom_batches,
-            atom_remainder=self.memory_manager.atom_remainder,
-        )
-        self._update_output_signatures()
+        # Data attributes
+        self._dtype: object = tf.float64
 
-    def _build_table_name(self, species: str = "System"):
-        """
-        Build the sql table name for the data storage.
+        # Plotting attributes
+        self.plotter = None
+        self.x_label = None
+        self.y_label = None
+        self.plot_array = []
 
-        Parameters
-        ----------
-        species : str
-                In the case of a species specific analysis, make sure a species
-                is put here. Otherwise, it is set to System.
-        Returns
-        -------
-        name : str
-                A correctly formatted name for the SQL database.
-        """
-        return f"{self.database_group}_{self.analysis_name}_{self.data_range}_{species}"
+    @property
+    def dtype(self):
+        """Get the dtype used for the calculator"""
+        return self._dtype
 
-    def run_visualization(self, x_data, y_data, title: str, layouts: object = None):
+    def run_visualization(
+        self, x_data: np.ndarray, y_data: np.ndarray, title: str, layouts: object = None
+    ):
         """
         Run a visualization session on the data.
 
         Parameters
         ----------
-        x_data
-        y_data
-        title
-        span
+        layouts : object
+                Additional plot features that may be added.
+                See https://docs.bokeh.org/en/latest/docs/reference/models.html for
+                more information.
+        x_data : np.ndarray
+                Data to be plotted along the x axis
+        y_data : np.ndarray
+                Data to be plotted along the y-axis
+        title : str
+                Title of the analysis.
 
         Returns
         -------
-
+        Updates the plot array with a Bokeh plot object.
         """
         self.plot_array.append(
             self.plotter.construct_plot(
@@ -620,334 +282,44 @@ def run_visualization(self, x_data, y_data, title: str, layouts: object = None):
             )
         )
 
-    def _check_input(self):
+    def run_calculator(self):
         """
-        Look for user input that would kill the analysis
+        Run the calculation. This should be implemented in each calculator.
 
         Returns
         -------
-        status: int
-            if 0, check failed, if 1, check passed.
-        """
-        if (
-            self.data_range
-            > self.experiment.number_of_configurations - self.correlation_time
-        ):
-            raise ValueError(
-                "Data range is impossible for this experiment, reduce and try again"
-            )
 
-    def _optimize_einstein_data_range(self, data: np.array):
         """
-        Optimize the tensor_values range of a experiment using the Einstein
-        method of calculation.
-
-        Parameters
-        ----------
-        data : np.array (2, data_range)
-                MSD to study
-
-        Returns
-        -------
-        Updates the data_range attribute of the class state
-        """
-
-        def func(x, m, a):
-            """
-            Standard linear function for fitting.
-
-            Parameters
-            ----------
-            x : list/np.array
-                    x axis tensor_values for the fit
-            m : float
-                    gradient of the line
-            a : float
-                    scalar offset, also the y-intercept for those who did not
-                    get much maths in school.
-
-            Returns
-            -------
-
-            """
-
-            return m * x + a
-
-        # get the logarithmic dataset
-        log_y = np.log10(data[0])
-        log_x = np.log10(data[1])
-
-        end_index = int(len(log_y) - 1)
-        start_index = int(0.4 * len(log_y))
-
-        popt, pcov = curve_fit(
-            func, log_x[start_index:end_index], log_y[start_index:end_index]
-        )
-
-        if 0.85 < popt[0] < 1.15:
-            self.loop_condition = True
-
-        else:
-            try:
-                self.data_range = int(1.1 * self.data_range)
-                self.time = np.linspace(
-                    0.0,
-                    self.data_range
-                    * self.experiment.time_step
-                    * self.experiment.sample_rate,
-                    self.data_range,
-                )
-
-                # end the calculation if the tensor_values range exceeds the relevant
-                # bounds
-                if (
-                    self.data_range
-                    > self.experiment.number_of_configurations - self.correlation_time
-                ):
-                    print("Trajectory not long enough to perform analysis.")
-                    raise RangeExceeded
-            except RangeExceeded:
-                raise RangeExceeded
-
-    def _update_properties_file(self, parameters: dict, delete_duplicate: bool = True):
-        """
-        Update the experiment properties YAML file.
-        """
-        log.warning(
-            "Using depreciated method `_update_properties_file` \t Please use"
-            " `update_database` instead."
-        )
-        self.update_database(parameters=parameters, delete_duplicate=delete_duplicate)
+        raise NotImplementedError
 
-    def _resolve_dependencies(self, dependency):
+    def plot_data(self, data):
         """
-        Resolve any calculation dependencies if possible.
+        Plot the data coming from the database
 
         Parameters
         ----------
-        dependency : str
-                Name of the dependency to resolve.
-
-        Returns
-        -------
-
+        data: db.Compution.data_dict
+                associated with the current project
         """
-
-        def _string_to_function(argument):
-            """
-            Select a transformation based on an input
-
-            Parameters
-            ----------
-            argument : str
-                    Name of the transformation required
-
-            Returns
-            -------
-            transformation call.
-            """
-
-            switcher_unwrapping = {
-                "Unwrapped_Positions": self._unwrap_choice(),
-            }
-
-            # add the other transformations and merge the dictionaries
-            switcher = {**switcher_unwrapping, **switcher_transformations}
-
-            choice = switcher.get(
-                argument, lambda: "Data not in database and can not be generated."
-            )
-            return choice
-
-        transformation = _string_to_function(dependency)
-        self.experiment.perform_transformation(transformation)
-
-    def _unwrap_choice(self):
-        """
-        Unwrap either with indices or with box arrays.
-        Returns
-        -------
-
-        """
-        indices = self.database.check_existence("Box_Images")
-        if indices:
-            return "UnwrapViaIndices"
-        else:
-            return "UnwrapCoordinates"
-
-    def _run_dependency_check(self):
-        """
-        Check to see if the necessary property exists and build it if required.
-
-        Returns
-        -------
-        Will call transformations if required.
-        """
-
-        if self.loaded_property is None:
-            return
-
-        if self.dependency is not None:
-            dependency = self.database.check_existence(self.dependency)
-            if not dependency:
-                self._resolve_dependencies(self.dependency)
-
-        loaded_property = self.database.check_existence(self.loaded_property)
-        if not loaded_property:
-            self._resolve_dependencies(self.loaded_property)
-
-    def perform_computation(self):
-        """
-        Perform the computation.
-        Returns
-        -------
-        Performs the analysis.
-        """
-        if self.system_property:
-            self._calculate_prefactor()
-
-            data_path = [join_path(self.loaded_property, self.loaded_property)]
-            self._prepare_managers(data_path)
-
-            batch_generator, batch_generator_args = self.data_manager.batch_generator(
-                system=self.system_property
-            )
-            batch_data_set = tf.data.Dataset.from_generator(
-                generator=batch_generator,
-                args=batch_generator_args,
-                output_signature=self.batch_output_signature,
+        for selected_species, val in data.items():
+            self.run_visualization(
+                x_data=np.array(val[self.result_series_keys[0]])
+                * self.experiment.units["time"],
+                y_data=np.array(val[self.result_series_keys[1]])
+                * self.experiment.units["time"],
+                title=(
+                    f"{selected_species}: {val[self.result_keys[0]]: 0.3E} +-"
+                    f" {val[self.result_keys[1]]: 0.3E}"
+                ),
             )
-            batch_data_set.prefetch(tf.data.experimental.AUTOTUNE)
-
-            for batch_index, batch in enumerate(batch_data_set):
-                (
-                    ensemble_generator,
-                    ensemble_generators_args,
-                ) = self.data_manager.ensemble_generator(system=self.system_property)
-                ensemble_data_set = tf.data.Dataset.from_generator(
-                    generator=ensemble_generator,
-                    args=ensemble_generators_args + (batch,),
-                    output_signature=self.ensemble_output_signature,
-                )
-
-                for ensemble_index, ensemble in tqdm(
-                    enumerate(ensemble_data_set),
-                    desc="Ensemble Loop",
-                    ncols=70,
-                    total=self.ensemble_loop,
-                ):
-                    self._apply_operation(ensemble, ensemble_index)
-
-            self._apply_averaging_factor()
-            self._post_operation_processes()
-
-        elif self.experimental:
-            data_path = [
-                join_path(species, self.loaded_property)
-                for species in self.experiment.species
-            ]
-            self._prepare_managers(data_path)
-            self.run_experimental_analysis()
-
-        elif self.post_generation:
-            self.run_post_generation_analysis()
-
-        else:
-            for species in self.species:
-                self._calculate_prefactor(species)
-
-                data_path = [join_path(species, self.loaded_property)]
-                self._prepare_managers(data_path)
-
-                (
-                    batch_generator,
-                    batch_generator_args,
-                ) = self.data_manager.batch_generator()
-                batch_data_set = tf.data.Dataset.from_generator(
-                    generator=batch_generator,
-                    args=batch_generator_args,
-                    output_signature=self.batch_output_signature,
-                )
-                batch_data_set = batch_data_set.prefetch(tf.data.experimental.AUTOTUNE)
-
-                for batch_index, batch in tqdm(
-                    enumerate(batch_data_set),
-                    ncols=70,
-                    desc=species,
-                    total=self.n_batches,
-                    disable=self.memory_manager.minibatch,
-                ):
-
-                    (
-                        ensemble_generator,
-                        ensemble_generators_args,
-                    ) = self.data_manager.ensemble_generator()
-                    ensemble_data_set = tf.data.Dataset.from_generator(
-                        generator=ensemble_generator,
-                        args=ensemble_generators_args + (batch,),
-                        output_signature=self.ensemble_output_signature,
-                    )
-                    ensemble_data_set = ensemble_data_set.prefetch(
-                        tf.data.experimental.AUTOTUNE
-                    )
-
-                    for ensemble_index, ensemble in enumerate(ensemble_data_set):
-                        self._apply_operation(ensemble, ensemble_index)
-
-                self._apply_averaging_factor()
-                self._post_operation_processes(species)
-
-    def run_experimental_analysis(self):
-        """
-        For experimental methods
-        Returns
-        -------
-
-        """
-        raise NotImplementedError
-
-    def run_post_generation_analysis(self):
-        """
-        Run a post-generation analysis.
-        """
-        raise NotImplementedError
 
     def run_analysis(self):
         """
         Run the appropriate analysis
         """
-        self._check_input()
-        self._run_dependency_check()
         if self.experimental:
             log.warning(
                 "This is an experimental calculator. Please see the "
                 "documentation before using the results."
             )
-        if self.optimize:
-            pass
-        else:
-            return self.perform_computation()
-
-    @property
-    def dtype(self):
-        """Get the dtype used for the calculator"""
-        return self._dtype
-
-    def plot_data(self, data):
-        """Plot the data coming from the database
-
-        Parameters
-        ----------
-        data: db.Compution.data_dict associated with the current project
-        """
-        for selectected_species, val in data.items():
-            self.run_visualization(
-                x_data=np.array(val[self.result_series_keys[0]])
-                * self.experiment.units["time"],
-                y_data=np.array(val[self.result_series_keys[1]])
-                * self.experiment.units["time"],
-                title=(
-                    f"{selectected_species}: {val[self.result_keys[0]]: 0.3E} +-"
-                    f" {val[self.result_keys[1]]: 0.3E}"
-                ),
-            )
+        self.run_calculator()
diff --git a/mdsuite/calculators/coordination_number_calculation.py b/mdsuite/calculators/coordination_number_calculation.py
index faba6068..cd4ae142 100644
--- a/mdsuite/calculators/coordination_number_calculation.py
+++ b/mdsuite/calculators/coordination_number_calculation.py
@@ -27,15 +27,27 @@
 import logging
 import numpy as np
 from scipy.signal import find_peaks
-from mdsuite.utils.exceptions import NotApplicableToAnalysis, CannotPerformThisAnalysis
+from mdsuite.utils.exceptions import CannotPerformThisAnalysis
 from mdsuite.calculators.calculator import Calculator, call
 from mdsuite.utils.meta_functions import golden_section_search
 from mdsuite.utils.meta_functions import apply_savgol_filter
 from bokeh.models import BoxAnnotation
+from dataclasses import dataclass
+
 
 log = logging.getLogger(__name__)
 
 
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
+
+    savgol_order: int
+    savgol_window_length: int
+
+
 class CoordinationNumbers(Calculator):
     """
     Class for the calculation of coordination numbers
@@ -99,11 +111,12 @@ def __init__(self, **kwargs):
         self.indices = None
         self.savgol_order = None
         self.savgol_window_length = None
+        self.data_range = 1
 
         self.post_generation = True
 
         self.database_group = "Coordination_Numbers"
-        self.x_label = r"$$\text{r}$$"
+        self.x_label = r"$$\text{r} /  nm$$"
         self.y_label = "CN"
         self.analysis_name = "Coordination_Numbers"
         self.result_keys = [
@@ -120,9 +133,7 @@ def __init__(self, **kwargs):
     def __call__(
         self,
         plot: bool = True,
-        save: bool = True,
         data_range: int = 1,
-        export: bool = False,
         savgol_order: int = 2,
         savgol_window_length: int = 17,
     ):
@@ -132,15 +143,10 @@ def __call__(
         ----------
         plot : bool (default=True)
                             Decision to plot the analysis.
-        save : bool (default=True)
-                            Decision to save the generated tensor_values arrays.
-
         data_range : int (default=500)
                             Range over which the property should be evaluated.
                             This is not applicable to the current analysis as
                             the full rdf will be calculated.
-        export : bool
-                If true, export the data directly to a csv.
         savgol_order : int
                 Order of the savgol polynomial filter
         savgol_window_length : int
@@ -150,20 +156,10 @@ def __call__(
         -------
         None.
         """
+        self.plot = plot
 
-        # Calculate the rdf if it has not been done already
-        if self.experiment.radial_distribution_function_state is False:
-            self.experiment.run.RadialDistributionFunction(plot=False, n_batches=-1)
-
-        self.update_user_args(
-            plot=plot, save=save, data_range=data_range, export=export
-        )
-
-        self.savgol_order = savgol_order
-        self.savgol_window_length = savgol_window_length
-
-        return self.update_db_entry_with_kwargs(
-            data_range=data_range,
+        # set args that will affect the computation result
+        self.args = Args(
             savgol_order=savgol_order,
             savgol_window_length=savgol_window_length,
         )
@@ -180,12 +176,6 @@ def _get_density(self, species: str) -> float:
 
         return rdf_number_of_atoms / self.experiment.volume
 
-    def _autocorrelation_time(self):
-        """
-        Not needed in this analysis
-        """
-        raise NotApplicableToAnalysis
-
     def _integrate_rdf(self, density):
         """
         Integrate the rdf currently in the class state
@@ -216,7 +206,9 @@ def _get_max_values(self):
         """
 
         filtered_data = apply_savgol_filter(
-            self.rdf, order=self.savgol_order, window_length=self.savgol_window_length
+            self.rdf,
+            order=self.args.savgol_order,
+            window_length=self.args.savgol_window_length,
         )
         peaks = find_peaks(filtered_data, height=1.0)[0]  # get the maximum values
 
@@ -316,7 +308,7 @@ def _get_coordination_numbers(self):
 
         return first_shell, first_shell_error
 
-    def run_post_generation_analysis(self):
+    def run_calculator(self):
         """
         Calculate the coordination numbers and perform error analysis
         """
diff --git a/mdsuite/calculators/einstein_diffusion_coefficients.py b/mdsuite/calculators/einstein_diffusion_coefficients.py
index 91ebc09c..44095050 100644
--- a/mdsuite/calculators/einstein_diffusion_coefficients.py
+++ b/mdsuite/calculators/einstein_diffusion_coefficients.py
@@ -23,52 +23,48 @@
 
 Summary
 -------
+Module for the computation of self-diffusion coefficients using the Einstein method.
 """
 from __future__ import annotations
 import logging
+from abc import ABC
+
 import numpy as np
-import warnings
 from typing import Union, Any, List
 from tqdm import tqdm
 import tensorflow as tf
-from mdsuite.calculators.calculator import Calculator, call
+from mdsuite.calculators.calculator import call
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+from mdsuite.calculators import TrajectoryCalculator
+from mdsuite.utils.calculator_helper_methods import fit_einstein_curve
 
-from typing import TYPE_CHECKING
 
-if TYPE_CHECKING:
-    from mdsuite.database.scheme import Computation
+log = logging.getLogger(__name__)
 
-tqdm.monitor_interval = 0
-warnings.filterwarnings("ignore")
 
-log = logging.getLogger(__name__)
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
+
+    data_range: int
+    correlation_time: int
+    atom_selection: np.s_
+    tau_values: np.s_
+    molecules: bool
+    species: list
 
 
-class EinsteinDiffusionCoefficients(Calculator):
+class EinsteinDiffusionCoefficients(TrajectoryCalculator, ABC):
     """
     Class for the Einstein diffusion coefficient implementation
 
-    Description:
-    This module contains the code for the Einstein diffusion coefficient class.
-    This class is called by the Experiment class and instantiated when the user calls
-    the  Experiment.einstein_diffusion_coefficients method. The methods in class can
-    then be called by the Experiment.einstein_diffusion_coefficients method and all
-    necessary calculations performed.
-
     Attributes
     ----------
-    experiment :  Experiment
-            Experiment class to call from
-    species : list
-            Which species to perform the analysis on
-    x_label : str
-            X label of the tensor_values when plotted
-    y_label : str
-            Y label of the tensor_values when plotted
-    analysis_name : str
-            Name of the analysis
-    loaded_property : str
-            Property loaded from the database_path for the analysis
+    msd_array : np.ndarray
+            MSd data updated during each ensemble computation.
 
     See Also
     --------
@@ -76,40 +72,33 @@ class EinsteinDiffusionCoefficients(Calculator):
 
     Examples
     --------
-    experiment.run_computation.EinsteinDiffusionCoefficients(data_range=500,
-                                                             plot=True,
-                                                             correlation_time=10)
+    project.experiment.run.EinsteinDiffusionCoefficients(data_range=500,
+                                                         plot=True,
+                                                         correlation_time=10)
     """
 
     def __init__(self, **kwargs):
         """
-
         Parameters
         ----------
         experiment :  Experiment
                 Experiment class to call from
         experiments :  Experiment
                 Experiment classes to call from
-        load_data :  bool
-                whether to load data or not
         """
 
         super().__init__(**kwargs)
         self.scale_function = {"linear": {"scale_factor": 150}}
-        self.loaded_property = "Unwrapped_Positions"
-        self.species = None
-        self.molecules = None
-        self.database_group = "Diffusion_Coefficients"
+        self.loaded_property = simulation_properties.unwrapped_positions
         self.x_label = r"$ \text{Time} / s$"
         self.y_label = r"$ \text{MSD} / m^{2}$"
         self.result_keys = ["diffusion_coefficient", "uncertainty"]
         self.result_series_keys = ["time", "msd"]
         self.analysis_name = "Einstein Self-Diffusion Coefficients"
-        self.loop_condition = False
-        self.optimize = None
-        self.msd_array = None  # define empty msd array
-        self.tau_values = None
-        self.species = list()
+        self._dtype = tf.float64
+
+        self.msd_array = None
+
         log.info("starting Einstein Diffusion Computation")
 
     @call
@@ -118,16 +107,12 @@ def __call__(
         plot: bool = True,
         species: list = None,
         data_range: int = 100,
-        save: bool = True,
-        optimize: bool = False,
         correlation_time: int = 1,
         atom_selection: np.s_ = np.s_[:],
-        export: bool = False,
         molecules: bool = False,
         tau_values: Union[int, List, Any] = np.s_[:],
         gpu: bool = False,
-    ) -> Computation:
-
+    ):
         """
 
         Parameters
@@ -138,16 +123,10 @@ def __call__(
                 List of species on which to operate.
         data_range : int
                 Data range to use in the analysis.
-        save : bool
-                if true, save the output.
-        optimize : bool
-                If true, an optimization loop will be run.
         correlation_time : int
                 Correlation time to use in the window sampling.
         atom_selection : np.s_
                 Selection of atoms to use within the HDF5 database.
-        export : bool
-                If true, export the data directly into a csv file.
         molecules : bool
                 If true, molecules are used instead of atoms.
         tau_values : Union[int, list, np.s_]
@@ -160,54 +139,37 @@ def __call__(
         -------
         None
         """
-        self.update_user_args(
-            plot=plot,
-            data_range=data_range,
-            save=save,
-            correlation_time=correlation_time,
-            atom_selection=atom_selection,
-            tau_values=tau_values,
-            export=export,
-            gpu=gpu,
-        )
-
-        self.species = species
-        self.molecules = molecules
-        self.optimize = optimize
-        # attributes based on user args
-        self.msd_array = np.zeros(self.data_resolution)  # define empty msd array
-
         if species is None:
             if molecules:
-                self.species = list(self.experiment.molecules)
+                species = list(self.experiment.molecules)
             else:
-                self.species = list(self.experiment.species)
-
-        return self.update_db_entry_with_kwargs(
+                species = list(self.experiment.species)
+        # set args that will affect the computation result
+        self.args = Args(
             data_range=data_range,
             correlation_time=correlation_time,
-            molecules=molecules,
             atom_selection=atom_selection,
             tau_values=tau_values,
+            molecules=molecules,
             species=species,
         )
+        self.gpu = gpu
+        self.plot = plot
+        self.system_property = False
+        self.time = self._handle_tau_values()
+        self.msd_array = np.zeros(self.data_resolution)  # define empty msd array
 
-    def _update_output_signatures(self):
+    def check_input(self):
         """
-        After having run _prepare managers, update the output signatures.
+        Check the user input to ensure no conflicts are present.
 
         Returns
         -------
-        Update the class state.
+
         """
-        self.batch_output_signature = tf.TensorSpec(
-            shape=(None, self.batch_size, 3), dtype=tf.float64
-        )
-        self.ensemble_output_signature = tf.TensorSpec(
-            shape=(None, self.data_range, 3), dtype=tf.float64
-        )
+        self._run_dependency_check()
 
-    def _calculate_prefactor(self, species: str = None):
+    def calculate_prefactor(self, species: str = None):
         """
         Compute the prefactor
 
@@ -220,7 +182,7 @@ def _calculate_prefactor(self, species: str = None):
         -------
         Updates the class state.
         """
-        if self.molecules:
+        if self.args.molecules:
             # Calculate the prefactor
             numerator = self.experiment.units["length"] ** 2
             denominator = (
@@ -237,43 +199,67 @@ def _calculate_prefactor(self, species: str = None):
 
         self.prefactor = numerator / denominator
 
-    def _apply_averaging_factor(self):
+    def msd_operation(self, ensemble: tf.Tensor, square: bool = True):
         """
-        Apply the averaging factor to the msd array.
+        Perform a simple msd operation.
+
+        Parameters
+        ----------
+        ensemble : tf.Tensor
+            Trajectory over which to compute the msd.
+        square : bool
+            If true, square the result, else just return the difference.
         Returns
         -------
-
+        msd : tf.Tensor
+                Mean square displacement.
         """
-        self.msd_array /= int(self.n_batches) * self.ensemble_loop
+        if square:
+            return tf.math.squared_difference(
+                tf.gather(ensemble, self.args.tau_values, axis=1), ensemble[:, None, 0]
+            )
+        else:
+            return tf.math.subtract(ensemble, ensemble[:, None, 0])
 
-    def _apply_operation(self, ensemble, index):
+    def ensemble_operation(self, ensemble):
         """
         Calculate and return the msd.
 
         Parameters
         ----------
-        ensemble
+        ensemble : tf.Tensor
+                An ensemble of data to be operated on.
 
         Returns
         -------
         MSD of the tensor_values.
         """
-        msd = self._msd_operation(ensemble)
+        msd = self.msd_operation(ensemble)
 
         # Sum over trajectory and then coordinates and apply averaging and pre-factors
         msd = self.prefactor * tf.reduce_sum(tf.reduce_sum(msd, axis=0), axis=1)
         self.msd_array += np.array(msd)  # Update the averaged function
 
-    def _post_operation_processes(self, species: str = None):
+    def postprocessing(self, species: str):
         """
-        Apply post-op processes such as saving and plotting.
+        Run post-processing on the data.
+
+        This will include an averaging factor, saving the results, and plotting
+        the msd against time.
+
+        Parameters
+        ----------
+        species : str
+                Name of the species being studied.
 
         Returns
         -------
-
+        This method will scale the data by the number of ensembles, compute the
+        diffusion coefficient, and store all of the results in the SQL database.
         """
+        self.msd_array /= int(self.n_batches) * self.ensemble_loop
 
-        result = self._fit_einstein_curve([self.time, self.msd_array])
+        result = fit_einstein_curve([self.time, self.msd_array])
         log.debug(f"Saving {species}")
 
         data = {
@@ -284,3 +270,34 @@ def _post_operation_processes(self, species: str = None):
         }
 
         self.queue_data(data=data, subjects=[species])
+
+    def run_calculator(self):
+        """
+        Run analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        # Loop over species
+        for species in self.args.species:
+            dict_ref = str.encode("/".join([species, self.loaded_property[0]]))
+            self.calculate_prefactor(species)
+
+            batch_ds = self.get_batch_dataset([species])
+
+            for batch in tqdm(
+                batch_ds,
+                ncols=70,
+                desc=species,
+                total=self.n_batches,
+                disable=self.memory_manager.minibatch,
+            ):
+                ensemble_ds = self.get_ensemble_dataset(batch, species)
+
+                for ensemble in ensemble_ds:
+                    self.ensemble_operation(ensemble[dict_ref])
+
+            # Scale, save, and plot the data.
+            self.postprocessing(species)
diff --git a/mdsuite/calculators/einstein_helfand_ionic_conductivity.py b/mdsuite/calculators/einstein_helfand_ionic_conductivity.py
index 37ce7365..5f062cd5 100644
--- a/mdsuite/calculators/einstein_helfand_ionic_conductivity.py
+++ b/mdsuite/calculators/einstein_helfand_ionic_conductivity.py
@@ -23,35 +23,37 @@
 
 Summary
 -------
+MDSuite module for the computation of ionic conductivity using the Einstein method.
 """
-import warnings
+from abc import ABC
+
 import numpy as np
 import tensorflow as tf
 from tqdm import tqdm
-from mdsuite.calculators.calculator import Calculator, call
+from mdsuite.calculators.calculator import call
 from mdsuite.utils.units import elementary_charge, boltzmann_constant
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+from mdsuite.calculators import TrajectoryCalculator
+from mdsuite.utils.calculator_helper_methods import fit_einstein_curve
+
+
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
 
-tqdm.monitor_interval = 0
-warnings.filterwarnings("ignore")
+    data_range: int
+    correlation_time: int
+    tau_values: np.s_
+    atom_selection: np.s_
 
 
-class EinsteinHelfandIonicConductivity(Calculator):
+class EinsteinHelfandIonicConductivity(TrajectoryCalculator, ABC):
     """
     Class for the Einstein-Helfand Ionic Conductivity
 
-    Attributes
-    ----------
-    experiment :  object
-            Experiment class to call from
-    x_label : str
-            X label of the tensor_values when plotted
-    y_label : str
-            Y label of the tensor_values when plotted
-    analysis_name : str
-            Name of the analysis
-    loaded_property : str
-            Property loaded from the database_path for the analysis
-
     See Also
     --------
     mdsuite.calculators.calculator.Calculator class
@@ -77,29 +79,26 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
         self.scale_function = {"linear": {"scale_factor": 5}}
 
-        self.loaded_property = (  # Property to be loaded for the analysis
-            "Translational_Dipole_Moment"
-        )
-        self.dependency = "Unwrapped_Positions"
+        self.loaded_property = simulation_properties.translational_dipole_moment
+        self.dependency = simulation_properties.unwrapped_positions
         self.system_property = True
 
-        self.database_group = "Ionic_Conductivity"
         self.x_label = r"$$\text{Time} / s$$"
         self.y_label = r"$$\text{MSD} / m^2/s$$"
         self.analysis_name = "Einstein Helfand Ionic Conductivity"
-        self.prefactor: float
 
         self.result_keys = ["ionic_conductivity", "uncertainty"]
         self.result_series_keys = ["time", "msd"]
 
+        self._dtype = tf.float64
+
     @call
     def __call__(
         self,
         plot=True,
         data_range=500,
-        save=True,
         correlation_time=1,
-        export: bool = False,
+        tau_values: np.s_ = np.s_[:],
         gpu: bool = False,
     ):
         """
@@ -111,54 +110,40 @@ def __call__(
                 if true, plot the tensor_values
         data_range :
                 Number of configurations to use in each ensemble
-        save :
-                If true, tensor_values will be saved after the analysis
         correlation_time : int
                 Correlation time to use in the analysis.
-        export : bool
-                If true, export the results to a csv.
         gpu : bool
                 If true, reduce memory usage to the maximum GPU capability.
 
         """
-        # parse to the experiment class
-        self.update_user_args(
-            plot=plot,
+
+        # set args that will affect the computation result
+        self.args = Args(
             data_range=data_range,
-            save=save,
             correlation_time=correlation_time,
-            export=export,
-            gpu=gpu,
+            tau_values=tau_values,
+            atom_selection=np.s_[:],
         )
-        self.msd_array = np.zeros(self.data_range)
 
-        return self.update_db_entry_with_kwargs(
-            data_range=data_range, correlation_time=correlation_time
-        )
+        self.gpu = gpu
+        self.plot = plot
+        self.time = self._handle_tau_values()
+        self.msd_array = np.zeros(self.data_resolution)
 
-    def _update_output_signatures(self):
+    def check_input(self):
         """
-        Update the output signature for the IC.
+        Check the user input to ensure no conflicts are present.
 
         Returns
         -------
 
         """
-        self.batch_output_signature = tf.TensorSpec(
-            shape=(self.batch_size, 3), dtype=tf.float64
-        )
-        self.ensemble_output_signature = tf.TensorSpec(
-            shape=(self.data_range, 3), dtype=tf.float64
-        )
+        self._run_dependency_check()
 
-    def _calculate_prefactor(self, species: str = None):
+    def _calculate_prefactor(self):
         """
         Compute the ionic conductivity prefactor.
 
-        Parameters
-        ----------
-        species
-
         Returns
         -------
 
@@ -183,7 +168,7 @@ def _apply_averaging_factor(self):
         """
         self.msd_array /= int(self.n_batches) * self.ensemble_loop
 
-    def _apply_operation(self, ensemble, index):
+    def ensemble_operation(self, ensemble):
         """
         Calculate and return the msd.
 
@@ -195,18 +180,20 @@ def _apply_operation(self, ensemble, index):
         -------
         MSD of the tensor_values.
         """
-        msd = tf.math.squared_difference(ensemble, ensemble[None, 0])
+        msd = tf.math.squared_difference(
+            tf.gather(ensemble, self.args.tau_values, axis=0), ensemble[None, 0]
+        )
         msd = self.prefactor * tf.reduce_sum(msd, axis=1)
         self.msd_array += np.array(msd)  # Update the averaged function
 
-    def _post_operation_processes(self, species: str = None):
+    def _post_operation_processes(self):
         """
         call the post-op processes
         Returns
         -------
 
         """
-        result = self._fit_einstein_curve([self.time, self.msd_array])
+        result = fit_einstein_curve([self.time, self.msd_array])
 
         data = {
             self.result_keys[0]: result[0].tolist(),
@@ -216,3 +203,37 @@ def _post_operation_processes(self, species: str = None):
         }
 
         self.queue_data(data=data, subjects=["System"])
+
+    def run_calculator(self):
+        """
+
+        Run analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        # Compute the pre-factor early.
+        self._calculate_prefactor()
+
+        dict_ref = str.encode(
+            "/".join([self.loaded_property[0], self.loaded_property[0]])
+        )
+
+        batch_ds = self.get_batch_dataset([self.loaded_property[0]])
+
+        for batch in tqdm(
+            batch_ds,
+            ncols=70,
+            total=self.n_batches,
+            disable=self.memory_manager.minibatch,
+        ):
+            ensemble_ds = self.get_ensemble_dataset(batch, self.loaded_property[0])
+
+            for ensemble in ensemble_ds:
+                self.ensemble_operation(ensemble[dict_ref])
+
+        # Scale, save, and plot the data.
+        self._apply_averaging_factor()
+        self._post_operation_processes()
diff --git a/mdsuite/calculators/einstein_helfand_thermal_conductivity.py b/mdsuite/calculators/einstein_helfand_thermal_conductivity.py
index d2759443..824b0524 100644
--- a/mdsuite/calculators/einstein_helfand_thermal_conductivity.py
+++ b/mdsuite/calculators/einstein_helfand_thermal_conductivity.py
@@ -23,19 +23,33 @@
 
 Summary
 -------
+MDSuite module for the computation of thermal conductivity using the Einstein method.
 """
-import warnings
+from abc import ABC
+
 import numpy as np
 import tensorflow as tf
 from tqdm import tqdm
-from mdsuite.calculators.calculator import Calculator, call
+from mdsuite.calculators.calculator import call
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+from mdsuite.calculators import TrajectoryCalculator
+from mdsuite.utils.calculator_helper_methods import fit_einstein_curve
+
 
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
 
-tqdm.monitor_interval = 0
-warnings.filterwarnings("ignore")
+    data_range: int
+    correlation_time: int
+    tau_values: np.s_
+    atom_selection: np.s_
 
 
-class EinsteinHelfandThermalConductivity(Calculator):
+class EinsteinHelfandThermalConductivity(TrajectoryCalculator, ABC):
     """
     Class for the Einstein-Helfand Ionic Conductivity
 
@@ -77,29 +91,24 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
         self.scale_function = {"linear": {"scale_factor": 5}}
 
-        self.loaded_property = (  # Property to be loaded for the analysis
-            "Integrated_Heat_Current"
-        )
-        self.dependency = "Unwrapped_Positions"
+        self.loaded_property = simulation_properties.integrated_heat_current
+        self.dependency = simulation_properties.unwrapped_positions
         self.system_property = True
 
         self.x_label = r"$$\text{Time} / s$$"
         self.y_label = r"$$\text{MSD}  / m^2/s$$"
         self.analysis_name = "Einstein Helfand Thermal Conductivity"
+        self._dtype = tf.float64
 
-        # Which database_path group to save the tensor_values in
-        self.database_group = "Thermal_Conductivity"
-
-        self.prefactor: float
+        self.prefactor = None
 
     @call
     def __call__(
         self,
         plot=True,
         data_range=500,
-        save=True,
         correlation_time=1,
-        export: bool = False,
+        tau_values: np.s_ = np.s_[:],
         gpu: bool = False,
     ):
         """
@@ -111,52 +120,39 @@ def __call__(
                 if true, plot the output.
         data_range : int
                 Data range to use in the analysis.
-        save : bool
-                if true, save the output.
         correlation_time : int
                 Correlation time to use in the window sampling.
-        export : bool
-                If true, export the data directly into a csv file.
         gpu : bool
                 If true, scale the memory requirement down to the amount of
                 the biggest GPU in the system.
         """
-
-        # parse to the experiment class
-        self.update_user_args(
-            plot=plot,
+        # set args that will affect the computation result
+        self.args = Args(
             data_range=data_range,
-            save=save,
             correlation_time=correlation_time,
-            export=export,
-            gpu=gpu,
+            tau_values=tau_values,
+            atom_selection=np.s_[:],
         )
 
-        self.msd_array = np.zeros(self.data_range)  # Initialize the msd array
+        self.gpu = gpu
+        self.plot = plot
+        self.time = self._handle_tau_values()
+        self.msd_array = np.zeros(self.data_resolution)
 
-    def _update_output_signatures(self):
+    def check_input(self):
         """
-        Update the output signature for the IC.
+        Check the user input to ensure no conflicts are present.
 
         Returns
         -------
 
         """
-        self.batch_output_signature = tf.TensorSpec(
-            shape=(self.batch_size, 3), dtype=tf.float64
-        )
-        self.ensemble_output_signature = tf.TensorSpec(
-            shape=(self.data_range, 3), dtype=tf.float64
-        )
+        self._run_dependency_check()
 
-    def _calculate_prefactor(self, species: str = None):
+    def _calculate_prefactor(self):
         """
         Compute the ionic conductivity prefactor.
 
-        Parameters
-        ----------
-        species
-
         Returns
         -------
 
@@ -167,7 +163,7 @@ def _calculate_prefactor(self, species: str = None):
             6
             * self.experiment.volume
             * self.experiment.temperature
-            * self.experiment.units["boltzman"]
+            * self.experiment.units["boltzmann"]
         )
         units_change = (
             self.experiment.units["energy"]
@@ -186,7 +182,7 @@ def _apply_averaging_factor(self):
         """
         self.msd_array /= int(self.n_batches) * self.ensemble_loop
 
-    def _apply_operation(self, ensemble, index):
+    def ensemble_operation(self, ensemble):
         """
         Calculate and return the msd.
 
@@ -203,14 +199,14 @@ def _apply_operation(self, ensemble, index):
         msd = self.prefactor * tf.reduce_sum(msd, axis=1)
         self.msd_array += np.array(msd)  # Update the averaged function
 
-    def _post_operation_processes(self, species: str = None):
+    def _post_operation_processes(self):
         """
         call the post-op processes
         Returns
         -------
 
         """
-        result = self._fit_einstein_curve([self.time, self.msd_array])
+        result = fit_einstein_curve([self.time, self.msd_array])
 
         data = {
             "thermal_conductivity": result[0],
@@ -227,3 +223,36 @@ def _post_operation_processes(self, species: str = None):
                 y_data=self.msd_array * self.experiment.units["time"],
                 title=f"{result[0]} += {result[1]}",
             )
+
+    def run_calculator(self):
+        """
+        Run analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        # Compute the pre-factor early.
+        self._calculate_prefactor()
+
+        dict_ref = str.encode(
+            "/".join([self.loaded_property[0], self.loaded_property[0]])
+        )
+
+        batch_ds = self.get_batch_dataset([self.loaded_property[0]])
+
+        for batch in tqdm(
+            batch_ds,
+            ncols=70,
+            total=self.n_batches,
+            disable=self.memory_manager.minibatch,
+        ):
+            ensemble_ds = self.get_ensemble_dataset(batch, self.loaded_property[0])
+
+            for ensemble in ensemble_ds:
+                self.ensemble_operation(ensemble[dict_ref])
+
+        # Scale, save, and plot the data.
+        self._apply_averaging_factor()
+        self._post_operation_processes()
diff --git a/mdsuite/calculators/einstein_helfand_thermal_kinaci.py b/mdsuite/calculators/einstein_helfand_thermal_kinaci.py
index 2b40fc5f..7d4a91f6 100644
--- a/mdsuite/calculators/einstein_helfand_thermal_kinaci.py
+++ b/mdsuite/calculators/einstein_helfand_thermal_kinaci.py
@@ -23,20 +23,34 @@
 
 Summary
 -------
+MDSuite module for the computation of the thermal conductivity in solids using the
+Einstein method as applied to the Kinaci integrated thermal flux.
 """
-import warnings
+from abc import ABC
+
 import numpy as np
 import tensorflow as tf
 from tqdm import tqdm
+from mdsuite.calculators.calculator import call
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+from mdsuite.calculators import TrajectoryCalculator
+from mdsuite.utils.calculator_helper_methods import fit_einstein_curve
 
-from mdsuite.calculators.calculator import Calculator, call
 
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
 
-tqdm.monitor_interval = 0
-warnings.filterwarnings("ignore")
+    data_range: int
+    correlation_time: int
+    tau_values: np.s_
+    atom_selection: np.s_
 
 
-class EinsteinHelfandThermalKinaci(Calculator):
+class EinsteinHelfandThermalKinaci(TrajectoryCalculator, ABC):
     """
     Class for the Einstein-Helfand Ionic Conductivity
 
@@ -79,28 +93,25 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
         self.scale_function = {"linear": {"scale_factor": 5}}
 
-        self.loaded_property = (  # Property to be loaded for the analysis
-            "Kinaci_Heat_Current"
-        )
-        self.dependency = "Unwrapped_Positions"
+        self.loaded_property = simulation_properties.kinaci_heat_current
+        self.dependency = simulation_properties.unwrapped_positions
         self.system_property = True
 
         self.x_label = r"$$\text{Time} / s)$$"
         self.y_label = r"$$\text{MSD} / m^2/s$$"
         self.analysis_name = "Einstein Helfand Thermal Conductivity Kinaci"
 
-        self.database_group = "Thermal_Conductivity"
+        self._dtype = tf.float64
 
-        self.prefactor: float
+        self.prefactor = None
 
     @call
     def __call__(
         self,
         plot=True,
         data_range=500,
-        save=True,
         correlation_time=1,
-        export: bool = False,
+        tau_values: np.s_ = np.s_[:],
         gpu: bool = False,
     ):
         """
@@ -112,50 +123,40 @@ def __call__(
                 if true, plot the output.
         data_range : int
                 Data range to use in the analysis.
-        save : bool
-                if true, save the output.
+
         correlation_time : int
                 Correlation time to use in the window sampling.
-        export : bool
-                If true, export the data directly into a csv file.
         gpu : bool
                 If true, scale the memory requirement down to the amount of
                 the biggest GPU in the system.
         """
-        # parse to the experiment class
-        self.update_user_args(
-            plot=plot,
+        # set args that will affect the computation result
+        self.args = Args(
             data_range=data_range,
-            save=save,
             correlation_time=correlation_time,
-            export=export,
-            gpu=gpu,
+            tau_values=tau_values,
+            atom_selection=np.s_[:],
         )
-        self.msd_array = np.zeros(self.data_range)  # Initialize the msd array
 
-    def _update_output_signatures(self):
+        self.plot = plot
+        self.gpu = gpu
+        self.time = self._handle_tau_values()
+        self.msd_array = np.zeros(self.data_resolution)
+
+    def check_input(self):
         """
-        Update the output signature for the IC.
+        Check the user input to ensure no conflicts are present.
 
         Returns
         -------
 
         """
-        self.batch_output_signature = tf.TensorSpec(
-            shape=(self.batch_size, 3), dtype=tf.float64
-        )
-        self.ensemble_output_signature = tf.TensorSpec(
-            shape=(self.data_range, 3), dtype=tf.float64
-        )
+        self._run_dependency_check()
 
-    def _calculate_prefactor(self, species: str = None):
+    def _calculate_prefactor(self):
         """
         Compute the ionic conductivity prefactor.
 
-        Parameters
-        ----------
-        species
-
         Returns
         -------
 
@@ -166,7 +167,7 @@ def _calculate_prefactor(self, species: str = None):
             6
             * self.experiment.volume
             * self.experiment.temperature
-            * self.experiment.units["boltzman"]
+            * self.experiment.units["boltzmann"]
         )
         units_change = (
             self.experiment.units["energy"]
@@ -185,7 +186,7 @@ def _apply_averaging_factor(self):
         """
         self.msd_array /= int(self.n_batches) * self.ensemble_loop
 
-    def _apply_operation(self, ensemble, index):
+    def ensemble_operation(self, ensemble):
         """
         Calculate and return the msd.
 
@@ -202,14 +203,14 @@ def _apply_operation(self, ensemble, index):
         msd = self.prefactor * tf.reduce_sum(msd, axis=1)
         self.msd_array += np.array(msd)  # Update the averaged function
 
-    def _post_operation_processes(self, species: str = None):
+    def _post_operation_processes(self):
         """
         call the post-op processes
         Returns
         -------
 
         """
-        result = self._fit_einstein_curve([self.time, self.msd_array])
+        result = fit_einstein_curve([self.time, self.msd_array])
 
         data = {
             "thermal_conductivity": result[0],
@@ -227,3 +228,36 @@ def _post_operation_processes(self, species: str = None):
                 y_data=self.msd_array * self.experiment.units["time"],
                 title=f"{result[0]} +- {result[1]}",
             )
+
+    def run_calculator(self):
+        """
+        Run analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        # Compute the pre-factor early.
+        self._calculate_prefactor()
+
+        dict_ref = str.encode(
+            "/".join([self.loaded_property[0], self.loaded_property[0]])
+        )
+
+        batch_ds = self.get_batch_dataset([self.loaded_property[0]])
+
+        for batch in tqdm(
+            batch_ds,
+            ncols=70,
+            total=self.n_batches,
+            disable=self.memory_manager.minibatch,
+        ):
+            ensemble_ds = self.get_ensemble_dataset(batch, self.loaded_property[0])
+
+            for ensemble in ensemble_ds:
+                self.ensemble_operation(ensemble[dict_ref])
+
+        # Scale, save, and plot the data.
+        self._apply_averaging_factor()
+        self._post_operation_processes()
diff --git a/mdsuite/calculators/green_kubo_ionic_conductivity.py b/mdsuite/calculators/green_kubo_ionic_conductivity.py
index f70bf2d5..7407c6ed 100644
--- a/mdsuite/calculators/green_kubo_ionic_conductivity.py
+++ b/mdsuite/calculators/green_kubo_ionic_conductivity.py
@@ -23,17 +23,39 @@
 
 Summary
 -------
+MDSuite module for the computation of the ionic conductivity of a system using the
+Green-Kubo relation. Ionic conductivity describes how well a system can conduct an
+electrical charge due to the mobility of the ions contained within it. This differs
+from electronic conductivity which is transferred by electrons.
 """
+from abc import ABC
+
 import numpy as np
-import tensorflow as tf
 import tensorflow_probability as tfp
+import tensorflow as tf
 from mdsuite.utils.units import boltzmann_constant, elementary_charge
-from mdsuite.calculators.calculator import Calculator, call
+from mdsuite.calculators.calculator import call
+from mdsuite.calculators import TrajectoryCalculator
 from bokeh.models import Span
-from mdsuite.database.scheme import Computation
+from tqdm import tqdm
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
 
 
-class GreenKuboIonicConductivity(Calculator):
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
+
+    data_range: int
+    correlation_time: int
+    tau_values: np.s_
+    atom_selection: np.s_
+    integration_range: int
+
+
+class GreenKuboIonicConductivity(TrajectoryCalculator, ABC):
     """
     Class for the Green-Kubo ionic conductivity implementation
 
@@ -73,10 +95,9 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
         self.scale_function = {"linear": {"scale_factor": 5}}
 
-        self.loaded_property = "Ionic_Current"
+        self.loaded_property = simulation_properties.ionic_current
         self.system_property = True
 
-        self.database_group = "Ionic_Conductivity"
         self.x_label = r"$$\text{Time} / s"
         self.y_label = r"$$\text{JACF} / C^{2}\cdot m^{2}/s^{2}$$"
         self.analysis_name = "Green_Kubo_Ionic_Conductivity"
@@ -84,19 +105,19 @@ def __init__(self, **kwargs):
         self.result_keys = ["ionic_conductivity", "uncertainty"]
         self.result_series_keys = ["time", "acf"]
 
-        self.prefactor: float
+        self.prefactor = None
+        self._dtype = tf.float64
 
     @call
     def __call__(
         self,
         plot=True,
         data_range=500,
-        save=True,
         correlation_time=1,
-        export: bool = False,
+        tau_values: np.s_ = np.s_[:],
         gpu: bool = False,
         integration_range: int = None,
-    ) -> Computation:
+    ):
         """
 
         Parameters
@@ -105,12 +126,8 @@ def __call__(
                 if true, plot the output.
         data_range : int
                 Data range to use in the analysis.
-        save : bool
-                if true, save the output.
         correlation_time : int
                 Correlation time to use in the window sampling.
-        export : bool
-                If true, export the data directly into a csv file.
         gpu : bool
                 If true, scale the memory requirement down to the amount of
                 the biggest GPU in the system.
@@ -118,50 +135,40 @@ def __call__(
                 Range over which integration should be performed.
         """
 
-        # update experiment class
-        self.update_user_args(
-            plot=plot,
-            data_range=data_range,
-            save=save,
-            correlation_time=correlation_time,
-            export=export,
-            gpu=gpu,
-        )
-        self.jacf = np.zeros(self.data_range)
+        self.gpu = gpu
+        self.plot = plot
+        self.jacf: np.ndarray
         self.sigma = []
 
         if integration_range is None:
-            self.integration_range = self.data_range
-        else:
-            self.integration_range = integration_range
+            integration_range = data_range
 
-        return self.update_db_entry_with_kwargs(
-            data_range=data_range, correlation_time=correlation_time
+        # set args that will affect the computation result
+        self.args = Args(
+            data_range=data_range,
+            correlation_time=correlation_time,
+            tau_values=tau_values,
+            atom_selection=np.s_[:],
+            integration_range=integration_range,
         )
 
-    def _update_output_signatures(self):
+        self.time = self._handle_tau_values()
+        self.jacf = np.zeros(self.data_resolution)
+
+    def check_input(self):
         """
-        Update the output signature for the IC.
+        Check the user input to ensure no conflicts are present.
 
         Returns
         -------
 
         """
-        self.batch_output_signature = tf.TensorSpec(
-            shape=(self.batch_size, 3), dtype=tf.float64
-        )
-        self.ensemble_output_signature = tf.TensorSpec(
-            shape=(self.data_range, 3), dtype=tf.float64
-        )
+        self._run_dependency_check()
 
-    def _calculate_prefactor(self, species: str = None):
+    def _calculate_prefactor(self):
         """
         Compute the ionic conductivity prefactor.
 
-        Parameters
-        ----------
-        species
-
         Returns
         -------
 
@@ -174,7 +181,7 @@ def _calculate_prefactor(self, species: str = None):
             * self.experiment.temperature
             * self.experiment.volume
             * (self.experiment.units["length"] ** 3)
-            * self.data_range
+            * self.args.data_range
             * self.experiment.units["time"]
         )
         self.prefactor = numerator / denominator
@@ -188,7 +195,7 @@ def _apply_averaging_factor(self):
         """
         pass
 
-    def _apply_operation(self, ensemble, index):
+    def ensemble_operation(self, ensemble):
         """
         Calculate and return the msd.
 
@@ -200,18 +207,19 @@ def _apply_operation(self, ensemble, index):
         -------
         MSD of the tensor_values.
         """
-        jacf = self.data_range * tf.reduce_sum(
+        jacf = self.args.data_range * tf.reduce_sum(
             tfp.stats.auto_correlation(ensemble, normalize=False, axis=0, center=False),
             axis=-1,
         )
         self.jacf += jacf
         self.sigma.append(
             np.trapz(
-                jacf[: self.integration_range], x=self.time[: self.integration_range]
+                jacf[: self.args.integration_range],
+                x=self.time[: self.args.integration_range],
             )
         )
 
-    def _post_operation_processes(self, species: str = None):
+    def _post_operation_processes(self):
         """
         call the post-op processes
         Returns
@@ -236,7 +244,7 @@ def plot_data(self, data):
                 location=(
                     np.array(val[self.result_series_keys[0]])
                     * self.experiment.units["time"]
-                )[self.integration_range - 1],
+                )[self.args.integration_range - 1],
                 dimension="height",
                 line_dash="dashed",
             )
@@ -250,3 +258,36 @@ def plot_data(self, data):
                 ),
                 layouts=[span],
             )
+
+    def run_calculator(self):
+        """
+        Run analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        # Compute the pre-factor early.
+        self._calculate_prefactor()
+
+        dict_ref = str.encode(
+            "/".join([self.loaded_property[0], self.loaded_property[0]])
+        )
+
+        batch_ds = self.get_batch_dataset([self.loaded_property[0]])
+
+        for batch in tqdm(
+            batch_ds,
+            ncols=70,
+            total=self.n_batches,
+            disable=self.memory_manager.minibatch,
+        ):
+            ensemble_ds = self.get_ensemble_dataset(batch, self.loaded_property[0])
+
+            for ensemble in ensemble_ds:
+                self.ensemble_operation(ensemble[dict_ref])
+
+        # Scale, save, and plot the data.
+        self._apply_averaging_factor()
+        self._post_operation_processes()
diff --git a/mdsuite/calculators/green_kubo_self_diffusion_coefficients.py b/mdsuite/calculators/green_kubo_self_diffusion_coefficients.py
index ca40dee9..76ef7848 100644
--- a/mdsuite/calculators/green_kubo_self_diffusion_coefficients.py
+++ b/mdsuite/calculators/green_kubo_self_diffusion_coefficients.py
@@ -23,16 +23,39 @@
 
 Summary
 -------
+Module for the computation of diffusion coefficients using the Green-Kubo approach.
 """
+from abc import ABC
+
 import numpy as np
 import tensorflow_probability as tfp
 import tensorflow as tf
 from bokeh.models import Span
-from mdsuite.calculators.calculator import Calculator, call
-from mdsuite.database.scheme import Computation
+from tqdm import tqdm
+from typing import Union
+from mdsuite.calculators.calculator import call
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+from typing import List, Any
+from mdsuite.calculators import TrajectoryCalculator
+
+
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
 
+    data_range: int
+    correlation_time: int
+    atom_selection: np.s_
+    tau_values: np.s_
+    molecules: bool
+    species: list
+    integration_range: int
 
-class GreenKuboDiffusionCoefficients(Calculator):
+
+class GreenKuboDiffusionCoefficients(TrajectoryCalculator, ABC):
     """
     Class for the Green-Kubo diffusion coefficient implementation
     Attributes
@@ -72,31 +95,30 @@ def __init__(self, **kwargs):
         """
         super().__init__(**kwargs)
 
-        self.loaded_property = "Velocities"
+        self.loaded_property = simulation_properties.velocities
         self.scale_function = {"linear": {"scale_factor": 150}}
 
-        self.database_group = "Diffusion_Coefficients"
         self.x_label = r"$$\text{Time} / s$$"
         self.y_label = r"$$\text{VACF} / m^{2}/s^{2}$$"
         self.analysis_name = "Green Kubo Self-Diffusion Coefficients"
-
         self.result_keys = ["diffusion_coefficient", "uncertainty"]
         self.result_series_keys = ["time", "acf"]
 
+        self._dtype = tf.float64
+
     @call
     def __call__(
         self,
         plot: bool = True,
         species: list = None,
         data_range: int = 500,
-        save: bool = True,
         correlation_time: int = 1,
         atom_selection=np.s_[:],
-        export: bool = False,
         molecules: bool = False,
         gpu: bool = False,
+        tau_values: Union[int, List, Any] = np.s_[:],
         integration_range: int = None,
-    ) -> Computation:
+    ):
         """
         Constructor for the Green-Kubo diffusion coefficients class.
 
@@ -108,14 +130,10 @@ def __call__(
                 List of species on which to operate.
         data_range : int
                 Data range to use in the analysis.
-        save : bool
-                if true, save the output.
         correlation_time : int
                 Correlation time to use in the window sampling.
         atom_selection : np.s_
                 Selection of atoms to use within the HDF5 database.
-        export : bool
-                If true, export the data directly into a csv file.
         molecules : bool
                 If true, molecules are used instead of atoms.
         gpu : bool
@@ -125,57 +143,40 @@ def __call__(
                 Range over which to integrate. Default is to integrate over
                 the full data range.
         """
-        self.update_user_args(
-            plot=plot,
-            data_range=data_range,
-            save=save,
-            correlation_time=correlation_time,
-            atom_selection=atom_selection,
-            export=export,
-            gpu=gpu,
-        )
-
-        self.molecules = molecules
-        self.species = species  # Which species to calculate for
-
-        self.vacf = np.zeros(self.data_range)
-        self.sigma = []
-
-        if integration_range is None:
-            self.integration_range = self.data_range
-        else:
-            self.integration_range = integration_range
-
         if species is None:
             if molecules:
-                self.species = list(self.experiment.molecules)
+                species = list(self.experiment.molecules)
             else:
-                self.species = list(self.experiment.species)
+                species = list(self.experiment.species)
+        if integration_range is None:
+            integration_range = data_range
 
-        return self.update_db_entry_with_kwargs(
-            species=species,
+        # set args that will affect the computation result
+        self.args = Args(
             data_range=data_range,
             correlation_time=correlation_time,
             atom_selection=atom_selection,
+            tau_values=tau_values,
+            molecules=molecules,
+            species=species,
+            integration_range=integration_range,
         )
 
-    def _update_output_signatures(self):
+        self.gpu = gpu
+        self.plot = plot
+        self.time = self._handle_tau_values()
+        self.vacf = np.zeros(self.data_resolution)
+        self.sigma = []
+
+    def check_input(self):
         """
-        After having run _prepare managers, update the output signatures.
+        Check the user input to ensure no conflicts are present.
 
         Returns
         -------
-        Update the class state.
-        """
-        # Update the batch update signature for database loading.
-        self.batch_output_signature = tf.TensorSpec(
-            shape=(None, self.batch_size, 3), dtype=tf.float64
-        )
 
-        # Update ensemble output signature for ensemble loading.
-        self.ensemble_output_signature = tf.TensorSpec(
-            shape=(None, self.data_range, 3), dtype=tf.float64
-        )
+        """
+        self._run_dependency_check()
 
     def _calculate_prefactor(self, species: str = None):
         """
@@ -191,12 +192,12 @@ def _calculate_prefactor(self, species: str = None):
         Updates the class state.
         """
         # Calculate the prefactor
-        if self.molecules:
+        if self.args.molecules:
             numerator = self.experiment.units["length"] ** 2
             denominator = (
                 3
                 * self.experiment.units["time"]
-                * (self.integration_range - 1)
+                * (self.args.integration_range - 1)
                 * len(self.experiment.molecules[species]["indices"])
             )
             self.prefactor = numerator / denominator
@@ -205,21 +206,12 @@ def _calculate_prefactor(self, species: str = None):
             denominator = (
                 3
                 * self.experiment.units["time"]
-                * self.integration_range
+                * self.args.integration_range
                 * len(self.experiment.species[species]["indices"])
             )
             self.prefactor = numerator / denominator
 
-    def _apply_averaging_factor(self):
-        """
-        Apply the averaging factor to the msd array.
-        Returns
-        -------
-
-        """
-        pass
-
-    def _apply_operation(self, ensemble, index):
+    def ensemble_operation(self, ensemble):
         """
         Calculate and return the msd.
 
@@ -231,7 +223,7 @@ def _apply_operation(self, ensemble, index):
         -------
         MSD of the tensor_values.
         """
-        vacf = self.data_range * tfp.stats.auto_correlation(
+        vacf = self.args.data_range * tfp.stats.auto_correlation(
             ensemble, normalize=False, axis=1, center=False
         )
 
@@ -239,37 +231,26 @@ def _apply_operation(self, ensemble, index):
         self.vacf += vacf
         self.sigma.append(
             np.trapz(
-                vacf[: self.integration_range], x=self.time[: self.integration_range]
+                vacf[: self.args.integration_range],
+                x=self.time[: self.args.integration_range],
             )
         )
 
-    def _post_operation_processes(self, species: str = None):
+    def plot_data(self, data: dict):
         """
-        Apply post-op processes such as saving and plotting.
-
-        Returns
-        -------
+        Plot the data
 
+        Parameters
+        ----------
+        data : dict
+                Data loaded from the sql database to be plotted.
         """
-        result = self.prefactor * np.array(self.sigma)
-
-        data = {
-            self.result_keys[0]: np.mean(result).tolist(),
-            self.result_keys[1]: (np.std(result) / np.sqrt(len(result))).tolist(),
-            self.result_series_keys[0]: self.time.tolist(),
-            self.result_series_keys[1]: self.vacf.numpy().tolist(),
-        }
-
-        self.queue_data(data=data, subjects=[species])
-
-    def plot_data(self, data):
-        """Plot the data"""
         for selected_species, val in data.items():
             span = Span(
                 location=(
                     np.array(val[self.result_series_keys[0]])
                     * self.experiment.units["time"]
-                )[self.integration_range - 1],
+                )[self.args.integration_range - 1],
                 dimension="height",
                 line_dash="dashed",
             )
@@ -283,3 +264,58 @@ def plot_data(self, data):
                 ),
                 layouts=[span],
             )
+
+    def postprocessing(self, species: str):
+        """
+        Apply post-processing to the data.
+
+        Parameters
+        ----------
+        species : str
+                Current species on which you are operating.
+
+        Returns
+        -------
+
+        """
+        result = self.prefactor * np.array(self.sigma)
+
+        data = {
+            self.result_keys[0]: np.mean(result).tolist(),
+            self.result_keys[1]: (np.std(result) / np.sqrt(len(result))).tolist(),
+            self.result_series_keys[0]: self.time.tolist(),
+            self.result_series_keys[1]: self.vacf.numpy().tolist(),
+        }
+
+        self.queue_data(data=data, subjects=[species])
+
+    def run_calculator(self):
+        """
+        Run analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        # Loop over species
+        for species in self.args.species:
+            dict_ref = str.encode("/".join([species, self.loaded_property[0]]))
+            self._calculate_prefactor(species)
+
+            batch_ds = self.get_batch_dataset([species])
+
+            for batch in tqdm(
+                batch_ds,
+                ncols=70,
+                desc=species,
+                total=self.n_batches,
+                disable=self.memory_manager.minibatch,
+            ):
+                ensemble_ds = self.get_ensemble_dataset(batch, species)
+
+                for ensemble in ensemble_ds:
+                    self.ensemble_operation(ensemble[dict_ref])
+
+            # Scale, save, and plot the data.
+            self.postprocessing(species)
diff --git a/mdsuite/calculators/green_kubo_thermal_conductivity.py b/mdsuite/calculators/green_kubo_thermal_conductivity.py
index 7231b0b8..ac648532 100644
--- a/mdsuite/calculators/green_kubo_thermal_conductivity.py
+++ b/mdsuite/calculators/green_kubo_thermal_conductivity.py
@@ -23,20 +23,35 @@
 
 Summary
 -------
+MDSuite module for the computation of the thermal conductivity using the Green-Kubo
+relation.
 """
-import warnings
+from abc import ABC
+import tensorflow_probability as tfp
 import numpy as np
 import tensorflow as tf
 from tqdm import tqdm
-from mdsuite.calculators.calculator import Calculator, call
-import tensorflow_probability as tfp
+from mdsuite.calculators.calculator import call
+from mdsuite.calculators import TrajectoryCalculator
 from bokeh.models import Span
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+
+
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
 
-tqdm.monitor_interval = 0
-warnings.filterwarnings("ignore")
+    data_range: int
+    correlation_time: int
+    tau_values: np.s_
+    atom_selection: np.s_
+    integration_range: int
 
 
-class GreenKuboThermalConductivity(Calculator):
+class GreenKuboThermalConductivity(TrajectoryCalculator, ABC):
     """
     Class for the Green-Kubo Thermal conductivity implementation
 
@@ -73,20 +88,20 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
         self.scale_function = {"linear": {"scale_factor": 5}}
 
-        self.loaded_property = "Thermal_Flux"
-        self.database_group = "Thermal_Conductivity"
+        self.loaded_property = simulation_properties.thermal_flux
         self.system_property = True
 
         self.x_label = r"$$\text{Time} / s$$"
         self.y_label = r"$$\text{JACF} / ($C^{2}\cdot m^{2}/s^{2}$$"
         self.analysis_name = "Green_Kubo_Thermal_Conductivity"
+        self._dtype = tf.float64
 
     @call
     def __call__(
         self,
         plot=False,
         data_range=500,
-        save=True,
+        tau_values: np.s_ = np.s_[:],
         correlation_time: int = 1,
         gpu: bool = False,
         integration_range: int = None,
@@ -100,62 +115,49 @@ def __call__(
                 if true, plot the output.
         data_range : int
                 Data range to use in the analysis.
-        save : bool
-                if true, save the output.
         correlation_time : int
                 Correlation time to use in the window sampling.
-        export : bool
-                If true, export the data directly into a csv file.
         gpu : bool
                 If true, scale the memory requirement down to the amount of
                 the biggest GPU in the system.
         integration_range : int
                 Range over which the integration should be performed.
         """
-        self.update_user_args(
-            plot=plot,
-            data_range=data_range,
-            save=save,
-            correlation_time=correlation_time,
-            gpu=gpu,
-        )
-
-        self.jacf = np.zeros(self.data_range)
+        self.gpu = gpu
+        self.plot = plot
+        self.jacf: np.ndarray
         self.prefactor: float
         self.sigma = []
 
         if integration_range is None:
-            self.integration_range = self.data_range
-        else:
-            self.integration_range = integration_range
+            integration_range = data_range
 
-        return self.update_db_entry_with_kwargs(
-            data_range=data_range, correlation_time=correlation_time
+        # set args that will affect the computation result
+        self.args = Args(
+            data_range=data_range,
+            correlation_time=correlation_time,
+            tau_values=tau_values,
+            atom_selection=np.s_[:],
+            integration_range=integration_range,
         )
 
-    def _update_output_signatures(self):
+        self.time = self._handle_tau_values()
+        self.jacf = np.zeros(self.data_resolution)
+
+    def check_input(self):
         """
-        Update the output signature for the IC.
+        Check the user input to ensure no conflicts are present.
 
         Returns
         -------
 
         """
-        self.batch_output_signature = tf.TensorSpec(
-            shape=(self.batch_size, 3), dtype=tf.float64
-        )
-        self.ensemble_output_signature = tf.TensorSpec(
-            shape=(self.data_range, 3), dtype=tf.float64
-        )
+        self._run_dependency_check()
 
-    def _calculate_prefactor(self, species: str = None):
+    def _calculate_prefactor(self):
         """
         Compute the ionic conductivity pre-factor.
 
-        Parameters
-        ----------
-        species
-
         Returns
         -------
 
@@ -165,7 +167,7 @@ def _calculate_prefactor(self, species: str = None):
         numerator = 1
         denominator = (
             3
-            * (self.data_range - 1)
+            * (self.args.data_range - 1)
             * self.experiment.temperature ** 2
             * self.experiment.units["boltzman"]
             * self.experiment.volume
@@ -187,7 +189,7 @@ def _apply_averaging_factor(self):
         """
         pass
 
-    def _apply_operation(self, ensemble: tf.Tensor, index: int):
+    def ensemble_operation(self, ensemble: tf.Tensor):
         """
         Calculate and return the msd.
 
@@ -200,18 +202,19 @@ def _apply_operation(self, ensemble: tf.Tensor, index: int):
         -------
         MSD of the tensor_values.
         """
-        jacf = self.data_range * tf.reduce_sum(
+        jacf = self.args.data_range * tf.reduce_sum(
             tfp.stats.auto_correlation(ensemble, normalize=False, axis=0, center=False),
             axis=-1,
         )
         self.jacf += jacf
         self.sigma.append(
             np.trapz(
-                jacf[: self.integration_range], x=self.time[: self.integration_range]
+                jacf[: self.args.integration_range],
+                x=self.time[: self.args.integration_range],
             )
         )
 
-    def _post_operation_processes(self, species: str = None):
+    def _post_operation_processes(self):
         """
         call the post-op processes
 
@@ -234,7 +237,7 @@ def _post_operation_processes(self, species: str = None):
         if self.plot:
             span = Span(
                 location=(np.array(self.time) * self.experiment.units["time"])[
-                    self.integration_range - 1
+                    self.args.integration_range - 1
                 ],
                 dimension="height",
                 line_dash="dashed",
@@ -245,3 +248,36 @@ def _post_operation_processes(self, species: str = None):
                 title=f"{result[0]} +- {result[1]}",
                 layouts=[span],
             )
+
+    def run_calculator(self):
+        """
+        Run analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        # Compute the pre-factor early.
+        self._calculate_prefactor()
+
+        dict_ref = str.encode(
+            "/".join([self.loaded_property[0], self.loaded_property[0]])
+        )
+
+        batch_ds = self.get_batch_dataset([self.loaded_property[0]])
+
+        for batch in tqdm(
+            batch_ds,
+            ncols=70,
+            total=self.n_batches,
+            disable=self.memory_manager.minibatch,
+        ):
+            ensemble_ds = self.get_ensemble_dataset(batch, self.loaded_property[0])
+
+            for ensemble in ensemble_ds:
+                self.ensemble_operation(ensemble[dict_ref])
+
+        # Scale, save, and plot the data.
+        self._apply_averaging_factor()
+        self._post_operation_processes()
diff --git a/mdsuite/calculators/green_kubo_viscosity.py b/mdsuite/calculators/green_kubo_viscosity.py
index 33b8da6a..42dddc01 100644
--- a/mdsuite/calculators/green_kubo_viscosity.py
+++ b/mdsuite/calculators/green_kubo_viscosity.py
@@ -23,15 +23,35 @@
 
 Summary
 -------
+MDSuite module for the computation of the viscosity in a system using the Green-Kubo
+relation as applied to the momentum flux measured during a simulation.
 """
+from abc import ABC
+import tensorflow_probability as tfp
 import numpy as np
 import tensorflow as tf
-import tensorflow_probability as tfp
-from mdsuite.calculators.calculator import Calculator, call
+from tqdm import tqdm
+from mdsuite.calculators.calculator import call
+from mdsuite.calculators import TrajectoryCalculator
 from bokeh.models import Span
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+
 
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
 
-class GreenKuboViscosity(Calculator):
+    data_range: int
+    correlation_time: int
+    tau_values: np.s_
+    atom_selection: np.s_
+    integration_range: int
+
+
+class GreenKuboViscosity(TrajectoryCalculator, ABC):
     """Class for the Green-Kubo ionic conductivity implementation
 
     Attributes
@@ -68,21 +88,21 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
         self.scale_function = {"linear": {"scale_factor": 5}}
 
-        self.loaded_property = "Momentum_Flux"
-        self.database_group = "Viscosity"
+        self.loaded_property = simulation_properties.momentum_flux
         self.system_property = True
 
         self.x_label = r"$$\text{Time} / s$$"
         self.y_label = r"\text{SACF} / C^{2}\cdot m^{2}/s^{2}$$"
         self.analysis_name = "Green_Kubo_Viscosity"
-        self.prefactor: float
+        self.prefactor = None
+        self._dtype = tf.float64
 
     @call
     def __call__(
         self,
         plot=False,
         data_range=500,
-        save=True,
+        tau_values: np.s_ = np.s_[:],
         correlation_time: int = 1,
         gpu: bool = False,
         integration_range: int = None,
@@ -95,53 +115,41 @@ def __call__(
                 if true, plot the tensor_values
         data_range :
                 Number of configurations to use in each ensemble
-        save :
-                If true, tensor_values will be saved after the analysis
         """
 
-        self.update_user_args(
-            plot=plot,
-            data_range=data_range,
-            save=save,
-            correlation_time=correlation_time,
-            gpu=gpu,
-        )
-
-        self.jacf = np.zeros(self.data_range)
+        self.gpu = gpu
+        self.plot = plot
         self.sigma = []
 
         if integration_range is None:
-            self.integration_range = self.data_range
-        else:
-            self.integration_range = integration_range
+            integration_range = data_range
 
-        return self.update_db_entry_with_kwargs(
-            data_range=data_range, correlation_time=correlation_time
+        # set args that will affect the computation result
+        self.args = Args(
+            data_range=data_range,
+            correlation_time=correlation_time,
+            tau_values=tau_values,
+            atom_selection=np.s_[:],
+            integration_range=integration_range,
         )
 
-    def _update_output_signatures(self):
+        self.time = self._handle_tau_values()
+        self.jacf = np.zeros(self.data_resolution)
+
+    def check_input(self):
         """
-        Update the output signature for the IC.
+        Check the user input to ensure no conflicts are present.
 
         Returns
         -------
 
         """
-        self.batch_output_signature = tf.TensorSpec(
-            shape=(self.batch_size, 3), dtype=tf.float64
-        )
-        self.ensemble_output_signature = tf.TensorSpec(
-            shape=(self.data_range, 3), dtype=tf.float64
-        )
+        self._run_dependency_check()
 
-    def _calculate_prefactor(self, species: str = None):
+    def _calculate_prefactor(self):
         """
         Compute the ionic conductivity prefactor.
 
-        Parameters
-        ----------
-        species
-
         Returns
         -------
 
@@ -150,9 +158,9 @@ def _calculate_prefactor(self, species: str = None):
         numerator = 1  # self.experiment.volume
         denominator = (
             3
-            * (self.data_range - 1)
+            * (self.args.data_range - 1)
             * self.experiment.temperature
-            * self.experiment.units["boltzman"]
+            * self.experiment.units["boltzmann"]
             * self.experiment.volume
         )
 
@@ -174,7 +182,7 @@ def _apply_averaging_factor(self):
         """
         pass
 
-    def _apply_operation(self, ensemble: tf.Tensor, index: int):
+    def ensemble_operation(self, ensemble: tf.Tensor):
         """
         Calculate and return the msd.
 
@@ -182,24 +190,24 @@ def _apply_operation(self, ensemble: tf.Tensor, index: int):
         ----------
         ensemble : tf.Tensor
                 An ensemble of data to be studied.
-        index : int
 
-                        Returns
+        Returns
         -------
         MSD of the tensor_values.
         """
-        jacf = self.data_range * tf.reduce_sum(
+        jacf = self.args.data_range * tf.reduce_sum(
             tfp.stats.auto_correlation(ensemble, normalize=False, axis=0, center=False),
             axis=-1,
         )
         self.jacf += jacf
         self.sigma.append(
             np.trapz(
-                jacf[: self.integration_range], x=self.time[: self.integration_range]
+                jacf[: self.args.integration_range],
+                x=self.time[: self.args.integration_range],
             )
         )
 
-    def _post_operation_processes(self, species: str = None):
+    def _post_operation_processes(self):
         """
         call the post-op processes
         Returns
@@ -221,7 +229,7 @@ def _post_operation_processes(self, species: str = None):
         if self.plot:
             span = Span(
                 location=(np.array(self.time) * self.experiment.units["time"])[
-                    self.integration_range - 1
+                    self.args.integration_range - 1
                 ],
                 dimension="height",
                 line_dash="dashed",
@@ -232,3 +240,36 @@ def _post_operation_processes(self, species: str = None):
                 title=f"{result[0]} +- {result[1]}",
                 layouts=[span],
             )
+
+    def run_calculator(self):
+        """
+        Run analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        # Compute the pre-factor early.
+        self._calculate_prefactor()
+
+        dict_ref = str.encode(
+            "/".join([self.loaded_property[0], self.loaded_property[0]])
+        )
+
+        batch_ds = self.get_batch_dataset([self.loaded_property[0]])
+
+        for batch in tqdm(
+            batch_ds,
+            ncols=70,
+            total=self.n_batches,
+            disable=self.memory_manager.minibatch,
+        ):
+            ensemble_ds = self.get_ensemble_dataset(batch, self.loaded_property[0])
+
+            for ensemble in ensemble_ds:
+                self.ensemble_operation(ensemble[dict_ref])
+
+        # Scale, save, and plot the data.
+        self._apply_averaging_factor()
+        self._post_operation_processes()
diff --git a/mdsuite/calculators/green_kubo_viscosity_flux.py b/mdsuite/calculators/green_kubo_viscosity_flux.py
index 92912d54..8f85b9e7 100644
--- a/mdsuite/calculators/green_kubo_viscosity_flux.py
+++ b/mdsuite/calculators/green_kubo_viscosity_flux.py
@@ -23,25 +23,35 @@
 
 Summary
 -------
+MDSuite module for the computation of viscosity using the Green-Kubo relation as applied
+to the stress on a system.
 """
-import warnings
+from abc import ABC
+import tensorflow_probability as tfp
 import numpy as np
 from tqdm import tqdm
 import tensorflow as tf
-import tensorflow_probability as tfp
-from mdsuite.calculators.calculator import Calculator, call
+from mdsuite.calculators import TrajectoryCalculator
+from mdsuite.calculators.calculator import call
 from bokeh.models import Span
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
 
-tqdm.monitor_interval = 0
-warnings.filterwarnings("ignore")
 
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
 
-# Set style preferences, turn off warning, and suppress the duplication of loading bars.
-tqdm.monitor_interval = 0
-warnings.filterwarnings("ignore")
+    data_range: int
+    correlation_time: int
+    tau_values: np.s_
+    atom_selection: np.s_
+    integration_range: int
 
 
-class GreenKuboViscosityFlux(Calculator):
+class GreenKuboViscosityFlux(TrajectoryCalculator, ABC):
     """
     Class for the Green Kubo viscosity from flux implementation
 
@@ -73,19 +83,17 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
         self.scale_function = {"linear": {"scale_factor": 5}}
 
-        self.loaded_property = "Stress_visc"  # Property to be loaded for the analysis
+        self.loaded_property = simulation_properties.stress_viscosity
         self.system_property = True
 
-        self.database_group = (  # Which database_path group to save the tensor_values
-            "Viscosity"
-        )
         self.analysis_name = "Viscosity_Flux"
         self.x_label = r"$$\text{Time} / s$$"
         self.y_label = r"$$\text{JACF} / C^{2}\\cdot m^{2}/s^{2}$$"
 
         self.prefactor: float
-        self.jacf = np.zeros(self.data_range)
+        self.jacf: np.ndarray
         self.sigma = []
+        self._dtype = tf.float64
 
     @call
     def __call__(
@@ -93,7 +101,7 @@ def __call__(
         plot=False,
         data_range=500,
         correlation_time=1,
-        save=True,
+        tau_values: np.s_ = np.s_[:],
         gpu: bool = False,
         integration_range: int = None,
     ):
@@ -108,49 +116,39 @@ def __call__(
                 Number of configurations to include in each ensemble
         """
 
-        self.update_user_args(
-            plot=plot,
-            data_range=data_range,
-            save=save,
-            correlation_time=correlation_time,
-            gpu=gpu,
-        )
-
-        self.jacf = np.zeros(self.data_range)
+        self.gpu = gpu
+        self.plot = plot
         self.sigma = []
 
         if integration_range is None:
-            self.integration_range = self.data_range
-        else:
-            self.integration_range = integration_range
+            integration_range = data_range
 
-        return self.update_db_entry_with_kwargs(
-            data_range=data_range, correlation_time=correlation_time
+        # set args that will affect the computation result
+        self.args = Args(
+            data_range=data_range,
+            correlation_time=correlation_time,
+            tau_values=tau_values,
+            atom_selection=np.s_[:],
+            integration_range=integration_range,
         )
 
-    def _update_output_signatures(self):
+        self.time = self._handle_tau_values()
+        self.jacf = np.zeros(self.data_resolution)
+
+    def check_input(self):
         """
-        Update the output signature for the IC.
+        Check the user input to ensure no conflicts are present.
 
         Returns
         -------
 
         """
-        self.batch_output_signature = tf.TensorSpec(
-            shape=(self.batch_size, 3), dtype=tf.float64
-        )
-        self.ensemble_output_signature = tf.TensorSpec(
-            shape=(self.data_range, 3), dtype=tf.float64
-        )
+        self._run_dependency_check()
 
-    def _calculate_prefactor(self, species: str = None):
+    def _calculate_prefactor(self):
         """
         Compute the ionic conductivity prefactor.
 
-        Parameters
-        ----------
-        species
-
         Returns
         -------
 
@@ -159,9 +157,9 @@ def _calculate_prefactor(self, species: str = None):
         numerator = self.experiment.volume
         denominator = (
             3
-            * (self.data_range - 1)
+            * (self.args.data_range - 1)
             * self.experiment.temperature
-            * self.experiment.units["boltzman"]
+            * self.experiment.units["boltzmann"]
         )
 
         prefactor_units = (
@@ -182,7 +180,7 @@ def _apply_averaging_factor(self):
         """
         self.jacf /= max(self.jacf)
 
-    def _apply_operation(self, ensemble, index):
+    def ensemble_operation(self, ensemble):
         """
         Calculate and return the vacf.
 
@@ -194,20 +192,22 @@ def _apply_operation(self, ensemble, index):
         -------
         updates class vacf with the tensor_values.
         """
-        jacf = self.data_range * tf.reduce_sum(
+        jacf = self.args.data_range * tf.reduce_sum(
             tfp.stats.auto_correlation(ensemble, normalize=False, axis=0, center=False),
             axis=-1,
         )
-        self.jacf += jacf[int(self.data_range - 1) :]
+        self.jacf += jacf[int(self.args.data_range - 1) :]
         self.sigma.append(
             np.trapz(
-                jacf[: self.integration_range], x=self.time[: self.integration_range]
+                jacf[: self.args.integration_range],
+                x=self.time[: self.args.integration_range],
             )
         )
 
-    def _post_operation_processes(self, species: str = None):
+    def _post_operation_processes(self):
         """
         call the post-op processes
+
         Returns
         -------
 
@@ -227,7 +227,7 @@ def _post_operation_processes(self, species: str = None):
         if self.plot:
             span = Span(
                 location=(np.array(self.time) * self.experiment.units["time"])[
-                    self.integration_range - 1
+                    self.args.integration_range - 1
                 ],
                 dimension="height",
                 line_dash="dashed",
@@ -238,3 +238,36 @@ def _post_operation_processes(self, species: str = None):
                 title=f"{result[0]} +- {result[1]}",
                 layouts=[span],
             )
+
+    def run_calculator(self):
+        """
+        Run analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+        # Compute the pre-factor early.
+        self._calculate_prefactor()
+
+        dict_ref = str.encode(
+            "/".join([self.loaded_property[0], self.loaded_property[0]])
+        )
+
+        batch_ds = self.get_batch_dataset([self.loaded_property[0]])
+
+        for batch in tqdm(
+            batch_ds,
+            ncols=70,
+            total=self.n_batches,
+            disable=self.memory_manager.minibatch,
+        ):
+            ensemble_ds = self.get_ensemble_dataset(batch, self.loaded_property[0])
+
+            for ensemble in ensemble_ds:
+                self.ensemble_operation(ensemble[dict_ref])
+
+        # Scale, save, and plot the data.
+        self._apply_averaging_factor()
+        self._post_operation_processes()
diff --git a/mdsuite/calculators/kirkwood_buff_integrals.py b/mdsuite/calculators/kirkwood_buff_integrals.py
index e3de02ed..377228d4 100644
--- a/mdsuite/calculators/kirkwood_buff_integrals.py
+++ b/mdsuite/calculators/kirkwood_buff_integrals.py
@@ -26,16 +26,26 @@
 """
 import logging
 import numpy as np
-from mdsuite.utils.exceptions import NotApplicableToAnalysis
 from mdsuite.calculators.calculator import Calculator, call
-from mdsuite.database.scheme import Computation
+from dataclasses import dataclass
+
 
 log = logging.getLogger(__name__)
 
 
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
+
+    savgol_order: int
+    savgol_window_length: int
+
+
 class KirkwoodBuffIntegral(Calculator):
     """
-    Class for the calculation of the Kikrwood-Buff integrals
+    Class for the calculation of the Kirkwood-Buff integrals
 
     Attributes
     ----------
@@ -89,17 +99,16 @@ def __init__(self, **kwargs):
         self.radii = None
         self.kb_integral = None
         self.database_group = "Kirkwood_Buff_Integral"
-        self.x_label = r"$$ \text{r} / \AA$$"
+        self.x_label = r"$$ \text{r} / nm$$"
         self.y_label = r"$$\text{G}(\mathbf{r})$$"
         self.analysis_name = "Kirkwood-Buff_Integral"
         self.result_series_keys = ["r", "kb_integral"]
+        self.data_range = 1
 
         self.post_generation = True
 
     @call
-    def __call__(
-        self, plot=True, save=True, data_range=1, export: bool = False
-    ) -> Computation:
+    def __call__(self, plot=True, data_range=1):
         """
         Doc string for this one.
         Parameters
@@ -107,25 +116,10 @@ def __call__(
         plot : bool
                 If true, the output will be displayed in a figure. This figure will also
                 be saved.
-        save : bool
-                If true, the data from the analysis will be saved in the sql database
         data_range : int
                 Default to 1 for this analysis
-        export : bool
-                If tue, csv files will be dumped after the analysis.
-        """
-
-        self.update_user_args(
-            plot=plot, save=save, data_range=data_range, export=export
-        )
-
-        return self.update_db_entry_with_kwargs(data_range=data_range)
-
-    def _autocorrelation_time(self):
-        """
-        Not needed in this analysis
         """
-        raise NotApplicableToAnalysis
+        self.plot = plot
 
     def _calculate_kb_integral(self):
         """
@@ -145,7 +139,7 @@ def _calculate_kb_integral(self):
                 )
             )
 
-    def run_post_generation_analysis(self):
+    def run_calculator(self):
         """
         Calculate the potential of mean-force and perform error analysis
         """
diff --git a/mdsuite/calculators/potential_of_mean_force.py b/mdsuite/calculators/potential_of_mean_force.py
index 9274aa57..1b273137 100644
--- a/mdsuite/calculators/potential_of_mean_force.py
+++ b/mdsuite/calculators/potential_of_mean_force.py
@@ -23,21 +23,33 @@
 
 Summary
 -------
+Module for the computation of the potential of mean force (PMF). The PMF can be used to
+better understand effective bond strength between species of a system.
 """
 import logging
 import numpy as np
 from scipy.signal import find_peaks
-from mdsuite.utils.exceptions import NotApplicableToAnalysis
 from mdsuite.calculators.calculator import Calculator, call
 from mdsuite.utils.meta_functions import golden_section_search
 from mdsuite.utils.meta_functions import apply_savgol_filter
 from mdsuite.utils.units import boltzmann_constant
 from bokeh.models import BoxAnnotation
-from mdsuite.database.scheme import Computation
+from dataclasses import dataclass
+
 
 log = logging.getLogger(__name__)
 
 
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
+
+    savgol_order: int
+    savgol_window_length: int
+
+
 class PotentialOfMeanForce(Calculator):
     """
     Class for the calculation of the potential of mean-force
@@ -101,9 +113,9 @@ def __init__(self, **kwargs):
         self.radii = None
         self.pomf = None
         self.indices = None
-        self.database_group = "Potential_Of_Mean_Force"
-        self.x_label = r"$$\text{r| /  \AA$$"
+        self.x_label = r"$$r /  nm$$"
         self.y_label = r"$$w^{(2)}(r)$$"
+        self.data_range = 1
 
         self.result_keys = ["min_pomf", "uncertainty", "left", "right"]
         self.result_series_keys = ["r", "pomf"]
@@ -115,12 +127,10 @@ def __init__(self, **kwargs):
     def __call__(
         self,
         plot=True,
-        save=True,
         data_range=1,
-        export: bool = False,
         savgol_order: int = 2,
         savgol_window_length: int = 17,
-    ) -> Computation:
+    ):
         """
         Python constructor for the class
 
@@ -128,39 +138,24 @@ def __call__(
         ----------
         plot : bool (default=True)
                             Decision to plot the analysis.
-        save : bool (default=True)
-                            Decision to save the generated tensor_values arrays.
-
         data_range : int (default=500)
                             Range over which the property should be evaluated.
                             This is not applicable to the current analysis as
                             the full rdf will be calculated.
-        export : bool
-                If true, export the data directly to a csv.
         savgol_order : int
                 Order of the savgol polynomial filter
         savgol_window_length : int
                 Window length of the savgol filter.
         """
-        self.update_user_args(
-            plot=plot, save=save, data_range=data_range, export=export
-        )
+
+        self.plot = plot
         self.data_files = []
-        self.savgol_order = savgol_order
-        self.savgol_window_length = savgol_window_length
 
-        return self.update_db_entry_with_kwargs(
-            data_range=data_range,
+        self.args = Args(
             savgol_order=savgol_order,
             savgol_window_length=savgol_window_length,
         )
 
-    def _autocorrelation_time(self):
-        """
-        Not needed in this analysis
-        """
-        raise NotApplicableToAnalysis
-
     def _calculate_potential_of_mean_force(self):
         """
         Calculate the potential of mean force
@@ -169,13 +164,16 @@ def _calculate_potential_of_mean_force(self):
         self.pomf = (
             -1 * boltzmann_constant * self.experiment.temperature * np.log(self.rdf)
         )
+        self.pomf *= 6.242e8  # convert to eV
 
     def _get_max_values(self):
         """
         Calculate the maximums of the rdf
         """
         filtered_data = apply_savgol_filter(
-            self.pomf, order=self.savgol_order, window_length=self.savgol_window_length
+            self.pomf,
+            order=self.args.savgol_order,
+            window_length=self.args.savgol_window_length,
         )
 
         # Find the maximums in the filtered dataset
@@ -231,7 +229,7 @@ def _get_pomf_value(self):
 
         return pomf_value, pomf_error
 
-    def run_post_generation_analysis(self):
+    def run_calculator(self):
         """
         Calculate the potential of mean-force and perform error analysis
         """
diff --git a/mdsuite/calculators/radial_distribution_function.py b/mdsuite/calculators/radial_distribution_function.py
index 09840685..6100fc13 100644
--- a/mdsuite/calculators/radial_distribution_function.py
+++ b/mdsuite/calculators/radial_distribution_function.py
@@ -23,13 +23,18 @@
 
 Summary
 -------
+MDSuite module for the computation of the radial distribution function (RDF). An RDF
+describes the probability of finding a particle of species b at a distance r of
+species a.
 """
 from __future__ import annotations
 import logging
 from abc import ABC
 from typing import Union
 import numpy as np
-import warnings
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+from mdsuite.calculators import TrajectoryCalculator
 
 # Import user packages
 from tqdm import tqdm
@@ -37,7 +42,7 @@
 import itertools
 from mdsuite.utils.meta_functions import join_path
 
-from mdsuite.calculators.calculator import Calculator, call
+from mdsuite.calculators.calculator import call
 from mdsuite.utils.meta_functions import split_array
 from mdsuite.utils.linalg import (
     apply_minimum_image,
@@ -47,19 +52,28 @@
 
 from timeit import default_timer as timer
 
-from typing import TYPE_CHECKING
+log = logging.getLogger(__name__)
 
-if TYPE_CHECKING:
-    from mdsuite.database.scheme import Computation
 
-# Set style preferences, turn off warning, and suppress the duplication of loading bars.
-tqdm.monitor_interval = 0
-warnings.filterwarnings("ignore")
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
 
-log = logging.getLogger(__name__)
+    number_of_bins: int
+    number_of_configurations: int
+    correlation_time: int
+    atom_selection: np.s_
+    data_range: int
+    cutoff: float
+    start: int
+    stop: int
+    species: list
+    molecules: bool
 
 
-class RadialDistributionFunction(Calculator, ABC):
+class RadialDistributionFunction(TrajectoryCalculator, ABC):
     """
     Class for the calculation of the radial distribution function
 
@@ -105,35 +119,23 @@ def __init__(self, **kwargs):
         super().__init__(**kwargs)
 
         self.scale_function = {"quadratic": {"outer_scale_factor": 1}}
-        self.loaded_property = "Positions"
-        self.database_group = "Radial_Distribution_Function"
-        self.x_label = r"$$r / \AA$$"
+        self.loaded_property = simulation_properties.positions
+        self.x_label = r"$$r / nm$$"
         self.y_label = r"$$g(r)$$"
         self.analysis_name = "Radial_Distribution_Function"
         self.result_series_keys = ["x", "y"]
-        self.experimental = True
 
         self._dtype = tf.float32
 
-        # Arguments set by the user in __call__
-        self.number_of_bins = None
-        self.cutoff = None
-        self.start = None
-        self.stop = None
-        self.number_of_configurations = None
-        self.molecules = None
         self.minibatch = None
         self.use_tf_function = None
         self.override_n_batches = None
         self.index_list = None
-        self.bin_range = None
         self.sample_configurations = None
         self.key_list = None
         self.rdf = None
 
         self.correct_minibatch_batching = None
-        # split the minibatches into equal sized chunks to use maximum computing and
-        # memory resources
 
     @call
     def __call__(
@@ -142,7 +144,6 @@ def __call__(
         number_of_bins=None,
         cutoff=None,
         save=True,
-        data_range=1,
         start=0,
         stop=None,
         number_of_configurations=500,
@@ -151,7 +152,7 @@ def __call__(
         molecules: bool = False,
         gpu: bool = False,
         **kwargs,
-    ) -> Computation:
+    ):
         """
         Compute the RDF with the given user parameters
 
@@ -167,8 +168,6 @@ def __call__(
             The cutoff value for the RDF. Default is half the box size
         save: bool
             save the data
-        data_range: int
-            None, must be here for the parent classes to work.
         start: int
             Starting position in the database. All values before start will be
             ignored.
@@ -187,71 +186,36 @@ def __call__(
         gpu: bool
             Calculate batch size based on GPU memory instead of CPU memory
         kwargs:
-            batches: int
+            overide_n_batches: int
                     override the automatic batch size calculation
             use_tf_function : bool
                     If true, tf.function is used in the calculation.
         """
-        self.number_of_bins = number_of_bins
-        self.cutoff = cutoff
-        self.start = start
-        self.stop = stop
-        self.number_of_configurations = number_of_configurations
-        self.molecules = molecules
-        self.minibatch = minibatch
-        self.species = species
-
-        self.update_user_args(plot=plot, save=save, data_range=data_range, gpu=gpu)
-
-        # kwarg parsing
-        self.use_tf_function = kwargs.pop("use_tf_function", False)
-        self.override_n_batches = kwargs.get("batches")
-        self.tqdm_limit = kwargs.pop("tqdm", 10)
-        # if there are more batches than in that limit it will show the batch tqdm,
-        # otherwise it will show multiple minibatch tqdms
-
-        # Initial checks and initialization.
-        self._check_input()
-        self._initialize_rdf_parameters()
-
-        return self.update_db_entry_with_kwargs(
-            data_range=self.data_range,
+        # set args that will affect the computation result
+        self.args = Args(
             number_of_bins=number_of_bins,
-            number_of_configurations=number_of_configurations,
+            cutoff=cutoff,
             start=start,
             stop=stop,
+            atom_selection=np.s_[:],
+            data_range=1,
+            correlation_time=1,
             molecules=molecules,
             species=species,
+            number_of_configurations=number_of_configurations,
         )
+        # args parsing that will not affect the computation result
+        # usually performance or plotting
+        self.minibatch = minibatch
+        self.plot = plot
+        self.gpu = gpu
 
-    def _initialize_rdf_parameters(self):
-        """
-        Initialize the RDF parameters.
-
-        Returns
-        -------
-        Updates class attributes.
-        """
-        self.bin_range = [0, self.cutoff]
-        self.index_list = [
-            i for i in range(len(self.species))
-        ]  # Get the indices of the species
-
-        self.sample_configurations = np.linspace(
-            self.start, self.stop, self.number_of_configurations, dtype=np.int
-        )  # choose sampled configurations
-
-        # Generate the tuples e.g ('Na', 'Cl'), ('Na', 'Na')
-        self.key_list = [
-            self._get_species_names(x)
-            for x in list(itertools.combinations_with_replacement(self.index_list, r=2))
-        ]
-
-        self.rdf = {
-            name: np.zeros(self.number_of_bins) for name in self.key_list
-        }  # instantiate the rdf tuples
+        # kwargs parsing
+        self.use_tf_function = kwargs.pop("use_tf_function", False)
+        self.override_n_batches = kwargs.get("batches")
+        self.tqdm_limit = kwargs.pop("tqdm", 10)
 
-    def _check_input(self):
+    def check_input(self):
         """
         Check the input of the call method and store defaults if needed.
 
@@ -259,61 +223,73 @@ def _check_input(self):
         -------
         Updates class attributes if required.
         """
-        if self.stop is None:
-            self.stop = self.experiment.number_of_configurations - 1
+        if self.args.stop is None:
+            self.args.stop = self.experiment.number_of_configurations - 1
 
-        if self.cutoff is None:
-            self.cutoff = (
+        if self.args.cutoff is None:
+            self.args.cutoff = (
                 self.experiment.box_array[0] / 2 - 0.1
             )  # set cutoff to half box size if none set
 
-        if self.number_of_configurations == -1:
-            self.number_of_configurations = self.experiment.number_of_configurations - 1
+        if self.args.number_of_configurations == -1:
+            self.args.number_of_configurations = (
+                self.experiment.number_of_configurations - 1
+            )
 
         if self.minibatch == -1:
-            self.minibatch = self.number_of_configurations
+            self.minibatch = self.args.number_of_configurations
 
-        if self.number_of_bins is None:
-            self.number_of_bins = int(
-                self.cutoff / 0.01
+        if self.args.number_of_bins is None:
+            self.args.number_of_bins = int(
+                self.args.cutoff / 0.01
             )  # default is 1/100th of an angstrom
 
-        if self.species is None:
-            self.species = list(self.experiment.species)
-
-        if self.molecules:
-            self.species = list(self.experiment.molecules)
+        # Get the correct species out.
+        if self.args.species is None:
+            if self.args.molecules:
+                self.args.species = list(self.experiment.molecules)
+            else:
+                self.args.species = list(self.experiment.species)
 
         if self.gpu:
             self.correct_minibatch_batching = 100
             # 100 seems to be a good value for most systems
 
-    def _load_positions(self, indices: list) -> tf.Tensor:
-        """
-        Load the positions matrix
+        self._initialize_rdf_parameters()
 
-        This function is here to optimize calculation speed
+    def _initialize_rdf_parameters(self):
+        """
+        Initialize the RDF parameters.
 
-        Parameters
-        ----------
-        indices : list
-                List of indices to take from the database_path
         Returns
         -------
-        loaded_data : tf.Tensor
-                tf.Tensor of tensor_values loaded from the hdf5 database_path
+        Updates class attributes.
         """
-        path_list = [join_path(species, "Positions") for species in self.species]
-        data = self.experiment.load_matrix(
-            "Positions", path=path_list, select_slice=np.s_[:, indices]
-        )
-        if len(self.species) == 1:
-            return tf.cast(data, dtype=self.dtype)
-        else:
-            return tf.cast(tf.concat(data, axis=0), dtype=self.dtype)
+        self.bin_range = [0, self.args.cutoff]
+        self.index_list = [
+            i for i in range(len(self.args.species))
+        ]  # Get the indices of the species
+
+        self.sample_configurations = np.linspace(
+            self.args.start,
+            self.args.stop,
+            self.args.number_of_configurations,
+            dtype=np.int,
+        )  # choose sampled configurations
+
+        # Generate the tuples e.g ('Na', 'Cl'), ('Na', 'Na')
+        self.key_list = [
+            self._get_species_names(x)
+            for x in list(itertools.combinations_with_replacement(self.index_list, r=2))
+        ]
+
+        self.rdf = {
+            name: np.zeros(self.args.number_of_bins) for name in self.key_list
+        }  # instantiate the rdf tuples
 
     def _get_species_names(self, species_tuple: tuple) -> str:
-        """Get the correct names of the species being studied
+        """
+        Get the correct names of the species being studied
 
         Parameters
         ----------
@@ -325,8 +301,9 @@ def _get_species_names(self, species_tuple: tuple) -> str:
         names : str
                 Prefix for the saved file
         """
-
-        return f"{self.species[species_tuple[0]]}_{self.species[species_tuple[1]]}"
+        arg_1 = self.args.species[species_tuple[0]]
+        arg_2 = self.args.species[species_tuple[1]]
+        return f"{arg_1}_{arg_2}"
 
     def _calculate_prefactor(self, species: Union[str, tuple] = None):
         """
@@ -343,7 +320,7 @@ def _calculate_prefactor(self, species: Union[str, tuple] = None):
         if species_split[0] == species_split[1]:
             species_scale_factor = 2
 
-        if self.molecules:
+        if self.args.molecules:
             # Density of all atoms / total volume
             rho = (
                 len(self.experiment.molecules[species_split[1]]["indices"])
@@ -351,7 +328,7 @@ def _calculate_prefactor(self, species: Union[str, tuple] = None):
             )
             numerator = species_scale_factor
             denominator = (
-                self.number_of_configurations
+                self.args.number_of_configurations
                 * rho
                 * self.ideal_correction
                 * len(self.experiment.molecules[species_split[0]]["indices"])
@@ -364,7 +341,7 @@ def _calculate_prefactor(self, species: Union[str, tuple] = None):
             )
             numerator = species_scale_factor
             denominator = (
-                self.number_of_configurations
+                self.args.number_of_configurations
                 * rho
                 * self.ideal_correction
                 * len(self.experiment.species[species_split[0]]["indices"])
@@ -377,10 +354,19 @@ def _calculate_radial_distribution_functions(self):
         """
         Take the calculated histograms and apply the correct pre-factor to them to get
         the correct RDF.
+
         Returns
         -------
-        Updates the class state
+        Updates the class state with the full RDF for each desired species pair.
         """
+        # Compute the true RDF for each species combination.
+        self.rdf.update(
+            {
+                key: np.array(val.numpy(), dtype=np.float)
+                for key, val in self.rdf.items()
+            }
+        )
+
         for names in self.key_list:
             self.selected_species = names.split("_")
             # TODO use selected_species instead of names, it is more clear!
@@ -391,18 +377,29 @@ def _calculate_radial_distribution_functions(self):
             )  # Apply the prefactor
             log.debug("Writing RDF to database!")
 
-            self.data_range = self.number_of_configurations
+            x_data = self._ang_to_nm(
+                np.linspace(0.0, self.args.cutoff, self.args.number_of_bins)
+            )
+            y_data = self.rdf.get(names)
+
+            # self.data_range = self.number_of_configurations
             data = {
-                self.result_series_keys[0]: np.linspace(
-                    0.0, self.cutoff, self.number_of_bins
-                ).tolist(),
-                self.result_series_keys[1]: self.rdf.get(names).tolist(),
+                self.result_series_keys[0]: x_data.tolist(),
+                self.result_series_keys[1]: y_data.tolist(),
             }
 
             self.queue_data(data=data, subjects=self.selected_species)
 
-        # TODO remove rdf_state
-        self.experiment.radial_distribution_function_state = True  # update the state
+    def _ang_to_nm(self, data_in: np.ndarray) -> np.ndarray:
+        """
+        Convert Angstroms to nm
+
+        Returns
+        -------
+        data_out : np.ndarray
+                data_in converted to nm
+        """
+        return (self.experiment.units["length"] / 1e-9) * data_in
 
     def _correct_batch_properties(self):
         """
@@ -412,194 +409,66 @@ def _correct_batch_properties(self):
         -------
         Updates the parent class.
         """
-        if self.batch_size > self.number_of_configurations:
-            self.batch_size = self.number_of_configurations
+        if self.batch_size > self.args.number_of_configurations:
+            self.batch_size = self.args.number_of_configurations
             self.n_batches = 1
         else:
-            self.n_batches = int(self.number_of_configurations / self.batch_size)
+            self.n_batches = int(self.args.number_of_configurations / self.batch_size)
 
         if self.override_n_batches is not None:
             self.n_batches = self.override_n_batches
 
-    def mini_calculate_histograms(self):
-        """Do the minibatch calculation"""
-
-        def combine_dictionaries(dict_a, dict_b):
-            """Combine two dictionaries in a tf.function call"""
-            out = dict()
-            for key in dict_a:
-                out[key] = dict_a[key] + dict_b[key]
-            return out
-
-        def run_minibatch_loop():
-            """Run a minibatch loop"""
-            minibatch_start = tf.constant(0)
-            stop = tf.constant(0)
-            rdf = {
-                name: tf.zeros(self.number_of_bins, dtype=tf.int32)
-                for name in self.key_list
-            }
-
-            # if self.correct_minibatch_batching is not None:
-            #     # per_atoms_ds with shape (configurations, 3)
-            #     corrected_ds = per_atoms_ds.batch(
-            #         int(len(per_atoms_ds) / self.correct_minibatch_batching)
-            #     )
-            #     for jdx, corrected_per_atoms_ds in tqdm(
-            #         enumerate(corrected_ds),
-            #         ncols=70,
-            #         disable=not batch_tqm,
-            #         total=self.correct_minibatch_batching,
-            #         desc=f"Mini batch {idx + 1}/{self.n_batches}",
-            #     ):
-            #         pre_factor = np.sqrt(jdx + 1)
-            #         new_ds = tf.data.Dataset.from_tensor_slices(
-            #         corrected_per_atoms_ds)
-            #         new_ds = new_ds.batch(int(pre_factor * self.minibatch))
-            #         log.debug(
-            #             "batch size:"
-            #             f" {int(pre_factor * self.minibatch)} ({self.minibatch} *"
-            #             f" {pre_factor})"
-            #         )
-            #         for atoms in new_ds:
-            #             atoms_per_batch, batch_size, _ = tf.shape(atoms)
-            #             stop += atoms_per_batch
-            #             start_time = timer()
-            #             indices = get_partial_triu_indices(
-            #                 n_atoms, atoms_per_batch, minibatch_start
-            #             )
-            #             log.debug(f"Calculating indices took {timer() - start_time}
-            #             s")
-            #
-            #             start_time = timer()
-            #             d_ij = self.get_dij(
-            #                 indices,
-            #                 positions_tensor,
-            #                 atoms,
-            #                 tf.cast(self.experiment.box_array, dtype=self.dtype),
-            #             )
-            #             exec_time = timer() - start_time
-            #             atom_pairs_per_second = (
-            #                 tf.cast(tf.shape(indices)[1], dtype=self.dtype)
-            #                 / exec_time
-            #                 / 10 ** 6
-            #             )
-            #             atom_pairs_per_second *= tf.cast(batch_size, dtype=self.dtype)
-            #             log.debug(
-            #                 f"Computing d_ij took {exec_time} s "
-            #                 f"({atom_pairs_per_second:.1f} million atom pairs / s)"
-            #             )
-            #
-            #             start_time = timer()
-            #             minibatch_rdf = self.compute_species_values(
-            #                 indices, minibatch_start, d_ij
-            #             )
-            #             log.debug(
-            #                 f"Computing species values took {timer() - start_time} s"
-            #             )
-            #
-            #             start_time = timer()
-            #             rdf = combine_dictionaries(rdf, minibatch_rdf)
-            #             log.debug(
-            #                 f"Updating dictionaries took {timer() - start_time} s"
-            #             )
-            #
-            #             minibatch_start = stop
-            #
-            #     return rdf
-
-            for atoms in tqdm(
-                per_atoms_ds.batch(self.minibatch).prefetch(tf.data.AUTOTUNE),
-                ncols=70,
-                disable=not batch_tqm,
-                desc=f"Running mini batch loop {idx + 1} / {self.n_batches}",
-            ):
-                atoms_per_batch, batch_size, _ = tf.shape(atoms)
-                stop += atoms_per_batch
-                start_time = timer()
-                indices = get_partial_triu_indices(
-                    n_atoms, atoms_per_batch, minibatch_start
-                )
-                log.debug(f"Calculating indices took {timer() - start_time} s")
-
-                start_time = timer()
-                d_ij = self.get_dij(
-                    indices,
-                    positions_tensor,
-                    atoms,
-                    tf.cast(self.experiment.box_array, dtype=self.dtype),
-                )
-                exec_time = timer() - start_time
-                atom_pairs_per_second = (
-                    tf.cast(tf.shape(indices)[1], dtype=self.dtype)
-                    / exec_time
-                    / 10 ** 6
-                )
-                atom_pairs_per_second *= tf.cast(batch_size, dtype=self.dtype)
-                log.debug(
-                    f"Computing d_ij took {exec_time} s "
-                    f"({atom_pairs_per_second:.1f} million atom pairs / s)"
-                )
-
-                start_time = timer()
-                minibatch_rdf = self.compute_species_values(
-                    indices, minibatch_start, d_ij
-                )
-                log.debug(f"Computing species values took {timer() - start_time} s")
-
-                start_time = timer()
-                rdf = combine_dictionaries(rdf, minibatch_rdf)
-                log.debug(f"Updating dictionaries took {timer() - start_time} s")
-
-                minibatch_start = stop
-            return rdf
-
-        execution_time = 0
-
-        batch_tqm = self.tqdm_limit > self.n_batches
-
-        for idx, sample_configuration in tqdm(
-            enumerate(np.array_split(self.sample_configurations, self.n_batches)),
-            ncols=70,
-            disable=batch_tqm,
-        ):
-            log.debug("Loading Data")
+    def run_minibatch_loop(
+        self, atoms, stop, n_atoms, minibatch_start, positions_tensor
+    ):
+        """
+        Run a minibatch loop
 
-            positions_tensor = self._load_positions(sample_configuration)
-            log.debug("Data loaded - creating dataset")
-            per_atoms_ds = tf.data.Dataset.from_tensor_slices(positions_tensor)
-            # create dataset of atoms from shape (n_atoms, n_timesteps, 3)
+        Parameters
+        ----------
+        atoms : tf.Tensor
+        stop : int
+        n_atoms : int
+        minibatch_start : int
+        positions_tensor : tf.Tensor
 
-            n_atoms = tf.shape(positions_tensor)[0]
-            log.debug("Starting calculation")
-            start = timer()
-            _rdf = run_minibatch_loop()
+        """
 
-            for key in self.rdf:
-                self.rdf[key] += _rdf[key]
+        # Compute the number of atoms and configurations in the batch.
+        atoms_per_batch, batch_size, _ = tf.shape(atoms)
 
-            execution_time += timer() - start
-            log.debug("Calculation done")
+        # Compute the indices
+        stop += atoms_per_batch
+        start_time = timer()
+        indices = get_partial_triu_indices(n_atoms, atoms_per_batch, minibatch_start)
+        log.debug(f"Calculating indices took {timer() - start_time} s")
 
-        self.rdf.update(
-            {
-                key: np.array(val.numpy(), dtype=np.float)
-                for key, val in self.rdf.items()
-            }
+        # Compute the d_ij matrix.
+        start_time = timer()
+        d_ij = self.get_dij(
+            indices,
+            positions_tensor,
+            atoms,
+            tf.cast(self.experiment.box_array, dtype=self.dtype),
+        )
+        exec_time = timer() - start_time
+        atom_pairs_per_second = (
+            tf.cast(tf.shape(indices)[1], dtype=self.dtype) / exec_time / 10 ** 6
+        )
+        atom_pairs_per_second *= tf.cast(batch_size, dtype=self.dtype)
+        log.debug(
+            f"Computing d_ij took {exec_time} s "
+            f"({atom_pairs_per_second:.1f} million atom pairs / s)"
         )
-        log.debug(f"RDF execution time: {execution_time} s")
 
-    def run_experimental_analysis(self):
-        """
-        Perform the rdf analysis
-        """
-        log.info("Starting RDF Calculation")
-        self._correct_batch_properties()
+        # Compute the rdf for the minibatch
+        start_time = timer()
+        minibatch_rdf = self.compute_species_values(indices, minibatch_start, d_ij)
+        log.debug(f"Computing species values took {timer() - start_time} s")
 
-        log.debug(f"Using minibatching with batch size: {self.minibatch}")
-        self.mini_calculate_histograms()
+        minibatch_start = stop
 
-        self._calculate_radial_distribution_functions()
+        return minibatch_rdf, minibatch_start, stop
 
     def compute_species_values(self, indices: tf.Tensor, start_batch, d_ij: tf.Tensor):
         """
@@ -607,17 +476,21 @@ def compute_species_values(self, indices: tf.Tensor, start_batch, d_ij: tf.Tenso
 
         Parameters
         ----------
-        indices: indices of the d_ij distances in the shape (x, 2)
-        start_batch: starts from 0 and increments by atoms_per_batch every batch
-        d_ij: d_ij matrix in the shape (x, batches) where x comes from the triu
-        computation
+        indices: tf.Tensor
+                indices of the d_ij distances in the shape (x, 2)
+                start_batch: starts from 0 and increments by atoms_per_batch every batch
+                d_ij: d_ij matrix in the shape (x, batches) where x comes from the triu
+                computation
+        start_batch : int
+        d_ij : tf.Tensor
+                distance matrix for the atoms.
 
         Returns
         -------
 
         """
         rdf = {
-            name: tf.zeros(self.number_of_bins, dtype=tf.int32)
+            name: tf.zeros(self.args.number_of_bins, dtype=tf.int32)
             for name in self.key_list
         }
         indices = tf.transpose(indices)
@@ -636,23 +509,128 @@ def compute_species_values(self, indices: tf.Tensor, start_batch, d_ij: tf.Tenso
             stop_ = start_ + tf.constant(
                 [particles_list[tuples[0]], particles_list[tuples[1]]]
             )
+
             rdf[names] = self.bin_minibatch(
                 start_,
                 stop_,
                 indices,
                 d_ij,
                 tf.cast(self.bin_range, dtype=self.dtype),
-                tf.cast(self.number_of_bins, dtype=tf.int32),
-                tf.cast(self.cutoff, dtype=self.dtype),
+                tf.cast(self.args.number_of_bins, dtype=tf.int32),
+                tf.cast(self.args.cutoff, dtype=self.dtype),
             )
         return rdf
 
+    def plot_data(self, data):
+        """Plot the RDF data"""
+        for selected_species, val in data.items():
+            # TODO fix units!
+            self.run_visualization(
+                x_data=np.array(val[self.result_series_keys[0]]),
+                y_data=np.array(val[self.result_series_keys[1]]),
+                title=selected_species,
+            )
+
+    def _format_data(self, batch: tf.Tensor, keys: list) -> tf.Tensor:
+        """
+        Format the loaded data for use in the rdf calculator.
+
+        The RDF requires a reshaped dataset. The generator will load a default
+        dict oriented type. This method restructures the data to be used in the
+        calculator.
+
+        Parameters
+        ----------
+        batch : tf.Tensor
+                A batch of data to transform.
+        keys : list
+                Dict keys to extract from the data.
+
+        Returns
+        -------
+        data : tf.Tensor
+                data tensor of the shape (n_atoms * n_species, n_configurations, 3)
+
+        """
+        formatted_data = []
+        for item in keys:
+            formatted_data.append(batch[item])
+
+        if len(self.args.species) == 1:
+            return tf.cast(formatted_data[0], dtype=self.dtype)
+        else:
+            return tf.cast(tf.concat(formatted_data, axis=0), dtype=self.dtype)
+
+    def prepare_computation(self):
+        """
+        Run the steps necessary to prepare for the RDF computation.
+
+        Returns
+        -------
+        dict_keys : list
+                dict keys to use when selecting data from the output.
+        split_arr : np.ndarray
+                Array of indices to load from the database split into sub-arrays which
+                fulfill the necessary batch size.
+        batch_tqdm : bool
+                If true, the main tqdm loop over batches is disabled and only the
+                mini-batch loop will be displayed.
+        """
+
+        path_list = [
+            join_path(item, self.loaded_property[0]) for item in self.args.species
+        ]
+        self._prepare_managers(path_list)
+
+        # batch loop correction
+        self._correct_batch_properties()
+
+        # Get the correct dict keys.
+        dict_keys = []
+        for item in self.args.species:
+            dict_keys.append(str.encode(join_path(item, self.loaded_property[0])))
+
+        # Split the configurations into batches.
+        split_arr = np.array_split(self.sample_configurations, self.n_batches)
+
+        # Turn off the tqdm for certain scenarios.
+        batch_tqdm = self.tqdm_limit > self.n_batches
+
+        return dict_keys, split_arr, batch_tqdm
+
+    @staticmethod
+    def combine_dictionaries(dict_a: dict, dict_b: dict):
+        """
+        Combine two dictionaries in a tf.function call
+
+        Parameters
+        ----------
+        dict_a : dict
+        dict_b : dict
+        """
+        out = dict()
+        for key in dict_a:
+            out[key] = dict_a[key] + dict_b[key]
+        return out
+
     @staticmethod
     @tf.function(experimental_relax_shapes=True)
     def bin_minibatch(
         start, stop, indices, d_ij, bin_range, number_of_bins, cutoff
     ) -> tf.Tensor:
-        """Compute the minibatch histogram"""
+        """
+        Compute the minibatch histogram
+
+        Parameters
+        ----------
+        start : list
+        stop : list
+        indices : tf.Tensor
+        d_ij : tf.Tensor
+        bin_range
+        number_of_bins : int
+        cutoff : float
+        """
 
         # select the indices that are within the boundaries of the current species /
         # molecule
@@ -670,7 +648,17 @@ def bin_minibatch(
     @staticmethod
     @tf.function(experimental_relax_shapes=True)
     def get_dij(indices, positions_tensor, atoms, box_array):
-        """Compute the distance matrix for the minibatch"""
+        """
+        Compute the distance matrix for the minibatch
+
+        Parameters
+        ----------
+        indices : tf.Tensor
+        positions_tensor : tf.Tensor
+        atoms : tf.Tensor
+        box_array : tf.Tensor
+
+        """
         start_time = timer()
         log.debug(f"Calculating indices took {timer() - start_time} s")
 
@@ -703,7 +691,13 @@ def get_dij(indices, positions_tensor, atoms, box_array):
 
     @property
     def particles_list(self):
-        if self.molecules:
+        """
+        List of number of atoms of each species being studied.
+        Returns
+        -------
+
+        """
+        if self.args.molecules:
             particles_list = [
                 len(self.experiment.molecules[item]["indices"])
                 for item in self.experiment.molecules
@@ -824,17 +818,67 @@ def _piecewise(data: np.array) -> np.array:
                         )
                     )
 
-        bin_width = self.cutoff / self.number_of_bins
-        bin_edges = np.linspace(0.0, self.cutoff, self.number_of_bins)
+        bin_width = self.args.cutoff / self.args.number_of_bins
+        bin_edges = np.linspace(0.0, self.args.cutoff, self.args.number_of_bins)
 
         return _piecewise(np.array(bin_edges)) * bin_width
 
-    def plot_data(self, data):
-        """Plot the RDF data"""
-        for selected_species, val in data.items():
-            # TODO fix units!
-            self.run_visualization(
-                x_data=np.array(val[self.result_series_keys[0]]),
-                y_data=np.array(val[self.result_series_keys[1]]),
-                title=selected_species,
-            )
+    def run_calculator(self):
+        """
+        Run the analysis.
+
+        Returns
+        -------
+
+        """
+        self.check_input()
+
+        dict_keys, split_arr, batch_tqm = self.prepare_computation()
+
+        # Get the batch dataset
+        batch_ds = self.get_batch_dataset(
+            subject_list=self.args.species, loop_array=split_arr, correct=True
+        )
+
+        # Loop over the batches.
+        for idx, batch in tqdm(enumerate(batch_ds), ncols=70, disable=batch_tqm):
+
+            # Reformat the data.
+            log.debug("Reformatting data.")
+            positions_tensor = self._format_data(batch=batch, keys=dict_keys)
+
+            # Create a new dataset to loop over.
+            log.debug("Creating dataset.")
+            per_atoms_ds = tf.data.Dataset.from_tensor_slices(positions_tensor)
+            n_atoms = tf.shape(positions_tensor)[0]
+
+            # Start the computation.
+            log.debug("Beginning calculation.")
+            minibatch_start = tf.constant(0)
+            stop = tf.constant(0)
+            rdf = {
+                name: tf.zeros(self.args.number_of_bins, dtype=tf.int32)
+                for name in self.key_list
+            }
+
+            for atoms in tqdm(
+                per_atoms_ds.batch(self.minibatch).prefetch(tf.data.AUTOTUNE),
+                ncols=70,
+                disable=not batch_tqm,
+                desc=f"Running mini batch loop {idx + 1} / {self.n_batches}",
+            ):
+                # Compute the minibatch update
+                minibatch_rdf, minibatch_start, stop = self.run_minibatch_loop(
+                    atoms, stop, n_atoms, minibatch_start, positions_tensor
+                )
+
+                # Update the rdf.
+                start_time = timer()
+                rdf = self.combine_dictionaries(rdf, minibatch_rdf)
+                log.debug(f"Updating dictionaries took {timer() - start_time} s")
+
+            # Update the class before the next batch.
+            for key in self.rdf:
+                self.rdf[key] += rdf[key]
+
+        self._calculate_radial_distribution_functions()
diff --git a/mdsuite/calculators/spatial_distribution_function.py b/mdsuite/calculators/spatial_distribution_function.py
index 319d9a4c..3d2977f7 100644
--- a/mdsuite/calculators/spatial_distribution_function.py
+++ b/mdsuite/calculators/spatial_distribution_function.py
@@ -23,18 +23,29 @@
 
 Summary
 -------
+Module for the spatial distribution function calculator.
 """
 from __future__ import annotations
 
 import logging
 
-from mdsuite.calculators.calculator import Calculator, call
+from mdsuite.calculators.calculator import call
 from mdsuite.utils.tensorflow.layers import NLLayer
 from mdsuite.utils.meta_functions import join_path
 from tqdm import tqdm
+import math
 import numpy as np
 import tensorflow as tf
 from mdsuite.visualizer.d3_data_visualizer import DataVisualizer3D
+from dataclasses import dataclass
+from mdsuite.database import simulation_properties
+from mdsuite.calculators import TrajectoryCalculator
+
+from mdsuite.utils.linalg import (
+    spherical_to_cartesian_coordinates,
+    cartesian_to_spherical_coordinates,
+    get2dHistogram,
+)
 
 from typing import TYPE_CHECKING
 
@@ -44,12 +55,27 @@
 log = logging.getLogger(__name__)
 
 
-class SpatialDistributionFunction(Calculator):
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
+
+    number_of_configurations: int
+    data_range: int
+    correlation_time: int
+    atom_selection: np.s_
+    molecules: bool
+    species: list
+    r_min: float
+    r_max: float
+    n_bins: int
+
+
+class SpatialDistributionFunction(TrajectoryCalculator):
     """Spatial Distribution Function Calculator based on the r_ij matrix"""
 
-    def __init__(
-        self, experiment: Experiment, experiments=None, load_data: bool = False
-    ):
+    def __init__(self, experiment: Experiment, experiments=None):
         """
         Constructor of the SpatialDistributionFunction
 
@@ -63,12 +89,11 @@ def __init__(
             managed by RunComputation
 
         """
-        super().__init__(experiment, experiments=experiments, load_data=load_data)
+        super().__init__(experiment, experiments=experiments)
 
         self.scale_function = {"quadratic": {"outer_scale_factor": 1}}
-        self.loaded_property = "Positions"
-        self.database_group = "Spatial_Distribution_Function"
-        self.x_label = r"$$\text{r} /  \AA$$"  # None
+        self.loaded_property = simulation_properties.positions
+        self.x_label = r"$$\text{r} /  nm$$"  # None
         self.y_label = r"$$\text{g(r)}$$"  # None
         self.analysis_name = "Spatial_Distribution_Function"
         self.experimental = True
@@ -85,6 +110,7 @@ def __call__(
         r_min: float = 4.0,
         r_max: float = 4.5,
         species: list = None,
+        n_bins: int = 100,
         **kwargs,
     ):
         """
@@ -109,18 +135,29 @@ def __call__(
             if None a single SDF of all available species will be computed
         kwargs
         """
-        self.species = species
-        self.molecules = molecules
-        self.r_min = r_min
-        self.r_max = r_max
+        if species is None:
+            if molecules:
+                species = list(self.experiment.molecules)
+            else:
+                species = list(self.experiment.species)
 
         # choose sampled configurations
         self.sample_configurations = np.linspace(
             start, stop, number_of_configurations, dtype=np.int
         )
-
-        self.update_user_args()
-        self._check_input()
+        self.plot = True
+
+        self.args = Args(
+            molecules=molecules,
+            species=species,
+            number_of_configurations=number_of_configurations,
+            r_min=r_min,
+            atom_selection=np.s_[:],
+            r_max=r_max,
+            data_range=number_of_configurations,
+            correlation_time=1,
+            n_bins=n_bins,
+        )
 
     def _load_positions(self, indices: list, species: str) -> tf.Tensor:
         """
@@ -139,29 +176,25 @@ def _load_positions(self, indices: list, species: str) -> tf.Tensor:
         loaded_data : tf.Tensor
                 tf.Tensor of tensor_values loaded from the hdf5 database_path
         """
-        # path_list = [join_path(species, "Positions") for species in self.species]
-
-        path_list = [join_path(species, "Positions")]
+        path_list = [join_path(species, self.loaded_property[0])]
 
-        data = self.experiment.load_matrix(
-            "Positions", path=path_list, select_slice=np.s_[:, indices]
+        data_dict = self.database.load_data(
+            path_list=path_list, select_slice=np.s_[:, indices]
         )
-        if len(self.species) == 1:
+        data = []
+        for item in path_list:
+            data.append(data_dict[item])
+        if len(self.args.species) == 1:
             return tf.cast(data, dtype=self.dtype)
         else:
             return tf.cast(tf.concat(data, axis=0), dtype=self.dtype)
 
-    def _check_input(self):
-        """Check and correct the user input"""
-        if self.species is None:
-            if self.molecules:
-                self.species = list(self.experiment.molecules)
-            else:
-                self.species = list(self.experiment.species)
-
-    def run_experimental_analysis(self):
+    def run_calculator(self):
         """Run the computation"""
-
+        path_list = [
+            join_path(item, self.loaded_property[0]) for item in self.args.species
+        ]
+        self._prepare_managers(path_list)
         # Iterate over batches
         sdf_values = []
 
@@ -173,7 +206,7 @@ def run_experimental_analysis(self):
         ):
             positions_tensor = []
             species_length = []
-            for species in self.species:
+            for species in self.args.species:
                 positions_tensor.append(
                     self._load_positions(sample_configuration, species)
                 )
@@ -191,7 +224,7 @@ def run_experimental_analysis(self):
 
             d_ij = tf.linalg.norm(r_ij, axis=-1)  # shape (b, i, j)
             # apply minimal and maximal distance and remove the diagonal elements of 0
-            mask = (d_ij > self.r_min) & (d_ij < self.r_max)  # & (d_ij != 0)
+            mask = (d_ij > self.args.r_min) & (d_ij < self.args.r_max)  # & (d_ij != 0)
 
             # Slicing the mask to the area where only the distances i!=j occur.
             # There are two such areas, so I am slicing them twice
@@ -199,18 +232,82 @@ def run_experimental_analysis(self):
             mask_ = mask[:, species_length[0] :, : species_length[1]]
             r_ij_cut = r_ij[:, species_length[0] :, : species_length[1], :]
             r_ij_cut = r_ij_cut[mask_]
-            sdf_values.append(r_ij_cut)
+            sdf_values.append(self.r_ij_to_bins(r_ij_cut))
             # and the other half (only effective if species[0] != species[1])
             mask_ = mask[:, : species_length[0], species_length[1] :]
             r_ij_cut = r_ij[:, : species_length[0], species_length[1] :, :]
             r_ij_cut = r_ij_cut[mask_]
-            sdf_values.append(r_ij_cut)
+            sdf_values.append(self.r_ij_to_bins(r_ij_cut))
+
+        sdf_values = tf.reduce_sum(sdf_values, axis=0)
+
+        # TODO fix subjects and maybe rename
+        self.queue_data(
+            data={
+                "sdf": sdf_values.numpy().tolist(),
+                "sphere": self._get_unit_sphere().numpy().tolist(),
+            },
+            subjects=["System"],
+        )
+
+        if self.plot:
+            coordinates = tf.reshape(
+                self._get_unit_sphere(), [self.args.n_bins ** 2, 3]
+            )
+            colour_map = tf.reshape(sdf_values, [-1])
+            self._run_visualization(coordinates, colour_map)
+
+    def _get_unit_sphere(self) -> tf.Tensor:
+        """Get the coordinates on the sphere for the bins
 
-        sdf_values = tf.concat(sdf_values, axis=0)
+        Returns
+        -------
+        tf.Tensor:
+            A Tensor with shape (n_bins, n_bins, 3) where 3 represents (x,y,z)
+            for the coordinates of a unit sphere
+        """
+        theta_range = [0, math.pi]
+        phi_range = [-math.pi, math.pi]
+        theta_vals = np.linspace(theta_range[0], theta_range[1], self.args.n_bins)
+        phi_vals = np.linspace(phi_range[0], phi_range[1], self.args.n_bins)
+
+        xx, yy = np.meshgrid(theta_vals, phi_vals)
+        spherical_map = tf.stack([tf.ones_like(xx), xx, yy], axis=-1)
+        cartesian_map = spherical_to_cartesian_coordinates(spherical_map)
+
+        return cartesian_map
+
+    def r_ij_to_bins(self, r_ij) -> tf.Tensor:
+        """Compute the 2D histogram in spherical coordinates while projecting
+        all values of r to a unit sphere
+
+        Parameters
+        ----------
+        r_ij: tf.Tensor
+            any  r_ij matrix with shape (..., 3)
+
+        Returns
+        -------
+        tf.Tensor:
+            bins with shape (n_bins, n_bins)
+
+        """
+        r_ij_spherical = cartesian_to_spherical_coordinates(r_ij)
+        theta_phi_pairs = tf.reshape(r_ij_spherical, (-1, 3))
+
+        theta_range = [0, math.pi]
+        phi_range = [-math.pi, math.pi]
+
+        bins = get2dHistogram(
+            theta_phi_pairs[:, 1],
+            theta_phi_pairs[:, 2],
+            value_range=[theta_range, phi_range],
+            nbins=self.args.n_bins,
+        )
 
-        self._run_visualization(sdf_values)
+        return bins
 
-    def _run_visualization(self, plot_data: tf.Tensor):
+    def _run_visualization(self, plot_data: tf.Tensor, colour_map: np.ndarray):
         """
         Run the visualizer.
 
@@ -218,20 +315,22 @@ def _run_visualization(self, plot_data: tf.Tensor):
         ----------
         plot_data : tf.Tensor
                 Data to be plot.
+        colour_map : tf.Tensor
+                A colour map to highlight density on the unit sphere
 
         """
-        if self.species[0] in list(self.experiment.species):
-            center = self.species[0]
+        if self.args.species[0] in list(self.experiment.species):
+            center = self.args.species[0]
         else:
-            center_dict = self.experiment.molecules[self.species[0]]["groups"]["0"]
+            center_dict = self.experiment.molecules[self.args.species[0]]["groups"]["0"]
             center = {}
             for item in center_dict:
                 for index in center_dict[item]:
                     center[f"{item}_{index}"] = self.database.load_data(
                         path_list=[join_path(item, "Positions")],
                         select_slice=np.s_[index, 0],
-                    )
+                    )[join_path(item, "Positions")]
         visualizer = DataVisualizer3D(
-            data=plot_data.numpy(), title="SDF", center=center
+            data=plot_data, title="SDF", center=center, colour_map=colour_map
         )
         visualizer.plot()
diff --git a/mdsuite/calculators/structure_factor.py b/mdsuite/calculators/structure_factor.py
index 0857672a..504f26db 100644
--- a/mdsuite/calculators/structure_factor.py
+++ b/mdsuite/calculators/structure_factor.py
@@ -30,15 +30,29 @@
 from scipy.integrate import simps
 from scipy.integrate import cumtrapz
 from tqdm import tqdm
-from mdsuite.utils.exceptions import NotApplicableToAnalysis
 from mdsuite.calculators.calculator import Calculator
-from mdsuite import data as static_data
+from mdsuite import data
 from importlib.resources import open_text
+from dataclasses import dataclass
 
 
 log = logging.getLogger(__name__)
 
 
+@dataclass
+class Args:
+    """
+    Data class for the saved properties.
+    """
+
+    data_range: int
+    correlation_time: int
+    atom_selection: np.s_
+    tau_values: np.s_
+    molecules: bool
+    species: list
+
+
 class StructureFactor(Calculator):
     """
     Class for the calculation of the total structure factor for X-ray scattering
@@ -104,33 +118,27 @@ def __init__(self, **kwargs):
 
         self.post_generation = True
 
-        self.database_group = "structure_factor"
-        self.x_label = r"$$\text{Q} / \AA ^{-1}$$"
+        self.x_label = r"$$\text{Q} / nm ^{-1}$$"
         self.y_label = r"$$\text{S(Q)}$$"
         self.analysis_name = "total_structure_factor"
 
         self.rho = None
 
-        with open_text(static_data, "form_fac_coeffs.csv") as file:
+        with open_text(data, "form_fac_coeffs.csv") as file:
             self.coeff_atomic_formfactor = pd.read_csv(
                 file, sep=","
             )  # stores coefficients for atomic form factors
 
-    def __call__(self, plot=True, save=True, data_range=1, export: bool = False):
+    def __call__(self, plot=True, data_range=1):
         """
         Parameters
         ----------
         plot : bool (default=True)
                             Decision to plot the analysis.
-        save : bool (default=True)
-                            Decision to save the generated tensor_values arrays.
-
         data_range : int (default=500)
                             Range over which the property should be evaluated.
                             This is not applicable to the current analysis as
                             the full rdf will be calculated.
-        export : bool
-                If true, export the data directly to a csv.
 
         Returns
         -------
@@ -141,9 +149,7 @@ def __call__(self, plot=True, save=True, data_range=1, export: bool = False):
         for experiment in self.experiments:
             self.experiment = experiment
 
-            self.update_user_args(
-                plot=plot, save=save, data_range=data_range, export=export
-            )
+            self.plot = plot
 
             self.rho = self.experiment.number_of_atoms / (
                 self.experiment.box_array[0]
@@ -163,12 +169,6 @@ def __call__(self, plot=True, save=True, data_range=1, export: bool = False):
         else:
             return out[self.experiment.name]
 
-    def _autocorrelation_time(self):
-        """
-        Not needed in this analysis
-        """
-        raise NotApplicableToAnalysis
-
     @staticmethod
     def gauss(a, b, scattering_scalar):
         """
@@ -322,7 +322,7 @@ def total_structure_factor(self, scattering_scalar):
             total_struc_fac += s_in
         return total_struc_fac
 
-    def run_post_generation_analysis(self):
+    def run_calculator(self):
         """
         Calculates the total structure factor for all the different Q-values
         of the Q_arr (magnitude of the scattering vector)
diff --git a/mdsuite/calculators/trajectory_calculator.py b/mdsuite/calculators/trajectory_calculator.py
new file mode 100644
index 00000000..d661cf80
--- /dev/null
+++ b/mdsuite/calculators/trajectory_calculator.py
@@ -0,0 +1,390 @@
+"""
+MDSuite: A Zincwarecode package.
+
+License
+-------
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License v2.0 which accompanies this distribution, and is
+available at https://www.eclipse.org/legal/epl-v20.html
+
+SPDX-License-Identifier: EPL-2.0
+
+Copyright Contributors to the Zincwarecode Project.
+
+Contact Information
+-------------------
+email: zincwarecode@gmail.com
+github: https://github.com/zincware
+web: https://zincwarecode.com/
+
+Citation
+--------
+If you use this module please cite us with:
+
+Summary
+-------
+A parent class for calculators that operate on the trajectory.
+"""
+from abc import ABC
+
+from .calculator import Calculator
+from typing import TYPE_CHECKING, List
+from mdsuite.memory_management import MemoryManager
+from mdsuite.database import DataManager
+from pathlib import Path
+from mdsuite.calculators.transformations_reference import switcher_transformations
+from mdsuite.database.simulation_database import Database
+import numpy as np
+from mdsuite.utils.meta_functions import join_path
+import tensorflow as tf
+
+
+if TYPE_CHECKING:
+    from mdsuite import Experiment
+
+
+class TrajectoryCalculator(Calculator, ABC):
+    """
+    Parent class for calculators operating on the trajectory.
+
+    Attributes
+    ----------
+    data_resolution : int
+            Resolution of the data to be plotted. This is necessary because if someone
+            wants a data_range of 500 they may not want
+    loaded_property : tuple
+            The property being loaded from the simulation database.
+    dependency : tuple
+            A dependency required for the analysis to run.
+    scale_function : dict
+            The scaling behaviour of the computer. e.g.
+            {"linear": {"scale_factor": 150}}.  See mdsuite.utils.scale_functions.py for
+            the list of possible functions.
+    batch_size : int
+            Batch size to use. This is the number of configurations that can be loaded
+            given the complexity and data requirements of the operation.
+    n_batches : int
+            Number of batches that can be looped over given the batch size.
+    remainder : int
+            The remainder of configurations after the batch process.
+    minibatch : bool
+            If true, atom-wise mini-batching will be used.
+    memory_manager : MemoryManager
+            Memory manager object to handle computation of batch sizes.
+    data_manager : DataManager
+            Data manager parent to handle preparation of data generators.
+    _database : Database
+            Simulation database from which data should be loaded.
+    """
+
+    def __init__(
+        self,
+        experiment: object = None,
+        experiments: List = None,
+    ):
+        """
+        Constructor for the TrajectoryCalculator class.
+
+        Parameters
+        ----------
+        experiment : Experiment
+                Experiment for which the calculator will be run.
+        experiments : List[Experiment]
+                List of experiments on which to run the calculator.
+        """
+        super(TrajectoryCalculator, self).__init__(
+            experiment=experiment, experiments=experiments
+        )
+
+        self.data_resolution = None
+        self.loaded_property = None
+        self.dependency = None
+        self.scale_function = None
+        self.batch_size = None
+        self.n_batches = None
+        self.remainder = None
+        self.minibatch = None
+        self.memory_manager = None
+        self.data_manager = None
+        self._database = None
+
+    @property
+    def database(self):
+        """Get the database based on the experiment database path"""
+        if self._database is None:
+            self._database = Database(
+                name=Path(self.experiment.database_path, "database.hdf5").as_posix()
+            )
+        return self._database
+
+    def _run_dependency_check(self):
+        """
+        Check to see if the necessary property exists and build it if required.
+
+        Returns
+        -------
+        Will call transformations if required.
+        """
+
+        if self.loaded_property is None:
+            return
+
+        if self.dependency is not None:
+            dependency = self.database.check_existence(self.dependency[0])
+            if not dependency:
+                self._resolve_dependencies(self.dependency[0])
+
+        loaded_property = self.database.check_existence(self.loaded_property[0])
+        if not loaded_property:
+            self._resolve_dependencies(self.loaded_property[0])
+
+    def _resolve_dependencies(self, dependency):
+        """
+        Resolve any calculation dependencies if possible.
+
+        Parameters
+        ----------
+        dependency : str
+                Name of the dependency to resolve.
+
+        Returns
+        -------
+
+        """
+
+        def _string_to_function(argument):
+            """
+            Select a transformation based on an input
+
+            Parameters
+            ----------
+            argument : str
+                    Name of the transformation required
+
+            Returns
+            -------
+            transformation call.
+            """
+
+            switcher_unwrapping = {
+                "Unwrapped_Positions": self._unwrap_choice(),
+            }
+
+            # add the other transformations and merge the dictionaries
+            switcher = {**switcher_unwrapping, **switcher_transformations}
+
+            choice = switcher.get(
+                argument, lambda: "Data not in database and can not be generated."
+            )
+            return choice
+
+        transformation = _string_to_function(dependency)
+        self.experiment.perform_transformation(transformation)
+
+    def _unwrap_choice(self):
+        """
+        Unwrap either with indices or with box arrays.
+        Returns
+        -------
+
+        """
+        indices = self.database.check_existence("Box_Images")
+        if indices:
+            return "UnwrapViaIndices"
+        else:
+            return "UnwrapCoordinates"
+
+    def _handle_tau_values(self) -> np.array:
+        """
+        Handle the parsing of custom tau values.
+
+        Returns
+        -------
+        times : np.array
+            The time values corresponding to the selected tau values
+        """
+        if isinstance(self.args.tau_values, int):
+            self.data_resolution = self.args.tau_values
+            self.args.tau_values = np.linspace(
+                0, self.args.data_range - 1, self.args.tau_values, dtype=int
+            )
+        if isinstance(self.args.tau_values, list) or isinstance(
+            self.args.tau_values, np.ndarray
+        ):
+            self.data_resolution = len(self.args.tau_values)
+            self.args.data_range = self.args.tau_values[-1] + 1
+        if isinstance(self.args.tau_values, slice):
+            self.args.tau_values = np.linspace(
+                0, self.args.data_range - 1, self.args.data_range, dtype=int
+            )[self.args.tau_values]
+            self.data_resolution = len(self.args.tau_values)
+
+        times = (
+            np.asarray(self.args.tau_values)
+            * self.experiment.time_step
+            * self.experiment.sample_rate
+        )
+
+        return times
+
+    def _prepare_managers(self, data_path: list, correct: bool = False):
+        """
+        Prepare the memory and tensor_values monitors for calculation.
+
+        Parameters
+        ----------
+        data_path : list
+                List of tensor_values paths to load from the hdf5
+                database_path.
+
+        Returns
+        -------
+        Updates the calculator class
+        """
+        self.memory_manager = MemoryManager(
+            data_path=data_path,
+            database=self.database,
+            memory_fraction=0.8,
+            scale_function=self.scale_function,
+            gpu=self.gpu,
+        )
+        (
+            self.batch_size,
+            self.n_batches,
+            self.remainder,
+        ) = self.memory_manager.get_batch_size(system=self.system_property)
+
+        self.ensemble_loop, minibatch = self.memory_manager.get_ensemble_loop(
+            self.args.data_range, self.args.correlation_time
+        )
+        if minibatch:
+            self.batch_size = self.memory_manager.batch_size
+            self.n_batches = self.memory_manager.n_batches
+            self.remainder = self.memory_manager.remainder
+
+        if correct:
+            self._correct_batch_properties()
+
+        self.data_manager = DataManager(
+            data_path=data_path,
+            database=self.database,
+            data_range=self.args.data_range,
+            batch_size=self.batch_size,
+            n_batches=self.n_batches,
+            ensemble_loop=self.ensemble_loop,
+            correlation_time=self.args.correlation_time,
+            remainder=self.remainder,
+            atom_selection=self.args.atom_selection,
+            minibatch=minibatch,
+            atom_batch_size=self.memory_manager.atom_batch_size,
+            n_atom_batches=self.memory_manager.n_atom_batches,
+            atom_remainder=self.memory_manager.atom_remainder,
+        )
+
+    def _correct_batch_properties(self):
+        """
+        Fix batch properties.
+
+        Notes
+        -----
+        This method is called by some calculator
+        """
+        raise NotImplementedError
+
+    def get_batch_dataset(
+        self,
+        subject_list: list = None,
+        loop_array: np.ndarray = None,
+        correct: bool = False,
+    ):
+        """
+        Collect the batch loop dataset
+
+        Parameters
+        ----------
+        correct
+        subject_list : list (default = None)
+                A str of subjects to collect data for in case this is necessary. The
+                method will first try to split this string by an '_' in the case where
+                tuples have been parsed. If None, the method assumes that this is a
+                system calculator and returns a generator appropriate to such an
+                analysis.
+                e.g. subject = ['Na']
+                     subject = ['Na', 'Cl', 'K']
+                     subject = ['Ionic_Current']
+        loop_array : np.ndarray (default = None)
+                If this is not None, elements of this array will be looped over in
+                in the batches which load data at their indices. For example,
+                    loop_array = [[1, 4, 7], [10, 13, 16], [19, 21, 24]]
+                In this case, in the fist batch, configurations 1, 4, and 7 will be
+                loaded for the analysis. This is particularly important in the
+                structural properties.
+
+        Returns
+        -------
+        dataset : tf.data.Dataset
+                A TensorFlow dataset for the batch loop to be iterated over.
+
+        """
+        path_list = [join_path(item, self.loaded_property[0]) for item in subject_list]
+        self._prepare_managers(path_list, correct=correct)
+
+        type_spec = {}
+        for item in subject_list:
+            dict_ref = "/".join([item, self.loaded_property[0]])
+            type_spec[str.encode(dict_ref)] = tf.TensorSpec(
+                shape=self.loaded_property[1], dtype=self.dtype
+            )
+        type_spec[str.encode("data_size")] = tf.TensorSpec(shape=(), dtype=tf.int32)
+
+        batch_generator, batch_generator_args = self.data_manager.batch_generator(
+            system=self.system_property, loop_array=loop_array
+        )
+        ds = tf.data.Dataset.from_generator(
+            generator=batch_generator,
+            args=batch_generator_args,
+            output_signature=type_spec,
+        )
+
+        return ds.prefetch(tf.data.AUTOTUNE)
+
+    def get_ensemble_dataset(self, batch: dict, subject: str):
+        """
+        Collect the ensemble loop dataset.
+
+        Parameters
+        ----------
+        split
+        subject
+        batch : tf.Tensor
+                A batch of data to be looped over in ensembles.
+
+        Returns
+        -------
+        dataset : tf.data.Dataset
+                A TensorFlow dataset object for the ensemble loop to be iterated over.
+
+        """
+        (
+            ensemble_generator,
+            ensemble_generators_args,
+        ) = self.data_manager.ensemble_generator(
+            glob_data=batch, system=self.system_property
+        )
+
+        type_spec = {}
+
+        loop_list = [subject]
+        for item in loop_list:
+            dict_ref = "/".join([item, self.loaded_property[0]])
+            type_spec[str.encode(dict_ref)] = tf.TensorSpec(
+                shape=self.loaded_property[1], dtype=self.dtype
+            )
+
+        ds = tf.data.Dataset.from_generator(
+            generator=ensemble_generator,
+            args=ensemble_generators_args,
+            output_signature=type_spec,
+        )
+
+        return ds.prefetch(tf.data.AUTOTUNE)
diff --git a/mdsuite/database/__init__.py b/mdsuite/database/__init__.py
index fb1dcb10..af3b4dcb 100644
--- a/mdsuite/database/__init__.py
+++ b/mdsuite/database/__init__.py
@@ -24,3 +24,9 @@
 Summary
 -------
 """
+from .simulation_data_class import SimulationProperties
+from .data_manager import DataManager
+
+simulation_properties = SimulationProperties()
+
+__all__ = ["simulation_properties", "DataManager"]
diff --git a/mdsuite/database/calculator_database.py b/mdsuite/database/calculator_database.py
index 7fa41686..9eb7b83a 100644
--- a/mdsuite/database/calculator_database.py
+++ b/mdsuite/database/calculator_database.py
@@ -28,7 +28,7 @@
 
 import mdsuite.database.scheme as db
 from collections import Counter
-from dataclasses import dataclass, field
+from dataclasses import dataclass, field, fields
 from sqlalchemy import and_
 from mdsuite.utils.meta_functions import is_jsonable
 
@@ -47,6 +47,13 @@ class ComputationResults:
     subjects: dict = field(default_factory=list)
 
 
+@dataclass
+class Args:
+    """Dummy Class for type hinting"""
+
+    pass
+
+
 def conv_to_db(val):
     """Convert the given value to something that can be stored in the database"""
     if is_jsonable(val):
@@ -71,53 +78,44 @@ def __init__(self, experiment):
         self.analysis_name = None
         self.load_data = None
 
+        self.args = Args()
+
         self._queued_data = []
 
         # List of computation attributes that will be added to the database
         self.db_computation_attributes = []
 
-    def clean_cache(self):
-        """Clean the lists of computed data"""
-        self._queued_data = []
-        self.db_computation_attributes = []
-
     def prepare_db_entry(self):
         """Prepare a database entry based on the attributes defined in the init"""
         with self.experiment.project.session as ses:
             experiment = (
                 ses.query(db.Experiment)
-                    .filter(db.Experiment.name == self.experiment.name)
-                    .first()
+                .filter(db.Experiment.name == self.experiment.name)
+                .first()
             )
 
         self.db_computation = db.Computation(experiment=experiment)
         self.db_computation.name = self.analysis_name
 
-    def update_db_entry_with_kwargs(self, **kwargs):
-        """Update the database entry with the given user args/kwargs
-
-        Parameters
-        ----------
-        kwargs: all arguments that are passed to the call method and should be stored
-        in the database
-
-
-        Notes
-        -----
-        This does require kwargs, args do not work!
+    def get_computation_data(self) -> db.Computation:
+        """Query the database for computation data
 
-        Returns
-        -------
-        db.Computation:
-            Either a db.Computation object if the calculation was already performed or
-            None
+        This method used the self.args dataclass to look for matching
+        calculator attributes and returns a db.Computation object if
+        the calculation has already been performed
 
+        Return
+        ------
+        db.Computation
+            Returns the computation object from the database if available,
+            otherwise returns None
         """
+        log.debug(f"Getting data for {self.experiment.name} with args {self.args}")
         with self.experiment.project.session as ses:
             experiment = (
                 ses.query(db.Experiment)
-                    .filter(db.Experiment.name == self.experiment.name)
-                    .first()
+                .filter(db.Experiment.name == self.experiment.name)
+                .first()
             )
 
             #  filter the correct experiment
@@ -126,8 +124,10 @@ def update_db_entry_with_kwargs(self, **kwargs):
                 db.Computation.name == self.analysis_name,
             )
 
-            # filter the passed arguments and only run, if they changed
-            for key, val in kwargs.items():
+            # filter set args
+            for args_field in fields(self.args):
+                key = args_field.name
+                val = getattr(self.args, key)
                 computations = computations.filter(
                     db.Computation.computation_attributes.any(
                         and_(
@@ -143,9 +143,8 @@ def update_db_entry_with_kwargs(self, **kwargs):
                 db.Computation.computation_attributes.any(
                     and_(
                         db.ComputationAttribute.name == "version",
-                        db.ComputationAttribute.data == conv_to_db(
-                            self.experiment.version
-                        )
+                        db.ComputationAttribute.data
+                        == conv_to_db(self.experiment.version),
                     )
                 )
             )
@@ -155,6 +154,7 @@ def update_db_entry_with_kwargs(self, **kwargs):
                 log.debug("Calculation already performed! Loading it up")
             # loading data_dict to avoid DetachedInstance errors
             # this can take some time, depending on the size of the data
+            # TODO remove and use lazy call
             for computation in computations:
                 _ = computation.data_dict
                 _ = computation.data_range
@@ -166,19 +166,29 @@ def update_db_entry_with_kwargs(self, **kwargs):
                     " given arguments!"
                 )
             return computations[0]  # it should only be one value
-        else:
-            for key, val in kwargs.items():
-                computation_attribute = db.ComputationAttribute(
-                    name=key, data=conv_to_db(val)
-                )
+        return None
 
-                self.db_computation_attributes.append(computation_attribute)
+    def save_computation_args(self):
+        """Store the user args
 
-            # save the current experiment version in the ComputationAttributes
-            experiment_version = db.ComputationAttribute(
-                name="version", data=conv_to_db(self.experiment.version)
+        This method stored the user args from the self.args dataclass
+        into SQLAlchemy objects and adds them to a list which will be
+        written to the database after the calculation was successful.
+        """
+        for args_field in fields(self.args):
+            key = args_field.name
+            val = getattr(self.args, key)
+            computation_attribute = db.ComputationAttribute(
+                name=key, data=conv_to_db(val)
             )
-            self.db_computation_attributes.append(experiment_version)
+
+            self.db_computation_attributes.append(computation_attribute)
+
+        # save the current experiment version in the ComputationAttributes
+        experiment_version = db.ComputationAttribute(
+            name="version", data=conv_to_db(self.experiment.version)
+        )
+        self.db_computation_attributes.append(experiment_version)
 
     def save_db_data(self):
         """Save all the collected computationattributes and computation data to the
@@ -206,8 +216,8 @@ def save_db_data(self):
                     # otherwise I would use .in_
                     species_list.append(
                         ses.query(db.ExperimentSpecies)
-                            .filter(db.ExperimentSpecies.name == species)
-                            .first()
+                        .filter(db.ExperimentSpecies.name == species)
+                        .first()
                     )
                 # in case of e.g. `System` species will be [None], which is then removed
                 species_list = [x for x in species_list if x is not None]
@@ -286,4 +296,5 @@ def _load_rdf_from_file(self, computation: db.Computation):
         # self.radii = np.array(computation.data_dict["x"]).astype(float)[1:]
         # self.rdf = np.array(computation.data_dict["y"]).astype(float)[1:]
 
+
 #####################
diff --git a/mdsuite/database/data_manager.py b/mdsuite/database/data_manager.py
index 7f2dafd6..aaa0c055 100644
--- a/mdsuite/database/data_manager.py
+++ b/mdsuite/database/data_manager.py
@@ -23,11 +23,14 @@
 
 Summary
 -------
+Module for the data manager. The data manager handles loading of data as TensorFlow
+generators. These generators allow for the full use of the TF data pipelines but can
+required special formatting rules.
 """
 import logging
-import sys
 import numpy as np
 import tensorflow as tf
+import numpy as np
 from tqdm import tqdm
 from mdsuite.database.simulation_database import Database
 
@@ -86,10 +89,18 @@ def __init__(
         self.atom_selection = atom_selection
 
     def batch_generator(
-        self, dictionary: bool = False, system: bool = False, remainder: bool = False
+        self,
+        dictionary: bool = False,
+        system: bool = False,
+        remainder: bool = False,
+        loop_array: np.ndarray = None,
     ) -> tuple:
         """
         Build a generator object for the batch loop
+
+        Parameters
+        ----------
+
         Returns
         -------
         Returns a generator function and its arguments
@@ -131,20 +142,30 @@ def generator(
             database = Database(name=database)
 
             for batch in range(batch_number + int(remainder)):
+
                 start = int(batch * batch_size) + self.offset
                 stop = int(start + batch_size)
                 data_size = tf.cast(batch_size, dtype=tf.int32)
+                # Handle the remainder
                 if batch == batch_number:
                     stop = int(start + self.remainder)
                     data_size = tf.cast(self.remainder, dtype=tf.int16)
-                if type(self.atom_selection) is dict:
-                    select_slice = {}
-                    for item in self.atom_selection:
-                        select_slice[item] = np.s_[
-                            self.atom_selection[item], start:stop
-                        ]
+                    # TODO make default
+
+                if loop_array is not None:
+                    select_slice = np.s_[:, loop_array[batch]]
+                elif system:
+                    select_slice = np.s_[start:stop]
                 else:
-                    select_slice = np.s_[self.atom_selection, start:stop]
+                    if type(self.atom_selection) is dict:
+                        select_slice = {}
+                        for item in self.atom_selection:
+                            select_slice[item] = np.s_[
+                                self.atom_selection[item], start:stop
+                            ]
+                    else:
+                        select_slice = np.s_[self.atom_selection, start:stop]
+
                 yield database.load_data(
                     data_path,
                     select_slice=select_slice,
@@ -152,7 +173,7 @@ def generator(
                     d_size=data_size,
                 )
 
-        def system_generator(
+        def atom_generator(
             batch_number: int,
             batch_size: int,
             database: str,
@@ -160,7 +181,7 @@ def system_generator(
             dictionary: bool,
         ):
             """
-            Generator function for the batch loop.
+            Generator function for a mini-batched calculation.
 
             Parameters
             ----------
@@ -176,45 +197,14 @@ def system_generator(
                     If true, tensor_values is returned in a dictionary
             Returns
             -------
-
             """
-            database = Database(name=database)
-
-            for batch in range(batch_number + int(remainder)):  # +1 for the remainder
-                start = int(batch * batch_size) + self.offset
-                stop = int(start + batch_size)
-                if batch == batch_number:
-                    stop = int(start + self.remainder)
-
-                yield database.load_data(
-                    data_path, select_slice=np.s_[start:stop], dictionary=dictionary
+            # Atom selection not currently available for mini-batched calculations
+            if type(self.atom_selection) is dict:
+                raise ValueError(
+                    "Atom selection is not currently available "
+                    "for mini-batched calculations"
                 )
 
-        def atom_generator(
-            batch_number: int,
-            batch_size: int,
-            database: str,
-            data_path: list,
-            dictionary: bool,
-        ):
-            """
-            Generator function for the batch loop.
-
-            Parameters
-            ----------
-            batch_number : int
-                    Number of batches to be looped over
-            batch_size : int
-                    size of each batch to load
-            database : Database
-                    database_path from which to load the tensor_values
-            data_path : str
-                    Path to the tensor_values in the database_path
-            dictionary : bool
-                    If true, tensor_values is returned in a dictionary
-            Returns
-            -------
-            """
             database = Database(name=database)
             _atom_remainder = [1 if self.atom_remainder else 0][0]
             start = 0
@@ -235,14 +225,8 @@ def atom_generator(
                     if batch == batch_number:
                         stop = int(start + self.remainder)
                         data_size = tf.cast(self.remainder, dtype=tf.int16)
-                    if type(self.atom_selection) is dict:
-                        log.warning(
-                            "Atom selection is not available for mini-batched"
-                            " calculations"
-                        )
-                        sys.exit(1)
-                    else:
-                        select_slice = np.s_[atom_start:atom_stop, start:stop]
+
+                    select_slice = np.s_[atom_start:atom_stop, start:stop]
                     yield database.load_data(
                         data_path,
                         select_slice=select_slice,
@@ -250,18 +234,13 @@ def atom_generator(
                         d_size=data_size,
                     )
 
-        if self.remainder == 0:
-            remainder = False
-
-        if system:
-            return system_generator, args
-        elif self.minibatch:
+        if self.minibatch:
             return atom_generator, args
         else:
             return generator, args
 
     def ensemble_generator(
-        self, system: bool = False, dictionary: bool = False
+        self, system: bool = False, dictionary: bool = False, glob_data: dict = None
     ) -> tuple:
         """
         Build a generator for the ensemble loop
@@ -303,7 +282,7 @@ def generator(ensemble_loop, correlation_time, data_range, data):
                 stop = start + data_range
                 yield data[:, start:stop]
 
-        def system_generator(ensemble_loop, correlation_time, data_range, data):
+        def dictionary_generator(ensemble_loop, correlation_time, data_range):
             """
             Generator for the ensemble loop
             Parameters
@@ -314,9 +293,6 @@ def system_generator(ensemble_loop, correlation_time, data_range, data):
                     Distance between ensembles
             data_range : int
                     Size of each ensemble
-            data : tf.data.Dataset
-                    Data from which to draw ensembles
-
             Returns
             -------
             None
@@ -324,39 +300,16 @@ def system_generator(ensemble_loop, correlation_time, data_range, data):
             for ensemble in range(ensemble_loop):
                 start = ensemble * correlation_time
                 stop = start + data_range
-                yield data[start:stop]
-
-        def dictionary_generator(
-            ensemble_loop, correlation_time, data_range, data_dict
-        ):
-            """
-            Generator for the ensemble loop
-            Parameters
-            ----------
-            ensemble_loop : int
-                    Number of ensembles to loop over
-            correlation_time : int
-                    Distance between ensembles
-            data_range : int
-                    Size of each ensemble
-            data_dict : Dictionary
-                    Data from which to draw ensembles
+                output_dict = {}
+                for item in glob_data:
+                    if item == str.encode("data_size"):
+                        pass
+                    else:
+                        if system:
+                            output_dict[item] = glob_data[item][start:stop]
+                        else:
+                            output_dict[item] = glob_data[item][:, start:stop]
 
-            Returns
-            -------
-            None
-            """
-            for ensemble in range(ensemble_loop):
-                start = ensemble * correlation_time
-                stop = start + data_range
-                output_dict = []
-                for item in data_dict[:-1]:
-                    output_dict[item] = data_dict[item][:, start:stop]
                 yield output_dict
 
-        if system:
-            return system_generator, args
-        elif dictionary:
-            return dictionary_generator, args
-        else:
-            return generator, args
+        return dictionary_generator, args
diff --git a/mdsuite/database/experiment_database.py b/mdsuite/database/experiment_database.py
index 8f742d57..4674d2b0 100644
--- a/mdsuite/database/experiment_database.py
+++ b/mdsuite/database/experiment_database.py
@@ -45,7 +45,50 @@
 log = logging.getLogger(__name__)
 
 
+class LazyProperty:
+    """Property preset for I/O with the database
+
+    References
+    ----------
+    https://realpython.com/python-descriptors/
+    """
+
+    def __set_name__(self, owner, name):
+        """See https://www.python.org/dev/peps/pep-0487/"""
+        self.name = name
+
+    def __get__(self, instance: ExperimentDatabase, owner):
+        """Get the value either from memory or from the database
+
+        Try to get the value from memory, if not write it to memory
+        """
+        try:
+            return instance.__dict__[self.name]
+        except KeyError:
+            instance.__dict__[self.name] = instance.get_db(self.name)
+            return self.__get__(instance, owner)
+
+    def __set__(self, instance: ExperimentDatabase, value):
+        """Write value to the database
+
+        Write the given value to the database and remove it from memory
+        """
+        if value is None:
+            return
+        instance.set_db(self.name, value)
+        instance.__dict__.pop(self.name, None)
+
+
 class ExperimentDatabase:
+    temperature = LazyProperty()
+    time_step = LazyProperty()
+    number_of_configurations = LazyProperty()
+    number_of_atoms = LazyProperty()
+    sample_rate = LazyProperty()
+    volume = LazyProperty()
+    property_groups = LazyProperty()
+    radial_distribution_function_state = LazyProperty()
+
     def __init__(self, project: Project, experiment_name):
         self.project = project
         self.name = experiment_name
@@ -73,7 +116,7 @@ def export_property_data(self, parameters: dict) -> List[db.Computation]:
             "This function has been removed and replaced by queue_database"
         )
 
-    def _set_db(self, name: str, value):
+    def set_db(self, name: str, value):
         """Store values in the database
 
         Parameters
@@ -93,7 +136,7 @@ def _set_db(self, name: str, value):
             attribute.data = value
             ses.commit()
 
-    def _get_db(self, name: str, default=None):
+    def get_db(self, name: str, default=None):
         """Load values from the database
 
         Parameters
@@ -234,7 +277,7 @@ def molecules(self, value):
     @property
     def box_array(self):
         """Get the sample_rate of the experiment"""
-        return self._get_db(name="box_array")
+        return self.get_db(name="box_array")
 
     @box_array.setter
     def box_array(self, value):
@@ -244,110 +287,19 @@ def box_array(self, value):
         if isinstance(value, np.ndarray):
             value = value.tolist()
 
-        self._set_db(name="box_array", value=value)
-
-    # Lazy Properties:
-    @property
-    def temperature(self):
-        """Get the temperature of the experiment"""
-        return self._get_db(name="temperature")
-
-    @temperature.setter
-    def temperature(self, value):
-        """Set the temperature of the experiment"""
-        if value is None:
-            return
-        self._set_db(name="temperature", value=value)
-
-    @property
-    def time_step(self):
-        """Get the time_step of the experiment"""
-        return self._get_db(name="time_step")
-
-    @time_step.setter
-    def time_step(self, value):
-        """Set the time_step of the experiment"""
-        if value is None:
-            return
-        self._set_db(name="time_step", value=value)
+        self.set_db(name="box_array", value=value)
 
     @property
     def units(self) -> Dict[str, float]:
         """Get the units of the experiment"""
-        return self._get_db(name="units")
+        return self.get_db(name="units")
 
     @units.setter
     def units(self, value: Units):
         """Set the units of the experiment"""
         if value is None:
             return
-        self._set_db(name="units", value=asdict(value))
-
-    @property
-    def number_of_configurations(self) -> int:
-        """Get the time_step of the experiment"""
-        return self._get_db(name="number_of_configurations")
-
-    @number_of_configurations.setter
-    def number_of_configurations(self, value):
-        """Set the time_step of the experiment"""
-        if value is None:
-            return
-        self._set_db(name="number_of_configurations", value=value)
-
-    @property
-    def number_of_atoms(self) -> int:
-        """Get the time_step of the experiment"""
-        return self._get_db(name="number_of_atoms")
-
-    @number_of_atoms.setter
-    def number_of_atoms(self, value):
-        """Set the time_step of the experiment"""
-        if value is None:
-            return
-        self._set_db(name="number_of_atoms", value=value)
-
-    @property
-    def sample_rate(self):
-        """Get the sample_rate of the experiment"""
-        return self._get_db(name="sample_rate")
-
-    @sample_rate.setter
-    def sample_rate(self, value):
-        """Set the time_step of the experiment"""
-        if value is None:
-            return
-        self._set_db(name="sample_rate", value=value)
-
-    @property
-    def volume(self):
-        """Get the volume of the experiment"""
-        return self._get_db(name="volume")
-
-    @volume.setter
-    def volume(self, value):
-        """Set the volume of the experiment"""
-        if value is None:
-            return
-        self._set_db(name="volume", value=value)
-
-    @property
-    def property_groups(self):
-        """Get the property groups from the database
-
-        Returns
-        -------
-        property_groups: dict
-                Example: {'Positions': [3, 4, 5], 'Velocities': [6, 7, 8], ...}
-        """
-        return self._get_db(name="property_groups")
-
-    @property_groups.setter
-    def property_groups(self, value):
-        """Write the property groups to the database"""
-        if value is None:
-            return
-        self._set_db(name="property_groups", value=value)
+        self.set_db(name="units", value=asdict(value))
 
     @property
     def read_files(self):
@@ -359,7 +311,7 @@ def read_files(self):
             A List of all files that were added to the database already
 
         """
-        return self._get_db(name="read_files", default=[])
+        return self.get_db(name="read_files", default=[])
 
     @read_files.setter
     def read_files(self, value):
@@ -375,27 +327,7 @@ def read_files(self, value):
         """
         if value is None:
             return
-        self._set_db(name="read_files", value=value)
-
-    @property
-    def radial_distribution_function_state(self) -> bool:
-        """Get the radial_distribution_function_state of the experiment
-
-        Returns
-        -------
-        bool:
-            the state of the RDF
-        # TODO this method could potentially be replaced by a quick query of the
-           database
-        """
-        return self._get_db(name="radial_distribution_function_state", default=False)
-
-    @radial_distribution_function_state.setter
-    def radial_distribution_function_state(self, value):
-        """Set the radial_distribution_function_state of the experiment"""
-        if value is None:
-            return
-        self._set_db(name="radial_distribution_function_state", value=value)
+        self.set_db(name="read_files", value=value)
 
     @property
     def simulation_data(self) -> dict:
@@ -408,7 +340,7 @@ def simulation_data(self) -> dict:
         dict: A dictionary containing all simulation_data
 
         """
-        return self._get_db(name="simulation_data", default={})
+        return self.get_db(name="simulation_data", default={})
 
     @simulation_data.setter
     def simulation_data(self, value: dict):
@@ -429,7 +361,7 @@ def simulation_data(self, value: dict):
         """
         if value is None:
             return
-        self._set_db(name="simulation_data", value=value)
+        self.set_db(name="simulation_data", value=value)
 
     @property
     def version(self) -> int:
@@ -437,7 +369,7 @@ def version(self) -> int:
 
         Versioning starts at 0 and can be increased by +1 for every added file
         """
-        return self._get_db(name="version", default=0)
+        return self.get_db(name="version", default=0)
 
     @version.setter
     def version(self, value: int):
@@ -448,4 +380,4 @@ def version(self, value: int):
         """
         if value is None:
             return
-        self._set_db(name="version", value=value)
+        self.set_db(name="version", value=value)
diff --git a/mdsuite/database/scheme.py b/mdsuite/database/scheme.py
index 8510d0b9..049fcce8 100644
--- a/mdsuite/database/scheme.py
+++ b/mdsuite/database/scheme.py
@@ -276,7 +276,7 @@ def data_range(self) -> int:
         """Get the data_range stored in computation_attributes"""
         for comp_attr in self.computation_attributes:
             if comp_attr.name == "data_range":
-                return int(comp_attr.data['serialized_value'])
+                return int(comp_attr.data["serialized_value"])
 
     @property
     def subjects(self) -> list:
diff --git a/mdsuite/database/simulation_data_class.py b/mdsuite/database/simulation_data_class.py
new file mode 100644
index 00000000..89ee0e8c
--- /dev/null
+++ b/mdsuite/database/simulation_data_class.py
@@ -0,0 +1,62 @@
+"""
+MDSuite: A Zincwarecode package.
+
+License
+-------
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License v2.0 which accompanies this distribution, and is
+available at https://www.eclipse.org/legal/epl-v20.html
+
+SPDX-License-Identifier: EPL-2.0
+
+Copyright Contributors to the Zincwarecode Project.
+
+Contact Information
+-------------------
+email: zincwarecode@gmail.com
+github: https://github.com/zincware
+web: https://zincwarecode.com/
+
+Citation
+--------
+If you use this module please cite us with:
+
+Summary
+-------
+"""
+from dataclasses import dataclass
+
+
+@dataclass(frozen=True)
+class SimulationProperties:
+    """
+    A dataclass for the simulation properties.
+
+    Simulation properties refer to the observables measured during a simulation
+    for which groups will exist in the HDF5 database.
+    """
+
+    temperature = ("Temperature", (None, 1))
+    time = ("Time", (None, 1))
+    thermal_flux = ("Thermal_Flux", (None, 3))
+    stress_viscosity = ("Stress_visc", (None, 3))
+    momentum_flux = ("Momentum_Flux", (None, 3))
+    ionic_current = ("Ionic_Current", (None, 3))
+    translational_dipole_moment = ("Translational_Dipole_Moment", (None, 3))
+    positions = ("Positions", (None, None, 3))
+    scaled_positions = ("Scaled_Positions", (None, None, 3))
+    unwrapped_positions = ("Unwrapped_Positions", (None, None, 3))
+    scaled_unwrapped_positions = ("Scaled_Unwrapped_Positions", (None, None, 3))
+    velocities = ("Velocities", (None, None, 3))
+    forces = ("Forces", (None, None, 3))
+    box_images = ("Box_Images", (None, None, 3))
+    dipole_orientation_magnitude = ("Dipole_Orientation_Magnitude", (None, None, 1))
+    angular_velocity_spherical = ("Angular_Velocity_Spherical", (None, None, 3))
+    angular_velocity_non_spherical = ("Angular_Velocity_Non_Spherical", (None, None, 3))
+    torque = ("Torque", (None, None, 3))
+    integrated_heat_current = ("Integrated_Heat_Current", (None, 3))
+    kinaci_heat_current = ("Kinaci_Heat_Current", (None, 3))
+    charge = ("Charge", (None, None, 1))
+    kinetic_energy = ("KE", (None, None, 1))
+    potential_energy = ("PE", (None, None, 1))
+    stress = ("Stress", (None, None, 6))
diff --git a/mdsuite/database/simulation_database.py b/mdsuite/database/simulation_database.py
index 4498ce87..4b36b371 100644
--- a/mdsuite/database/simulation_database.py
+++ b/mdsuite/database/simulation_database.py
@@ -30,9 +30,9 @@
 from mdsuite.utils.exceptions import DatabaseDoesNotExist
 import tensorflow as tf
 import time
-from typing import Union
 import pandas as pd
 
+
 var_names = [
     "Temperature",
     "Time",
@@ -562,42 +562,34 @@ def load_data(
         -------
 
         """
-        data: Union[list, dict] = {}
         if scaling is None:
             scaling = [1 for _ in range(len(path_list))]
 
         with hf.File(self.name, "r") as database:
-            if not dictionary:
-                data = []
-                for i, item in enumerate(path_list):
-                    data.append(
-                        tf.convert_to_tensor(
-                            database[item][select_slice], dtype=tf.float64
-                        )
-                        * scaling[i]
-                    )
+            data = {}
+            for i, item in enumerate(path_list):
+                if type(select_slice) is dict:
+                    my_slice = select_slice[item]
+                else:
+                    my_slice = select_slice
 
-            if dictionary:
-                data = {}
-                for item in path_list:
-                    if type(select_slice) is dict:
-                        my_slice = select_slice[item]
-                    else:
-                        my_slice = select_slice
-                    data[item] = tf.convert_to_tensor(
-                        database[item][my_slice], dtype=tf.float64
-                    )
-                data[str.encode("data_size")] = d_size
+                data[item] = (
+                    tf.convert_to_tensor(database[item][my_slice], dtype=tf.float64)
+                    * scaling[i]
+                )
+            data[str.encode("data_size")] = d_size
 
-        if len(data) == 1:
-            if dictionary:
-                return data
-            else:
-                return data[0]
-        if dictionary:
-            return data
-        else:
-            return data
+            # else:
+            #     data = []
+            #     for i, item in enumerate(path_list):
+            #         data.append(
+            #             tf.convert_to_tensor(
+            #                 database[item][select_slice], dtype=tf.float64
+            #             )
+            #             * scaling[i]
+            #         )
+
+        return data
 
     def get_load_time(self, database_path: str = None):
         """
@@ -667,77 +659,6 @@ def get_data_size(
 
         return data_tuple
 
-    def export_csv(self, group: str, key: str = None, sub_key: str = None):
-        """
-        Export a csv of database data.
-
-        This method is not designed for simulation databases, only for analysis
-        databases.
-
-        Parameters
-        ----------
-        group : str
-                Group in the database from which data should be loaded
-        key  : str
-                Additional identifier.
-        sub_key : str
-                Additional identifier.
-
-        Returns
-        -------
-        saves a csv to the working directory.
-        """
-        if self.architecture == "simulation":
-            print("Method cannot be called in simulation databases.")
-            return
-
-        identifier = None
-        with hf.File(self.name, "r") as database:
-            first_layer = list(database.keys())
-            for item in first_layer:
-                if group in item:
-                    identifier = item
-            if identifier is None:
-                print("This group does not seem to exist.")
-                return
-            second_layer = list(database[identifier].keys())
-            if type(database[identifier][second_layer[0]]) is hf.Dataset:
-                for item in second_layer:
-                    data = np.array(database[identifier][item])
-                    data = data.reshape((len(data[0]), 2))
-                    df = pd.DataFrame(data=data, columns=["column1", "column2"])
-                    df.to_csv(f"{identifier}_{item}.csv")
-            else:
-                if key is None:
-                    for item in second_layer:
-                        third_layer = list(database[identifier][item].keys())
-                        for sub_item in third_layer:
-                            data = np.array(database[identifier][item][sub_item])
-                            data = data.reshape((len(data[0]), 2))
-                            df = pd.DataFrame(data=data, columns=["column1", "column2"])
-                            df.to_csv(f"{identifier}_{item}.csv")
-                else:
-                    for item in second_layer:
-                        if key in item:
-                            sub_item = item
-                    if sub_key is None:
-                        for dataset in database[identifier][sub_item]:
-                            data = np.array(database[identifier][sub_item][dataset])
-                            data = data.reshape((len(data[0]), 2))
-                            df = pd.DataFrame(data=data, columns=["column1", "column2"])
-                            df.to_csv(f"{identifier}_{item}_{dataset}.csv")
-                    else:
-                        last_layer = list(database[identifier][sub_item])
-                        for sub_sub_item in last_layer:
-                            if sub_key in sub_sub_item:
-                                final_identifier = sub_sub_item
-                        data = np.array(
-                            database[identifier][sub_item][final_identifier]
-                        )
-                        data = data.reshape((len(data[0]), 2))
-                        df = pd.DataFrame(data=data, columns=["column1", "column2"])
-                        df.to_csv(f"{identifier}_{item}_{final_identifier}.csv")
-
     def get_database_summary(self):
         """
         Print a summary of the database properties.
diff --git a/mdsuite/experiment/experiment.py b/mdsuite/experiment/experiment.py
index 1e5865e9..1684d53b 100644
--- a/mdsuite/experiment/experiment.py
+++ b/mdsuite/experiment/experiment.py
@@ -149,11 +149,6 @@ def __init__(
         self.logfile_path: str
         self._create_internal_file_paths()  # fill the path attributes
 
-        # Memory properties
-        self.memory_requirements = (
-            {}
-        )  # TODO I think this can be removed. - Not until all calcs are updated.
-
         # Check if the experiment exists and load if it does.
         self._load_or_build()
 
@@ -263,9 +258,6 @@ def _load_or_build(self) -> bool:
             self._build_model()
             return False
 
-    def save_class(self):
-        log.warning("Using depreciated method `save_class`!")
-
     def perform_transformation(self, transformation_name, **kwargs):
         """
         Perform a transformation on the experiment.
diff --git a/mdsuite/graph_modules/molecular_graph.py b/mdsuite/graph_modules/molecular_graph.py
index 3998f8e3..b31809d7 100644
--- a/mdsuite/graph_modules/molecular_graph.py
+++ b/mdsuite/graph_modules/molecular_graph.py
@@ -166,10 +166,13 @@ def build_configuration_graph(self, cutoff: float, adjacency: bool = True):
 
         """
         path_list = [join_path(species, "Positions") for species in self.species]
-        configuration_tensor = tf.concat(
-            self.database.load_data(path_list=path_list, select_slice=np.s_[:, 0]),
-            axis=0,
+        data_dict = self.database.load_data(
+            path_list=path_list, select_slice=np.s_[:, 0]
         )
+        data = []
+        for item in path_list:
+            data.append(data_dict[item])
+        configuration_tensor = tf.concat(data, axis=0)
         distance_matrix = self.get_neighbour_list(
             configuration_tensor, cell=self.experiment.box_array
         )
diff --git a/mdsuite/memory_management/__init__.py b/mdsuite/memory_management/__init__.py
index fb1dcb10..4df5406b 100644
--- a/mdsuite/memory_management/__init__.py
+++ b/mdsuite/memory_management/__init__.py
@@ -24,3 +24,6 @@
 Summary
 -------
 """
+from .memory_manager import MemoryManager
+
+__all__ = ["MemoryManager"]
diff --git a/mdsuite/project/project.py b/mdsuite/project/project.py
index b04f4029..c0c46822 100644
--- a/mdsuite/project/project.py
+++ b/mdsuite/project/project.py
@@ -35,6 +35,7 @@
 import mdsuite.database.scheme as db
 from mdsuite.experiment import Experiment
 from mdsuite.utils import Units
+from mdsuite.utils.helpers import NoneType
 
 from typing import Dict
 
@@ -123,7 +124,7 @@ def __str__(self):
 
     def add_experiment(
         self,
-        experiment: str = None,
+        experiment: str = NoneType,
         timestep: float = None,
         temperature: float = None,
         units: Union[str, Units] = None,
@@ -152,7 +153,16 @@ def add_experiment(
                 in calculation.
         units : str
                 LAMMPS units used
+
+        Notes
+        ------
+        Using custom NoneType to raise a custom ValueError message with useful info.
         """
+        if experiment is NoneType:
+            raise ValueError(
+                "Experiment can not be empty! "
+                "Use None to automatically generate a unique name."
+            )
 
         if experiment is None:
             experiment = f"Experiment_{datetime.now().strftime('%Y%m%d-%H%M%S')}"
@@ -298,7 +308,7 @@ def run(self) -> RunComputation:
         RunComputation:
             class that has all available calculators as properties
         """
-        return RunComputation(experiments=[x for x in self.experiments.values()])
+        return RunComputation(experiments=[x for x in self.active_experiments.values()])
 
     @property
     def experiments(self) -> Dict[str, Experiment]:
@@ -329,7 +339,7 @@ def active_experiments(self) -> Dict[str, Experiment]:
         """Get a DotDict of instantiated experiments that are currently selected!"""
 
         active_experiment = {
-            key: val for key, val in self._experiments.items() if val.active
+            key: val for key, val in self.experiments.items() if val.active
         }
 
         return DotDict(active_experiment)
diff --git a/mdsuite/transformations/kinaci_integrated_heat_current.py b/mdsuite/transformations/kinaci_integrated_heat_current.py
index 08bd8910..62f87dbc 100644
--- a/mdsuite/transformations/kinaci_integrated_heat_current.py
+++ b/mdsuite/transformations/kinaci_integrated_heat_current.py
@@ -140,13 +140,15 @@ def _compute_thermal_flux(self):
             (positions_path, velocities_path, forces_path, pe_path)
         )
 
-        self._prepare_monitors(data_path)
+        self._prepare_monitors(data_path)  # create the data and memory managers.
+
         # update the dictionary (mutable object)
         type_spec = self._update_species_type_dict(type_spec, positions_path, 3)
         type_spec = self._update_species_type_dict(type_spec, velocities_path, 3)
         type_spec = self._update_species_type_dict(type_spec, forces_path, 3)
         type_spec = self._update_species_type_dict(type_spec, pe_path, 1)
         type_spec[str.encode("data_size")] = tf.TensorSpec(None, dtype=tf.int16)
+
         batch_generator, batch_generator_args = self.data_manager.batch_generator(
             dictionary=True, remainder=True
         )
diff --git a/mdsuite/transformations/map_molecules.py b/mdsuite/transformations/map_molecules.py
index 2143c0d5..066e14d2 100644
--- a/mdsuite/transformations/map_molecules.py
+++ b/mdsuite/transformations/map_molecules.py
@@ -215,9 +215,13 @@ def _load_batch(self, path_list: list, slice: np.s_, factor: list) -> tf.Tensor:
         data : tf.Tensor
                 A tensor of stacked data.
         """
-        data = self.database.load_data(
-            path_list=path_list, select_slice=slice, scaling=factor
+        data_dict = self.database.load_data(
+            path_list=path_list, select_slice=slice, scaling=factor, dictionary=True
         )
+        data = []
+        for item in path_list:
+            data.append(data_dict[item])
+
         return tf.concat(data, axis=0)
 
     def _prepare_mass_array(self, species: list) -> list:
@@ -255,7 +259,6 @@ def _map_molecules(self):
             )
             path_list = [join_path(s, "Unwrapped_Positions") for s in species]
             self._prepare_monitors(data_path=path_list)
-            # TODO for #338
             scaling_factor = self.reference_molecules[item]["mass"]
             molecules = self.experiment.molecules
             molecules[item] = {}
@@ -354,4 +357,3 @@ def run_transformation(self):
         self.experiment.perform_transformation(
             "WrapCoordinates", species=[item for item in self.molecules]
         )
-        self.experiment.save_class()
diff --git a/mdsuite/transformations/scale_coordinates.py b/mdsuite/transformations/scale_coordinates.py
index 1ae73c1c..1df03b98 100644
--- a/mdsuite/transformations/scale_coordinates.py
+++ b/mdsuite/transformations/scale_coordinates.py
@@ -29,6 +29,7 @@
 import sys
 import tensorflow as tf
 import numpy as np
+from mdsuite.database import simulation_properties
 from tqdm import tqdm
 
 
@@ -63,6 +64,7 @@ def __init__(self, experiment: object, species: list = None):
         super().__init__(experiment)
         self.species = species
         self.scale_function = {"linear": {"scale_factor": 2}}
+        self.dtype = tf.float64
 
         if self.species is None:
             self.species = list(self.experiment.species)
@@ -164,19 +166,25 @@ def _scale_coordinates(self):
             data_structure = self._prepare_database_entry(species)
             data_path = [join_path(species, "Scaled_Positions")]
             self._prepare_monitors(data_path)
-            batch_generator, batch_generator_args = self.data_manager.batch_generator()
+            batch_generator, batch_generator_args = self.data_manager.batch_generator(
+                dictionary=True
+            )
+            type_spec = {
+                str.encode(data_path[0]): tf.TensorSpec(
+                    shape=simulation_properties.scaled_positions[1], dtype=self.dtype
+                ),
+                str.encode("data_size"): tf.TensorSpec(shape=(), dtype=tf.int32),
+            }
             data_set = tf.data.Dataset.from_generator(
                 batch_generator,
                 args=batch_generator_args,
-                output_signature=tf.TensorSpec(
-                    shape=(None, self.batch_size, 3), dtype=tf.float64
-                ),
+                output_signature=type_spec,
             )
             data_set = data_set.prefetch(tf.data.experimental.AUTOTUNE)
             for index, x in tqdm(
                 enumerate(data_set), ncols=70, desc=f"{species}: Scaling Coordinates"
             ):
-                data = self._transformation(x)
+                data = self._transformation(x[str.encode(data_path[0])])
                 self._save_coordinates(
                     data=data,
                     data_structure=data_structure,
diff --git a/mdsuite/transformations/transformations.py b/mdsuite/transformations/transformations.py
index 2e2e796a..f1a7f53a 100644
--- a/mdsuite/transformations/transformations.py
+++ b/mdsuite/transformations/transformations.py
@@ -23,6 +23,7 @@
 
 Summary
 -------
+Parent class for the transformations.
 """
 from __future__ import annotations
 import time
diff --git a/mdsuite/transformations/unwrap_via_indices.py b/mdsuite/transformations/unwrap_via_indices.py
index c8a16c5b..065eabc3 100644
--- a/mdsuite/transformations/unwrap_via_indices.py
+++ b/mdsuite/transformations/unwrap_via_indices.py
@@ -27,6 +27,7 @@
 from mdsuite.transformations.transformations import Transformations
 from mdsuite.utils.meta_functions import join_path
 import sys
+from mdsuite.database import simulation_properties
 import tensorflow as tf
 import numpy as np
 from tqdm import tqdm
@@ -61,6 +62,8 @@ def __init__(self, experiment: object, species: list = None):
         super().__init__(experiment)
         self.scale_function = {"linear": {"scale_factor": 2}}
 
+        self.dtype = tf.float64
+
         self.species = species
         if self.species is None:
             self.species = list(self.experiment.species)
@@ -85,7 +88,7 @@ def _check_for_indices(self):
             )
             sys.exit(1)
 
-    def _transformation(self, data: tf.Tensor):
+    def _transformation(self, data: tf.Tensor, dict_keys: list):
         """
         Apply the transformation to a batch of tensor_values.
 
@@ -93,13 +96,17 @@ def _transformation(self, data: tf.Tensor):
         ----------
         data : tf.Tensor
                 Data on which to operate.
+        dict_keys : list
+                Dict reference keys to use when selecting data.
 
         Returns
         -------
         Scaled coordinates : tf.Tensor
                 Coordinates scaled by the image number.
         """
-        return data[0] + tf.math.multiply(data[1], self.experiment.box_array)
+        return data[dict_keys[0]] + tf.math.multiply(
+            data[dict_keys[1]], self.experiment.box_array
+        )
 
     def _save_unwrapped_coordinates(
         self, data: tf.Tensor, index: int, batch_size: int, data_structure: dict
@@ -194,13 +201,26 @@ def _unwrap_particles(self):
                 join_path(species, "Box_Images"),
             ]
             self._prepare_monitors(data_path)
-            batch_generator, batch_generator_args = self.data_manager.batch_generator()
+            batch_generator, batch_generator_args = self.data_manager.batch_generator(
+                dictionary=True
+            )
+            dict_ref = [
+                str.encode(join_path(species, "Positions")),
+                str.encode(join_path(species, "Box_Images")),
+            ]
+            type_spec = {
+                dict_ref[0]: tf.TensorSpec(
+                    shape=simulation_properties.positions[1], dtype=self.dtype
+                ),
+                dict_ref[1]: tf.TensorSpec(
+                    shape=simulation_properties.box_images[1], dtype=self.dtype
+                ),
+                str.encode("data_size"): tf.TensorSpec(shape=(), dtype=tf.int32),
+            }
             data_set = tf.data.Dataset.from_generator(
                 batch_generator,
                 args=batch_generator_args,
-                output_signature=tf.TensorSpec(
-                    shape=(2, None, self.batch_size, 3), dtype=tf.float64
-                ),
+                output_signature=type_spec,
             )
             data_set = data_set.prefetch(tf.data.experimental.AUTOTUNE)
             for index, batch in tqdm(
@@ -208,7 +228,7 @@ def _unwrap_particles(self):
                 ncols=70,
                 desc=f"{species}: Unwrapping Coordinates.",
             ):
-                data = self._transformation(batch)
+                data = self._transformation(batch, dict_ref)
                 self._save_coordinates(
                     data=data,
                     data_structure=data_structure,
diff --git a/mdsuite/transformations/wrap_coordinates.py b/mdsuite/transformations/wrap_coordinates.py
index 3d0b14b4..89d8a2ed 100644
--- a/mdsuite/transformations/wrap_coordinates.py
+++ b/mdsuite/transformations/wrap_coordinates.py
@@ -105,10 +105,10 @@ def _load_data(self, species):
         """
         Load the tensor_values to be unwrapped
         """
-        path = join_path(species, "Unwrapped_Positions")
-        self.data = np.array(
-            self.experiment.load_matrix(path=[path], select_slice=np.s_[:])
-        )
+        path_list = [join_path(species, "Unwrapped_Positions")]
+        self.data = self.database.load_data(
+            path_list=path_list, select_slice=np.s_[:], dictionary=True
+        )[path_list[0]]
 
     def _center_box(self):
         """
@@ -193,7 +193,6 @@ def wrap_particles(self):
                 )
 
         self.experiment.memory_requirements = self.database.get_memory_information()
-        self.experiment.save_class()
 
     def run_transformation(self):
         """
diff --git a/mdsuite/utils/calculator_helper_methods.py b/mdsuite/utils/calculator_helper_methods.py
new file mode 100644
index 00000000..a52b4d8b
--- /dev/null
+++ b/mdsuite/utils/calculator_helper_methods.py
@@ -0,0 +1,165 @@
+"""
+MDSuite: A Zincwarecode package.
+
+License
+-------
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License v2.0 which accompanies this distribution, and is
+available at https://www.eclipse.org/legal/epl-v20.html
+
+SPDX-License-Identifier: EPL-2.0
+
+Copyright Contributors to the Zincwarecode Project.
+
+Contact Information
+-------------------
+email: zincwarecode@gmail.com
+github: https://github.com/zincware
+web: https://zincwarecode.com/
+
+Citation
+--------
+If you use this module please cite us with:
+
+Summary
+-------
+Static methods used in calculators are kept here rather than polluting the parent class.
+"""
+import numpy as np
+from scipy.optimize import curve_fit
+import random
+
+
+def fit_einstein_curve(data: list) -> list:
+    """
+    Fit operation for Einstein calculations
+
+    Parameters
+    ----------
+    data : list
+            x and y tensor_values for the fitting [np.array, np.array] of
+            (2, data_range)
+
+    Returns
+    -------
+    fit results : list
+            A tuple list with the fit value along with the error of the fit
+    """
+
+    fits = []  # define an empty fit array so errors may be extracted
+
+    def func(x, m, a):
+        """
+        Standard linear function for fitting.
+
+        Parameters
+        ----------
+        x : list/np.array
+                x axis tensor_values for the fit
+        m : float
+                gradient of the line
+        a : float
+                scalar offset, also the y-intercept for those who did not
+                get much maths in school.
+
+        Returns
+        -------
+
+        """
+        return m * x + a
+
+    # get the logarithmic dataset
+    log_y = np.log10(data[1][1:])
+    log_x = np.log10(data[0][1:])
+
+    min_end_index, max_end_index = int(0.8 * len(log_y)), int(len(log_y) - 1)
+    min_start_index, max_start_index = int(0.3 * len(log_y)), int(0.5 * len(log_y))
+
+    for _ in range(100):
+        end_index = random.randint(min_end_index, max_end_index)
+        start_index = random.randint(min_start_index, max_start_index)
+
+        popt, pcov = curve_fit(
+            func, log_x[start_index:end_index], log_y[start_index:end_index]
+        )
+        fits.append(10 ** popt[1])
+
+    return [np.mean(fits), np.std(fits)]
+
+
+# def _optimize_einstein_data_range(self, data: np.array):
+#     """
+#     Optimize the tensor_values range of a experiment using the Einstein
+#     method of calculation.
+#
+#     Parameters
+#     ----------
+#     data : np.array (2, data_range)
+#             MSD to study
+#
+#     Returns
+#     -------
+#     Updates the data_range attribute of the class state
+#
+#     Notes
+#     -----
+#     TODO: Update this and add it to the code.
+#     """
+#
+#     def func(x, m, a):
+#         """
+#         Standard linear function for fitting.
+#
+#         Parameters
+#         ----------
+#         x : list/np.array
+#                 x axis tensor_values for the fit
+#         m : float
+#                 gradient of the line
+#         a : float
+#                 scalar offset, also the y-intercept for those who did not
+#                 get much maths in school.
+#
+#         Returns
+#         -------
+#
+#         """
+#
+#         return m * x + a
+#
+#     # get the logarithmic dataset
+#     log_y = np.log10(data[0])
+#     log_x = np.log10(data[1])
+#
+#     end_index = int(len(log_y) - 1)
+#     start_index = int(0.4 * len(log_y))
+#
+#     popt, pcov = curve_fit(
+#         func, log_x[start_index:end_index], log_y[start_index:end_index]
+#     )
+#
+#     if 0.85 < popt[0] < 1.15:
+#         self.loop_condition = True
+#
+#     else:
+#         try:
+#             self.args.data_range = int(1.1 * self.args.data_range)
+#             self.time = np.linspace(
+#                 0.0,
+#                 self.args.data_range
+#                 * self.experiment.time_step
+#                 * self.experiment.sample_rate,
+#                 self.args.data_range,
+#             )
+#
+#             # end the calculation if the tensor_values range exceeds the relevant
+#             # bounds
+#             if (
+#                 self.args.data_range
+#                 > self.experiment.number_of_configurations
+#                 - self.args.correlation_time
+#             ):
+#                 print("Trajectory not long enough to perform analysis.")
+#                 raise RangeExceeded
+#         except RangeExceeded:
+#             raise RangeExceeded
diff --git a/mdsuite/utils/helpers.py b/mdsuite/utils/helpers.py
new file mode 100644
index 00000000..3c15b075
--- /dev/null
+++ b/mdsuite/utils/helpers.py
@@ -0,0 +1,25 @@
+"""
+This program and the accompanying materials are made available under the terms of the
+Eclipse Public License v2.0 which accompanies this distribution, and is available at
+https://www.eclipse.org/legal/epl-v20.html
+SPDX-License-Identifier: EPL-2.0
+
+Copyright Contributors to the Zincware Project.
+
+Description: 
+"""
+
+
+class NoneType:
+    """Custom NoneType that is != None
+
+    Examples
+    --------
+    you can check for NoneType or None
+    >>> x = NoneType
+    >>> x is NoneType
+    >>> x is not None
+    """
+
+    def __init__(self):
+        raise NotImplementedError()
diff --git a/mdsuite/utils/linalg.py b/mdsuite/utils/linalg.py
index eb54b0ab..a60f9001 100644
--- a/mdsuite/utils/linalg.py
+++ b/mdsuite/utils/linalg.py
@@ -139,3 +139,112 @@ def apply_system_cutoff(tensor: tf.Tensor, cutoff: float) -> tf.Tensor:
     cutoff_mask = tf.cast(tf.less(tensor, cutoff), dtype=tf.bool)  # Construct the mask
 
     return tf.boolean_mask(tensor, cutoff_mask)
+
+
+def cartesian_to_spherical_coordinates(
+    point_cartesian, name="cartesian_to_spherical_coordinates"
+):
+    """
+    References
+    ----------
+    https://www.tensorflow.org/graphics/api_docs/python/tfg/math/math_helpers/cartesian_to_spherical_coordinates
+
+    Function to transform Cartesian coordinates to spherical coordinates.
+    This function assumes a right handed coordinate system with `z` pointing up.
+    When `x` and `y` are both `0`, the function outputs `0` for `phi`. Note that
+    the function is not smooth when `x = y = 0`.
+    Note:
+      In the following, A1 to An are optional batch dimensions.
+    Args:
+      point_cartesian: A tensor of shape `[A1, ..., An, 3]`. In the last
+        dimension, the data follows the `x`, `y`, `z` order.
+      eps: A small `float`, to be added to the denominator. If left as `None`, its
+        value is automatically selected using `point_cartesian.dtype`.
+      name: A name for this op. Defaults to "cartesian_to_spherical_coordinates".
+    Returns:
+      A tensor of shape `[A1, ..., An, 3]`. The last dimensions contains
+      (`r`,`theta`,`phi`), where `r` is the sphere radius, `theta` is the polar
+      angle and `phi` is the azimuthal angle. Returns `NaN` gradient if x = y = 0.
+    """
+    with tf.name_scope(name):
+        point_cartesian = tf.convert_to_tensor(value=point_cartesian)
+
+        x, y, z = tf.unstack(point_cartesian, axis=-1)
+        radius = tf.norm(tensor=point_cartesian, axis=-1)
+        theta = tf.acos(tf.clip_by_value(tf.math.divide_no_nan(z, radius), -1.0, 1.0))
+        phi = tf.atan2(y, x)
+        return tf.stack((radius, theta, phi), axis=-1)
+
+
+def spherical_to_cartesian_coordinates(
+    point_spherical, name="spherical_to_cartesian_coordinates"
+):
+    """
+    References
+    ----------
+    https://www.tensorflow.org/graphics/api_docs/python/tfg/math/math_helpers/spherical_to_cartesian_coordinates
+
+    Function to transform Cartesian coordinates to spherical coordinates.
+    Note:
+      In the following, A1 to An are optional batch dimensions.
+    Args:
+      point_spherical: A tensor of shape `[A1, ..., An, 3]`. The last dimension
+        contains r, theta, and phi that respectively correspond to the radius,
+        polar angle and azimuthal angle; r must be non-negative.
+      name: A name for this op. Defaults to "spherical_to_cartesian_coordinates".
+    Raises:
+      tf.errors.InvalidArgumentError: If r, theta or phi contains out of range
+      data.
+    Returns:
+      A tensor of shape `[A1, ..., An, 3]`, where the last dimension contains the
+      cartesian coordinates in x,y,z order.
+    """
+    with tf.name_scope(name):
+        point_spherical = tf.convert_to_tensor(value=point_spherical)
+
+        r, theta, phi = tf.unstack(point_spherical, axis=-1)
+        tmp = r * tf.sin(theta)
+        x = tmp * tf.cos(phi)
+        y = tmp * tf.sin(phi)
+        z = r * tf.cos(theta)
+        return tf.stack((x, y, z), axis=-1)
+
+
+def get2dHistogram(x, y, value_range, nbins=100, dtype=tf.dtypes.int32):
+    """
+    Bins x, y coordinates of points onto simple square 2d histogram
+
+    Given the tensor x and y:
+    x: x coordinates of points
+    y: y coordinates of points
+    this operation returns a rank 2 `Tensor`
+    representing the indices of a histogram into which each element
+    of `values` would be binned. The bins are equal width and
+    determined by the arguments `value_range` and `nbins`.
+
+
+    Parameters
+    ----------
+    x:  Numeric `Tensor`.
+    y: Numeric `Tensor`.
+    value_range[0] lims for x
+    value_range[1] lims for y
+
+    nbins:  Scalar `int32 Tensor`.  Number of histogram bins.
+    dtype:  dtype for returned histogram.
+
+    References
+    ----------
+    https://gist.github.com/isentropic/a86effab2c007e86912a50f995cac52b
+
+    """
+    x_range = value_range[0]
+    y_range = value_range[1]
+
+    histy_bins = tf.histogram_fixed_width_bins(y, y_range, nbins=nbins, dtype=dtype)
+
+    H = tf.map_fn(
+        lambda i: tf.histogram_fixed_width(x[histy_bins == i], x_range, nbins=nbins),
+        tf.range(nbins),
+    )
+    return H  # Matrix!
diff --git a/mdsuite/utils/meta_functions.py b/mdsuite/utils/meta_functions.py
index e186990e..b40ac3de 100644
--- a/mdsuite/utils/meta_functions.py
+++ b/mdsuite/utils/meta_functions.py
@@ -45,7 +45,6 @@
 log = logging.getLogger(__name__)
 
 
-
 # https://stackoverflow.com/questions/42033142/is-there-an-easy-way-to-check-if-an-object-is-json-serializable-in-python
 def is_jsonable(x: dict) -> bool:
     """
diff --git a/mdsuite/visualizer/d3_data_visualizer.py b/mdsuite/visualizer/d3_data_visualizer.py
index dfb46f13..f03ab314 100644
--- a/mdsuite/visualizer/d3_data_visualizer.py
+++ b/mdsuite/visualizer/d3_data_visualizer.py
@@ -23,12 +23,15 @@
 
 Summary
 -------
+Module for the MDSuite 3d visualizer.
 """
 import open3d as o3d
 import open3d.visualization.gui as gui
 from typing import Union
 from PIL.ImageColor import getcolor
 import importlib.resources
+from matplotlib import cm
+import matplotlib.pyplot as plt
 import numpy as np
 import json
 
@@ -38,7 +41,13 @@ class DataVisualizer3D:
     Class for the visualizer of three dimensional data.
     """
 
-    def __init__(self, data: np.ndarray, title: str, center: Union[str, dict] = None):
+    def __init__(
+        self,
+        data: np.ndarray,
+        title: str,
+        center: Union[str, dict] = None,
+        colour_map: np.ndarray = None,
+    ):
         """
         Constructor for the data visualizer.
 
@@ -50,16 +59,39 @@ def __init__(self, data: np.ndarray, title: str, center: Union[str, dict] = None
                 centre molecule to be displayed (optional)
         title : str
                 title of the plot.
+        colour_map : np.ndarray
+                A colour map to apply to the data.
         """
         self.data = data
         self.title = title
         self.center = center
-
+        self.colour_map = colour_map
         self.point_cloud = o3d.geometry.PointCloud()
         self.point_cloud.points = o3d.utility.Vector3dVector(self.data)
+        self.point_cloud.colors = o3d.utility.Vector3dVector(self._build_colour_map())
+
+        plt.plot(
+            np.linspace(0, len(self.colour_map), len(self.colour_map), dtype=int),
+            self.colour_map,
+            ".",
+        )
+        plt.show()
 
         self._build_app()
 
+    def _build_colour_map(self):
+        """
+        Build the colour map
+        Returns
+        -------
+
+        """
+        viridis = cm.get_cmap("binary")
+
+        dat = viridis(self.colour_map)[:, :-1]
+
+        return dat
+
     def _build_app(self):
         """
         Build the app window and update the class.
diff --git a/requirements.txt b/requirements.txt
index 63be8510..d39f2649 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,3 @@
-sphinxcontrib.bibtex
-pybtex
-nbsphinx
-sphinx_rtd_theme
-numpydoc
-sphinx-copybutton
-colorutils
-IPython
-pandoc
-pytest
 tensorflow>=2.5
 h5py
 pysmiles
@@ -24,5 +14,6 @@ sqlalchemy >= 1.4
 pandas >= 1.0.0
 tensorflow_probability
 open3d
+colorutils
 bokeh
-git+git://github.com/zincware/ZincHub.git#egg=zinchub
\ No newline at end of file
+pandoc
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 1cc6244f..53519803 100644
--- a/setup.py
+++ b/setup.py
@@ -29,6 +29,10 @@
 with open("README.rst", "r") as fh:
     long_description = fh.read()
 
+
+with open("requirements.txt") as f:
+    required = f.read().splitlines()
+
 setuptools.setup(
     name="MDSuite",
     version="0.0.1",
@@ -47,23 +51,5 @@
     package_data={"": ["form_fac_coeffs.csv"]},
     include_package_data=True,
     python_requires=">=3.7",
-    install_requires=[
-        "tensorflow>=2.5",
-        "h5py",
-        "pysmiles",
-        "matplotlib",
-        "scipy",
-        "tqdm",
-        "psutil>=5.6.6",
-        "numpy",
-        "gputil",
-        "pubchempy",
-        "PyYAML>=5.4",
-        "scooby",
-        "sqlalchemy >= 1.4",
-        "pandas >= 1.0.0",
-        "tensorflow_probability",
-        "open3d",
-        "bokeh",
-    ],
+    install_requires=required,
 )


Test patch:
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 25e4dcf0..270d18d5 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -31,7 +31,7 @@ jobs:
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install -r requirements.txt
+          pip install -r dev-requirements.txt
       - name: Install package
         run: |
           pip install .
diff --git a/CI/integration_tests/calculators/__test_einstein_distinct_diffusion_coefficients.py b/CI/integration_tests/calculators/__test_einstein_distinct_diffusion_coefficients.py
index 8c40b9d3..b9d558c7 100644
--- a/CI/integration_tests/calculators/__test_einstein_distinct_diffusion_coefficients.py
+++ b/CI/integration_tests/calculators/__test_einstein_distinct_diffusion_coefficients.py
@@ -31,52 +31,27 @@
 import pytest
 
 import numpy as np
-import urllib.request
-import gzip
-import shutil
 from pathlib import Path
-
-import data as static_data
 import mdsuite as mds
+from zinchub import DataHub
 
 
 @pytest.fixture(scope="session")
-def traj_files(tmp_path_factory) -> list:
-    """Download files into a temporary directory and keep them for all tests"""
-    # time_step = 0.002
-    # temperature = 1400.0
-    base_url = "https://github.com/zincware/ExampleData/raw/main/"
-
-    files_in_url = [
-        "NaCl_gk_i_q.lammpstraj",
-        # "NaCl_gk_ni_nq.lammpstraj",
-        # "NaCl_i_q.lammpstraj",
-        # "NaCl_ni_nq.lammpstraj",
-    ]
-
-    files = []
+def traj_file(tmp_path_factory) -> str:
+    """Download trajectory file into a temporary directory and keep it for all tests"""
     temporary_path = tmp_path_factory.getbasetemp()
 
-    for item in files_in_url:
-        filename, headers = urllib.request.urlretrieve(
-            f"{base_url}{item}.gz", filename=f"{temporary_path / item}.gz"
-        )
-        with gzip.open(filename, "rb") as f_in:
-            new_file = temporary_path / item
-            with open(new_file, "wb") as f_out:
-                shutil.copyfileobj(f_in, f_out)
-
-            files.append(new_file.as_posix())
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    NaCl.get_file(path=temporary_path)
 
-    return files
+    return (temporary_path / NaCl.file_raw).as_posix()
 
 
 @pytest.fixture(scope="session")
 def true_values() -> dict:
-    """Values to compare to"""
-    static_path = Path(static_data.__file__).parent
-    data = static_path / "einstein_distinct_diffusion_coefficients.json"
-    return json.loads(data.read_bytes())
+    """Example fixture for downloading analysis results from github"""
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    return NaCl.get_analysis(analysis="RadialDistributionFunction.json")
 
 
 def test_eddc_project(traj_files, true_values, tmp_path):
diff --git a/CI/integration_tests/calculators/__test_green_kubo_distinct_diffusion_coefficients.py b/CI/integration_tests/calculators/__test_green_kubo_distinct_diffusion_coefficients.py
index dbe9773a..15e36ff9 100644
--- a/CI/integration_tests/calculators/__test_green_kubo_distinct_diffusion_coefficients.py
+++ b/CI/integration_tests/calculators/__test_green_kubo_distinct_diffusion_coefficients.py
@@ -24,59 +24,31 @@
 Summary
 -------
 """
-
 import json
 import os
-
 import pytest
-
 import numpy as np
-import urllib.request
-import gzip
-import shutil
 from pathlib import Path
-
-import data as static_data
 import mdsuite as mds
+from zinchub import DataHub
 
 
 @pytest.fixture(scope="session")
-def traj_files(tmp_path_factory) -> list:
-    """Download files into a temporary directory and keep them for all tests"""
-    # time_step = 0.002
-    # temperature = 1400.0
-    base_url = "https://github.com/zincware/ExampleData/raw/main/"
-
-    files_in_url = [
-        "NaCl_gk_i_q.lammpstraj",
-        # "NaCl_gk_ni_nq.lammpstraj",
-        # "NaCl_i_q.lammpstraj",
-        # "NaCl_ni_nq.lammpstraj",
-    ]
-
-    files = []
+def traj_file(tmp_path_factory) -> str:
+    """Download trajectory file into a temporary directory and keep it for all tests"""
     temporary_path = tmp_path_factory.getbasetemp()
 
-    for item in files_in_url:
-        filename, headers = urllib.request.urlretrieve(
-            f"{base_url}{item}.gz", filename=f"{temporary_path / item}.gz"
-        )
-        with gzip.open(filename, "rb") as f_in:
-            new_file = temporary_path / item
-            with open(new_file, "wb") as f_out:
-                shutil.copyfileobj(f_in, f_out)
-
-            files.append(new_file.as_posix())
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    NaCl.get_file(path=temporary_path)
 
-    return files
+    return (temporary_path / NaCl.file_raw).as_posix()
 
 
 @pytest.fixture(scope="session")
 def true_values() -> dict:
-    """Values to compare to"""
-    static_path = Path(static_data.__file__).parent
-    data = static_path / "green_kubo_distinct_diffusion_coefficients.json"
-    return json.loads(data.read_bytes())
+    """Example fixture for downloading analysis results from github"""
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    return NaCl.get_analysis(analysis="RadialDistributionFunction.json")
 
 
 def test_project(traj_files, true_values, tmp_path):
diff --git a/CI/integration_tests/calculators/__test_structure_factor.py b/CI/integration_tests/calculators/__test_structure_factor.py
index d22c0ce3..ca3c37ae 100644
--- a/CI/integration_tests/calculators/__test_structure_factor.py
+++ b/CI/integration_tests/calculators/__test_structure_factor.py
@@ -26,56 +26,29 @@
 """
 import json
 import os
-
 import pytest
-
 import numpy as np
-import urllib.request
-import gzip
-import shutil
 from pathlib import Path
-
-import data as static_data
+from zinchub import DataHub
 import mdsuite as mds
 
 
 @pytest.fixture(scope="session")
-def traj_files(tmp_path_factory) -> list:
-    """Download files into a temporary directory and keep them for all tests"""
-    # time_step = 0.002
-    # temperature = 1400.0
-    base_url = "https://github.com/zincware/ExampleData/raw/main/"
-
-    files_in_url = [
-        "NaCl_gk_i_q.lammpstraj",
-        # "NaCl_gk_ni_nq.lammpstraj",
-        # "NaCl_i_q.lammpstraj",
-        # "NaCl_ni_nq.lammpstraj",
-    ]
-
-    files = []
+def traj_file(tmp_path_factory) -> str:
+    """Download trajectory file into a temporary directory and keep it for all tests"""
     temporary_path = tmp_path_factory.getbasetemp()
 
-    for item in files_in_url:
-        filename, headers = urllib.request.urlretrieve(
-            f"{base_url}{item}.gz", filename=f"{temporary_path / item}.gz"
-        )
-        with gzip.open(filename, "rb") as f_in:
-            new_file = temporary_path / item
-            with open(new_file, "wb") as f_out:
-                shutil.copyfileobj(f_in, f_out)
-
-            files.append(new_file.as_posix())
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    NaCl.get_file(path=temporary_path)
 
-    return files
+    return (temporary_path / NaCl.file_raw).as_posix()
 
 
 @pytest.fixture(scope="session")
 def true_values() -> dict:
-    """Values to compare to"""
-    static_path = Path(static_data.__file__).parent
-    data = static_path / "structure_factor.json"
-    return json.loads(data.read_bytes())
+    """Example fixture for downloading analysis results from github"""
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    return NaCl.get_analysis(analysis="StructureFactor.json")
 
 
 def test_structure_factor_project(traj_files, true_values, tmp_path):
diff --git a/CI/integration_tests/calculators/_test_coordination_numbers.py b/CI/integration_tests/calculators/_test_coordination_numbers.py
index f51c5b2f..be0e64a7 100644
--- a/CI/integration_tests/calculators/_test_coordination_numbers.py
+++ b/CI/integration_tests/calculators/_test_coordination_numbers.py
@@ -24,58 +24,29 @@
 Summary
 -------
 """
-import json
 import os
-
 import pytest
-
 import numpy as np
-import urllib.request
-import gzip
-import shutil
-from pathlib import Path
-
-import data as static_data
+from zinchub import DataHub
 import mdsuite as mds
 
 
 @pytest.fixture(scope="session")
-def traj_files(tmp_path_factory) -> list:
-    """Download files into a temporary directory and keep them for all tests"""
-    # time_step = 0.002
-    # temperature = 1400.0
-    base_url = "https://github.com/zincware/ExampleData/raw/main/"
-
-    files_in_url = [
-        "NaCl_gk_i_q.lammpstraj",
-        # "NaCl_gk_ni_nq.lammpstraj",
-        # "NaCl_i_q.lammpstraj",
-        # "NaCl_ni_nq.lammpstraj",
-    ]
-
-    files = []
+def traj_file(tmp_path_factory) -> str:
+    """Download trajectory file into a temporary directory and keep it for all tests"""
     temporary_path = tmp_path_factory.getbasetemp()
 
-    for item in files_in_url:
-        filename, headers = urllib.request.urlretrieve(
-            f"{base_url}{item}.gz", filename=f"{temporary_path / item}.gz"
-        )
-        with gzip.open(filename, "rb") as f_in:
-            new_file = temporary_path / item
-            with open(new_file, "wb") as f_out:
-                shutil.copyfileobj(f_in, f_out)
-
-            files.append(new_file.as_posix())
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    NaCl.get_file(path=temporary_path)
 
-    return files
+    return (temporary_path / NaCl.file_raw).as_posix()
 
 
 @pytest.fixture(scope="session")
 def true_values() -> dict:
-    """Values to compare to"""
-    static_path = Path(static_data.__file__).parent
-    data = static_path / "coordination_numbers.json"
-    return json.loads(data.read_bytes())
+    """Example fixture for downloading analysis results from github"""
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    return NaCl.get_analysis(analysis="CoordinationNumbers.json")
 
 
 def test_project(traj_files, true_values, tmp_path):
diff --git a/CI/integration_tests/calculators/_test_einstein_helfand_thermal_kinaci.py b/CI/integration_tests/calculators/_test_einstein_helfand_thermal_kinaci.py
index 56ae7e67..a5cafc8d 100644
--- a/CI/integration_tests/calculators/_test_einstein_helfand_thermal_kinaci.py
+++ b/CI/integration_tests/calculators/_test_einstein_helfand_thermal_kinaci.py
@@ -27,56 +27,29 @@
 
 import json
 import os
-
 import pytest
-
 import numpy as np
-import urllib.request
-import gzip
-import shutil
 from pathlib import Path
-
-import data as static_data
 import mdsuite as mds
+from zinchub import DataHub
 
 
 @pytest.fixture(scope="session")
-def traj_files(tmp_path_factory) -> list:
-    """Download files into a temporary directory and keep them for all tests"""
-    # time_step = 0.002
-    # temperature = 1400.0
-    base_url = "https://github.com/zincware/ExampleData/raw/main/"
-
-    files_in_url = [
-        "NaCl_gk_i_q.lammpstraj",
-        # "NaCl_gk_ni_nq.lammpstraj",
-        # "NaCl_i_q.lammpstraj",
-        # "NaCl_ni_nq.lammpstraj",
-    ]
-
-    files = []
+def traj_file(tmp_path_factory) -> str:
+    """Download trajectory file into a temporary directory and keep it for all tests"""
     temporary_path = tmp_path_factory.getbasetemp()
 
-    for item in files_in_url:
-        filename, headers = urllib.request.urlretrieve(
-            f"{base_url}{item}.gz", filename=f"{temporary_path / item}.gz"
-        )
-        with gzip.open(filename, "rb") as f_in:
-            new_file = temporary_path / item
-            with open(new_file, "wb") as f_out:
-                shutil.copyfileobj(f_in, f_out)
-
-            files.append(new_file.as_posix())
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    NaCl.get_file(path=temporary_path)
 
-    return files
+    return (temporary_path / NaCl.file_raw).as_posix()
 
 
 @pytest.fixture(scope="session")
 def true_values() -> dict:
-    """Values to compare to"""
-    static_path = Path(static_data.__file__).parent
-    data = static_path / "einstein_helfand_thermal_kinaci.json"
-    return json.loads(data.read_bytes())
+    """Example fixture for downloading analysis results from github"""
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    return NaCl.get_analysis(analysis="EinsteinHelfandThermanKinaci.json")
 
 
 def test_project(traj_files, true_values, tmp_path):
diff --git a/CI/integration_tests/calculators/_test_green_kubo_thermal_conductivity.py b/CI/integration_tests/calculators/_test_green_kubo_thermal_conductivity.py
index 86af3265..a86bdd10 100644
--- a/CI/integration_tests/calculators/_test_green_kubo_thermal_conductivity.py
+++ b/CI/integration_tests/calculators/_test_green_kubo_thermal_conductivity.py
@@ -26,56 +26,29 @@
 """
 import json
 import os
-
 import pytest
-
 import numpy as np
-import urllib.request
-import gzip
-import shutil
 from pathlib import Path
-
-import data as static_data
 import mdsuite as mds
+from zinchub import DataHub
 
 
 @pytest.fixture(scope="session")
-def traj_files(tmp_path_factory) -> list:
-    """Download files into a temporary directory and keep them for all tests"""
-    # time_step = 0.002
-    # temperature = 1400.0
-    base_url = "https://github.com/zincware/ExampleData/raw/main/"
-
-    files_in_url = [
-        # "NaCl_gk_i_q.lammpstraj",
-        "NaCl_gk_ni_nq.lammpstraj",
-        # "NaCl_i_q.lammpstraj",
-        # "NaCl_ni_nq.lammpstraj",
-    ]
-
-    files = []
+def traj_file(tmp_path_factory) -> str:
+    """Download trajectory file into a temporary directory and keep it for all tests"""
     temporary_path = tmp_path_factory.getbasetemp()
 
-    for item in files_in_url:
-        filename, headers = urllib.request.urlretrieve(
-            f"{base_url}{item}.gz", filename=f"{temporary_path / item}.gz"
-        )
-        with gzip.open(filename, "rb") as f_in:
-            new_file = temporary_path / item
-            with open(new_file, "wb") as f_out:
-                shutil.copyfileobj(f_in, f_out)
-
-            files.append(new_file.as_posix())
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    NaCl.get_file(path=temporary_path)
 
-    return files
+    return (temporary_path / NaCl.file_raw).as_posix()
 
 
 @pytest.fixture(scope="session")
 def true_values() -> dict:
-    """Values to compare to"""
-    static_path = Path(static_data.__file__).parent
-    data = static_path / "green_kubo_thermal_conductivity.json"
-    return json.loads(data.read_bytes())
+    """Example fixture for downloading analysis results from github"""
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    return NaCl.get_analysis(analysis="GreenKuboThermalConductivity.json")
 
 
 def test_roject(traj_files, true_values, tmp_path):
diff --git a/CI/integration_tests/calculators/_test_green_kubo_viscosity.py b/CI/integration_tests/calculators/_test_green_kubo_viscosity.py
index 5e79a333..be6e3d54 100644
--- a/CI/integration_tests/calculators/_test_green_kubo_viscosity.py
+++ b/CI/integration_tests/calculators/_test_green_kubo_viscosity.py
@@ -26,56 +26,29 @@
 """
 import json
 import os
-
 import pytest
-
 import numpy as np
-import urllib.request
-import gzip
-import shutil
 from pathlib import Path
-
-import data as static_data
 import mdsuite as mds
+from zinchub import DataHub
 
 
 @pytest.fixture(scope="session")
-def traj_files(tmp_path_factory) -> list:
-    """Download files into a temporary directory and keep them for all tests"""
-    # time_step = 0.002
-    # temperature = 1400.0
-    base_url = "https://github.com/zincware/ExampleData/raw/main/"
-
-    files_in_url = [
-        # "NaCl_gk_i_q.lammpstraj",
-        "NaCl_gk_ni_nq.lammpstraj",
-        # "NaCl_i_q.lammpstraj",
-        # "NaCl_ni_nq.lammpstraj",
-    ]
-
-    files = []
+def traj_file(tmp_path_factory) -> str:
+    """Download trajectory file into a temporary directory and keep it for all tests"""
     temporary_path = tmp_path_factory.getbasetemp()
 
-    for item in files_in_url:
-        filename, headers = urllib.request.urlretrieve(
-            f"{base_url}{item}.gz", filename=f"{temporary_path / item}.gz"
-        )
-        with gzip.open(filename, "rb") as f_in:
-            new_file = temporary_path / item
-            with open(new_file, "wb") as f_out:
-                shutil.copyfileobj(f_in, f_out)
-
-            files.append(new_file.as_posix())
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    NaCl.get_file(path=temporary_path)
 
-    return files
+    return (temporary_path / NaCl.file_raw).as_posix()
 
 
 @pytest.fixture(scope="session")
 def true_values() -> dict:
-    """Values to compare to"""
-    static_path = Path(static_data.__file__).parent
-    data = static_path / "green_kubo_viscosity.json"
-    return json.loads(data.read_bytes())
+    """Example fixture for downloading analysis results from github"""
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    return NaCl.get_analysis(analysis="GreenKuboViscosity.json")
 
 
 def test_gkv_project(traj_files, true_values, tmp_path):
diff --git a/CI/integration_tests/calculators/_test_green_kubo_viscosity_flux.py b/CI/integration_tests/calculators/_test_green_kubo_viscosity_flux.py
index 186c9f39..01122044 100644
--- a/CI/integration_tests/calculators/_test_green_kubo_viscosity_flux.py
+++ b/CI/integration_tests/calculators/_test_green_kubo_viscosity_flux.py
@@ -31,52 +31,27 @@
 import pytest
 
 import numpy as np
-import urllib.request
-import gzip
-import shutil
 from pathlib import Path
-
-import data as static_data
 import mdsuite as mds
+from zinchub import DataHub
 
 
 @pytest.fixture(scope="session")
-def traj_files(tmp_path_factory) -> list:
-    """Download files into a temporary directory and keep them for all tests"""
-    # time_step = 0.002
-    # temperature = 1400.0
-    base_url = "https://github.com/zincware/ExampleData/raw/main/"
-
-    files_in_url = [
-        "NaCl_gk_i_q.lammpstraj",
-        # "NaCl_gk_ni_nq.lammpstraj",
-        # "NaCl_i_q.lammpstraj",
-        # "NaCl_ni_nq.lammpstraj",
-    ]
-
-    files = []
+def traj_file(tmp_path_factory) -> str:
+    """Download trajectory file into a temporary directory and keep it for all tests"""
     temporary_path = tmp_path_factory.getbasetemp()
 
-    for item in files_in_url:
-        filename, headers = urllib.request.urlretrieve(
-            f"{base_url}{item}.gz", filename=f"{temporary_path / item}.gz"
-        )
-        with gzip.open(filename, "rb") as f_in:
-            new_file = temporary_path / item
-            with open(new_file, "wb") as f_out:
-                shutil.copyfileobj(f_in, f_out)
-
-            files.append(new_file.as_posix())
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    NaCl.get_file(path=temporary_path)
 
-    return files
+    return (temporary_path / NaCl.file_raw).as_posix()
 
 
 @pytest.fixture(scope="session")
 def true_values() -> dict:
-    """Values to compare to"""
-    static_path = Path(static_data.__file__).parent
-    data = static_path / "green_kubo_viscosity_flux.json"
-    return json.loads(data.read_bytes())
+    """Example fixture for downloading analysis results from github"""
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    return NaCl.get_analysis(analysis="GreenKuboViscosityFlux.json")
 
 
 def test_project(traj_files, true_values, tmp_path):
diff --git a/CI/integration_tests/calculators/_test_nernst_einstein_ionic_conductivity.py b/CI/integration_tests/calculators/_test_nernst_einstein_ionic_conductivity.py
index 04bb1f8d..166efa99 100644
--- a/CI/integration_tests/calculators/_test_nernst_einstein_ionic_conductivity.py
+++ b/CI/integration_tests/calculators/_test_nernst_einstein_ionic_conductivity.py
@@ -24,59 +24,31 @@
 Summary
 -------
 """
-
 import json
 import os
-
 import pytest
-
 import numpy as np
-import urllib.request
-import gzip
-import shutil
 from pathlib import Path
-
-import data as static_data
 import mdsuite as mds
+from zinchub import DataHub
 
 
 @pytest.fixture(scope="session")
-def traj_files(tmp_path_factory) -> list:
-    """Download files into a temporary directory and keep them for all tests"""
-    # time_step = 0.002
-    # temperature = 1400.0
-    base_url = "https://github.com/zincware/ExampleData/raw/main/"
-
-    files_in_url = [
-        "NaCl_gk_i_q.lammpstraj",
-        # "NaCl_gk_ni_nq.lammpstraj",
-        # "NaCl_i_q.lammpstraj",
-        # "NaCl_ni_nq.lammpstraj",
-    ]
-
-    files = []
+def traj_file(tmp_path_factory) -> str:
+    """Download trajectory file into a temporary directory and keep it for all tests"""
     temporary_path = tmp_path_factory.getbasetemp()
 
-    for item in files_in_url:
-        filename, headers = urllib.request.urlretrieve(
-            f"{base_url}{item}.gz", filename=f"{temporary_path / item}.gz"
-        )
-        with gzip.open(filename, "rb") as f_in:
-            new_file = temporary_path / item
-            with open(new_file, "wb") as f_out:
-                shutil.copyfileobj(f_in, f_out)
-
-            files.append(new_file.as_posix())
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    NaCl.get_file(path=temporary_path)
 
-    return files
+    return (temporary_path / NaCl.file_raw).as_posix()
 
 
 @pytest.fixture(scope="session")
 def true_values() -> dict:
-    """Values to compare to"""
-    static_path = Path(static_data.__file__).parent
-    data = static_path / "nernst_einstein_ionic_conductivity.json"
-    return json.loads(data.read_bytes())
+    """Example fixture for downloading analysis results from github"""
+    NaCl = DataHub(url="https://github.com/zincware/DataHub/tree/main/NaCl_gk_i_q")
+    return NaCl.get_analysis(analysis="NernstEinsteinIonicConductivity.json")
 
 
 def test_neic_project(traj_files, true_values, tmp_path):
diff --git a/CI/integration_tests/calculators/test_green_kubo_ionic_conductivity.py b/CI/integration_tests/calculators/test_green_kubo_ionic_conductivity.py
index 50222062..3d845720 100644
--- a/CI/integration_tests/calculators/test_green_kubo_ionic_conductivity.py
+++ b/CI/integration_tests/calculators/test_green_kubo_ionic_conductivity.py
@@ -28,6 +28,7 @@
 import os
 import mdsuite as mds
 from zinchub import DataHub
+from mdsuite.utils.testing import assertDeepAlmostEqual
 
 
 @pytest.fixture(scope="session")
@@ -56,4 +57,4 @@ def test_project(traj_file, true_values, tmp_path):
 
     computation = project.run.GreenKuboIonicConductivity(plot=False)
 
-    assert computation["NaCl"].data_dict == true_values
+    assertDeepAlmostEqual(computation["NaCl"].data_dict, true_values, decimal=3)
diff --git a/CI/integration_tests/calculators/test_green_kubo_self_diffusion_coefficients.py b/CI/integration_tests/calculators/test_green_kubo_self_diffusion_coefficients.py
index 643b7271..348fd15a 100644
--- a/CI/integration_tests/calculators/test_green_kubo_self_diffusion_coefficients.py
+++ b/CI/integration_tests/calculators/test_green_kubo_self_diffusion_coefficients.py
@@ -28,6 +28,7 @@
 import os
 import mdsuite as mds
 from zinchub import DataHub
+from mdsuite.utils.testing import assertDeepAlmostEqual
 
 
 @pytest.fixture(scope="session")
@@ -56,4 +57,4 @@ def test_project(traj_file, true_values, tmp_path):
 
     computation = project.run.GreenKuboDiffusionCoefficients(plot=False)
 
-    assert computation["NaCl"].data_dict == true_values
+    assertDeepAlmostEqual(computation["NaCl"].data_dict, true_values, decimal=3)