Skip to content

Generalized normal distribution #3157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions stan/math/prim/prob/generalized_normal_lpdf.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#ifndef STAN_MATH_PRIM_PROB_GENERALIZED_NORMAL_LPDF_HPP
#define STAN_MATH_PRIM_PROB_GENERALIZED_NORMAL_LPDF_HPP

#include <cmath>
#include <stan/math/prim/err.hpp>
#include <stan/math/prim/functor/partials_propagator.hpp>
#include <stan/math/prim/fun/abs.hpp>
#include <stan/math/prim/fun/as_value_column_array_or_scalar.hpp>
#include <stan/math/prim/fun/constants.hpp>
#include <stan/math/prim/fun/digamma.hpp>
#include <stan/math/prim/fun/lgamma.hpp>
#include <stan/math/prim/fun/log.hpp>
#include <stan/math/prim/fun/max_size.hpp>
#include <stan/math/prim/fun/multiply_log.hpp>
#include <stan/math/prim/fun/pow.hpp>
#include <stan/math/prim/fun/sign.hpp>
#include <stan/math/prim/fun/size.hpp>
#include <stan/math/prim/fun/size_zero.hpp>
#include <stan/math/prim/fun/square.hpp>
#include <stan/math/prim/fun/to_ref.hpp>
#include <stan/math/prim/fun/value_of.hpp>
#include <stan/math/prim/meta.hpp>

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.
*
* <p>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 <bool propto, typename T_y, typename T_loc, typename T_scale,
typename T_shape,
require_all_not_nonscalar_prim_or_rev_kernel_expression_t<
T_y, T_loc, T_scale, T_shape>* = nullptr>
inline return_type_t<T_y, T_loc, T_scale, T_shape> generalized_normal_lpdf(
T_y&& y, T_loc&& mu, T_scale&& alpha, T_shape&& beta) {
using T_partials_return = partials_return_t<T_y, T_loc, T_scale, T_shape>;
using T_y_ref = ref_type_if_not_constant_t<T_y>;
using T_mu_ref = ref_type_if_not_constant_t<T_loc>;
using T_alpha_ref = ref_type_if_not_constant_t<T_scale>;
using T_beta_ref = ref_type_if_not_constant_t<T_shape>;
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<T_y>(y);
T_mu_ref mu_ref = std::forward<T_loc>(mu);
T_alpha_ref alpha_ref = std::forward<T_scale>(alpha);
T_beta_ref beta_ref = std::forward<T_shape>(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);
check_positive(function, "Shape parameter",
beta_val); // With β = +∞ this could be defined to be uniform,
// but we don't support that.

if (size_zero(y, mu, alpha, beta)) {
return 0;
}
if (!include_summand<propto, T_y, T_loc, T_scale, T_shape>::value) {
return 0;
}

const auto& inv_beta1p
= to_ref_if<!is_constant<T_shape>::value>(inv(beta_val) + 1);
const auto& diff
= to_ref_if<!is_constant_all<T_y, T_loc>::value>(y_val - mu_val);
const auto& inv_alpha = to_ref(inv(alpha_val));
const auto& scaled_abs_diff
= to_ref_if<!is_constant_all<T_y, T_loc, T_shape>::value>(abs(diff)
* inv_alpha);
const auto& scaled_abs_diff_pow
= to_ref_if<!is_constant_all<T_scale, T_shape>::value>(
pow(scaled_abs_diff, beta_val));
const size_t N = max_size(y, mu, alpha, beta);

T_partials_return logp = -sum(scaled_abs_diff_pow);

if (include_summand<propto>::value) {
logp -= LOG_TWO * N;
}
if (include_summand<propto, T_scale>::value) {
logp -= sum(log(alpha_val)) * (N / math::size(alpha));
}
if (include_summand<propto, T_shape>::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 (!is_constant_all<T_y, T_loc>::value) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For any if statements with known compile time values we can use if constexpr

Suggested change
if (!is_constant_all<T_y, T_loc>::value) {
if constexpr (!is_constant_all<T_y, T_loc>::value) {

// note: The partial derivatives for y, mu are undefined when y == mu &&
// beta < 1. The derivative limit as mu -> y goes:
// to 0 from both sides if beta > 1 (defined as 0)
// to +1/alpha from right but -1/alpha from left if beta == 1 (defined as
// 0, consistent with double_exponential_lpdf) to +∞ from right but -∞
// from left as y -> mu if beta < 1 (undefined)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a hard time understanding your comment here. Is this the right interpretation? Either way can you clean it up a bit so the cut points and conditions are laid out a bit nicer

Suggested change
// note: The partial derivatives for y, mu are undefined when y == mu &&
// beta < 1. The derivative limit as mu -> y goes:
// to 0 from both sides if beta > 1 (defined as 0)
// to +1/alpha from right but -1/alpha from left if beta == 1 (defined as
// 0, consistent with double_exponential_lpdf) to +∞ from right but -∞
// from left as y -> mu if beta < 1 (undefined)
// note: The partial derivatives for y, mu are undefined when
// y == mu && beta < 1.
// The derivative limit as mu -> y has the following cases:
// beta > 1: 0 from both sides(defined as 0)
// beta == 1: +1/alpha from right, but -1/alpha from left (defined as
// 0, consistent with double_exponential_lpdf) to +∞ from right
// beta < 1: -∞ from left as y -> mu (undefined)

const auto& rep_deriv
= to_ref_if < !is_constant<T_y>::value
&& !is_constant<T_loc>::value
> (sign(diff) * beta_val * pow(scaled_abs_diff, beta_val - 1)
* inv_alpha);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once you are in this block you know that T_y and T_loc are not constant

Suggested change
const auto& rep_deriv
= to_ref_if < !is_constant<T_y>::value
&& !is_constant<T_loc>::value
> (sign(diff) * beta_val * pow(scaled_abs_diff, beta_val - 1)
* inv_alpha);
auto rep_deriv = eval(sign(diff) * beta_val * pow(scaled_abs_diff, beta_val - 1) * inv_alpha);

if (!is_constant<T_y>::value) {
partials<0>(ops_partials) = -rep_deriv;
}
if (!is_constant<T_loc>::value) {
partials<1>(ops_partials) = std::move(rep_deriv);
}
}
if (!is_constant<T_scale>::value) {
partials<2>(ops_partials)
= (beta_val * scaled_abs_diff_pow - 1) * inv_alpha;
}
if (!is_constant<T_shape>::value) {
partials<3>(ops_partials)
= digamma(inv_beta1p) * inv_square(beta_val)
- multiply_log(scaled_abs_diff_pow, scaled_abs_diff);
}

return ops_partials.build(logp);
}

template <typename T_y, typename T_loc, typename T_scale, typename T_shape>
inline return_type_t<T_y, T_loc, T_scale, T_shape> generalized_normal_lpdf(
T_y&& y, T_loc&& mu, T_scale&& alpha, T_shape&& beta) {
return generalized_normal_lpdf<false>(
std::forward<T_y>(y), std::forward<T_loc>(mu),
std::forward<T_scale>(alpha), std::forward<T_shape>(beta));
}

} // namespace math
} // namespace stan
#endif
176 changes: 176 additions & 0 deletions test/prob/generalized_normal/generalized_normal_test.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Arguments: Doubles, Doubles, Doubles, Doubles
#include <stan/math/prim/prob/generalized_normal_lpdf.hpp>
#include <stan/math/prim/fun/abs.hpp>
#include <stan/math/prim/fun/log.hpp>
#include <stan/math/prim/fun/pow.hpp>
#include <stan/math/prim/fun/tgamma.hpp>
#include <stan/math/prim/fun/constants.hpp>

using std::numeric_limits;
using std::vector;

class AgradDistributionGeneralizedNormal : public AgradDistributionTest {
public:
void valid_values(vector<vector<double> >& parameters,
vector<double>& log_prob) {
vector<double> 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
}

void invalid_values(vector<size_t>& index, vector<double>& value) {
// y

// mu
index.push_back(1U);
value.push_back(numeric_limits<double>::infinity());

index.push_back(1U);
value.push_back(-numeric_limits<double>::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<double>::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<double>::infinity());
}

template <typename T_y, typename T_loc, typename T_scale, typename T_shape,
typename T5, typename T6>
stan::return_type_t<T_y, T_loc, T_scale, T_shape> 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 <bool propto, typename T_y, typename T_loc, typename T_scale,
typename T_shape, typename T5, typename T6>
stan::return_type_t<T_y, T_loc, T_scale, T_shape> 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<propto>(y, mu, alpha, beta);
}

template <typename T_y, typename T_loc, typename T_scale, typename T_shape,
typename T5, typename T6>
stan::return_type_t<T_y, T_loc, T_scale, T_shape> 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;

return -LOG_TWO - log(alpha) - lgamma(1.0 + inv(beta))
- pow(abs(y - mu) / alpha, beta);
}
};