From da42a48375aadce2d642f37d86a5dcc0439cf9f0 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Fri, 13 Feb 2026 15:28:02 -0500 Subject: [PATCH 1/6] update to allow finite_diff to handle higher order autodiff --- stan/math/fwd/core/fvar.hpp | 3 +- stan/math/fwd/fun/tangent_of.hpp | 65 +++++ stan/math/fwd/functor/finite_diff.hpp | 13 +- stan/math/fwd/meta/is_fvar.hpp | 3 - stan/math/prim/functor/make_holder_tuple.hpp | 2 +- stan/math/prim/functor/map.hpp | 24 ++ stan/math/prim/meta/is_fvar.hpp | 67 +++++ stan/math/prim/meta/is_matrix_cl.hpp | 6 + stan/math/rev/fun/adjoint_of.hpp | 96 ++++--- test/unit/math/fwd/fun/tangent_of_test.cpp | 219 ++++++++++++++ test/unit/math/mix/functor/map_test.cpp | 282 +++++++++++++++++++ test/unit/math/prim/functor/map_if_test.cpp | 4 +- test/unit/math/rev/fun/adjoint_of_test.cpp | 166 +++++++++++ test/unit/math/rev/functor/map_if_test.cpp | 41 +++ 14 files changed, 934 insertions(+), 57 deletions(-) create mode 100644 stan/math/fwd/fun/tangent_of.hpp create mode 100644 stan/math/prim/functor/map.hpp create mode 100644 test/unit/math/fwd/fun/tangent_of_test.cpp create mode 100644 test/unit/math/mix/functor/map_test.cpp create mode 100644 test/unit/math/rev/fun/adjoint_of_test.cpp create mode 100644 test/unit/math/rev/functor/map_if_test.cpp diff --git a/stan/math/fwd/core/fvar.hpp b/stan/math/fwd/core/fvar.hpp index 187d6a5700b..a2dca06eef2 100644 --- a/stan/math/fwd/core/fvar.hpp +++ b/stan/math/fwd/core/fvar.hpp @@ -65,7 +65,8 @@ struct fvar { * * @return tangent of this variable */ - Scalar d() const { return d_; } + const Scalar& d() const { return d_; } + Scalar& d() { return d_; } /** * Construct a forward variable with zero value and tangent. diff --git a/stan/math/fwd/fun/tangent_of.hpp b/stan/math/fwd/fun/tangent_of.hpp new file mode 100644 index 00000000000..33a26ce19b9 --- /dev/null +++ b/stan/math/fwd/fun/tangent_of.hpp @@ -0,0 +1,65 @@ +#ifndef STAN_MATH_REV_FUN_TANGENT_OF_HPP +#define STAN_MATH_REV_FUN_TANGENT_OF_HPP + +#include +#include + +namespace stan::math { + +/** + * Returns a reference to a variable's tangent. + * @note In the case of a `std::vector`, this will return back an Eigen map of a vector of the tangents. + * @tparam T Any object contains a scalar type of `fvar` + * @param x a fvar + * @return reference to `x`'s tangent + */ +template +inline decltype(auto) tangent_of(T&& x) noexcept { + if constexpr (!is_all_fvar_scalar_v) { + static_assert(sizeof(std::decay_t*) == 0, "Non-fvar types do not have an tangent."); + } else { + return map([](auto&& x_) -> decltype(auto) { + using arg_t = decltype(x_); + using arg_decay_t = std::decay_t; + if constexpr (is_tuple_v) { + return stan::math::apply([](auto&&... args) -> decltype(auto) { + return make_holder_tuple(tangent_of(std::forward(args))...); + }, std::forward(x_)); + } else if constexpr (is_fvar_v) { + return std::forward(x_).d(); + } else if constexpr (is_eigen_v) { + return make_holder([](auto&& x_hold_) -> decltype(auto) { + return std::forward(x_hold_).d(); + }, std::forward(x_)); + } else if constexpr (is_std_vector_v) { + if constexpr (is_fvar_v>) { + return make_holder([](auto&& x_hold_) -> decltype(auto) { + constexpr bool const_x = std::is_const_v>; + using base_map_t = Eigen::Matrix, -1, 1>; + using map_t = std::conditional_t; + return Eigen::Map(x_hold_.data(), x_hold_.size()).d(); + }, std::forward(x_)); + } else { + if constexpr (std::is_rvalue_reference_v) { + std::vector tangents; + tangents.reserve(x_.size()); + for (auto&& elem : x_) { + tangents.push_back(tangent_of(std::move(elem))); + } + return tangents; + } else { + std::vector tangents; + tangents.reserve(x_.size()); + for (auto&& elem : x_) { + tangents.push_back(tangent_of(elem)); + } + return tangents; + } + } + } + }, std::forward(x)); + } +} +} // namespace stan::math + +#endif // TANGENT_OF_HPP diff --git a/stan/math/fwd/functor/finite_diff.hpp b/stan/math/fwd/functor/finite_diff.hpp index 6def7b0bc65..be873d5be5e 100644 --- a/stan/math/fwd/functor/finite_diff.hpp +++ b/stan/math/fwd/functor/finite_diff.hpp @@ -1,11 +1,12 @@ #ifndef STAN_MATH_FWD_FUNCTOR_FINITE_DIFF_HPP #define STAN_MATH_FWD_FUNCTOR_FINITE_DIFF_HPP -#include +#include +#include +#include +#include #include #include -#include -#include #include namespace stan { @@ -43,9 +44,9 @@ inline constexpr double aggregate_tangent(const FuncTangent& tangent, */ template * = nullptr> -inline auto aggregate_tangent(const FuncTangent& tangent, const InputArg& arg) { - return sum(apply_scalar_binary([](auto&& x, auto&& y) { return x * y.d_; }, - tangent, arg)); +inline auto aggregate_tangent(FuncTangent&& tangent, InputArg&& arg) { + return sum(apply_scalar_binary([](auto&& x, auto&& y) { return x * y; }, + std::forward(tangent), tangent_of(std::forward(arg)))); } } // namespace internal diff --git a/stan/math/fwd/meta/is_fvar.hpp b/stan/math/fwd/meta/is_fvar.hpp index c5d67de8894..e208d08bc71 100644 --- a/stan/math/fwd/meta/is_fvar.hpp +++ b/stan/math/fwd/meta/is_fvar.hpp @@ -21,8 +21,5 @@ struct is_fvar>::value>> : std::true_type {}; -template -inline constexpr bool is_fvar_v = is_fvar::value; - } // namespace stan #endif diff --git a/stan/math/prim/functor/make_holder_tuple.hpp b/stan/math/prim/functor/make_holder_tuple.hpp index f436ccfcc28..ef9ff8fbd57 100644 --- a/stan/math/prim/functor/make_holder_tuple.hpp +++ b/stan/math/prim/functor/make_holder_tuple.hpp @@ -28,7 +28,7 @@ namespace internal { template struct deduce_cvr { using type - = std::conditional_t, std::decay_t, T&&>; + = std::conditional_t, std::decay_t, T&&>; }; template diff --git a/stan/math/prim/functor/map.hpp b/stan/math/prim/functor/map.hpp new file mode 100644 index 00000000000..4717f1c56bf --- /dev/null +++ b/stan/math/prim/functor/map.hpp @@ -0,0 +1,24 @@ +#ifndef STAN_MATH_PRIM_FUNCTOR_MAP_HPP +#define STAN_MATH_PRIM_FUNCTOR_MAP_HPP + +#include +#include + +namespace stan::math { +template +inline decltype(auto) map(F&& f, T&& arg) { + if constexpr (is_tuple_v) { + return stan::math::apply( + [](auto&& f, auto&&... args) { + return make_holder_tuple( + std::forward(f)( + std::forward(args))...); + }, + std::forward(arg), std::forward(f)); + } else { + return std::forward(f)(std::forward(arg)); + } +} +} + +#endif // STAN_MATH_PRIM_FUNCTOR_MAP_HPP diff --git a/stan/math/prim/meta/is_fvar.hpp b/stan/math/prim/meta/is_fvar.hpp index 0a26f93c1b5..4e8687f2bbf 100644 --- a/stan/math/prim/meta/is_fvar.hpp +++ b/stan/math/prim/meta/is_fvar.hpp @@ -14,6 +14,9 @@ namespace stan { template struct is_fvar : std::false_type {}; +template +inline constexpr bool is_fvar_v = is_fvar::value; + /** \ingroup type_trait * Specialization for pointers returns the underlying value the pointer is * pointing to. @@ -23,6 +26,70 @@ struct value_type>::value>> { using type = typename std::decay_t::Scalar; }; +template +using has_fvar_scalar_type = is_fvar>; + + +namespace internal { + +template +struct is_any_fvar_scalar_impl { + static constexpr bool value + = (has_fvar_scalar_type>::value || ...); +}; + +template +struct is_any_fvar_scalar_impl> { + static constexpr bool value + = (is_any_fvar_scalar_impl>::value || ...); +}; + +template +struct is_any_fvar_scalar_impl> { + static constexpr bool value = is_any_fvar_scalar_impl>::value; +}; + +} // namespace internal + +template +struct is_any_fvar_scalar + : std::disjunction< + internal::is_any_fvar_scalar_impl>...> {}; + +template +constexpr bool is_any_fvar_scalar_v + = is_any_fvar_scalar...>::value; + +namespace internal { +template +struct is_all_fvar_scalar_impl { + static constexpr bool value + = (has_fvar_scalar_type>::value && ...); +}; +template +struct is_all_fvar_scalar_impl> { + static constexpr bool value + = (is_all_fvar_scalar_impl>::value && ...); +}; + +template +struct is_all_fvar_scalar_impl, VecArgs...>> { + static constexpr bool value + = (is_all_fvar_scalar_impl>::value && ...); +}; + +} // namespace internal + +template +struct is_all_fvar_scalar + : std::disjunction< + internal::is_all_fvar_scalar_impl>...> {}; + +template +constexpr bool is_all_fvar_scalar_v + = is_all_fvar_scalar...>::value; + + /*! \ingroup require_stan_scalar_real */ /*! \defgroup fvar_types fvar */ /*! \addtogroup fvar_types */ diff --git a/stan/math/prim/meta/is_matrix_cl.hpp b/stan/math/prim/meta/is_matrix_cl.hpp index a61e9817d59..7375cbb5857 100644 --- a/stan/math/prim/meta/is_matrix_cl.hpp +++ b/stan/math/prim/meta/is_matrix_cl.hpp @@ -25,6 +25,12 @@ template struct is_matrix_cl : public std::is_base_of> {}; + +/** \ingroup matrix_cl_group + * Checks if the decayed type of T is a matrix_cl. + */ +template +inline constexpr bool is_matrix_cl_v = is_matrix_cl>::value; /*! \ingroup matrix_cl_group */ /*! \defgroup matrix_cl_types matrix_cl */ /*! \addtogroup matrix_cl_types */ diff --git a/stan/math/rev/fun/adjoint_of.hpp b/stan/math/rev/fun/adjoint_of.hpp index 7eb135f3167..67a83234af6 100644 --- a/stan/math/rev/fun/adjoint_of.hpp +++ b/stan/math/rev/fun/adjoint_of.hpp @@ -2,58 +2,64 @@ #define STAN_MATH_REV_FUN_ADJOINT_OF_HPP #include +#include -namespace stan { -namespace math { - -namespace internal { -struct nonexisting_adjoint { - template - nonexisting_adjoint operator+(const T&) { - return *this; - } - template - nonexisting_adjoint operator+=(T) const { - throw std::runtime_error( - "internal::nonexisting_adjoint::operator+= should never be called! " - "Please file a bug report. rev/fun/adjoint_of.hpp line " - + std::to_string(__LINE__)); - } - template - nonexisting_adjoint operator-=(T) const { - throw std::runtime_error( - "internal::nonexisting_adjoint::operator-= should never be called! " - "Please file a bug report. rev/fun/adjoint_of.hpp line " - + std::to_string(__LINE__)); - } -}; -} // namespace internal +namespace stan::math { /** * Returns a reference to a variable's adjoint. - * + * @note In the case of a `std::vector`, this will return back an Eigen map of a vector of the adjoints. + * @tparam T Any object contains a scalar type of `var` * @param x a var * @return reference to `x`'s adjoint */ -template * = nullptr> -inline auto& adjoint_of(const T& x) noexcept { - return x.adj(); -} - -/** - * Returns a reference to a variable's adjoint. If the input object is not var, - * it does not have an adjoint and this returns a dummy object. It defines - * operators += and -=, but they should not actually be called. - * - * @param x any non-var object - * @return a dummy adjoint - */ -template * = nullptr> -internal::nonexisting_adjoint adjoint_of(const T& x) { - return {}; +template +inline decltype(auto) adjoint_of(T&& x) noexcept { + if constexpr (!is_all_var_scalar_v) { + static_assert(sizeof(std::decay_t*) == 0, "Non-var types do not have an adjoint."); + } else { + return map([](auto&& x_) -> decltype(auto) { + using arg_t = decltype(x_); + using arg_decay_t = std::decay_t; + if constexpr (is_tuple_v) { + return stan::math::apply([](auto&&... args) -> decltype(auto) { + return make_holder_tuple(adjoint_of(std::forward(args))...); + }, std::forward(x_)); + } else if constexpr (is_var_v) { + return std::forward(x_).adj(); + } else if constexpr (is_eigen_v) { + return make_holder([](auto&& x_hold_) -> decltype(auto) { + return std::forward(x_hold_).adj(); + }, std::forward(x_)); + } else if constexpr (is_std_vector_v) { + if constexpr (is_var_v>) { + return make_holder([](auto&& x_hold_) -> decltype(auto) { + constexpr bool const_x = std::is_const_v>; + using base_map_t = Eigen::Matrix; + using map_t = std::conditional_t; + return Eigen::Map(x_hold_.data(), x_hold_.size()).adj(); + }, std::forward(x_)); + } else { + if constexpr (std::is_rvalue_reference_v) { + std::vector adjoints; + adjoints.reserve(x_.size()); + for (auto&& elem : x_) { + adjoints.push_back(adjoint_of(std::move(elem))); + } + return adjoints; + } else { + std::vector adjoints; + adjoints.reserve(x_.size()); + for (auto&& elem : x_) { + adjoints.push_back(adjoint_of(elem)); + } + return adjoints; + } + } + } + }, std::forward(x)); + } } - -} // namespace math -} // namespace stan +} // namespace stan::math #endif // ADJOINT_OF_HPP diff --git a/test/unit/math/fwd/fun/tangent_of_test.cpp b/test/unit/math/fwd/fun/tangent_of_test.cpp new file mode 100644 index 00000000000..9eddb112277 --- /dev/null +++ b/test/unit/math/fwd/fun/tangent_of_test.cpp @@ -0,0 +1,219 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TEST_F(AgradRev, FwdFun_tangent_of_fvar_var_scalar_lvalue) { + using fvar_var = stan::math::fvar; + + fvar_var x(stan::math::var(2.0), stan::math::var(0.0)); + auto&& x_tan = stan::math::tangent_of(x); + EXPECT_SAME_TYPE(stan::math::var&, decltype(x_tan)); + x_tan = 1.75; + EXPECT_FLOAT_EQ(x.d_.val(), 1.75); +} + +TEST_F(AgradRev, FwdFun_tangent_of_fvar_var_scalar_rvalue) { + using fvar_var = stan::math::fvar; + + auto&& x_tan + = stan::math::tangent_of(fvar_var(stan::math::var(2.0), stan::math::var(0.0))); + EXPECT_SAME_TYPE(stan::math::var&, decltype(x_tan)); + + x_tan = 3.25; + EXPECT_FLOAT_EQ(x_tan.val(), 3.25); +} + +TEST_F(AgradRev, FwdFun_tangent_of_matrix_fvar_var_lvalue_type_and_values) { + using fvar_var = stan::math::fvar; + using matrix_fvar_var + = Eigen::Matrix; + + matrix_fvar_var x(2, 2); + for (Eigen::Index i = 0; i < x.rows(); ++i) { + for (Eigen::Index j = 0; j < x.cols(); ++j) { + x(i, j) = fvar_var(stan::math::var(0.0), stan::math::var(0.0)); + } + } + decltype(auto) x_tan = stan::math::tangent_of(x); + EXPECT_SAME_TYPE(decltype(x.d()), decltype(x_tan)); + + Eigen::MatrixXd expected(2, 2); + expected << 1.0, 2.0, 3.0, 4.0; + x_tan = expected; + for (Eigen::Index i = 0; i < x.rows(); ++i) { + for (Eigen::Index j = 0; j < x.cols(); ++j) { + EXPECT_FLOAT_EQ(x(i, j).d_.val(), expected(i, j)); + } + } +} + +TEST_F(AgradRev, FwdFun_tangent_of_matrix_fvar_var_lvalue) { + using fvar_var = stan::math::fvar; + using matrix_fvar_var + = Eigen::Matrix; + + matrix_fvar_var x(2, 3); + for (Eigen::Index i = 0; i < x.rows(); ++i) { + for (Eigen::Index j = 0; j < x.cols(); ++j) { + x(i, j) = fvar_var(stan::math::var(0.0), stan::math::var(0.0)); + } + } + auto&& x_tan = stan::math::tangent_of(x); + + Eigen::MatrixXd expected(2, 3); + expected << 0.5, 1.5, 2.5, 3.5, 4.5, 5.5; + x_tan = expected; + + for (Eigen::Index i = 0; i < x.rows(); ++i) { + for (Eigen::Index j = 0; j < x.cols(); ++j) { + EXPECT_FLOAT_EQ(x(i, j).d_.val(), expected(i, j)); + } + } +} + +TEST_F(AgradRev, FwdFun_tangent_of_accepts_eigen_matrix_fvar_var_expression_rvalue) { + using fvar_var = stan::math::fvar; + using matrix_fvar_var + = Eigen::Matrix; + + matrix_fvar_var a(2, 2); + matrix_fvar_var b(2, 2); + for (Eigen::Index i = 0; i < a.rows(); ++i) { + for (Eigen::Index j = 0; j < a.cols(); ++j) { + const double aval = static_cast(i + j + 1); + const double bval = static_cast(2 * i + j + 0.5); + a(i, j) = fvar_var(stan::math::var(0.0), stan::math::var(aval)); + b(i, j) = fvar_var(stan::math::var(0.0), stan::math::var(bval)); + } + } + auto a_plus_b = a + b; + decltype(auto) expr_tan = stan::math::tangent_of(a_plus_b); + Eigen::MatrixXd expected(2, 2); + expected << 1.5, 3.5, 4.5, 6.5; + EXPECT_MATRIX_EQ(stan::math::value_of(expr_tan), expected); +} + +TEST_F(AgradRev, FwdFun_tangent_of_std_vector_fvar_var_lvalue) { + using fvar_var = stan::math::fvar; + + std::vector x(3); + x[0] = fvar_var(stan::math::var(1.0), stan::math::var(0.0)); + x[1] = fvar_var(stan::math::var(2.0), stan::math::var(0.0)); + x[2] = fvar_var(stan::math::var(3.0), stan::math::var(0.0)); + const auto x_size_before = x.size(); + + auto&& x_tan = stan::math::tangent_of(x); + EXPECT_EQ(x_tan.size(), x_size_before); + x_tan[0] = 2.0; + x_tan[1] = 4.0; + x_tan[2] = 6.0; + + EXPECT_FLOAT_EQ(x[0].d_.val(), 2.0); + EXPECT_FLOAT_EQ(x[1].d_.val(), 4.0); + EXPECT_FLOAT_EQ(x[2].d_.val(), 6.0); +} + +TEST_F(AgradRev, FwdFun_tangent_of_std_vector_fvar_var_rvalue) { + using fvar_var = stan::math::fvar; + + auto x_tan + = stan::math::tangent_of(std::vector{ + fvar_var(stan::math::var(1.0), stan::math::var(0.0)), + fvar_var(stan::math::var(2.0), stan::math::var(0.0))}); + + EXPECT_EQ(x_tan.size(), 2); + x_tan[0] = 5.0; + x_tan[1] = 7.0; + EXPECT_FLOAT_EQ(x_tan[0].val(), 5.0); + EXPECT_FLOAT_EQ(x_tan[1].val(), 7.0); +} + +TEST_F(AgradRev, FwdFun_tangent_of_nested_std_vector_fvar_var_lvalue) { + using fvar_var = stan::math::fvar; + + std::vector> x{ + {fvar_var(stan::math::var(1.0), stan::math::var(0.0)), + fvar_var(stan::math::var(2.0), stan::math::var(0.0))}, + {fvar_var(stan::math::var(3.0), stan::math::var(0.0))}}; + + const auto outer_size_before = x.size(); + const auto first_inner_size_before = x[0].size(); + const auto second_inner_size_before = x[1].size(); + auto&& x_tan = stan::math::tangent_of(x); + EXPECT_EQ(x_tan.size(), outer_size_before); + EXPECT_EQ(x_tan[0].size(), first_inner_size_before); + EXPECT_EQ(x_tan[1].size(), second_inner_size_before); + + x_tan[0][0] = 1.5; + x_tan[0][1] = 2.5; + x_tan[1][0] = 3.5; + + EXPECT_FLOAT_EQ(x[0][0].d_.val(), 1.5); + EXPECT_FLOAT_EQ(x[0][1].d_.val(), 2.5); + EXPECT_FLOAT_EQ(x[1][0].d_.val(), 3.5); +} + +TEST_F(AgradRev, FwdFun_tangent_of_nested_std_vector_fvar_var_rvalue) { + using fvar_var = stan::math::fvar; + + auto x_tan = stan::math::tangent_of(std::vector>{ + {fvar_var(stan::math::var(1.0), stan::math::var(0.0)), + fvar_var(stan::math::var(2.0), stan::math::var(0.0))}, + {fvar_var(stan::math::var(3.0), stan::math::var(0.0)), + fvar_var(stan::math::var(4.0), stan::math::var(0.0)), + fvar_var(stan::math::var(5.0), stan::math::var(0.0))}}); + ASSERT_EQ(x_tan.size(), 2); + EXPECT_EQ(x_tan[0].size(), 2); + EXPECT_EQ(x_tan[1].size(), 3); +} + +TEST_F(AgradRev, FwdFun_tangent_of_tuple_mixed_inputs) { + using fvar_var = stan::math::fvar; + using stan::test::is_lvalue_ref_element_v; + + fvar_var scalar(stan::math::var(1.0), stan::math::var(0.0)); + Eigen::Matrix mat(2, 2); + for (Eigen::Index i = 0; i < mat.rows(); ++i) { + for (Eigen::Index j = 0; j < mat.cols(); ++j) { + mat(i, j) = fvar_var(stan::math::var(0.0), stan::math::var(0.0)); + } + } + std::vector vec{ + fvar_var(stan::math::var(0.0), stan::math::var(0.0)), + fvar_var(stan::math::var(0.0), stan::math::var(0.0)), + fvar_var(stan::math::var(0.0), stan::math::var(0.0))}; + + auto out = stan::math::tangent_of(std::forward_as_tuple(scalar, mat, vec)); + using out_t = decltype(out); + EXPECT_EQ(std::tuple_size_v, 3); + EXPECT_TRUE((is_lvalue_ref_element_v<0, out_t>)); + + std::get<0>(out) = 7.0; + Eigen::MatrixXd mat_expected = Eigen::MatrixXd::Constant(2, 2, 3.0); + std::get<1>(out) = mat_expected; + for (int i = 0; i < 3; ++i) { + std::get<2>(out)[i] = 4.0; + } + + EXPECT_FLOAT_EQ(scalar.d_.val(), 7.0); + for (Eigen::Index i = 0; i < mat.rows(); ++i) { + for (Eigen::Index j = 0; j < mat.cols(); ++j) { + EXPECT_FLOAT_EQ(mat(i, j).d_.val(), mat_expected(i, j)); + } + } + EXPECT_FLOAT_EQ(vec[0].d_.val(), 4.0); + EXPECT_FLOAT_EQ(vec[1].d_.val(), 4.0); + EXPECT_FLOAT_EQ(vec[2].d_.val(), 4.0); +} + +TEST_F(AgradRev, FwdFun_tangent_of_empty_tuple) { + auto out = stan::math::tangent_of(std::tuple<>{}); + EXPECT_EQ(std::tuple_size_v, 0); +} diff --git a/test/unit/math/mix/functor/map_test.cpp b/test/unit/math/mix/functor/map_test.cpp new file mode 100644 index 00000000000..e37acce083a --- /dev/null +++ b/test/unit/math/mix/functor/map_test.cpp @@ -0,0 +1,282 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct mixed_output_functor { + int operator()(double x) const { return static_cast(x * 2.0); } + + double operator()(const stan::math::var& x) const { return x.val(); } + + Eigen::Index operator()(const Eigen::VectorXd& x) const { return x.size(); } + + double operator()(const Eigen::MatrixXd& x) const { return x.sum(); } + + std::size_t operator()(const std::vector& x) const { return x.size(); } + + std::vector operator()(const std::vector>& x) const { + std::vector out; + out.reserve(x.size()); + for (const auto& inner : x) { + out.push_back(std::accumulate(inner.begin(), inner.end(), 0)); + } + return out; + } +}; + +TEST(MathMixFunctor, map_scalar_support_and_return_type_changes) { + int i = 4; + auto&& i_ref = stan::math::map( + [](auto& x) -> auto& { return x; }, i); + EXPECT_SAME_TYPE(int&, decltype(i_ref)); + i_ref = 11; + EXPECT_EQ(i, 11); + + const int ci = 5; + auto&& ci_ref = stan::math::map( + [](const auto& x) -> const auto& { return x; }, ci); + EXPECT_SAME_TYPE(const int&, decltype(ci_ref)); + + auto as_bool = stan::math::map([](double x) { return x > 0.0; }, -1.5); + EXPECT_TYPE(bool, as_bool); + EXPECT_FALSE(as_bool); + + stan::math::var ad = 2.0; + auto ad_out = stan::math::map([](const auto& x) { return x * 3.0; }, ad); + EXPECT_TYPE(stan::math::var, ad_out); + EXPECT_FLOAT_EQ(ad_out.val(), 6.0); + + stan::math::fvar fvad(2.0, 1.0); + auto fvad_out + = stan::math::map([](const auto& x) { return x * 4.0; }, fvad); + EXPECT_TYPE(stan::math::fvar, fvad_out); + EXPECT_FLOAT_EQ(fvad_out.val_.val(), 8.0); + EXPECT_FLOAT_EQ(fvad_out.d_.val(), 4.0); +} + +TEST(MathMixFunctor, map_non_tuple_accepts_rvalue_scalar) { + auto out = stan::math::map([](double&& x) { return x + 1.0; }, 2.5); + EXPECT_TYPE(double, out); + EXPECT_FLOAT_EQ(out, 3.5); +} + +TEST(MathMixFunctor, map_non_tuple_eigen_vector) { + Eigen::VectorXd eigen_vec(3); + eigen_vec << 1.0, 2.0, 3.0; + auto vec_out = stan::math::map( + [](const Eigen::VectorXd& x) { + return (2.0 * x.array()).matrix(); + }, + eigen_vec) + .eval(); + EXPECT_SAME_TYPE(Eigen::VectorXd, std::decay_t); + Eigen::VectorXd expected_vec(3); + expected_vec << 2.0, 4.0, 6.0; + EXPECT_MATRIX_EQ(vec_out, expected_vec); +} + +TEST(MathMixFunctor, map_non_tuple_eigen_matrix) { + Eigen::MatrixXd eigen_mat(2, 2); + eigen_mat << 1.0, 2.0, 3.0, 4.0; + auto mat_out + = stan::math::map([](const Eigen::MatrixXd& x) { return (x * 2.0); }, + eigen_mat) + .eval(); + EXPECT_SAME_TYPE(Eigen::MatrixXd, std::decay_t); + EXPECT_MATRIX_EQ(mat_out, (eigen_mat * 2.0).eval()); +} + +TEST(MathMixFunctor, map_non_tuple_accepts_eigen_vector_expression) { + Eigen::VectorXd lhs(3); + lhs << 1.0, 2.0, 3.0; + Eigen::VectorXd rhs(3); + rhs << 4.0, 5.0, 6.0; + + auto vec_out + = stan::math::map([](const auto& x) { return x.eval(); }, lhs + rhs) + .eval(); + EXPECT_SAME_TYPE(Eigen::VectorXd, std::decay_t); + EXPECT_MATRIX_EQ(vec_out, (lhs + rhs).eval()); +} + +TEST(MathMixFunctor, map_non_tuple_accepts_eigen_matrix_expression) { + Eigen::MatrixXd lhs(2, 2); + lhs << 1.0, 2.0, 3.0, 4.0; + Eigen::MatrixXd rhs(2, 2); + rhs << -1.0, 0.5, 1.5, -2.0; + + auto mat_out + = stan::math::map([](const auto& x) { return x.eval(); }, lhs * rhs) + .eval(); + EXPECT_SAME_TYPE(Eigen::MatrixXd, std::decay_t); + EXPECT_MATRIX_EQ(mat_out, (lhs * rhs).eval()); +} + +TEST(MathMixFunctor, map_non_tuple_std_vector) { + std::vector std_vec{1, 2, 3}; + auto std_vec_out = stan::math::map( + [](const std::vector& x) { + std::vector out; + out.reserve(x.size()); + for (int v : x) { + out.push_back(v + 0.5); + } + return out; + }, + std_vec); + EXPECT_SAME_TYPE(std::vector, std::decay_t); + ASSERT_EQ(std_vec_out.size(), std_vec.size()); + EXPECT_FLOAT_EQ(std_vec_out[0], 1.5); + EXPECT_FLOAT_EQ(std_vec_out[1], 2.5); + EXPECT_FLOAT_EQ(std_vec_out[2], 3.5); +} + +TEST(MathMixFunctor, map_non_tuple_accepts_rvalue_std_vector) { + auto std_vec_out = stan::math::map( + [](std::vector&& x) { + x.push_back(4); + return std::move(x); + }, + std::vector{1, 2, 3}); + EXPECT_SAME_TYPE(std::vector, std::decay_t); + std::vector expected_std_vec{1, 2, 3, 4}; + EXPECT_EQ(std_vec_out, expected_std_vec); +} + +TEST(MathMixFunctor, map_non_tuple_nested_std_vector) { + std::vector> nested_vec{{1, 2, 3}, {4}, {}}; + auto nested_out = stan::math::map( + [](const std::vector>& x) { + std::vector out; + out.reserve(x.size()); + for (const auto& inner : x) { + out.push_back(std::accumulate(inner.begin(), inner.end(), 0)); + } + return out; + }, + nested_vec); + EXPECT_SAME_TYPE(std::vector, std::decay_t); + std::vector expected_nested{6, 4, 0}; + EXPECT_EQ(nested_out, expected_nested); +} + +TEST(MathMixFunctor, map_non_tuple_accepts_rvalue_nested_std_vector) { + auto nested_out = stan::math::map( + [](std::vector>&& x) { + x.push_back({7, 8}); + return std::move(x); + }, + std::vector>{{1, 2}, {3, 4}}); + EXPECT_SAME_TYPE(std::vector>, + std::decay_t); + ASSERT_EQ(nested_out.size(), 3); + EXPECT_EQ(nested_out.back(), (std::vector{7, 8})); +} + +TEST(MathMixFunctor, map_tuple_mixed_inputs_respect_output_types) { + using stan::test::is_ref_element_v; + using stan::test::is_same_tuple_element_v; + + double scalar = 1.5; + stan::math::var ad_scalar = 2.5; + Eigen::VectorXd eigen_vec(2); + eigen_vec << 3.0, 4.0; + Eigen::MatrixXd eigen_mat(2, 2); + eigen_mat << 1.0, 2.0, 3.0, 4.0; + std::vector std_vec{5, 6, 7}; + std::vector> nested_vec{{1, 2}, {3, 4, 5}}; + + auto out = stan::math::map( + mixed_output_functor{}, + std::forward_as_tuple(scalar, ad_scalar, eigen_vec, eigen_mat, std_vec, + nested_vec)); + using out_t = decltype(out); + EXPECT_EQ(std::tuple_size_v, 6); + EXPECT_TRUE((is_same_tuple_element_v<0, out_t, int>)); + EXPECT_TRUE((is_same_tuple_element_v<1, out_t, double>)); + EXPECT_TRUE((is_same_tuple_element_v<2, out_t, Eigen::Index>)); + EXPECT_TRUE((is_same_tuple_element_v<3, out_t, double>)); + EXPECT_TRUE((is_same_tuple_element_v<4, out_t, std::size_t>)); + EXPECT_TRUE((is_same_tuple_element_v<5, out_t, std::vector>)); + EXPECT_FALSE((is_ref_element_v<0, out_t>)); + EXPECT_FALSE((is_ref_element_v<1, out_t>)); + EXPECT_FALSE((is_ref_element_v<2, out_t>)); + EXPECT_FALSE((is_ref_element_v<3, out_t>)); + EXPECT_FALSE((is_ref_element_v<4, out_t>)); + EXPECT_FALSE((is_ref_element_v<5, out_t>)); + + EXPECT_EQ(std::get<0>(out), 3); + EXPECT_FLOAT_EQ(std::get<1>(out), 2.5); + EXPECT_EQ(std::get<2>(out), 2); + EXPECT_FLOAT_EQ(std::get<3>(out), 10.0); + EXPECT_EQ(std::get<4>(out), 3); + std::vector expected_nested{3, 12}; + EXPECT_EQ(std::get<5>(out), expected_nested); +} + +TEST(MathMixFunctor, map_tuple_preserves_lvalue_references_when_returned) { + using stan::test::is_lvalue_ref_element_v; + + int scalar = 2; + stan::math::var ad_scalar = 3.0; + Eigen::VectorXd eigen_vec(2); + eigen_vec << 1.0, 2.0; + std::vector std_vec{5, 6}; + std::vector> nested_vec{{1}, {2, 3}}; + + auto out = stan::math::map( + [](auto&& x) -> decltype(auto) { return std::forward(x); }, + std::forward_as_tuple(scalar, ad_scalar, eigen_vec, std_vec, nested_vec)); + using out_t = decltype(out); + EXPECT_TRUE((is_lvalue_ref_element_v<0, out_t>)); + EXPECT_TRUE((is_lvalue_ref_element_v<1, out_t>)); + EXPECT_TRUE((is_lvalue_ref_element_v<2, out_t>)); + EXPECT_TRUE((is_lvalue_ref_element_v<3, out_t>)); + EXPECT_TRUE((is_lvalue_ref_element_v<4, out_t>)); + + std::get<0>(out) = 11; + std::get<1>(out) = 7.0; + std::get<2>(out)(0) = -1.0; + std::get<3>(out)[1] = 42; + std::get<4>(out)[1].push_back(9); + + EXPECT_EQ(scalar, 11); + EXPECT_FLOAT_EQ(ad_scalar.val(), 7.0); + EXPECT_FLOAT_EQ(eigen_vec(0), -1.0); + EXPECT_EQ(std_vec[1], 42); + EXPECT_EQ(nested_vec[1].back(), 9); +} + +TEST(MathMixFunctor, map_tuple_rvalues_are_stored_by_value) { + using stan::test::is_ref_element_v; + + auto out = stan::math::map( + [](auto&& x) -> decltype(auto) { return std::forward(x); }, + std::make_tuple(4.0, std::vector{1, 2, 3}, + Eigen::VectorXd::Constant(2, 5.0))); + using out_t = decltype(out); + EXPECT_FALSE((is_ref_element_v<0, out_t>)); + EXPECT_FALSE((is_ref_element_v<1, out_t>)); + EXPECT_FALSE((is_ref_element_v<2, out_t>)); + + EXPECT_FLOAT_EQ(std::get<0>(out), 4.0); + std::vector expected_std_vec{1, 2, 3}; + EXPECT_EQ(std::get<1>(out), expected_std_vec); + EXPECT_FLOAT_EQ(std::get<2>(out)(0), 5.0); + EXPECT_FLOAT_EQ(std::get<2>(out)(1), 5.0); +} + +TEST(MathMixFunctor, map_empty_tuple_returns_empty_tuple) { + auto out = stan::math::map([](auto&& x) { return x; }, std::tuple<>{}); + EXPECT_EQ(std::tuple_size_v, 0); +} + +} // namespace diff --git a/test/unit/math/prim/functor/map_if_test.cpp b/test/unit/math/prim/functor/map_if_test.cpp index a96640ba0f9..63b2a3d5a40 100644 --- a/test/unit/math/prim/functor/map_if_test.cpp +++ b/test/unit/math/prim/functor/map_if_test.cpp @@ -10,12 +10,14 @@ using is_floating_point_decay = std::is_floating_point>; TEST(MathPrim, map_if_base) { using stan::math::map_if; auto x - = map_if([](auto&& x) noexcept { return x + 1; }, + = map_if([](auto&& x) noexcept { return static_cast(x) + 1; }, std::forward_as_tuple(1, 2.0, 3, 4.0)); EXPECT_EQ(std::get<0>(x), 1); EXPECT_EQ(std::get<1>(x), 3.0); EXPECT_EQ(std::get<2>(x), 3); EXPECT_EQ(std::get<3>(x), 5.0); + static_assert(std::is_same_v>, + "Should be tuple of int, double, int, double!"); } TEST(MathPrim, map_if_eigen) { diff --git a/test/unit/math/rev/fun/adjoint_of_test.cpp b/test/unit/math/rev/fun/adjoint_of_test.cpp new file mode 100644 index 00000000000..d8865ed65a1 --- /dev/null +++ b/test/unit/math/rev/fun/adjoint_of_test.cpp @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TEST_F(AgradRev, RevFun_adjoint_of_var_scalar_lvalue) { + stan::math::var x = 2.0; + auto&& x_adj = stan::math::adjoint_of(x); + EXPECT_SAME_TYPE(double&, decltype(x_adj)); + x_adj = 1.75; + EXPECT_FLOAT_EQ(x.adj(), 1.75); +} + +TEST_F(AgradRev, RevFun_adjoint_of_var_scalar_rvalue) { + auto&& x_adj = stan::math::adjoint_of(stan::math::var(2.0)); + EXPECT_SAME_TYPE(double&, decltype(x_adj)); + + x_adj = 3.25; + EXPECT_FLOAT_EQ(x_adj, 3.25); +} + +TEST_F(AgradRev, RevFun_adjoint_of_var_value_matrix_lvalue) { + using stan::math::var_value; + var_value x(Eigen::MatrixXd::Zero(2, 2)); + auto&& x_adj = stan::math::adjoint_of(x); + EXPECT_SAME_TYPE(decltype(x.adj()), decltype(x_adj)); + + Eigen::MatrixXd expected(2, 2); + expected << 1.0, 2.0, 3.0, 4.0; + x_adj = expected; + EXPECT_MATRIX_EQ(x.adj(), expected); +} + +TEST_F(AgradRev, RevFun_adjoint_of_matrix_var_lvalue) { + using matrix_v = Eigen::Matrix; + matrix_v x = Eigen::MatrixXd::Zero(2, 3).template cast(); + auto&& x_adj = stan::math::adjoint_of(x); + + Eigen::MatrixXd expected(2, 3); + expected << 0.5, 1.5, 2.5, 3.5, 4.5, 5.5; + x_adj = expected; + + for (Eigen::Index i = 0; i < x.rows(); ++i) { + for (Eigen::Index j = 0; j < x.cols(); ++j) { + EXPECT_FLOAT_EQ(x(i, j).adj(), expected(i, j)); + } + } +} + +TEST_F(AgradRev, RevFun_adjoint_of_accepts_eigen_var_value_expression_rvalue) { + using stan::math::set_zero_all_adjoints; + using stan::math::var_value; + + var_value a(Eigen::MatrixXd::Random(2, 2)); + var_value b(Eigen::MatrixXd::Random(2, 2)); + auto a_plus_b = a + b; + decltype(auto) expr_adj = stan::math::adjoint_of(a_plus_b); + auto sum_var = stan::math::sum(a_plus_b); + stan::math::grad(sum_var.vi_); + Eigen::MatrixXd expected = Eigen::MatrixXd::Constant(2, 2, 1); + EXPECT_MATRIX_EQ(expr_adj, expected); + EXPECT_MATRIX_EQ(a.adj(), expected); + EXPECT_MATRIX_EQ(b.adj(), expected); +} + +TEST_F(AgradRev, RevFun_adjoint_of_std_vector_var_lvalue) { + std::vector x(3); + x[0] = 1.0; + x[1] = 2.0; + x[2] = 3.0; + const auto x_size_before = x.size(); + + auto&& x_adj = stan::math::adjoint_of(x); + EXPECT_EQ(x.size(), x_size_before); + + Eigen::VectorXd expected(3); + expected << 2.0, 4.0, 6.0; + x_adj = expected; + + EXPECT_FLOAT_EQ(x[0].adj(), 2.0); + EXPECT_FLOAT_EQ(x[1].adj(), 4.0); + EXPECT_FLOAT_EQ(x[2].adj(), 6.0); +} + +TEST_F(AgradRev, RevFun_adjoint_of_std_vector_var_rvalue) { + auto x_adj = stan::math::adjoint_of( + std::vector{stan::math::var(1.0), stan::math::var(2.0)}); + + EXPECT_EQ(x_adj.size(), 2); + Eigen::VectorXd expected(2); + expected << 5.0, 7.0; + x_adj = expected; + EXPECT_MATRIX_EQ(x_adj, expected); +} + +TEST_F(AgradRev, RevFun_adjoint_of_nested_std_vector_var_lvalue) { + std::vector> x{ + {stan::math::var(1.0), stan::math::var(2.0)}, + {stan::math::var(3.0)}}; + + const auto outer_size_before = x.size(); + const auto first_inner_size_before = x[0].size(); + const auto second_inner_size_before = x[1].size(); + auto&& x_adj = stan::math::adjoint_of(x); + EXPECT_EQ(x.size(), outer_size_before); + EXPECT_EQ(x[0].size(), first_inner_size_before); + EXPECT_EQ(x[1].size(), second_inner_size_before); + + Eigen::VectorXd expected0(2); + expected0 << 1.5, 2.5; + Eigen::VectorXd expected1(1); + expected1 << 3.5; + x_adj[0] = expected0; + x_adj[1] = expected1; + + EXPECT_FLOAT_EQ(x[0][0].adj(), 1.5); + EXPECT_FLOAT_EQ(x[0][1].adj(), 2.5); + EXPECT_FLOAT_EQ(x[1][0].adj(), 3.5); +} + +TEST_F(AgradRev, RevFun_adjoint_of_nested_std_vector_var_rvalue) { + auto x_adj = stan::math::adjoint_of( + std::vector>{ + {stan::math::var(1.0), stan::math::var(2.0)}, + {stan::math::var(3.0), stan::math::var(4.0), stan::math::var(5.0)}}); + ASSERT_EQ(x_adj.size(), 2); + EXPECT_EQ(x_adj[0].size(), 2); + EXPECT_EQ(x_adj[1].size(), 3); +} + +TEST_F(AgradRev, RevFun_adjoint_of_tuple_mixed_inputs) { + using stan::test::is_lvalue_ref_element_v; + + stan::math::var scalar = 1.0; + Eigen::Matrix mat + = Eigen::MatrixXd::Zero(2, 2).template cast(); + std::vector vec{stan::math::var(0.0), stan::math::var(0.0), + stan::math::var(0.0)}; + + auto out = stan::math::adjoint_of(std::forward_as_tuple(scalar, mat, vec)); + using out_t = decltype(out); + EXPECT_EQ(std::tuple_size_v, 3); + EXPECT_TRUE((is_lvalue_ref_element_v<0, out_t>)); + + std::get<0>(out) = 7.0; + Eigen::MatrixXd mat_expected = Eigen::MatrixXd::Constant(2, 2, 3.0); + std::get<1>(out) = mat_expected; + Eigen::VectorXd vec_expected = Eigen::VectorXd::Constant(3, 4.0); + std::get<2>(out) = vec_expected; + + EXPECT_FLOAT_EQ(scalar.adj(), 7.0); + EXPECT_MATRIX_EQ(mat.adj(), mat_expected); + EXPECT_FLOAT_EQ(vec[0].adj(), 4.0); + EXPECT_FLOAT_EQ(vec[1].adj(), 4.0); + EXPECT_FLOAT_EQ(vec[2].adj(), 4.0); +} + +TEST_F(AgradRev, RevFun_adjoint_of_empty_tuple) { + auto out = stan::math::adjoint_of(std::tuple<>{}); + EXPECT_EQ(std::tuple_size_v, 0); +} diff --git a/test/unit/math/rev/functor/map_if_test.cpp b/test/unit/math/rev/functor/map_if_test.cpp new file mode 100644 index 00000000000..ca32dc0ff72 --- /dev/null +++ b/test/unit/math/rev/functor/map_if_test.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include +#include +namespace { + +TEST(MathPrim, map_if_base) { + using stan::is_any_var_scalar; + using stan::math::map_if; + using stan::math::var; + auto x + = map_if([](auto&& x) noexcept { return x + 1; }, + std::forward_as_tuple(1, var(2.0), std::vector{3.0, 4.0}, std::vector{4, 3, 2, 1})); + static_assert(std::is_same_v, std::vector>>, + "Should be tuple of int, var, vector, vector!"); +} + +TEST(MathPrim, map_if_eigen) { + using stan::math::map_if; + using stan::test::is_ref_element_v; + using stan::test::is_same_tuple_element_v; + Eigen::MatrixXd a = Eigen::MatrixXd::Random(3, 3); + std::vector b{1, 2, 3}; + auto x = map_if( + [](auto&& x) { return (x * x).eval(); }, + std::forward_as_tuple(a, b, a * a, std::vector{1, 2, 3})); + EXPECT_MATRIX_EQ(std::get<0>(x), (a * a).eval()); + static_assert(is_same_tuple_element_v<0, decltype(x), Eigen::MatrixXd>, + "1st should be MatrixXd!"); + static_assert(!is_ref_element_v<0, decltype(x)>, "0th should be an lvalue!"); + for (int i = 0; i < b.size(); i++) { + EXPECT_EQ(std::get<1>(x)[i], b[i]); + } + static_assert(is_ref_element_v<1, decltype(x)>, + "1st should be an reference!"); + static_assert(!is_ref_element_v<2, decltype(x)>, "2nd should be an lvalue!"); + static_assert(!is_ref_element_v<3, decltype(x)>, "3rd should be an lvalue!"); +} +} // namespace From 0f97d9a162fbff7f7140b08836f225a733faaadc Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Thu, 19 Feb 2026 17:09:05 -0500 Subject: [PATCH 2/6] add tuple version of finite_diff_gradient_auto --- stan/math/fwd/functor/finite_diff.hpp | 91 +++- .../functor/conditional_copy_and_promote.hpp | 102 +--- stan/math/mix/functor/laplace_likelihood.hpp | 2 +- .../mix/functor/laplace_marginal_density.hpp | 2 +- .../functor/finite_diff_gradient_auto.hpp | 443 +++++++++++++++++- stan/math/prim/meta.hpp | 1 + .../meta/conditional_copy_and_promote.hpp | 114 +++++ .../math/rev/core/filter_var_scalar_types.hpp | 6 +- .../math/fwd/functor/finite_diff_test.cpp | 66 +++ .../finite_diff_gradient_auto_test.cpp | 38 ++ 10 files changed, 737 insertions(+), 128 deletions(-) create mode 100644 stan/math/prim/meta/conditional_copy_and_promote.hpp create mode 100644 test/unit/math/fwd/functor/finite_diff_test.cpp diff --git a/stan/math/fwd/functor/finite_diff.hpp b/stan/math/fwd/functor/finite_diff.hpp index be873d5be5e..a0b56a5be2d 100644 --- a/stan/math/fwd/functor/finite_diff.hpp +++ b/stan/math/fwd/functor/finite_diff.hpp @@ -4,10 +4,10 @@ #include #include #include -#include -#include +#include #include -#include +#include +#include namespace stan { namespace math { @@ -45,11 +45,59 @@ inline constexpr double aggregate_tangent(const FuncTangent& tangent, template * = nullptr> inline auto aggregate_tangent(FuncTangent&& tangent, InputArg&& arg) { - return sum(apply_scalar_binary([](auto&& x, auto&& y) { return x * y; }, - std::forward(tangent), tangent_of(std::forward(arg)))); + auto tangent_arr = as_array_or_scalar(std::forward(tangent)); + auto arg_tangent_arr + = as_array_or_scalar(tangent_of(std::forward(arg))); + if constexpr (is_stan_scalar_v>) { + return tangent_arr * arg_tangent_arr; + } else { + using RetType = return_type_t, + std::decay_t>; + RetType rtn = 0; + for (Eigen::Index i = 0; i < tangent_arr.size(); ++i) { + rtn += tangent_arr(i) * arg_tangent_arr(i); + } + return rtn; + } } } // namespace internal +template +inline constexpr std::integer_sequence concat_sequences( + std::integer_sequence, std::integer_sequence) { + return {}; +} + +template +inline constexpr auto concat_sequences(std::integer_sequence, + std::integer_sequence, R...) { + return concat_sequences(std::integer_sequence{}, R{}...); +} + +template