rng: model chance(), the only entry point that can cost zero words
Both early-outs return without touching the generator: p <= 0 false, p >= 1 true, otherwise exactly one word compared with a strict <. The zero-cost cases decide stream alignment wherever a caller's probability climbs -- the spy counter-mission adds 0.2f per failed turn and stops drawing entirely from the fifth. A model without the early-outs drifts one word from there on, for ever. NaN takes neither early-out in the original, so it draws and returns false; reproduced rather than smoothed over.
This commit is contained in:
parent
35b05ebc18
commit
989c692c53
3 changed files with 82 additions and 0 deletions
|
|
@ -112,6 +112,20 @@ Three consequences worth stating separately, because each one is a way a ledger
|
|||
fraction of words; `test_rng.cpp` asserts the two models are distinguishable so the
|
||||
shortcut cannot creep back in unnoticed.
|
||||
|
||||
**The probability test (`chance`) is the only entry point that can cost nothing, and that is
|
||||
load-bearing.** `p <= 0` returns false and `p >= 1` returns true, each without touching the
|
||||
generator; in between it spends exactly one word and compares with a **strict** `<`. The zero-cost
|
||||
cases decide stream alignment wherever a caller's probability *climbs*: the spy counter-mission
|
||||
adds `0.2f` per failed turn, so it spends a word for four turns and then nothing at all from the
|
||||
fifth on. A model without the early-outs is one word out of step from that point forward, for
|
||||
ever — and one word of drift is the whole determinism claim.
|
||||
|
||||
NaN is deliberately not special-cased. Both early-out tests are written in the original as pairs
|
||||
of ordered comparisons, which a NaN makes false, so a NaN probability falls through, **draws a
|
||||
word**, and returns false. That word is reproduced here: a caller that computes a NaN probability
|
||||
really does perturb the stream, and smoothing it over would hide a genuine divergence rather than
|
||||
prevent one.
|
||||
|
||||
`int_range_bell` is triangular, not uniform: the span is split into `h/2` and `h - h/2`
|
||||
(truncating toward zero) and each half is drawn separately, so the sum peaks in the middle.
|
||||
The two bounds reach the draw as unsigned, so an inverted range does not produce an empty
|
||||
|
|
|
|||
|
|
@ -80,6 +80,32 @@ public:
|
|||
// the first bound huge rather than empty; that is reproduced, not corrected.
|
||||
int32_t int_range_bell(int32_t lo, int32_t hi);
|
||||
|
||||
// A weighted coin, and the only entry point that can cost ZERO words. Both
|
||||
// early-outs return without touching the generator:
|
||||
//
|
||||
// p <= 0 -> false, no draw
|
||||
// p >= 1 -> true, no draw
|
||||
// else -> next_float() < p (STRICT less-than), exactly one word
|
||||
//
|
||||
// The zero-cost cases are not a detail. A caller that accumulates its
|
||||
// probability -- the spy counter-mission adds 0.2f per failed turn -- spends a
|
||||
// word for four turns and then NOTHING on the fifth, and a reimplementation
|
||||
// without the early-out is one word out of step from there on, permanently.
|
||||
// The comparison direction matters for the same reason: `<` and `<=` differ on
|
||||
// the measure-zero case where the drawn float lands exactly on p, and the
|
||||
// original uses `<`.
|
||||
//
|
||||
// NaN is deliberately not special-cased. A NaN p fails both early-out tests in
|
||||
// the original (each is written as a pair of ordered comparisons that a NaN
|
||||
// makes false), so it DRAWS A WORD and then returns false, since `u < NaN` is
|
||||
// false. That word is reproduced here: a caller that computes a NaN
|
||||
// probability perturbs the stream, and hiding that would hide a real divergence.
|
||||
bool chance(float p) {
|
||||
if (!(p > 0.0f)) return p != p ? draw_and_compare(p) : false;
|
||||
if (p >= 1.0f) return true;
|
||||
return draw_and_compare(p);
|
||||
}
|
||||
|
||||
// --- pure mappings (no draw), so tests can pin them word by word ----------
|
||||
static double unit_from(uint32_t y) { return static_cast<double>(y) * kUnitScale; }
|
||||
static float float_from(uint32_t y) { return static_cast<float>(unit_from(y)); }
|
||||
|
|
@ -109,6 +135,9 @@ private:
|
|||
uint32_t mt_[N];
|
||||
int left_ = 0;
|
||||
|
||||
// The drawing half of chance(): always one word, strict `<`.
|
||||
bool draw_and_compare(float p) { return next_float() < p; }
|
||||
|
||||
void twist();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
#include "mars/rng/mt19937.h"
|
||||
|
||||
|
|
@ -248,6 +249,44 @@ int main() {
|
|||
CHECK(hist[5] > hist[0] * 2);
|
||||
CHECK(hist[5] > hist[10] * 2);
|
||||
}
|
||||
{
|
||||
// chance(): the only entry point that can cost ZERO words, and the zero
|
||||
// cases are the whole point -- a caller whose probability climbs past 1
|
||||
// stops perturbing the stream, and a model without the early-outs drifts
|
||||
// one word per such call, permanently.
|
||||
MT19937 a(5489u);
|
||||
CHECK(a.chance(0.0f) == false);
|
||||
CHECK(a.chance(-1.0f) == false);
|
||||
CHECK(a.chance(1.0f) == true);
|
||||
CHECK(a.chance(2.0f) == true);
|
||||
CHECK(a.index() == 0); // four calls, not one word spent
|
||||
|
||||
// in (0,1) it is exactly one word, and the comparison is STRICT `<`
|
||||
// against the same float the generator would hand any other consumer.
|
||||
MT19937 b(5489u), c(5489u);
|
||||
const float u = c.next_float();
|
||||
CHECK(b.chance(0.5f) == (u < 0.5f));
|
||||
CHECK(b.index() == 1);
|
||||
CHECK(b.index() == c.index());
|
||||
|
||||
// the accumulating case the spy counter-mission actually runs: 0.2f,
|
||||
// 0.4f, 0.6f, 0.8f each draw, then p >= 1 costs nothing for ever after.
|
||||
MT19937 d(1234u);
|
||||
float p = 0.2f;
|
||||
for (int i = 0; i < 4; ++i, p += 0.2f) (void)d.chance(p);
|
||||
const int spent = d.index();
|
||||
CHECK(spent == 4);
|
||||
for (int i = 0; i < 10; ++i) CHECK(d.chance(p) == true);
|
||||
CHECK(d.index() == spent); // ten more calls, still four words
|
||||
|
||||
// NaN takes neither early-out in the original -- each is a pair of ordered
|
||||
// comparisons a NaN makes false -- so it DRAWS and returns false. The word
|
||||
// is reproduced deliberately: hiding it would hide a real divergence.
|
||||
MT19937 e(77u);
|
||||
const float nan = std::numeric_limits<float>::quiet_NaN();
|
||||
CHECK(e.chance(nan) == false);
|
||||
CHECK(e.index() == 1);
|
||||
}
|
||||
std::printf("test_rng: %s\n", fails ? "FAILED" : "ok");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue