From 767bdc556f57984a65fe6f0a67f072eece990f32 Mon Sep 17 00:00:00 2001 From: Florence Bockting Date: Fri, 17 Jul 2026 16:37:54 +0300 Subject: [PATCH 01/14] feat: add map-like helpers map, mapN, row_map, row_mapN, col_map, col_mapN --- stan/math/prim/functor.hpp | 1 + stan/math/prim/functor/map.hpp | 366 +++++++++++++++++++++++++++++++++ 2 files changed, 367 insertions(+) create mode 100644 stan/math/prim/functor/map.hpp diff --git a/stan/math/prim/functor.hpp b/stan/math/prim/functor.hpp index 2f9de59c706..22a3934f99d 100644 --- a/stan/math/prim/functor.hpp +++ b/stan/math/prim/functor.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp new file mode 100644 index 00000000000..8130e5508c1 --- /dev/null +++ b/stan/math/prim/functor/map.hpp @@ -0,0 +1,366 @@ +#ifndef STAN_MATH_PRIM_FUNCTOR_MAP_HPP +#define STAN_MATH_PRIM_FUNCTOR_MAP_HPP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace stan { +namespace math { + +namespace internal { + +template +inline void check_all_matching_sizes(const char* function, const T& x, + const Types&... xs) { + std::size_t arg_idx = 2; + ( + [&](const auto& y) { + if (x.size() != y.size()) { + [&]() STAN_COLD_PATH { + const std::string name = "x" + std::to_string(arg_idx); + check_matching_sizes(function, "x1", x, name.c_str(), y); + }(); + } + ++arg_idx; + }(xs), + ...); +} + +template +inline void check_all_matching_dims(const char* function, const T& x, + const Types&... xs) { + std::size_t arg_idx = 2; + ( + [&](const auto& y) { + if (x.rows() != y.rows() || x.cols() != y.cols()) { + [&]() STAN_COLD_PATH { + const std::string name = "x" + std::to_string(arg_idx); + check_matching_dims(function, "x1", x, name.c_str(), y); + }(); + } + ++arg_idx; + }(xs), + ...); +} + +template +inline void assign_matrix_row( + const char* function, + Eigen::Matrix& result, + Eigen::Index i, T&& x) { + if (i == 0) { + result.resize(result.rows(), x.cols()); + } else { + check_size_match(function, "columns of result", result.cols(), + "columns of returned row", x.cols()); + } + result.row(i) = std::forward(x); +} + +template +inline void assign_matrix_col( + const char* function, + Eigen::Matrix& result, + Eigen::Index j, T&& x) { + if (j == 0) { + result.resize(x.rows(), result.cols()); + } else { + check_size_match(function, "rows of result", result.rows(), + "rows of returned column", x.rows()); + } + result.col(j) = std::forward(x); +} + +} // namespace internal + +/** + * Apply a functor to each element of a std::vector and collect the results. + * + * For a vector `x` of length `n`, returns a std::vector whose `i`-th element + * is `f(x[i], args...)`. Additional arguments are shared across calls, which + * allows passing parameters without closures. If `x` is empty, returns an + * empty std::vector. + * + * The element type of the returned std::vector is deduced from the return type + * of `f` to allow for cases where the input and return scalar types differ + * (e.g., functions implicitly promoting integers). + * + * @tparam F Type of functor to apply. + * @tparam T Type of input std::vector. + * @tparam Args Types of shared arguments passed to each call. + * @param f functor to apply to each element. + * @param x std::vector input to which operation is applied. + * @param args shared arguments passed to each call. + * @return std::vector with result of applying functor to each element of `x`. + */ +template * = nullptr> +inline auto map(F&& f, T&& x, Args&&... args) { + using T_return = std::decay_t; + std::vector result; + result.reserve(x.size()); + + for (auto&& xi : x) { + result.push_back(f(xi, args...)); + } + + return result; +} + +/** + * Apply a functor elementwise to N std::vector inputs of the same size. + * + * For vectors `args...` of length `n`, returns a std::vector whose `i`-th + * element is `f(args[0][i], args[1][i], ...)`. If the inputs are empty, + * returns an empty std::vector. + * + * Unlike `map`, this overload only zips containers and does not accept + * additional shared trailing arguments. Pass shared state through the + * functor (e.g. a capturing lambda) when needed. + * + * The element type of the returned std::vector is deduced from the return type + * of `f` to allow for cases where the input and return scalar types differ + * (e.g., functions implicitly promoting integers). + * + * @tparam F Type of functor to apply. + * @tparam Types Types of input std::vector containers. + * @param f functor to apply to each tuple of elements. + * @param args std::vector inputs to which operation is applied. + * @return std::vector with result of applying functor to each tuple of + * elements. + * @throw std::invalid_argument if the inputs have different sizes. + */ +template * = nullptr> +inline auto mapN(F&& f, Types&&... args) { + static_assert(sizeof...(Types) >= 2, + "mapN requires at least two std::vector inputs."); + static constexpr const char* function = "mapN"; + internal::check_all_matching_sizes(function, args...); + + const std::size_t n = std::get<0>(std::forward_as_tuple(args...)).size(); + using T_return = std::decay_t; + std::vector result; + result.reserve(n); + + for (std::size_t i = 0; i < n; ++i) { + result.push_back(f((args[i])...)); + } + + return result; +} + +/** + * Apply a functor to each row of an Eigen matrix and collect the results. + * + * For a matrix `m` with `n` rows, returns a matrix whose `i`-th row is + * `f(m.row(i), args...)`. Additional arguments are shared across calls. If + * `m` has zero rows, returns a 0x0 matrix (output column count is unknown + * when the functor is never called). + * + * Expensive Eigen expressions passed as `m` are evaluated once via `to_ref` + * before the row loop, so compound expressions (e.g. `A * B`) are not + * re-evaluated for every row. + * + * The functor must return a type assignable to a matrix row. All returned + * rows must have the same number of columns. + * + * @tparam F Type of functor to apply. + * @tparam T Eigen matrix type. + * @tparam Args Types of shared arguments passed to each call. + * @param f functor to apply to each row. + * @param m Eigen matrix input to which operation is applied. + * @param args shared arguments passed to each call. + * @return Eigen matrix with result of applying functor to each row of `m`. + * @throw std::invalid_argument if returned rows have inconsistent sizes. + */ +template * = nullptr> +inline auto row_map(F&& f, T&& m, Args&&... args) { + static constexpr const char* function = "row_map"; + decltype(auto) m_ref = to_ref(std::forward(m)); + using T_return + = scalar_type_t>; + using matrix_t = Eigen::Matrix; + + const Eigen::Index n_rows = m_ref.rows(); + if (n_rows == 0) { + return matrix_t(0, 0); + } + + matrix_t result(n_rows, 0); + for (Eigen::Index i = 0; i < n_rows; ++i) { + internal::assign_matrix_row(function, result, i, f(m_ref.row(i), args...)); + } + return result; +} + +/** + * Apply a functor to each column of an Eigen matrix and collect the results. + * + * For a matrix `m` with `k` columns, returns a matrix whose `j`-th column is + * `f(m.col(j), args...)`. Additional arguments are shared across calls. If + * `m` has zero columns, returns a 0x0 matrix (output row count is unknown + * when the functor is never called). + * + * Expensive Eigen expressions passed as `m` are evaluated once via `to_ref` + * before the column loop, so compound expressions (e.g. `A * B`) are not + * re-evaluated for every column. + * + * The functor must return a type assignable to a matrix column. All returned + * columns must have the same number of rows. + * + * @tparam F Type of functor to apply. + * @tparam T Eigen matrix type. + * @tparam Args Types of shared arguments passed to each call. + * @param f functor to apply to each column. + * @param m Eigen matrix input to which operation is applied. + * @param args shared arguments passed to each call. + * @return Eigen matrix with result of applying functor to each column of `m`. + * @throw std::invalid_argument if returned columns have inconsistent sizes. + */ +template * = nullptr> +inline auto col_map(F&& f, T&& m, Args&&... args) { + static constexpr const char* function = "col_map"; + decltype(auto) m_ref = to_ref(std::forward(m)); + using T_return + = scalar_type_t>; + using matrix_t = Eigen::Matrix; + + const Eigen::Index n_cols = m_ref.cols(); + if (n_cols == 0) { + return matrix_t(0, 0); + } + + matrix_t result(0, n_cols); + for (Eigen::Index j = 0; j < n_cols; ++j) { + internal::assign_matrix_col(function, result, j, f(m_ref.col(j), args...)); + } + return result; +} + +/** + * Apply a functor rowwise to N Eigen matrices with identical dimensions. + * + * For matrices `args...` with `n` rows, returns a matrix whose `i`-th row is + * `f(args[0].row(i), args[1].row(i), ...)`. If the inputs have zero rows, + * returns a 0x0 matrix. + * + * Unlike `row_map`, this overload only zips matrices and does not accept + * additional shared trailing arguments. + * + * Expensive Eigen expressions in `args...` are each evaluated once via + * `to_ref` before the row loop. + * + * The functor must return a type assignable to a matrix row. All returned + * rows must have the same number of columns. + * + * @tparam F Type of functor to apply. + * @tparam Types Eigen matrix types. + * @param f functor to apply to each tuple of rows. + * @param args Eigen matrix inputs to which operation is applied. + * @return Eigen matrix with result of applying functor to each tuple of rows. + * @throw std::invalid_argument if the inputs have different dimensions or + * returned rows have inconsistent sizes. + */ +template * = nullptr> +inline auto row_mapN(F&& f, Types&&... args) { + static_assert(sizeof...(Types) >= 2, + "row_mapN requires at least two Eigen matrix inputs."); + static constexpr const char* function = "row_mapN"; + internal::check_all_matching_dims(function, args...); + + std::tuple...> m_refs{ + to_ref(std::forward(args))...}; + const Eigen::Index n_rows = std::get<0>(m_refs).rows(); + using T_return = scalar_type_t>().row(0))...))>>; + using matrix_t = Eigen::Matrix; + + if (n_rows == 0) { + return matrix_t(0, 0); + } + + matrix_t result(n_rows, 0); + apply( + [&](auto&&... ms) { + for (Eigen::Index i = 0; i < n_rows; ++i) { + internal::assign_matrix_row(function, result, i, f((ms.row(i))...)); + } + }, + m_refs); + return result; +} + +/** + * Apply a functor columnwise to N Eigen matrices with identical dimensions. + * + * For matrices `args...` with `k` columns, returns a matrix whose `j`-th + * column is `f(args[0].col(j), args[1].col(j), ...)`. If the inputs have + * zero columns, returns a 0x0 matrix. + * + * Unlike `col_map`, this overload only zips matrices and does not accept + * additional shared trailing arguments. + * + * Expensive Eigen expressions in `args...` are each evaluated once via + * `to_ref` before the column loop. + * + * The functor must return a type assignable to a matrix column. All returned + * columns must have the same number of rows. + * + * @tparam F Type of functor to apply. + * @tparam Types Eigen matrix types. + * @param f functor to apply to each tuple of columns. + * @param args Eigen matrix inputs to which operation is applied. + * @return Eigen matrix with result of applying functor to each tuple of + * columns. + * @throw std::invalid_argument if the inputs have different dimensions or + * returned columns have inconsistent sizes. + */ +template * = nullptr> +inline auto col_mapN(F&& f, Types&&... args) { + static_assert(sizeof...(Types) >= 2, + "col_mapN requires at least two Eigen matrix inputs."); + static constexpr const char* function = "col_mapN"; + internal::check_all_matching_dims(function, args...); + + std::tuple...> m_refs{ + to_ref(std::forward(args))...}; + const Eigen::Index n_cols = std::get<0>(m_refs).cols(); + using T_return = scalar_type_t>().col(0))...))>>; + using matrix_t = Eigen::Matrix; + + if (n_cols == 0) { + return matrix_t(0, 0); + } + + matrix_t result(0, n_cols); + apply( + [&](auto&&... ms) { + for (Eigen::Index j = 0; j < n_cols; ++j) { + internal::assign_matrix_col(function, result, j, f((ms.col(j))...)); + } + }, + m_refs); + return result; +} + +} // namespace math +} // namespace stan + +#endif From 518aaf5e7420da20f9fdba93342d0381ba42f01e Mon Sep 17 00:00:00 2001 From: Florence Bockting Date: Fri, 17 Jul 2026 16:38:45 +0300 Subject: [PATCH 02/14] tests: add unit tests for map-like helpers --- test/unit/math/prim/functor/map_test.cpp | 389 +++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 test/unit/math/prim/functor/map_test.cpp diff --git a/test/unit/math/prim/functor/map_test.cpp b/test/unit/math/prim/functor/map_test.cpp new file mode 100644 index 00000000000..e11d86148e0 --- /dev/null +++ b/test/unit/math/prim/functor/map_test.cpp @@ -0,0 +1,389 @@ +#include +#include +#include +#include +#include +#include + +TEST(MathFunctor, map_square) { + std::vector x{1.0, 2.0, 3.0}; + auto y = stan::math::map([](double v) { return v * v; }, x); + ASSERT_EQ(y.size(), 3u); + EXPECT_FLOAT_EQ(y[0], 1.0); + EXPECT_FLOAT_EQ(y[1], 4.0); + EXPECT_FLOAT_EQ(y[2], 9.0); +} + +TEST(MathFunctor, map_type_change) { + std::vector x{1, 2, 3}; + auto y = stan::math::map([](int v) { return v + 0.5; }, x); + EXPECT_FLOAT_EQ(y[0], 1.5); + EXPECT_FLOAT_EQ(y[1], 2.5); + EXPECT_FLOAT_EQ(y[2], 3.5); +} + +TEST(MathFunctor, map_empty) { + std::vector x; + auto y = stan::math::map([](double v) { return v; }, x); + EXPECT_EQ(y.size(), 0u); +} + +TEST(MathFunctor, map_const_vector) { + const std::vector x{1.0, 2.0, 3.0}; + auto y = stan::math::map([](double v) { return 2.0 * v; }, x); + ASSERT_EQ(y.size(), 3u); + EXPECT_FLOAT_EQ(y[0], 2.0); + EXPECT_FLOAT_EQ(y[1], 4.0); + EXPECT_FLOAT_EQ(y[2], 6.0); +} + +TEST(MathFunctor, map_variadic_args) { + std::vector x{1.0, 2.0, 3.0}; + double offset = 10.0; + double scale = 2.0; + auto y = stan::math::map( + [](double v, double mu, double sigma) { return mu + sigma * v; }, x, + offset, scale); + ASSERT_EQ(y.size(), 3u); + EXPECT_FLOAT_EQ(y[0], 12.0); + EXPECT_FLOAT_EQ(y[1], 14.0); + EXPECT_FLOAT_EQ(y[2], 16.0); +} + +TEST(MathFunctor, map_shared_arg_reused) { + std::vector x{1.0, 2.0, 3.0}; + auto offset = std::make_unique(10.0); + auto y = stan::math::map( + [](double v, const std::unique_ptr& mu) { return v + *mu; }, x, + std::move(offset)); + ASSERT_EQ(y.size(), 3u); + EXPECT_FLOAT_EQ(y[0], 11.0); + EXPECT_FLOAT_EQ(y[1], 12.0); + EXPECT_FLOAT_EQ(y[2], 13.0); +} + +TEST(MathFunctor, mapN_add) { + std::vector a{1.0, 2.0}; + std::vector b{10.0, 20.0}; + auto y = stan::math::mapN([](double u, double v) { return u + v; }, a, b); + ASSERT_EQ(y.size(), 2u); + EXPECT_FLOAT_EQ(y[0], 11.0); + EXPECT_FLOAT_EQ(y[1], 22.0); +} + +TEST(MathFunctor, mapN_mixed_scalar_types) { + std::vector a{1, 2, 3}; + std::vector b{0.5, 1.5, 2.5}; + auto y = stan::math::mapN([](int u, double v) { return u + v; }, a, b); + ASSERT_EQ(y.size(), 3u); + EXPECT_FLOAT_EQ(y[0], 1.5); + EXPECT_FLOAT_EQ(y[1], 3.5); + EXPECT_FLOAT_EQ(y[2], 5.5); +} + +TEST(MathFunctor, mapN_three_vectors) { + std::vector a{1.0, 2.0}; + std::vector b{10.0, 20.0}; + std::vector c{100.0, 200.0}; + auto y = stan::math::mapN( + [](double u, double v, double w) { return u + v + w; }, a, b, c); + ASSERT_EQ(y.size(), 2u); + EXPECT_FLOAT_EQ(y[0], 111.0); + EXPECT_FLOAT_EQ(y[1], 222.0); +} + +TEST(MathFunctor, mapN_empty) { + std::vector a; + std::vector b; + auto y = stan::math::mapN([](double u, double v) { return u + v; }, a, b); + EXPECT_EQ(y.size(), 0u); +} + +TEST(MathFunctor, mapN_size_mismatch) { + std::vector a{1.0, 2.0}; + std::vector b{10.0}; + EXPECT_THROW(stan::math::mapN([](double u, double v) { return u + v; }, a, b), + std::invalid_argument); +} + +TEST(MathFunctor, row_map_standardize) { + Eigen::MatrixXd x(2, 3); + x << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0; + Eigen::RowVectorXd mu(3); + mu << 1.0, 2.0, 3.0; + Eigen::RowVectorXd sigma(3); + sigma << 1.0, 2.0, 3.0; + + auto y = stan::math::row_map( + [](const Eigen::RowVectorXd& row, const Eigen::RowVectorXd& m, + const Eigen::RowVectorXd& s) { + return (row.array() - m.array()) / s.array(); + }, + x, mu, sigma); + + Eigen::MatrixXd expected(2, 3); + expected << 0.0, 0.0, 0.0, 3.0, 1.5, 1.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, row_map_empty) { + Eigen::MatrixXd x(0, 3); + auto y = stan::math::row_map( + [](const Eigen::RowVectorXd& row) { return row; }, x); + EXPECT_EQ(y.rows(), 0); + EXPECT_EQ(y.cols(), 0); +} + +TEST(MathFunctor, row_map_inconsistent_row_size) { + Eigen::MatrixXd x(2, 2); + x << 1.0, 2.0, 3.0, 4.0; + int call = 0; + EXPECT_THROW(stan::math::row_map( + [&call](const Eigen::RowVectorXd&) { + ++call; + if (call == 1) { + return Eigen::RowVectorXd::Ones(2); + } + return Eigen::RowVectorXd::Ones(3); + }, + x), + std::invalid_argument); +} + +TEST(MathFunctor, row_map_shared_arg_reused) { + Eigen::MatrixXd x(2, 2); + x << 1.0, 2.0, 3.0, 4.0; + auto offset = std::make_unique(10.0); + auto y = stan::math::row_map( + [](const Eigen::RowVectorXd& row, const std::unique_ptr& mu) { + return row.array() + *mu; + }, + x, std::move(offset)); + + Eigen::MatrixXd expected(2, 2); + expected << 11.0, 12.0, 13.0, 14.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, col_map_sum_rows) { + Eigen::MatrixXd x(2, 2); + x << 1.0, 2.0, 3.0, 4.0; + + auto y = stan::math::col_map( + [](const Eigen::VectorXd& col) { + return Eigen::VectorXd::Constant(1, col.sum()); + }, + x); + + Eigen::MatrixXd expected(1, 2); + expected << 4.0, 6.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, col_map_empty) { + Eigen::MatrixXd x(3, 0); + auto y + = stan::math::col_map([](const Eigen::VectorXd& col) { return col; }, x); + EXPECT_EQ(y.rows(), 0); + EXPECT_EQ(y.cols(), 0); +} + +TEST(MathFunctor, col_map_inconsistent_col_size) { + Eigen::MatrixXd x(2, 2); + x << 1.0, 2.0, 3.0, 4.0; + int call = 0; + EXPECT_THROW(stan::math::col_map( + [&call](const Eigen::VectorXd&) { + ++call; + if (call == 1) { + return Eigen::VectorXd::Ones(2); + } + return Eigen::VectorXd::Ones(3); + }, + x), + std::invalid_argument); +} + +TEST(MathFunctor, col_map_shared_arg_reused) { + Eigen::MatrixXd x(2, 2); + x << 1.0, 2.0, 3.0, 4.0; + auto scale = std::make_unique(2.0); + auto y = stan::math::col_map( + [](const Eigen::VectorXd& col, const std::unique_ptr& s) { + return (*s) * col; + }, + x, std::move(scale)); + + Eigen::MatrixXd expected(2, 2); + expected << 2.0, 4.0, 6.0, 8.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, row_mapN_add) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd b(2, 2); + b << 10.0, 20.0, 30.0, 40.0; + + auto y + = stan::math::row_mapN([](const Eigen::RowVectorXd& u, + const Eigen::RowVectorXd& v) { return u + v; }, + a, b); + + Eigen::MatrixXd expected(2, 2); + expected << 11.0, 22.0, 33.0, 44.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, row_mapN_dim_mismatch) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd b(2, 3); + b << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0; + + EXPECT_THROW( + stan::math::row_mapN([](const Eigen::RowVectorXd& u, + const Eigen::RowVectorXd& v) { return u + v; }, + a, b), + std::invalid_argument); +} + +TEST(MathFunctor, row_mapN_empty) { + Eigen::MatrixXd a(0, 2); + Eigen::MatrixXd b(0, 2); + auto y + = stan::math::row_mapN([](const Eigen::RowVectorXd& u, + const Eigen::RowVectorXd& v) { return u + v; }, + a, b); + EXPECT_EQ(y.rows(), 0); + EXPECT_EQ(y.cols(), 0); +} + +TEST(MathFunctor, row_mapN_mixed_scalar_types) { + Eigen::MatrixXi a(2, 2); + a << 1, 2, 3, 4; + Eigen::MatrixXd b(2, 2); + b << 0.5, 1.5, 2.5, 3.5; + auto y = stan::math::row_mapN( + [](const auto& u, const auto& v) { + return u.template cast() + v; + }, + a, b); + + Eigen::MatrixXd expected(2, 2); + expected << 1.5, 3.5, 5.5, 7.5; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, row_mapN_inconsistent_row_size) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd b(2, 2); + b << 10.0, 20.0, 30.0, 40.0; + int call = 0; + EXPECT_THROW( + stan::math::row_mapN( + [&call](const Eigen::RowVectorXd&, const Eigen::RowVectorXd&) { + ++call; + if (call == 1) { + return Eigen::RowVectorXd::Ones(2); + } + return Eigen::RowVectorXd::Ones(3); + }, + a, b), + std::invalid_argument); +} + +TEST(MathFunctor, col_mapN_add) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd b(2, 2); + b << 10.0, 20.0, 30.0, 40.0; + + auto y = stan::math::col_mapN( + [](const Eigen::VectorXd& u, const Eigen::VectorXd& v) { return u + v; }, + a, b); + + Eigen::MatrixXd expected(2, 2); + expected << 11.0, 22.0, 33.0, 44.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, col_mapN_dim_mismatch) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd b(3, 2); + b << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0; + + EXPECT_THROW( + stan::math::col_mapN([](const Eigen::VectorXd& u, + const Eigen::VectorXd& v) { return u + v; }, + a, b), + std::invalid_argument); +} + +TEST(MathFunctor, col_mapN_empty) { + Eigen::MatrixXd a(2, 0); + Eigen::MatrixXd b(2, 0); + auto y = stan::math::col_mapN( + [](const Eigen::VectorXd& u, const Eigen::VectorXd& v) { return u + v; }, + a, b); + EXPECT_EQ(y.rows(), 0); + EXPECT_EQ(y.cols(), 0); +} + +TEST(MathFunctor, col_mapN_mixed_scalar_types) { + Eigen::MatrixXi a(2, 2); + a << 1, 2, 3, 4; + Eigen::MatrixXd b(2, 2); + b << 0.5, 1.5, 2.5, 3.5; + auto y = stan::math::col_mapN( + [](const auto& u, const auto& v) { + return u.template cast() + v; + }, + a, b); + + Eigen::MatrixXd expected(2, 2); + expected << 1.5, 3.5, 5.5, 7.5; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, col_mapN_inconsistent_col_size) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd b(2, 2); + b << 10.0, 20.0, 30.0, 40.0; + int call = 0; + EXPECT_THROW(stan::math::col_mapN( + [&call](const Eigen::VectorXd&, const Eigen::VectorXd&) { + ++call; + if (call == 1) { + return Eigen::VectorXd::Ones(2); + } + return Eigen::VectorXd::Ones(3); + }, + a, b), + std::invalid_argument); +} + +TEST(MathFunctor, row_map_block_expression) { + Eigen::MatrixXd x(3, 3); + x << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0; + auto y = stan::math::row_map([](const auto& row) { return 2.0 * row; }, + x.block(0, 0, 2, 3)); + + Eigen::MatrixXd expected(2, 3); + expected << 2.0, 4.0, 6.0, 8.0, 10.0, 12.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, col_map_block_expression) { + Eigen::MatrixXd x(3, 3); + x << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0; + auto y = stan::math::col_map([](const auto& col) { return 2.0 * col; }, + x.block(0, 0, 3, 2)); + + Eigen::MatrixXd expected(3, 2); + expected << 2.0, 4.0, 8.0, 10.0, 14.0, 16.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} From 4fed4360b9a6c91c1cbbca7f420176278a574dc5 Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Fri, 17 Jul 2026 10:01:22 -0400 Subject: [PATCH 03/14] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- stan/math/prim/functor/map.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index 8130e5508c1..ec0190ef60d 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -286,8 +286,8 @@ inline auto row_mapN(F&& f, Types&&... args) { std::tuple...> m_refs{ to_ref(std::forward(args))...}; const Eigen::Index n_rows = std::get<0>(m_refs).rows(); - using T_return = scalar_type_t>().row(0))...))>>; + using T_return = scalar_type_t>().row(0))...))>>; using matrix_t = Eigen::Matrix; if (n_rows == 0) { @@ -341,8 +341,8 @@ inline auto col_mapN(F&& f, Types&&... args) { std::tuple...> m_refs{ to_ref(std::forward(args))...}; const Eigen::Index n_cols = std::get<0>(m_refs).cols(); - using T_return = scalar_type_t>().col(0))...))>>; + using T_return = scalar_type_t>().col(0))...))>>; using matrix_t = Eigen::Matrix; if (n_cols == 0) { From 8459b1c2fa7688c04ef32b3e5e1bba7632d895b4 Mon Sep 17 00:00:00 2001 From: Florence Bockting Date: Sun, 19 Jul 2026 22:00:11 +0300 Subject: [PATCH 04/14] refactor: minor refactoring in assign_matrix_row/col --- stan/math/prim/functor/map.hpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index ec0190ef60d..15728d4a3bf 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -60,7 +61,8 @@ inline void assign_matrix_row( Eigen::Matrix& result, Eigen::Index i, T&& x) { if (i == 0) { - result.resize(result.rows(), x.cols()); + result = Eigen::Matrix( + result.rows(), x.cols()); } else { check_size_match(function, "columns of result", result.cols(), "columns of returned row", x.cols()); @@ -74,7 +76,8 @@ inline void assign_matrix_col( Eigen::Matrix& result, Eigen::Index j, T&& x) { if (j == 0) { - result.resize(x.rows(), result.cols()); + result = Eigen::Matrix( + x.rows(), result.cols()); } else { check_size_match(function, "rows of result", result.rows(), "rows of returned column", x.rows()); @@ -286,8 +289,8 @@ inline auto row_mapN(F&& f, Types&&... args) { std::tuple...> m_refs{ to_ref(std::forward(args))...}; const Eigen::Index n_rows = std::get<0>(m_refs).rows(); - using T_return = scalar_type_t>().row(0))...))>>; + using T_return = scalar_type_t>().row(0))...))>>; using matrix_t = Eigen::Matrix; if (n_rows == 0) { @@ -341,8 +344,8 @@ inline auto col_mapN(F&& f, Types&&... args) { std::tuple...> m_refs{ to_ref(std::forward(args))...}; const Eigen::Index n_cols = std::get<0>(m_refs).cols(); - using T_return = scalar_type_t>().col(0))...))>>; + using T_return = scalar_type_t>().col(0))...))>>; using matrix_t = Eigen::Matrix; if (n_cols == 0) { From ca611e9d075f8a1982fd6dfa67269496e7886163 Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Sun, 19 Jul 2026 15:01:12 -0400 Subject: [PATCH 05/14] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- stan/math/prim/functor/map.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index 15728d4a3bf..ba87d604402 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -289,8 +289,8 @@ inline auto row_mapN(F&& f, Types&&... args) { std::tuple...> m_refs{ to_ref(std::forward(args))...}; const Eigen::Index n_rows = std::get<0>(m_refs).rows(); - using T_return = scalar_type_t>().row(0))...))>>; + using T_return = scalar_type_t>().row(0))...))>>; using matrix_t = Eigen::Matrix; if (n_rows == 0) { @@ -344,8 +344,8 @@ inline auto col_mapN(F&& f, Types&&... args) { std::tuple...> m_refs{ to_ref(std::forward(args))...}; const Eigen::Index n_cols = std::get<0>(m_refs).cols(); - using T_return = scalar_type_t>().col(0))...))>>; + using T_return = scalar_type_t>().col(0))...))>>; using matrix_t = Eigen::Matrix; if (n_cols == 0) { From ef708603a679492c0cc9ebfc674f5fa3e80fd6ea Mon Sep 17 00:00:00 2001 From: Florence Bockting Date: Mon, 20 Jul 2026 07:46:41 +0300 Subject: [PATCH 06/14] ci: retrigger Jenkins after auto-formatting From 5d27bf1d5057644964bbc9d3253756aea0670016 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Wed, 22 Jul 2026 15:50:17 -0400 Subject: [PATCH 07/14] remove assign_(row/col) and generalize the check_consistent_size(...) --- stan/math/prim/err/check_consistent_sizes.hpp | 30 ++++ stan/math/prim/functor/map.hpp | 153 ++++++++---------- .../prim/err/check_consistent_sizes_test.cpp | 32 ++++ test/unit/math/prim/functor/map_test.cpp | 58 +++++++ 4 files changed, 190 insertions(+), 83 deletions(-) create mode 100644 test/unit/math/prim/err/check_consistent_sizes_test.cpp diff --git a/stan/math/prim/err/check_consistent_sizes.hpp b/stan/math/prim/err/check_consistent_sizes.hpp index ec1cee072e6..d01636e33b0 100644 --- a/stan/math/prim/err/check_consistent_sizes.hpp +++ b/stan/math/prim/err/check_consistent_sizes.hpp @@ -1,8 +1,10 @@ #ifndef STAN_MATH_PRIM_ERR_CHECK_CONSISTENT_SIZES_HPP #define STAN_MATH_PRIM_ERR_CHECK_CONSISTENT_SIZES_HPP +#include #include #include +#include #include #include #include @@ -66,6 +68,34 @@ inline void check_consistent_sizes(const char* function, const char* name1, } } +/** + * Check that the unnamed container inputs have the same size. Inputs are + * labeled `arg1`, `arg2`, and so on in error messages. + * + * @tparam T type of the first input + * @tparam Types types of the remaining inputs + * @param function function name (for error messages) + * @param x first input + * @param xs remaining inputs + * @throw `invalid_argument` if sizes are inconsistent + */ +template * = nullptr> +inline void check_consistent_sizes(const char* function, T&& x, Types&&... xs) { + std::size_t arg_idx = 2; + ( + [&](const auto& y) { + if (x.size() != y.size()) { + [&]() STAN_COLD_PATH { + const std::string name = "arg" + std::to_string(arg_idx); + check_matching_sizes(function, "arg1", x, name.c_str(), y); + }(); + } + ++arg_idx; + }(xs), + ...); +} + } // namespace math } // namespace stan #endif diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index ba87d604402..f4bd3338ed7 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -4,8 +4,8 @@ #include #include #include +#include #include -#include #include #include @@ -21,23 +21,6 @@ namespace math { namespace internal { -template -inline void check_all_matching_sizes(const char* function, const T& x, - const Types&... xs) { - std::size_t arg_idx = 2; - ( - [&](const auto& y) { - if (x.size() != y.size()) { - [&]() STAN_COLD_PATH { - const std::string name = "x" + std::to_string(arg_idx); - check_matching_sizes(function, "x1", x, name.c_str(), y); - }(); - } - ++arg_idx; - }(xs), - ...); -} - template inline void check_all_matching_dims(const char* function, const T& x, const Types&... xs) { @@ -55,36 +38,6 @@ inline void check_all_matching_dims(const char* function, const T& x, ...); } -template -inline void assign_matrix_row( - const char* function, - Eigen::Matrix& result, - Eigen::Index i, T&& x) { - if (i == 0) { - result = Eigen::Matrix( - result.rows(), x.cols()); - } else { - check_size_match(function, "columns of result", result.cols(), - "columns of returned row", x.cols()); - } - result.row(i) = std::forward(x); -} - -template -inline void assign_matrix_col( - const char* function, - Eigen::Matrix& result, - Eigen::Index j, T&& x) { - if (j == 0) { - result = Eigen::Matrix( - x.rows(), result.cols()); - } else { - check_size_match(function, "rows of result", result.rows(), - "rows of returned column", x.rows()); - } - result.col(j) = std::forward(x); -} - } // namespace internal /** @@ -150,7 +103,7 @@ inline auto mapN(F&& f, Types&&... args) { static_assert(sizeof...(Types) >= 2, "mapN requires at least two std::vector inputs."); static constexpr const char* function = "mapN"; - internal::check_all_matching_sizes(function, args...); + check_consistent_sizes(function, args...); const std::size_t n = std::get<0>(std::forward_as_tuple(args...)).size(); using T_return = std::decay_t; @@ -170,7 +123,8 @@ inline auto mapN(F&& f, Types&&... args) { * For a matrix `m` with `n` rows, returns a matrix whose `i`-th row is * `f(m.row(i), args...)`. Additional arguments are shared across calls. If * `m` has zero rows, returns a 0x0 matrix (output column count is unknown - * when the functor is never called). + * when the functor is never called). If the first returned row is empty, + * returns a 0x0 matrix without calling the functor on the remaining rows. * * Expensive Eigen expressions passed as `m` are evaluated once via `to_ref` * before the row loop, so compound expressions (e.g. `A * B`) are not @@ -193,8 +147,8 @@ template (m)); - using T_return - = scalar_type_t>; + using result_row_t = plain_type_t; + using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; const Eigen::Index n_rows = m_ref.rows(); @@ -202,9 +156,17 @@ inline auto row_map(F&& f, T&& m, Args&&... args) { return matrix_t(0, 0); } - matrix_t result(n_rows, 0); - for (Eigen::Index i = 0; i < n_rows; ++i) { - internal::assign_matrix_row(function, result, i, f(m_ref.row(i), args...)); + result_row_t first_row = f(m_ref.row(0), args...); + if (first_row.size() == 0) { + return matrix_t(0, 0); + } + matrix_t result(n_rows, first_row.cols()); + result.row(0) = first_row; + for (Eigen::Index i = 1; i < n_rows; ++i) { + result_row_t row = f(m_ref.row(i), args...); + check_size_match(function, "columns of result", result.cols(), + "columns of returned row", row.cols()); + result.row(i) = row; } return result; } @@ -215,7 +177,8 @@ inline auto row_map(F&& f, T&& m, Args&&... args) { * For a matrix `m` with `k` columns, returns a matrix whose `j`-th column is * `f(m.col(j), args...)`. Additional arguments are shared across calls. If * `m` has zero columns, returns a 0x0 matrix (output row count is unknown - * when the functor is never called). + * when the functor is never called). If the first returned column is empty, + * returns a 0x0 matrix without calling the functor on the remaining columns. * * Expensive Eigen expressions passed as `m` are evaluated once via `to_ref` * before the column loop, so compound expressions (e.g. `A * B`) are not @@ -238,8 +201,8 @@ template (m)); - using T_return - = scalar_type_t>; + using result_col_t = plain_type_t; + using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; const Eigen::Index n_cols = m_ref.cols(); @@ -247,9 +210,17 @@ inline auto col_map(F&& f, T&& m, Args&&... args) { return matrix_t(0, 0); } - matrix_t result(0, n_cols); - for (Eigen::Index j = 0; j < n_cols; ++j) { - internal::assign_matrix_col(function, result, j, f(m_ref.col(j), args...)); + result_col_t first_col = f(m_ref.col(0), args...); + if (first_col.size() == 0) { + return matrix_t(0, 0); + } + matrix_t result(first_col.rows(), n_cols); + result.col(0) = first_col; + for (Eigen::Index j = 1; j < n_cols; ++j) { + result_col_t col = f(m_ref.col(j), args...); + check_size_match(function, "rows of result", result.rows(), + "rows of returned column", col.rows()); + result.col(j) = col; } return result; } @@ -259,7 +230,8 @@ inline auto col_map(F&& f, T&& m, Args&&... args) { * * For matrices `args...` with `n` rows, returns a matrix whose `i`-th row is * `f(args[0].row(i), args[1].row(i), ...)`. If the inputs have zero rows, - * returns a 0x0 matrix. + * returns a 0x0 matrix. If the first returned row is empty, returns a 0x0 + * matrix without calling the functor on the remaining rows. * * Unlike `row_map`, this overload only zips matrices and does not accept * additional shared trailing arguments. @@ -286,26 +258,34 @@ inline auto row_mapN(F&& f, Types&&... args) { static constexpr const char* function = "row_mapN"; internal::check_all_matching_dims(function, args...); - std::tuple...> m_refs{ - to_ref(std::forward(args))...}; + auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_rows = std::get<0>(m_refs).rows(); - using T_return = scalar_type_t>().row(0))...))>>; + using result_row_t = plain_type_t>().row(0))...))>; + using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; if (n_rows == 0) { return matrix_t(0, 0); } - matrix_t result(n_rows, 0); - apply( + return apply( [&](auto&&... ms) { - for (Eigen::Index i = 0; i < n_rows; ++i) { - internal::assign_matrix_row(function, result, i, f((ms.row(i))...)); + result_row_t first_row = f((ms.row(0))...); + if (first_row.size() == 0) { + return matrix_t(0, 0); } + matrix_t result(n_rows, first_row.cols()); + result.row(0) = first_row; + for (Eigen::Index i = 1; i < n_rows; ++i) { + result_row_t row = f((ms.row(i))...); + check_size_match(function, "columns of result", result.cols(), + "columns of returned row", row.cols()); + result.row(i) = row; + } + return result; }, m_refs); - return result; } /** @@ -313,7 +293,8 @@ inline auto row_mapN(F&& f, Types&&... args) { * * For matrices `args...` with `k` columns, returns a matrix whose `j`-th * column is `f(args[0].col(j), args[1].col(j), ...)`. If the inputs have - * zero columns, returns a 0x0 matrix. + * zero columns, returns a 0x0 matrix. If the first returned column is empty, + * returns a 0x0 matrix without calling the functor on the remaining columns. * * Unlike `col_map`, this overload only zips matrices and does not accept * additional shared trailing arguments. @@ -341,26 +322,32 @@ inline auto col_mapN(F&& f, Types&&... args) { static constexpr const char* function = "col_mapN"; internal::check_all_matching_dims(function, args...); - std::tuple...> m_refs{ - to_ref(std::forward(args))...}; + auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_cols = std::get<0>(m_refs).cols(); - using T_return = scalar_type_t>().col(0))...))>>; + using result_col_t = plain_type_t>().col(0))...))>; + using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; - if (n_cols == 0) { return matrix_t(0, 0); } - - matrix_t result(0, n_cols); - apply( + return apply( [&](auto&&... ms) { - for (Eigen::Index j = 0; j < n_cols; ++j) { - internal::assign_matrix_col(function, result, j, f((ms.col(j))...)); + result_col_t first_col = f((ms.col(0))...); + if (first_col.size() == 0) { + return matrix_t(0, 0); } + matrix_t result(first_col.rows(), n_cols); + result.col(0) = first_col; + for (Eigen::Index j = 1; j < n_cols; ++j) { + result_col_t col = f((ms.col(j))...); + check_size_match(function, "rows of result", result.rows(), + "rows of returned column", col.rows()); + result.col(j) = col; + } + return result; }, m_refs); - return result; } } // namespace math diff --git a/test/unit/math/prim/err/check_consistent_sizes_test.cpp b/test/unit/math/prim/err/check_consistent_sizes_test.cpp new file mode 100644 index 00000000000..62fcc9eaa46 --- /dev/null +++ b/test/unit/math/prim/err/check_consistent_sizes_test.cpp @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +TEST(ErrorHandling, checkConsistentSizesUnnamedArguments) { + using stan::math::check_consistent_sizes; + + std::vector x1(3); + std::vector x2(3); + std::vector x3(2); + + EXPECT_NO_THROW(check_consistent_sizes("check_consistent_sizes", x1, x2)); + EXPECT_THROW_MSG_WITH_COUNT( + check_consistent_sizes("check_consistent_sizes", x1, x2, x3), + std::invalid_argument, "arg1", 1); + EXPECT_THROW_MSG_WITH_COUNT( + check_consistent_sizes("check_consistent_sizes", x1, x2, x3), + std::invalid_argument, "arg3", 1); + EXPECT_NO_THROW(check_consistent_sizes("check_consistent_sizes", x1)); +} + +TEST(ErrorHandling, checkConsistentSizesNamedArgumentsStillResolve) { + using stan::math::check_consistent_sizes; + + std::vector x1(3); + std::vector x2(2); + + EXPECT_THROW_MSG( + check_consistent_sizes("check_consistent_sizes", "x1", x1, "x2", x2), + std::invalid_argument, "x2"); +} diff --git a/test/unit/math/prim/functor/map_test.cpp b/test/unit/math/prim/functor/map_test.cpp index e11d86148e0..d6ca255c357 100644 --- a/test/unit/math/prim/functor/map_test.cpp +++ b/test/unit/math/prim/functor/map_test.cpp @@ -134,6 +134,20 @@ TEST(MathFunctor, row_map_empty) { EXPECT_EQ(y.cols(), 0); } +TEST(MathFunctor, row_map_empty_result) { + Eigen::MatrixXd x = Eigen::MatrixXd::Ones(2, 2); + int calls = 0; + auto y = stan::math::row_map( + [&calls](const Eigen::RowVectorXd&) { + ++calls; + return Eigen::RowVectorXd(0); + }, + x); + EXPECT_EQ(y.rows(), 0); + EXPECT_EQ(y.cols(), 0); + EXPECT_EQ(calls, 1); +} + TEST(MathFunctor, row_map_inconsistent_row_size) { Eigen::MatrixXd x(2, 2); x << 1.0, 2.0, 3.0, 4.0; @@ -188,6 +202,20 @@ TEST(MathFunctor, col_map_empty) { EXPECT_EQ(y.cols(), 0); } +TEST(MathFunctor, col_map_empty_result) { + Eigen::MatrixXd x = Eigen::MatrixXd::Ones(2, 2); + int calls = 0; + auto y = stan::math::col_map( + [&calls](const Eigen::VectorXd&) { + ++calls; + return Eigen::VectorXd(0); + }, + x); + EXPECT_EQ(y.rows(), 0); + EXPECT_EQ(y.cols(), 0); + EXPECT_EQ(calls, 1); +} + TEST(MathFunctor, col_map_inconsistent_col_size) { Eigen::MatrixXd x(2, 2); x << 1.0, 2.0, 3.0, 4.0; @@ -259,6 +287,21 @@ TEST(MathFunctor, row_mapN_empty) { EXPECT_EQ(y.cols(), 0); } +TEST(MathFunctor, row_mapN_empty_result) { + Eigen::MatrixXd a = Eigen::MatrixXd::Ones(2, 2); + Eigen::MatrixXd b = Eigen::MatrixXd::Ones(2, 2); + int calls = 0; + auto y = stan::math::row_mapN( + [&calls](const Eigen::RowVectorXd&, const Eigen::RowVectorXd&) { + ++calls; + return Eigen::RowVectorXd(0); + }, + a, b); + EXPECT_EQ(y.rows(), 0); + EXPECT_EQ(y.cols(), 0); + EXPECT_EQ(calls, 1); +} + TEST(MathFunctor, row_mapN_mixed_scalar_types) { Eigen::MatrixXi a(2, 2); a << 1, 2, 3, 4; @@ -332,6 +375,21 @@ TEST(MathFunctor, col_mapN_empty) { EXPECT_EQ(y.cols(), 0); } +TEST(MathFunctor, col_mapN_empty_result) { + Eigen::MatrixXd a = Eigen::MatrixXd::Ones(2, 2); + Eigen::MatrixXd b = Eigen::MatrixXd::Ones(2, 2); + int calls = 0; + auto y = stan::math::col_mapN( + [&calls](const Eigen::VectorXd&, const Eigen::VectorXd&) { + ++calls; + return Eigen::VectorXd(0); + }, + a, b); + EXPECT_EQ(y.rows(), 0); + EXPECT_EQ(y.cols(), 0); + EXPECT_EQ(calls, 1); +} + TEST(MathFunctor, col_mapN_mixed_scalar_types) { Eigen::MatrixXi a(2, 2); a << 1, 2, 3, 4; From 05b7c8d6d9702b215d9fce59c0f42ae2bae4adcf Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Wed, 22 Jul 2026 15:52:25 -0400 Subject: [PATCH 08/14] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- stan/math/prim/functor/map.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index f4bd3338ed7..6111dac7eb2 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -260,8 +260,8 @@ inline auto row_mapN(F&& f, Types&&... args) { auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_rows = std::get<0>(m_refs).rows(); - using result_row_t = plain_type_t>().row(0))...))>; + using result_row_t = plain_type_t>().row(0))...))>; using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; @@ -324,8 +324,8 @@ inline auto col_mapN(F&& f, Types&&... args) { auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_cols = std::get<0>(m_refs).cols(); - using result_col_t = plain_type_t>().col(0))...))>; + using result_col_t = plain_type_t>().col(0))...))>; using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; if (n_cols == 0) { From eba6918380a6d79fc9741042a1156c9548986dd9 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Wed, 22 Jul 2026 15:57:07 -0400 Subject: [PATCH 09/14] generalize the check_matching_dims(...) of map.hpp --- stan/math/prim/err/check_matching_dims.hpp | 29 +++++++++++++++++++ stan/math/prim/functor/map.hpp | 26 ++--------------- .../prim/err/check_matching_dims_test.cpp | 17 +++++++++++ 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/stan/math/prim/err/check_matching_dims.hpp b/stan/math/prim/err/check_matching_dims.hpp index 5308545834f..7856b9a7b97 100644 --- a/stan/math/prim/err/check_matching_dims.hpp +++ b/stan/math/prim/err/check_matching_dims.hpp @@ -155,6 +155,35 @@ inline void check_matching_dims(const char* function, const char* name1, check_matching_dims(function, name1, y1, name2, y2); } +/** + * Check that the unnamed Eigen inputs have the same dimensions. Inputs are + * labeled `arg1`, `arg2`, and so on in error messages. + * + * @tparam T type of the first input + * @tparam Types types of the remaining inputs + * @param function function name (for error messages) + * @param x first input + * @param xs remaining inputs + * @throw `invalid_argument` if dimensions do not match + */ +template * = nullptr> +inline void check_matching_dims(const char* function, const T& x, + const Types&... xs) { + std::size_t arg_idx = 2; + ( + [&](const auto& y) { + if (x.rows() != y.rows() || x.cols() != y.cols()) { + [&]() STAN_COLD_PATH { + const std::string name = "arg" + std::to_string(arg_idx); + check_matching_dims(function, "arg1", x, name.c_str(), y); + }(); + } + ++arg_idx; + }(xs), + ...); +} + } // namespace math } // namespace stan #endif diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index 6111dac7eb2..0da61b50fd3 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -10,7 +10,6 @@ #include #include -#include #include #include #include @@ -19,27 +18,6 @@ namespace stan { namespace math { -namespace internal { - -template -inline void check_all_matching_dims(const char* function, const T& x, - const Types&... xs) { - std::size_t arg_idx = 2; - ( - [&](const auto& y) { - if (x.rows() != y.rows() || x.cols() != y.cols()) { - [&]() STAN_COLD_PATH { - const std::string name = "x" + std::to_string(arg_idx); - check_matching_dims(function, "x1", x, name.c_str(), y); - }(); - } - ++arg_idx; - }(xs), - ...); -} - -} // namespace internal - /** * Apply a functor to each element of a std::vector and collect the results. * @@ -256,7 +234,7 @@ inline auto row_mapN(F&& f, Types&&... args) { static_assert(sizeof...(Types) >= 2, "row_mapN requires at least two Eigen matrix inputs."); static constexpr const char* function = "row_mapN"; - internal::check_all_matching_dims(function, args...); + check_matching_dims(function, args...); auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_rows = std::get<0>(m_refs).rows(); @@ -320,7 +298,7 @@ inline auto col_mapN(F&& f, Types&&... args) { static_assert(sizeof...(Types) >= 2, "col_mapN requires at least two Eigen matrix inputs."); static constexpr const char* function = "col_mapN"; - internal::check_all_matching_dims(function, args...); + check_matching_dims(function, args...); auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_cols = std::get<0>(m_refs).cols(); diff --git a/test/unit/math/prim/err/check_matching_dims_test.cpp b/test/unit/math/prim/err/check_matching_dims_test.cpp index 4643355b52f..f0b62a70493 100644 --- a/test/unit/math/prim/err/check_matching_dims_test.cpp +++ b/test/unit/math/prim/err/check_matching_dims_test.cpp @@ -27,6 +27,23 @@ TEST(ErrorHandlingMatrix, checkMatchingDimsMatrix) { std::invalid_argument); } +TEST(ErrorHandlingMatrix, checkMatchingDimsUnnamedArguments) { + using stan::math::check_matching_dims; + + Eigen::MatrixXd x1(2, 3); + Eigen::MatrixXd x2(2, 3); + Eigen::MatrixXd x3(3, 2); + + EXPECT_NO_THROW(check_matching_dims("checkMatchingDims", x1)); + EXPECT_NO_THROW(check_matching_dims("checkMatchingDims", x1, x2)); + EXPECT_THROW_MSG_WITH_COUNT( + check_matching_dims("checkMatchingDims", x1, x2, x3), + std::invalid_argument, "arg1", 1); + EXPECT_THROW_MSG_WITH_COUNT( + check_matching_dims("checkMatchingDims", x1, x2, x3), + std::invalid_argument, "arg3", 1); +} + TEST(ErrorHandlingMatrix, checkMatchingDimsArray) { std::vector y(3); std::vector x(3); From 189b9712b60f481f2472154da00db893689ed654 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Wed, 22 Jul 2026 16:37:49 -0400 Subject: [PATCH 10/14] add tuple arguments to mapN helpers --- stan/math/prim/functor/map.hpp | 89 ++++++++++++++++++++++-- test/unit/math/prim/functor/map_test.cpp | 46 ++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index 0da61b50fd3..00eeaf63b0c 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -18,6 +18,22 @@ namespace stan { namespace math { +namespace internal { + +template +inline decltype(auto) with_tuple_prefix(F& f, Tuple& tup, Callback&& callback) { + return apply( + [&](auto&&... tuple_args) -> decltype(auto) { + auto prefixed_f = [&](auto&&... args) -> decltype(auto) { + return f(tuple_args..., std::forward(args)...); + }; + return std::forward(callback)(prefixed_f); + }, + tup); +} + +} // namespace internal + /** * Apply a functor to each element of a std::vector and collect the results. * @@ -95,6 +111,28 @@ inline auto mapN(F&& f, Types&&... args) { return result; } +/** + * Apply a functor elementwise to N std::vectors with tuple arguments prepended + * to each call. + * + * @tparam F Type of functor to apply. + * @tparam Tuple Type of tuple containing leading shared arguments. + * @tparam Types Types of input std::vector containers. + * @param f functor to apply to each tuple of elements. + * @param tup leading shared arguments expanded before each tuple of elements. + * @param args std::vector inputs to which operation is applied. + * @return std::vector with result of applying functor to each tuple of + * elements. + */ +template * = nullptr, + require_all_std_vector_t* = nullptr> +inline auto mapN(F&& f, Tuple&& tup, Types&&... args) { + return internal::with_tuple_prefix(f, tup, [&](auto&& prefixed_f) { + return mapN(prefixed_f, std::forward(args)...); + }); +} + /** * Apply a functor to each row of an Eigen matrix and collect the results. * @@ -238,8 +276,8 @@ inline auto row_mapN(F&& f, Types&&... args) { auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_rows = std::get<0>(m_refs).rows(); - using result_row_t = plain_type_t>().row(0))...))>; + using result_row_t = plain_type_t>().row(0))...))>; using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; @@ -266,6 +304,27 @@ inline auto row_mapN(F&& f, Types&&... args) { m_refs); } +/** + * Apply a functor rowwise to N Eigen matrices with tuple arguments prepended + * to each call. + * + * @tparam F Type of functor to apply. + * @tparam Tuple Type of tuple containing leading shared arguments. + * @tparam Types Eigen matrix types. + * @param f functor to apply to each tuple of rows. + * @param tup leading shared arguments expanded before each tuple of rows. + * @param args Eigen matrix inputs to which operation is applied. + * @return Eigen matrix with result of applying functor to each tuple of rows. + */ +template * = nullptr, + require_all_eigen_matrix_dynamic_t* = nullptr> +inline auto row_mapN(F&& f, Tuple&& tup, Types&&... args) { + return internal::with_tuple_prefix(f, tup, [&](auto&& prefixed_f) { + return row_mapN(prefixed_f, std::forward(args)...); + }); +} + /** * Apply a functor columnwise to N Eigen matrices with identical dimensions. * @@ -302,8 +361,8 @@ inline auto col_mapN(F&& f, Types&&... args) { auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_cols = std::get<0>(m_refs).cols(); - using result_col_t = plain_type_t>().col(0))...))>; + using result_col_t = plain_type_t>().col(0))...))>; using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; if (n_cols == 0) { @@ -328,6 +387,28 @@ inline auto col_mapN(F&& f, Types&&... args) { m_refs); } +/** + * Apply a functor columnwise to N Eigen matrices with tuple arguments + * prepended to each call. + * + * @tparam F Type of functor to apply. + * @tparam Tuple Type of tuple containing leading shared arguments. + * @tparam Types Eigen matrix types. + * @param f functor to apply to each tuple of columns. + * @param tup leading shared arguments expanded before each tuple of columns. + * @param args Eigen matrix inputs to which operation is applied. + * @return Eigen matrix with result of applying functor to each tuple of + * columns. + */ +template * = nullptr, + require_all_eigen_matrix_dynamic_t* = nullptr> +inline auto col_mapN(F&& f, Tuple&& tup, Types&&... args) { + return internal::with_tuple_prefix(f, tup, [&](auto&& prefixed_f) { + return col_mapN(prefixed_f, std::forward(args)...); + }); +} + } // namespace math } // namespace stan diff --git a/test/unit/math/prim/functor/map_test.cpp b/test/unit/math/prim/functor/map_test.cpp index d6ca255c357..9ea1da6daf5 100644 --- a/test/unit/math/prim/functor/map_test.cpp +++ b/test/unit/math/prim/functor/map_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include TEST(MathFunctor, map_square) { @@ -92,6 +93,16 @@ TEST(MathFunctor, mapN_three_vectors) { EXPECT_FLOAT_EQ(y[1], 222.0); } +TEST(MathFunctor, mapN_tuple_args) { + std::vector a{1.0, 2.0}; + std::vector b{10.0, 20.0}; + auto args = std::make_tuple(100.0); + auto y = stan::math::mapN( + [](double offset, double u, double v) { return offset + u + v; }, args, a, + b); + EXPECT_STD_VECTOR_FLOAT_EQ(y, std::vector({111.0, 122.0})); +} + TEST(MathFunctor, mapN_empty) { std::vector a; std::vector b; @@ -263,6 +274,24 @@ TEST(MathFunctor, row_mapN_add) { EXPECT_MATRIX_FLOAT_EQ(y, expected); } +TEST(MathFunctor, row_mapN_tuple_args) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd b(2, 2); + b << 10.0, 20.0, 30.0, 40.0; + auto args = std::make_tuple(100.0); + auto y = stan::math::row_mapN( + [](double offset, const Eigen::RowVectorXd& u, + const Eigen::RowVectorXd& v) { + return offset + u.array() + v.array(); + }, + args, a, b); + + Eigen::MatrixXd expected(2, 2); + expected << 111.0, 122.0, 133.0, 144.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + TEST(MathFunctor, row_mapN_dim_mismatch) { Eigen::MatrixXd a(2, 2); a << 1.0, 2.0, 3.0, 4.0; @@ -352,6 +381,23 @@ TEST(MathFunctor, col_mapN_add) { EXPECT_MATRIX_FLOAT_EQ(y, expected); } +TEST(MathFunctor, col_mapN_tuple_args) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd b(2, 2); + b << 10.0, 20.0, 30.0, 40.0; + auto args = std::make_tuple(100.0); + auto y = stan::math::col_mapN( + [](double offset, const Eigen::VectorXd& u, const Eigen::VectorXd& v) { + return offset + u.array() + v.array(); + }, + args, a, b); + + Eigen::MatrixXd expected(2, 2); + expected << 111.0, 122.0, 133.0, 144.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + TEST(MathFunctor, col_mapN_dim_mismatch) { Eigen::MatrixXd a(2, 2); a << 1.0, 2.0, 3.0, 4.0; From 819a8d943d1bd4167a5c5640105c5cd3d8c65b92 Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Wed, 22 Jul 2026 16:38:52 -0400 Subject: [PATCH 11/14] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- stan/math/prim/functor/map.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index 00eeaf63b0c..8ed257391f7 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -276,8 +276,8 @@ inline auto row_mapN(F&& f, Types&&... args) { auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_rows = std::get<0>(m_refs).rows(); - using result_row_t = plain_type_t>().row(0))...))>; + using result_row_t = plain_type_t>().row(0))...))>; using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; @@ -361,8 +361,8 @@ inline auto col_mapN(F&& f, Types&&... args) { auto m_refs = std::tuple{to_ref(std::forward(args))...}; const Eigen::Index n_cols = std::get<0>(m_refs).cols(); - using result_col_t = plain_type_t>().col(0))...))>; + using result_col_t = plain_type_t>().col(0))...))>; using T_return = scalar_type_t; using matrix_t = Eigen::Matrix; if (n_cols == 0) { From 4dc1c0038eb0e09e993cb1868513eeca320e1a4f Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Thu, 23 Jul 2026 16:21:51 -0400 Subject: [PATCH 12/14] fix tuple map Eigen expression lifetime --- stan/math/prim/functor/map.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index 8ed257391f7..2bd43c531ac 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -2,6 +2,7 @@ #define STAN_MATH_PRIM_FUNCTOR_MAP_HPP #include +#include #include #include #include @@ -25,7 +26,8 @@ inline decltype(auto) with_tuple_prefix(F& f, Tuple& tup, Callback&& callback) { return apply( [&](auto&&... tuple_args) -> decltype(auto) { auto prefixed_f = [&](auto&&... args) -> decltype(auto) { - return f(tuple_args..., std::forward(args)...); + return eval( + f(tuple_args..., std::forward(args)...)); }; return std::forward(callback)(prefixed_f); }, From db518e298763911639230d9ca0b460222349e4f3 Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Thu, 23 Jul 2026 16:23:01 -0400 Subject: [PATCH 13/14] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- stan/math/prim/functor/map.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index 2bd43c531ac..67bd65b4e9a 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -26,8 +26,7 @@ inline decltype(auto) with_tuple_prefix(F& f, Tuple& tup, Callback&& callback) { return apply( [&](auto&&... tuple_args) -> decltype(auto) { auto prefixed_f = [&](auto&&... args) -> decltype(auto) { - return eval( - f(tuple_args..., std::forward(args)...)); + return eval(f(tuple_args..., std::forward(args)...)); }; return std::forward(callback)(prefixed_f); }, From 8927f56e50da25e51531f80433ea906ea22950fc Mon Sep 17 00:00:00 2001 From: Florence Bockting Date: Wed, 29 Jul 2026 12:01:11 +0300 Subject: [PATCH 14/14] refactor: add trailing shared args --- stan/math/prim/functor/map.hpp | 490 +++++++++++------------ test/unit/math/prim/functor/map_test.cpp | 68 +++- 2 files changed, 282 insertions(+), 276 deletions(-) diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp index 67bd65b4e9a..0eb5740b733 100644 --- a/stan/math/prim/functor/map.hpp +++ b/stan/math/prim/functor/map.hpp @@ -2,7 +2,6 @@ #define STAN_MATH_PRIM_FUNCTOR_MAP_HPP #include -#include #include #include #include @@ -19,41 +18,18 @@ namespace stan { namespace math { -namespace internal { - -template -inline decltype(auto) with_tuple_prefix(F& f, Tuple& tup, Callback&& callback) { - return apply( - [&](auto&&... tuple_args) -> decltype(auto) { - auto prefixed_f = [&](auto&&... args) -> decltype(auto) { - return eval(f(tuple_args..., std::forward(args)...)); - }; - return std::forward(callback)(prefixed_f); - }, - tup); -} - -} // namespace internal - /** - * Apply a functor to each element of a std::vector and collect the results. - * - * For a vector `x` of length `n`, returns a std::vector whose `i`-th element - * is `f(x[i], args...)`. Additional arguments are shared across calls, which - * allows passing parameters without closures. If `x` is empty, returns an - * empty std::vector. + * Return a std::vector whose i-th element is `f(x[i], args...)`. + * Additional arguments are shared across calls. If `x` is empty, the result + * is empty. The element type of the result is deduced from `f`. * - * The element type of the returned std::vector is deduced from the return type - * of `f` to allow for cases where the input and return scalar types differ - * (e.g., functions implicitly promoting integers). - * - * @tparam F Type of functor to apply. - * @tparam T Type of input std::vector. - * @tparam Args Types of shared arguments passed to each call. - * @param f functor to apply to each element. - * @param x std::vector input to which operation is applied. - * @param args shared arguments passed to each call. - * @return std::vector with result of applying functor to each element of `x`. + * @tparam F type of functor + * @tparam T type of input std::vector + * @tparam Args types of shared arguments + * @param[in] f functor applied to each element + * @param[in] x input std::vector + * @param[in] args shared arguments passed to each call + * @return std::vector of results */ template * = nullptr> @@ -70,94 +46,84 @@ inline auto map(F&& f, T&& x, Args&&... args) { } /** - * Apply a functor elementwise to N std::vector inputs of the same size. - * - * For vectors `args...` of length `n`, returns a std::vector whose `i`-th - * element is `f(args[0][i], args[1][i], ...)`. If the inputs are empty, - * returns an empty std::vector. - * - * Unlike `map`, this overload only zips containers and does not accept - * additional shared trailing arguments. Pass shared state through the - * functor (e.g. a capturing lambda) when needed. - * - * The element type of the returned std::vector is deduced from the return type - * of `f` to allow for cases where the input and return scalar types differ - * (e.g., functions implicitly promoting integers). + * Return a std::vector whose i-th element is + * `f(get<0>(xs)[i], get<1>(xs)[i], ..., args...)`. + * The tuple `xs` must contain at least two equal-sized std::vector inputs. + * Additional trailing arguments are shared across calls. If the inputs are + * empty, the result is empty. The element type of the result is deduced from + * `f`. * - * @tparam F Type of functor to apply. - * @tparam Types Types of input std::vector containers. - * @param f functor to apply to each tuple of elements. - * @param args std::vector inputs to which operation is applied. - * @return std::vector with result of applying functor to each tuple of - * elements. - * @throw std::invalid_argument if the inputs have different sizes. + * @tparam F type of functor + * @tparam Tuple type of tuple of std::vector inputs + * @tparam Args types of shared arguments + * @param[in] f functor applied to each tuple of elements + * @param[in] xs tuple of std::vector inputs + * @param[in] args shared arguments passed to each call + * @return std::vector of results + * @throw std::invalid_argument if the inputs have different sizes */ -template * = nullptr> -inline auto mapN(F&& f, Types&&... args) { - static_assert(sizeof...(Types) >= 2, - "mapN requires at least two std::vector inputs."); +template * = nullptr> +inline auto mapN(F&& f, Tuple&& xs, Args&&... args) { + static_assert(tuple_size_v >= 2, + "mapN requires a tuple of at least two std::vector inputs."); static constexpr const char* function = "mapN"; - check_consistent_sizes(function, args...); - const std::size_t n = std::get<0>(std::forward_as_tuple(args...)).size(); - using T_return = std::decay_t; - std::vector result; - result.reserve(n); + return apply( + [&](auto&&... xs) { + static_assert((is_std_vector>::value && ...), + "mapN tuple elements must be std::vector."); + check_consistent_sizes(function, xs...); - for (std::size_t i = 0; i < n; ++i) { - result.push_back(f((args[i])...)); - } + const std::size_t n = std::get<0>(std::forward_as_tuple(xs...)).size(); + using T_return = std::decay_t; + std::vector result; + result.reserve(n); - return result; + for (std::size_t i = 0; i < n; ++i) { + result.push_back(f((xs[i])..., args...)); + } + return result; + }, + std::forward(xs)); } /** - * Apply a functor elementwise to N std::vectors with tuple arguments prepended - * to each call. + * Return a std::vector whose i-th element is `f(x1[i], x2[i], ...)`. + * Convenience overload equivalent to + * `mapN(f, std::forward_as_tuple(x1, x2, xs...))`. * - * @tparam F Type of functor to apply. - * @tparam Tuple Type of tuple containing leading shared arguments. - * @tparam Types Types of input std::vector containers. - * @param f functor to apply to each tuple of elements. - * @param tup leading shared arguments expanded before each tuple of elements. - * @param args std::vector inputs to which operation is applied. - * @return std::vector with result of applying functor to each tuple of - * elements. + * @tparam F type of functor + * @tparam T1 type of first std::vector + * @tparam T2 type of second std::vector + * @tparam Ts types of additional std::vector inputs + * @param[in] f functor applied to each tuple of elements + * @param[in] x1 first std::vector + * @param[in] x2 second std::vector + * @param[in] xs additional std::vector inputs + * @return std::vector of results + * @throw std::invalid_argument if the inputs have different sizes */ -template * = nullptr, - require_all_std_vector_t* = nullptr> -inline auto mapN(F&& f, Tuple&& tup, Types&&... args) { - return internal::with_tuple_prefix(f, tup, [&](auto&& prefixed_f) { - return mapN(prefixed_f, std::forward(args)...); - }); +template * = nullptr> +inline auto mapN(F&& f, T1&& x1, T2&& x2, Ts&&... xs) { + return mapN(std::forward(f), std::forward_as_tuple(x1, x2, xs...)); } /** - * Apply a functor to each row of an Eigen matrix and collect the results. - * - * For a matrix `m` with `n` rows, returns a matrix whose `i`-th row is - * `f(m.row(i), args...)`. Additional arguments are shared across calls. If - * `m` has zero rows, returns a 0x0 matrix (output column count is unknown - * when the functor is never called). If the first returned row is empty, - * returns a 0x0 matrix without calling the functor on the remaining rows. - * - * Expensive Eigen expressions passed as `m` are evaluated once via `to_ref` - * before the row loop, so compound expressions (e.g. `A * B`) are not - * re-evaluated for every row. - * - * The functor must return a type assignable to a matrix row. All returned - * rows must have the same number of columns. + * Return a matrix whose i-th row is `f(m.row(i), args...)`. + * Additional arguments are shared across calls. If `m` has zero rows, or if + * the first returned row is empty, returns a 0x0 matrix. All returned rows + * must have the same number of columns. * - * @tparam F Type of functor to apply. - * @tparam T Eigen matrix type. - * @tparam Args Types of shared arguments passed to each call. - * @param f functor to apply to each row. - * @param m Eigen matrix input to which operation is applied. - * @param args shared arguments passed to each call. - * @return Eigen matrix with result of applying functor to each row of `m`. - * @throw std::invalid_argument if returned rows have inconsistent sizes. + * @tparam F type of functor + * @tparam T type of Eigen matrix + * @tparam Args types of shared arguments + * @param[in] f functor applied to each row + * @param[in] m input matrix + * @param[in] args shared arguments passed to each call + * @return matrix of results + * @throw std::invalid_argument if returned rows have inconsistent sizes */ template * = nullptr> @@ -189,29 +155,19 @@ inline auto row_map(F&& f, T&& m, Args&&... args) { } /** - * Apply a functor to each column of an Eigen matrix and collect the results. - * - * For a matrix `m` with `k` columns, returns a matrix whose `j`-th column is - * `f(m.col(j), args...)`. Additional arguments are shared across calls. If - * `m` has zero columns, returns a 0x0 matrix (output row count is unknown - * when the functor is never called). If the first returned column is empty, - * returns a 0x0 matrix without calling the functor on the remaining columns. - * - * Expensive Eigen expressions passed as `m` are evaluated once via `to_ref` - * before the column loop, so compound expressions (e.g. `A * B`) are not - * re-evaluated for every column. - * - * The functor must return a type assignable to a matrix column. All returned + * Return a matrix whose j-th column is `f(m.col(j), args...)`. + * Additional arguments are shared across calls. If `m` has zero columns, or + * if the first returned column is empty, returns a 0x0 matrix. All returned * columns must have the same number of rows. * - * @tparam F Type of functor to apply. - * @tparam T Eigen matrix type. - * @tparam Args Types of shared arguments passed to each call. - * @param f functor to apply to each column. - * @param m Eigen matrix input to which operation is applied. - * @param args shared arguments passed to each call. - * @return Eigen matrix with result of applying functor to each column of `m`. - * @throw std::invalid_argument if returned columns have inconsistent sizes. + * @tparam F type of functor + * @tparam T type of Eigen matrix + * @tparam Args types of shared arguments + * @param[in] f functor applied to each column + * @param[in] m input matrix + * @param[in] args shared arguments passed to each call + * @return matrix of results + * @throw std::invalid_argument if returned columns have inconsistent sizes */ template * = nullptr> @@ -243,171 +199,183 @@ inline auto col_map(F&& f, T&& m, Args&&... args) { } /** - * Apply a functor rowwise to N Eigen matrices with identical dimensions. - * - * For matrices `args...` with `n` rows, returns a matrix whose `i`-th row is - * `f(args[0].row(i), args[1].row(i), ...)`. If the inputs have zero rows, - * returns a 0x0 matrix. If the first returned row is empty, returns a 0x0 - * matrix without calling the functor on the remaining rows. - * - * Unlike `row_map`, this overload only zips matrices and does not accept - * additional shared trailing arguments. - * - * Expensive Eigen expressions in `args...` are each evaluated once via - * `to_ref` before the row loop. - * - * The functor must return a type assignable to a matrix row. All returned - * rows must have the same number of columns. + * Return a matrix whose i-th row is + * `f(get<0>(ms).row(i), get<1>(ms).row(i), ..., args...)`. + * The tuple `ms` must contain at least two Eigen matrices with identical + * dimensions. Additional trailing arguments are shared across calls. If the + * inputs have zero rows, or if the first returned row is empty, returns a 0x0 + * matrix. All returned rows must have the same number of columns. * - * @tparam F Type of functor to apply. - * @tparam Types Eigen matrix types. - * @param f functor to apply to each tuple of rows. - * @param args Eigen matrix inputs to which operation is applied. - * @return Eigen matrix with result of applying functor to each tuple of rows. + * @tparam F type of functor + * @tparam Tuple type of tuple of Eigen matrix inputs + * @tparam Args types of shared arguments + * @param[in] f functor applied to each tuple of rows + * @param[in] ms tuple of Eigen matrix inputs + * @param[in] args shared arguments passed to each call + * @return matrix of results * @throw std::invalid_argument if the inputs have different dimensions or - * returned rows have inconsistent sizes. + * returned rows have inconsistent sizes */ -template * = nullptr> -inline auto row_mapN(F&& f, Types&&... args) { - static_assert(sizeof...(Types) >= 2, - "row_mapN requires at least two Eigen matrix inputs."); +template * = nullptr> +inline auto row_mapN(F&& f, Tuple&& ms, Args&&... args) { + static_assert(tuple_size_v >= 2, + "row_mapN requires a tuple of at least two Eigen matrix " + "inputs."); static constexpr const char* function = "row_mapN"; - check_matching_dims(function, args...); - auto m_refs = std::tuple{to_ref(std::forward(args))...}; - const Eigen::Index n_rows = std::get<0>(m_refs).rows(); - using result_row_t = plain_type_t>().row(0))...))>; - using T_return = scalar_type_t; - using matrix_t = Eigen::Matrix; + return apply( + [&](auto&&... mats) { + static_assert( + (is_eigen_matrix_dynamic>::value + && ...), + "row_mapN tuple elements must be Eigen matrices."); + check_matching_dims(function, mats...); - if (n_rows == 0) { - return matrix_t(0, 0); - } + auto m_refs = std::tuple{to_ref(std::forward(mats))...}; + const Eigen::Index n_rows = std::get<0>(m_refs).rows(); + using result_row_t = plain_type_t>().row(0))..., args...))>; + using T_return = scalar_type_t; + using matrix_t + = Eigen::Matrix; - return apply( - [&](auto&&... ms) { - result_row_t first_row = f((ms.row(0))...); - if (first_row.size() == 0) { + if (n_rows == 0) { return matrix_t(0, 0); } - matrix_t result(n_rows, first_row.cols()); - result.row(0) = first_row; - for (Eigen::Index i = 1; i < n_rows; ++i) { - result_row_t row = f((ms.row(i))...); - check_size_match(function, "columns of result", result.cols(), - "columns of returned row", row.cols()); - result.row(i) = row; - } - return result; + + return apply( + [&](auto&&... mrs) { + result_row_t first_row = f((mrs.row(0))..., args...); + if (first_row.size() == 0) { + return matrix_t(0, 0); + } + matrix_t result(n_rows, first_row.cols()); + result.row(0) = first_row; + for (Eigen::Index i = 1; i < n_rows; ++i) { + result_row_t row = f((mrs.row(i))..., args...); + check_size_match(function, "columns of result", result.cols(), + "columns of returned row", row.cols()); + result.row(i) = row; + } + return result; + }, + m_refs); }, - m_refs); + std::forward(ms)); } /** - * Apply a functor rowwise to N Eigen matrices with tuple arguments prepended - * to each call. + * Return a matrix whose i-th row is `f(m1.row(i), m2.row(i), ...)`. + * Convenience overload equivalent to + * `row_mapN(f, std::forward_as_tuple(m1, m2, ms...))`. * - * @tparam F Type of functor to apply. - * @tparam Tuple Type of tuple containing leading shared arguments. - * @tparam Types Eigen matrix types. - * @param f functor to apply to each tuple of rows. - * @param tup leading shared arguments expanded before each tuple of rows. - * @param args Eigen matrix inputs to which operation is applied. - * @return Eigen matrix with result of applying functor to each tuple of rows. + * @tparam F type of functor + * @tparam T1 type of first Eigen matrix + * @tparam T2 type of second Eigen matrix + * @tparam Ts types of additional Eigen matrix inputs + * @param[in] f functor applied to each tuple of rows + * @param[in] m1 first Eigen matrix + * @param[in] m2 second Eigen matrix + * @param[in] ms additional Eigen matrix inputs + * @return matrix of results + * @throw std::invalid_argument if the inputs have different dimensions or + * returned rows have inconsistent sizes */ -template * = nullptr, - require_all_eigen_matrix_dynamic_t* = nullptr> -inline auto row_mapN(F&& f, Tuple&& tup, Types&&... args) { - return internal::with_tuple_prefix(f, tup, [&](auto&& prefixed_f) { - return row_mapN(prefixed_f, std::forward(args)...); - }); +template * = nullptr> +inline auto row_mapN(F&& f, T1&& m1, T2&& m2, Ts&&... ms) { + return row_mapN(std::forward(f), std::forward_as_tuple(m1, m2, ms...)); } /** - * Apply a functor columnwise to N Eigen matrices with identical dimensions. - * - * For matrices `args...` with `k` columns, returns a matrix whose `j`-th - * column is `f(args[0].col(j), args[1].col(j), ...)`. If the inputs have - * zero columns, returns a 0x0 matrix. If the first returned column is empty, - * returns a 0x0 matrix without calling the functor on the remaining columns. - * - * Unlike `col_map`, this overload only zips matrices and does not accept - * additional shared trailing arguments. - * - * Expensive Eigen expressions in `args...` are each evaluated once via - * `to_ref` before the column loop. + * Return a matrix whose j-th column is + * `f(get<0>(ms).col(j), get<1>(ms).col(j), ..., args...)`. + * The tuple `ms` must contain at least two Eigen matrices with identical + * dimensions. Additional trailing arguments are shared across calls. If the + * inputs have zero columns, or if the first returned column is empty, returns + * a 0x0 matrix. All returned columns must have the same number of rows. * - * The functor must return a type assignable to a matrix column. All returned - * columns must have the same number of rows. - * - * @tparam F Type of functor to apply. - * @tparam Types Eigen matrix types. - * @param f functor to apply to each tuple of columns. - * @param args Eigen matrix inputs to which operation is applied. - * @return Eigen matrix with result of applying functor to each tuple of - * columns. + * @tparam F type of functor + * @tparam Tuple type of tuple of Eigen matrix inputs + * @tparam Args types of shared arguments + * @param[in] f functor applied to each tuple of columns + * @param[in] ms tuple of Eigen matrix inputs + * @param[in] args shared arguments passed to each call + * @return matrix of results * @throw std::invalid_argument if the inputs have different dimensions or - * returned columns have inconsistent sizes. + * returned columns have inconsistent sizes */ -template * = nullptr> -inline auto col_mapN(F&& f, Types&&... args) { - static_assert(sizeof...(Types) >= 2, - "col_mapN requires at least two Eigen matrix inputs."); +template * = nullptr> +inline auto col_mapN(F&& f, Tuple&& ms, Args&&... args) { + static_assert(tuple_size_v >= 2, + "col_mapN requires a tuple of at least two Eigen matrix " + "inputs."); static constexpr const char* function = "col_mapN"; - check_matching_dims(function, args...); - auto m_refs = std::tuple{to_ref(std::forward(args))...}; - const Eigen::Index n_cols = std::get<0>(m_refs).cols(); - using result_col_t = plain_type_t>().col(0))...))>; - using T_return = scalar_type_t; - using matrix_t = Eigen::Matrix; - if (n_cols == 0) { - return matrix_t(0, 0); - } return apply( - [&](auto&&... ms) { - result_col_t first_col = f((ms.col(0))...); - if (first_col.size() == 0) { + [&](auto&&... mats) { + static_assert( + (is_eigen_matrix_dynamic>::value + && ...), + "col_mapN tuple elements must be Eigen matrices."); + check_matching_dims(function, mats...); + + auto m_refs = std::tuple{to_ref(std::forward(mats))...}; + const Eigen::Index n_cols = std::get<0>(m_refs).cols(); + using result_col_t = plain_type_t>().col(0))..., args...))>; + using T_return = scalar_type_t; + using matrix_t + = Eigen::Matrix; + + if (n_cols == 0) { return matrix_t(0, 0); } - matrix_t result(first_col.rows(), n_cols); - result.col(0) = first_col; - for (Eigen::Index j = 1; j < n_cols; ++j) { - result_col_t col = f((ms.col(j))...); - check_size_match(function, "rows of result", result.rows(), - "rows of returned column", col.rows()); - result.col(j) = col; - } - return result; + + return apply( + [&](auto&&... mrs) { + result_col_t first_col = f((mrs.col(0))..., args...); + if (first_col.size() == 0) { + return matrix_t(0, 0); + } + matrix_t result(first_col.rows(), n_cols); + result.col(0) = first_col; + for (Eigen::Index j = 1; j < n_cols; ++j) { + result_col_t col = f((mrs.col(j))..., args...); + check_size_match(function, "rows of result", result.rows(), + "rows of returned column", col.rows()); + result.col(j) = col; + } + return result; + }, + m_refs); }, - m_refs); + std::forward(ms)); } /** - * Apply a functor columnwise to N Eigen matrices with tuple arguments - * prepended to each call. + * Return a matrix whose j-th column is `f(m1.col(j), m2.col(j), ...)`. + * Convenience overload equivalent to + * `col_mapN(f, std::forward_as_tuple(m1, m2, ms...))`. * - * @tparam F Type of functor to apply. - * @tparam Tuple Type of tuple containing leading shared arguments. - * @tparam Types Eigen matrix types. - * @param f functor to apply to each tuple of columns. - * @param tup leading shared arguments expanded before each tuple of columns. - * @param args Eigen matrix inputs to which operation is applied. - * @return Eigen matrix with result of applying functor to each tuple of - * columns. + * @tparam F type of functor + * @tparam T1 type of first Eigen matrix + * @tparam T2 type of second Eigen matrix + * @tparam Ts types of additional Eigen matrix inputs + * @param[in] f functor applied to each tuple of columns + * @param[in] m1 first Eigen matrix + * @param[in] m2 second Eigen matrix + * @param[in] ms additional Eigen matrix inputs + * @return matrix of results + * @throw std::invalid_argument if the inputs have different dimensions or + * returned columns have inconsistent sizes */ -template * = nullptr, - require_all_eigen_matrix_dynamic_t* = nullptr> -inline auto col_mapN(F&& f, Tuple&& tup, Types&&... args) { - return internal::with_tuple_prefix(f, tup, [&](auto&& prefixed_f) { - return col_mapN(prefixed_f, std::forward(args)...); - }); +template * = nullptr> +inline auto col_mapN(F&& f, T1&& m1, T2&& m2, Ts&&... ms) { + return col_mapN(std::forward(f), std::forward_as_tuple(m1, m2, ms...)); } } // namespace math diff --git a/test/unit/math/prim/functor/map_test.cpp b/test/unit/math/prim/functor/map_test.cpp index 9ea1da6daf5..f4ddeee9eae 100644 --- a/test/unit/math/prim/functor/map_test.cpp +++ b/test/unit/math/prim/functor/map_test.cpp @@ -93,13 +93,15 @@ TEST(MathFunctor, mapN_three_vectors) { EXPECT_FLOAT_EQ(y[1], 222.0); } -TEST(MathFunctor, mapN_tuple_args) { +TEST(MathFunctor, mapN_shared_arg_reused) { std::vector a{1.0, 2.0}; std::vector b{10.0, 20.0}; - auto args = std::make_tuple(100.0); + auto offset = std::make_unique(100.0); auto y = stan::math::mapN( - [](double offset, double u, double v) { return offset + u + v; }, args, a, - b); + [](double u, double v, const std::unique_ptr& mu) { + return u + v + *mu; + }, + std::forward_as_tuple(a, b), std::move(offset)); EXPECT_STD_VECTOR_FLOAT_EQ(y, std::vector({111.0, 122.0})); } @@ -274,18 +276,18 @@ TEST(MathFunctor, row_mapN_add) { EXPECT_MATRIX_FLOAT_EQ(y, expected); } -TEST(MathFunctor, row_mapN_tuple_args) { +TEST(MathFunctor, row_mapN_shared_arg_reused) { Eigen::MatrixXd a(2, 2); a << 1.0, 2.0, 3.0, 4.0; Eigen::MatrixXd b(2, 2); b << 10.0, 20.0, 30.0, 40.0; - auto args = std::make_tuple(100.0); + auto offset = std::make_unique(100.0); auto y = stan::math::row_mapN( - [](double offset, const Eigen::RowVectorXd& u, - const Eigen::RowVectorXd& v) { - return offset + u.array() + v.array(); + [](const Eigen::RowVectorXd& u, const Eigen::RowVectorXd& v, + const std::unique_ptr& mu) { + return *mu + u.array() + v.array(); }, - args, a, b); + std::forward_as_tuple(a, b), std::move(offset)); Eigen::MatrixXd expected(2, 2); expected << 111.0, 122.0, 133.0, 144.0; @@ -381,17 +383,18 @@ TEST(MathFunctor, col_mapN_add) { EXPECT_MATRIX_FLOAT_EQ(y, expected); } -TEST(MathFunctor, col_mapN_tuple_args) { +TEST(MathFunctor, col_mapN_shared_arg_reused) { Eigen::MatrixXd a(2, 2); a << 1.0, 2.0, 3.0, 4.0; Eigen::MatrixXd b(2, 2); b << 10.0, 20.0, 30.0, 40.0; - auto args = std::make_tuple(100.0); + auto offset = std::make_unique(100.0); auto y = stan::math::col_mapN( - [](double offset, const Eigen::VectorXd& u, const Eigen::VectorXd& v) { - return offset + u.array() + v.array(); + [](const Eigen::VectorXd& u, const Eigen::VectorXd& v, + const std::unique_ptr& mu) { + return *mu + u.array() + v.array(); }, - args, a, b); + std::forward_as_tuple(a, b), std::move(offset)); Eigen::MatrixXd expected(2, 2); expected << 111.0, 122.0, 133.0, 144.0; @@ -491,3 +494,38 @@ TEST(MathFunctor, col_map_block_expression) { expected << 2.0, 4.0, 8.0, 10.0, 14.0, 16.0; EXPECT_MATRIX_FLOAT_EQ(y, expected); } + +TEST(MathFunctor, row_mapN_expression_inputs) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 0.0, 0.0, 1.0; + Eigen::MatrixXd b(2, 2); + b << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd c(2, 2); + c << 10.0, 20.0, 30.0, 40.0; + + auto y + = stan::math::row_mapN([](const Eigen::RowVectorXd& u, + const Eigen::RowVectorXd& v) { return u + v; }, + a * b, c); + + Eigen::MatrixXd expected(2, 2); + expected << 11.0, 22.0, 33.0, 44.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +} + +TEST(MathFunctor, col_mapN_expression_inputs) { + Eigen::MatrixXd a(2, 2); + a << 1.0, 0.0, 0.0, 1.0; + Eigen::MatrixXd b(2, 2); + b << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd c(2, 2); + c << 10.0, 20.0, 30.0, 40.0; + + auto y = stan::math::col_mapN( + [](const Eigen::VectorXd& u, const Eigen::VectorXd& v) { return u + v; }, + a * b, c); + + Eigen::MatrixXd expected(2, 2); + expected << 11.0, 22.0, 33.0, 44.0; + EXPECT_MATRIX_FLOAT_EQ(y, expected); +}