190 lines
7.4 KiB
C#
190 lines
7.4 KiB
C#
using System.Diagnostics;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace skyscraper8.UI.MonoGame.Bridge;
|
|
|
|
//This one is heavily inspired from https://github.com/colgreen/Redzen/blob/main/Redzen/Numerics/Distributions/Double/ZigguratGaussian.cs
|
|
//To understand how this algorithm works, please read those Github comments.
|
|
|
|
|
|
public class Ziggurat
|
|
{
|
|
const int __blockCount = 128;
|
|
const double __R = 3.442619855899;
|
|
const double __A = 9.91256303526217e-3;
|
|
const ulong __MAXINT = (1UL << 53) - 1;
|
|
const double __INCR = 1.0 / __MAXINT;
|
|
private double[] __x;
|
|
private double[] __y;
|
|
private ulong[] __xComp;
|
|
private double __A_Div_Y0;
|
|
private Random rng;
|
|
|
|
public Ziggurat()
|
|
: this(null)
|
|
{
|
|
rng = new Random((int)DateTime.Now.Ticks);
|
|
}
|
|
|
|
public Ziggurat(int seed)
|
|
: this(null)
|
|
{
|
|
rng = new Random(seed);
|
|
}
|
|
public Ziggurat(Random randomSrc)
|
|
{
|
|
this.rng = randomSrc;
|
|
// Initialise rectangle position data.
|
|
// __x[i] and __y[i] describe the top-right position of Box i.
|
|
|
|
// Allocate storage. We add one to the length of _x so that we have an entry at __x[__blockCount], this avoids having
|
|
// to do a special case test when sampling from the top box.
|
|
__x = new double[__blockCount + 1];
|
|
__y = new double[__blockCount];
|
|
|
|
// Determine top right position of the base rectangle/box (the rectangle with the Gaussian tale attached).
|
|
// We call this Box 0 or B0 for short.
|
|
// Note. x[0] also describes the right-hand edge of B1. (See diagram).
|
|
__x[0] = __R;
|
|
__y[0] = GaussianPdfDenorm(__R);
|
|
|
|
// The next box (B1) has a right hand X edge the same as B0.
|
|
// Note. B1's height is the box area divided by its width, hence B1 has a smaller height than B0 because
|
|
// B0's total area includes the attached distribution tail.
|
|
__x[1] = __R;
|
|
__y[1] = __y[0] + (__A / __x[1]);
|
|
|
|
// Calc positions of all remaining rectangles.
|
|
for(int i=2; i < __blockCount; i++)
|
|
{
|
|
__x[i] = GaussianPdfDenormInv(__y[i-1]);
|
|
__y[i] = __y[i-1] + (__A / __x[i]);
|
|
}
|
|
|
|
// For completeness we define the right-hand edge of a notional box 6 as being zero (a box with no area).
|
|
__x[__blockCount] = 0.0;
|
|
|
|
// Useful precomputed values.
|
|
__A_Div_Y0 = __A / __y[0];
|
|
__xComp = new ulong[__blockCount];
|
|
|
|
// Special case for base box. __xComp[0] stores the area of B0 as a proportion of __R
|
|
// (recalling that all segments have area __A, but that the base segment is the combination of B0 and the distribution tail).
|
|
// Thus __xComp[0] is the probability that a sample point is within the box part of the segment.
|
|
__xComp[0] = (ulong)(((__R * __y[0]) / __A) * (double)__MAXINT);
|
|
|
|
for(int i=1; i < __blockCount-1; i++)
|
|
{
|
|
__xComp[i] = (ulong)((__x[i+1] / __x[i]) * (double)__MAXINT);
|
|
}
|
|
__xComp[__blockCount-1] = 0; // Shown for completeness.
|
|
|
|
// Sanity check. Test that the top edge of the topmost rectangle is at y=1.0.
|
|
// Note. We expect there to be a tiny drift away from 1.0 due to the inexactness of floating
|
|
// point arithmetic.
|
|
Debug.Assert(System.Math.Abs(1.0 - __y[__blockCount-1]) < 1e-10);
|
|
}
|
|
|
|
private static double GaussianPdfDenorm(double x)
|
|
{
|
|
return System.Math.Exp(-(x * x * 0.5));
|
|
}
|
|
|
|
private static double GaussianPdfDenormInv(double y)
|
|
{
|
|
// Operates over the y interval (0,1], which happens to be the y interval of the pdf,
|
|
// with the exception that it does not include y=0, but we would never call with
|
|
// y=0 so it doesn't matter. Note that a Gaussian effectively has a tail going
|
|
// into infinity on the x-axis, hence asking what is x when y=0 is an invalid question
|
|
// in the context of this class.
|
|
return System.Math.Sqrt(-2.0 * System.Math.Log(y));
|
|
}
|
|
|
|
private void SampleTail(out double x)
|
|
{
|
|
double y;
|
|
do
|
|
{
|
|
// Note. we use NextDoubleNonZero() because Log(0) returns -Infinity and will also tend to be a very slow execution path (when it occurs, which is rarely).
|
|
x = -System.Math.Log(rng.NextDouble()) / __R;
|
|
y = -System.Math.Log(rng.NextDouble());
|
|
}
|
|
while(y + y < x * x);
|
|
x += __R;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void SetSignBit(ref double x, ref ulong signBit)
|
|
{
|
|
Unsafe.As<double, ulong>(ref x) |= signBit;
|
|
}
|
|
|
|
public double Sample()
|
|
{
|
|
double x = 0.0;
|
|
while(true)
|
|
{
|
|
// Generate 64 random bits.
|
|
ulong u = (ulong)rng.NextInt64();
|
|
|
|
// Note. 61 random bits are required and therefore the lowest three bits are discarded
|
|
// (a typical characteristic of PRNGs is that the least significant bits exhibit lower
|
|
// quality randomness than the higher bits).
|
|
// Note. The bit index values in comments are zero based, i..e the first bit is bit 0.
|
|
// Select a segment (7 bits, bits 3 to 9).
|
|
int s = (int)((u >> 3) & 0x7f);
|
|
|
|
// Select the sign bit (bit 10), and shift it to the sign bit position for IEEE754
|
|
// double-precision floating-point format.
|
|
// Previously, the sign bit handling used a conditional branch that optionally multiplied the
|
|
// positive Gaussian sample by -1. However, that approach is considerably slower because modern
|
|
// superscalar CPUs rely heavily on branch prediction, but the sign bit here is randomly generated
|
|
// and therefore entirely unpredictable, i.e. the absolute worse case scenario for a branch
|
|
// predictor!
|
|
ulong signBit = (u & 0x400UL) << 53;
|
|
|
|
// Get a uniform random value with interval [0, 2^53-1], or in hexadecimal [0, 0x1f_ffff_ffff_ffff]
|
|
// (i.e. a random 53 bit number) (bits 11 to 63).
|
|
ulong u2 = u >> 11;
|
|
|
|
// Special case for the base segment.
|
|
if(s == 0)
|
|
{
|
|
if(u2 < __xComp[0])
|
|
{
|
|
// Generated x is within R0.
|
|
x = u2 * __INCR * __A_Div_Y0;
|
|
}
|
|
else
|
|
{
|
|
// Generated x is in the tail of the distribution.
|
|
SampleTail(out x);
|
|
}
|
|
|
|
SetSignBit(ref x, ref signBit);
|
|
return x;
|
|
}
|
|
|
|
// All other segments.
|
|
if(u2 < __xComp[s])
|
|
{
|
|
// Generated x is within the rectangle.
|
|
x = u2 * __INCR * __x[s];
|
|
SetSignBit(ref x, ref signBit);
|
|
return x;
|
|
}
|
|
|
|
// Generated x is outside of the rectangle.
|
|
// Generate a random y coordinate and test if our (x,y) is within the distribution curve.
|
|
// This execution path is relatively slow/expensive (makes a call to Math.Exp()) but is relatively rarely executed,
|
|
// although more often than the 'tail' path (above).
|
|
x = u2 * __INCR * __x[s];
|
|
if(__y[s-1] + ((__y[s] - __y[s-1]) * rng.NextDouble()) < GaussianPdfDenorm(x))
|
|
{
|
|
SetSignBit(ref x, ref signBit);
|
|
return x;
|
|
}
|
|
}
|
|
}
|
|
}
|