diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/core/processes/base.py b/core/processes/base.py
index 9dadb0470eab4de33f56538e9008382496415fda..142bd6c3fa2c5ef0fc578d3d09bfa47530482619 100644
--- a/core/processes/base.py
+++ b/core/processes/base.py
@@ -1,54 +1 @@
-from pathlib import Path, PosixPath
-from typing import Union
-from abc import ABC, abstractmethod
-
-from yaml import safe_load, dump
-
-
-class ParametersABC(ABC):
-    """
-    Base class to add yaml functionality to parameters
-
-    """
-
-    def to_dict(self):
-        return self.__dict__
-
-    @classmethod
-    def from_dict(cls, d: dict):
-        return cls(**d)
-
-    def to_yaml(self, path: Union[PosixPath, str] = None):
-        if path:
-            with open(Path(path), "w") as f:
-                dump(self.to_dict(), f)
-        return dump(self.to_dict())
-
-    @classmethod
-    def from_yaml(cls, path: Union[PosixPath, str]):
-        with open(Path(file)) as f:
-            params = safe_load(f)
-        return cls(**params)
-
-    @classmethod
-    @abstractmethod
-    def default(cls):
-        pass
-
-
-class ProcessABC(ABC):
-    "Base class for processes"
-
-    def __init__(self, parameters):
-        self._parameters = parameters
-
-        for k, v in parameters.to_dict().items():  # access parameters directly
-            setattr(self, k, v)
-
-    @property
-    def parameters(self):
-        return self._parameters
-
-    @abstractmethod
-    def run(self):
-        pass
+from agora.base import ParametersABC, ProcessABC
\ No newline at end of file
diff --git a/core/processes/dsignal.py b/core/processes/dsignal.py
index c596075721934a2517c4a20c648deabce8db1453..484ad59944c12cd12005c02b9470b84d8f2f19d7 100644
--- a/core/processes/dsignal.py
+++ b/core/processes/dsignal.py
@@ -1,7 +1,6 @@
 import pandas as pd
 
 from postprocessor.core.processes.base import ParametersABC, ProcessABC
-from postprocessor.core.functions.tracks import clean_tracks, merge_tracks, join_tracks
 
 
 class dsignalParameters(ParametersABC):
diff --git a/core/processes/gpsignal.py b/core/processes/gpsignal.py
new file mode 100644
index 0000000000000000000000000000000000000000..f6cdb45fbd1416630382246fb3152c9d99184076
--- /dev/null
+++ b/core/processes/gpsignal.py
@@ -0,0 +1,97 @@
+"""Gaussian process fit of a Signal."""
+import logging
+
+from postprocessor.core.processes.base import ParametersABC, ProcessABC
+import numpy as np
+import pandas as pd
+
+import gaussian_processes.gaussianprocess as gp
+
+def estimate_gr(volume, dt, noruns, bounds, verbose):
+    """
+    Parameters
+    ----------
+
+    volume : pd.Series
+        The volume series of a given cell
+    dt : float
+        The time interval in hours
+    noruns : int
+        The number of runs for optimisation
+    bounds : dict
+        The hyperparameter bounds used for optimisation
+    verbose : bool
+        If True, prints results
+
+    Returns
+    -------
+    """
+    volume = volume.values
+    n = len(volume)
+    idx = np.arange(n)
+    t = idx * dt
+    y = volume[volume > 0]
+    x = t[volume > 0]
+    idx = idx[volume > 0]
+    # Fit the GP
+    mg = gp.maternGP(bounds, x, y)
+    mg.findhyperparameters(noruns=noruns)
+    if verbose:
+        mg.results()  # Prints out the hyperparameters
+    mg.predict(x, derivs=2)  # Saves the predictions to object
+    # Save the predictions to a csv file so they can be reused
+    results = dict(time=mg.x, volume=mg.y, fit_time=mg.xnew, fit_volume=mg.f,
+                   growth_rate=mg.df, d_growth_rate=mg.ddf,
+                   volume_var=mg.fvar, growth_rate_var=mg.dfvar,
+                   d_growth_rate_var=mg.ddfvar)
+    for name, value in results.items():
+        results[name] = np.full((n, ), np.nan)
+        results[name][idx] = value
+    return results
+
+# Give that to a writer: NOTE the writer needs to learn how to write the
+# output of a process that returns multiple signals like this one does.
+
+class gpParameters(ParametersABC):
+    default_dict = dict(dt=5,
+             noruns=5,
+             bounds={0: (-2, 3),
+                     1: (-2, 1),
+                     2: (-4, -1)},
+             verbose=False)
+    def __init__(self, dt, noruns, bounds, verbose):
+        """
+        Parameters
+        ----------
+            dt : float
+                The time step between time points, in minutes
+            noruns : int
+                The number of times the optimisation is tried
+            bounds : dict
+                Hyperparameter bounds for the Matern Kernel
+            verbose : bool
+                Determines whether to print hyperparameter results
+        """
+        self.dt = dt
+        self.noruns = noruns
+        self.bounds = bounds
+        self.verbose = verbose
+
+    @classmethod
+    def default(cls):
+        return cls.from_dict(cls.default_dict)
+
+
+class GPSignal(ProcessABC):
+    """Gaussian process fit of a Signal.
+    """
+    def __init__(self, parameters: gpParameters):
+        super().__init__(parameters)
+
+    def run(self, signal: pd.DataFrame):
+        results = signal.apply(lambda x: estimate_gr(x,
+                                                     **self.parameters.to_dict()),
+                               result_type='expand').T
+        multi_signal = {name: pd.DataFrame(np.vstack(results[name]))
+                        for name in results.columns}
+        return multi_signal
diff --git a/core/processes/merger.py b/core/processes/merger.py
index 1fa07b9191f7d2e0b2370c54b0364581b00b8de4..f7e90a5cb0b2fe50be7542c642ebd986d8f63152 100644
--- a/core/processes/merger.py
+++ b/core/processes/merger.py
@@ -1,4 +1,4 @@
-from postprocessor.core.processes.base import ParametersABC, ProcessABC
+from agora.base import ParametersABC, ProcessABC
 from postprocessor.core.functions.tracks import get_joinable
 
 
