diff --git a/include/evalhyd/detail/determinist/elements.hpp b/include/evalhyd/detail/determinist/elements.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8c8fe1b11f0da79a4771c38050aebe40f6e64715
--- /dev/null
+++ b/include/evalhyd/detail/determinist/elements.hpp
@@ -0,0 +1,460 @@
+#ifndef EVALHYD_DETERMINIST_ELEMENTS_HPP
+#define EVALHYD_DETERMINIST_ELEMENTS_HPP
+
+#include <xtensor/xtensor.hpp>
+#include <xtensor/xview.hpp>
+
+#include "../maths.hpp"
+
+
+namespace evalhyd
+{
+    namespace determinist
+    {
+        namespace elements
+        {
+            // Compute the mean of the observations.
+            //
+            // \param q_obs
+            //     Streamflow observations.
+            //     shape: (1, time)
+            // \param t_msk
+            //     Temporal subsets of the whole record.
+            //     shape: (subsets, series, time)
+            // \param b_exp
+            //     Boostrap samples.
+            //     shape: (samples, time slice)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Mean observed streamflow.
+            //     shape: (subsets, samples, series, 1)
+            template <class D2>
+            inline xt::xtensor<double, 4> calc_mean_obs(
+                    const D2& q_obs,
+                    const xt::xtensor<bool, 3>& t_msk,
+                    const std::vector<xt::xkeep_slice<int>>& b_exp,
+                    std::size_t n_srs,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                xt::xtensor<double, 4> mean_obs =
+                        xt::zeros<double>({n_msk, n_exp, n_srs, std::size_t {1}});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    // apply the mask
+                    // (using NaN workaround until reducers work on masked_view)
+                    auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                    // compute variable one sample at a time
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        // apply the bootstrap sampling
+                        auto obs = xt::view(obs_masked, xt::all(), b_exp[e]);
+                        xt::view(mean_obs, m, e) =
+                                xt::nanmean(obs, -1, xt::keep_dims);
+                    }
+                }
+
+                return mean_obs;
+            }
+
+            // Compute the mean of the predictions.
+            //
+            // \param q_prd
+            //     Streamflow predictions.
+            //     shape: (series, time)
+            // \param t_msk
+            //     Temporal subsets of the whole record.
+            //     shape: (subsets, series, time)
+            // \param b_exp
+            //     Boostrap samples.
+            //     shape: (samples, time slice)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Mean predicted streamflow.
+            //     shape: (subsets, samples, series, 1)
+            template <class D2>
+            inline xt::xtensor<double, 4> calc_mean_prd(
+                    const D2& q_prd,
+                    const xt::xtensor<bool, 3>& t_msk,
+                    const std::vector<xt::xkeep_slice<int>>& b_exp,
+                    std::size_t n_srs,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                xt::xtensor<double, 4> mean_prd =
+                        xt::zeros<double>({n_msk, n_exp, n_srs, std::size_t {1}});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    // apply the mask
+                    // (using NaN workaround until reducers work on masked_view)
+                    auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
+
+                    // compute variable one sample at a time
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        // apply the bootstrap sampling
+                        auto prd = xt::view(prd_masked, xt::all(), b_exp[e]);
+                        xt::view(mean_prd, m, e) =
+                                xt::nanmean(prd, -1, xt::keep_dims);
+                    }
+                }
+
+                return mean_prd;
+            }
+
+            // Compute the quadratic error between observations and predictions.
+            //
+            // \param q_obs
+            //     Streamflow observations.
+            //     shape: (1, time)
+            // \param q_prd
+            //     Streamflow predictions.
+            //     shape: (series, time)
+            // \return
+            //     Quadratic errors between observations and predictions.
+            //     shape: (series, time)
+            template <class D2>
+            inline xt::xtensor<double, 2> calc_quad_err(
+                    const D2& q_obs,
+                    const D2& q_prd
+            )
+            {
+                return xt::square(q_obs - q_prd);
+            }
+
+            // Compute the quadratic error between observations and mean observation.
+            //
+            // \param q_obs
+            //     Streamflow observations.
+            //     shape: (series, time)
+            // \param mean_obs
+            //     Mean observed streamflow.
+            //     shape: (subsets, samples, series, 1)
+            // \param t_msk
+            //     Temporal subsets of the whole record.
+            //     shape: (subsets, series, time)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_tim
+            //     Number of time steps.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Quadratic errors between observations and mean observation.
+            //     shape: (subsets, samples, series, time)
+            template <class D2>
+            inline xt::xtensor<double, 4> calc_quad_obs(
+                    const D2& q_obs,
+                    const xt::xtensor<double, 4>& mean_obs,
+                    const xt::xtensor<bool, 3>& t_msk,
+                    std::size_t n_srs,
+                    std::size_t n_tim,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                xt::xtensor<double, 4> quad_obs =
+                        xt::zeros<double>({n_msk, n_exp, n_srs, n_tim});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    // apply the mask
+                    // (using NaN workaround until reducers work on masked_view)
+                    auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        xt::view(quad_obs, m, e) = xt::square(
+                                obs_masked - xt::view(mean_obs, m, e)
+                        );
+                    }
+                }
+
+                return quad_obs;
+            }
+
+            // Compute the quadratic error between predictions and mean prediction.
+            //
+            // \param q_prd
+            //     Streamflow predictions.
+            //     shape: (series, time)
+            // \param mean_prd
+            //     Mean predicted streamflow.
+            //     shape: (subsets, samples, series, 1)
+            // \param t_msk
+            //     Temporal subsets of the whole record.
+            //     shape: (subsets, series, time)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_tim
+            //     Number of time steps.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Quadratic errors between predictions and mean prediction.
+            //     shape: (subsets, samples, series, time)
+            template <class D2>
+            inline xt::xtensor<double, 4> calc_quad_prd(
+                    const D2& q_prd,
+                    const xt::xtensor<double, 4>& mean_prd,
+                    const xt::xtensor<bool, 3>& t_msk,
+                    std::size_t n_srs,
+                    std::size_t n_tim,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                xt::xtensor<double, 4> quad_prd =
+                        xt::zeros<double>({n_msk, n_exp, n_srs, n_tim});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    // apply the mask
+                    // (using NaN workaround until reducers work on masked_view)
+                    auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
+
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        xt::view(quad_prd, m, e) = xt::square(
+                                prd_masked - xt::view(mean_prd, m, e)
+                        );
+                    }
+                }
+
+                return quad_prd;
+            }
+
+            // Compute the Pearson correlation coefficient.
+            //
+            // \param q_obs
+            //     Streamflow observations.
+            //     shape: (series, time)
+            // \param q_prd
+            //     Streamflow predictions.
+            //     shape: (series, time)
+            // \param mean_obs
+            //     Mean observed streamflow.
+            //     shape: (subsets, samples, series, 1)
+            // \param mean_prd
+            //     Mean predicted streamflow.
+            //     shape: (subsets, samples, series, 1)
+            // \param quad_obs
+            //     Quadratic errors between observations and mean observation.
+            //     shape: (subsets, samples, series, time)
+            // \param quad_prd
+            //     Quadratic errors between predictions and mean prediction.
+            //     shape: (subsets, samples, series, time)
+            // \param t_msk
+            //     Temporal subsets of the whole record.
+            //     shape: (subsets, series, time)
+            // \param b_exp
+            //     Boostrap samples.
+            //     shape: (samples, time slice)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Pearson correlation coefficients.
+            //     shape: (subsets, samples, series)
+            template <class D2>
+            inline xt::xtensor<double, 3> calc_r_pearson(
+                    const D2& q_obs,
+                    const D2& q_prd,
+                    const xt::xtensor<double, 4>& mean_obs,
+                    const xt::xtensor<double, 4>& mean_prd,
+                    const xt::xtensor<double, 4>& quad_obs,
+                    const xt::xtensor<double, 4>& quad_prd,
+                    const xt::xtensor<bool, 3>& t_msk,
+                    const std::vector<xt::xkeep_slice<int>>& b_exp,
+                    std::size_t n_srs,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                // calculate error in timing and dynamics $r_{pearson}$
+                // (Pearson's correlation coefficient)
+                xt::xtensor<double, 3> r_pearson =
+                        xt::zeros<double>({n_msk, n_exp, n_srs});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    // apply the mask
+                    // (using NaN workaround until reducers work on masked_view)
+                    auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
+                    auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                    // compute variable one sample at a time
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        auto prd = xt::view(prd_masked, xt::all(), b_exp[e]);
+                        auto obs = xt::view(obs_masked, xt::all(), b_exp[e]);
+                        auto r_num = xt::nansum(
+                                (prd - xt::view(mean_prd, m, e))
+                                * (obs - xt::view(mean_obs, m, e)),
+                                -1
+                        );
+
+                        auto prd2 = xt::view(quad_prd, m, e, xt::all(), b_exp[e]);
+                        auto obs2 = xt::view(quad_obs, m, e, xt::all(), b_exp[e]);
+                        auto r_den = xt::sqrt(
+                                xt::nansum(prd2, -1) * xt::nansum(obs2, -1)
+                        );
+
+                        xt::view(r_pearson, m, e) = r_num / r_den;
+                    }
+                }
+
+                return r_pearson;
+            }
+
+            // Compute alpha.
+            //
+            // \param q_obs
+            //     Streamflow observations.
+            //     shape: (series, time)
+            // \param q_prd
+            //     Streamflow predictions.
+            //     shape: (series, time)
+            // \param mean_obs
+            //     Mean observed streamflow.
+            //     shape: (subsets, samples, series, 1)
+            // \param mean_prd
+            //     Mean predicted streamflow.
+            //     shape: (subsets, samples, series, 1)
+            // \param t_msk
+            //     Temporal subsets of the whole record.
+            //     shape: (subsets, series, time)
+            // \param b_exp
+            //     Boostrap samples.
+            //     shape: (samples, time slice)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Alphas, ratios of standard deviations.
+            //     shape: (subsets, samples, series)
+            template <class D2>
+            inline xt::xtensor<double, 3> calc_alpha(
+                    const D2& q_obs,
+                    const D2& q_prd,
+                    const xt::xtensor<double, 4>& mean_obs,
+                    const xt::xtensor<double, 4>& mean_prd,
+                    const xt::xtensor<bool, 3>& t_msk,
+                    const std::vector<xt::xkeep_slice<int>>& b_exp,
+                    std::size_t n_srs,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                // calculate error in spread of flow $alpha$
+                xt::xtensor<double, 3> alpha =
+                        xt::zeros<double>({n_msk, n_exp, n_srs});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    // apply the mask
+                    // (using NaN workaround until reducers work on masked_view)
+                    auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
+                    auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                    // compute variable one sample at a time
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        auto prd = xt::view(prd_masked, xt::all(), b_exp[e]);
+                        auto obs = xt::view(obs_masked, xt::all(), b_exp[e]);
+                        xt::view(alpha, m, e) =
+                                maths::nanstd(prd, xt::view(mean_prd, m, e))
+                                / maths::nanstd(obs, xt::view(mean_obs, m, e));
+                    }
+                }
+
+                return alpha;
+            }
+
+            // Compute the bias.
+            //
+            // \param q_obs
+            //     Streamflow observations.
+            //     shape: (series, time)
+            // \param q_prd
+            //     Streamflow predictions.
+            //     shape: (series, time)
+            // \param t_msk
+            //     Temporal subsets of the whole record.
+            //     shape: (subsets, series, time)
+            // \param b_exp
+            //     Boostrap samples.
+            //     shape: (samples, time slice)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Biases.
+            //     shape: (subsets, samples, series)
+            template <class D2>
+            inline xt::xtensor<double, 3> calc_bias(
+                    const D2& q_obs,
+                    const D2& q_prd,
+                    const xt::xtensor<bool, 3>& t_msk,
+                    const std::vector<xt::xkeep_slice<int>>& b_exp,
+                    std::size_t n_srs,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                // calculate $bias$
+                xt::xtensor<double, 3> bias =
+                        xt::zeros<double>({n_msk, n_exp, n_srs});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    // apply the mask
+                    // (using NaN workaround until reducers work on masked_view)
+                    auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
+                    auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+
+                    // compute variable one sample at a time
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        auto prd = xt::view(prd_masked, xt::all(), b_exp[e]);
+                        auto obs = xt::view(obs_masked, xt::all(), b_exp[e]);
+                        xt::view(bias, m, e) =
+                                xt::nansum(prd, -1) / xt::nansum(obs, -1);
+                    }
+                }
+
+                return bias;
+            }
+        }
+    }
+}
+
+#endif //EVALHYD_DETERMINIST_ELEMENTS_HPP
diff --git a/include/evalhyd/detail/determinist/evaluator.hpp b/include/evalhyd/detail/determinist/evaluator.hpp
index 74b92d0348d3ca49ae0668c4ea6365ec7c904a59..8e494af48fcd524b2e43e7bdfeb4798e2f6d47f5 100644
--- a/include/evalhyd/detail/determinist/evaluator.hpp
+++ b/include/evalhyd/detail/determinist/evaluator.hpp
@@ -3,10 +3,13 @@
 
 #include <vector>
 
