What's wrong
IRandomProvider.NextDoubleExclusive() is defined in Essentials/IRandomProvider.cs, around line 173:
public double NextDoubleExclusive() => ((NextUInt64() >> 11) + 0.5) * RandomHelpers.UnitScale; // UnitScale = 2^-53
When the 53-bit draw is at its maximum, 2^53 - 1, the sum (2^53 - 1) + 0.5 cannot be represented as a double. Doubles near 2^53 are spaced 1 apart, so this is an exact tie between 2^53 - 1 and 2^53. Round-half-to-even picks 2^53, and the result is 2^53 * 2^-53 = 1.0.
The method's own remarks say the half-step offset exists so that inverse-transform sampling never sees 0 or 1, because the quantile of an unbounded distribution is infinite there. The upper end is not excluded.
Consequence
The default IDistribution.Sample computes Quantile(NextDoubleExclusive()). When the draw hits this value:
Normal(0, 1).Sample(random) returns +Infinity.
- LogNormal behaves the same way.
- Every other distribution that uses the default
Sample receives u = 1, which the method's contract excludes.
Reproduction
A stub IRandomProvider whose NextBytes fills the buffer with 0xFF was run against the net10.0 build:
NextDoubleExclusive = 1 (== 1.0? True)
Normal(0,1).Sample: Infinity
The chance per draw is 2^-53. However, a seeded Xoshiro or Pcg state that produces this word will produce it every time it is replayed. The existing test at RandomProviderTests.cs:152 only checks random samples, so it can never hit this boundary.
Suggested fix / acceptance criteria
- Use 52 bits so that the sum is always exact:
((NextUInt64() >> 12) + 0.5) * (1.0 / 4503599627370496.0) // 2^-52
The largest result is then 1 - 2^-53 and the smallest is 2^-53, both strictly inside (0, 1).
- Add a boundary test with a stub provider that returns
ulong.MaxValue and one that returns 0. Assert 0 < u < 1, and assert that Normal(0,1).Sample is finite for both.
What's wrong
IRandomProvider.NextDoubleExclusive()is defined inEssentials/IRandomProvider.cs, around line 173:When the 53-bit draw is at its maximum,
2^53 - 1, the sum(2^53 - 1) + 0.5cannot be represented as a double. Doubles near 2^53 are spaced 1 apart, so this is an exact tie between2^53 - 1and2^53. Round-half-to-even picks2^53, and the result is2^53 * 2^-53 = 1.0.The method's own remarks say the half-step offset exists so that inverse-transform sampling never sees 0 or 1, because the quantile of an unbounded distribution is infinite there. The upper end is not excluded.
Consequence
The default
IDistribution.SamplecomputesQuantile(NextDoubleExclusive()). When the draw hits this value:Normal(0, 1).Sample(random)returns+Infinity.Samplereceivesu = 1, which the method's contract excludes.Reproduction
A stub
IRandomProviderwhoseNextBytesfills the buffer with0xFFwas run against the net10.0 build:The chance per draw is 2^-53. However, a seeded Xoshiro or Pcg state that produces this word will produce it every time it is replayed. The existing test at
RandomProviderTests.cs:152only checks random samples, so it can never hit this boundary.Suggested fix / acceptance criteria
1 - 2^-53and the smallest is2^-53, both strictly inside (0, 1).ulong.MaxValueand one that returns0. Assert0 < u < 1, and assert thatNormal(0,1).Sampleis finite for both.