dasmig-core 1.2.0
Shared random engine, distributions and data tiers for the dasmig generator libraries
Loading...
Searching...
No Matches
distributions.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_CORE_DISTRIBUTIONS_HPP
5#define DASMIG_CORE_DISTRIBUTIONS_HPP
6
7#include "math.hpp"
8#include "rng.hpp"
9
10#include <algorithm>
11#include <cmath>
12#include <concepts>
13#include <cstddef>
14#include <cstdint>
15#include <initializer_list>
16#include <iterator>
17#include <limits>
18#include <numeric>
19#include <ranges>
20#include <span>
21#include <stdexcept>
22#include <utility>
23#include <vector>
24
25/// @file distributions.hpp
26/// @brief Random distributions with output fixed by the core, not by the
27/// standard library.
28/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
29///
30/// `std::uniform_int_distribution`, `std::normal_distribution`,
31/// `std::discrete_distribution` and `std::shuffle` use different algorithms
32/// in libc++, libstdc++ and the MSVC STL, so the same seed gives different
33/// results on different platforms. Every algorithm here is specified exactly
34/// (see each function) and is part of the core's output contract.
35///
36/// Each function consumes a fixed number of engine values for given
37/// arguments, except where rejection sampling is documented.
38
39
40namespace dasmig::core
41{
42
43/// @cond INTERNAL
44namespace detail
45{
46
47// High 64 bits of the 128-bit product a * b, without a 128-bit type.
48// The product commutes, so swapped arguments cannot be a mistake.
49[[nodiscard]] constexpr std::uint64_t mul_hi64(
50 std::uint64_t a, std::uint64_t b) noexcept // NOLINT(bugprone-easily-swappable-parameters)
51{
52 constexpr std::uint64_t mask = 0xffffffffULL;
53 const std::uint64_t a_lo = a & mask;
54 const std::uint64_t a_hi = a >> 32U;
55 const std::uint64_t b_lo = b & mask;
56 const std::uint64_t b_hi = b >> 32U;
57 const std::uint64_t p0 = a_lo * b_lo;
58 const std::uint64_t p1 = a_lo * b_hi;
59 const std::uint64_t p2 = a_hi * b_lo;
60 const std::uint64_t p3 = a_hi * b_hi;
61 const std::uint64_t mid = (p0 >> 32U) + (p1 & mask) + (p2 & mask);
62 return p3 + (p1 >> 32U) + (p2 >> 32U) + (mid >> 32U);
63}
64
65} // namespace detail
66/// @endcond
67
68/// @brief Uniform integer in [0, n).
69///
70/// Lemire's multiply-and-reject method (2019): the result is the high 64 bits
71/// of `r() * n`, redrawn while the low 64 bits are below `2^64 mod n`.
72/// Unbiased; consumes one value, rarely more.
73///
74/// @param r Engine.
75/// @param n Exclusive upper bound.
76/// @return A value in [0, n).
77/// @throws std::invalid_argument If @p n is 0.
78[[nodiscard]] constexpr std::uint64_t bounded(rng& r, std::uint64_t n)
79{
80 if (n == 0)
81 {
82 throw std::invalid_argument("bounded: n must be positive");
83 }
84 std::uint64_t x = r();
85 std::uint64_t low = x * n;
86 if (low < n)
87 {
88 const std::uint64_t threshold = (0 - n) % n;
89 while (low < threshold)
90 {
91 x = r();
92 low = x * n;
93 }
94 }
95 return detail::mul_hi64(x, n);
96}
97
98/// @brief Uniform integer in [lo, hi], for any integral type except bool.
99///
100/// `lo + bounded(r, hi - lo + 1)` computed in 64-bit unsigned arithmetic;
101/// the full 64-bit range takes one raw engine value.
102///
103/// @tparam T Integral type.
104/// @param r Engine.
105/// @param lo Inclusive lower bound.
106/// @param hi Inclusive upper bound.
107/// @return A value in [lo, hi].
108/// @throws std::invalid_argument If @p lo > @p hi.
109template <std::integral T>
110 requires(!std::same_as<T, bool>)
111[[nodiscard]] constexpr T uniform_int(rng& r, T lo, T hi)
112{
113 if (lo > hi)
114 {
115 throw std::invalid_argument("uniform_int: lo > hi");
116 }
117 const std::uint64_t span =
118 static_cast<std::uint64_t>(hi) - static_cast<std::uint64_t>(lo);
119 const std::uint64_t offset =
120 span == std::numeric_limits<std::uint64_t>::max() ? r()
121 : bounded(r, span + 1);
122 return static_cast<T>(static_cast<std::uint64_t>(lo) + offset);
123}
124
125/// @brief Uniform double in [0, 1).
126///
127/// The top 53 bits of one engine value, times 2^-53. Exact.
128///
129/// @param r Engine.
130/// @return A multiple of 2^-53 in [0, 1).
131[[nodiscard]] constexpr double uniform01(rng& r) noexcept
132{
133 return static_cast<double>(r() >> 11U) * 0x1.0p-53;
134}
135
136/// @cond INTERNAL
137namespace detail
138{
139
140// The weighted choice shared by discrete and weighted_table: the first index
141// whose cumulative weight exceeds uniform01(r) * total. Rounding can make the
142// target reach the total; then the last index with a positive weight.
143[[nodiscard]] inline std::size_t pick_cumulative(rng& r,
144 std::span<const double> cumulative,
145 std::size_t last_positive)
146{
147 const double target = uniform01(r) * cumulative.back();
148 const auto it = std::ranges::upper_bound(cumulative, target);
149 if (it == cumulative.end()) return last_positive;
150 return static_cast<std::size_t>(it - cumulative.begin());
151}
152
153} // namespace detail
154/// @endcond
155
156/// @brief Uniform double in [lo, hi).
157///
158/// `lo + (hi - lo) * uniform01(r)`, replaced by the largest double below
159/// @p hi if rounding reaches @p hi. Returns @p lo when lo == hi. Consumes one
160/// value.
161///
162/// @param r Engine.
163/// @param lo Inclusive lower bound.
164/// @param hi Exclusive upper bound.
165/// @return A value in [lo, hi), or lo when lo == hi.
166/// @throws std::invalid_argument If a bound is not finite, lo > hi, or
167/// hi - lo overflows.
168[[nodiscard]] inline double uniform_real(rng& r, double lo, double hi)
169{
170 const double width = hi - lo;
171 if (!std::isfinite(lo) || !std::isfinite(hi) || !(lo <= hi) ||
172 !std::isfinite(width))
173 {
174 throw std::invalid_argument("uniform_real: need finite lo <= hi");
175 }
176 const double u = uniform01(r);
177 if (width == 0.0) return lo;
178 const double x = lo + math::fenced(width * u);
179 return x < hi ? x : std::nextafter(hi, lo);
180}
181
182/// @brief True with probability @p p.
183///
184/// `uniform01(r) < p`. Consumes one value, also for p = 0 and p = 1.
185///
186/// @param r Engine.
187/// @param p Probability of true.
188/// @return The outcome.
189/// @throws std::invalid_argument If @p p is not in [0, 1].
190[[nodiscard]] inline bool bernoulli(rng& r, double p)
191{
192 if (std::isnan(p) || p < 0.0 || p > 1.0)
193 {
194 throw std::invalid_argument("bernoulli: p must be in [0, 1]");
195 }
196 return uniform01(r) < p;
197}
198
199/// @brief Normally distributed double.
200///
201/// Marsaglia's polar method: draw u and v as `2 * uniform01(r) - 1` until
202/// s = u*u + v*v is in (0, 1), then return
203/// `mean + sd * (u * sqrt((-2 * log(s)) / s))`, with math::log. The second
204/// variate (from v) is discarded so no state is kept between calls.
205/// Consumes 2 values per attempt (on average 2.55 values).
206///
207/// @param r Engine.
208/// @param mean Mean.
209/// @param sd Standard deviation (0 returns @p mean).
210/// @return The variate.
211/// @throws std::invalid_argument If @p mean or @p sd is not finite, or
212/// @p sd is negative.
213[[nodiscard]] inline double normal(rng& r, double mean, double sd)
214{
215 if (!std::isfinite(mean) || !std::isfinite(sd) || sd < 0.0)
216 {
217 throw std::invalid_argument("normal: need finite mean and sd >= 0");
218 }
219 double u = 0.0;
220 double s = 0.0;
221 for (;;)
222 {
223 u = (2.0 * uniform01(r)) - 1.0;
224 const double v = (2.0 * uniform01(r)) - 1.0;
225 s = math::fenced(u * u) + math::fenced(v * v);
226 if (s < 1.0 && s != 0.0) break;
227 }
228 const double factor = std::sqrt((-2.0 * math::log(s)) / s);
229 return mean + math::fenced(sd * (u * factor));
230}
231
232/// @brief Log-normally distributed double.
233///
234/// `math::exp(normal(r, mu, sigma))`.
235///
236/// @param r Engine.
237/// @param mu Mean of the underlying normal.
238/// @param sigma Standard deviation of the underlying normal.
239/// @return The variate (positive).
240/// @throws std::invalid_argument As normal().
241[[nodiscard]] inline double lognormal(rng& r, double mu, double sigma)
242{
243 return math::exp(normal(r, mu, sigma));
244}
245
246/// @brief Index chosen with probability proportional to a weight.
247///
248/// Keeps the cumulative sums of the weights (added left to right). A draw is
249/// the first index whose cumulative sum exceeds `uniform01(r) * total`, so a
250/// zero weight is never chosen. Consumes one value.
251///
252/// @code
253/// dasmig::core::discrete eye_colour{{0.7, 0.2, 0.1}};
254/// std::size_t i = eye_colour(r);
255/// @endcode
257{
258 public:
259 /// @brief Build from weights.
260 /// @param weights Non-negative, finite weights, at least one positive.
261 /// @throws std::invalid_argument If the weights are empty, a weight is
262 /// negative or not finite, or they sum to 0 or overflow.
263 explicit discrete(std::span<const double> weights)
264 {
265 if (weights.empty())
266 {
267 throw std::invalid_argument("discrete: no weights");
268 }
269 _cumulative.reserve(weights.size());
270 double total = 0.0;
271 for (std::size_t i = 0; i < weights.size(); ++i)
272 {
273 const double w = weights[i];
274 if (!std::isfinite(w) || w < 0.0)
275 {
276 throw std::invalid_argument(
277 "discrete: weights must be finite and non-negative");
278 }
279 total += w;
280 if (w > 0.0) _last_positive = i;
281 _cumulative.push_back(total);
282 }
283 if (!(total > 0.0) || !std::isfinite(total))
284 {
285 throw std::invalid_argument(
286 "discrete: weights must have a positive, finite sum");
287 }
288 }
289
290 /// @brief Build from a list of weights.
291 /// @param weights Non-negative, finite weights, at least one positive.
292 /// @throws std::invalid_argument As the span constructor.
293 discrete(std::initializer_list<double> weights)
294 : discrete{std::span<const double>{weights.begin(), weights.size()}}
295 {
296 }
297
298 /// @brief Draw an index.
299 /// @param r Engine.
300 /// @return An index in [0, size()) with a positive weight.
301 [[nodiscard]] std::size_t operator()(rng& r) const
302 {
303 return detail::pick_cumulative(r, _cumulative, _last_positive);
304 }
305
306 /// @brief Number of weights.
307 /// @return The number of indices.
308 [[nodiscard]] std::size_t size() const noexcept { return _cumulative.size(); }
309
310 /// @brief Sum of the weights.
311 /// @return The total weight.
312 [[nodiscard]] double total() const noexcept { return _cumulative.back(); }
313
314 /// @brief Probability of an index.
315 /// @param i Index.
316 /// @return weight(i) / total().
317 /// @throws std::out_of_range If @p i >= size().
318 [[nodiscard]] double probability(std::size_t i) const
319 {
320 const double before = i == 0 ? 0.0 : _cumulative.at(i - 1);
321 return (_cumulative.at(i) - before) / total();
322 }
323
324 private:
325 std::vector<double> _cumulative;
326 std::size_t _last_positive{0};
327};
328
329/// @brief Shuffle a random-access range in place.
330///
331/// Fisher-Yates from the end: for i = n-1 down to 1, swap element i with
332/// element bounded(r, i + 1). Consumes n - 1 values (rarely more).
333///
334/// @param r Engine.
335/// @param range Range to shuffle.
336template <std::ranges::random_access_range R>
337 requires std::ranges::sized_range<R> &&
338 std::permutable<std::ranges::iterator_t<R>>
339constexpr void shuffle(rng& r, R&& range) // NOLINT(cppcoreguidelines-missing-std-forward): permuted in place, never moved
340{
341 auto first = std::ranges::begin(range);
342 const auto n = static_cast<std::uint64_t>(std::ranges::size(range));
343 for (std::uint64_t i = n; i-- > 1;)
344 {
345 const std::uint64_t j = bounded(r, i + 1);
346 std::ranges::iter_swap(first + static_cast<std::ptrdiff_t>(i),
347 first + static_cast<std::ptrdiff_t>(j));
348 }
349}
350
351/// @brief Choose k distinct indices from [0, n), in random order.
352///
353/// Partial Fisher-Yates from the front: start from 0..n-1 and for i = 0 to
354/// k-1 swap element i with element i + bounded(r, n - i); return the first k.
355/// Consumes k values (rarely more). Allocates n indices.
356///
357/// @param r Engine.
358/// @param n Population size.
359/// @param k Sample size.
360/// @return k distinct indices.
361/// @throws std::invalid_argument If @p k > @p n.
362[[nodiscard]] inline std::vector<std::size_t> sample_indices(rng& r,
363 std::size_t n,
364 std::size_t k)
365{
366 if (k > n)
367 {
368 throw std::invalid_argument("sample_indices: k > n");
369 }
370 // A loop, not std::ranges::iota: older libc++ releases lack it.
371 std::vector<std::size_t> indices(n);
372 for (std::size_t i = 0; i < n; ++i)
373 {
374 indices[i] = i;
375 }
376 for (std::size_t i = 0; i < k; ++i)
377 {
378 const auto j = i + static_cast<std::size_t>(bounded(r, n - i));
379 std::swap(indices[i], indices[j]);
380 }
381 indices.resize(k);
382 return indices;
383}
384
385/// @brief A uniformly chosen element of a non-empty range.
386///
387/// `range[bounded(r, size)]`. Consumes one value (rarely more).
388///
389/// @param r Engine.
390/// @param range Non-empty random-access range (an lvalue: the result refers
391/// into it).
392/// @return A reference to the chosen element.
393/// @throws std::invalid_argument If @p range is empty.
394template <std::ranges::random_access_range R>
395 requires std::ranges::sized_range<R>
396[[nodiscard]] constexpr decltype(auto) pick(rng& r, R& range)
397{
398 const auto n = static_cast<std::uint64_t>(std::ranges::size(range));
399 if (n == 0)
400 {
401 throw std::invalid_argument("pick: empty range");
402 }
403 return std::ranges::begin(range)[static_cast<std::ptrdiff_t>(bounded(r, n))];
404}
405
406} // namespace dasmig::core
407
408
409#endif // DASMIG_CORE_DISTRIBUTIONS_HPP
Index chosen with probability proportional to a weight.
double probability(std::size_t i) const
Probability of an index.
double total() const noexcept
Sum of the weights.
discrete(std::initializer_list< double > weights)
Build from a list of weights.
std::size_t operator()(rng &r) const
Draw an index.
std::size_t size() const noexcept
Number of weights.
discrete(std::span< const double > weights)
Build from weights.
The core random engine: xoshiro256** (Blackman and Vigna 2018).
Definition rng.hpp:108
std::vector< std::size_t > sample_indices(rng &r, std::size_t n, std::size_t k)
Choose k distinct indices from [0, n), in random order.
constexpr double uniform01(rng &r) noexcept
Uniform double in [0, 1).
constexpr void shuffle(rng &r, R &&range)
Shuffle a random-access range in place.
double lognormal(rng &r, double mu, double sigma)
Log-normally distributed double.
double uniform_real(rng &r, double lo, double hi)
Uniform double in [lo, hi).
constexpr decltype(auto) pick(rng &r, R &range)
A uniformly chosen element of a non-empty range.
constexpr std::uint64_t bounded(rng &r, std::uint64_t n)
Uniform integer in [0, n).
bool bernoulli(rng &r, double p)
True with probability p.
double normal(rng &r, double mean, double sd)
Normally distributed double.
constexpr T uniform_int(rng &r, T lo, T hi)
Uniform integer in [lo, hi], for any integral type except bool.
Floating-point functions that give bit-identical results on every compiler, standard library and plat...
The random engine every generator library uses: xoshiro256** seeded through SplitMix64,...