Skip to content

Commit c34d9a7

Browse files
committed
Improve rmultinom with 'Kahan summation' as R-devel does now
1 parent bfbe390 commit c34d9a7

3 files changed

Lines changed: 59 additions & 30 deletions

File tree

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
* inst/include/armadillo_bits/: Sync with Armadillo 15.2.6
44

5+
* inst/include/RcppArmadilloExtensions/rmultinom.h (rmultinom): Carry
6+
'Kahan summation' improvement from R-devel over here
7+
* inst/tinytest/test_rmultinom.R: Run one sub-test only under R-devel
8+
as improved code here corresponds to the improved code there
9+
510
2026-04-18 Dirk Eddelbuettel <edd@debian.org>
611

712
* DESCRIPTION (Version, Date): RcppArmadillo 15.2.5-1

inst/include/RcppArmadilloExtensions/rmultinom.h

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
// -*- mode: C++; c-indent-level: 4; c-basic-offset: 4; indent-tabs-mode: nil; -*-
2-
/* :tabSize=4:indentSize=4:noTabs=false:folding=explicit:collapseFolds=1: */
1+
// rmultinom.h: Rcpp/Armadillo equivalent to R's stats::rmultinom().
32
//
4-
// rmultinom.h: Rcpp/Armadillo equivalent to R's stats::rmultinom().
53
// This is intended for use in C++ functions, and should *not* be called from R.
64
// It should yield identical results to R.
75
//
8-
// Copyright (C) 2014 Christian Gunning
6+
// Copyright (C) 2014-2026 Christian Gunning
7+
// Copyright (C) 2026 Dirk Eddelbuettel and R Core
98
//
109
// This file is part of RcppArmadillo.
1110
//
@@ -29,38 +28,62 @@
2928
namespace Rcpp{
3029
namespace RcppArmadillo{
3130

32-
IntegerVector rmultinom(int size, NumericVector prob) {
31+
IntegerVector rmultinom(int size, NumericVector prob) {
3332
// meaning of n, size, prob as in ?rmultinom
3433
// opposite of sample() - n=number of draws
35-
double pp;
36-
int ii;
3734
int probsize = prob.size();
38-
// Return object
39-
IntegerVector draws(probsize);
40-
if (size < 0 || size == NA_INTEGER) throw std::range_error( "Invalid size");
41-
long double p_tot = 0.;
42-
p_tot = std::accumulate(prob.begin(), prob.end(), p_tot);
43-
if (fabs((double)(p_tot - 1.)) > 1e-7) {
44-
throw std::range_error("Probabilities don't sum to 1, please use FixProb");
45-
}
46-
47-
// do as rbinom
48-
if (size == 0 ) {
49-
return draws;
35+
IntegerVector draws(probsize); // Return object
36+
37+
if (size < 0 || size == NA_INTEGER)
38+
Rcpp::stop( "Invalid size");
39+
40+
// Using Kahan compensated summation for platform-independent accuracy
41+
// avoids relying on LDOUBLE (long double) which varies across platforms.
42+
// Code borrowed with full credits from R.
43+
double p_tot = 0.0,
44+
p_comp = 0.0; // Kahan compensation term
45+
46+
for (int ii = 0; ii < probsize; ii++) {
47+
double pp = prob[ii];
48+
if (!R_FINITE(pp) || pp < 0. || pp > 1.) {
49+
Rcpp::warning("Domain issue in rmultinom");
50+
draws[ii] = NA_INTEGER;
51+
return draws;
52+
}
53+
// Kahan summation: p_tot += pp with compensation
54+
double y = pp - p_comp,
55+
t = p_tot + y;
56+
p_comp = (t - p_tot) - y;
57+
p_tot = t;
58+
draws[ii] = 0;
5059
}
51-
//rmultinom(size, REAL(prob), k, &INTEGER(ans)[ik]);
60+
61+
if (fabs((double)(p_tot - 1.0)) > 1e-7)
62+
Rcpp::stop("Probabilities do not sum to 1, please use FixProb");
63+
if (probsize == 1 && p_tot == 0.0) // trivial border case: do as rbinom
64+
return draws;
65+
if (size == 0 ) // do as rbinom
66+
return draws;
67+
68+
// rmultinom(size, REAL(prob), k, &INTEGER(ans)[ik]);
69+
// generate first draws-1 obs via binomials
5270
// for each slot
53-
for (ii = 0; ii < probsize-1; ii++) { /* (p_tot, n) are for "remaining binomial" */
54-
if (prob[ii]) {
55-
pp = prob[ii] / p_tot;
56-
// >= 1; > 1 happens because of rounding
71+
for (int ii = 0; ii < probsize-1; ii++) { /* (p_tot, n) are for "remaining binomial" */
72+
if (prob[ii] != 0.) {
73+
double pp = prob[ii] / p_tot;
74+
// >= 1; > 1 happens because of rounding
5775
draws[ii] = ((pp < 1.) ? (int) Rf_rbinom((double) size, pp) : size);
5876
size -= draws[ii];
59-
} // else { ret[ii] = 0; }
77+
} else {
78+
draws[ii] = 0;
79+
}
6080
// all done
61-
if (size <= 0) return draws;
62-
// i.e. p_tot = sum(prob[(k+1):K])
63-
p_tot -= prob[ii];
81+
if (size <= 0) return draws;
82+
// Kahan subtraction: p_tot -= prob[k] with compensation
83+
double y = -prob[ii] - p_comp,
84+
t = p_tot + y;
85+
p_comp = (t - p_tot) - y;
86+
p_tot = t;
6487
}
6588
// the rest go here
6689
draws[probsize-1] = size;

inst/tinytest/test_rmultinom.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ tests <- list(vanilla=list( n=5, size=100, prob=rep(1/10,10)),
3838
size0=list( n=10, size=0, prob=1:10)
3939
)
4040
## In April 2026, R-devel improved rmultinom() vai a more precise calculation
41-
## Our calculation here corresponds to the prior version and this test is affected
42-
if (getRversion() < "4.7.0")
41+
## The routine in RcppArmadilloExtensions has been updated to but we can only
42+
## compare with R-devel (i.e. R 4.7.0 or later)
43+
if (getRversion() >= "4.7.0")
4344
tests[["big"]] <- list(n=5, size=1e6, prob=rep(1/1e3,1e3))
4445

4546

0 commit comments

Comments
 (0)