diff --git a/README.md b/README.md
index 67fb2ae18806fb0fb97ef154603082e28b451107..9b7e3b2826865dc537682ccf0acd66f9bfc6c7ee 100644
--- a/README.md
+++ b/README.md
@@ -43,8 +43,16 @@ The behavior can be finely controlled by passing additional keyword arguments to
 
 This is an up to date list of available keyword options:
 
-  - `nsteps`: integer parameter, number of steps to be used in the extrapolation.
-  **Note:** Calling `guess` before loading `nsteps` data points will cause a `ValueError`.
+  - `nsteps`: integer, default 6, number of steps to be used in the extrapolation.
+  - `verbose`: boolean, default False, if True print additional information.
+  - `descriptor`: string, default "distance", possible options are "distance" and "coulomb".
+  - `fitting`: string, default "leastsquare", possible options are "leastsquare" and "qtr".
+  - `allow_partially_filled`: bool, default True. If True allow to do a guess before `nsteps` data points have been loaded, if False asking for a guess before `nsteps` data points will cause a `ValueError`.
+  - `store_overlap`:  bool, default True. Store the overlaps for later usage in calling guess without passing the current overlap. It can be disabled for performance, but calling guess will require passing the overlap.
+
+Some options can be piped to the fitting modules.
+  - `fitting_regularization`: float, default 0.0. Controls the regularization for both the "leastsquare" and "qtr" fitting schemes.
+
 
 ## Acknowledgments
 
diff --git a/gext/descriptors.py b/gext/descriptors.py
index f1f6657e3111ef600331fcb59244d12dc8ec6527..e7813e40626fb3e6dffc7dc05e2a666a68f4b93e 100644
--- a/gext/descriptors.py
+++ b/gext/descriptors.py
@@ -7,6 +7,8 @@ class Distance:
 
     """Distance matrix descriptors."""
 
+    supported_options = {}
+
     def __init__(self, **kwargs):
         self.set_options(**kwargs)
 
@@ -24,6 +26,8 @@ class Coulomb(Distance):
 
     """Coulomb matrix descriptors."""
 
+    supported_options = {}
+
     def compute(self, coords: np.ndarray) -> np.ndarray:
         """Compute the Coulomb matrix as a descriptor."""
         return 1.0/super().compute(coords)
diff --git a/gext/fitting.py b/gext/fitting.py
index 4d058aaa349e2e3d764bafc4b9c64b435afcdbd3..c72614289fc596063d53cf0ce0de13f56f15cadd 100644
--- a/gext/fitting.py
+++ b/gext/fitting.py
@@ -8,16 +8,29 @@ class AbstractFitting(abc.ABC):
 
     """Base class for fitting schemes."""
 
+    supported_options = {}
+
     def __init__(self, **kwargs):
         self.set_options(**kwargs)
 
     @abc.abstractmethod
     def set_options(self, **kwargs):
         """Base method for setting options."""
+        self.options = {}
+        for key, value in kwargs.items():
+            if key in self.supported_options:
+                self.options[key] = value
+            else:
+                raise ValueError(f"Unsupported option: {key}")
+
+        for option, default_value in self.supported_options.items():
+            if option not in self.options:
+                self.options[option] = default_value
 
     @abc.abstractmethod
-    def compute(self, vectors: List[np.ndarray], target:np.ndarray):
+    def fit(self, vectors: List[np.ndarray], target:np.ndarray):
         """Base method for computing new fitting coefficients."""
+        return np.zeros(0)
 
     def linear_combination(self, vectors: List[np.ndarray],
             coefficients: np. ndarray) -> np.ndarray:
@@ -38,34 +51,66 @@ class LeastSquare(AbstractFitting):
 
     def set_options(self, **kwargs):
         """Set options for least square minimization"""
