From 5ad52452c6f18da800802801b91da2e6fbf33934 Mon Sep 17 00:00:00 2001 From: Treston Cuzzort Date: Mon, 26 Oct 2026 03:40:30 -0500 Subject: [PATCH 1/2] Handle degenerate negative-binomial parameters before sampling Return zero without using the engine when k is zero or p is one. Clarify that positive k requires p > 0 and assert that precondition before computing the gamma scale. Preserve the interior sampling path. Cover the boundary cases through construction, parameter changes, explicit parameters and streaming for several integer and real types. Generated-by: OpenAI Codex --- .../random/negative_binomial_distribution.hpp | 10 ++- test/test_negative_binomial_distribution.cpp | 86 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/include/boost/random/negative_binomial_distribution.hpp b/include/boost/random/negative_binomial_distribution.hpp index 31dedb64c6..e95d2d3adb 100644 --- a/include/boost/random/negative_binomial_distribution.hpp +++ b/include/boost/random/negative_binomial_distribution.hpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -32,6 +33,8 @@ namespace random { * \f$\displaystyle P(i) = {k+i-1\choose i}p^k(1-p)^i\f$. * * This implementation uses a gamma-poisson mixture. + * When @c k is zero or @c p is one, the result is always zero. + * For positive @c k, @c p must be strictly positive. */ template class negative_binomial_distribution { @@ -46,7 +49,7 @@ class negative_binomial_distribution { * Construct a param_type object. @c k and @c p * are the parameters of the distribution. * - * Requires: k >=0 && 0 <= p <= 1 + * Requires: k >=0 && 0 <= p <= 1 && (k == 0 || p > 0) */ explicit param_type(IntType k_arg = 1, RealType p_arg = RealType (0.5)) : _k(k_arg), _p(p_arg) @@ -94,7 +97,7 @@ class negative_binomial_distribution { * Construct a @c negative_binomial_distribution object. @c k and @c p * are the parameters of the distribution. * - * Requires: k >=0 && 0 <= p <= 1 + * Requires: k >=0 && 0 <= p <= 1 && (k == 0 || p > 0) */ explicit negative_binomial_distribution(IntType k_arg = 1, RealType p_arg = RealType(0.5)) @@ -116,6 +119,9 @@ class negative_binomial_distribution { template IntType operator()(URNG& urng) const { + if(_k == IntType(0) || _p == RealType(1)) + return IntType(0); + BOOST_ASSERT(_p > RealType(0)); gamma_distribution gamma(_k, (1-_p)/_p); poisson_distribution poisson(gamma(urng)); return poisson(urng); diff --git a/test/test_negative_binomial_distribution.cpp b/test/test_negative_binomial_distribution.cpp index c6cd6c9172..7e381c4ec1 100644 --- a/test/test_negative_binomial_distribution.cpp +++ b/test/test_negative_binomial_distribution.cpp @@ -35,3 +35,89 @@ #define BOOST_RANDOM_TEST2_MIN 50 #include "test_distribution.ipp" + +namespace { + +struct counting_engine { + typedef boost::uint32_t result_type; + + counting_engine() : calls(0) {} + static result_type min() { return 0; } + static result_type max() { return (std::numeric_limits::max)(); } + result_type operator()() { ++calls; return 123; } + + unsigned calls; +}; + +template +void check_degenerate(IntType k, RealType p) +{ + typedef boost::random::negative_binomial_distribution distribution; + typedef typename distribution::param_type param_type; + counting_engine engine; + const param_type param(k, p); + distribution direct(k, p); + distribution from_param(param); + distribution assigned; + assigned.param(param); + distribution explicit_param(5, RealType(0.5)); + distribution streamed; + std::stringstream stream; + stream << direct; + stream >> streamed; + BOOST_REQUIRE(!stream.fail()); + + for(unsigned i = 0; i < 4; ++i) { + BOOST_TEST(direct(engine) == IntType(0)); + BOOST_TEST(from_param(engine) == IntType(0)); + BOOST_TEST(assigned(engine) == IntType(0)); + BOOST_TEST(explicit_param(engine, param) == IntType(0)); + BOOST_TEST(streamed(engine) == IntType(0)); + } + BOOST_TEST(engine.calls == 0u); + BOOST_TEST(explicit_param.k() == IntType(5)); + BOOST_TEST(explicit_param.p() == RealType(0.5)); +} + +template +void check_zero_target() +{ + const RealType probabilities[] = { + RealType(0), (std::numeric_limits::min)(), + RealType(0.25), RealType(0.5), RealType(0.75), RealType(1) + }; + for(unsigned i = 0; i < sizeof(probabilities) / sizeof(probabilities[0]); ++i) + check_degenerate(IntType(0), probabilities[i]); +} + +template +void check_unit_probability() +{ + const IntType targets[] = { + IntType(0), IntType(1), IntType(10), (std::numeric_limits::max)() + }; + for(unsigned i = 0; i < sizeof(targets) / sizeof(targets[0]); ++i) + check_degenerate(targets[i], RealType(1)); +} + +} + +BOOST_AUTO_TEST_CASE(test_zero_target) +{ + check_zero_target(); + check_zero_target(); + check_zero_target(); + check_zero_target(); + check_zero_target(); + check_zero_target(); +} + +BOOST_AUTO_TEST_CASE(test_unit_probability) +{ + check_unit_probability(); + check_unit_probability(); + check_unit_probability(); + check_unit_probability(); + check_unit_probability(); + check_unit_probability(); +} From 117e3442274244b53ff172dec7c0f6cab6561950 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Tue, 3 Nov 2026 13:51:52 -0500 Subject: [PATCH 2/2] Fix mac testing failures --- test/test_negative_binomial_distribution.cpp | 35 ++++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/test/test_negative_binomial_distribution.cpp b/test/test_negative_binomial_distribution.cpp index 7e381c4ec1..02e2486c27 100644 --- a/test/test_negative_binomial_distribution.cpp +++ b/test/test_negative_binomial_distribution.cpp @@ -10,7 +10,10 @@ */ #include +#include +#include #include +#include #define BOOST_RANDOM_DISTRIBUTION boost::random::negative_binomial_distribution<> #define BOOST_RANDOM_ARG1 k @@ -38,14 +41,17 @@ namespace { -struct counting_engine { - typedef boost::uint32_t result_type; +// Forwards to a real engine so non-degenerate draws terminate, and counts every call +struct counting_engine +{ + typedef boost::random::minstd_rand::result_type result_type; - counting_engine() : calls(0) {} - static result_type min() { return 0; } - static result_type max() { return (std::numeric_limits::max)(); } - result_type operator()() { ++calls; return 123; } + counting_engine() : calls {0} {} + static result_type min() { return (boost::random::minstd_rand::min)(); } + static result_type max() { return (boost::random::minstd_rand::max)(); } + result_type operator()() { ++calls; return base(); } + boost::random::minstd_rand base; unsigned calls; }; @@ -63,7 +69,8 @@ void check_degenerate(IntType k, RealType p) distribution explicit_param(5, RealType(0.5)); distribution streamed; std::stringstream stream; - stream << direct; + // At default precision min() prints as a subnormal, which libc++ fails to read back + stream << std::setprecision(std::numeric_limits::max_digits10) << direct; stream >> streamed; BOOST_REQUIRE(!stream.fail()); @@ -121,3 +128,17 @@ BOOST_AUTO_TEST_CASE(test_unit_probability) check_unit_probability(); check_unit_probability(); } + +// Positive control: a non-degenerate draw must consume the engine, +// so the engine.calls == 0 checks above would catch a sampling call +BOOST_AUTO_TEST_CASE(test_non_degenerate_uses_engine) +{ + counting_engine engine; + boost::random::negative_binomial_distribution<> dist {1, 0.5}; + for (unsigned i {0}; i < 4; ++i) + { + const int value {dist(engine)}; + BOOST_TEST(value >= 0); + } + BOOST_TEST(engine.calls > 0u); +}