diff --git a/src/stan/model/indexing/assign.hpp b/src/stan/model/indexing/assign.hpp index 841be70a4a0..883e3e5d950 100644 --- a/src/stan/model/indexing/assign.hpp +++ b/src/stan/model/indexing/assign.hpp @@ -27,6 +27,8 @@ namespace model { * index_max - index from 1:max * index_min_max - index from min:max * nil_index_list - no-op + * Ranges are empty when the lower bound exceeds the upper bound, including + * min > N for index_min and max < 1 for index_max. * The order of the overloads are * vector / row_vector: * - all index overloads @@ -169,11 +171,16 @@ template * = nullptr, require_all_not_std_vector_t* = nullptr> inline void assign(Vec1&& x, const Vec2& y, const char* name, index_min idx) { - stan::math::check_range("vector[min] assign", name, x.size(), idx.min_); - stan::math::check_size_match("vector[min] assign", name, - x.size() - idx.min_ + 1, "right hand side", - y.size()); - internal::assign_impl(x.tail(x.size() - idx.min_ + 1), y, name); + if (likely(idx.min_ <= x.size())) { + stan::math::check_range("vector[min] assign", name, x.size(), idx.min_); + stan::math::check_size_match("vector[min] assign", name, + x.size() - idx.min_ + 1, "right hand side", + y.size()); + internal::assign_impl(x.tail(x.size() - idx.min_ + 1), y, name); + } else { + stan::math::check_size_match("vector[min > size] assign", name, 0, + "right hand side", y.size()); + } } /** @@ -329,13 +336,20 @@ template * = nullptr, require_matrix_t* = nullptr> inline void assign(Mat1&& x, const Mat2& y, const char* name, index_min idx) { - const auto row_size = x.rows() - (idx.min_ - 1); - stan::math::check_range("matrix[min] assign row", name, x.rows(), idx.min_); - stan::math::check_size_match("matrix[min] assign rows", name, row_size, - "right hand side rows", y.rows()); - stan::math::check_size_match("matrix[min] assign columns", name, x.cols(), - "right hand side columns", y.cols()); - internal::assign_impl(x.bottomRows(row_size), y, name); + if (likely(idx.min_ <= x.rows())) { + stan::math::check_range("matrix[min] assign row", name, x.rows(), idx.min_); + const auto row_size = x.rows() - idx.min_ + 1; + stan::math::check_size_match("matrix[min] assign rows", name, row_size, + "right hand side rows", y.rows()); + stan::math::check_size_match("matrix[min] assign columns", name, x.cols(), + "right hand side columns", y.cols()); + internal::assign_impl(x.bottomRows(row_size), y, name); + } else { + stan::math::check_size_match("matrix[min > rows] assign rows", name, 0, + "right hand side rows", y.rows()); + stan::math::check_size_match("matrix[min] assign columns", name, x.cols(), + "right hand side columns", y.cols()); + } } /** @@ -685,13 +699,17 @@ template * = nullptr> inline void assign(Mat1&& x, const Mat2& y, const char* name, const Idx& row_idx, index_min col_idx) { - const auto start_col = col_idx.min_ - 1; - const auto col_size = x.cols() - start_col; - stan::math::check_range("matrix[..., min] assign column", name, x.cols(), - col_idx.min_); - stan::math::check_size_match("matrix[..., min] assign columns", name, - col_size, "right hand side columns", y.cols()); - assign(x.rightCols(col_size), y, name, row_idx); + if (likely(col_idx.min_ <= x.cols())) { + stan::math::check_range("matrix[..., min] assign column", name, x.cols(), + col_idx.min_); + const auto col_size = x.cols() - col_idx.min_ + 1; + stan::math::check_size_match("matrix[..., min] assign columns", name, + col_size, "right hand side columns", y.cols()); + assign(x.rightCols(col_size), y, name, row_idx); + } else { + stan::math::check_size_match("matrix[..., min > cols] assign columns", name, + 0, "right hand side columns", y.cols()); + } } /** diff --git a/src/stan/model/indexing/rvalue.hpp b/src/stan/model/indexing/rvalue.hpp index 2c1854c1796..0b57f5c6409 100644 --- a/src/stan/model/indexing/rvalue.hpp +++ b/src/stan/model/indexing/rvalue.hpp @@ -25,6 +25,8 @@ namespace model { * index_max - index from 1:max * index_min_max - index from min:max * nil_index_list - no-op + * Ranges are empty when the lower bound exceeds the upper bound, including + * min > N for index_min and max < 1 for index_max. * The order of the overloads are * vector / row_vector: * - all index overloads @@ -211,8 +213,12 @@ inline auto rvalue(Vec&& v, const char* name, index_min_max idx) { template * = nullptr, require_not_std_vector_t* = nullptr> inline auto rvalue(Vec&& x, const char* name, index_min idx) { - stan::math::check_range("vector[min] indexing", name, x.size(), idx.min_); - return x.tail(x.size() - idx.min_ + 1); + if (idx.min_ <= x.size()) { + stan::math::check_range("vector[min] indexing", name, x.size(), idx.min_); + return x.tail(x.size() - idx.min_ + 1); + } else { + return x.tail(0); + } } /** @@ -301,9 +307,12 @@ inline auto rvalue(EigMat&& x, const char* name, MultiIndex&& idx) { */ template * = nullptr> inline auto rvalue(Mat&& x, const char* name, index_min idx) { - const auto row_size = x.rows() - (idx.min_ - 1); - math::check_range("matrix[min] row indexing", name, x.rows(), idx.min_); - return x.bottomRows(row_size); + if (idx.min_ <= x.rows()) { + math::check_range("matrix[min] row indexing", name, x.rows(), idx.min_); + return x.bottomRows(x.rows() - idx.min_ + 1); + } else { + return x.bottomRows(0); + } } /** @@ -637,10 +646,14 @@ inline auto rvalue(Mat&& x, const char* name, Idx&& row_idx, template * = nullptr> inline auto rvalue(Mat&& x, const char* name, Idx&& row_idx, index_min col_idx) { - const Eigen::Index col_size = x.cols() - (col_idx.min_ - 1); - math::check_range("matrix[..., min] column indexing", name, x.cols(), - col_idx.min_); - return rvalue(x.rightCols(col_size), name, std::forward(row_idx)); + if (col_idx.min_ <= x.cols()) { + math::check_range("matrix[..., min] column indexing", name, x.cols(), + col_idx.min_); + const Eigen::Index col_size = x.cols() - col_idx.min_ + 1; + return rvalue(x.rightCols(col_size), name, std::forward(row_idx)); + } else { + return rvalue(x.rightCols(0), name, std::forward(row_idx)); + } } /** diff --git a/src/stan/model/indexing/rvalue_index_size.hpp b/src/stan/model/indexing/rvalue_index_size.hpp index f0044c95e23..00e70196f95 100644 --- a/src/stan/model/indexing/rvalue_index_size.hpp +++ b/src/stan/model/indexing/rvalue_index_size.hpp @@ -50,7 +50,7 @@ inline int rvalue_index_size(const index_omni& idx, int size) noexcept { * @return Size of result. */ inline int rvalue_index_size(const index_min& idx, int size) noexcept { - return size - idx.min_ + 1; + return (idx.min_ > size) ? 0 : (size - idx.min_ + 1); } /** diff --git a/src/test/unit/model/indexing/assign_test.cpp b/src/test/unit/model/indexing/assign_test.cpp index fdf87338fa0..943d7e17924 100644 --- a/src/test/unit/model/indexing/assign_test.cpp +++ b/src/test/unit/model/indexing/assign_test.cpp @@ -615,7 +615,7 @@ TEST(ModelIndexing, lvalueMatrixMultiMulti) { EXPECT_FLOAT_EQ(y(1, 2), x(2, 3)); test_throw(x, y, index_min_max(2, 3), index_min(0)); - test_throw(x, y, index_min_max(2, 3), index_min(10)); + test_throw_ia(x, y, index_min_max(2, 3), index_min(10)); test_throw_ia(x, y, index_min_max(1, 3), index_min(2)); x << 0.0, 0.1, 0.2, 0.3, 1.0, 1.1, 1.2, 1.3, 2.0, 2.1, 2.2, 2.3; @@ -1113,7 +1113,7 @@ TEST(model_indexing, assign_densemat_densemat_min_max_index_min_index) { EXPECT_FLOAT_EQ(y(1, 2), x(2, 3)); test_throw(x, y, index_min_max(2, 3), index_min(0)); - test_throw(x, y, index_min_max(2, 3), index_min(10)); + test_throw_ia(x, y, index_min_max(2, 3), index_min(10)); test_throw_ia(x, y, index_min_max(1, 3), index_min(2)); } diff --git a/src/test/unit/model/indexing/assign_varmat_test.cpp b/src/test/unit/model/indexing/assign_varmat_test.cpp index a6435bccfb9..f4355578a66 100644 --- a/src/test/unit/model/indexing/assign_varmat_test.cpp +++ b/src/test/unit/model/indexing/assign_varmat_test.cpp @@ -256,7 +256,7 @@ void test_min_vec() { check_adjs(check_i, x, "lhs"); check_adjs([](int /* i */) { return true; }, y, "rhs"); test_throw_out_of_range(x, y, index_min(0)); - test_throw_out_of_range(x, y, index_min(6)); + test_throw_invalid_arg(x, y, index_min(6)); test_throw_invalid_arg(x, conditionally_generate_linear_var_vector(4), index_min(3)); test_throw_invalid_arg(x, conditionally_generate_linear_var_vector(2), @@ -1104,7 +1104,7 @@ void min_matrix_test() { check_adjs(check_i_x, check_all, x, "lhs", 0); check_adjs(check_all, y, "rhs", 1.0); test_throw_out_of_range(x, y, index_min(0)); - test_throw_out_of_range(x, y, index_min(4)); + test_throw_invalid_arg(x, y, index_min(4)); test_throw_invalid_arg(x, y, index_min(1)); var_value z(MatrixXd::Ones(1, 2)); test_throw_invalid_arg(x, z, index_min(2)); @@ -1137,7 +1137,7 @@ void minmax_min_matrix_test() { test_throw_out_of_range(x, y, index_min_max(0, 3), index_min(2)); test_throw_out_of_range(x, y, index_min_max(2, 4), index_min(2)); test_throw_out_of_range(x, y, index_min_max(2, 3), index_min(0)); - test_throw_out_of_range(x, y, index_min_max(2, 3), index_min(5)); + test_throw_invalid_arg(x, y, index_min_max(2, 3), index_min(5)); test_throw_invalid_arg(x, conditionally_generate_linear_var_matrix(1, 3, 10), index_min_max(2, 3), index_min(2)); test_throw_invalid_arg(x, conditionally_generate_linear_var_matrix(2, 5, 10), @@ -1202,7 +1202,7 @@ void min_max_matrix_test() { auto check_all = [](int /* i*/) { return true; }; check_adjs(check_all, check_all, y, "rhs"); test_throw_out_of_range(x, y, index_min(0), index_max(2)); - test_throw_out_of_range(x, y, index_min(5), index_max(2)); + test_throw_invalid_arg(x, y, index_min(5), index_max(2)); test_throw_invalid_arg(x, y, index_min(2), index_max(0)); test_throw_out_of_range(x, y, index_min(2), index_max(5)); test_throw_invalid_arg(x, y, index_min(2), index_max(1)); diff --git a/src/test/unit/model/indexing/empty_range_test.cpp b/src/test/unit/model/indexing/empty_range_test.cpp new file mode 100644 index 00000000000..ff0461dc374 --- /dev/null +++ b/src/test/unit/model/indexing/empty_range_test.cpp @@ -0,0 +1,180 @@ +#include +#include +#include +#include +#include +#include + +using stan::model::assign; +using stan::model::index_max; +using stan::model::index_min; +using stan::model::index_multi; +using stan::model::index_omni; +using stan::model::index_uni; +using stan::model::rvalue; + +namespace { + +// An empty read contributes no adjoints; an empty assignment leaves the +// original values and their derivatives unchanged. +template +void check_empty_slice(T& x, int rows, int cols, const Idxs&... idxs) { + auto selected = rvalue(x, "x", idxs...); + EXPECT_EQ(rows, selected.rows()); + EXPECT_EQ(cols, selected.cols()); + if constexpr (stan::is_var>::value) { + stan::math::set_zero_all_adjoints(); + stan::math::sum(selected).grad(); + for (int i = 0; i < x.size(); ++i) { + if constexpr (stan::is_var_matrix::value) { + EXPECT_EQ(0, x.adj().coeff(i)); + } else { + EXPECT_EQ(0, x.coeff(i).adj()); + } + } + } + using plain_t = stan::plain_type_t; + plain_t empty(selected); + EXPECT_NO_THROW(assign(x, empty, "x", idxs...)); + EXPECT_TRUE(stan::math::value_of(x).isOnes()); + if constexpr (stan::is_var>::value) { + stan::math::set_zero_all_adjoints(); + stan::math::sum(x).grad(); + for (int i = 0; i < x.size(); ++i) { + if constexpr (stan::is_var_matrix::value) { + EXPECT_EQ(1, x.adj().coeff(i)); + } else { + EXPECT_EQ(1, x.coeff(i).adj()); + } + } + } +} + +template +void check_vector_empty_ranges() { + using values_t + = Eigen::Matrix; + for (int size : {0, 3}) { + T x(values_t::Ones(size)); + const int rows = T::RowsAtCompileTime == 1 ? 1 : 0; + const int cols = T::RowsAtCompileTime == 1 ? 0 : 1; + for (int min : {size + 1, size + 4, std::numeric_limits::max()}) { + check_empty_slice(x, rows, cols, index_min(min)); + EXPECT_THROW(assign(x, values_t::Ones(1), "x", index_min(min)), + std::invalid_argument); + } + for (int max : {0, -3, std::numeric_limits::min()}) { + check_empty_slice(x, rows, cols, index_max(max)); + EXPECT_THROW(assign(x, values_t::Ones(1), "x", index_max(max)), + std::invalid_argument); + } + EXPECT_THROW(rvalue(x, "x", index_min(0)), std::out_of_range); + EXPECT_THROW(rvalue(x, "x", index_max(size + 1)), std::out_of_range); + } +} + +template +void check_matrix_empty_ranges() { + for (int rows : {0, 3}) { + for (int cols : {0, 4}) { + T x(Eigen::MatrixXd::Ones(rows, cols)); + for (int min : {rows + 1, rows + 4, std::numeric_limits::max()}) { + check_empty_slice(x, 0, cols, index_min(min)); + check_empty_slice(x, 0, cols, index_min(min), index_omni()); + EXPECT_THROW(assign(x, Eigen::MatrixXd(1, cols), "x", index_min(min)), + std::invalid_argument); + EXPECT_THROW( + assign(x, Eigen::MatrixXd(0, cols + 1), "x", index_min(min)), + std::invalid_argument); + if (cols > 0) { + check_empty_slice(x, 0, 1, index_min(min), index_uni(1)); + check_empty_slice(x, 0, 2, index_min(min), + index_multi(std::vector{1, 2})); + } + } + for (int min : {cols + 1, cols + 4, std::numeric_limits::max()}) { + check_empty_slice(x, rows, 0, index_omni(), index_min(min)); + check_empty_slice(x, 0, 0, index_min(rows + 4), index_min(min)); + check_empty_slice(x, 0, 0, index_max(0), index_min(min)); + EXPECT_THROW(assign(x, Eigen::MatrixXd(rows, 1), "x", index_omni(), + index_min(min)), + std::invalid_argument); + if (rows > 0) { + check_empty_slice(x, 1, 0, index_uni(1), index_min(min)); + check_empty_slice(x, 2, 0, index_multi(std::vector{1, 2}), + index_min(min)); + } + } + for (int max : {0, -3, std::numeric_limits::min()}) { + check_empty_slice(x, 0, cols, index_max(max)); + check_empty_slice(x, rows, 0, index_omni(), index_max(max)); + check_empty_slice(x, 0, 0, index_min(rows + 4), index_max(max)); + EXPECT_THROW(assign(x, Eigen::MatrixXd(1, cols), "x", index_max(max)), + std::invalid_argument); + EXPECT_THROW(assign(x, Eigen::MatrixXd(rows, 1), "x", index_omni(), + index_max(max)), + std::invalid_argument); + } + EXPECT_THROW(rvalue(x, "x", index_min(0)), std::out_of_range); + EXPECT_THROW(rvalue(x, "x", index_omni(), index_min(0)), + std::out_of_range); + EXPECT_THROW(rvalue(x, "x", index_max(rows + 1)), std::out_of_range); + EXPECT_THROW(rvalue(x, "x", index_omni(), index_max(cols + 1)), + std::out_of_range); + } + } +} + +} // namespace + +TEST(ModelIndexingEmptyRange, eigen) { + check_vector_empty_ranges(); + check_vector_empty_ranges(); + check_matrix_empty_ranges(); +} + +TEST(ModelIndexingEmptyRange, eigenVar) { + stan::math::nested_rev_autodiff nested; + check_vector_empty_ranges>(); + check_vector_empty_ranges>(); + check_matrix_empty_ranges>(); +} + +TEST(ModelIndexingEmptyRange, varmat) { + stan::math::nested_rev_autodiff nested; + check_vector_empty_ranges>(); + check_vector_empty_ranges>(); + check_matrix_empty_ranges>(); +} + +TEST(ModelIndexingEmptyRange, arrays) { + for (int size : {0, 3}) { + std::vector x(size, 1); + std::vector> xx(size, x); + const auto check = [&](auto idx) { + EXPECT_TRUE(rvalue(x, "x", idx).empty()); + EXPECT_TRUE(rvalue(xx, "xx", idx, index_omni()).empty()); + EXPECT_NO_THROW(assign(x, std::vector{}, "x", idx)); + EXPECT_NO_THROW(assign(xx, std::vector>{}, "xx", idx, + index_omni())); + EXPECT_THROW(assign(x, std::vector{2}, "x", idx), + std::invalid_argument); + if (size > 0) { + auto inner_empty = rvalue(xx, "xx", index_omni(), idx); + EXPECT_EQ(size, inner_empty.size()); + for (const auto& inner : inner_empty) { + EXPECT_TRUE(inner.empty()); + } + EXPECT_NO_THROW(assign(xx, inner_empty, "xx", index_omni(), idx)); + } + EXPECT_EQ(std::vector(size, 1), x); + EXPECT_EQ(std::vector>(size, x), xx); + }; + for (int min : {size + 1, size + 4, std::numeric_limits::max()}) { + check(index_min(min)); + } + for (int max : {0, -3, std::numeric_limits::min()}) { + check(index_max(max)); + } + } +} diff --git a/src/test/unit/model/indexing/rvalue_index_size_test.cpp b/src/test/unit/model/indexing/rvalue_index_size_test.cpp index 11215e6a42a..d9738f8a4aa 100644 --- a/src/test/unit/model/indexing/rvalue_index_size_test.cpp +++ b/src/test/unit/model/indexing/rvalue_index_size_test.cpp @@ -31,6 +31,9 @@ TEST(modelIndexingRvalueIndexSize, min) { index_min idx(3); EXPECT_EQ(8, rvalue_index_size(idx, 10)); + EXPECT_EQ(0, rvalue_index_size(index_min(11), 10)); + EXPECT_EQ(0, rvalue_index_size(index_min(20), 10)); + EXPECT_EQ(0, rvalue_index_size(index_min(1), 0)); } TEST(modelIndexingRvalueIndexSize, max) { @@ -39,6 +42,9 @@ TEST(modelIndexingRvalueIndexSize, max) { index_max idx(5); EXPECT_EQ(5, rvalue_index_size(idx, 10)); + EXPECT_EQ(0, rvalue_index_size(index_max(0), 10)); + EXPECT_EQ(0, rvalue_index_size(index_max(-5), 10)); + EXPECT_EQ(0, rvalue_index_size(index_max(0), 0)); } TEST(modelIndexingRvalueIndexSize, minMax) { diff --git a/src/test/unit/model/indexing/rvalue_test.cpp b/src/test/unit/model/indexing/rvalue_test.cpp index 7af6eb9008c..27791da8c8f 100644 --- a/src/test/unit/model/indexing/rvalue_test.cpp +++ b/src/test/unit/model/indexing/rvalue_test.cpp @@ -100,9 +100,9 @@ TEST(ModelIndexing, rvalue_vector_min_nil) { EXPECT_FLOAT_EQ(x[n + k], rx[n]); } - EXPECT_THROW(rvalue(x, "", index_min(7)), std::domain_error); + EXPECT_EQ(0, rvalue(x, "", index_min(7)).size()); - // test_out_of_range(x, index_min(0)); + test_out_of_range(x, index_min(0)); } TEST(ModelIndexing, rvalue_eigen_vector_min_nil) { @@ -115,7 +115,7 @@ TEST(ModelIndexing, rvalue_eigen_vector_min_nil) { EXPECT_FLOAT_EQ(x[n + k - 1], rx[n]); } - test_out_of_range(x, index_min(7)); + EXPECT_EQ(0, rvalue(x, "", index_min(7)).size()); test_out_of_range(x, index_min(0)); } diff --git a/src/test/unit/model/indexing/rvalue_varmat_test.cpp b/src/test/unit/model/indexing/rvalue_varmat_test.cpp index 7a474698392..a6fd698a5a6 100644 --- a/src/test/unit/model/indexing/rvalue_varmat_test.cpp +++ b/src/test/unit/model/indexing/rvalue_varmat_test.cpp @@ -524,7 +524,7 @@ TEST_F(RvalueRev, min_uni_mat) { EXPECT_MATRIX_EQ(y.adj(), y_exp_adj); test_throw_out_of_range(x, index_min(0), index_uni(3)); - test_throw_out_of_range(x, index_min(20), index_uni(3)); + EXPECT_EQ(0, rvalue(x, "", index_min(20), index_uni(3)).size()); test_throw_out_of_range(x, index_min(2), index_uni(0)); test_throw_out_of_range(x, index_min(2), index_uni(30)); } @@ -784,7 +784,7 @@ TEST_F(RvalueRev, min_mat) { EXPECT_MATRIX_EQ(x.adj(), x_exp_adj); EXPECT_MATRIX_EQ(y.adj(), y_exp_adj); test_throw_out_of_range(x, index_min(0)); - test_throw_out_of_range(x, index_min(12)); + EXPECT_EQ(0, rvalue(x, "", index_min(12)).size()); } TEST_F(RvalueRev, uni_min_mat) { @@ -807,7 +807,7 @@ TEST_F(RvalueRev, uni_min_mat) { test_throw_out_of_range(x, index_uni(0), index_min(2)); test_throw_out_of_range(x, index_uni(12), index_min(2)); test_throw_out_of_range(x, index_uni(1), index_min(0)); - test_throw_out_of_range(x, index_uni(1), index_min(12)); + EXPECT_EQ(0, rvalue(x, "", index_uni(1), index_min(12)).size()); } TEST_F(RvalueRev, min_min_mat) { @@ -830,9 +830,9 @@ TEST_F(RvalueRev, min_min_mat) { EXPECT_MATRIX_EQ(x.adj(), x_exp_adj); EXPECT_MATRIX_EQ(y.adj(), y_exp_adj); test_throw_out_of_range(x, index_min(0), index_min(3)); - test_throw_out_of_range(x, index_min(12), index_min(3)); + EXPECT_EQ(0, rvalue(x, "", index_min(12), index_min(3)).size()); test_throw_out_of_range(x, index_min(2), index_min(0)); - test_throw_out_of_range(x, index_min(2), index_min(12)); + EXPECT_EQ(0, rvalue(x, "", index_min(2), index_min(12)).size()); } TEST_F(RvalueRev, minmax_min_matrix) { @@ -853,7 +853,7 @@ TEST_F(RvalueRev, minmax_min_matrix) { test_throw_out_of_range(x, index_min_max(0, 3), index_min(2)); test_throw_out_of_range(x, index_min_max(2, 7), index_min(2)); test_throw_out_of_range(x, index_min_max(2, 3), index_min(0)); - test_throw_out_of_range(x, index_min_max(2, 3), index_min(7)); + EXPECT_EQ(0, rvalue(x, "", index_min_max(2, 3), index_min(7)).size()); } // max @@ -895,7 +895,7 @@ TEST_F(RvalueRev, min_max_matrix) { EXPECT_MATRIX_EQ(x.adj(), x_exp_adj); EXPECT_MATRIX_EQ(y.adj(), y_exp_adj); test_throw_out_of_range(x, index_min(0), index_max(2)); - test_throw_out_of_range(x, index_min(12), index_max(3)); + EXPECT_EQ(0, rvalue(x, "", index_min(12), index_max(3)).size()); test_throw_out_of_range(x, index_min(2), index_max(12)); EXPECT_NO_THROW(rvalue(x, "", index_min(2), index_max(0))); }