SHiP Random
Random number helpers shared by the SHiP software
Loading...
Searching...
No Matches
philox_rng.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 CERN for the benefit of the SHiP Collaboration
2//
3// SPDX-License-Identifier: LGPL-3.0-or-later
4
20
21#pragma once
22
23#include <Random123/philox.h>
24#include <cmath>
25#include <cstdint>
26#include <numbers>
27#include <stdexcept>
28
29namespace SHiP::random {
30
31// Every multiply-add below is written as std::fma. Left as a * b + c, the
32// compiler may or may not fuse it depending on the target and flags (aarch64
33// and x86 with -mfma fuse it, plain x86-64 does not), so the same seed would
34// give different bits on different machines. std::fma rounds once everywhere.
35
36// Resolution of the uniform draws feeding the derived distributions
37// (gaussian, gamma_wh, beta_dist). Bits32 is the historical default and keeps
38// existing digitisation output bit-identical; Bits53 uses uniform53() and
39// extends the Box–Muller tail from ~6.7σ to ~8.6σ at the cost of twice as
40// many Philox words per deviate.
41enum class Precision : std::uint8_t { Bits32, Bits53 };
42
43class PhiloxRng {
44 public:
46 // with the same seed draw uncorrelated sequences. substream initializes
47 // the counter, giving each (seed, key_hi, substream) triple a disjoint
51 // The sub-stream index is 64 bits wide, split over counter words 1 (low)
52 // and 2 (high), so event numbers beyond 2^32 don't wrap onto earlier
53 // events. Indices below 2^32 leave word 2 at zero, as before.
54 explicit PhiloxRng(std::uint32_t seed, std::uint32_t key_hi = 0xBEEFCAFE,
55 std::uint64_t substream = 0)
56 : key_{{seed, key_hi}},
57 ctr_{{0, static_cast<std::uint32_t>(substream),
58 static_cast<std::uint32_t>(substream >> 32u), 0}} {}
59
61 double uniform() {
62 if (idx_ >= 4) {
63 buf_ = rng_(ctr_, key_);
64 ctr_[0]++;
65 idx_ = 0;
66 }
67 return buf_[idx_++] * (1.0 / 4294967296.0);
68 }
69
71 double uniform(double lo, double hi) { return std::fma(hi - lo, uniform(), lo); }
72
78 double uniform53() {
79 if (idx_ > 2) {
80 buf_ = rng_(ctr_, key_);
81 ctr_[0]++;
82 idx_ = 0;
83 }
84 std::uint64_t const hi = buf_[idx_++];
85 std::uint64_t const lo = buf_[idx_++];
86 std::uint64_t const bits = (hi << 32U) | lo;
87 return static_cast<double>(bits >> 11U) * (1.0 / 9007199254740992.0);
88 }
89
91 double uniform53(double lo, double hi) { return std::fma(hi - lo, uniform53(), lo); }
92
96 double gaussian(double mean, double sigma, Precision precision = Precision::Bits32) {
97 double const u1 = 1.0 - draw(precision);
98 double const u2 = draw(precision);
99 return std::fma(sigma * std::sqrt(-2.0 * std::log(u1)),
100 std::cos(2.0 * std::numbers::pi * u2), mean);
101 }
102
103 // An approximation of a gamma function - Wilson-Hilferty (1931)
104 // This rapidly approaches a Gaussian by alpha ~ 10
105 double gamma_wh(double alpha, double scale = 1, Precision precision = Precision::Bits32) {
106 if (alpha < 1.0) {
107 throw std::invalid_argument(
108 "Provided alpha for gamma function approximation is less than 1. Consider using an "
109 "exact gamma function for this instance.");
110 }
111 const double a = 1.0 - (1.0 / (9.0 * alpha));
112 const double b = 1.0 / (3.0 * std::sqrt(alpha));
113
114 double x = 0.0;
115 while (x <= 0.0) {
116 x = std::fma(b, gaussian(0.0, 1.0, precision), a);
117 }
118
119 return scale * alpha * x * x * x;
120 }
121
122 // An approximate beta distribution. Uses approximations of the gamma function for maximum
123 // speed. Not appropriate for very small alpha or zeta.
124 double beta_dist_approx(double alpha, double zeta, Precision precision = Precision::Bits32) {
125 const double x = gamma_wh(alpha, 1, precision);
126 const double y = gamma_wh(zeta, 1, precision);
127
128 if (x == 0.0 && y == 0.0) {
129 return alpha >= zeta ? 1.0 : 0.0; // both underflowed
130 }
131 if (x >= y) {
132 return 1.0 / (1.0 + (y / x));
133 }
134 const double r = x / y;
135 return r / (1.0 + r);
136 }
137
138 private:
139 double draw(Precision precision) {
140 return precision == Precision::Bits53 ? uniform53() : uniform();
141 }
142
143 r123::Philox4x32 rng_;
144 r123::Philox4x32::key_type key_;
145 r123::Philox4x32::ctr_type ctr_;
146 r123::Philox4x32::ctr_type buf_{};
147 int idx_ = 4;
148};
149
150} // namespace SHiP::random
Definition philox_rng.hpp:43
double uniform()
Uniform in [0, 1), with 32 bits of resolution.
Definition philox_rng.hpp:61
double gamma_wh(double alpha, double scale=1, Precision precision=Precision::Bits32)
Definition philox_rng.hpp:105
PhiloxRng(std::uint32_t seed, std::uint32_t key_hi=0xBEEFCAFE, std::uint64_t substream=0)
key_hi selects an independent stream, so different generators seeded
Definition philox_rng.hpp:54
double uniform53()
Uniform in [0, 1) with full double resolution: combines two consecutive 32-bit Philox words into a 64...
Definition philox_rng.hpp:78
double gaussian(double mean, double sigma, Precision precision=Precision::Bits32)
Gaussian deviate by the Box–Muller transform, cosine branch only (two uniforms per deviate).
Definition philox_rng.hpp:96
double beta_dist_approx(double alpha, double zeta, Precision precision=Precision::Bits32)
Definition philox_rng.hpp:124
double uniform53(double lo, double hi)
Uniform in [lo, hi) with full double resolution.
Definition philox_rng.hpp:91
double uniform(double lo, double hi)
Uniform in [lo, hi), with 32 bits of resolution.
Definition philox_rng.hpp:71
Definition philox_rng.hpp:29
Precision
Definition philox_rng.hpp:41