Skip to content

NextDoubleExclusive() can return exactly 1.0, so Normal/LogNormal Sample returns +Infinity #60

Description

@matt-edmondson

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingreadyFully specified; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions