diff --git a/include/evalhyd/detail/determinist/elements.hpp b/include/evalhyd/detail/determinist/elements.hpp index 74007923355a95f16279450a2fed03d9c6c68969..fb8dc43fa7ee5dcfde87093cad0c960e97c2c5d5 100644 --- a/include/evalhyd/detail/determinist/elements.hpp +++ b/include/evalhyd/detail/determinist/elements.hpp @@ -24,7 +24,7 @@ namespace evalhyd /// shape: (1, time) /// \param t_msk /// Temporal subsets of the whole record. - /// shape: (subsets, series, time) + /// shape: (series, subsets, time) /// \param b_exp /// Boostrap samples. /// shape: (samples, time slice) @@ -54,7 +54,8 @@ namespace evalhyd { // 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); + auto obs_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_obs, NAN); // compute variable one sample at a time for (std::size_t e = 0; e < n_exp; e++) @@ -76,7 +77,7 @@ namespace evalhyd /// shape: (series, time) /// \param t_msk /// Temporal subsets of the whole record. - /// shape: (subsets, series, time) + /// shape: (series, subsets, time) /// \param b_exp /// Boostrap samples. /// shape: (samples, time slice) @@ -106,7 +107,8 @@ namespace evalhyd { // 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 prd_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_prd, NAN); // compute variable one sample at a time for (std::size_t e = 0; e < n_exp; e++) @@ -151,7 +153,7 @@ namespace evalhyd /// shape: (subsets, samples, series, 1) /// \param t_msk /// Temporal subsets of the whole record. - /// shape: (subsets, series, time) + /// shape: (series, subsets, time) /// \param n_srs /// Number of prediction series. /// \param n_tim @@ -181,7 +183,8 @@ namespace evalhyd { // 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); + auto obs_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_obs, NAN); for (std::size_t e = 0; e < n_exp; e++) { @@ -204,7 +207,7 @@ namespace evalhyd /// shape: (subsets, samples, series, 1) /// \param t_msk /// Temporal subsets of the whole record. - /// shape: (subsets, series, time) + /// shape: (series, subsets, time) /// \param n_srs /// Number of prediction series. /// \param n_tim @@ -234,7 +237,8 @@ namespace evalhyd { // 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 prd_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_prd, NAN); for (std::size_t e = 0; e < n_exp; e++) { @@ -269,7 +273,7 @@ namespace evalhyd /// shape: (subsets, samples, series, time) /// \param t_msk /// Temporal subsets of the whole record. - /// shape: (subsets, series, time) + /// shape: (series, subsets, time) /// \param b_exp /// Boostrap samples. /// shape: (samples, time slice) @@ -306,8 +310,10 @@ namespace evalhyd { // 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); + auto prd_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_prd, NAN); + auto obs_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_obs, NAN); // compute variable one sample at a time for (std::size_t e = 0; e < n_exp; e++) @@ -349,7 +355,7 @@ namespace evalhyd /// shape: (subsets, samples, series, 1) /// \param t_msk /// Temporal subsets of the whole record. - /// shape: (subsets, series, time) + /// shape: (series, subsets, time) /// \param b_exp /// Boostrap samples. /// shape: (samples, time slice) @@ -383,8 +389,10 @@ namespace evalhyd { // 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); + auto prd_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_prd, NAN); + auto obs_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_obs, NAN); // compute variable one sample at a time for (std::size_t e = 0; e < n_exp; e++) @@ -410,7 +418,7 @@ namespace evalhyd /// shape: (series, time) /// \param t_msk /// Temporal subsets of the whole record. - /// shape: (subsets, series, time) + /// shape: (series, subsets, time) /// \param b_exp /// Boostrap samples. /// shape: (samples, time slice) @@ -442,8 +450,10 @@ namespace evalhyd { // 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); + auto prd_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_prd, NAN); + auto obs_masked = xt::where(xt::view(t_msk, xt::all(), m), + q_obs, NAN); // compute variable one sample at a time for (std::size_t e = 0; e < n_exp; e++) diff --git a/include/evalhyd/detail/determinist/evaluator.hpp b/include/evalhyd/detail/determinist/evaluator.hpp index e17bfd54fbe3a5b40ae8155d5a21488acf4d04ea..6314795e7096cfc29a69c988e5fcfdd0a5f64605 100644 --- a/include/evalhyd/detail/determinist/evaluator.hpp +++ b/include/evalhyd/detail/determinist/evaluator.hpp @@ -19,14 +19,15 @@ namespace evalhyd { namespace determinist { - template <class XD2, class XB2> + template <class XD2, class XB3> class Evaluator { private: // members for input data const XD2& q_obs; const XD2& q_prd; - xt::xtensor<bool, 3> t_msk; + // members for optional input data + XB3 t_msk; const std::vector<xt::xkeep_slice<int>>& b_exp; // members for dimensions @@ -56,7 +57,7 @@ namespace evalhyd { if (!mean_obs.has_value()) { - mean_obs = elements::calc_mean_obs<XD2>( + mean_obs = elements::calc_mean_obs( q_obs, t_msk, b_exp, n_srs, n_msk, n_exp ); } @@ -149,28 +150,34 @@ namespace evalhyd // constructor method Evaluator(const XD2& obs, const XD2& prd, - const XB2& msk, + const XB3& msk, const std::vector<xt::xkeep_slice<int>>& exp) : - q_obs{obs}, q_prd{prd}, b_exp{exp} + q_obs{obs}, q_prd{prd}, t_msk{msk}, b_exp{exp} { + // initialise a mask if none provided + // (corresponding to no temporal subset) + if (msk.size() < 1) + { + t_msk = xt::ones<bool>( + {q_prd.shape(0), std::size_t {1}, q_prd.shape(1)} + ); + } + // determine size for recurring dimensions - n_tim = q_prd.shape(1); n_srs = q_prd.shape(0); - n_msk = msk.shape(0); + n_tim = q_prd.shape(1); + n_msk = msk.shape(1); n_exp = b_exp.size(); // 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++) + for (std::size_t s = 0; s < n_srs; s++) { - xt::view(t_msk, m) = - xt::where(obs_nan || prd_nan, - false, xt::view(t_msk, m)); + auto obs_nan = xt::isnan(xt::view(q_obs, 0)); + auto prd_nan = xt::isnan(xt::view(q_prd, s)); + + auto msk_nan = xt::where(obs_nan || prd_nan)[0]; + + xt::view(t_msk, s, xt::all(), xt::keep(msk_nan)) = false; } }; diff --git a/include/evalhyd/detail/determinist/metrics.hpp b/include/evalhyd/detail/determinist/metrics.hpp index 8ee318f0cfadff8f4c4933c79c85f1fbf3c5a6d1..550c69561efd023d56f7ad820242134d95341560 100644 --- a/include/evalhyd/detail/determinist/metrics.hpp +++ b/include/evalhyd/detail/determinist/metrics.hpp @@ -23,7 +23,7 @@ namespace evalhyd /// shape: (series, time) /// \param t_msk /// Temporal subsets of the whole record. - /// shape: (subsets, series, time) + /// shape: (series, subsets, time) /// \param b_exp /// Boostrap samples. /// shape: (samples, time slice) @@ -53,7 +53,8 @@ namespace evalhyd { // 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); + auto quad_err_masked = xt::where(xt::view(t_msk, xt::all(), m), + quad_err, NAN); // compute variable one sample at a time for (std::size_t e = 0; e < n_exp; e++) @@ -76,7 +77,7 @@ namespace evalhyd /// shape: (subsets, samples, series, time) /// \param t_msk /// Temporal subsets of the whole record. - /// shape: (subsets, series, time) + /// shape: (series, subsets, time) /// \param b_exp /// Boostrap samples. /// shape: (samples, time slice) @@ -106,7 +107,8 @@ namespace evalhyd { // 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); + auto quad_err_masked = xt::where(xt::view(t_msk, xt::all(), m), + quad_err, NAN); // compute variable one sample at a time for (std::size_t e = 0; e < n_exp; e++) diff --git a/include/evalhyd/evald.hpp b/include/evalhyd/evald.hpp index 9bdd3658a697b0d739e63fccc08b186dcd73ed37..31bc5440410e44783d7af37c80795a2549f14866 100644 --- a/include/evalhyd/evald.hpp +++ b/include/evalhyd/evald.hpp @@ -32,9 +32,9 @@ namespace evalhyd /// (e.g. ``xt::xtensor<double, 2>``, ``xt::pytensor<double, 2>``, /// ``xt::rtensor<double, 2>``, etc.). /// - /// XB2: Any 2-dimensional container class storing boolean elements - /// (e.g. ``xt::xtensor<bool, 4>``, ``xt::pytensor<bool, 4>``, - /// ``xt::rtensor<bool, 4>``, etc.). + /// XB3: Any 3-dimensional container class storing boolean elements + /// (e.g. ``xt::xtensor<bool, 3>``, ``xt::pytensor<bool, 3>``, + /// ``xt::rtensor<bool, 3>``, etc.). /// /// :Parameters: /// @@ -76,13 +76,11 @@ namespace evalhyd /// epsilon, as recommended by `Pushpalatha et al. (2012) /// <https://doi.org/10.1016/j.jhydrol.2011.11.055>`_. /// - /// t_msk: ``XB2``, optional + /// t_msk: ``XB3``, optional /// Mask used to temporally subset of the whole streamflow time series /// (where True/False is used for the time steps to include/discard in - /// the subset). If provided, masks must feature the same number of - /// dimensions as observations and predictions, and it must - /// broadcastable with both of them. - /// shape: (subsets, time) + /// the subset). + /// shape: (series, subsets, time) /// /// .. seealso:: :doc:`../../functionalities/temporal-masking` /// @@ -153,12 +151,12 @@ namespace evalhyd /// /// .. code-block:: c++ /// - /// xt::xtensor<double, 2> msk = {{ 1, 1, 0, 1, 0 }}; + /// xt::xtensor<double, 3> msk = {{{ 1, 1, 0, 1, 0 }}}; /// /// evalhyd::evald(obs, prd, {"NSE"}, "none", 1, -9, msk); /// /// \endrst - template <class XD2, class XB2 = xt::xtensor<bool, 2>> + template <class XD2, class XB3 = xt::xtensor<bool, 3>> std::vector<xt::xarray<double>> evald( const xt::xexpression<XD2>& q_obs, const xt::xexpression<XD2>& q_prd, @@ -169,7 +167,7 @@ namespace evalhyd xtl::missing<double>(), xtl::xoptional<double, bool> epsilon = xtl::missing<double>(), - const xt::xexpression<XB2>& t_msk = XB2({}), + const xt::xexpression<XB3>& t_msk = XB3({}), const xt::xtensor<std::array<char, 32>, 1>& m_cdt = {}, xtl::xoptional<const std::unordered_map<std::string, int>, bool> bootstrap = xtl::missing<const std::unordered_map<std::string, int>>(), @@ -183,10 +181,10 @@ namespace evalhyd "observations and/or predictions are not two-dimensional" ); } - if (xt::get_rank<XB2>::value != 2) + if (xt::get_rank<XB3>::value != 3) { throw std::runtime_error( - "temporal masks are not two-dimensional" + "temporal masks are not three-dimensional" ); } @@ -194,7 +192,7 @@ namespace evalhyd const XD2& q_obs_ = q_obs.derived_cast(); const XD2& q_prd_ = q_prd.derived_cast(); - const XB2& t_msk_ = t_msk.derived_cast(); + const XB3& t_msk_ = t_msk.derived_cast(); // check that the metrics to be computed are valid utils::check_metrics( @@ -219,7 +217,7 @@ namespace evalhyd } if (t_msk_.size() > 0) { - if (q_obs_.shape(1) != t_msk_.shape(1)) + if (q_obs_.shape(1) != t_msk_.shape(2)) { throw std::runtime_error( "observations and masks feature different " @@ -245,44 +243,51 @@ namespace evalhyd "observations contain more than one time series" ); } + if (t_msk_.size() > 0) + { + if (q_prd_.shape(0) != t_msk_.shape(0)) + { + throw std::runtime_error( + "predictions and masks feature different " + "number of series" + ); + } + } // retrieve dimensions - std::size_t n_tim = q_obs_.shape(1); - std::size_t n_msk = t_msk_.size() > 0 ? t_msk_.shape(0) : - (m_cdt.size() > 0 ? m_cdt.shape(0) : 1); + std::size_t n_tim = q_prd_.shape(1); - // initialise a mask if none provided - // (corresponding to no temporal subset) + // generate masks from conditions if provided auto gen_msk = [&]() { - // if t_msk provided, it takes priority - if (t_msk_.size() > 0) + if ((t_msk_.size() < 1) && (m_cdt.size() > 0)) { - return t_msk_; - } - // else if m_cdt provided, use them to generate t_msk - else if (m_cdt.size() > 0) - { - XB2 c_msk = xt::zeros<bool>({n_msk, n_tim}); + std::size_t n_srs = q_prd_.shape(0); + std::size_t n_msk = m_cdt.shape(0); - for (std::size_t m = 0; m < n_msk; m++) + XB3 c_msk = xt::zeros<bool>({n_srs, n_msk, n_tim}); + + for (std::size_t s = 0; s < n_srs; s++) { - xt::view(c_msk, m) = - masks::generate_mask_from_conditions( - m_cdt[0], xt::view(q_obs_, 0), q_prd_ - ); + for (std::size_t m = 0; m < n_msk; m++) + { + xt::view(c_msk, s, m) = + masks::generate_mask_from_conditions( + xt::view(m_cdt, m), + xt::view(q_obs_, 0), + xt::view(q_prd_, s, xt::newaxis()) + ); + } } return c_msk; } - // if neither t_msk nor m_cdt provided, generate dummy mask else { - return XB2({xt::ones<bool>({std::size_t{1}, n_tim})}); + return XB3({}); } }; - - auto msk = gen_msk(); + const XB3 c_msk = gen_msk(); // apply streamflow transformation if requested auto q_transform = [&](const XD2& q) @@ -392,7 +397,11 @@ namespace evalhyd } // instantiate determinist evaluator - determinist::Evaluator<XD2, XB2> evaluator(obs, prd, msk, exp); + determinist::Evaluator<XD2, XB3> evaluator( + obs, prd, + t_msk_.size() > 0 ? t_msk_: (m_cdt.size() > 0 ? c_msk : t_msk_), + exp + ); // retrieve or compute requested metrics std::vector<xt::xarray<double>> r; diff --git a/tests/test_determinist.cpp b/tests/test_determinist.cpp index decdf97cef9b02b689d58be8f3ac2adc3d38dfe9..b7ee38e3364101e29d78b27393ac4632089ce7f4 100644 --- a/tests/test_determinist.cpp +++ b/tests/test_determinist.cpp @@ -152,9 +152,11 @@ TEST(DeterministTests, TestMasks) std::tie(observed, predicted) = load_data_d(); // generate temporal subset by dropping 20 first time steps - xt::xtensor<bool, 2> masks = - xt::ones<bool>({std::size_t {1}, std::size_t {observed.size()}}); - xt::view(masks, 0, xt::range(0, 20)) = 0; + xt::xtensor<bool, 3> masks = + xt::ones<bool>({std::size_t {predicted.shape(0)}, + std::size_t {1}, + std::size_t {observed.size()}}); + xt::view(masks, xt::all(), 0, xt::range(0, 20)) = 0; // compute scores using masks to subset whole record std::vector<xt::xarray<double>> metrics_masked = @@ -183,7 +185,7 @@ TEST(DeterministTests, TestMaskingConditions) std::tie(observed, predicted) = load_data_d(); // generate dummy empty masks required to access next optional argument - xt::xtensor<bool, 2> masks; + xt::xtensor<bool, 3> masks; // conditions on streamflow values _________________________________________ @@ -360,7 +362,7 @@ TEST(DeterministTests, TestBootstrap) {}, // transform {}, // exponent {}, // epsilon - xt::xtensor<bool, 2>({}), // t_msk + xt::xtensor<bool, 3>({}), // t_msk {}, // m_cdt bootstrap, datetimes