-        self.options = {}
-        for key, value in kwargs.items():
-            if key in self.supported_options:
-                self.options[key] = value
-            else:
-                raise ValueError(f"Unsupported option: {key}")
-
-        for option, default_value in self.supported_options.items():
-            if option not in self.options:
-                self.options[option] = default_value
+        super().set_options(**kwargs)
 
         if self.options["regularization"] < 0 \
                 or self.options["regularization"] > 100:
             raise ValueError("Unsupported value for regularization")
 
-    def compute(self, vectors: List[np.ndarray], target: np.ndarray):
+    def fit(self, vectors: List[np.ndarray], target: np.ndarray):
         """Given a set of vectors and a target return the fitting
         coefficients."""
-        matrix = np.vstack(vectors).T
-        coefficients, _, _, _ = np.linalg.lstsq(matrix, target, rcond=None)
+        matrix = np.array(vectors).T
+        a = matrix.T @ matrix
+        b = matrix.T @ target
+        if self.options["regularization"] > 0.0:
+            a += np.identity(len(b))*self.options["regularization"]
+        coefficients = np.linalg.solve(a, b)
         return np.array(coefficients, dtype=np.float64)
 
 class QuasiTimeReversible(AbstractFitting):
 
     """Quasi time reversible fitting scheme. Not yet implemented."""
 
+    supported_options = {
+        "regularization": 0.0,
+    }
+
     def set_options(self, **kwargs):
         """Set options for quasi time reversible fitting"""
+        super().set_options(**kwargs)
 
-    def compute(self, vectors: List[np.ndarray], target: np.ndarray):
+        if self.options["regularization"] < 0 \
+                or self.options["regularization"] > 100:
+            raise ValueError("Unsupported value for regularization")
+
+    def fit(self, vectors: List[np.ndarray], target: np.ndarray):
         """Time reversible least square minimization fitting."""
+
+        past_target = vectors[0]
+        matrix = np.array(vectors[1:]).T
+
+        q = matrix.shape[1]
+        if q == 1:
+            time_reversible_matrix = matrix
+        elif q%2 == 0:
+            time_reversible_matrix = matrix[:, :q//2] + matrix[:, :q//2-1:-1]
+        else:
+            time_reversible_matrix = matrix[:, :q//2+1] + matrix[:, :q//2-1:-1]
+
+        a = time_reversible_matrix.T @ time_reversible_matrix
+        b = time_reversible_matrix.T @ (target + past_target)
+
+        if self.options["regularization"] > 0.0:
+            a += np.identity(len(b))*self.options["regularization"]
+        coefficients = np.linalg.solve(a, b)
+
+        if q == 1:
+            full_coefficients = np.concatenate(([-1.0], coefficients))
+        elif q%2 == 0:
+            full_coefficients = np.concatenate(([-1.0], coefficients,
+                coefficients[::-1]))
+        else:
+            full_coefficients = np.concatenate(([-1.0], coefficients[:-1],
+                2.0*coefficients[-1:], coefficients[-2::-1]))
+        return np.array(full_coefficients, dtype=np.float64)
diff --git a/gext/main.py b/gext/main.py
index 1c9e692e22035a252444827029898617eb22232f..71de73a5488307de15cefb3360d7dc86c399f5fd 100644
--- a/gext/main.py
+++ b/gext/main.py
@@ -12,19 +12,21 @@ class Extrapolator:
 
     """Class for performing Grassmann extrapolations. On initialization
     it requires the number of electrons, the number of basis functions
-    and the number of atoms of the molecule. The number of previous
-    steps used by the extrapolator is an optional argument with default
-    value of 6."""
+    and the number of atoms of the molecule."""
+
+    supported_options = {
+        "verbose": False,
+        "nsteps": 6,
+        "descriptor": "distance",
+        "fitting": "leastsquare",
+        "allow_partially_filled": True,
+        "store_overlap": True,
+    }
 
     def __init__(self, nelectrons: int, nbasis: int, natoms: int, **kwargs):
 
-        self.supported_options = {
-            "verbose": False,
-            "nsteps": 6,
-            "descriptor": "distance",
-            "fitting": "leastsquare",
-            "allow_partially_filled": True,
-        }
+        if not (type(nelectrons) == int and type(nbasis) == int and type(natoms) == int):
+            raise ValueError("Dimensions are not integers")
 
         self.nelectrons = nelectrons
         self.nbasis = nbasis
@@ -32,9 +34,10 @@ class Extrapolator:
         self.set_options(**kwargs)
 
         self.gammas = CircularBuffer(self.options["nsteps"], (self.nelectrons//2, self.nbasis))
-        self.overlaps = CircularBuffer(self.options["nsteps"], (self.nbasis, self.nbasis))
         self.descriptors = CircularBuffer(self.options["nsteps"],
             ((self.natoms - 1)*self.natoms//2, ))
+        if self.options["store_overlap"]:
+            self.overlaps = CircularBuffer(self.options["nsteps"], (self.nbasis, self.nbasis))
 
         self.tangent: Optional[np.ndarray] = None
 
@@ -47,6 +50,7 @@ class Extrapolator:
         descriptor_options = {}
         fitting_options = {}
 
+        # set specified options
         for key, value in kwargs.items():
             if key in self.supported_options:
                 self.options[key] = value
@@ -57,11 +61,14 @@ class Extrapolator:
             else:
                 raise ValueError(f"Unsupported option: {key}")
 
+        # set unspecified options with defaults
         for option, default_value in self.supported_options.items():
             if not option in self.options:
                 self.options[option] = default_value
 
-        if self.options["nsteps"] <= 1 or self.options["nsteps"] >= 100:
+        # do some check on the options, set things and pipe options
+        # to submodules
+        if self.options["nsteps"] < 1 or self.options["nsteps"] >= 100:
             raise ValueError("Unsupported nsteps")
 
         if self.options["descriptor"] == "distance":
@@ -77,49 +84,68 @@ class Extrapolator:
         elif self.options["fitting"] == "qtr":
             self.fitting_calculator = QuasiTimeReversible()
         else:
-            raise ValueError("Unsupported descriptor")
+            raise ValueError("Unsupported fitting")
         self.fitting_calculator.set_options(**fitting_options)
 
-    def load_data(self, coords: np.ndarray, coeff: np.ndarray,
-            overlap: np.ndarray):
+    def load_data(self, coords: np.ndarray, coeff: np.ndarray, overlap):
         """Load a new data point in the extrapolator."""
+
+        # Crop the coefficient matrix up to the number of electron
+        # pairs, then apply S^1/2
         coeff = self._crop_coeff(coeff)
         coeff = self._normalize(coeff, overlap)
 
+        # if it is the first time we load data, set the tangent point
         if self.tangent is None:
             self._set_tangent(coeff)
 
+        # push the new data to the corresponding vectors
         self.gammas.push(self._grassmann_log(coeff))
         self.descriptors.push(self._compute_descriptor(coords))
-        self.overlaps.push(overlap)
 
-    def guess(self, coords: np.ndarray, overlap = None) -> np.ndarray:
+        if self.options["store_overlap"]:
+            self.overlaps.push(overlap)
+
+    def guess(self, coords: np.ndarray, overlap=None) -> np.ndarray:
         """Get a new electronic density matrix to be used as a guess."""
         c_guess = self.guess_coefficients(coords, overlap)
         return c_guess @ c_guess.T
 
-    def guess_coefficients(self, coords: np.ndarray, overlap = None) -> np.ndarray:
+    def guess_coefficients(self, coords: np.ndarray, overlap=None) -> np.ndarray:
         """Get a new coefficient matrix to be used as a guess."""
 
+        # check if we have enough data points to perform an extrapolation
+        count = self.descriptors.count
         if self.options["allow_partially_filled"]:
-            n = min(self.options["nsteps"], self.descriptors.count)
+            if count == 0:
+                raise ValueError("Not enough data loaded in the extrapolator")
+            n = min(self.options["nsteps"], count)
         else:
             n = self.options["nsteps"]
+            if count < n:
+                raise ValueError("Not enough data loaded in the extrapolator")
 
+        if overlap is None and not self.options["store_overlap"]:
+            raise ValueError("Guessing without overlap requires `store_overlap` true.")
+
+        # use the descriptors to find the fitting coefficients
         prev_descriptors = self.descriptors.get(n)
         descriptor = self._compute_descriptor(coords)
-        fit_coefficients = self.fitting_calculator.compute(prev_descriptors, descriptor)
+        fit_coefficients = self._fit(prev_descriptors, descriptor)
 
+        # use the fitting coefficients and the previous gammas to
+        # extrapolate a new gamma
         gammas = self.gammas.get(n)
         gamma = self.fitting_calculator.linear_combination(gammas, fit_coefficients)
 
-        fit_descriptor = self.fitting_calculator.linear_combination(
-            prev_descriptors, fit_coefficients)
-
         if self.options["verbose"]:
+            fit_descriptor = self.fitting_calculator.linear_combination(
+                prev_descriptors, fit_coefficients)
             print("error on descriptor:", \
                 np.linalg.norm(fit_descriptor - descriptor, ord=np.inf))
 
+        # if the overlap is not given, use the coefficients to fit
+        # a new overlap
         if overlap is None:
             overlaps = self.overlaps.get(n)
             overlap = self.fitting_calculator.linear_combination(overlaps, fit_coefficients)
@@ -127,10 +153,9 @@ class Extrapolator:
         else:
             inverse_sqrt_overlap = self._inverse_sqrt_overlap(overlap)
 
+        # use the overlap and gamma to find a new set of coefficients
         c_guess = self._grassmann_exp(gamma)
-        c_guess = inverse_sqrt_overlap @ c_guess
-
-        return c_guess
+        return inverse_sqrt_overlap @ c_guess
 
     def _get_tangent(self) -> np.ndarray:
         """Get the tangent point."""
@@ -176,3 +201,8 @@ class Extrapolator:
     def _compute_descriptor(self, coords) -> np.ndarray:
         """Given a set of coordinates compute the corresponding descriptor."""
         return self.descriptor_calculator.compute(coords)
+
+    def _fit(self, prev_descriptors, descriptor) -> np.ndarray:
+        """Fit the current descriptor using previous descriptors and
+        the specified fitting scheme."""
+        return self.fitting_calculator.fit(prev_descriptors, descriptor)
diff --git a/tests/test_descriptor_fitting.py b/tests/test_descriptor_fitting.py
index cc8c42be6ea39e764fd41a17308e238b26fbc8d9..f0ae573404705b11cfa65cb87d50facdfae97282 100644
--- a/tests/test_descriptor_fitting.py
+++ b/tests/test_descriptor_fitting.py
@@ -10,10 +10,12 @@ import gext.fitting
 import gext.grassmann
 import utils
 
-SMALL = 1e-10
+SMALL = 1e-8
+THRESHOLD = 5e-2
 
 @pytest.mark.parametrize("datafile", ["urea.json", "glucose.json"])
-def test_descriptor_fitting(datafile):
+@pytest.mark.parametrize("regularization", [0.0, 0.01, 0.05])
+def test_least_square(datafile, regularization):
 
     # load test data from json file
     data = utils.load_json(f"tests/{datafile}")
@@ -23,33 +25,107 @@ def test_descriptor_fitting(datafile):
     nframes = data["trajectory"].shape[0]
 
     # initialize an extrapolator
-    extrapolator = gext.Extrapolator(nelectrons, nbasis, natoms, nsteps=nframes)
+    extrapolator = gext.Extrapolator(nelectrons, nbasis, natoms,
+        nsteps=nframes, fitting_regularization=regularization,
+        fitting="leastsquare")
 
     # load data in the extrapolator
     for (coords, coeff, overlap) in zip(data["trajectory"],
             data["coefficients"], data["overlaps"]):
         extrapolator.load_data(coords, coeff, overlap)
 
-    # we check if the error goes down with a larger data set
-    errors = []
     descriptors = extrapolator.descriptors.get(10)
     target = descriptors[-1]
 
-    fitting_calculator = gext.fitting.LeastSquare()
+    fitting_calculator = extrapolator.fitting_calculator
 
+    # check if things are reasonable
     for start in range(0, 9):
         vectors = descriptors[start:-1]
-        fit_coefficients = fitting_calculator.compute(vectors, target)
+        fit_coefficients = fitting_calculator.fit(vectors, target)
         fitted_target = fitting_calculator.linear_combination(vectors, fit_coefficients)
-        errors.append(np.linalg.norm(target - fitted_target, ord=np.inf))
+        error = np.linalg.norm(target - fitted_target, ord=np.inf)
+        assert error < THRESHOLD
 
-    assert errors[0] < errors[-1]
-
-    # we check that we get a zero error if we put the target in the vectors
-    # used for the fitting
+    # if we put the target in the vectors used for the fitting,
+    # check that we get an error smaller than the regularization
     vectors = descriptors[:-1]
     vectors[0] = target
-    fit_coefficients = fitting_calculator.compute(vectors, target)
+    fit_coefficients = fitting_calculator.fit(vectors, target)
     fitted_target = fitting_calculator.linear_combination(vectors, fit_coefficients)
 
-    assert np.linalg.norm(target - fitted_target, ord=np.inf) < SMALL
+    assert np.linalg.norm(target - fitted_target, ord=np.inf) < max(SMALL, regularization)
+
+@pytest.mark.parametrize("datafile", ["urea.json", "glucose.json"])
+@pytest.mark.parametrize("regularization", [0.0, 0.01, 0.05])
+def test_quasi_time_reversible(datafile, regularization):
+
+    # load test data from json file
+    data = utils.load_json(f"tests/{datafile}")
+    nelectrons = data["nelectrons"]
+    natoms = data["trajectory"].shape[1]
+    nbasis = data["overlaps"].shape[1]
+    nframes = data["trajectory"].shape[0]
+
+    # initialize an extrapolator
+    extrapolator = gext.Extrapolator(nelectrons, nbasis, natoms,
+        nsteps=nframes, fitting="qtr", fitting_regularization=regularization)
+
+    # load data in the extrapolator
+    for (coords, coeff, overlap) in zip(data["trajectory"],
+            data["coefficients"], data["overlaps"]):
+        extrapolator.load_data(coords, coeff, overlap)
+
+    descriptors = extrapolator.descriptors.get(10)
+    target = descriptors[-1]
+
+    fitting_calculator = extrapolator.fitting_calculator
+
+    # check if things are reasonable
+    for start in range(0, 8):
+        vectors = descriptors[start:-1]
+        fit_coefficients = fitting_calculator.fit(vectors, target)
+        fitted_target = fitting_calculator.linear_combination(vectors, fit_coefficients)
+        error = np.linalg.norm(target - fitted_target, ord=np.inf)
+        assert error < THRESHOLD
+
+@pytest.mark.parametrize("datafile", ["urea.json", "glucose.json"])
+def test_time_reversibility(datafile):
+
+    # load test data from json file
+    data = utils.load_json(f"tests/{datafile}")
+    nelectrons = data["nelectrons"]
+    natoms = data["trajectory"].shape[1]
+    nbasis = data["overlaps"].shape[1]
+    nframes = data["trajectory"].shape[0]
+
+    # initialize an extrapolator
+    extrapolator = gext.Extrapolator(nelectrons, nbasis, natoms,
+        nsteps=nframes, fitting="qtr")
+
+    # load data in the extrapolator
+    for (coords, coeff, overlap) in zip(data["trajectory"],
+            data["coefficients"], data["overlaps"]):
+        extrapolator.load_data(coords, coeff, overlap)
+
+    descriptors = extrapolator.descriptors.get(10)
+
+    # we symmetrize the future and past targets (remember that it is
+    # quasi time reversible, not exactly time reversible)
+    target = descriptors[0] + descriptors[-1]
+    descriptors[0] = target
+    descriptors[-1] = target
+
+    fitting_calculator = extrapolator.fitting_calculator
+
+    # fit the future target
+    fit_coefficients = fitting_calculator.fit(descriptors[:-1], descriptors[-1])
+    fitted_target = fitting_calculator.linear_combination(descriptors[:-1], fit_coefficients)
+
+    # fit the past target
+    reversed_descriptors = list(reversed(descriptors))
+    fit_coefficients = fitting_calculator.fit(reversed_descriptors[:-1], reversed_descriptors[-1])
+    fitted_target_reverse = fitting_calculator.linear_combination(reversed_descriptors[:-1], fit_coefficients)
+
+    # check the time reversibility
+    assert np.linalg.norm(fitted_target - fitted_target_reverse, ord=np.inf) < SMALL
diff --git a/tests/test_extrapolation.py b/tests/test_extrapolation.py
index eed97dcd10f31c7bfaa8735cf1752e40c8ed1d44..3ae4bb071ad307a12764b42325dc712b03d03124 100644
--- a/tests/test_extrapolation.py
+++ b/tests/test_extrapolation.py
@@ -9,10 +9,13 @@ import gext.grassmann
 import utils
 
 SMALL = 1e-10
-THRESHOLD = 1e-2
+THRESHOLD = 5e-2
 
 @pytest.mark.parametrize("datafile", ["urea.json", "glucose.json"])
-def test_extrapolation(datafile):
+@pytest.mark.parametrize("fitting", ["leastsquare", "qtr"])
+@pytest.mark.parametrize("regularization", [0.0, 1e-6, 5e-6])
+@pytest.mark.parametrize("descriptor", ["distance", "coulomb"])
+def test_extrapolation(datafile, fitting, regularization, descriptor):
 
     # load test data from json file
     data = utils.load_json(f"tests/{datafile}")
@@ -26,7 +29,9 @@ def test_extrapolation(datafile):
     assert n < nframes
 
     # initialize an extrapolator
-    extrapolator = gext.Extrapolator(nelectrons, nbasis, natoms, nsteps=n)
+    extrapolator = gext.Extrapolator(nelectrons, nbasis, natoms,
+        nsteps=n, fitting=fitting, fitting_regularization=regularization,
+        descriptor=descriptor)
 
     # load data in the extrapolator up to index n - 1
     for (coords, coeff, overlap) in zip(data["trajectory"][:n],
@@ -106,3 +111,37 @@ def test_coefficient_extrapolation(datafile):
     assert np.linalg.norm(guessed_density - density, ord=np.inf) < THRESHOLD
     assert np.linalg.norm(guessed_density - density, ord=np.inf) \
           /np.linalg.norm(density, ord=np.inf) < THRESHOLD
+
+@pytest.mark.parametrize("datafile", ["urea.json", "glucose.json"])
+def test_errors(datafile):
+
+    # load test data from json file
+    data = utils.load_json(f"tests/{datafile}")
+    nelectrons = data["nelectrons"]
+    natoms = data["trajectory"].shape[1]
+    nbasis = data["overlaps"].shape[1]
+    nframes = data["trajectory"].shape[0]
+
+    # amount of data we want to use for fitting
+    n = 9
+    assert n < nframes
+
+    # initialize an extrapolator
+    extrapolator = gext.Extrapolator(nelectrons, nbasis, natoms, nsteps=n)
+
+    with pytest.raises(ValueError):
+        extrapolator.guess(data["trajectory"][0])
+
+    # initialize a new extrapolator
+    extrapolator = gext.Extrapolator(nelectrons, nbasis, natoms, nsteps=n,
+        allow_partially_filled=False)
+
+    # load data in the extrapolator up to index m - 1
+    m = 4
+    for (coords, coeff, overlap) in zip(data["trajectory"][:m],
+            data["coefficients"][:m], data["overlaps"][:m]):
+        extrapolator.load_data(coords, coeff, overlap)
+
+    # check an extrapolation at index m
+    with pytest.raises(ValueError):
+        extrapolator.guess(data["trajectory"][m])