Skip to content

Commit fb463df

Browse files
committed
Prim implementation and test
1 parent 8b8057a commit fb463df

3 files changed

Lines changed: 181 additions & 59 deletions

File tree

stan/math/prim/constraint/sum_to_zero_constrain.hpp

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -61,47 +61,82 @@ inline plain_type_t<Vec> sum_to_zero_constrain(const Vec& y) {
6161
}
6262

6363
/**
64-
* Return a vector with sum zero corresponding to the specified
65-
* free vector.
64+
* Return a matrix that sums to zero over both the rows
65+
* and columns corresponding to the free matrix x.
6666
*
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
67+
* This is a linear transform, with no Jacobian.
7468
*
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
69+
* @tparam Mat type of the matrix
70+
* @param x Free matrix input of dimensionality (N - 1, M - 1).
71+
* @return Zero-sum matrix of dimensionality (N, M).
72+
*/
73+
template <typename Mat, require_eigen_matrix_dynamic_t<Mat>* = nullptr,
74+
require_not_st_var<Mat>* = nullptr>
75+
inline plain_type_t<Mat> sum_to_zero_constrain(const Mat& x) {
76+
const auto N = x.rows();
77+
const auto M = x.cols();
78+
const auto s = std::max(N, M);
79+
80+
plain_type_t<Mat> Z = Eigen::MatrixXd::Zero(N + 1, M + 1);
81+
if (unlikely(N == 0 || M == 0)) {
82+
return Z;
83+
}
84+
auto&& x_ref = to_ref(x);
85+
86+
Eigen::VectorXd beta = Eigen::VectorXd::Zero(N);
87+
88+
for (int j = M - 1; j >= 0; --j) {
89+
value_type_t<Mat> ax_previous(0);
90+
91+
double a_j = 1.0 / std::sqrt((j + 1.0) * (j + 2.0));
92+
double b_j = (j + 1.0) * a_j;
93+
94+
for (int i = N - 1; i >= 0; --i) {
95+
double a_i = 1.0 / std::sqrt((i + 1.0) * (i + 2.0));
96+
double b_i = (i + 1.0) * a_i;
97+
98+
auto b_i_x = b_i * x_ref(i, j) - ax_previous;
99+
100+
Z(i, j) = (b_j * b_i_x) - beta(i);
101+
beta(i) += a_j * b_i_x;
102+
103+
Z(N, j) -= Z(i, j);
104+
Z(i, M) -= Z(i, j);
105+
106+
ax_previous += a_i * x_ref(i, j);
107+
}
108+
Z(N, M) -= Z(N, j);
109+
}
110+
111+
return Z;
112+
}
113+
114+
/**
115+
* Return a vector or matrix with sum zero corresponding to the specified
116+
* free input.
80117
*
81118
* This is a linear transform, with no Jacobian.
82119
*
83-
* @tparam Vec type of the vector
120+
* @tparam T type of the input, either a vector or a matrix
84121
* @tparam Lp unused
85-
* @param y Free vector input of dimensionality K - 1.
122+
* @param y Free vector or matrix
86123
* @param lp unused
87-
* @return Zero-sum vector of dimensionality K.
124+
* @return Zero-sum vector or matrix which is one larger in each dimension
88125
*/
89-
template <typename Vec, typename Lp, require_eigen_col_vector_t<Vec>* = nullptr,
90-
require_not_st_var<Vec>* = nullptr>
91-
inline plain_type_t<Vec> sum_to_zero_constrain(const Vec& y, Lp& lp) {
126+
template <typename T, typename Lp, require_not_st_var<T>* = nullptr>
127+
inline plain_type_t<T> sum_to_zero_constrain(const T& y, Lp& lp) {
92128
return sum_to_zero_constrain(y);
93129
}
94130

95131
/**
96-
* Return a vector with sum zero corresponding to the specified
97-
* free vector.
132+
* Return a vector or matrix with sum zero corresponding to the specified
133+
* free input.
98134
* This overload handles looping over the elements of a standard vector.
99135
*
100-
* @tparam Vec A standard vector with inner type inheriting from
101-
* `Eigen::DenseBase` or a `var_value` with inner type inheriting from
102-
* `Eigen::DenseBase` with compile time dynamic rows and 1 column
103-
* @param[in] y free vector
104-
* @return Zero-sum vectors of dimensionality one greater than `y`
136+
* @tparam T A standard vector with inner type that is either a vector or a
137+
* matrix
138+
* @param[in] y free vector or matrix
139+
* @return Zero-sum vectors or matrices which are one larger in each dimension
105140
*/
106141
template <typename T, require_std_vector_t<T>* = nullptr>
107142
inline auto sum_to_zero_constrain(const T& y) {
@@ -114,13 +149,12 @@ inline auto sum_to_zero_constrain(const T& y) {
114149
* free vector.
115150
* This overload handles looping over the elements of a standard vector.
116151
*
117-
* @tparam Vec A standard vector with inner type inheriting from
118-
* `Eigen::DenseBase` or a `var_value` with inner type inheriting from
119-
* `Eigen::DenseBase` with compile time dynamic rows and 1 column
152+
* @tparam T A standard vector with inner type that is either a vector or a
153+
* matrix
120154
* @tparam Lp unused
121-
* @param[in] y free vector
155+
* @param[in] y free vector or matrix
122156
* @param[in, out] lp unused
123-
* @return Zero-sum vectors of dimensionality one greater than `y`
157+
* @return Zero-sum vectors or matrices which are one larger in each dimension
124158
*/
125159
template <typename T, typename Lp, require_std_vector_t<T>* = nullptr,
126160
require_convertible_t<return_type_t<T>, Lp>* = nullptr>
@@ -130,36 +164,19 @@ inline auto sum_to_zero_constrain(const T& y, Lp& lp) {
130164
}
131165

132166
/**
133-
* Return a vector with sum zero corresponding to the specified
134-
* free vector.
135-
*
136-
* The sum-to-zero transform is defined using a modified version of
137-
* the inverse of the isometric log ratio transform (ILR).
138-
* See:
139-
* Egozcue, Juan Jose; Pawlowsky-Glahn, Vera; Mateu-Figueras, Gloria;
140-
* Barcelo-Vidal, Carles (2003), "Isometric logratio transformations for
141-
* compositional data analysis", Mathematical Geology, 35 (3): 279–300,
142-
* doi:10.1023/A:1023818214614, S2CID 122844634
143-
*
144-
* This implementation is closer to the description of the same using "pivot
145-
* coordinates" in
146-
* Filzmoser, P., Hron, K., Templ, M. (2018). Geometrical Properties of
147-
* Compositional Data. In: Applied Compositional Data Analysis. Springer Series
148-
* in Statistics. Springer, Cham. https://doi.org/10.1007/978-3-319-96422-5_3
149-
*
167+
* Return a vector or matrix with sum zero corresponding to the specified
168+
* free input.
150169
* This is a linear transform, with no Jacobian.
151170
*
152171
* @tparam Jacobian unused
153-
* @tparam Vec A type inheriting from `Eigen::DenseBase` or a `var_value` with
154-
* inner type inheriting from `Eigen::DenseBase` with compile time dynamic rows
155-
* and 1 column, or a standard vector thereof
172+
* @tparam T type of the input
156173
* @tparam Lp unused
157-
* @param[in] y free vector
174+
* @param[in] y free vector or matrix
158175
* @param[in, out] lp unused
159-
* @return Zero-sum vector of dimensionality one greater than `y`
176+
* @return Zero-sum vector or matrix which is one larger in each dimension
160177
*/
161-
template <bool Jacobian, typename Vec, typename Lp>
162-
inline plain_type_t<Vec> sum_to_zero_constrain(const Vec& y, Lp& lp) {
178+
template <bool Jacobian, typename T, typename Lp>
179+
inline plain_type_t<T> sum_to_zero_constrain(const T& y, Lp& lp) {
163180
return sum_to_zero_constrain(y);
164181
}
165182

stan/math/prim/constraint/sum_to_zero_free.hpp

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,56 @@ inline plain_type_t<Vec> sum_to_zero_free(const Vec& z) {
6262
}
6363

6464
/**
65-
* Overload of `sum_to_zero_free()` to untransform each Eigen vector
65+
* Return an unconstrained matrix.
66+
*
67+
* @tparam Mat a column vector type
68+
* @param z Matrix of size (N, M)
69+
* @return Free matrix of length (N - 1, M - 1)
70+
* @throw std::domain_error if z does not sum to zero
71+
*/
72+
template <typename Mat, require_eigen_matrix_dynamic_t<Mat>* = nullptr>
73+
inline plain_type_t<Mat> sum_to_zero_free(const Mat& z) {
74+
const auto& z_ref = to_ref(z);
75+
check_sum_to_zero("stan::math::sum_to_zero_free", "sum_to_zero variable",
76+
z_ref);
77+
78+
const auto N = z_ref.rows() - 1;
79+
const auto M = z_ref.cols() - 1;
80+
const auto s = std::max(N, M);
81+
82+
plain_type_t<Mat> x = Eigen::MatrixXd::Zero(N, M);
83+
if (unlikely(N == 0 || M == 0)) {
84+
return x;
85+
}
86+
87+
Eigen::Matrix<value_type_t<Mat>, -1, 1> beta = Eigen::VectorXd::Zero(N);
88+
89+
for (int j = M - 1; j >= 0; --j) {
90+
value_type_t<Mat> ax_previous(0);
91+
92+
double a_j = 1.0 / std::sqrt((j + 1.0) * (j + 2.0));
93+
double b_j = (j + 1.0) * a_j;
94+
95+
for (int i = N - 1; i >= 0; --i) {
96+
double a_i = 1.0 / std::sqrt((i + 1.0) * (i + 2.0));
97+
double b_i = (i + 1.0) * a_i;
98+
99+
auto alpha_plus_beta = z_ref(i, j) + beta(i);
100+
101+
x(i, j) = (alpha_plus_beta + b_j * ax_previous) / (b_j * b_i);
102+
beta(i) += a_j * (b_i * x(i, j) - ax_previous);
103+
ax_previous += a_i * x(i, j);
104+
}
105+
}
106+
107+
return x;
108+
}
109+
110+
/**
111+
* Overload of `sum_to_zero_free()` to untransform each Eigen type
66112
* in a standard vector.
67113
* @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.
114+
* `Eigen::MatrixBase`
69115
* @param z The standard vector to untransform.
70116
*/
71117
template <typename T, require_std_vector_t<T>* = nullptr>

test/unit/math/prim/constraint/sum_to_zero_transform_test.cpp

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ TEST(prob_transform, sum_to_zero_match) {
5151

5252
EXPECT_EQ(4, y.size());
5353
EXPECT_EQ(4, y2.size());
54-
for (int i = 0; i < x.size(); ++i)
54+
for (int i = 0; i < y.size(); ++i)
5555
EXPECT_FLOAT_EQ(y[i], y2[i]);
5656
}
5757

@@ -62,3 +62,62 @@ TEST(prob_transform, sum_to_zero_f_exception) {
6262
y << 0.5, -0.55;
6363
EXPECT_THROW(stan::math::sum_to_zero_free(y), std::domain_error);
6464
}
65+
66+
TEST(prob_transform, sum_to_zero_mat_rt0) {
67+
double lp = 0;
68+
auto x = Eigen::MatrixXd::Zero(4, 3);
69+
std::vector<Eigen::MatrixXd> x_vec{x, x, x};
70+
std::vector<Eigen::MatrixXd> y_vec
71+
= stan::math::sum_to_zero_constrain<false>(x_vec, lp);
72+
EXPECT_NO_THROW(stan::math::check_sum_to_zero("checkSumToZero", "y", y_vec));
73+
74+
for (auto&& y_i : y_vec) {
75+
EXPECT_MATRIX_FLOAT_EQ(Eigen::MatrixXd::Zero(5, 4), y_i);
76+
}
77+
std::vector<Eigen::MatrixXd> xrt = stan::math::sum_to_zero_free(y_vec);
78+
EXPECT_EQ(x.rows() + 1, y_vec[2].rows());
79+
EXPECT_EQ(x.cols() + 1, y_vec[2].cols());
80+
for (auto&& x_i : xrt) {
81+
EXPECT_MATRIX_NEAR(x, x_i, 1E-10);
82+
}
83+
}
84+
85+
TEST(prob_transform, sum_to_zero_mat_rt) {
86+
Eigen::MatrixXd x(3, 4);
87+
x << 1.0, -1.0, 2.0, 1.1, -1.1, 2.1, 1.2, -1.2, 2.2, 1.3, -1.3, 2.3;
88+
89+
Eigen::MatrixXd y = stan::math::sum_to_zero_constrain(x);
90+
91+
EXPECT_EQ(x.rows() + 1, y.rows());
92+
EXPECT_EQ(x.cols() + 1, y.cols());
93+
EXPECT_FLOAT_EQ(y.sum(), 0.0);
94+
EXPECT_MATRIX_NEAR(y.rowwise().sum(), Eigen::VectorXd::Zero(4), 1E-15);
95+
EXPECT_MATRIX_NEAR(y.colwise().sum(), Eigen::RowVectorXd::Zero(5), 1E-15);
96+
EXPECT_NO_THROW(stan::math::check_sum_to_zero("checkSumToZero", "y", y));
97+
98+
Eigen::MatrixXd xrt = stan::math::sum_to_zero_free(y);
99+
100+
EXPECT_EQ(x.size(), xrt.size());
101+
EXPECT_MATRIX_FLOAT_EQ(x, xrt);
102+
}
103+
104+
TEST(prob_transform, sum_to_zero_mat_match) {
105+
Eigen::MatrixXd x(3, 4);
106+
x << 1.0, -1.0, 2.0, 1.1, -1.1, 2.1, 1.2, -1.2, 2.2, 1.3, -1.3, 2.3;
107+
108+
double lp = 0;
109+
Eigen::MatrixXd y = stan::math::sum_to_zero_constrain(x);
110+
Eigen::MatrixXd y2 = stan::math::sum_to_zero_constrain(x, lp);
111+
112+
EXPECT_EQ(y.size(), y2.size());
113+
EXPECT_MATRIX_FLOAT_EQ(y, y2);
114+
EXPECT_EQ(lp, 0);
115+
}
116+
117+
TEST(prob_transform, sum_to_zero_mat_f_exception) {
118+
Eigen::MatrixXd x(3, 4);
119+
x << 1.0, -1.0, 2.0, 1.1, -1.1, 2.1, 1.2, -1.2, 2.2, 1.3, -1.3, 2.3;
120+
auto y = stan::math::sum_to_zero_constrain(x);
121+
y(0, 0) += 0.1;
122+
EXPECT_THROW(stan::math::sum_to_zero_free(y), std::domain_error);
123+
}

0 commit comments

Comments
 (0)