//
// Copyright 2021 Ettus Research, A National Instruments Brand
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Package: PkgRandom
//
// Description:
//
//   SystemVerilog has great randomization support, but some features require a
//   more expensive license or aren't supported by all tools. This package
//   tries to fill that gap by providing some useful randomization functions
//   beyond what's supported by standard Verilog.
//

package PkgRandom;

  import PkgMath::*;

  //---------------------------------------------------------------------------
  // Functions
  //---------------------------------------------------------------------------

  // Return a real value in the range [0,max), where max is 1.0 by default.
  function automatic real frand(real max = 1.0);
    bit [63:0] real_bits;
    real num;

    // Build a double-precision floating point value per IEEE-754 standard,
    // which SystemVerilog follows.

    // Positive, with exponent 0
    real_bits[63:52] = 12'h3FF;

    // Mantissa in the range [1.0, 2.0). The leading 1 in the mantissa is
    // implied by the floating point format.
    real_bits[31: 0] = $urandom();
    real_bits[51:32] = $urandom();

    // Compensate for the implied leading 1 in the mantissa by subtracting 1.
    num = $bitstoreal(real_bits) - 1.0;

    // Scale the result to return a value in the desired range.
    return num * max;
  endfunction : frand


  // Return a real value in the range [a,b), [b,a), or [0,a) depending on
  // whether a or b is larger and whether b is provided. This matches the
  // behavior of $urandom_range().
  //
  //   frand_range(1.0, 2.0) -> Random value in the range [1,2)
  //   frand_range(2.0, 1.0) -> Random value in the range [1,2)
  //   frand_range(1.0)      -> Random value in the range [0,1)
  //
  function automatic real frand_range(real a = 1.0, real b = 0.0);
    if (a > b) return b + frand(a - b);
    if (b > a) return a + frand(b - a);
    return a;
  endfunction : frand_range


  // Return a real value with a normal distribution, having the mean value mu
  // and standard deviation sigma.
  function automatic real frandn(real sigma = 1.0, real mu = 0.0);
    // Use the Box-Muller transform to convert uniform random variables to a
    // Gaussian one.
    return sigma*$sqrt(-2.0*$ln(frand())) * $cos(TAU*frand()) + mu;
  endfunction : frandn


  //---------------------------------------------------------------------------
  // Template Functions
  //---------------------------------------------------------------------------

  class Rand #(WIDTH = 64);

    // These are static class functions. They can be called directly, as in:
    //
    //   Rand#(N)::rand_bit()
    //
    // Or, you can declare an object reference, as in:
    //
    //   Rand #(N) rand;
    //   rand.rand_bit();

    typedef bit        [WIDTH-1:0] unsigned_t;
    typedef bit signed [WIDTH-1:0] signed_t;


    // Returns a WIDTH-bit random bit packed array.
    static function unsigned_t rand_bit();
      unsigned_t result;
      int num_rand32 = (WIDTH + 31) / 32;
      for (int i = 0; i < num_rand32; i++) begin
        result = {result, $urandom()};
      end
      return result;
    endfunction : rand_bit


    // Returns a WIDTH-bit random number in the UNSIGNED range [a,b], [b,a], or
    // [0,a] depending on whether a or b is greater and if b is provided. This
    // is equivalent to $urandom_range() but works with any length.
    static function bit [WIDTH-1:0] rand_bit_range(
      unsigned_t a = {WIDTH{1'b1}},
      unsigned_t b = 0
    );
      unsigned_t num;
      int num_bits;
      if (a > b) begin
        // Swap a and b
        unsigned_t temp;
        temp = a;
        a = b;
        b = temp;
      end
      num_bits = $clog2(b - a + (WIDTH+1)'(1));
      do begin
        num = a + (rand_bit() & (((WIDTH+1)'(1) << num_bits) - 1));
      end while (num > b);
      return num;
    endfunction : rand_bit_range


    // Returns a random number in the given SIGNED range. Behavior is the same
    // as rand_bit_range(), but treats the range values as SIGNED numbers.
    static function signed_t rand_sbit_range(
      signed_t a = {1'b0, {WIDTH{1'b1}}},
      signed_t b = 0
    );
      if (a > b) return b + $signed(rand_bit_range(0, a-b));
      if (b > a) return a + $signed(rand_bit_range(0, b-a));
      return a;
    endfunction : rand_sbit_range


    // Rand#(WIDTH)::rand_logic() returns a WIDTH-bit random logic packed
    // array. Each bit will be 0 or 1 with equal probability (not X or Z).
    static function logic [WIDTH-1:0] rand_logic();
      return rand_bit();
    endfunction : rand_logic

  endclass : Rand

endpackage : PkgRandom
