Skip to content

Commit 9b1f08b

Browse files
authored
Merge pull request #3101 from stan-dev/sum-to-zero-constraint-ilr
Add sum_to_zero constraint, free, and check based on ILR transform
2 parents f120c5e + 5ae902d commit 9b1f08b

10 files changed

Lines changed: 640 additions & 0 deletions

File tree

stan/math/prim/constraint.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
#include <stan/math/prim/constraint/stochastic_column_free.hpp>
3838
#include <stan/math/prim/constraint/stochastic_row_constrain.hpp>
3939
#include <stan/math/prim/constraint/stochastic_row_free.hpp>
40+
#include <stan/math/prim/constraint/sum_to_zero_constrain.hpp>
41+
#include <stan/math/prim/constraint/sum_to_zero_free.hpp>
4042
#include <stan/math/prim/constraint/ub_constrain.hpp>
4143
#include <stan/math/prim/constraint/ub_free.hpp>
4244
#include <stan/math/prim/constraint/unit_vector_constrain.hpp>
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#ifndef STAN_MATH_PRIM_CONSTRAINT_SUM_TO_ZERO_CONSTRAIN_HPP
2+
#define STAN_MATH_PRIM_CONSTRAINT_SUM_TO_ZERO_CONSTRAIN_HPP
3+
4+
#include <stan/math/prim/meta.hpp>
5+
#include <stan/math/prim/fun/Eigen.hpp>
6+
#include <stan/math/prim/fun/to_ref.hpp>
7+
#include <stan/math/prim/fun/inv_sqrt.hpp>
8+
#include <stan/math/prim/functor/apply_vector_unary.hpp>
9+
#include <cmath>
10+
11+
namespace stan {
12+
namespace math {
13+
14+
/**
15+
* Return a vector with sum zero corresponding to the specified
16+
* free vector.
17+
*
18+
* The sum-to-zero transform is defined using a modified version of the
19+
* the inverse of the isometric log ratio transform (ILR).
20+
* See:
21+
* Egozcue, Juan Jose; Pawlowsky-Glahn, Vera; Mateu-Figueras, Gloria;
22+
* Barcelo-Vidal, Carles (2003), "Isometric logratio transformations for
23+
* compositional data analysis", Mathematical Geology, 35 (3): 279–300,
24+
* doi:10.1023/A:1023818214614, S2CID 122844634
25+
*
26+
* This implementation is closer to the description of the same using "pivot
27+
* coordinates" in
28+
* Filzmoser, P., Hron, K., Templ, M. (2018). Geometrical Properties of
29+
* Compositional Data. In: Applied Compositional Data Analysis. Springer Series
30+
* in Statistics. Springer, Cham. https://doi.org/10.1007/978-3-319-96422-5_3
31+
*
32+
* This is a linear transform, with no Jacobian.
33+
*
34+
* @tparam Vec type of the vector
35+
* @param y Free vector input of dimensionality K - 1.
36+
* @return Zero-sum vector of dimensionality K.
37+
*/
38+
template <typename Vec, require_eigen_col_vector_t<Vec>* = nullptr,
39+
require_not_st_var<Vec>* = nullptr>
40+
inline plain_type_t<Vec> sum_to_zero_constrain(const Vec& y) {
41+
const auto N = y.size();
42+
43+
plain_type_t<Vec> z = Eigen::VectorXd::Zero(N + 1);
44+
if (unlikely(N == 0)) {
45+
return z;
46+
}
47+
48+
auto&& y_ref = to_ref(y);
49+
50+
value_type_t<Vec> sum_w(0);
51+
for (int i = N; i > 0; --i) {
52+
double n = static_cast<double>(i);
53+
auto w = y_ref(i - 1) * inv_sqrt(n * (n + 1));
54+
sum_w += w;
55+
56+
z.coeffRef(i - 1) += sum_w;
57+
z.coeffRef(i) -= w * n;
58+
}
59+
60+
return z;
61+
}
62+
63+
/**
64+
* Return a vector with sum zero corresponding to the specified
65+
* free vector.
66+
*
67+
* The sum-to-zero transform is defined using a modified version of the
68+
* the inverse of the isometric log ratio transform (ILR).
69+
* See:
70+
* Egozcue, Juan Jose; Pawlowsky-Glahn, Vera; Mateu-Figueras, Gloria;
71+
* Barcelo-Vidal, Carles (2003), "Isometric logratio transformations for
72+
* compositional data analysis", Mathematical Geology, 35 (3): 279–300,
73+
* doi:10.1023/A:1023818214614, S2CID 122844634
74+
*
75+
* This implementation is closer to the description of the same using "pivot
76+
* coordinates" in
77+
* Filzmoser, P., Hron, K., Templ, M. (2018). Geometrical Properties of
78+
* Compositional Data. In: Applied Compositional Data Analysis. Springer Series
79+
* in Statistics. Springer, Cham. https://doi.org/10.1007/978-3-319-96422-5_3
80+
*
81+
* This is a linear transform, with no Jacobian.
82+
*
83+
* @tparam Vec type of the vector
84+
* @param y Free vector input of dimensionality K - 1.
85+
* @param lp unused
86+
* @return Zero-sum vector of dimensionality K.
87+
*/
88+
template <typename Vec, require_eigen_col_vector_t<Vec>* = nullptr,
89+
require_not_st_var<Vec>* = nullptr>
90+
inline plain_type_t<Vec> sum_to_zero_constrain(const Vec& y,
91+
value_type_t<Vec>& lp) {
92+
return sum_to_zero_constrain(y);
93+
}
94+
95+
/**
96+
* Return a vector with sum zero corresponding to the specified
97+
* free vector.
98+
*
99+
* The sum-to-zero transform is defined using a modified version of
100+
* the inverse of the isometric log ratio transform (ILR).
101+
* See:
102+
* Egozcue, Juan Jose; Pawlowsky-Glahn, Vera; Mateu-Figueras, Gloria;
103+
* Barcelo-Vidal, Carles (2003), "Isometric logratio transformations for
104+
* compositional data analysis", Mathematical Geology, 35 (3): 279–300,
105+
* doi:10.1023/A:1023818214614, S2CID 122844634
106+
*
107+
* This implementation is closer to the description of the same using "pivot
108+
* coordinates" in
109+
* Filzmoser, P., Hron, K., Templ, M. (2018). Geometrical Properties of
110+
* Compositional Data. In: Applied Compositional Data Analysis. Springer Series
111+
* in Statistics. Springer, Cham. https://doi.org/10.1007/978-3-319-96422-5_3
112+
*
113+
* This is a linear transform, with no Jacobian.
114+
*
115+
* @tparam Jacobian unused
116+
* @tparam Vec A type inheriting from `Eigen::DenseBase` or a `var_value` with
117+
* inner type inheriting from `Eigen::DenseBase` with compile time dynamic rows
118+
* and 1 column
119+
* @param[in] y free vector
120+
* @param[in, out] lp unused
121+
* @return Zero-sum vector of dimensionality one greater than `y`
122+
*/
123+
template <bool Jacobian, typename Vec, require_not_std_vector_t<Vec>* = nullptr>
124+
inline plain_type_t<Vec> sum_to_zero_constrain(const Vec& y,
125+
return_type_t<Vec>& lp) {
126+
return sum_to_zero_constrain(y);
127+
}
128+
129+
/**
130+
* Return a vector with sum zero corresponding to the specified
131+
* free vector.
132+
*
133+
* The sum-to-zero transform is defined using a modified version of
134+
* the inverse of the isometric log ratio transform (ILR).
135+
* See:
136+
* Egozcue, Juan Jose; Pawlowsky-Glahn, Vera; Mateu-Figueras, Gloria;
137+
* Barcelo-Vidal, Carles (2003), "Isometric logratio transformations for
138+
* compositional data analysis", Mathematical Geology, 35 (3): 279–300,
139+
* doi:10.1023/A:1023818214614, S2CID 122844634
140+
*
141+
* This implementation is closer to the description of the same using "pivot
142+
* coordinates" in
143+
* Filzmoser, P., Hron, K., Templ, M. (2018). Geometrical Properties of
144+
* Compositional Data. In: Applied Compositional Data Analysis. Springer Series
145+
* in Statistics. Springer, Cham. https://doi.org/10.1007/978-3-319-96422-5_3
146+
*
147+
* This is a linear transform, with no Jacobian.
148+
*
149+
* @tparam Jacobian unused
150+
* @tparam Vec A standard vector with inner type inheriting from
151+
* `Eigen::DenseBase` or a `var_value` with inner type inheriting from
152+
* `Eigen::DenseBase` with compile time dynamic rows and 1 column
153+
* @param[in] y free vector
154+
* @param[in, out] lp unused
155+
* @return Zero-sum vectors of dimensionality one greater than `y`
156+
*/
157+
template <bool Jacobian, typename T, require_std_vector_t<T>* = nullptr>
158+
inline auto sum_to_zero_constrain(const T& y, return_type_t<T>& lp) {
159+
return apply_vector_unary<T>::apply(
160+
y, [](auto&& v) { return sum_to_zero_constrain(v); });
161+
}
162+
163+
} // namespace math
164+
} // namespace stan
165+
166+
#endif
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#ifndef STAN_MATH_PRIM_CONSTRAINT_SUM_TO_ZERO_FREE_HPP
2+
#define STAN_MATH_PRIM_CONSTRAINT_SUM_TO_ZERO_FREE_HPP
3+
4+
#include <stan/math/prim/meta.hpp>
5+
#include <stan/math/prim/err.hpp>
6+
#include <stan/math/prim/fun/Eigen.hpp>
7+
#include <stan/math/prim/fun/to_ref.hpp>
8+
#include <stan/math/prim/fun/sqrt.hpp>
9+
#include <stan/math/prim/functor/apply_vector_unary.hpp>
10+
#include <cmath>
11+
12+
namespace stan {
13+
namespace math {
14+
15+
/**
16+
* Return an unconstrained vector.
17+
*
18+
* The sum-to-zero transform is defined using a modified version of the
19+
* isometric log ratio transform (ILR).
20+
* See:
21+
* Egozcue, Juan Jose; Pawlowsky-Glahn, Vera; Mateu-Figueras, Gloria;
22+
* Barcelo-Vidal, Carles (2003), "Isometric logratio transformations for
23+
* compositional data analysis", Mathematical Geology, 35 (3): 279–300,
24+
* doi:10.1023/A:1023818214614, S2CID 122844634
25+
*
26+
* This implementation is closer to the description of the same using "pivot
27+
* coordinates" in
28+
* Filzmoser, P., Hron, K., Templ, M. (2018). Geometrical Properties of
29+
* Compositional Data. In: Applied Compositional Data Analysis. Springer Series
30+
* in Statistics. Springer, Cham. https://doi.org/10.1007/978-3-319-96422-5_3
31+
*
32+
* @tparam ColVec a column vector type
33+
* @param z Vector of length K.
34+
* @return Free vector of length (K-1).
35+
* @throw std::domain_error if z does not sum to zero
36+
*/
37+
template <typename Vec, require_eigen_vector_t<Vec>* = nullptr>
38+
inline plain_type_t<Vec> sum_to_zero_free(const Vec& z) {
39+
const auto& z_ref = to_ref(z);
40+
check_sum_to_zero("stan::math::sum_to_zero_free", "sum_to_zero variable",
41+
z_ref);
42+
43+
const auto N = z.size() - 1;
44+
45+
plain_type_t<Vec> y = Eigen::VectorXd::Zero(N);
46+
if (unlikely(N == 0)) {
47+
return y;
48+
}
49+
50+
y.coeffRef(N - 1) = -z_ref(N) * sqrt(N * (N + 1)) / N;
51+
52+
value_type_t<Vec> sum_w(0);
53+
54+
for (int i = N - 2; i >= 0; --i) {
55+
double n = static_cast<double>(i + 1);
56+
auto w = y(i + 1) / sqrt((n + 1) * (n + 2));
57+
sum_w += w;
58+
y.coeffRef(i) = (sum_w - z_ref(i + 1)) * sqrt(n * (n + 1)) / n;
59+
}
60+
61+
return y;
62+
}
63+
64+
/**
65+
* Overload of `sum_to_zero_free()` to untransform each Eigen vector
66+
* in a standard vector.
67+
* @tparam T A standard vector with with a `value_type` which inherits from
68+
* `Eigen::MatrixBase` with compile time rows or columns equal to 1.
69+
* @param z The standard vector to untransform.
70+
*/
71+
template <typename T, require_std_vector_t<T>* = nullptr>
72+
auto sum_to_zero_free(const T& z) {
73+
return apply_vector_unary<T>::apply(
74+
z, [](auto&& v) { return sum_to_zero_free(v); });
75+
}
76+
77+
} // namespace math
78+
} // namespace stan
79+
80+
#endif

stan/math/prim/err.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#include <stan/math/prim/err/check_std_vector_index.hpp>
4242
#include <stan/math/prim/err/check_stochastic_column.hpp>
4343
#include <stan/math/prim/err/check_stochastic_row.hpp>
44+
#include <stan/math/prim/err/check_sum_to_zero.hpp>
4445
#include <stan/math/prim/err/check_symmetric.hpp>
4546
#include <stan/math/prim/err/check_unit_vector.hpp>
4647
#include <stan/math/prim/err/check_vector.hpp>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#ifndef STAN_MATH_PRIM_ERR_CHECK_SUM_TO_ZERO_HPP
2+
#define STAN_MATH_PRIM_ERR_CHECK_SUM_TO_ZERO_HPP
3+
4+
#include <stan/math/prim/fun/Eigen.hpp>
5+
#include <stan/math/prim/meta.hpp>
6+
#include <stan/math/prim/err/constraint_tolerance.hpp>
7+
#include <stan/math/prim/err/make_iter_name.hpp>
8+
#include <stan/math/prim/err/throw_domain_error.hpp>
9+
#include <stan/math/prim/fun/to_ref.hpp>
10+
#include <stan/math/prim/fun/value_of_rec.hpp>
11+
#include <sstream>
12+
#include <string>
13+
14+
namespace stan {
15+
namespace math {
16+
17+
/**
18+
* Throw an exception if the specified vector does not sum to 0.
19+
* This function tests that the sum is within the tolerance specified by
20+
* `CONSTRAINT_TOLERANCE`.
21+
* This function only accepts Eigen vectors, statically
22+
* typed vectors, not general matrices with 1 column.
23+
* @tparam T A type inheriting from `Eigen::EigenBase`
24+
* @param function Function name (for error messages)
25+
* @param name Variable name (for error messages)
26+
* @param theta Vector to test
27+
* @throw `std::invalid_argument` if `theta` is a 0-vector
28+
* @throw `std::domain_error` if the vector does not sum to zero
29+
*/
30+
template <typename T, require_matrix_t<T>* = nullptr>
31+
void check_sum_to_zero(const char* function, const char* name, const T& theta) {
32+
using std::fabs;
33+
// the size-zero case is technically a valid sum-to-zero vector,
34+
// but it cannot be unconstrained to anything
35+
check_nonzero_size(function, name, theta);
36+
auto&& theta_ref = to_ref(value_of_rec(theta));
37+
if (unlikely(!(fabs(theta_ref.sum()) <= CONSTRAINT_TOLERANCE))) {
38+
[&]() STAN_COLD_PATH {
39+
std::stringstream msg;
40+
scalar_type_t<T> sum = theta_ref.sum();
41+
msg << "does not sum to zero.";
42+
msg.precision(10);
43+
msg << " sum(" << name << ") = " << sum << ", but should be ";
44+
std::string msg_str(msg.str());
45+
throw_domain_error(function, name, 0.0, msg_str.c_str());
46+
}();
47+
}
48+
}
49+
50+
/**
51+
* Throw an exception if any vector in a standard vector does not sum to 0.
52+
* This function tests that the sum is within the tolerance specified by
53+
* `CONSTRAINT_TOLERANCE`.
54+
* @tparam T A standard vector with inner type inheriting from
55+
* `Eigen::EigenBase`
56+
* @param function Function name (for error messages)
57+
* @param name Variable name (for error messages)
58+
* @param theta Vector to test.
59+
* @throw `std::invalid_argument` if `theta` is a 0-vector
60+
* @throw `std::domain_error` if the vector does not sum to zero
61+
*/
62+
template <typename T, require_std_vector_t<T>* = nullptr>
63+
void check_sum_to_zero(const char* function, const char* name, const T& theta) {
64+
for (size_t i = 0; i < theta.size(); ++i) {
65+
check_sum_to_zero(function, internal::make_iter_name(name, i).c_str(),
66+
theta[i]);
67+
}
68+
}
69+
70+
} // namespace math
71+
} // namespace stan
72+
#endif

stan/math/rev/constraint.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <stan/math/rev/constraint/simplex_constrain.hpp>
1818
#include <stan/math/rev/constraint/stochastic_column_constrain.hpp>
1919
#include <stan/math/rev/constraint/stochastic_row_constrain.hpp>
20+
#include <stan/math/rev/constraint/sum_to_zero_constrain.hpp>
2021
#include <stan/math/rev/constraint/unit_vector_constrain.hpp>
2122
#include <stan/math/rev/constraint/ub_constrain.hpp>
2223

0 commit comments

Comments
 (0)