+#include <xtl/xoptional.hpp>
 #include <xtensor/xexpression.hpp>
 #include <xtensor/xtensor.hpp>
 
-#include "../maths.hpp"
+#include "elements.hpp"
+#include "metrics.hpp"
+
 
 namespace evalhyd
 {
@@ -27,492 +30,195 @@ namespace evalhyd
             std::size_t n_msk;
             std::size_t n_srs;
             std::size_t n_exp;
-            std::vector<std::size_t> inner_dims;
-            std::vector<std::size_t> inter_dims;
-            std::vector<std::size_t> mean_dims;
-            std::vector<std::size_t> final_dims;
 
             // members for computational elements
-            xt::xtensor<double, 4> mean_obs;
-            xt::xtensor<double, 4> mean_prd;
-            xt::xtensor<double, 2> quad_err;
-            xt::xtensor<double, 4> quad_obs;
-            xt::xtensor<double, 4> quad_prd;
-            xt::xtensor<double, 3> r_pearson;
-            xt::xtensor<double, 3> alpha;
-            xt::xtensor<double, 3> bias;
-
-        public:
-            // constructor method
-            Evaluator(const D2& obs,
-                      const D2& prd,
-                      const B2& msk,
-                      const std::vector<xt::xkeep_slice<int>>& exp) :
-                    q_obs{obs}, q_prd{prd}, b_exp{exp}
-            {
-                // determine size for recurring dimensions
-                n_tim = q_prd.shape(1);
-                n_srs = q_prd.shape(0);
-                n_msk = msk.shape(0);
-                n_exp = b_exp.size();
+            xtl::xoptional<xt::xtensor<double, 4>, bool> mean_obs;
+            xtl::xoptional<xt::xtensor<double, 4>, bool> mean_prd;
+            xtl::xoptional<xt::xtensor<double, 2>, bool> quad_err;
+            xtl::xoptional<xt::xtensor<double, 4>, bool> quad_obs;
+            xtl::xoptional<xt::xtensor<double, 4>, bool> quad_prd;
+            xtl::xoptional<xt::xtensor<double, 3>, bool> r_pearson;
+            xtl::xoptional<xt::xtensor<double, 3>, bool> alpha;
+            xtl::xoptional<xt::xtensor<double, 3>, bool> bias;
 
-                // determine dimensions for inner components, intermediate
-                // elements, for time average elements, and for final metrics:
-                // -> inner components shape: (samples, subsets, series)
-                inner_dims = {n_msk, n_exp, n_srs};
-                // -> intermediate elements shape: (samples, subsets, series, time)
-                inter_dims = {n_msk, n_exp, n_srs, n_tim};
-                // -> time average elements shape: (samples, subsets, series, 1)
-                mean_dims = {n_msk, n_exp, n_srs, 1};
-                // -> final metrics shape: (series, subsets, samples)
-                final_dims = {n_srs, n_msk, n_exp};
-
-                // drop time steps where observations or predictions are NaN
-                auto obs_nan = xt::isnan(q_obs);
-                auto prd_nan = xt::isnan(q_prd);
+            // members for evaluation metrics
+            xtl::xoptional<xt::xtensor<double, 3>, bool> RMSE;
+            xtl::xoptional<xt::xtensor<double, 3>, bool> NSE;
+            xtl::xoptional<xt::xtensor<double, 3>, bool> KGE;
+            xtl::xoptional<xt::xtensor<double, 3>, bool> KGEPRIME;
 
-                t_msk = xt::ones<bool>({n_msk, n_srs, n_tim});
-                xt::view(t_msk, xt::all()) =
-                        xt::view(msk, xt::all(), xt::newaxis(), xt::all());
-                for (std::size_t m = 0; m < n_msk; m++)
+            // methods to compute elements
+            xt::xtensor<double, 4> get_mean_obs()
+            {
+                if (!mean_obs.has_value())
                 {
-                    xt::view(t_msk, m) =
-                            xt::where(obs_nan || prd_nan,
-                                      false, xt::view(t_msk, m));
+                    mean_obs = elements::calc_mean_obs<D2>(
+                            q_obs, t_msk, b_exp, n_srs, n_msk, n_exp
+                    );
                 }
+                return mean_obs.value();
             };
 
-            // members for evaluation metrics
-            xt::xtensor<double, 3> RMSE;
-            xt::xtensor<double, 3> NSE;
-            xt::xtensor<double, 3> KGE;
-            xt::xtensor<double, 3> KGEPRIME;
-
-            // methods to compute elements
-            void calc_mean_obs();
-            void calc_mean_prd();
-            void calc_quad_err();
-            void calc_quad_obs();
-            void calc_quad_prd();
-            void calc_r_pearson();
-            void calc_alpha();
-            void calc_bias();
-
-            // methods to compute metrics
-            void calc_RMSE();
-            void calc_NSE();
-            void calc_KGE();
-            void calc_KGEPRIME();
-        };
-
-        // Compute the mean of the observations.
-        //
-        // \require q_obs:
-        //     Streamflow observations.
-        //     shape: (series, time)
-        // \assign mean_obs:
-        //     Mean observed streamflow.
-        //     shape: (subsets, samples, series, 1)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_mean_obs()
-        {
-            mean_obs = xt::zeros<double>(mean_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            xt::xtensor<double, 4> get_mean_prd()
             {
-                // apply the mask
-                // (using NaN workaround until reducers work on masked_view)
-                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
-
-                // compute variable one sample at a time
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!mean_prd.has_value())
                 {
-                    // apply the bootstrap sampling
-                    auto obs = xt::view(obs_masked, xt::all(), b_exp[e]);
-                    xt::view(mean_obs, m, e) =
-                            xt::nanmean(obs, -1, xt::keep_dims);
+                    mean_prd = elements::calc_mean_prd<D2>(
+                            q_prd, t_msk, b_exp, n_srs, n_msk, n_exp
+                    );
                 }
-            }
-        }
+                return mean_prd.value();
+            };
 