diff --git a/core/processes/picker.py b/core/processes/picker.py
index 2c057df93afd40a1a109f2866191e8d2c0451a02..3668e92291d9889cd7def4a04d8302c2296bfa98 100644
--- a/core/processes/picker.py
+++ b/core/processes/picker.py
@@ -6,7 +6,7 @@ import pandas as pd
 
 from core.cells import CellsHDF
 
-from postprocessor.core.processes.base import ParametersABC, ProcessABC
+from agora.base import ParametersABC, ProcessABC
 from postprocessor.core.functions.tracks import max_ntps, max_nonstop_ntps
 
 
@@ -106,7 +106,7 @@ class picker(ProcessABC):
     ):
         threshold_asint = _as_int(threshold, signals.shape[1])
         case_mgr = {
-            "present": signals.apply(max_ntps, axis=1) > threshold_asint,
+            "present": signals.notna().sum(axis=1) > threshold_asint,
             "nonstoply_present": signals.apply(max_nonstop_ntps, axis=1)
             > threshold_asint,
             "quantile": [np.quantile(signals.values[signals.notna()], threshold)],
diff --git a/core/processor.py b/core/processor.py
index e5eb499525991fcb7d8aed774a08472400e37176..239edd99314d004c5f4301060c2ce074a0f67936 100644
--- a/core/processor.py
+++ b/core/processor.py
@@ -5,13 +5,13 @@ from pydoc import locate
 import numpy as np
 import pandas as pd
 
-from postprocessor.core.processes.base import ParametersABC
-from postprocessor.core.processes.merger import mergerParameters, merger
-from postprocessor.core.processes.picker import pickerParameters, picker
+from agora.base import ParametersABC
 from core.io.writer import Writer
 from core.io.signal import Signal
 
 from core.cells import Cells
+from postprocessor.core.processes.merger import mergerParameters, merger
+from postprocessor.core.processes.picker import pickerParameters, picker
 
 
 class PostProcessorParameters(ParametersABC):
@@ -165,9 +165,17 @@ class PostProcessor:
                 else:
                     raise ("Outpath not defined", type(dataset))
 
-                self.write_result(
-                    "/postprocessing/" + process + "/" + outpath, result, metadata={}
-                )
+                if isinstance(result, dict): # Multiple Signals as output
+                    for k, v in result:
+                        self.write_result(
+                            "/postprocessing/" + process + "/" + outpath +
+                            f'/{k}',
+                            v, metadata={}
+                        )
+                else:
+                    self.write_result(
+                        "/postprocessing/" + process + "/" + outpath, result, metadata={}
+                    )
 
     def write_result(
         self, path: str, result: Union[List, pd.DataFrame, np.ndarray], metadata: Dict