From 8356623c90c1cf9d8e47032c757d8f783f646652 Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Tue, 11 Aug 2026 14:15:18 +0300 Subject: [PATCH 1/2] fix standalone generated quantities csv writer --- src/stan/services/util/gq_writer.hpp | 5 ++- .../sample/standalone_gqs_parallel_test.cpp | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/stan/services/util/gq_writer.hpp b/src/stan/services/util/gq_writer.hpp index 54d52358f69..db83bd9450a 100644 --- a/src/stan/services/util/gq_writer.hpp +++ b/src/stan/services/util/gq_writer.hpp @@ -148,7 +148,10 @@ class gq_writer { logger_.info(e.what()); throw; } - sample_writer_(values); + // drop the constrained parameters, as the std::vector overload does + plain_type_t gq_values + = values.tail(values.size() - num_constrained_params_); + sample_writer_(gq_values); } /** diff --git a/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp b/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp index 5bdc8a3f21e..3cacc8c4174 100644 --- a/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp +++ b/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp @@ -158,3 +158,41 @@ TEST_F(ServicesStandaloneGQ, genDraws_bernoulli_init_chain_id_offset) { EXPECT_EQ(from_3[1], from_1[3]); EXPECT_NE(from_1[0], from_1[1]); } + +// The multi-chain path must write only the generated quantities: one value +// per header name. Guards the Eigen `write_gq_values` overload, which used +// to emit the constrained parameters as extra leading columns. +TEST_F(ServicesStandaloneGQ, genDraws_bernoulli_multi_chain_column_count) { + Eigen::MatrixXd draws = bernoulli_fit_draws(); + std::vector sample_ss(2); + std::vector sample_writer; + sample_writer.reserve(2); + std::vector draws_vec; + for (int i = 0; i < 2; i++) { + sample_writer.emplace_back( + std::unique_ptr(&sample_ss[i]), ""); + draws_vec.push_back(draws); + } + EXPECT_EQ(stan::services::standalone_generate( + model, 2, draws_vec, 12345, interrupt, logger, sample_writer), + stan::services::error_codes::OK); + auto n_fields = [](const std::string& line) { + return std::count(line.begin(), line.end(), ',') + 1; + }; + for (int i = 0; i < 2; i++) { + std::stringstream in(sample_ss[i].str()); + std::string header; + ASSERT_TRUE(std::getline(in, header)); + EXPECT_EQ(n_fields(header), 11); // mu + y_rep.1 .. y_rep.10 + std::string line; + int rows = 0; + while (std::getline(in, line)) { + if (line.empty() || line[0] == '#' + || line.find("Elapsed Time") != std::string::npos) + continue; + EXPECT_EQ(n_fields(line), n_fields(header)) << "row " << rows; + rows++; + } + EXPECT_EQ(rows, 1000); + } +} From 01d2aba9e24dd41090185b736c1921e3787eb682 Mon Sep 17 00:00:00 2001 From: Aki Vehtari Date: Tue, 11 Aug 2026 17:47:57 +0300 Subject: [PATCH 2/2] rebase and update after related PR had been merged --- .../sample/standalone_gqs_parallel_test.cpp | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp b/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp index 3cacc8c4174..3e8fe43cfe3 100644 --- a/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp +++ b/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp @@ -92,7 +92,9 @@ Eigen::MatrixXd bernoulli_fit_draws() { } // drop the timing-dependent " Elapsed Time" line, which the test writers -// emit without a comment prefix +// emit without a comment prefix, and normalize the separator: the Eigen +// writer path emits ", " where the std::vector path emits ",", which the +// cross-path test below has to see through std::string data_lines(const std::string& csv) { std::stringstream in(csv), out; std::string line; @@ -101,6 +103,7 @@ std::string data_lines(const std::string& csv) { continue; if (line.find("Elapsed Time") != std::string::npos) continue; + line.erase(std::remove(line.begin(), line.end(), ' '), line.end()); out << line << "\n"; } return out.str(); @@ -196,3 +199,30 @@ TEST_F(ServicesStandaloneGQ, genDraws_bernoulli_multi_chain_column_count) { EXPECT_EQ(rows, 1000); } } + +// Cross-path check: the multi-chain and single-chain code paths must agree. +TEST_F(ServicesStandaloneGQ, genDraws_bernoulli_multi_chain_matches_single) { + Eigen::MatrixXd draws = bernoulli_fit_draws(); + std::vector sample_ss(num_chains); + std::vector sample_writer; + sample_writer.reserve(num_chains); + std::vector draws_vec; + for (int i = 0; i < num_chains; i++) { + sample_writer.emplace_back( + std::unique_ptr(&sample_ss[i]), ""); + draws_vec.push_back(draws); + } + EXPECT_EQ( + stan::services::standalone_generate(model, num_chains, draws_vec, 12345, + interrupt, logger, sample_writer), + stan::services::error_codes::OK); + for (int i = 0; i < num_chains; i++) { + std::stringstream single_ss; + test_writer single_writer( + std::unique_ptr(&single_ss), ""); + EXPECT_EQ(stan::services::standalone_generate( + model, draws, 12345, interrupt, logger, single_writer, 1 + i), + stan::services::error_codes::OK); + EXPECT_EQ(data_lines(single_ss.str()), data_lines(sample_ss[i].str())); + } +}