diff --git a/include/evalhyd/probabilistic.hpp b/include/evalhyd/probabilistic.hpp new file mode 100644 index 0000000000000000000000000000000000000000..880742fdef49b27e68775690e12b076813987f84 --- /dev/null +++ b/include/evalhyd/probabilistic.hpp @@ -0,0 +1,115 @@ +#ifndef EVALHYD_PROBABILISTIC_HPP +#define EVALHYD_PROBABILISTIC_HPP + +#include <xtensor/xexpression.hpp> +#include <xtensor/xmath.hpp> +#include <xtensor/xindex_view.hpp> +#include <xtensor/xio.hpp> + +#include "utils.hpp" + +namespace eh = evalhyd; + +namespace evalhyd +{ + /// Compute the Brier Score (BS). + /// + /// \param [in] q_obs: + /// 2D array of streamflow observations. + /// \param [in] q_frc: + /// 2D array of streamflow forecasts. + /// \param [in] q_thr: + /// 1D array of streamflow exceedance threshold(s). + /// \param [in] axis (optional): + /// Array axis along which the temporal dimension is for both + /// *frc* and *obs* (either 0 or 1). If not provided, set to + /// default value 0. + /// \return + /// 1D array of Brier Score for each threshold. + xt::xtensor<double, 1> bs( + const xt::xtensor<double, 2>& q_obs, + const xt::xtensor<double, 2>& q_frc, + const xt::xtensor<double, 1>& q_thr, + int axis = 0 + ) + { + // check that axis is a valid index for a 2D array + if ((axis != 0) && (axis != 1)) + { + throw std::out_of_range( + "array axis must be 0 or 1 for 2D arrays" + ); + } + + // determine observed realisation of threshold(s) exceedance + xt::xtensor<double, 2> p_obs = + xt::squeeze(eh::utils::is_above_threshold(q_obs, q_thr)); + + // determine forecast probability of threshold(s) exceedance + // > determine if members have exceeded threshold(s) + xt::xtensor<double, 3> e_frc = + eh::utils::is_above_threshold(q_frc, q_thr); + + // > calculate how many members have exceeded threshold(s) + int axis_mbr = (axis == 0) ? 1 : 0; + xt::xtensor<int, 2> n_frc = xt::sum(e_frc, axis_mbr); + + // > determine correspondence between number of exceeding members + // and forecast probabilities of exceedance assuming members are + // equiprobable + std::size_t n_mbr = q_frc.shape(axis_mbr); + xt::xtensor<double, 1> p_mbr = + xt::arange<double>(double (n_mbr + 1)) / (n_mbr + 1); + + // > replace number exceeding members with corresponding probability + xt::xtensor<double, 2> p_frc = xt::zeros<double>(n_frc.shape()); + for (int i = 0; i < q_thr.size(); i++) + { + xt::col(p_frc, i) = xt::index_view(p_mbr, xt::col(n_frc, i)); + } + + // return computed BS + return xt::mean(xt::square(p_obs - p_frc), std::vector<int> { axis }); + } + + /// Compute the Brier Skill Score (BSS). + /// + /// \param [in] q_obs: + /// 2D array of streamflow observations. + /// \param [in] q_frc: + /// 2D array of streamflow forecasts. + /// \param [in] q_thr: + /// 1D array of streamflow exceedance threshold(s). + /// \param [in] axis (optional): + /// Array axis along which the temporal dimension is for both + /// *frc* and *obs* (either 0 or 1). If not provided, set to + /// default value 0. + /// \return + /// 1D array of Brier Skill Score for each threshold. + xt::xtensor<double, 1> bss( + const xt::xtensor<double, 2>& q_obs, + const xt::xtensor<double, 2>& q_frc, + const xt::xtensor<double, 1>& q_thr, + int axis = 0 + ) + { + // determine observed realisation of threshold(s) exceedance + xt::xtensor<double, 2> p_obs = + xt::squeeze(eh::utils::is_above_threshold(q_obs, q_thr)); + + // calculate mean observed realisation of threshold(s) exceedance + xt::xtensor<double, 1> p_obs_mean = + xt::mean(p_obs, std::vector<int> { axis }); + + // calculate reference Brier Score + xt::xtensor<double, 1> bs_ref = xt::mean( + xt::square(p_obs - p_obs_mean), std::vector<int> { axis } + ); + + // return computed BSS + return 1 - (bs(q_obs, q_frc, q_thr, axis) / bs_ref); + } + +} + +#endif //EVALHYD_PROBABILISTIC_HPP diff --git a/include/evalhyd/utils.hpp b/include/evalhyd/utils.hpp new file mode 100644 index 0000000000000000000000000000000000000000..016e986fbc51adba8f7192e85cd240147381a27b --- /dev/null +++ b/include/evalhyd/utils.hpp @@ -0,0 +1,80 @@ +#ifndef EVALHYD_UTILS_HPP +#define EVALHYD_UTILS_HPP + +#include <xtensor/xtensor.hpp> +#include <xtensor/xbroadcast.hpp> +#include <xtensor/xadapt.hpp> +#include <xtensor/xview.hpp> +#include <xtensor/xmath.hpp> + +namespace evalhyd +{ + namespace utils + { + /// Determine whether flows are strictly greater than given threshold(s) + /// + /// Streamflow data is always expected as a 2D array (even if only for + /// one time series). Threshold(s) given is (are) always expected as a + /// 1D array (even if only one threshold is given). A 3D array is + /// returned whose first two dimensions are of the same sizes as the + /// streamflow data and whose third dimension is of size equal to the + /// number of thresholds given. The returned array contains ones where + /// the threshold is strictly exceeded, and zeros otherwise. + /// + /// \param [in] q: + /// 2D array of streamflow data + /// \param [in] thr: + /// 1D array of streamflow threshold(s) + /// \return + /// 3D array of ones and zeros + xt::xtensor<double, 3> is_above_threshold( + const xt::xtensor<double, 2>& q, + const xt::xtensor<double, 1>& thr + ) + { + // add one dimension to the streamflow data + auto new_shape = {q.shape(0), q.shape(1), thr.size()}; + auto q2 = xt::broadcast( + xt::view(q, xt::all(), xt::all(), xt::newaxis()), + new_shape + ); + + // return a boolean-like array + return q2 > thr; + } + + /// Determine whether flows are strictly lower than given threshold(s) + /// + /// Streamflow data is always expected as a 2D array (even if only for + /// one time series). Threshold(s) given is (are) always expected as a + /// 1D array (even if only one threshold is given). A 3D array is + /// returned whose first two dimensions are of the same sizes as the + /// streamflow data and whose third dimension is of size equal to the + /// number of thresholds given. The returned array contains ones where + /// the threshold is strictly not exceeded, and zeros otherwise. + /// + /// \param [in] q: + /// 2D array of streamflow data + /// \param [in] thr: + /// 1D array of streamflow threshold(s) + /// \return + /// 3D array of ones and zeros + xt::xtensor<double, 3> is_below_threshold( + const xt::xtensor<double, 2>& q, + const xt::xtensor<double, 1>& thr + ) + { + // add one dimension to the streamflow data + auto new_shape = {q.shape(0), q.shape(1), thr.size()}; + auto q2 = xt::broadcast( + xt::view(q, xt::all(), xt::all(), xt::newaxis()), + new_shape + ); + + // return a boolean-like array + return q2 < thr; + } + } +} + +#endif //EVALHYD_UTILS_HPP