-        // Compute the mean of the predictions.
-        //
-        // \require q_prd:
-        //     Streamflow predictions.
-        //     shape: (series, time)
-        // \assign mean_prd:
-        //     Mean predicted streamflow.
-        //     shape: (subsets, samples, series, 1)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_mean_prd()
-        {
-            mean_prd = xt::zeros<double>(mean_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            xt::xtensor<double, 2> get_quad_err()
             {
-                // apply the mask
-                // (using NaN workaround until reducers work on masked_view)
-                auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
-
-                // compute variable one sample at a time
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!quad_err.has_value())
                 {
-                    // apply the bootstrap sampling
-                    auto prd = xt::view(prd_masked, xt::all(), b_exp[e]);
-                    xt::view(mean_prd, m, e) =
-                            xt::nanmean(prd, -1, xt::keep_dims);
+                    quad_err = elements::calc_quad_err<D2>(
+                            q_obs, q_prd
+                    );
                 }
-            }
-        }
-
-        // Compute the quadratic error between observations and predictions.
-        //
-        // \require q_obs:
-        //     Streamflow observations.
-        //     shape: (series, time)
-        // \require q_prd:
-        //     Streamflow predictions.
-        //     shape: (series, time)
-        // \assign quad_err:
-        //     Quadratic errors between observations and predictions.
-        //     shape: (series, time)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_quad_err()
-        {
-            quad_err = xt::square(q_obs - q_prd);
-        }
+                return quad_err.value();
+            };
 
