Skip to content

StepRng always returns lower bound on random_range #1633

Closed

Description

@Typas

Summary

When I tried to use StepRng::new(1, 0) to test a special case of my crate, I find out the result of rng.random_range(0..=N) would always be 0.
After some experiments, I figure out the returned value has nothing to do with the initial value and the step of StepRng. The returned value is a constant, and it is the lower bound of the given range in random_range's parameter.

What behaviour is expected, and why?

The result of rng.random_range(0..=N) would return k for let mut rng = StepRng::new(k, 0) when k is in the range of 0..=N. Otherwise it is an UB.

Code sample

Here's a sample on the playground.
https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=db19fd77a6eb6195eecfcdfc5dce9f3b

use rand; // 0.9.0
use rand::Rng;

fn main() {
    const N: u64 = 123;
    const M1: u64 = 45;
    const M2: u64 = 678;
    const M3: u64 = 910;
    const ULIMIT: u64 = 10 * N;
    const STEP: u64 = 0;
    let mut rng = rand::rngs::mock::StepRng::new(N, STEP);
    let rnr1: u64 = rng.random_range(M1..=ULIMIT);
    let rnr2: u64 = rng.random_range(M2..=ULIMIT);
    let rnr3: u64 = rng.random_range(M3..=ULIMIT);
    println!("step_rng.random_range({M1}..={ULIMIT}) = {rnr1}.");
    println!("step_rng.random_range({M2}..={ULIMIT}) = {rnr2}.");
    println!("step_rng.random_range({M3}..={ULIMIT}) = {rnr3}.");
}

Activity

  1. benjamin-lieser commented on Apr 21, 2025

    @benjamin-lieser
    Member

    If you read the documentation it says

    This generates an arithmetic sequence (i.e. adds a constant each step) over a u64 number,
    using wrapping arithmetic. If the increment is 0 the generator yields a constant.
    

    So yes this is the correct behavior and StepRng is absolutely not supposed as a random source for any purpose, that why it is under mock

  2. Typas commented on Apr 21, 2025

    @Typas
    Author

    I know it yields a constant. However, the constant yielded from rng.random() and rng.random_range(LOWER_BOUND..=UPPER_BOUND) are not the same.

    Let's have an example

    use rand; // 0.9.0
    use rand::Rng;
    
    fn main() {
        const N: u64 = 100;
        const LOWER: u64 = 29;
        const UPPER: u64 = 10 * N;
        const STEP: u64 = 0;
        let mut rng = rand::rngs::mock::StepRng::new(N, STEP);
        let rn1: u64 = rng.random();
        let rnr1: u64 = rng.random_range(LOWER..=UPPER);
        println!("step_rng.random() = {rn1}.");
        println!("step_rng.random_range({LOWER}..={UPPER}) = {rnr1}.");
        assert_eq!(rn1, rnr1);
    }

    Moreover, when STEP has been changed to non-zero, the generator would yield increasing number on rng.random(), but the number from rng.random_range(LOWER_BOUND..=UPPER_BOUND) would always be the same.

    Here's the example
    https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=2c17c21898aeb374e0a480537b25fdfe

    use rand; // 0.9.0
    use rand::Rng;
    
    fn main() {
        const N: u64 = 100;
        const LOWER: u64 = 29;
        const UPPER: u64 = 10 * N;
        const STEP: u64 = 3;
        let mut rng = rand::rngs::mock::StepRng::new(N, STEP);
        let rn1: u64 = rng.random();
        let rnr1: u64 = rng.random_range(LOWER..=UPPER);
        println!("step_rng.random() = {rn1}.");
        println!("step_rng.random_range({LOWER}..={UPPER}) = {rnr1}.");
        assert_eq!(rn1, rnr1);
    }
  3. benjamin-lieser commented on Apr 22, 2025

    @benjamin-lieser
    Member

    The random generator yields a value in the range 0..u64::MAX. If you want to sample something else from it you need to map every of these number to an output.

    If you want to sample a random u64 this is very easy, you just use the identity which is also the choice we made here (but you could also do something different). There are not guaranties made about this.

    If you want to sample a random number in the range 0..100, you also have to map every possible u64 value to a number in 0..100, so this means a lot of numbers will map to the same output. If the mapping is good, each output will have the same amount of u64 values mapping to it.

    In this case a whole range of consecutive numbers are mapped to the same output. This is absolutely ok, because the source random u64 is assumed to be random. This breaks down for StepRng, which as I said is not a suitable source of randomness.

    If you put a big enough number into StepRng you see, that it does not always sample the lowest value.

  4. dhardy commented on Apr 22, 2025

    @dhardy
    Member

    What behaviour is expected, and why?

    There is no specification on this behaviour. In particular, there are no expectations on random algorithms when their input is not random.

    However, the constant yielded from rng.random() and rng.random_range(LOWER_BOUND..=UPPER_BOUND) are not the same.

    Again, there's no specification on this (beyond the output being within the specified range).

    You're welcome to consider probing our random algorithms using StepRng, but for the most part don't expect to find anything useful (I mean, the code for those algorithms is already open source). Usually we use a small pseudo-random generator (e.g. SmallRng, Pcg32, Xoshiro...) with seed_from_u64 for testing.

  5. dhardy commented on Apr 22, 2025

    @dhardy
    Member

    The result of rng.random_range(0..=N) would return k for let mut rng = StepRng::new(k, 0) when k is in the range of 0..=N. Otherwise it is an UB.

    This demonstrates a complete mis-understanding of the way generators and algorithms work. Generators may yield any number within their output range (e.g. next_u64() may yield anything from 0 to u64::MAX). Algorithms like random_range must transform any such number into a valid output for their distribution (in this case, the range 0..=N with uniform sampling), though the algorithm may draw multiple RNG samples to do this. So there must not be UB.

    Also, it's completely undefined how random_range maps from the RNG's output to its own output. We don't use n % range.

  6. newpavlov commented on Apr 22, 2025

    @newpavlov
    Member

    Maybe we should deprecate StepRng? It's not the first time users complain about its output despite the docs warnings.

  7. dhardy commented on Apr 22, 2025

    @dhardy
    Member

    Maybe we should deprecate StepRng? It's not the first time users complain about its output despite the docs warnings.

    I think we should: I can't think of any case where it's actually useful.

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

    X-bugType: bug report

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions