RobustToolbox icon indicating copy to clipboard operation
RobustToolbox copied to clipboard

NextFloat() has an inclusive maximum value

Open slarticodefast opened this issue 10 months ago • 1 comments

The current implementation was copied from a solution for double precision numbers according to the code comment

Image

But this results in different behaviour for floating point values, which will include the maximum of 1.0f: Here a simple test that calculates the maximum value in bit representation to make sure it is not caused by rounding when printing:

public static void Main(string[] args)
{
    float a = 4.6566128752458E-10f;
    float b = (Int32.MaxValue - 1) * 4.6566128752458E-10f;
    float c = Int32.MaxValue * 4.6566128752458E-10f;
    float d = 1.0f;
    string bitsA = Convert.ToString(BitConverter.ToInt32(BitConverter.GetBytes(a), 0), 2).PadLeft(32, '0');
    string bitsB = Convert.ToString(BitConverter.ToInt32(BitConverter.GetBytes(b), 0), 2).PadLeft(32, '0');
    string bitsC = Convert.ToString(BitConverter.ToInt32(BitConverter.GetBytes(c), 0), 2).PadLeft(32, '0');
    string bitsD = Convert.ToString(BitConverter.ToInt32(BitConverter.GetBytes(d), 0), 2).PadLeft(32, '0');
    Console.WriteLine ($"{a}");
    Console.WriteLine ($"{bitsA}");
    Console.WriteLine ($"{b}");
    Console.WriteLine ($"{bitsB}");
    Console.WriteLine ($"{c}");
    Console.WriteLine ($"{bitsC}");
    Console.WriteLine ($"{d}");
    Console.WriteLine ($"{bitsD}");
}
}

which gives the output

4.656613E-10
00110000000000000000000000000000
1
00111111100000000000000000000000
1
00111111100000000000000000000000
1
00111111100000000000000000000000

The same problem does not occurr for double precision:

public static void Main(string[] args)
{
    double a = 4.6566128752458E-10f;
    double b = (Int32.MaxValue - 1) * 4.6566128752458E-10;
    double c = Int32.MaxValue * 4.6566128752458E-10;
    double d = 1.0;
    string bitsA = Convert.ToString(BitConverter.ToInt64(BitConverter.GetBytes(a), 0), 2).PadLeft(64, '0');
    string bitsB = Convert.ToString(BitConverter.ToInt64(BitConverter.GetBytes(b), 0), 2).PadLeft(64, '0');
    string bitsC = Convert.ToString(BitConverter.ToInt64(BitConverter.GetBytes(c), 0), 2).PadLeft(64, '0');
    string bitsD = Convert.ToString(BitConverter.ToInt64(BitConverter.GetBytes(d), 0), 2).PadLeft(64, '0');
    Console.WriteLine ($"{a}");
    Console.WriteLine ($"{bitsA}");
    Console.WriteLine ($"{b}");
    Console.WriteLine ($"{bitsB}");
    Console.WriteLine ($"{c}");
    Console.WriteLine ($"{bitsC}");
    Console.WriteLine ($"{d}");
    Console.WriteLine ($"{bitsD}");
}
4.6566128752458E-10
0011111000000000000000000000000000000000001000000000000000000011
0.999999999534339
0011111111101111111111111111111111111111110000000000000000000110
1
0011111111110000000000000000000000000000000000000000000000000011
1
0011111111110000000000000000000000000000000000000000000000000000

Usually random number generators exclude the maximum value, so this may cause numerical errors in rare cases where the user was not aware of this. The documentation says that the maximum should be excluded:

Image

@Fildrance

slarticodefast avatar Jun 04 '25 02:06 slarticodefast

It looks to me that its better to move onto random.NextSingle - microbenchmark provided minimal perf difference... yet it exists so i am a bit hesitant.

Image

Fildrance avatar Jun 14 '25 11:06 Fildrance