-        // Compute the quadratic error between observations and mean observation.
-        //
-        // \require q_obs:
-        //     Streamflow observations.
-        //     shape: (series, time)
-        // \require mean_obs:
-        //     Mean observed streamflow.
-        //     shape: (samples, series, 1)
-        // \assign quad_obs:
-        //     Quadratic errors between observations and mean observation.
-        //     shape: (subsets, samples, series, time)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_quad_obs()
-        {
-            quad_obs = xt::zeros<double>(inter_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            xt::xtensor<double, 4> get_quad_obs()
             {
-                // apply the mask
-                // (using NaN workaround until reducers work on masked_view)
-                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
-
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!quad_obs.has_value())
                 {
-                    xt::view(quad_obs, m, e) = xt::square(
-                            obs_masked - xt::view(mean_obs, m, e)
+                    quad_obs = elements::calc_quad_obs<D2>(
+                            q_obs, get_mean_obs(), t_msk,
+                            n_srs, n_tim, n_msk, n_exp
                     );
                 }
-            }
-        }
+                return quad_obs.value();
+            };
 
-        // Compute the quadratic error between predictions and mean prediction.
-        //
-        // \require q_prd:
-        //     Streamflow predictions.
-        //     shape: (series, time)
-        // \require mean_prd:
-        //     Mean predicted streamflow.
-        //     shape: (samples, series, 1)
-        // \assign quad_prd:
-        //     Quadratic errors between predictions and mean prediction.
-        //     shape: (subsets, samples, series, time)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_quad_prd()
-        {
-            quad_prd = xt::zeros<double>(inter_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            xt::xtensor<double, 4> get_quad_prd()
             {
-                // apply the mask
-                // (using NaN workaround until reducers work on masked_view)
-                auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
-
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!quad_prd.has_value())
                 {
-                    xt::view(quad_prd, m, e) = xt::square(
-                            prd_masked - xt::view(mean_prd, m, e)
+                    quad_prd = elements::calc_quad_prd<D2>(
+                            q_prd, get_mean_prd(), t_msk,
+                            n_srs, n_tim, n_msk, n_exp
                     );
                 }
-            }
-        }
+                return quad_prd.value();
+            };
 
-        // Compute the Pearson correlation coefficient.
-        //
-        // \require q_obs:
-        //     Streamflow observations.
-        //     shape: (series, time)
-        // \require q_prd:
-        //     Streamflow predictions.
-        //     shape: (series, time)
-        // \require mean_obs:
-        //     Mean observed streamflow.
-        //     shape: (samples, series, 1)
-        // \require mean_prd:
-        //     Mean predicted streamflow.
-        //     shape: (samples, series, 1)
-        // \require quad_obs:
-        //     Quadratic errors between observations and mean observation.
-        //     shape: (samples, series, time)
-        // \require quad_prd:
-        //     Quadratic errors between predictions and mean prediction.
-        //     shape: (samples, series, time)
-        // \assign r_pearson:
-        //     Pearson correlation coefficients.
-        //     shape: (subsets, samples, series)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_r_pearson()
-        {
-            // calculate error in timing and dynamics $r_{pearson}$
-            // (Pearson's correlation coefficient)
-            r_pearson = xt::zeros<double>(inner_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            xt::xtensor<double, 3> get_r_pearson()
             {
-                // apply the mask
-                // (using NaN workaround until reducers work on masked_view)
-                auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
-                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
-
-                // compute variable one sample at a time
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!r_pearson.has_value())
                 {
-                    auto prd = xt::view(prd_masked, xt::all(), b_exp[e]);
-                    auto obs = xt::view(obs_masked, xt::all(), b_exp[e]);
-                    auto r_num = xt::nansum(
-                            (prd - xt::view(mean_prd, m, e))
-                            * (obs - xt::view(mean_obs, m, e)),
-                            -1
+                    r_pearson = elements::calc_r_pearson<D2>(
+                            q_obs, q_prd, get_mean_obs(), get_mean_prd(),
+                            get_quad_obs(), get_quad_prd(), t_msk, b_exp,
+                            n_srs, n_msk, n_exp
                     );
+                }
+                return r_pearson.value();
+            };
 
-                    auto prd2 = xt::view(quad_prd, m, e, xt::all(), b_exp[e]);
-                    auto obs2 = xt::view(quad_obs, m, e, xt::all(), b_exp[e]);
-                    auto r_den = xt::sqrt(
-                            xt::nansum(prd2, -1) * xt::nansum(obs2, -1)
+            xt::xtensor<double, 3> get_alpha()
+            {
+                if (!alpha.has_value())
+                {
+                    alpha = elements::calc_alpha<D2>(
+                            q_obs, q_prd, get_mean_obs(), get_mean_prd(),
+                            t_msk, b_exp, n_srs, n_msk, n_exp
                     );
-
-                    xt::view(r_pearson, m, e) = r_num / r_den;
                 }
-            }
-        }
+                return alpha.value();
+            };
 
-        // Compute alpha.
-        //
-        // \require q_obs:
-        //     Streamflow observations.
-        //     shape: (series, time)
-        // \require q_prd:
-        //     Streamflow predictions.
-        //     shape: (series, time)
-        // \require mean_obs:
-        //     Mean observed streamflow.
-        //     shape: (samples, series, 1)
-        // \require mean_prd:
-        //     Mean predicted streamflow.
-        //     shape: (samples, series, 1)
-        // \assign alpha:
-        //     Alphas, ratios of standard deviations.
-        //     shape: (subsets, samples, series)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_alpha()
-        {
-            // calculate error in spread of flow $alpha$
-            alpha = xt::zeros<double>(inner_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            xt::xtensor<double, 3> get_bias()
             {
-                // apply the mask
-                // (using NaN workaround until reducers work on masked_view)
-                auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
-                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
-
-                // compute variable one sample at a time
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!bias.has_value())
                 {
-                    auto prd = xt::view(prd_masked, xt::all(), b_exp[e]);
-                    auto obs = xt::view(obs_masked, xt::all(), b_exp[e]);
-                    xt::view(alpha, m, e) =
-                            evalhyd::maths::nanstd(prd, xt::view(mean_prd, m, e))
-                            / evalhyd::maths::nanstd(obs, xt::view(mean_obs, m, e));
+                    bias = elements::calc_bias<D2>(
+                            q_obs, q_prd, t_msk, b_exp, n_srs, n_msk, n_exp
+                    );
                 }
-            }
-        }
+                return bias.value();
+            };
 
-        // Compute the bias.
-        //
-        // \require q_obs:
-        //     Streamflow observations.
-        //     shape: (series, time)
-        // \require q_prd:
-        //     Streamflow predictions.
-        //     shape: (series, time)
-        // \assign bias:
-        //     Biases.
-        //     shape: (subsets, samples, series)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_bias()
-        {
-            // calculate $bias$
-            bias = xt::zeros<double>(inner_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+        public:
+            // constructor method
+            Evaluator(const D2& obs,
+                      const D2& prd,
+                      const B2& msk,
+                      const std::vector<xt::xkeep_slice<int>>& exp) :
+                    q_obs{obs}, q_prd{prd}, b_exp{exp}
             {
-                // apply the mask
-                // (using NaN workaround until reducers work on masked_view)
-                auto prd_masked = xt::where(xt::view(t_msk, m), q_prd, NAN);
-                auto obs_masked = xt::where(xt::view(t_msk, m), q_obs, NAN);
+                // determine size for recurring dimensions
+                n_tim = q_prd.shape(1);
+                n_srs = q_prd.shape(0);
+                n_msk = msk.shape(0);
+                n_exp = b_exp.size();
 
-                // compute variable one sample at a time
-                for (std::size_t e = 0; e < n_exp; e++)
+                // drop time steps where observations or predictions are NaN
+                auto obs_nan = xt::isnan(q_obs);
+                auto prd_nan = xt::isnan(q_prd);
+
+                t_msk = xt::ones<bool>({n_msk, n_srs, n_tim});
+                xt::view(t_msk, xt::all()) =
+                        xt::view(msk, xt::all(), xt::newaxis(), xt::all());
+                for (std::size_t m = 0; m < n_msk; m++)
                 {
-                    auto prd = xt::view(prd_masked, xt::all(), b_exp[e]);
-                    auto obs = xt::view(obs_masked, xt::all(), b_exp[e]);
-                    xt::view(bias, m, e) =
-                            xt::nansum(prd, -1) / xt::nansum(obs, -1);
+                    xt::view(t_msk, m) =
+                            xt::where(obs_nan || prd_nan,
+                                      false, xt::view(t_msk, m));
                 }
-            }
-        }
+            };
 
-        // Compute the root-mean-square error (RMSE).
-        //
-        // \require quad_err:
-        //     Quadratic errors between observations and predictions.
-        //     shape: (series, time)
-        // \assign RMSE:
-        //     Root-mean-square errors.
-        //     shape: (series, subsets, samples)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_RMSE()
-        {
-            // compute RMSE
-            RMSE = xt::zeros<double>(final_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            // methods to compute metrics
+            xt::xtensor<double, 3> get_RMSE()
             {
-                // apply the mask
-                // (using NaN workaround until reducers work on masked_view)
-                auto quad_err_masked = xt::where(xt::view(t_msk, m), quad_err, NAN);
-
-                // compute variable one sample at a time
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!RMSE.has_value())
                 {
-                    auto err2 = xt::view(quad_err_masked, xt::all(), b_exp[e]);
-                    xt::view(RMSE, xt::all(), m, e) = xt::sqrt(xt::nanmean(err2, -1));
+                    RMSE = metrics::calc_RMSE(
+                            get_quad_err(), t_msk, b_exp, n_srs, n_msk, n_exp
+                    );
                 }
-            }
-        }
+                return RMSE.value();
+            };
 
-        // Compute the Nash-Sutcliffe Efficiency (NSE).
-        //
-        // \require quad_err:
-        //     Quadratic errors between observations and predictions.
-        //     shape: (series, time)
-        // \require quad_obs:
-        //     Quadratic errors between observations and mean observation.
-        //     shape: (samples, series, time)
-        // \assign NSE:
-        //     Nash-Sutcliffe efficiencies.
-        //     shape: (series, subsets, samples)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_NSE()
-        {
-            NSE = xt::zeros<double>(final_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            xt::xtensor<double, 3> get_NSE()
             {
-                // apply the mask
-                // (using NaN workaround until reducers work on masked_view)
-                auto quad_err_masked = xt::where(xt::view(t_msk, m), quad_err, NAN);
-
-                // compute variable one sample at a time
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!NSE.has_value())
                 {
-                    // compute squared errors operands
-                    auto err2 = xt::view(quad_err_masked, xt::all(), b_exp[e]);
-                    xt::xtensor<double, 1> f_num =
-                            xt::nansum(err2, -1);
-                    auto obs2 = xt::view(quad_obs, m, e, xt::all(), b_exp[e]);
-                    xt::xtensor<double, 1> f_den =
-                            xt::nansum(obs2, -1);
-
-                    // compute NSE
-                    xt::view(NSE, xt::all(), m, e) = 1 - (f_num / f_den);
+                    NSE = metrics::calc_NSE(
+                            get_quad_err(), get_quad_obs(), t_msk, b_exp,
+                            n_srs, n_msk, n_exp
+                    );
                 }
-            }
-        }
+                return NSE.value();
+            };
 
-        // Compute the Kling-Gupta Efficiency (KGE).
-        //
-        // \require r_pearson:
-        //     Pearson correlation coefficients.
-        //     shape: (samples, series)
-        // \require alpha:
-        //     Alphas, ratios of standard deviations.
-        //     shape: (samples, series)
-        // \require bias:
-        //     Biases.
-        //     shape: (samples, series)
-        // \assign KGE:
-        //     Kling-Gupta efficiencies.
-        //     shape: (series, subsets, samples)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_KGE()
-        {
-            KGE = xt::zeros<double>(final_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            xt::xtensor<double, 3> get_KGE()
             {
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!KGE.has_value())
                 {
-                    // compute KGE
-                    xt::view(KGE, xt::all(), m, e) = 1 - xt::sqrt(
-                            xt::square(xt::view(r_pearson, m, e) - 1)
-                            + xt::square(xt::view(alpha, m, e) - 1)
-                            + xt::square(xt::view(bias, m, e) - 1)
+                    KGE = metrics::calc_KGE(
+                            get_r_pearson(), get_alpha(), get_bias(),
+                            n_srs, n_msk, n_exp
                     );
                 }
-            }
-        }
+                return KGE.value();
+            };
 
-        // Compute the modified Kling-Gupta Efficiency (KGEPRIME).
-        //
-        // \require mean_obs:
-        //     Mean observed streamflow.
-        //     shape: (samples, series, 1)
-        // \require mean_prd:
-        //     Mean predicted streamflow.
-        //     shape: (samples, series, 1)
-        // \require r_pearson:
-        //     Pearson correlation coefficients.
-        //     shape: (samples, series)
-        // \require alpha:
-        //     Alphas, ratios of standard deviations.
-        //     shape: (samples, series)
-        // \require bias:
-        //     Biases.
-        //     shape: (samples, series)
-        // \assign KGEPRIME:
-        //     Modified Kling-Gupta efficiencies.
-        //     shape: (series, subsets, samples)
-        template <class D2, class B2>
-        void Evaluator<D2, B2>::calc_KGEPRIME()
-        {
-            KGEPRIME = xt::zeros<double>(final_dims);
-            for (std::size_t m = 0; m < n_msk; m++)
+            xt::xtensor<double, 3> get_KGEPRIME()
             {
-                for (std::size_t e = 0; e < n_exp; e++)
+                if (!KGEPRIME.has_value())
                 {
-                    // calculate error in spread of flow $gamma$
-                    auto gamma = xt::view(alpha, m, e)
-                                 * (xt::view(mean_obs, m, e, xt::all(), 0)
-                                    / xt::view(mean_prd, m, e, xt::all(), 0));
-
-                    // compute KGEPRIME
-                    xt::view(KGEPRIME, xt::all(), m, e) =  1 - xt::sqrt(
-                            xt::square(xt::view(r_pearson, m, e) - 1)
-                            + xt::square(gamma - 1)
-                            + xt::square(xt::view(bias, m, e) - 1)
+                    KGEPRIME = metrics::calc_KGEPRIME(
+                            get_mean_obs(), get_mean_prd(),
+                            get_r_pearson(), get_alpha(), get_bias(),
+                            n_srs, n_msk, n_exp
                     );
                 }
-            }
-        }
+                return KGEPRIME.value();
+            };
+        };
     }
 }
 
diff --git a/include/evalhyd/detail/determinist/metrics.hpp b/include/evalhyd/detail/determinist/metrics.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..23f93429194442bc5e5b7a17a9e03b5a8c49da58
--- /dev/null
+++ b/include/evalhyd/detail/determinist/metrics.hpp
@@ -0,0 +1,238 @@
+#ifndef EVALHYD_DETERMINIST_METRICS_HPP
+#define EVALHYD_DETERMINIST_METRICS_HPP
+
+#include <xtensor/xtensor.hpp>
+#include <xtensor/xview.hpp>
+#include <xtensor/xoperation.hpp>
+
+
+namespace evalhyd
+{
+    namespace determinist
+    {
+        namespace metrics
+        {
+            // Compute the root-mean-square error (RMSE).
+            //
+            // \param quad_err
+            //     Quadratic errors between observations and predictions.
+            //     shape: (series, time)
+            // \param t_msk
+            //     Temporal subsets of the whole record.
+            //     shape: (subsets, series, time)
+            // \param b_exp
+            //     Boostrap samples.
+            //     shape: (samples, time slice)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Root-mean-square errors.
+            //     shape: (series, subsets, samples)
+            inline xt::xtensor<double, 3> calc_RMSE(
+                    const xt::xtensor<double, 2>& quad_err,
+                    const xt::xtensor<bool, 3>& t_msk,
+                    const std::vector<xt::xkeep_slice<int>>& b_exp,
+                    std::size_t n_srs,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                // compute RMSE
+                xt::xtensor<double, 3> RMSE =
+                        xt::zeros<double>({n_srs, n_msk, n_exp});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    // apply the mask
+                    // (using NaN workaround until reducers work on masked_view)
+                    auto quad_err_masked = xt::where(xt::view(t_msk, m), quad_err, NAN);
+
+                    // compute variable one sample at a time
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        auto err2 = xt::view(quad_err_masked, xt::all(), b_exp[e]);
+                        xt::view(RMSE, xt::all(), m, e) = xt::sqrt(xt::nanmean(err2, -1));
+                    }
+                }
+
+                return RMSE;
+            }
+
+            // Compute the Nash-Sutcliffe Efficiency (NSE).
+            //
+            // \param quad_err
+            //     Quadratic errors between observations and predictions.
+            //     shape: (series, time)
+            // \param quad_obs
+            //     Quadratic errors between observations and mean observation.
+            //     shape: (subsets, samples, series, time)
+            // \param t_msk
+            //     Temporal subsets of the whole record.
+            //     shape: (subsets, series, time)
+            // \param b_exp
+            //     Boostrap samples.
+            //     shape: (samples, time slice)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Nash-Sutcliffe efficiencies.
+            //     shape: (series, subsets, samples)
+            inline xt::xtensor<double, 3> calc_NSE(
+                    const xt::xtensor<double, 2>& quad_err,
+                    const xt::xtensor<double, 4>& quad_obs,
+                    const xt::xtensor<bool, 3>& t_msk,
+                    const std::vector<xt::xkeep_slice<int>>& b_exp,
+                    std::size_t n_srs,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                xt::xtensor<double, 3> NSE =
+                        xt::zeros<double>({n_srs, n_msk, n_exp});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    // apply the mask
+                    // (using NaN workaround until reducers work on masked_view)
+                    auto quad_err_masked = xt::where(xt::view(t_msk, m), quad_err, NAN);
+
+                    // compute variable one sample at a time
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        // compute squared errors operands
+                        auto err2 = xt::view(quad_err_masked, xt::all(), b_exp[e]);
+                        xt::xtensor<double, 1> f_num =
+                                xt::nansum(err2, -1);
+                        auto obs2 = xt::view(quad_obs, m, e, xt::all(), b_exp[e]);
+                        xt::xtensor<double, 1> f_den =
+                                xt::nansum(obs2, -1);
+
+                        // compute NSE
+                        xt::view(NSE, xt::all(), m, e) = 1 - (f_num / f_den);
+                    }
+                }
+
+                return NSE;
+            }
+
+            // Compute the Kling-Gupta Efficiency (KGE).
+            //
+            // \param r_pearson
+            //     Pearson correlation coefficients.
+            //     shape: (subsets, samples, series)
+            // \param alpha
+            //     Alphas, ratios of standard deviations.
+            //     shape: (subsets, samples, series)
+            // \param bias
+            //     Biases.
+            //     shape: (subsets, samples, series)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Kling-Gupta efficiencies.
+            //     shape: (series, subsets, samples)
+            inline xt::xtensor<double, 3> calc_KGE(
+                    const xt::xtensor<double, 3>& r_pearson,
+                    const xt::xtensor<double, 3>& alpha,
+                    const xt::xtensor<double, 3>& bias,
+                    std::size_t n_srs,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                xt::xtensor<double, 3> KGE =
+                        xt::zeros<double>({n_srs, n_msk, n_exp});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        // compute KGE
+                        xt::view(KGE, xt::all(), m, e) = 1 - xt::sqrt(
+                                xt::square(xt::view(r_pearson, m, e) - 1)
+                                + xt::square(xt::view(alpha, m, e) - 1)
+                                + xt::square(xt::view(bias, m, e) - 1)
+                        );
+                    }
+                }
+
+                return KGE;
+            }
+
+            // Compute the modified Kling-Gupta Efficiency (KGEPRIME).
+            //
+            // \param mean_obs
+            //     Mean observed streamflow.
+            //     shape: (subsets, samples, series, 1)
+            // \param mean_prd
+            //     Mean predicted streamflow.
+            //     shape: (subsets, samples, series, 1)
+            // \param r_pearson
+            //     Pearson correlation coefficients.
+            //     shape: (subsets, samples, series)
+            // \param alpha
+            //     Alphas, ratios of standard deviations.
+            //     shape: (subsets, samples, series)
+            // \param bias
+            //     Biases.
+            //     shape: (subsets, samples, series)
+            // \param n_srs
+            //     Number of prediction series.
+            // \param n_msk
+            //     Number of temporal subsets.
+            // \param n_exp
+            //     Number of bootstrap samples.
+            // \return
+            //     Modified Kling-Gupta efficiencies.
+            //     shape: (series, subsets, samples)
+            inline xt::xtensor<double, 3> calc_KGEPRIME(
+                    const xt::xtensor<double, 4>& mean_obs,
+                    const xt::xtensor<double, 4>& mean_prd,
+                    const xt::xtensor<double, 3>& r_pearson,
+                    const xt::xtensor<double, 3>& alpha,
+                    const xt::xtensor<double, 3>& bias,
+                    std::size_t n_srs,
+                    std::size_t n_msk,
+                    std::size_t n_exp
+            )
+            {
+                xt::xtensor<double, 3> KGEPRIME =
+                        xt::zeros<double>({n_srs, n_msk, n_exp});
+
+                for (std::size_t m = 0; m < n_msk; m++)
+                {
+                    for (std::size_t e = 0; e < n_exp; e++)
+                    {
+                        // calculate error in spread of flow $gamma$
+                        auto gamma = xt::view(alpha, m, e)
+                                     * (xt::view(mean_obs, m, e, xt::all(), 0)
+                                        / xt::view(mean_prd, m, e, xt::all(), 0));
+
+                        // compute KGEPRIME
+                        xt::view(KGEPRIME, xt::all(), m, e) =  1 - xt::sqrt(
+                                xt::square(xt::view(r_pearson, m, e) - 1)
+                                + xt::square(gamma - 1)
+                                + xt::square(xt::view(bias, m, e) - 1)
+                        );
+                    }
+                }
+
+                return KGEPRIME;
+            }
+        }
+    }
+}
+
+#endif //EVALHYD_DETERMINIST_METRICS_HPP
diff --git a/include/evalhyd/detail/utils.hpp b/include/evalhyd/detail/utils.hpp
index 1d11b4bd1274a4b281b80d5a472b56f85454d6e2..64934cab69a06e81fe19bbb923578244f2d06b51 100644
--- a/include/evalhyd/detail/utils.hpp
+++ b/include/evalhyd/detail/utils.hpp
@@ -12,67 +12,6 @@ namespace evalhyd
 {
     namespace utils
     {
-        /// Procedure to determine based on a list of metrics which elements
-        /// and which metrics (and their associated elements) require to be
-        /// pre-computed for memoisation purposes.
-        ///
-        /// \param [in] metrics:
-        ///     Vector of strings for the metric(s) to be computed.
-        /// \param [in] elements:
-        ///     Map between metrics and their required computation elements.
-        /// \param [in] dependencies:
-        ///     Map between metrics and their dependencies.
-        /// \param [out] required_elements:
-        ///     Set of elements identified as required to be pre-computed.
-        /// \param [out] required_dependencies:
-        ///     Set of metrics identified as required to be pre-computed.
-        inline void find_requirements (
-                const std::vector<std::string>& metrics,
-                std::unordered_map<std::string, std::vector<std::string>>& elements,
-                std::unordered_map<std::string, std::vector<std::string>>& dependencies,
-                std::vector<std::string>& required_elements,
-                std::vector<std::string>& required_dependencies
-        )
-        {
-            std::unordered_set<std::string> found_elements;
-            std::unordered_set<std::string> found_dependencies;
-
-            for (const auto& metric : metrics)
-            {
-                // add elements to pre-computation set
-                for (const auto& element : elements[metric])
-                {
-                    if (found_elements.find(element) == found_elements.end())
-                    {
-                        found_elements.insert(element);
-                        required_elements.push_back(element);
-                    }
-                }
-
-                // add metric dependencies to pre-computation set
-                if (dependencies.find(metric) != dependencies.end())
-                {
-                    for (const auto& dependency : dependencies[metric])
-                    {
-                        if (found_dependencies.find(dependency) == found_dependencies.end())
-                        {
-                            found_dependencies.insert(dependency);
-                            required_dependencies.push_back(dependency);
-                        }
-                        // add dependency elements to pre-computation set
-                        for (const auto& element : elements[dependency])
-                        {
-                            if (found_elements.find(element) == found_elements.end())
-                            {
-                                found_elements.insert(element);
-                                required_elements.push_back(element);
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
         /// Procedure to check that all elements in the list of metrics are
         /// valid metrics.
         ///
diff --git a/include/evalhyd/evald.hpp b/include/evalhyd/evald.hpp
index f24254e522055a9b20615261cd5f3edebfcc4425..c7e7c149e2c2cd8568b514b89d330439a41859fc 100644
--- a/include/evalhyd/evald.hpp
+++ b/include/evalhyd/evald.hpp
@@ -340,67 +340,6 @@ namespace evalhyd
         // instantiate determinist evaluator
         determinist::Evaluator<D2, B2> evaluator(obs, prd, msk, exp);
 
-        // declare maps for memoisation purposes
-        std::unordered_map<std::string, std::vector<std::string>> elt;
-        std::unordered_map<std::string, std::vector<std::string>> dep;
-
-        // register potentially recurring computation elt across metrics
-        elt["RMSE"] = {"quad_err"};
-        elt["NSE"] = {"mean_obs", "quad_obs", "quad_err"};
-        elt["KGE"] = {"mean_obs", "mean_prd", "quad_obs", "quad_prd",
-                      "r_pearson", "alpha", "bias"};
-        elt["KGEPRIME"] = {"mean_obs", "mean_prd", "quad_obs", "quad_prd",
-                           "r_pearson", "alpha", "bias"};
-
-        // TODO: register nested metrics (i.e. metric dependent on another metric)
-        // dep[...] = {...};
-
-        // determine required elt/dep to be pre-computed
-        std::vector<std::string> req_elt;
-        std::vector<std::string> req_dep;
-
-        utils::find_requirements(metrics, elt, dep, req_elt, req_dep);
-
-        // pre-compute required elt
-        for ( const auto& element : req_elt )
-        {
-            if ( element == "mean_obs" )
-            {
-                evaluator.calc_mean_obs();
-            }
-            else if ( element == "mean_prd" )
-            {
-                evaluator.calc_mean_prd();
-            }
-            else if ( element == "quad_err" )
-            {
-                evaluator.calc_quad_err();
-            }
-            else if ( element == "quad_obs" )
-            {
-                evaluator.calc_quad_obs();
-            }
-            else if ( element == "quad_prd" )
-            {
-                evaluator.calc_quad_prd();
-            }
-            else if ( element == "r_pearson" )
-            {
-                evaluator.calc_r_pearson();
-            }
-            else if ( element == "alpha" )
-            {
-                evaluator.calc_alpha();
-            }
-            else if ( element == "bias" )
-            {
-                evaluator.calc_bias();
-            }
-        }
-
-        // TODO: pre-compute required dep
-        // for ( const auto& dependency : req_dep ) {...}
-
         // retrieve or compute requested metrics
         std::vector<xt::xarray<double>> r;
 
@@ -410,39 +349,27 @@ namespace evalhyd
         {
             if ( metric == "RMSE" )
             {
-                if (std::find(req_dep.begin(), req_dep.end(), metric)
-                    == req_dep.end())
-                {
-                    evaluator.calc_RMSE();
-                }
-                r.emplace_back(uncertainty::summarise(evaluator.RMSE, summary));
+                r.emplace_back(
+                        uncertainty::summarise(evaluator.get_RMSE(), summary)
+                );
             }
             else if ( metric == "NSE" )
             {
-                if (std::find(req_dep.begin(), req_dep.end(), metric)
-                    == req_dep.end())
-                {
-                    evaluator.calc_NSE();
-                }
-                r.emplace_back(uncertainty::summarise(evaluator.NSE, summary));
+                r.emplace_back(
+                        uncertainty::summarise(evaluator.get_NSE(), summary)
+                );
             }
             else if ( metric == "KGE" )
             {
-                if (std::find(req_dep.begin(), req_dep.end(), metric)
-                    == req_dep.end())
-                {
-                    evaluator.calc_KGE();
-                }
-                r.emplace_back(uncertainty::summarise(evaluator.KGE, summary));
+                r.emplace_back(
+                        uncertainty::summarise(evaluator.get_KGE(), summary)
+                );
             }
             else if ( metric == "KGEPRIME" )
             {
-                if (std::find(req_dep.begin(), req_dep.end(), metric)
-                    == req_dep.end())
-                {
-                    evaluator.calc_KGEPRIME();
-                }
-                r.emplace_back(uncertainty::summarise(evaluator.KGEPRIME, summary));
+                r.emplace_back(
+                        uncertainty::summarise(evaluator.get_KGEPRIME(), summary)
+                );
             }
         }