diff --git a/stan/math/prim/prob/generalized_normal_lpdf.hpp b/stan/math/prim/prob/generalized_normal_lpdf.hpp new file mode 100644 index 00000000000..d78120b9cdd --- /dev/null +++ b/stan/math/prim/prob/generalized_normal_lpdf.hpp @@ -0,0 +1,181 @@ +#ifndef STAN_MATH_PRIM_PROB_GENERALIZED_NORMAL_LPDF_HPP +#define STAN_MATH_PRIM_PROB_GENERALIZED_NORMAL_LPDF_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace stan { +namespace math { + +/** \ingroup prob_dists + * The log of the generalized normal density for the specified scalar(s) given + * the specified location, scale and shape parameters. y, mu, alpha, or beta can + * each be either a scalar or a vector. Any vector inputs must be the same + * length. + * + *

The result log probability is defined to be the sum of the + * log probabilities for each observation/mean/scale/shape tuple. + * + * @tparam T_y type of scalar + * @tparam T_loc type of location parameter + * @tparam T_scale type of scale parameter + * @tparam T_shape type of shape parameter + * @param y (Sequence of) scalar(s) + * @param mu (Sequence of) location parameter(s) + * @param alpha (Sequence of) scale parameter(s) + * @param beta (Sequence of) shape parameter(s) + * @return The log of the product of the densities + * @throw std::domain_error if alpha or beta is not positive + */ +template * = nullptr> +inline return_type_t generalized_normal_lpdf( + T_y&& y, T_loc&& mu, T_scale&& alpha, T_shape&& beta) { + using T_partials_return = partials_return_t; + using T_y_ref = ref_type_if_not_constant_t; + using T_mu_ref = ref_type_if_not_constant_t; + using T_alpha_ref = ref_type_if_not_constant_t; + using T_beta_ref = ref_type_if_not_constant_t; + static constexpr const char* function = "generalized_normal_lpdf"; + check_consistent_sizes(function, "Random variable", y, "Location parameter", + mu, "Scale parameter", alpha, "Shape parameter", beta); + + T_y_ref y_ref = std::forward(y); + T_mu_ref mu_ref = std::forward(mu); + T_alpha_ref alpha_ref = std::forward(alpha); + T_beta_ref beta_ref = std::forward(beta); + + decltype(auto) y_val = to_ref(as_value_column_array_or_scalar(y_ref)); + decltype(auto) mu_val = to_ref(as_value_column_array_or_scalar(mu_ref)); + decltype(auto) alpha_val = to_ref(as_value_column_array_or_scalar(alpha_ref)); + decltype(auto) beta_val = to_ref(as_value_column_array_or_scalar(beta_ref)); + + check_not_nan(function, "Random variable", y_val); + check_finite(function, "Location parameter", mu_val); + check_positive(function, "Scale parameter", alpha_val); + + // With β = +∞ this could be defined to be uniform, but we don't support that. + check_positive(function, "Shape parameter", beta_val); + + if (size_zero(y, mu, alpha, beta)) { + return 0; + } + if constexpr (!include_summand::value) { + return 0; + } + + const auto& inv_beta1p + = to_ref_if::value>(inv(beta_val) + 1); + const auto& diff + = to_ref_if::value>(y_val - mu_val); + const auto& inv_alpha = to_ref(inv(alpha_val)); + const auto& scaled_abs_diff + = to_ref_if::value>(abs(diff) + * inv_alpha); + // pow(0, beta) is 0 for beta > 0, but autodiff of pow at base == 0 + // produces NaN (0 * log(0) in the tangent) and pollutes the autodiff stack. + // Replace 0 with 1 in the base (pow(1, p) is always clean), then zero + // out those results afterward. + const auto& sad_is_zero = eval(value_of_rec(scaled_abs_diff) == 0); + auto safe_abs_diff = eval(scaled_abs_diff); + if constexpr (is_eigen_v>) { + for (Eigen::Index i = 0; i < sad_is_zero.size(); ++i) + if (sad_is_zero.coeff(i)) + safe_abs_diff.coeffRef(i) += 1; + } else { + if (sad_is_zero) + safe_abs_diff += 1; + } + auto scaled_abs_diff_pow = eval(pow(safe_abs_diff, beta_val)); + if constexpr (is_eigen_v>) { + for (Eigen::Index i = 0; i < sad_is_zero.size(); ++i) { + if (sad_is_zero.coeff(i)) + scaled_abs_diff_pow.coeffRef(i) = 0; + } + } else { + if (sad_is_zero) + scaled_abs_diff_pow = 0; + } + const size_t N = max_size(y, mu, alpha, beta); + + T_partials_return logp = -sum(scaled_abs_diff_pow); + + if constexpr (include_summand::value) { + logp -= LOG_TWO * N; + } + if constexpr (include_summand::value) { + logp -= sum(log(alpha_val)) * (N / math::size(alpha)); + } + if constexpr (include_summand::value) { + logp -= sum(lgamma(inv_beta1p)) * (N / math::size(beta)); + } + + auto ops_partials + = make_partials_propagator(y_ref, mu_ref, alpha_ref, beta_ref); + + if constexpr (!is_constant_all::value) { + // At y == mu, the derivative is 0 for beta >= 1, undefined for beta < 1. + // Use safe_abs_diff (0 replaced with 1) to avoid NaN from pow(0, beta-1). + auto rep_deriv = eval(sign(diff) * beta_val + * pow(safe_abs_diff, beta_val - 1) * inv_alpha); + if constexpr (is_eigen_v>) { + for (Eigen::Index i = 0; i < sad_is_zero.size(); ++i) { + if (sad_is_zero.coeff(i)) + rep_deriv.coeffRef(i) = 0; + } + } else { + if (sad_is_zero) + rep_deriv = 0; + } + if constexpr (!is_constant::value) { + partials<0>(ops_partials) = -rep_deriv; + } + if constexpr (!is_constant::value) { + partials<1>(ops_partials) = std::move(rep_deriv); + } + } + if constexpr (!is_constant::value) { + partials<2>(ops_partials) + = (beta_val * scaled_abs_diff_pow - 1) * inv_alpha; + } + if constexpr (!is_constant::value) { + // multiply_log(0, 0) = 0 by convention, but fvar autodiff of + // multiply_log at (0, 0) produces NaN. safe_abs_diff avoids this. + partials<3>(ops_partials) + = digamma(inv_beta1p) * inv_square(beta_val) + - multiply_log(scaled_abs_diff_pow, safe_abs_diff); + } + + return ops_partials.build(logp); +} + +template +inline return_type_t generalized_normal_lpdf( + T_y&& y, T_loc&& mu, T_scale&& alpha, T_shape&& beta) { + return generalized_normal_lpdf( + std::forward(y), std::forward(mu), + std::forward(alpha), std::forward(beta)); +} + +} // namespace math +} // namespace stan +#endif diff --git a/test/prob/generalized_normal/generalized_normal_test.hpp b/test/prob/generalized_normal/generalized_normal_test.hpp new file mode 100644 index 00000000000..fd17690ce0e --- /dev/null +++ b/test/prob/generalized_normal/generalized_normal_test.hpp @@ -0,0 +1,228 @@ +// Arguments: Doubles, Doubles, Doubles, Doubles +#include +#include +#include +#include +#include +#include + +using std::numeric_limits; +using std::vector; + +class AgradDistributionGeneralizedNormal : public AgradDistributionTest { + public: + void valid_values(vector >& parameters, + vector& log_prob) { + vector param(4); + + param[0] = 0; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 2; // beta + parameters.push_back(param); + log_prob.push_back( + -0.57236494292470008707171367567652935582); // expected log_prob + + param[0] = 1; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 2; // beta + parameters.push_back(param); + log_prob.push_back( + -1.5723649429247000870717136756765293558); // expected log_prob + + param[0] = -2; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 2; // beta + parameters.push_back(param); + log_prob.push_back( + -4.5723649429247000870717136756765293558); // expected log_prob + + param[0] = -3.5; // y + param[1] = 1.9; // mu + param[2] = 7.2; // alpha + param[3] = 2; // beta + parameters.push_back(param); + log_prob.push_back( + -3.1089459689467097140959090592117462617); // expected log_prob + + param[0] = 0; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 1; // beta + parameters.push_back(param); + log_prob.push_back( + -0.69314718055994530941723212145817656808); // expected log_prob + + param[0] = 1; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 1; // beta + parameters.push_back(param); + log_prob.push_back( + -1.6931471805599453094172321214581765681); // expected log_prob + + param[0] = -2; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 1; // beta + parameters.push_back(param); + log_prob.push_back( + -2.6931471805599453094172321214581765681); // expected log_prob + + param[0] = -3.5; // y + param[1] = 1.9; // mu + param[2] = 7.2; // alpha + param[3] = 1; // beta + parameters.push_back(param); + log_prob.push_back( + -3.4172282065819549364414275049933934740); // expected log_prob + + param[0] = 0; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 1.5; // beta + parameters.push_back(param); + log_prob.push_back( + -0.59083234759930449611508182336583846717); // expected log_prob + + param[0] = 1; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 1.5; // beta + parameters.push_back(param); + log_prob.push_back( + -1.5908323475993044961150818233658384672); // expected log_prob + + param[0] = -2; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 1.5; // beta + parameters.push_back(param); + log_prob.push_back( + -3.4192594723454945937184592717852346243); // expected log_prob + + param[0] = -3.5; // y + param[1] = 1.9; // mu + param[2] = 7.2; // alpha + param[3] = 1.5; // beta + parameters.push_back(param); + log_prob.push_back( + -3.2144324264596431082120695849657575107); // expected log_prob + + param[0] = 0.5; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 1; // beta + parameters.push_back(param); + log_prob.push_back( + -1.1931471805599453094172321214581765681); // expected log_prob + + param[0] = 0.5; // y + param[1] = 0; // mu + param[2] = 1; // alpha + param[3] = 1.5; // beta + parameters.push_back(param); + log_prob.push_back( + -0.94438573819257864983001127257011830807); // expected log_prob + } + + void invalid_values(vector& index, vector& value) { + // y + + // mu + index.push_back(1U); + value.push_back(numeric_limits::infinity()); + + index.push_back(1U); + value.push_back(-numeric_limits::infinity()); + + // alpha + index.push_back(2U); + value.push_back(0.0); + + index.push_back(2U); + value.push_back(-1.0); + + index.push_back(2U); + value.push_back(-numeric_limits::infinity()); + + // beta + index.push_back(3U); + value.push_back(0.0); + + index.push_back(3U); + value.push_back(-1.0); + + index.push_back(3U); + value.push_back(-numeric_limits::infinity()); + } + + template + stan::return_type_t log_prob( + const T_y& y, const T_loc& mu, const T_scale& alpha, const T_shape& beta, + const T5&, const T6&) { + return stan::math::generalized_normal_lpdf(y, mu, alpha, beta); + } + + template + stan::return_type_t log_prob( + const T_y& y, const T_loc& mu, const T_scale& alpha, const T_shape& beta, + const T5&, const T6&) { + return stan::math::generalized_normal_lpdf(y, mu, alpha, beta); + } + + template + stan::return_type_t log_prob_function( + const T_y& y, const T_loc& mu, const T_scale& alpha, const T_shape& beta, + const T5&, const T6&) { + using stan::math::abs; + using stan::math::inv; + using stan::math::lgamma; + using stan::math::log; + using stan::math::LOG_TWO; + + auto base = abs(y - mu) / alpha; + bool at_zero = stan::math::value_of_rec(base) == 0; + if (at_zero) + base += 1; + auto pow_term = pow(base, beta); + if (at_zero) + pow_term = 0; + return -LOG_TWO - log(alpha) - lgamma(1.0 + inv(beta)) - pow_term; + } +}; + +TEST(ProbDistributionsGeneralizedNormal, VectorWithYEqualsMu) { + using Eigen::VectorXd; + using stan::math::generalized_normal_lpdf; + using stan::math::var; + + // y[1] == mu[1], other elements differ + VectorXd y(3), mu(3); + y << -1.0, 0.5, 2.0; + mu << 0.0, 0.5, 0.0; + double alpha = 1.0; + double beta = 1.5; + + double lp = generalized_normal_lpdf(y, mu, alpha, beta); + EXPECT_TRUE(std::isfinite(lp)); + + // Same with var types to check autodiff + std::vector y_v(y.data(), y.data() + y.size()); + std::vector mu_v(mu.data(), mu.data() + mu.size()); + var alpha_v = alpha; + var beta_v = beta; + var lp_v = generalized_normal_lpdf(y_v, mu_v, alpha_v, beta_v); + lp_v.grad(); + for (size_t i = 0; i < y_v.size(); ++i) { + EXPECT_TRUE(std::isfinite(y_v[i].adj())); + EXPECT_TRUE(std::isfinite(mu_v[i].adj())); + } + EXPECT_TRUE(std::isfinite(alpha_v.adj())); + EXPECT_TRUE(std::isfinite(beta_v.adj())); +}