Changelog
Source:NEWS.md
gkwdist (development version)
- Shorter examples on the help pages of all seven families. Each shows what the function returns and checks it: the nesting identities,
p*()as the integral ofd*(),q*()invertingp*(), and the analytic gradients and Hessians againstnumDeriv. They run in under a second.
gkwdist 1.1.7
Bug Fixes
Log-space chain, subnormal band (
utils.h,gkw.cpp,bkw.cpp,kkw.cpp): the underflow bridge fired only at an exact 0, so forx^alphain[5e-324, 2.2e-308)ll*,gr*andhs*were silently wrong.llbkw(c(161.8, 2, 1.5, 1), c(.01, .3, .6, .9))was 1522.87 against a true 1523.21, and the gradient was off by up to 16%. The bridge now covers the band. Ordinary data is bit-identical.EKw had no bridge (
ekw.cpp):dekw(),llekw(),grekw()andhsekw()returned-Inf,+InforNaNwhere the nested GKw is finite, andoptim()stopped on them. They now matchdgkw(gamma = 1, delta = 0).Tails flushed to 0 or 1 in the p, q and r functions of GKw, BKw, KKw, EKw, Kw and Mc.
pgkw(1e-9, 40, 2, 0.05, 0.5, 0.1)returned 0 (true 0.0164), andrgkw()drew exact zeros thatllgkw()then rejected. The same held in the deep upper tail on the log scale (log.p = TRUE, log probabilities below about -718). RNG streams are otherwise unchanged.Upper tail of
qgkw()andqmc(): both now reflect above y = 1/2, asqbkw()already did.qgkw(1e-26, 2, 3, 1.5, 0.5, 1.2, lower.tail = FALSE)returned exactly 1.pgkw()andpbkw()withlog.p = TRUEreturned 0 near 1 for a tiny negative log-probability.hsgkw()rebuilt in log space: it returnedNaNwherellgkw()andgrgkw()are finite (e.g.beta = 200). It is now finite there, matchesnumDerivto 5e-9, equalshskkw()atgamma = 1, and is about 2x faster.Memory leak on caught warnings: a warning raised from C++ and caught by
tryCatch()oroptions(warn = 2)skipped the C++ destructors (308 MB over 20 calls on 2e6 values). Warnings now unwind cleanly; messages are unchanged.gkwgetstartvalues(family = "beta")started fromBeta(gamma, delta + 2)instead ofBeta(gamma, delta + 1). Starting values are now the same on every compiler.Missing data give the documented value in all seven families:
+Inffromll*(),NaNfromgr*()andhs*().safe_exp()no longer returnsInffor results betweenDBL_MAX / 10andDBL_MAX.
gkwdist 1.1.6
Numerical Utilities
-
gkw_log1mexp()had the sign of its second-order correction inverted (utils.h): the Taylor branch, used for-1e-14 < u <= 0, returnedlog(-u) - u/2where the expansion giveslog(-u) + u/2. Since1 - exp(u) = -u (1 + u/2 + u^2/6 + ...), the correction islog(1 + u/2) ~ +u/2, and the derivation in the comment above the line carried the same slip. Withu < 0the two forms differ by|u|. Against a 400-digit reference:u reference err before err after -9.9e-15 -32.246241637770147 1.42e-14 0 -5.0e-15 -32.929338482476588 0 0About 1.4 ulp at this magnitude, but in the wrong direction, and it left a step where the function crosses into the
expm1branch – breaking the “relative error < 2*EPSILON” the header promises.Downstream the effect is at the noise floor, and is reported as such: 56 of 238,140 grid values move, by at most 2.3e-13 relative, and adjudicating the changed densities against a 400-digit reference gives 4 closer and 2 farther, all between 1e-15 and 2e-14. The defensible claim is the direct one, on the function itself.
safe_pow()cast adoubleexponent tointwithout a range check (utils.h): undefined behaviour once|y| > INT_MAX, which UBSan flags.vec_safe_pow()had a guard for it, but that guard answered the parity question wrongly in the process, reporting “even” for every|y|aboveINT_MAX– and an odd integer betweenINT_MAXand2^53is exactly representable as adouble. Both now take the parity fromfmod(|y|, 2) == 1, which is correct at every magnitude: above2^53everydoubleis even, andfmodsays so.-
safe_pow()’s documentation claimed an accuracy it does not have (utils.h): it said theexp(y * log(x))form “provides better numerical stability than directpow()”. The reverse is true. That form carries a relative error of roughly|y log x| * EPSILON, whilestd::powon a conforming libm is very nearly correctly rounded. Measured against a 60-digit reference:x y exp(y*log x) std::pow 10 100 1.11e-14 0 10 300 9.00e-14 0 2 1000 6.85e-14 0The note now says what the form is actually for – intercepting overflow and underflow before they happen – and points callers who do not need that to
std::pow.
Base R Contract
-
The
@returncontract saidNaN, the wrappers saidstop()(all 28d*,p*,q*andr*functions): the audit asked for one contract chosen and propagated.stop()is the one kept – it is what the package has shipped for its whole CRAN life, a named error is a better diagnostic than aNaNfor the case that actually occurs, and switching toNaNwould break everytryCatch(..., error = )guard written against it. All 28@returnblocks now describe the behaviour the functions have.gr*()andhs*()genuinely do returnNaN, and their 14 blocks are unchanged.Documentation only. No guard, message or value changes.
Known and deliberately left open: an infinite parameter is not intercepted by the wrapper –
any(alpha <= 0)isFALSEforInfandanyNA()does not catch it – so it reaches C++, wherecheck_*_pars()has always rejected it, and the routine leaves its fill value. Tightening the R guard is correct in principle but is a behaviour change with a reverse-dependency cost:gkwreg::predict()clamps only the lower bound of its linear predictor, soexp(eta)can overflow toInfon extrapolatednewdata, and one such row would abort the whole prediction vector. The value it gets today is 0, which is the correct limit –Kw(alpha, beta)concentrates at 1 asalphagrows – so nothing is currently wrong, only silent. An upper clamp ingkwregis the prerequisite. The current behaviour is pinned intests/testthat/test-return-contract.Rso the eventual change is visible. Only two of seven
ll*()warned about data outside the open support (llekwandllbeta):ll*()returns+Infthere – an infinite objective with no gradient direction, which an optimiser can only sit on – and five of the seven announced a corrupted sample not at all. All seven warn now.-
Rcpp::warning()fired once per element inside the vectorised loops (dgkw,pgkw,qgkw,rgkw, and the six nestedr*): on 50,000 values with half the parameters invalid that cost 51x, and underoptions(warn = 2)each call longjmps out of the loop through C++ frames holding live Armadillo objects. R’s own convention is a single warning per call. Each routine now sets a flag and warns once after the loop.50,000 values, half invalid before after elapsed 0.514 s 0.005 s warnings raised 25,000 1The message now also names what the routine actually returns, which base R pairs up:
dbeta(0.5, -1, 1)isNaNand warns “NaNs produced”, whilerbeta(2, -1, 1)isNAand warns “NAs produced”.p*,q*andr*fillNA_REALand say so.dgkw()leaves its fill value, 0, so it says “invalid parameters” and claims no return value at all – promising aNaNthat is not there is the defect this release fixed forq*().Values are bit-identical over the 238,140-value regression grid. The path is reachable from the exported API only through an
Infparameter, since every wrapperstop()s on<= 0,NAandNaN.Known inconsistency:
man/dgkw.Rdsaid the function returnsNaNfor invalid parameters; it returns 0, and the other six families document and return 0 too. The@returncontract entry above corrects the documentation. -
d*()returned 0 atx = 0andx = 1, where base R returns the limit (all seven families): the GKw support is the open interval, but base R’s density functions carry the limiting value at the closed boundary –dbeta(0, 0.5, 1)isInfanddbeta(1, 2, 1)is 2 – and any code that plots a density across[0, 1]depends on it. Curves fell to zero exactly where they should have diverged.Substituting the first-order forms the log chain already uses, the log-density at each boundary collapses to a constant plus one power of the vanishing quantity, so a single exponent decides the answer:
at x = 0: alpha*gamma*lambda - 1 at x = 1: beta*(delta + 1) - 1 > 0 -> 0 = 0 -> the constant < 0 -> InfEach nested family reaches this through its own fixed parameters. The rule was checked against
stats::dbetaat all ten combinations the Beta parameterisation can express, and against the nesting identities at both boundaries for the other six.dkw(0, 0.5, 1) 0 -> Inf dbeta_(1, 2, 0) 0 -> 2Anything strictly outside
[0, 1]is still 0, as in base R, and the interior is bit-identical over the 238,140-value regression grid. -
d*(),p*()andq*()droppeddim,dimnamesandnames(all seven families): base R carries the first argument’s attributes through to the output, so code that indexes or plots the result by shape keeps working.before after stats::dbeta dim(d(matrix(x, 2, 2), 2, 3)) NULL 2 2 2 2 names(d(c(a = .2, b = .5), 2, 3)) NULL a b a bThe copy is conditional on the lengths agreeing, which is also what base R does: once a recycled parameter makes the output longer than the first argument,
dim()isNULLin both. Values are bit-identical over the 238,140-value regression grid. r*(0)raised an error instead of returningnumeric(0)(all seven families):stats::rbeta(0, 2, 3)isnumeric(0), and a generator that errors instead breaks any loop orreplicate()that reaches an empty case. All seven now returnnumeric(0). A negative or missingnis still an error, as in base R.
Critical Bug Fixes
-
pmc()reachedR::pbetathroughexp(lambda * log(x)), which is not a round trip (bpmc.cpp): forlambda = 1the Mc family is the Beta family, andpmc(x, gamma, delta, 1)should beR::pbeta(x, gamma, delta+1)exactly. It was not:exp(1 * log(x))fails to returnxfor 2 of 9 ordinary values under glibc, and for a different pair under the macOS ARM64 libm, which is where CI caught it.std::powis used instead. C99 requirespow(x, 1.0) == x, so the identity now holds bit-for-bit on every platform, and the form is also the more accurate one for every otherlambda. Against a 60-digit reference over 1,030 grid points:lambda exp(l*log x) std::pow improved worse 1.5 1.19e-14 1.67e-16 403 3 0.1 6.57e-16 1.64e-16 56 0 2.5 1.57e-14 1.11e-16 527 0Only
pmcmoves; 1,093 of 238,140 grid values, all inp mc. -
The upper tail of
pgkw()andpbkw()collapsed to exactly zero (gkw.cpp,bkw.cpp): the CDF batch movedlower.tailandlog.pontoR::pbetainstead of applying them afterwards, which fixed the lower tail. A second defect remained, in the argument rather than the tail flag.pgkw()formsy = [1 - (1 - x^alpha)^beta]^lambdaand evaluatesI_y(gamma, delta+1). Asxapproaches 1 the exponentlambda*log_wfalls below 1.1e-16,exp()returns exactly 1, andR::pbeta(1, ., ., lower = FALSE)returns exactly 0.pbkw()reaches the same place through-expm1of an exponent running off to-Inf. Against a 300-digit incomplete beta, forpgkw(x, 2, 3, 1.5, 2, 0.8, lower.tail = FALSE):1-x returned exact rel err 1e-4 5.731692e-34 5.731820e-34 2.2e-05 1e-5 5.840671e-43 5.734142e-43 1.9e-02 1e-6 0 5.734374e-52 1.00 <- collapse 1e-16 0 1.469535e-141 1.00The true tail is still representable sixteen decades past the point where the routine gave up, and
lower.tail = FALSEis ordinary documented API, so survival probabilities, p-values and quantile residuals were silently zero.I_y(a,b) = 1 - I_{1-y}(b,a)is exact, and1 - yis-expm1of the same exponent that producesy, at full relative accuracy. Reflecting abovey = 1/2sends the small quantity intopbetaand the large one out of it; below that crossover the direct form already holds the small quantity and is left alone. This is the same correctionpmc()received in this release.After the fix the maximum relative error over those fifteen decades is 6.1e-14 for
pgkw()and 5.3e-14 forpbkw(). Confinement over the 238,140-value grid: onlypgkwandpbkwmove, only withlower.tail = FALSE, and nod*orq*value changes at all. The nesting identity againstpmc()– corrected independently, in another translation unit – goes from 4.88e-01 to 2.84e-14. -
NA,NaNand infinite input did not propagate (all seven families, 21 routines):NA_REALis aNaN, so every comparison against it is false and!R_finite()is true. Missing input therefore fell into the “outside the support” branch and was silently replaced by the fill value:before after base R d*(NA) 0 NA NA d*(NaN) 0 NaN NaN p*(NA) 0 NA NA p*(NaN) 0 NaN NaN p*(+Inf) 0 1 1 p*(-Inf) 0 0 0p*(Inf) = 0also violated monotonicity outright:pkw(2, 2, 3)was 1 whilepkw(Inf, 2, 3)was 0. The fix needed no new branch – dropping!R_finite()from the lower boundary test lets+Inffall through to theq >= 1case that was already there.NAandNaNare distinguished, as base R distinguishes them:R_IsNA()is asked beforeR_IsNaN(), sois.na()andis.nan()both answer correctly. -
q*(p)saturated at 0 or 1 for probabilities outside[0, 1](all seven families): the R wrappers have always warned that such a value “will produce NaN”, while the C++ returned a bound. Defensive code testingis.nan()– exactly what the warning tells the caller to expect – saw nothing, and 0 and 1 are outside the open support, so the value flowed on intod*()andll*()as valid data.qkw(-0.5, 2, 3) 0 -> NaN qkw(1.5, 2, 3) 1 -> NaN qkw(-Inf, 2, 3) 0 -> NaN qkw(Inf, 2, 3) 1 -> NaNThe closed boundary is unchanged:
q*(0)is still 0 andq*(1)still 1, in both tails and on both scales.Confinement was checked by bit-comparison rather than adjudication, since no in-range value should move: 238,140 values over the seven families, both tails, both scales, and x from 1e-320 to 1 - 1e-16 are bit-identical.
-
log,lower.tailandlog.psilently acceptedNAand read it asTRUE(all seven families, 35 guards): the check was!is.logical(flag) || length(flag) != 1, andNApasses both halves –is.logical(NA)isTRUEandlength(NA)is 1. The value reached C++ asNA_LOGICAL, which is a non-zero integer, so it was read asTRUE:dgkw(0.5, 2, 3, 1.5, 0.5, 2, log = NA) 0.8517722 <- the LOG density dgkw(0.5, 2, 3, 1.5, 0.5, 2) 2.343797 <- the densityThe documented error is now raised, with the message the help pages already promised.
-
An
NAshape parameter behaved three different ways (all seven families, 92 guards):gkw,bkw,kkw,mcandkwwroteany(alpha <= 0), soifreceivedNAand R raised its own opaque “missing value where TRUE/FALSE needed” instead of the documented message;ekwandbeta_wroteany(alpha <= 0, na.rm = TRUE), which dropped theNAand returned 0 as though the call had been valid. All seven now raise the package’s own error:dkw(0.5, NA, 3) before Error: missing value where TRUE/FALSE needed dekw(0.5, NA, 3, 1) before 0Both now raise the package’s own error.
Valid calls are unaffected:
dgkw(0.5, 2, 3, 1.5, 0.5, 2)is still 2.343797, anddelta = 0is still accepted. -
grkkw()andhskkw()failed silently and only partly (kkw.cpp): alone among the seven families, they carried none of the guards their BKw counterparts have. A refused input – a short parameter vector, an invalid parameter, data outside(0,1)– returned aNaNresult with nothing said, so the caller could not tell a rejected call from a genuine boundary. Neither function checked its result at all, so where the chain did reach a boundary they returned whatever survived:par = (1e-8, 1e-8, 0, 1e300), x = c(0.01, 0.3, 0.6, 0.9) grkkw 1.378417e+307 -Inf -2.000000 31.43853 (no warning) hskkw 2 of 16 entries NaN, 14 finite (no warning) grbkw at the corresponding BKw point: warns, and every component is NaNA gradient with a finite component next to an
-Inf, or a Hessian with twoNaNentries among fourteen, is worse than no answer: it looks partly usable, and the Hessian would have been inverted for a standard error. Both now warn and return a uniformlyNaNresult, matchinggrbkw()andhsbkw().The support test also gained
has_nan(). ANaNcompares false against both bounds, soNaNdata passed straight through;hskkw()returned a matrix with fifteenNaNentries and one finite one.This is a reporting change only. Over the same 43,792-value grid used above, no value differs from the previous fix – every case the guards catch was already
NaNorInf. -
The BKw and KKw likelihoods collapsed to
+Infon ordinary data (bkw.cpp,kkw.cpp): both families walk the same log-space chain as their GKw parent –v = 1 - x^alpha,w = 1 - v^beta,z = 1 - w^lambda, withlambda = 1for BKw andgamma = 1for KKw – and both stopped at the point wherevunderflows to exactly 1.log1mexp()then receives an argument of exactly 0 and can only answer-Inf. The threshold isalpha*log(min(x)) < -745, which perfectly ordinary data crosses:x = c(0.01, 0.3, 0.6, 0.9) alpha = 161 llbkw(c(a,2,1.5,1), x) = 1515.5261017902 llkkw(c(a,2,1,1.5), x) = 1516.4186759096 alpha = 162 llbkw = Inf llkkw = Inf alpha = 200 llbkw = Inf llkkw = InfEvery larger
alphastayed atInf, so the likelihood surface carried an infinite plateau that an optimiser cannot leave. The true values, 1525.13 and 1526.03 atalpha = 162, are now returned.The same boundary reached the density, the gradient and the Hessian.
dbkw()anddkkw()dropped such observations and returned the fill value (-Infin log, 0 otherwise). The derivatives built the ratiosv^beta/wandw^lambda/zas separate factors; each overflowed to+Infon its own while the quantity it multiplied had underflowed to 0, and0 * InfisNaN:par = (200, 2, 1.5, 1), x = c(.01, .30, .60, .90) grbkw NaN NaN NaN NaN grgkw(lambda=1) 9.617994 -3.000000 1278.026571 -2.721489 par = (200, 2, 1, 1.5), x = c(.01, .30, .60, .90) grkkw NaN NaN -2 NaN (partly NaN, and silently so) grgkw(gamma=1) 9.617994 -3.000000 -2.000000 1279.626571Both files now use the first-order limits that are exact to the last representable bit – as
x -> 0,log_w = log(beta) + alpha*log(x); asx -> 1,log_z = log(lambda) + beta*log_v– keep every ratio inside a singleexp()of a sum of logs, and never let a coefficient of exactly zero multiply a logarithm. This is the same repairgkw.cppreceived, so the nesting identitiesBKw(a,b,g,d) == GKw(a,b,g,d,1)andKKw(a,b,d,l) == GKw(a,b,1,d,l)now hold where they previously did not.Over a grid of 19,488 values spanning
xfrom 1e-300 to1 - 1e-16andalphafrom 0.01 to 5000, 1,966 results changed fromNaN/Infto a finite value and none went the other way. 1,089NaNHessian entries – 71 whole unusable matrices – became finite. Adjudicated against a 400-digit reference and against the GKw parent: the maximum relative error fell from infinite to 1.8e-07 for the log-likelihoods and 1.4e-04 for the gradients, both attained only atalpha >= 1000where double precision has no digits left to lose. No value that was already exactly correct changed; values correct to within one ulp rose from 100 to 152 (log-likelihood), 348 to 479 (gradient) and 1,337 to 1,716 (Hessian).pbkw(),qbkw(),pkkw()andqkkw()are untouched and bit-identical. -
pmc()’s upper tail was quantised by the argument it handed toR::pbeta(bpmc.cpp):F(x) = I_{x^lambda}(gamma, delta+1), andpmc()formedx^lambdain linear arithmetic. Oncex^lambdapasses 1/2 a double holds it no more finely than 1.1e-16, and the upper tail is a function of1 - x^lambdaalone, so it was quantised to whatever that left – and to exactly 0 oncex^lambdareached 1:pmc(x, 4, 2, 0.25, lower.tail = FALSE) before exact x = 1 - 1e-15 2.1895288505e-46 3.1175127579e-46 x = 1 - 1.1e-16 0 4.2764235361e-49a relative error of 700%, then of 100%.
I_y(a,b) = 1 - I_{1-y}(b,a)is exact, and1 - x^lambdacomes from-expm1of the same exponent at full relative accuracy, so reflecting sends the small quantity intopbeta. The reflection is applied only where the direct form is the one holding the large quantity: the lower tail never changes, and neither does any upper tail withx^lambda <= 1/2.Over 8942 grid cells whose exact tail is representable at all, 8494 are bit-identical, 353 improved and 76 moved the other way. The maximum relative error fell from 7.00 to 1.36e-13, and that residual sits on a cell this change did not touch. The 76 that moved the other way went from at most 2.67e-14 to at most 4.10e-14 relative, which is
R::pbeta’s own accuracy at tail probabilities of 1e-71: both routes hand it an argument good to one ulp there, and they round differently.dmc(),qmc(),rmc(),llmc(),grmc()andhsmc()are bit-identical, as is every lower tail and everylambda = 1result, which stays identical tostats::pbetato the bit. -
grmc()andhsmc()swappedR::digammaandR::trigammafor two-term asymptotic expansions above three separate thresholds (bpmc.cpp):gamma > 100,delta > 100andgamma + delta > 100.log(z) - 1/(2z)truncates psi’s expansion before the1/(12z^2)term and is wrong by 8.33e-06 atz = 100and by 1.30e-03 atz = 8, which thegamma + deltathreshold can reach withgammathat small;1/z + 1/(2z^2)drops psi’-s1/(6z^3)term and is wrong by 1.67e-07 atz = 100.Each threshold put a step of
ntimes that error into a different component, at a different place. On the seven observationsc(.1,.25,.4,.5,.6,.75,.9):grmc(c(gamma, 3, 1), x)[1] gamma = 99.999 5.9262333798 gamma = 100.001 5.9262971512a jump of 6.38e-05, of which 5.83e-05 is discontinuity rather than slope; the step scales with
nand reaches 0.018 atn = 2160. The largest step between neighbouringgammafalls from 6.11e-05 to 2.72e-06 in the gradient and from 1.22e-06 to 5.38e-08 inH[gamma, gamma].R::digammaandR::trigammaare accurate to 1e-16 over the whole range, so the substitution bought nothing.Over 330 gradient cells the maximum relative error fell from 3.85e-04 to 1.07e-12 and no cell got worse; over 660 Hessian cells 105 improved and 20 moved the other way. Ten of those twenty are
H[gamma, delta]moving by one ulp. The other ten areH[gamma, gamma]andH[delta, delta]atgammaordelta = 1e12, where the entry is a difference of two psi’ values that agree to eleven digits: the result, around 1.8e-23, can carry no better than 1e-04 relative in double precision whatever psi’ returns, and the measured relative error moves from 1.9e-05 to 7.2e-04, both inside that floor. The smooth asymptotic form landed inside it by luck at that one point while being 5.1e-04 wrong and discontinuous at the far more plausiblegamma = 100.dmc(),pmc(),qmc(),rmc()andllmc()are bit-identical. -
llmc()swappedR::lbetafor a difference oflgammaabovegamma = 100ordelta = 100(bpmc.cpp): that difference is the cancellationR::lbetaexists to avoid. Atgamma = 1e12,delta = 2the two outerlgammavalues are 2.66e13, where one ulp is 3.9e-03, so their difference cannot resolve an answer of -82.2 any better than that:R::lbeta(1e12, 3) -82.1999161672287 (exact) lgamma(1e12) + lgamma(3) - lgamma(1e12+3) -82.203125 (off by 3.2e-03)dmc()andllbeta()always calledR::lbeta, sollmc()also disagreed with-sum(dmc(..., log = TRUE)), the objective it is supposed to be, and withllbeta()atlambda = 1, where the two are the same model:llmc(c(1e12, 2, 1), 1 - 1e-12) before -25.9371543959 exact -25.9378518132 llmc(c(2, 1e12, 1), 1e-12) before -26.6306976341 exact -26.6310211159R::lbetais now called at everygammaanddelta. Over 110 likelihood cells the maximum error fell from 16283 ulps of the working magnitude to 3.00, which is where the branch-free regimes already sat; 91 cells are bit-identical, 16 improved and 2 moved by at most 2 ulps.dmc(),pmc(),qmc(),rmc(),grmc()andhsmc()are bit-identical. -
dmc()lost the density asxapproached 1 (bpmc.cpp): it formedx^lambdain linear arithmetic and then tooklog(1 - x^lambda). Doubles are spaced 2.2e-16 apart just below 1, so1 - x^lambdacarries an absolute error of one ulp of 1 however small it truly is, andx^lambdarounds to exactly 1 once1 - xdrops under about 1e-16, at which point a guard returned a density of zero.Mc(gamma, delta, lambda)isGKw(1, 1, gamma, delta, lambda), sodgkw()withalpha = beta = 1is the same density computed a different way, and it was already right:gamma = 1.5, delta = 2, lambda = 0.8 exact (400 digits) dgkw(x,1,1,..) dmc x = 1 - 1e-13 -58.65464965 -58.65464965 -58.65409479 x = 1 - 1e-15 -67.86721101 -67.86721101 -67.92355276 x = 1 - 1e-16 -72.26166017 -72.26166017 -71.81537306log(1 - x^lambda)now goes throughgkw_log1mexp(lambda * log(x)), the helperdgkw()already uses, and the guard is gone.llmc()andgrmc()carried the mirror image of the same defect at the other end of the support:log(-expm1(u))has to represent a number just below 1 and so reportedlog(1 - x^lambda)as a multiple of 1.11e-16 – usually as exactly 0 – for everyx^lambdaunder one ulp. Atdelta = 1e12the missing term is worth 2e-05 nats an observation. All four functions now sharegkw_log1mexp().Adjudicated against a 120-digit
decimalreference over 9130 density cells (22 parameter settings x 415 quantiles from 5e-324 to 1 - 1.1e-16): 8424 cells are bit-identical, 512 improved, 143 moved by at most 4 ulps of the working magnitude, and the maximum error fell from infinite – six cells returned-Inffor a finite density – to 10 ulps. The nesting identitydmc(x, g, d, l) == dgkw(x, 1, 1, g, d, l)closed from 9.8e13 ulps to 10.pmc(),qmc(),rmc(),hsmc(),dgkw(),llgkw()andllbeta()are bit-identical across the whole grid. -
dgkw(),llgkw()andgrgkw()broke down along the same log-space chain (gkw.cpp): all three walkv = 1 - x^alpha,w = 1 - v^beta,z = 1 - w^lambda, and each lost the chain in its own way.llgkw()computedlog(x^alpha)asvec_safe_log(vec_safe_pow(x, alpha)), a round trip that both lost digits and made it disagree withdgkw(), which already usedalpha * log(x).Two of the three transformations underflow to a boundary that
log1mexp()cannot recover from: its argument arrives as exactly 0 andlog(1 - exp(0))is-Inf. With a zero coefficient in front –delta = 0, orgamma*lambda = 1–0 * -InfisNaN:llgkw(c(1, 300, 1, 0, 1), c(.8, .85, .9, .95)) NaN, for an exact 2609.84Both regimes have a first-order limit that is exact to the last representable bit: as
x -> 0,log_w = log(beta) + alpha*log(x); asx -> 1,log_z = log(lambda) + beta*log_v. These are now used where the direct form underflows, and a coefficient of exactly zero never multiplies a logarithm.grgkw()built1/v,1/wand1/zas separate reciprocals, each of which overflowed on its own long before the product it belonged to was large:par = (1, 70, 1.5, 2, 1), x = c(.10, .25, .40, .72, .99) llgkw 1386.78983044 (finite, correct) grgkw NaN NaN NaN NaN NaN numDeriv::grad -662.27 20.27 -6.76 472.41 -15.00Correcting
LOG_DBL_MAXin 1.1.6 moved that boundary out by 2.3x but did not remove it;beta = 70recovered whilebeta = 200still failed. Every ratio is now a singleexp()of a difference of logs, so only the difference has to be representable, not the reciprocal.Over 96 parameter/data blocks:
llgkw()was non-finite in 30 and is now non-finite in none;grgkw()returnedNaNin 30 and now in none. Adjudicated against a 900-digit reference and againstgrbkw(), an independent implementation of the same gradient atlambda = 1: the maximum error inllgkw()fell from 8.7e-05 to 7.5e-16, andgrgkw()agrees withgrbkw()to 5.4e-08.dgkw()dropped 812 spurious-Inflog-densities across eight parameter settings while leaving every already-finite value bit-identical.Note on references:
numDerivis not usable as an arbiter in part of this region. Forbeta = 1000the intermediatelog_wbecomes subnormal, with 15 significant bits left, andlambda * log_wis then bit-identical forlambda = 1 +/- 1e-6– the finite difference sees no dependence at all and reports alambdacomponent short by exactlydelta/lambda. The analytic value is correct there;grbkw()and the 900-digit reference confirm it. -
grkw()andhskw()were not the gradient and Hessian ofllkw()(kw.cpp):kw.cppwas the last family file whose derivatives were still evaluated in linear arithmetic. It formedv = 1 - x^alphaand then appliedarma::clamp(v, eps, 1 - eps)witheps = 2.22e-14, freezinglog(v)at-31.4384832for every observation near 1 regardless of the data:par = (0.5, 2), x = 1 - 1e-14 d/dalpha d/dbeta grkw -2.44999999999999 30.938483203129 numDeriv::grad(llkw) -3.99999999995549 32.430138079912 grekw(lambda = 1), same dist. -3.99999999999997 32.430138079907The relative error reached 38.75% in the gradient, 47% in
H[alpha,alpha]and 78% inH[alpha,beta], and ordinary data was enough to show it:c(1-1e-9, 1-1e-11, 0.5)already diverged by 3.4e-05. Standard errors and confidence intervals were wrong whenever the sample held observations close to 1.Both routines are now
grekw()/hsekw()withlambdafixed at 1, evaluated from logarithms. Adjudicated over 80 parameter/data blocks against two independent references: the maximum relative disagreement with the EKw path fell from 3.93 to exactly 0 in the gradient and from 4.00 to 1.4e-14 in the Hessian, and againstnumDerivfrom 3.93 to 1.6e-09, which is numDeriv’s own noise. No block moved in the wrong direction and no sign changed. -
llgkw()returned-Inffor data outside the open support (gkw.cpp):ll*()is the negative log-likelihood, so an invalid point must be+Inf– the valueoptim()moves away from.llgkw()returned-Inf, making data outside(0, 1)the global minimum of the function being minimised, and it was the only one of the seven families with that sign;llbkw(),llkkw(),llekw(),llkw(),llmc()andllbeta()all returned+Inf. The parameter path inllgkw()was already+Infand is unchanged.The practical damage was in comparing likelihoods rather than in optimisation:
optim()refuses to start at either infinity, so no optimiser silently converged on bad data. But on a sample holding a single0– ordinary in untransformed proportions – the GKw family won every comparison:gkw nll = -Inf <- wins argmin = gkw bkw nll = Inf AIC = -Inf ... nll = InfValues for valid data are bit-identical, and the nesting identities against
llkw(),llbkw()andllekw()still agree to 1e-12. Zero-length arguments crashed the R process (all seven families): the vectorised
d*(),p*(),q*()andr*()routines size their output as the maximum length of their inputs and then recycle withi % vec.n_elem. When one argument had length zero while another did not, the output length stayed at one or more and the recycling evaluatedi % 0. Integer division by zero is undefined behaviour; on x86-64 it raisesSIGFPE, terminating the R process with no error, no message and nothing fortryCatch()to catch. The R-level validation did not intercept it either, becauseany(numeric(0) <= 0)isFALSE. All 28 exported routines now short-circuit before the loop:d*(),p*()andq*()returnnumeric(0), matchingstats::dbeta(numeric(0), 1, 1), andr*()returnnmissing values with a warning, matchingstats::rbeta(3, numeric(0), 1). A filtered vector that happened to be empty, such asdkw(x[x > 1], 2, 3), was enough to trigger the crash. Numerical output is unchanged for every non-empty input.-
rbkw()andrkkw()generated values outside the open support (gkw.cpp,bkw.cpp,kkw.cpp,ekw.cpp,kw.cpp):rbkw()drewV ~ Beta(gamma, delta+1)and then formed1.0 - V. ForVbelow1.1e-16that rounds to exactly 1 and the generator returned 0.R::rbetaitself never returned a zero – every one was fabricated by the subtraction:rbkw(1e5, 2, 3, 0.02, 0) 48,602 exact zeros 48.6% of the sample rbkw(1e5, 2, 3, 0.05, 0) 16,440 exact zeros rkkw(1e5, 0.2, 3, 0, 0.3) 6 exact zerosA zero is outside the
(0,1)the likelihood accepts, so the package’s own simulate-then-fit workflow broke on six zeros in a hundred thousand:llkkw()at the true parameters returnedInfandoptim()stopped with “L-BFGS-B needs finite values of ‘fn’”. Kolmogorov-Smirnov against the package’s own CDF rejected thegamma = 0.02sample outright,D = 0.486.The five generators that formed
1 - u–rgkw(),rbkw(),rkkw(),rekw(),rkw()– now invert in log space, the same chain the quantile functions use.rmc()andrbeta_()never had the defect and are untouched.The draws themselves are unchanged, so
set.seed()reproduces exactly the stream it did before:.Random.seedafter 100,000 variates is bit-identical for all seven generators. Only the inversion that follows differs. Replaying the same Beta draws, 97,559 of 100,000rbkw()values changed and every one moved closer to the closed-form inversion, none away; the largest relative error falls from1.0to7.2e-15, and forrkkw()from1.0to8.5e-14. The Kolmogorov-Smirnov statistic forgamma = 0.02goes fromD = 0.486(p < 1e-16, 24,277 variates outside the support) toD = 0.0042,p = 0.336, none outside. The fit now converges and recovers the parameters. -
All seven quantile functions returned values outside the open support (
gkw.cpp,bkw.cpp,kkw.cpp,ekw.cpp,bpmc.cpp,kw.cpp,beta_.cpp): eachq*()undidlog.pwithexp(), folded the upper tail with1 - p, and then inverted using1 - uin linear space at every step. The result was not merely imprecise – it left(0,1)altogether:qekw(0.02, 20, 0.1, 0.1) returned 0 true value 0.1587 qekw(1e-08, 5, 2, 0.5) returned 0 true value 5.49e-04 qkw(1e-16, 0.2, 2) returned 0 true value 3.125e-82 qbeta_(-1000, 2, 3, log.p=T) returned 0 true value 2.25e-218A quantile of exactly 0 or 1 then feeds
d*()andll*()a value outside the support they accept, so the damage propagates into simulation by inversion and into any likelihood built on it.The inversion now carries
log(u)andlog(1-u)from whatever scale and tail the caller used, so neither is recovered by subtraction, and the four families that route through the incomplete beta handlower_tail/log_ptoR::qbeta.qbkw()needslog(1-z): it takeslog1p(-z)whilez <= 1/2and otherwise gets1-zdirectly fromR::qbetathrough the symmetryI_z(a,b) = 1 - I_{1-z}(b,a).Judged against closed-form inversions written independently in R: of 22,236 values, 8,311 changed and every one improved. The largest relative error falls from
2.06to5.7e-14. The boundary conventions of 1.1.5 are preserved exactly, including the saturating result for out-of-rangep.One limit remains: below about
log(p) = -745,exp(log u)underflows,1-urounds to exactly 1 and the inversion has nothing left to invert, so the quantile is still 0. Recovering it needs each step to carry bothlog(q)andlog(1-q). 1.1.5 already returned 0 fromlog(p) = -40downward. -
All seven cumulative distribution functions collapsed to 0 or 1 (
gkw.cpp,bkw.cpp,kkw.cpp,ekw.cpp,bpmc.cpp,kw.cpp,beta_.cpp): eachp*()formed1 - x^alphaand1 - (1 - x^alpha)^betain linear space. Oncex^alphafell below1.1e-16the first rounded to exactly 1 and the second to exactly 0, and the CDF returned 0 or 1. The error was absolute, not a lost digit:pekw(5.62e-09, 2, 5, 0.02) returned 0 true value 0.483 pekw(0.14, 20, 20, 0.1) returned 0 true value 0.0264 pkw(1e-09, 2, 5, log.p=TRUE) returned -Inf true value -39.84The second sits at
x = 0.14, nowhere near a tail: a smalllambdacompresses the result toward 1 and pulls the collapse into the body of the distribution. A systematic sweep found 8,917 affected points forpgkw()alone, with a maximum absolute error of 1.0 – the largest a probability can be wrong by.lower.tailandlog.pwere also applied afterwards, as1 - pandlog(p), instead of being passed toR::pbeta, which implements both without ever forming those quantities. That cost the opposite tail:pmc(1 - 1e-06, 2, 3, 2.5, lower.tail = FALSE)returned exactly 0 against a true1.95e-22, andpbeta_(1e-200, 2, 3, log.p = TRUE)returned-Infagainst a true-918.73.Every chain now runs in log space through
gkw_log1mexp(), the survival function is computed directly rather than as1 - F, andlower_tail/log_pgo straight toR::pbetawhere the family routes through the incomplete beta.Judged against log-space references written independently in R: of 61,100 values over a grid spanning
1e-300to1 - 1e-16, 23,720 changed, 23,719 improved and one moved by a single ulp. The largest relative error falls from5.9e+305to1.6e-01, and only 14 values of the 61,100 still exceed1e-9. Those 14 sit atx >= 0.999, wherex^lambdais within an ulp of 1 and the argument handed toR::pbetacannot carry more precision. All seven CDFs are monotone, stay within[0, 1], satisfyF + S = 1to1.1e-16, and reproduce their nesting identities exactly. -
The McDonald log-likelihood was unbounded below, and silently rewrote the data (
src/bpmc.cpp):llmc(),grmc()andhsmc()clamped every observation to[1e-10, 1-1e-10]before use. For a legitimate observation at1e-20that moved the likelihood by 23 nats, and it broke the identityllmc(gamma, delta, 1) == llbeta(gamma, delta), where the two are the same model: the disagreement reached 1140 nats.Separately, for
delta > 1000the termdelta * log(1 - x^lambda)was floored at-700per observation, so it stopped growing withdeltawhile the constant termn(log lambda - log B(gamma, delta+1))kept growing. The negative log-likelihood became unbounded below:llmc(c(1e300, 1e300, 1e-6), x)returned-2.77e+302, a global minimum at absurd parameters, with a visible step atdelta = 1000. It now returns+2.58e+303.grmc()andhsmc()additionally flooredv = 1 - x^lambdaat1e-10and capped their lambda terms at±1e6, so the gradient plateaued where the objective kept moving.llmc()also computedlog(1 - x^lambda)aslog1p(-x^lambda), which cannot recover digitsx^lambdahas already lost, whilegrmc()andhsmc()already used-expm1(lambda * log(x)); the objective and its gradient therefore disagreed asxapproached 1. All three now use-expm1of the same exponent.These clamps were removed together rather than one at a time:
llmc()shared them withgrmc()andhsmc(), so removing only the documented subset would have introduced an objective/gradient mismatch that did not previously exist.Because the three functions shared the same clamps, checking the analytic gradient against
numDeriv::grad(llmc)passed even with the defect present. Validation therefore used a closed-form reference written independently in R. Against it, the largest relative error falls fromInfto1.0e-14forllmc(), from1.12to3.1e-05forgrmc(), andhsmc()now agrees with the jacobian of the analytic gradient to6.0e-08. The nesting identity withstats::dbeta()goes from 1140 nats of error to9.1e-13. Maximum-likelihood fits on well-behaved data are bit-identical, under both BFGS with the analytic gradient and Nelder-Mead.The residual
3.1e-05ingrmc()appears only forgamma + delta > 100and is unchanged by this commit: it comes from the asymptotic digamma expansions, a separate defect. -
hsgkw()silently returned the Hessian of a smaller sample (src/gkw.cpp): the observation loop skipped past any point whoselog(1-x^alpha),log(1-v^beta)orlog(1-w^lambda)came out non-finite, leaving the remaining terms to be returned as a finite, symmetric matrix with noNaNand no warning. For a quantity whose purpose is to produce standard errors, that is the worst available failure mode. Withbeta = 500and four observations every point was dropped and only the parameter-only terms survived, soH(alpha, alpha)came back asn / alpha^2 = 4against a true1996.3– wrong by a factor of 499, and indistinguishable from a valid result. With five observations one survived and the function returned the Hessian of a single point as if it described all five.The loop now stops on the first such observation and returns a
NaNmatrix with a warning, matching what the function’s own intermediate-value check already did. Matrices are bit-identical wherever they were finite before. Computing those terms correctly requires the log-space rework and is not attempted here.grgkw()is unaffected: it is fully vectorised and propagatesNaNrather than dropping observations. -
dgkw()returned a density of zero asxapproached 1 (src/gkw.cpp): the density formedx^alphain linear space and bailed out wheneverx^alpha >= 1 - sqrt(.Machine$double.eps). The guard was there becauselog(x^alpha)loses its significant digits in that band – doubles are spaced2.2e-16apart near 1, so the relative error reaches4e-6by1 - x = 1e-12– but returning zero is a far worse answer than an imprecise one. It also broke the nesting identity:dgkw(1 - 1e-9, 1, 0.1, 1, 0, 1)returned 0 whiledkw(1 - 1e-9, 1, 0.1), the same density, returned1.26e+07. ForGKw(0.1, 0.1, 10, 0.1, 0.1)the discarded band held 13% of the probability mass, and forbeta < 1, where the density diverges at 1, the rising tail was replaced by a cliff to zero.log(x^alpha)is now taken asalpha * log(x), which is exact and removes the need for the guard;gkw_log1mexp()already covers the resulting regime. Over the regression grid, 6,780 of 47,104dgkw()values changed, every one closer to an independent log-space reference and none further away; no other family moved. Recovered mass shows up in the integral:GKw(0.1, 0.1, 10, 0.1, 0.1)goes from 0.8671 to 0.9998.Two
continueguards further downdgkw()still discard a point whenlog(1 - w^lambda)underflows, even wheredelta = 0makes that term vanish from the density. That is unchanged here and belongs with the log-space rework. -
Two logarithmic bound constants held the wrong quantity (
src/utils.h):LOG_DBL_MAXwas documented aslog(DBL_MAX_SAFE)but heldlog10(DBL_MAX) = 308.2547, while the correct natural logarithm is707.4801. Since it is used as the overflow threshold ofsafe_exp()andsafe_pow(), every result aboveexp(308.25)was returned as+Inf, discarding roughly 174 orders of magnitude of representable double range. Reachable from the public API:dkw(1e-300, 0.5, 2)returnedInfinstead of1e+150.Separately,
safe_log()scaled its underflow branch byLOG_DBL_MIN, which islog(DBL_MIN), while dividing byDBL_MIN_SAFE, which is10 * DBL_MIN. Every result below2.225e-307was therefore off by exactlylog(10) = 2.302585– a finite, plausible, wrong number rather than a visible failure. It propagated intodkw(x, log = TRUE),llkw(),llgkw()andpmc(log.p = TRUE), and madellgkw()disagree withdgkw(), which takeslog(x)directly.The constants are now named for what they are –
LOG_DBL_MIN,LOG_DBL_MIN_SAFEandLOG_DBL_MAX– andsafe_log()scales by the logarithm of the divisor it actually uses. Over a regression grid of 401,373 values, 328 density values, 16 log-likelihoods and 3 tail probabilities changed; every one moved closer to an independent log-space reference, and none moved away. The largest relative error against that reference fell fromInfto1.6e-16for densities and from 4.5% to1.2e-10forllgkw(). As a side effect the all-NaNregion ofgrgkw()recedes: withx_max = 0.99it began atbeta = 80and now extends pastbeta = 130, with the newly finite values agreeing withnumDeriv::grad()to 1.3e-9 or better.safe_exp()still saturates abovelog(DBL_MAX_SAFE), i.e. one order of magnitude below the true double maximum. That headroom is the documented intent of theDBL_MAX_SAFEconstant and is left in place. -
gkwgetstartvalues()never ran the multi-start it documents;n_startswas inert (gkwinit.cpp): the selection loop decided whether to optimize a starting point by comparing the raw objective at that point againstbest_obj, which after the first iteration already held an optimized value. A start that was merely poor – exactly the case multi-start exists to rescue – was therefore discarded before Nelder-Mead ever saw it, and in practice only the first of the candidates was optimized at all. Every value ofn_startsreturned the same answer, bit for bit:set.seed(202); x <- rgkw(500, 5, 1.2, 3, 0.5, 2) gkwgetstartvalues(x, "gkw", n_starts = k) moment objective k = 1 6.262579e-04 k = 10 6.262579e-04 (identical) k = 200 6.262579e-04 (identical) after k = 1 1.008584e-07 k = 10 1.369536e-08 k = 200 1.037278e-10Every candidate is now optimized and only the optimized objectives are compared. A second defect surfaced once the multi-start was live: Nelder-Mead is unconstrained and can leave the family’s parameter box, so a winner chosen on its pre-clamp objective could be handed back worse than a rival that stayed inside, making the returned error non-monotone in
n_starts. Candidates are now clipped to the box before being scored, so the objective that is compared is the objective of the vector that is returned.The practical failure was worse than a suboptimal start. The single optimized path ran into a degenerate corner in which the numerical integral of the density underflows,
moment_theoretical()falls back to its closed-form Kumaraswamy moment, and the optimizer is rewarded for parameters whose real moments are nothing like the sample’s. In 15 of 168 sweep cases the returned vector scored the maximum possible objective of exactly 3.0 – all five relative moment errors equal to 1:set.seed(3050); x <- rgkw(50, 2, 3, 1.5, 2, 0.8) sample mean 0.296299 before alpha = 0.100000 (pinned at the lower bound), beta = 9.874503, gamma = 0.574504, delta = 0.131984, lambda = 1.276909 theoretical mean 1.401418e-06, objective 3.000000 after alpha = 0.773802, beta = 3.322900, gamma = 3.662037, delta = 0.670337, lambda = 1.437505, objective 4.830e-07Downstream the damage reached the fits themselves. Starting
optim()from that corner, 8 of 10 GKw samples of size 500 converged to a positive negative log-likelihood – around +4100 to +4900 where the correct region is near -300:nll from old start nll from new start gkw seed 1 4117.357 -295.479 gkw seed 4 4829.810 -297.731 gkw seed 10 4855.630 -284.388Verified over 168 cases (seven families x n in {50, 200, 1000} x 8 seeds) at the default
n_starts = 5: 77 improved, 91 unchanged, none worse, worst ratio exactly 1.000000. The 15 cases at the maximum objective of 3.0 fell to none, and the largest objective over the sweep fell from 3.0 to 5.876e-04. Over 70 MLE fits driven from the two sets of starting values, the largest improvement in the attained negative log-likelihood was 5230.4 nats and the largest regression 0.09 nats, on akkwsample that settled in a neighbouring local optimum; mean relative parameter error fell from 0.507 to 0.409. The full test suite is unchanged at 0 failures.Cost:
n_startsnow buys what it claims, so it also costs what it claims. At the defaultn_starts = 5a GKw call goes from 0.058s to 0.27s (n = 300); atn_starts = 1000, from 0.09s to 50s. The default is unchanged. Four fixed, family-specific starting points are always used, son_startsbelow 4 still behaves as 4; this is now documented rather than silently true. -
gkwgetstartvalues()truncated out-of-support data without saying so (gkwinit.cpp): every observation was clamped into[1e-10, 1 - 1e-10]in silence. Truncation moves every sample moment and therefore every estimate the function returns, and its commonest cause – data on a percentage or 0-100 scale – is exactly the case a caller needs to be told about. On that input the function did not fail; it answered, and the answer was both wrong and unremarkable-looking:set.seed(1); y <- rkw(300, 2, 3) gkwgetstartvalues(y, "kw") alpha 2.137549 beta 3.506511 gkwgetstartvalues(c(y, 5, -3), "kw") alpha 2.047567 beta 3.251306 gkwgetstartvalues(y * 100, "kw") alpha 50.000000 beta 50.000000The last line is the whole problem in one row: a sample handed over on a 0-100 scale came back with both parameters pinned at the upper edge of the parameter box, with nothing to distinguish it from a fit.
tryCatch(..., warning = )caught no condition in any of the three calls.Observations outside the open interval
(0,1)– exact 0 and exact 1 included, matching the supportll*()enforces since the fix earlier in this release – now raise a warning naming how many were truncated and the range they spanned:gkwgetstartvalues: 300 of 300 observations lie outside the open interval (0,1) (observed range [6.6169, 89.7703]) and were clamped to it; the estimates below are those of the clamped sample. Data on a percentage or 0-100 scale must be rescaled before use.The clamp itself is kept, so a single boundary observation still does not abort a fit and no existing call changes its return value; only the silence is removed.
NAand non-finite values continue to be dropped without a warning, which the@param xentry now states.Verified: the warning fires on
c(y, 5, -3), on a lone exact 0, on a lone exact 1 and ony * 100, reporting 2, 1, 1 and 300 offenders respectively with the correct observed range in each; it does not fire on the clean sample, nor on samples carryingNAorInf. Every returned vector is unchanged. The full test suite, whose data are all strictly inside the support, still reports 0 warnings.
Documentation Fixes
-
Bimodality was claimed in four places and the family does not reach it (
R/gkwdist-package.R,README.Rmd,README.md,vignettes/gkwdist.Rmd): the overview listed bimodality among the shapes the GKw accommodates, the “Advantages” list said “bimodal, U-shaped, bathtub”, and both shape-selection tables offered GKw for “Bimodal or U-shaped” data.No density with two interior modes was found in roughly 330,000 parameter vectors:
search draws 2+ interior modes structured grid, 7 x 7 x 6 x 5 x 6 8,820 0 log-uniform (0.05, 30)^5, 4001-point grid 200,000 0 log-uniform (1e-3, 300)^5, 3001-point grid 120,000 0The third sweep first flagged 644 candidates, every one of them with . At that size underflows for all but a sliver next to 1, so a grid uniform in cannot resolve the density. The extra peaks are jitter: for (225.7, 70.1, 0.041, 8.04, 2.48) their count grows with resolution – 2, then 5, then 55 as the grid goes 3001, 30001, 300001 points – at heights some thirty orders of magnitude below the mode at 19.23. On a grid uniform in each flagged case has at most one interior mode.
Every shape observed was monotone, unimodal or U-shaped. A second peak appears only as a divergence at a boundary, governed by the exponents and already documented in
dgkw(). U-shapes and bathtubs are real and those claims stand; only bimodality is removed. The overview now states what the family does instead, so that data with two separated interior modes is sent to a mixture rather than to a larger member of this family. The claim is reported as what it is – a search, not a proof. The vignette’s U-shape recommendation named a condition that does not hold for EKw (
vignettes/gkwdist.Rmd): the row replacing the bimodal one first read “Kumaraswamy or Exponentiated Kw ()”. For EKw the left-hand exponent is , so is not sufficient:dekw(c(1e-8, 1e-4, 0.1), 0.5, 0.5, 3)is 1.9e-05, 1.9e-03, 8.6e-02 – rising from 0, not a U. The row now names Kumaraswamy alone, where is exactly right.-
Two sub-family constraints in the package overview were mathematically wrong (
R/gkwdist-package.R): the “Distribution Family Hierarchy” block gave Kumaraswamy as “GKw with ” and the uniform as “set all shape parameters to 1”. In this parameterization is neutral at 0, not 1 – it enters through and – so both statements name a different distribution than the one they claim:claim max |difference| Kw(2.3, 3.1) vs GKw(2.3, 3.1, 1, 1, 1) 0.887 Kw(2.3, 3.1) vs GKw(2.3, 3.1, 1, 0, 1) 0 <- correct Uniform vs GKw(1, 1, 1, 1, 1) 0.960 Uniform vs GKw(1, 1, 1, 0, 1) 0 <- correctThis is the same off-by-one that the deleted
_pkgdown.ymlblock carried, and it survived there because the overview was the one place where the constraints were never checked against the implementation. Every other statement of a sub-family constraint in the package – the\itemizelist indgkw()’s details, the@detailsof each family, and thedesclines in_pkgdown.yml– was verified numerically in this pass and is correct. The uniform entry now says explicitly why rather than is the neutral value, since that is what the two wrong statements had in common. -
The
@returnof all seven densities denied the boundary contract (R/gkw.R,R/bkw.R,R/kkw.R,R/ekw.R,R/bpmc.R,R/kw.R,R/beta.R): once the closed boundaries began carrying the limiting density, the seven blocks still promised0– or-Infon the log scale – forx“outside the interval (0, 1)”. No rendered page mentioned the limit at all, and 0 and 1 lie outside that open interval, so the sentence denied precisely the two points the change had added:call documented returned dgkw(c(0, 1), 0.5, 0.5, 1, 0, 1) 0 0 Inf Inf dkw(c(0, 1), 2, 1) 0 0 0 2 dbeta_(c(0, 1), 0.5, 0) 0 0 Inf 0.5 dgkw(c(-0.1, 1.1), 2, 3, 1, 0, 1) 0 0 0 0The last row is the case that really does return
0, and the wording is narrowed to it: “strictly outside the interval [0, 1]”. Each block now states thatx = 0andx = 1carry the limiting density, namesstats::dbetaas the base R convention being followed – with theshape2 = delta + 1shift spelled out indbeta_(), whose parameterization differs – and records that the limit is0, a finite positive value, orInfaccording to the parameters.Documentation only; no executable code changes and no numerical result is affected.
-
The out-of-support warning was undocumented in all seven
ll*()(R/gkw.R,R/bkw.R,R/kkw.R,R/ekw.R,R/bpmc.R,R/kw.R,R/beta.R): the guard that warns'data' contains values outside (0, 1)appeared in no help page, so the one signal separating a mis-scaled sample from a genuine fit failure was invisible to a reader of the documentation. All seven@returnblocks now record it, with the reason: an infinite objective offers an optimiser no gradient direction to follow.In the same pass
llgkw()loses “returns a large positive value (e.g.,Inf)”, the only one of the seven that did not nameInfexactly. Measured, all seven returnInfexactly, for an invalidparand for out-of-supportdataalike.Documentation only; no executable code changes and no numerical result is affected.
-
The Cordeiro & de Castro (2011) citation was truncated in 41 of its 42 appearances (the six family files that cite it –
R/kw.Rdoes not): the entry existed in two broken shapes – 14 ended at the journal name with no volume, pages or full stop, and 27 ended at a dangling comma. Onlydgkw()carried it complete. All 42 now read*Journal of Statistical Computation and Simulation*, *81*(7), 883-898.In the same pass, Carrasco, Ferrari & Cordeiro (2010) – the primary source for the five-parameter distribution this package implements – is added to the
@referencesof the seven GKw topics. Their@detailscredited it in prose while@referenceslisted only the two secondary sources; the earlier attribution fix reached the prose and not the list. The six sub-families are left alone: each defines itself as a special case of GKw and links todgkw(), so the attribution reaches them through that link. -
References were not linkable outside the package overview (all seven family files,
src/gkwinit.cpp):\doi{}appeared on 7 citations inR/gkwdist-package.Rand nowhere else, so 103 citations across the 50 function topics rendered as plain text. Every citation whose DOI was already recorded in the package – Carrasco (2010), Cordeiro & de Castro (2011), Jones (2009), Kumaraswamy (1980) and McDonald (1984) – now carries it, taken verbatim fromR/gkwdist-package.RandDESCRIPTION. Every topic that has a\referencessection now has at least one DOI.Nadarajah, Johnson/Kotz/Balakrishnan and Devroye are deliberately left without one: no DOI for those works is recorded anywhere in this package, and a DOI is not something to reconstruct from memory.
gkwgetstartvalues()’s Jones citation, the only one still in plain text with no\emph, is brought into the same form as the other seven, and McDonald’s volume number is italicised to match every other volume number in the package. The package overview page was generated but unreachable (
R/gkwdist-package.R):_PACKAGEcarried@keywords internal, which removes a topic from the help index and from the pkgdown reference.docs/reference/gkwdist-package.htmlwas being built and nothing linked to it, so the whole overview – family hierarchy, performance notes, model-selection workflow, four worked estimation examples – could be reached only by typing the URL. The keyword is dropped and the topic is added to_pkgdown.ymlunder aPackage Overviewheading, which is also what keepspkgdown::check_pkgdown()clean once the topic is no longer internal.-
Ten of the thirteen
\keywordentries were outside R’s controlled vocabulary (all seven family files):density,cumulative,quantile,random,likelihood,gradient,hessian,beta,kumaraswamyandmcdonaldare absent fromR/doc/KEYWORDS, so they indexed nothing. The seven that name a role become@familygroupings – density, cumulative distribution, quantile, random generation, log-likelihood, gradient and Hessian functions, seven members each – which roxygen2 renders as bidirectional “Other …:” links and pkgdown exposes as concepts. The three that name a distribution become@concept.distributionandoptimize, which are standard, are kept.This is the axis the hand-written
\seealsoblocks did not cover: they link each function to the siblings of its own distribution, never across distributions, so nothing led fromdgkw()to the other six densities. The curated blocks are untouched and the generated lists are appended below them.Only
beta,kumaraswamyandmcdonaldexisted as distribution keywords. The four families that had none –generalized kumaraswamy,beta-kumaraswamy,kumaraswamy-kumaraswamyandexponentiated kumaraswamy– are given the matching concept, so all 49 topics now carry one family and one distribution concept. -
Every example in the package sat inside
\donttest{}(all seven family files): 51 of 52 topics wrapped their entire@examplesblock, soR CMD checkwithout--run-donttest– the form run locally and in most CI configurations – executed no example at all, while--as-cranran all 9,217 lines regardless. The wrapper bought nothing and hid everything.Measured on one machine, the 28
d/p/q/rblocks take 0.05 s in total and the 21ll/gr/hsblocks take 41 s. The wrapper is removed from the first group and kept on the second, where the cost is real and where the weakly-identified fits behind the confidence-region entry below make the examples platform-sensitive. The default check now exercises 1,515 lines of examples instead of none. gkwgetstartvalues()had no@seealso(src/gkwinit.cpp): the one topic whose output is meant to be fed straight into other functions of this package linked to none of them. It now points at the sevenll*()objectives it seeds and atstats::optim().-
Help page titles followed five competing patterns (all seven family files):
dmc()read “Beta Power Distribution Distribution”; the GKw CDF, quantile and RNG topics put the role after the distribution (“Generalized Kumaraswamy Distribution CDF”) where the other six put it first; “CDF of the” and “Cumulative Distribution Function (CDF) of the” both appeared, as did “Random Generation for” and “Random Number Generation for”, and “Negative Log-Likelihood” took “for the”, “for” and “of the” in different topics. The KKw family was writtenkkwin all seven of its titles while the README,_pkgdown.ymland the package overview writeKKw.Sixteen titles are repaired – the duplicated word, nine against the majority form, and the seven KKw case fixes below – leaving all 49 on exactly seven patterns, one per role. Titles that abbreviate the distribution in the gradient and Hessian topics are left abbreviated: those are the longest titles in the package and spelling the family out would push them past 120 characters.
The lower-case
kkwis corrected in the prose ofR/kkw.Ras well, 27 occurrences across descriptions, details,@returnblocks,@seealsolabels and example plot titles. Three kinds ofkkware deliberately left alone, because they are identifiers rather than the name of the distribution: the file referencessrc/kkw.cpp, the exported function names (dkkw,pkkw,qkkw,rkkw,llkkw,grkkw,hskkw), and thefamily = "kkw"argument value that users pass togkwgetstartvalues(). Eight verifications inside
@exampleswere commented out (all seven family files): sixq*()topics computed a round tripp -> q*() -> p*(), printed both numbers, and left the assertionabs(p_check - p_recalc) < 1e-9commented.dgkw()andpgkw()were worse: each builtpdf_beta_check/cdf_beta_checkagainststats::dbeta()/stats::pbeta()and then commented out the only line that used it, leaving a variable computed for nothing. All eight now run and print. Checked before enabling: the twostatscomparisons agree to 4.4e-16 and 3.3e-16, and every round trip is exact to at worst 1.1e-16.The
intronavbar entry pointed at nothing (_pkgdown.yml,vignettes/): pkgdown fills “Get started” fromvignettes/<package>.Rmd, and the introductory vignette wasinto-gkwdist.Rmd– “into” for “intro” – so the entry was silently dropped and the published site had no “Get started” link. The file is renamedgkwdist.Rmd, which fixes the spelling and activates the entry in one move. Its title andVignetteIndexEntryare unchanged. Links toarticles/into-gkwdist.htmland calls tovignette("into-gkwdist")will no longer resolve.Half of
_pkgdown.ymlwas a superseded copy of itself (_pkgdown.yml): 131 of its 284 lines were a commented-out earlier reference layout. It was not a duplicate – it carried the nesting relation for each sub-family, which the active version had dropped – and two of those relations were wrong, giving EKw as “GKw with γ = δ = 1” and Kw as “GKw with γ = δ = λ = 1” where δ = 0 is the neutral value in this parameterization and is what every@detailsblock and every function default states. The nesting is carried over to the activedesclines with δ = 0, and the dead block is removed.No
inst/WORDLIST(new file):spelling::spell_check_package()reported 191 words and was therefore unusable as a check. A curated list of 45 – the family abbreviations, the cited authors, the institutions, terms such asdigamma,trigamma,unimodalityandbimodality, and the three fragments the<doi:...>markup and the quoted package name leave inDESCRIPTION– now leaves the.Rdfiles andDESCRIPTIONreporting nothing, so a real typo will show up. Nothing consumes the list automatically: there is notests/spelling.Randspellingis not inSuggests, so it serves whoever runs the check by hand.The author name was rendered two ways (
R/gkwdist-package.R): 49 topics say “Lopes, J. E.” and the package overview said “J. E. Lopes”. The overview now matches the rest.-
Editorial hedging in the rendered help pages (all seven family files): 29
\seealsoentries qualified functions that exist and are exported – “(if these exist)”, “(gradient, if available)”, “(other functions for this parameterization, if they exist)”. A CRAN help page should not speculate about its own package’s contents. The qualifiers are removed and the informative half of each label is kept, so\code{grbkw} (gradient, if available)reads\code{\link{grbkw}} (gradient).41 cross-references in those same blocks used
\code{}where\link{}was meant, so they rendered as plain text and the help pages could not be navigated between. All 50 distinct\link{}targets now resolve to an alias in the package.Documentation only; no executable code changes and no numerical result is affected.
-
gkwgetstartvalues()is deterministic, and now says so (gkwinit.cpp): the help page described “multiple random starting points” without stating that the randomness is internal and fixed. The extra starting points come from a generator seeded with a constant, soset.seed()has no effect on the returned value and.Random.seedis neither read nor advanced:set.seed(1); a <- gkwgetstartvalues(x, "gkw", 20) set.seed(9999); b <- gkwgetstartvalues(x, "gkw", 20) identical(a, b) TRUE .Random.seed unchanged across the call TRUE the caller's next runif(1) unchanged TRUEThis is deliberate and is being documented, not changed. The function is a method-of-moments estimator whose output seeds optimisers elsewhere in a fit; two calls on the same data must agree, or every downstream fit would inherit a dependence on the ambient seed and would silently consume draws the caller did not ask to spend. The lever for a wider search is
n_starts, which since the fix above is both effective and monotone – more starts can only lower the objective – so a seed argument would add a lottery where a monotone control already exists. ADeterminismparagraph inDetailsnow states all three facts, the@examplesblock demonstrates them, and a comment at the generator ingkwinit.cpprecords the intent so the constant seed is not mistaken for an oversight.The
set.seed(123)opening the example is correct and is kept: it makes therbeta()sample on the next line reproducible. Its comment now says which of the two calls it governs.Documentation only; no numerical result changes.
-
Confidence-region examples were undrawable where the observed information was not positive definite (29
@examplesblocks across the seven families): every one built a confidence region fromeigen(solve(hs*(mle, data))[1:2, 1:2])and then tookdiag(sqrt(eig_decomp$values)), with nothing to guarantee the eigenvalues were non-negative.solve()of an observed information matrix is a covariance matrix only where that information is positive definite;optim()reportsconvergence = 0on a flat likelihood ridge without establishing it. When an eigenvalue came back negative,sqrt()producedNaN, the whole region becameNaN, andplot()aborted withneed finite 'xlim' values.The fit these examples rest on is weakly identified – the observed information has a condition number between
4.4e+06and1.3e+07– so which side of the boundary it lands on depends on the BLAS and the optimiser’s path. The examples passed on Linux and macOS and failed on Windows under--run-donttest.eigen()is now called withsymmetric = TRUE, which is what a covariance matrix warrants and which keeps the eigenvalues real and ordered, and the eigenvalues are clamped at zero, so the region degenerates rather than vanishing. Reproduced against the indefinite block directly: 500 of 500 ellipse coordinates wereNaNbefore and none are after, andplot()raises the sameneed finite 'xlim' valuesbefore and succeeds after.Documentation only; no executable code in
R/orsrc/is changed, and every numerical result is unaffected. grmc()gradient formula had inverted digamma signs (R/bpmc.R): the@detailsblock documentedpsi(gamma + delta + 1) - psi(gamma)for the gamma component andpsi(gamma + delta + 1) - psi(delta + 1)for delta. Sinced log B(gamma, delta+1) / d gamma = psi(gamma) - psi(gamma + delta + 1), both signs were reversed, and the documented formula disagreed with the returned value by two orders of magnitude.R/beta.Rdocumented the opposite sign for the same quantity. The implementation was correct throughout; only the documentation is changed. The same block’s Hessian entry ford2l/dgamma ddeltainhsmc()had the same sign reversal.README example 6 inverted the sign of the observed information matrix (
README.Rmd):hsekw()already returns the Hessian of the negative log-likelihood, so negating it again produced a negative definite matrix and printedNaNfor every asymptotic standard error. Example 3 of the same README and the vignettes were already correct.GKw attribution corrected (
R/gkw.R): the main help page credited Cordeiro & de Castro (2011), which introduces the Kw-G family, rather than Carrasco, Ferrari & Cordeiro (2010), which introduces the five-parameter generalized Kumaraswamy distribution implemented here.DESCRIPTION, the README and the vignettes already cited the latter.
Packaging
-
A cheat sheet, generated rather than written (
cheatsheet/,pkgdown/assets/cheatsheet/): two A4 pages covering the family tree, all 49 exported functions, thed/p/q/rcontract, the shapes the family reaches, the maximum-likelihood recipe, the per-familyparordering and the nested-model map. It is published at/cheatsheet/on the pkgdown site and linked from the navbar and the home sidebar.Nothing on the sheet is typed twice, because a sheet that prints the package version would be stale at the first release.
cheatsheet/build.Rfills the template from the package itself: the version fromDESCRIPTION, the logo fromman/figures, the six density curves from the package’s ownd*functions, and the function count and matrix from the namespace. A gained or lost export is a build error rather than a quietly wrong sheet.Rscript cheatsheet/build.R --checkexits non-zero when the committed HTML differs from a fresh build, and the pkgdown workflow runs it before building the site, so a release that forgets to regenerate the sheet fails CI instead of publishing the wrong version number. Both directories are in.Rbuildignore; neither reaches the tarball. -
inst/CITATIONnamed the version and the year by hand and had to be edited at every release to stay true – the same staleness the cheat sheet is built to avoid. Both now come from the package metadata: the version frommeta$Version, the year from theDate/Publicationfield CRAN adds to the installedDESCRIPTION, falling back to the current year on a development install where that field does not exist.citation("gkwdist")follows a version bump on its own.metadata citation reads Version 1.1.6, no Date/Publication "(2026) ... version 1.1.6" Version 1.0.9, Date/Publication 2025-03-04 "(2025) ... version 1.0.9"tools:::.check_citation()reports no problems. The README lost what the vignettes already carry. 777 lines to 221: the seven mathematical specifications now live only in
theory-gkwdist, which derives them with proofs, and seven of the nine worked examples only ingkwdist. What stays is the overview, the hierarchy, the function table, two quick starts, and pointers to the cheat sheet, the vignettes and the reference index.RcppArmadillomoved out ofImports. It was listed there only becauseR/zzz.Rcarried@import RcppArmadillo, which putimport(RcppArmadillo)inNAMESPACE. Armadillo is header-only for a client package: everything gkwdist uses from it is compiled intogkwdist.soat install time throughLinkingTo, whereRcppArmadilloalready appeared. Loading its R namespace at run time bought nothing, andR CMD check --as-cranreportedPackage in Depends/Imports which should probably only be in LinkingTo: 'RcppArmadillo'. The tag, theNAMESPACEentry and theImportsline are gone;LinkingTois untouched, so the build is unchanged.numDerivmoved fromImportstoSuggests. No function inR/callsgrad()orhessian(); the package’s own derivatives are analytic and live insrc/.numDerivis used only by the test suite, as the independent reference the analyticgr*()andhs*()routines are checked against, and by\seealsocross-references in the help pages, whichSuggestskeeps valid. Every test that calls it is guarded byskip_if_not_installed(), so the suite runs to completion without it. Installing gkwdist no longer pullsnumDerivin.The
utils::globalVariables()registration inR/zzz.Ris gone. It listed 39 names, all 39 of which also appear in the sibling packagegkwreg’s own registration – regression-diagnostic artefacts such ascook_dist,leverage,linpredandmodel_labelthat no distributions package produces, plus"::",":::"and"log", which are functions, not variables. The list had been copied across and never pruned. With the whole call removed,R CMD checkstill reportschecking R code for possible problems ... OK, so not one of the 39 was suppressing a real finding. Removing it restores the check’s ability to notice a genuine undefined global in future.-
New
.github/workflows/sanitizers.yamlruns the compiled code under runtime sanitizers. CI had none: no ASan, UBSan, valgrind or rhub job anywhere. That gap is what let the worst defect of this release through. TheSIGFPEfromi % 0on zero-length input, which killed the R process outright, produced no diagnostic under-Wall -Wextra -Wformat=2, passedR CMD checkcleanly, and kept the whole five-platformR-CMD-checkmatrix green. UBSan names it on the first call, with a stack trace:gkw.cpp:134:21: runtime error: division by zero #0 ... in dgkw(...) src/gkw.cpp:134 #1 ... in _gkwdist_dgkw src/RcppExports.cpp:415The workflow has two jobs: UBSan on stock R with GCC, which links
libubsanintogkwdist.soand needs no instrumented R, and R-hub’sclang-asancontainer, which adds AddressSanitizer. Both arecontinue-on-error: truefor now, so a finding reports without blocking a pull request.
Deprecated
-
The re-exported pipe,
%>%, is deprecated and will be removed in a future release. Nothing changes in this one: it is still exported and still behaves exactly as before.gkwdist does not use the pipe anywhere – not in
R/, not in the tests, not in the vignettes. It re-exportsmagrittr’s operator and nothing else, which is the sole reasonmagrittris a hard dependency, so every installation of a distributions package pulls in a package it never calls. R has had a native pipe,|>, since 4.1.0, and users who want%>%can attach it from its own source.This is announced a release ahead rather than done now because
export("%>%")is public API: code that readslibrary(gkwdist)and then uses%>%without attaching magrittr or a tidyverse package would stop working the moment the export went away, withcould not find function "%>%". If your code depends on gkwdist supplying it, switch now tolibrary(magrittr)(or any tidyverse package that re-exports it), or to|>. The change is invisible if you already attach magrittr yourself.
Testing
test-return-contract.R– sweeps every parameter of every family throughd,p,qandrwith-1,0,NAandNaN, asserting they take the identical route, and asserts separately thatdelta = 0is still accepted –deltais the one parameter whose bound is>= 0, and the only place the change could have gone off by one. Also covers the out-of-support warning in all sevenll*(). Fails 60 assertions and errors on 1 against the preceding commit.New
tests/testthat/test-zero-length-input.Rcovers all 28 routines with zero-length data and zero-length parameters, the empty-subset idiom, and the correspondence with thestatspackage’s convention.New
tests/testthat/test-deep-tail-precision.Rpins the subnormal and large-density regimes against a log-space reference. It fails 37 assertions against 1.1.5.New
tests/testthat/test-density-near-upper-bound.Rpinsdgkw()in the band the old guard rejected, together with the nesting identities and the total mass. It fails 17 assertions against 1.1.5.New
tests/testthat/test-hessian-degenerate.Rpins the degenerate cases ofhsgkw()and checks that healthy parameters keep their finite, symmetric matrices. It fails 8 assertions against 1.1.5.New
tests/testthat/test-mcdonald-no-clamping.Rpinsllmc(),grmc()andhsmc()against a closed-form reference, checks the Beta nesting identity and the boundedness of the objective, and fits a model end to end. It fails 45 assertions against 1.1.5.New
tests/testthat/test-cdf-log-space.Rpins the seven CDFs against log-space references, checks monotonicity, range,F + S = 1, the nesting identities and agreement with the integral of the density. It fails 9 assertions against 1.1.5.New
tests/testthat/test-quantile-log-space.Rpins the seven quantiles against closed-form inversions, checks that they stay inside(0,1), thatp(q(u))recoversu, that the boundary conventions are unchanged and that the nesting identities hold. It fails 28 assertions against 1.1.5.New
tests/testthat/test-startvalues-contract.Rpins the three contractsgkwgetstartvalues()advertises: thatn_startswidens the search and never worsens the fit, that the estimate reproduces the first sample moment, that the answer is deterministic and leaves.Random.seedalone, and that truncating out-of-support data raises a warning naming how many observations were moved. It fails 25 assertions against 1.1.5 and passes 54 on this branch.test-mle-performance.Rcompared each scenario’s mean parameter error over its own converged subset, which penalises the more robust scenario: the analytical gradient fits datasets the numerical baseline gives up on, and those are the hard ones. After the generators changed, agkwrun converged on 4 reps the baseline could not touch, whose mean error was 16.1 against 0.50 on the 23 shared reps, and the reported ratio went from 0.82 to 4.66. The gradient was in fact the better of the two throughout – it also reached a lower negative log-likelihood in 21 of those 23 reps. The comparison is now paired over the reps where both converged, which is what the data generation in that file was already written for. The corrected check passes against 1.1.5, against the previous commit and against this one.New
tests/testthat/test-rng-log-space.Rchecks that every generator stays inside(0,1), thatrbkw()reproduces the closed-form inversion of its own replayed draw, thatset.seed()still reproduces, that the sample passes a Kolmogorov-Smirnov test against its own CDF, and that simulate-then-fit runs end to end. It fails 8 assertions against 1.1.5.-
New
tests/testthat/test-argument-contract.Rcovers the two surfaces the suite had never touched: the roughly 218 documentedstop()conditions in the R wrappers, and non-finite input. It asserts every bound on every shape parameter of everyd*/p*/q*/r*, thenguard, thelog,lower.tailandlog.pguards, and thepar-length anddataguards of everyll*/gr*/hs*; then thatNA_real_,NaN,+Infand-Infare accepted without error, return one double per input element and leave their finite neighbours untouched. 526 assertions, and it fails 11 of them against 1.1.5:qkw(NA)returned 1 there, andq*(NaN)collapsed toNAin six of the seven families. The log-space quantile inversion in this release fixed both, and this file pins them.Six of its tests are marked
skip(). They assert the behaviour the functions should have for non-finite input –d*(NA)givingNArather than0,p*(+Inf)giving1rather than0,q*outside[0, 1]giving theNaNits own warning promises, and thelog = NAand NA-shape -parameter holes – and will fail 86 assertions until that defect is fixed. They are the specification, written down and executable; the fix removes theskip()line and nothing else. tests/testthat/test-derivatives-validation.Rhad 69 tests where its own header promises 70: BKw was missing Hessian config 3. Restored.tests/testthat/test-loglikelihood-functions.Rassertedexpect_true(result < 0)on all seven families, commented “Log-likelihood should be negative”. Thell*()functions return the negative log likelihood, whose sign is not a property of anything –llkw(c(1,1), .)is 0 on a uniform sample andllkw(c(2,2), .)is +2.1. Each test now asserts the defining identity,ll*(par, data) == -sum(d*(data, ..., log = TRUE)).
gkwdist 1.1.5
CRAN release: 2026-08-23
Critical Bug Fixes
dgkw()returned zero for every input (gkw.cpp,utils.h): the package’s numerical helperslog1mexp()andlog1pexp()collide with functions of the same name in R’s publicRmath.hAPI, which use the opposite convention (log(1 - exp(-x))forx >= 0). In translation units whereRmath.h’s macro was active, calls bound to R’s version, which returnsNaNfor the negative arguments used here; every density evaluation then failed its finiteness guard and returned 0. The helpers are now namedgkw_log1mexp()andgkw_log1pexp(). The sub-family densities were unaffected, as wasllgkw(), which routes throughvec_log1mexp().Log-likelihoods of EKw, KKw and BKw were wrong for data near zero (
ekw.cpp,kkw.cpp,bkw.cpp): these routines clampedv = 1 - x^alphaandw = 1 - v^betaat1e-10instead of working in log space. For smallxand moderatealpha,x^alpharounds to 1 in double precision andwcollapses to zero, so the clamp replacedlog(w) = -53bylog(1e-10) = -23. Deviations reached 6,100 log-units, which silently corrupts AIC, BIC and likelihood ratio tests. All three families now use the samegkw_log1mexp()formulation asgkw.cpp, and their scores and Hessians are expressed as ratios of logarithms.Mixed second derivatives were zeroed at degenerate parameter values (
bkw.cpp,ekw.cpp,kkw.cpp): guards of the formif (abs(p - 1) > eps)gated mixed partial derivatives that do not carry the vanishing factor. Becaused2l/dalpha dgammais obtained by differentiating(gamma-1)*log(w)once ingamma, the(gamma-1)factor is consumed and the term survives atgamma = 1.hsbkw()returned 0 where the correct value was 271.12;hsekw()andhskkw()had the same defect atbeta = 1.grkkw()clamped gradient terms at 1000 (kkw.cpp): arbitrarystd::min(..., 1000.0)caps distorted the score inbetaby up to 5%. The same clamp appeared aseffective_deltainsidellkkw(), capping the likelihood fordelta > 1000. This is the defect removed fromekw.cppin 1.1.3, which had survived here.grkkw()andhskkw()skipped thezblock atdelta = 0(kkw.cpp): the shortcut omittedsum(log(z))fromdl/ddeltaand zeroedd2l/dalpha ddelta,d2l/dbeta ddeltaandd2l/ddelta dlambda, none of which carry adeltafactor.delta = 0is a valid interior value of the likelihood.The Beta sub-family rejected
delta = 0(utils.h):check_beta_pars()requireddelta > 0, unlike the other five validators. Since the sub-family is parameterised asBeta(gamma, delta + 1),delta = 0is the legitimateBeta(gamma, 1)boundary;dbeta_(),pbeta_(),qbeta_(),rbeta_(),llbeta(),grbeta()andhsbeta()all returnedNA/Infthere.
Validation
test-boundary-derivatives.R(new): every gradient component and every Hessian entry of all seven sub-families is now compared individually against two independent references, the general GKw routines restricted to the constrained parameter point andnumDerivRichardson extrapolation, over grids that include the degenerate valuesgamma = 1,beta = 1,lambda = 1anddelta = 0and samples containing observations near zero.test-density-correctness.R(new): densities are checked to integrate to one, to agree with the general GKw density at the constrained parameter point, to match base R for the Beta and closed-form Kumaraswamy cases, and to be the derivative of the corresponding distribution function. The previous PDF tests asserted only type, length, non-negativity and finiteness, all of which a vector of zeros satisfies.
Accuracy after the fixes
Componentwise maximum relative error over 720 parameter configurations per family, against the general GKw routines and against numDeriv:
| Family | log-likelihood | gradient | Hessian |
|---|---|---|---|
| GKw | 1.5e-11 | 1.5e-08 | 1.5e-07 |
| BKw | 1.6e-12 | 3.7e-08 | 3.6e-08 |
| KKw | 3.1e-13 | 3.5e-08 | 4.9e-08 |
| EKw | 5.4e-14 | 7.8e-09 | 2.7e-08 |
| Mc | 3.5e-13 | 2.5e-09 | 9.5e-10 |
| Kw | 4.1e-15 | 7.2e-10 | 1.2e-09 |
| Beta | 7.2e-15 | 4.6e-10 | 8.9e-11 |
The residual gradient and Hessian errors are at the accuracy limit of Richardson extrapolation itself; against the GKw reference all seven families agree to 1e-13.
Documentation and Project Infrastructure
inst/paper/: the JOSS manuscript was rewritten. It now positions the package explicitly as the distribution layer of the GKw ecosystem, records that the split fromgkwregwas made at the request of JOSS reviewers during that package’s review, reports the measured validation and timing results in place of the previous unverified figures, and follows the current JOSS AI disclosure policy. The bibliography was expanded to 19 entries with DOIs verified against Crossref.CONTRIBUTING.mdandCODE_OF_CONDUCT.md(new): contribution workflow, support expectations, governance, and the testing standard numerical contributions are held to. The contributing guide documents the cross-check that makes bug reports actionable: every sub-family routine must agree with the general GKw routine evaluated at the corresponding constrained parameter point.README: the claim that the C++ routines are “10-50x faster than equivalent R implementations” was replaced with measured figures. The original benchmark compared-sum(log(dkw(x, 2, 3)))againstllkw(), which is C++ against C++ plus R loop overhead, and gives roughly 3x. The genuine gain is in the derivatives: the analytical score is about 9x faster than Richardson extrapolation and the analytical Hessian about 38x faster at n = 20,000.inst/CITATION: updated to version 1.1.5 and pointed at the CRAN canonical URL; it had been stale at version 1.0.8.Test coverage rose from 70.8% to 74.2% of combined R and C++ lines. The largest single gain is in
src/gkw.cpp(42.9% to 71.5%), which reflects thatdgkw()now executes its density computation instead of falling through to its finiteness guard.
gkwdist 1.1.4
CRAN release: 2026-05-28
CRAN Fix
-
test-mle-performance.R: Addedskip_on_cran()to all timing-based benchmark tests. These tests compare wall-clock times of analytical vs. numerical gradients and are inherently unreliable on shared/loaded CRAN check machines, causing spuriousERRORresults. The tests remain available for local development.
gkwdist 1.1.3
CRAN release: 2026-05-21
Bug Fixes
llgkw()invalid parameter return (gkw.cpp): Fixed critical error where the negative log-likelihood returnedR_NegInf(−∞) for invalid parameters instead ofR_PosInf(+∞). Gradient-based MLE optimizers interpret −∞ as a global minimum, causing them to converge to the invalid boundary rather than the true MLE.gkwinit.cpp— delta validation (gkwinit.cpp): Fixed internalgkw_pdf()rejectingdelta = 0(a valid GKw parameter value) due to a strictdelta <= 0check that should have beendelta < 0.gkwinit.cpp— EKw/Kw sub-family PDF mapping (gkwinit.cpp): Fixedekw_pdf()andkw_pdf()passingdelta = 1instead of the correctdelta = 0when delegating togkw_pdf(). EKw and Kw are GKw sub-families withdelta = 0, notdelta = 1. This produced wrong starting values for MLE of these families.hsbkw()— v^(β−1) computation (bkw.cpp): Fixed the Hessian of the BKw negative log-likelihood returning a wrong value for β < 1. The ternary expression(beta > 1.0) ? v_beta/v : 1.0coincidentally produces the correct result for β = 1 but is wrong for all 0 < β < 1. Replaced with the exact formulasafe_exp((beta - 1.0) * ln_v).
Numerical Stability
safe_exp()underflow scaling (utils.h): Fixed a systematic 10× error in the moderate-underflow branch. The previous implementation usedDBL_MIN_SAFE * exp(x − log(DBL_MIN))whereDBL_MIN_SAFE = 10 * DBL_MIN, yielding10 * exp(x)instead ofexp(x). The fix usesDBL_MIN * exp(x − log(DBL_MIN)) = exp(x)exactly.dgkw()silent boundary truncation removed (gkw.cpp): Removed a block that silently skipped data points withinSQRT_EPSILON^(1/α)of 0 or 1, returning density 0 for those points without warning. The log-space computation handles near-boundary values correctly without this truncation.llekw()/grekw()— lambda clamping removed (ekw.cpp): Removed the arbitrary caplambda_factor = min(lambda_factor, 1000)applied to gradient and Hessian terms when λ > 1000. This distorted optimization for large-λ scenarios and produced incorrect standard errors.
Code Quality
gkwinit.cpp: Removedusing namespace Rcpp;at file scope; replaced with explicitRcpp::qualifications. Added NA/NaN filtering before moment computation to prevent silent corruption when input data contains missing values.bkw.cpp: Removed spurioustry/catchblocks wrappingRcpp::as<arma::vec>()conversions ingrbkw()andhsbkw(). These conversions cannot throw in this context and the silent fallback masked type errors.gkw.cpp/ekw.cpp: Refactored Hessian accumulation to build only the upper triangle inside the observation loop and symmetrize once afterwards witharma::symmatu(), eliminating O(n × p²) redundant assignments.utils.h—vec_safe_pow()UB guard: Added guard preventing undefined behaviour when casting largey_roundedvalues (>INT_MAX) tointfor odd-exponent sign detection.utils.h—vec_safe_pow()SIMD fast path: Added an early-return patharma::exp(y * arma::log(x))for the common case (y > 0, all x > 0) that is fully auto-vectorizable, improving throughput in gradient/Hessian evaluation.
gkwdist 1.1.2
CRAN release: 2026-01-08
Code Cleanup and Testing Enhancement
C++ Code Cleanup
-
Removed legacy commented code: Cleaned up all C++ source files (
gkw.cpp,bkw.cpp,kkw.cpp,ekw.cpp,kw.cpp,bpmc.cpp,beta_.cpp) by removing old commented-out implementations that were kept for reference. -
Code formatting: Improved R wrapper formatting with consistent indentation and alignment in
.Call()invocations and roxygen examples.
New Test Suites
-
Analytical derivatives validation (
test-derivatives-validation.R):- 70 comprehensive tests validating gradient (
gr*) and Hessian (hs*) functions - Compares analytical derivatives against numerical differentiation via
numDeriv - Covers all 7 subfamilies: GKw, BKw, KKw, EKw, Mc, Kw, Beta
- Multiple parameter configurations per subfamily for robustness
- 70 comprehensive tests validating gradient (
-
MLE performance benchmarks (
test-mle-performance.R):- Compares optimization efficiency across three scenarios: numerical-only, analytical gradient, and analytical gradient + Hessian
- Validates that analytical derivatives provide equivalent or better accuracy
- Tests convergence rates and computational time across all distribution families
gkwdist 1.1.1
CRAN release: 2025-11-27
Major Refactoring Release
This release represents a comprehensive refactoring of the entire package codebase, focusing on numerical stability, code consistency, and maintainability.
C++ Backend Overhaul
-
Unified utility functions: Introduced
utils.hheader providing numerically stable implementations of critical functions:-
log1mexp(): Stable computation of log(1 - exp(x)) using Mächler (2012) methodology -
log1pexp(): Overflow-protected computation of log(1 + exp(x)) -
safe_log(),safe_exp(),safe_pow(): Protected arithmetic operations with graceful handling of edge cases - Vectorized versions (
vec_safe_log,vec_log1mexp, etc.) for efficient array operations
-
Consistent parameter validation: All distribution families now use dedicated parameter checkers (
check_pars(),check_kw_pars(),check_ekw_pars(), etc.) that properly handle NaN, Inf, and boundary conditions.-
Complete documentation: All C++ source files now include comprehensive Doxygen-style documentation headers describing:
- Mathematical formulas for PDF, CDF, quantile, and random generation
- Parameter constraints and special cases
- Numerical stability considerations
- Relationship to parent GKw distribution
Bug Fixes
Fixed critical bug in
qgkw(): Corrected logic error wherelower_tailtransformation was incorrectly applied whenlog_p = TRUE. The probability is now properly converted to linear scale before tail adjustment.Fixed gradient calculation in
grkkw(): Resolved issue wherelog_zwas not recomputed after clampingzto minimum threshold, causing corrupted gradient values near boundaries.Fixed Hessian calculation in
hsmc(): Corrected sign errors and formula for the lambda component of the Hessian matrix for the Beta-Power/McDonald distribution.Fixed gradient signs in
grmc(): Ensured consistent computation of log-likelihood gradient before negation for optimization.
Code Quality Improvements
Eliminated unused variables: Removed declared but unused constants (
exp_threshold) and intermediate variables across all distribution files.Removed redundant calculations: Streamlined computations, notably in
pgkw()where logarithm was computed twice for the same quantity.Simplified parameter recycling: Replaced double-modulo indexing pattern (
idx = i % k; vec[idx % vec.n_elem]) with direct single-modulo access (vec[i % vec.n_elem]) in random generation functions.Standardized function signatures: All distribution functions now follow consistent patterns for parameter order, validation, and return value handling.
R Wrapper Layer
-
Complete separation of R and C++ interfaces: All exported R functions now serve as wrappers around internal C++ implementations (
.dgkw_cpp,.pgkw_cpp, etc.), providing:- Enhanced input validation with informative error messages
- Consistent argument checking across all distribution families
- Proper NA/NaN propagation
- Documentation accessible via standard R help system
Distribution Families
All seven distribution families have been refactored with identical improvements:
| Distribution | Parameters | File |
|---|---|---|
| Generalized Kumaraswamy (GKw) | α, β, γ, δ, λ | gkw.cpp |
| Kumaraswamy-Kumaraswamy (KKw) | α, β, δ, λ | kkw.cpp |
| Beta-Kumaraswamy (BKw) | α, β, γ, δ | bkw.cpp |
| Exponentiated Kumaraswamy (EKw) | α, β, λ | ekw.cpp |
| Beta-Power/McDonald (BP/Mc) | γ, δ, λ | bpmc.cpp |
| Kumaraswamy (Kw) | α, β | kw.cpp |
| Beta (GKw-style) | γ, δ | beta.cpp |
Each family includes: density (d*), distribution (p*), quantile (q*), random generation (r*), negative log-likelihood (ll*), gradient (gr*), and Hessian (hs*) functions.
gkwdist 1.0.5
Documentation Improvements
-
Enhanced Examples for Likelihood Functions: All
ll*,gr*, andhs*functions now include comprehensive examples demonstrating:- Maximum likelihood estimation with analytical gradients
- Univariate profile likelihoods with confidence thresholds
- 2D likelihood surfaces with confidence regions (90%, 95%, 99%)
- Confidence ellipses with marginal intervals for parameter pairs
- Numerical vs analytical derivative verification
- Likelihood ratio tests and score tests
-
Professional Visualization Standards:
- Consistent color scheme across all examples
- Grid-adaptive algorithms for computational efficiency
- Base R only - no external dependencies required
Complete Coverage: Enhanced documentation for all distribution families (Kw, EKw, KKw, GKw) covering 2 to 5 parameters
Theoretical References: Documentation cites foundational work by Carrasco et al. (2010), Jones (2009), Kumaraswamy (1980), and standard inference theory from Casella & Berger (2002)
gkwdist 1.0.1
Major Improvements
Enhanced gkwgetstartvalues() Function
-
NEW: Added
familyparameter to support all distribution families- Automatically returns correct number of parameters for each family
- Family-specific initial value strategies for better convergence
- Supported families:
"gkw","bkw","kkw","ekw","mc","kw","beta" - Case-insensitive family names for user convenience
Documentation Enhancements
-
README.md: Complete rewrite with mathematical rigor
- All LaTeX formulas corrected and verified for proper rendering
- Eight comprehensive examples using
optim()with analytical gradients - Corrected function signatures: all
ll*(),gr*(), andhs*()functions use(par, data)signature - Added performance benchmarks demonstrating 10-50× speedup with C++ implementation
- Hierarchical structure diagram for all distribution families
- Model selection workflow and practical guidelines
- Removed all references to deprecated
gkwfit()function
Bug Fixes
- Fixed function call signatures in all README examples to match actual implementation
- Corrected parameter passing in optimization examples (now consistently use
(par, data)) - Fixed LaTeX rendering issues with
\left/\rightdelimiters in GitHub Markdown
Testing
-
NEW: Comprehensive test suite using
testthat- 100+ tests covering all exported functions
- Tests for all 7 distribution families (GKw, BKw, KKw, EKw, MC, Kw, Beta)
- PDF, CDF, quantile, and random generation tests
- Log-likelihood, gradient, and Hessian validation
- Parameter recovery tests with MLE
- Edge cases and boundary condition handling
- Integration tests for PDF-CDF consistency
Performance
- All functions implemented in C++ for maximum computational efficiency
- Analytical derivatives (gradient and Hessian) provide exact computations
- Optimized numerical stability for extreme parameter values
Notes
- This is the initial CRAN submission
- Package focuses exclusively on distribution functions (no high-level fitting interface)
- Companion package
gkwregprovides regression modeling capabilities - All user-facing functions maintain backward compatibility
- C++ implementation uses RcppArmadillo for linear algebra operations
- Analytical functions use robust log-scale computations to prevent overflow/underflow
- Random generation uses inverse CDF method where closed-form solutions exist
gkwdist 0.1.0
New Features
- Initial CRAN release
- Generalized Kumaraswamy distribution (5 parameters)
- Six nested sub-families: Beta, Kumaraswamy, Exponentiated-Kumaraswamy, Kumaraswamy-Kumaraswamy, Beta-Kumaraswamy, and McDonald distributions
- Complete set of distribution functions (d/p/q/r)
- Log-likelihood, gradient, and Hessian functions for all families