Swift 4.2+ seeding a random number generator

前端 未结 4 761
萌比男神i
萌比男神i 2021-02-13 17:19

I\'m trying to generate seeded random numbers with Swift 4.2+, with the Int.random() function, however there is no given implementation that allows for the random n

相关标签:
4条回答
  • 2021-02-13 17:30

    Here's alternative to the answer from RPatel99 that accounts GKRandom values range.

    import GameKit
    
    struct ArbitraryRandomNumberGenerator : RandomNumberGenerator {
    
        mutating func next() -> UInt64 {
            // GKRandom produces values in [INT32_MIN, INT32_MAX] range; hence we need two numbers to produce 64-bit value.
            let next1 = UInt64(bitPattern: Int64(gkrandom.nextInt()))
            let next2 = UInt64(bitPattern: Int64(gkrandom.nextInt()))
            return next1 ^ (next2 << 32)
        }
    
        init(seed: UInt64) {
            self.gkrandom = GKMersenneTwisterRandomSource(seed: seed)
        }
    
        private let gkrandom: GKRandom
    }
    
    0 讨论(0)
  • 2021-02-13 17:33

    So I used Martin R's suggestion to use GamePlayKit's GKMersenneTwisterRandomSource to make a class that conformed to the RandomNumberGenerator protocol, which I was able to use an instance of in functions like Int.random():

    import GameplayKit
    
    class SeededGenerator: RandomNumberGenerator {
        let seed: UInt64
        private let generator: GKMersenneTwisterRandomSource
        convenience init() {
            self.init(seed: 0)
        }
        init(seed: UInt64) {
            self.seed = seed
            generator = GKMersenneTwisterRandomSource(seed: seed)
        }
        func next<T>(upperBound: T) -> T where T : FixedWidthInteger, T : UnsignedInteger {
            return T(abs(generator.nextInt(upperBound: Int(upperBound))))
        }
        func next<T>() -> T where T : FixedWidthInteger, T : UnsignedInteger {
            return T(abs(generator.nextInt()))
        }
    }
    

    Usage:

    // Make a random seed and store in a database
    let seed = UInt64.random(in: UInt64.min ... UInt64.max)
    var generator = Generator(seed: seed)
    // Or if you just need the seeding ability for testing,
    // var generator = Generator()
    // uses a default seed of 0
    
    let chars = ['a','b','c','d','e','f']
    let randomChar = chars.randomElement(using: &generator)
    let randomInt = Int.random(in: 0 ..< 1000, using: &generator)
    // etc.
    

    This gave me the flexibility and easy implementation that I needed by combining the seeding functionality of GKMersenneTwisterRandomSource and the simplicity of the standard library's random functions (like .randomElement() for arrays and .random() for Int, Bool, Double, etc.)

    0 讨论(0)
  • 2021-02-13 17:34

    Looks like Swift's implementation of RandomNumberGenerator.next(using:) changed in 2019. This affects Collection.randomElement(using:) and causes it to always return the first element if your generator's next()->UInt64 implementation doesn't produce values uniformly across the domain of UInt64. The GKRandom solution provided here is therefore problematic because it's next->Int method states:

         * The value is in the range of [INT32_MIN, INT32_MAX].
    

    Here's a solution that works for me using the RNG in Swift's TensorFlow found here:

    
    public struct ARC4RandomNumberGenerator: RandomNumberGenerator {
      var state: [UInt8] = Array(0...255)
      var iPos: UInt8 = 0
      var jPos: UInt8 = 0
    
      /// Initialize ARC4RandomNumberGenerator using an array of UInt8. The array
      /// must have length between 1 and 256 inclusive.
      public init(seed: [UInt8]) {
        precondition(seed.count > 0, "Length of seed must be positive")
        precondition(seed.count <= 256, "Length of seed must be at most 256")
        var j: UInt8 = 0
        for i: UInt8 in 0...255 {
          j &+= S(i) &+ seed[Int(i) % seed.count]
          swapAt(i, j)
        }
      }
    
      // Produce the next random UInt64 from the stream, and advance the internal
      // state.
      public mutating func next() -> UInt64 {
        var result: UInt64 = 0
        for _ in 0..<UInt64.bitWidth / UInt8.bitWidth {
          result <<= UInt8.bitWidth
          result += UInt64(nextByte())
        }
        print(result)
        return result
      }
    
      // Helper to access the state.
      private func S(_ index: UInt8) -> UInt8 {
        return state[Int(index)]
      }
    
      // Helper to swap elements of the state.
      private mutating func swapAt(_ i: UInt8, _ j: UInt8) {
        state.swapAt(Int(i), Int(j))
      }
    
      // Generates the next byte in the keystream.
      private mutating func nextByte() -> UInt8 {
        iPos &+= 1
        jPos &+= S(iPos)
        swapAt(iPos, jPos)
        return S(S(iPos) &+ S(jPos))
      }
    }
    

    Hat tip to my coworkers Samuel, Noah, and Stephen who helped me get to the bottom of this.

    0 讨论(0)
  • 2021-02-13 17:49

    I ended up using srand48() and drand48() to generate a pseudo-random number with a seed for a specific test.

    class SeededRandomNumberGenerator : RandomNumberGenerator {
    
        let range: ClosedRange<Double> = Double(UInt64.min) ... Double(UInt64.max)
    
        init(seed: Int) {
            // srand48() — Pseudo-random number initializer
            srand48(seed)
        }
    
        func next() -> UInt64 {
            // drand48() — Pseudo-random number generator
            return UInt64(range.lowerBound + (range.upperBound - range.lowerBound) * drand48())
        }
        
    }
    

    So, in production the implementation uses the SystemRandomNumberGenerator but in the test suite it uses the SeededRandomNumberGenerator.

    Example:

    let messageFixtures: [Any] = [
        "a string",
        ["some", ["values": 456]],
    ]
    
    var seededRandomNumberGenerator = SeededRandomNumberGenerator(seed: 13)
    
    func randomMessageData() -> Any {
        return messageFixtures.randomElement(using: &seededRandomNumberGenerator)!
    }
    
    // Always return the same element in the same order
    randomMessageData() //"a string"
    randomMessageData() //["some", ["values": 456]]
    randomMessageData() //["some", ["values": 456]]
    randomMessageData() //["some", ["values": 456]]
    randomMessageData() //"a string"
    
    0 讨论(0)
提交回复
热议问题