using System; using System.Runtime.CompilerServices; using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; namespace UnityEngine.Experimental.Perception.Randomization.Samplers { /// /// A set of utility functions for defining sampler interfaces /// public static class SamplerUtility { internal const uint largePrime = 0x202A96CF; const int k_SamplingBatchSize = 64; /// /// Returns the sampler's display name /// /// The sampler type /// The display name public static string GetSamplerDisplayName(Type samplerType) { return samplerType.Name.Replace("Sampler", string.Empty); } /// /// Non-deterministically generates a non-zero random seed /// /// A non-deterministically generated random seed public static uint GenerateRandomSeed() { return (uint)Random.Range(1, uint.MaxValue); } /// /// Hashes using constants generated from a program that maximizes the avalanche effect, independence of /// output bit changes, and the probability of a change in each output bit if any input bit is changed. /// Source: https://github.com/h2database/h2database/blob/master/h2/src/test/org/h2/test/store/CalculateHashConstant.java /// /// Unsigned integer to hash /// The calculated hash value [MethodImpl(MethodImplOptions.AggressiveInlining)] static uint Hash32(uint x) { x = ((x >> 16) ^ x) * 0x45d9f3b; x = ((x >> 16) ^ x) * 0x45d9f3b; x = (x >> 16) ^ x; return x; } /// /// Based on splitmix64: http://xorshift.di.unimi.it/splitmix64.c /// /// 64-bit value to hash /// The calculated hash value [MethodImpl(MethodImplOptions.AggressiveInlining)] static ulong Hash64(ulong x) { x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ul; x = (x ^ (x >> 27)) * 0x94d049bb133111ebul; x ^= (x >> 31); return x; } /// /// Generates new a new non-zero random state by deterministically hashing a base seed with an iteration index /// /// Usually the current scenario iteration or framesSinceInitialization /// The seed to be offset /// A new random state [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint IterateSeed(uint index, uint baseSeed) { var state = (uint)Hash64(((ulong)index << 32) | baseSeed); return state == 0u ? largePrime : state; } /// /// Schedules a multi-threaded job to generate an array of samples /// /// The sampler to generate samples from /// The number of samples to generate /// The handle of the scheduled job /// The type of sampler to sample /// A NativeArray of generated samples public static NativeArray GenerateSamples( T sampler, int sampleCount, out JobHandle jobHandle) where T : struct, ISampler { var samples = new NativeArray( sampleCount, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); jobHandle = new SampleJob { sampler = sampler, samples = samples }.ScheduleBatch(sampleCount, k_SamplingBatchSize); return samples; } [BurstCompile] struct SampleJob : IJobParallelForBatch where T : ISampler { public T sampler; public NativeArray samples; public void Execute(int startIndex, int count) { var endIndex = startIndex + count; var batchIndex = startIndex / k_SamplingBatchSize; sampler.IterateState(batchIndex); for (var i = startIndex; i < endIndex; i++) samples[i] = sampler.Sample(); } } /// /// Source: https://www.johndcook.com/blog/csharp_phi/ /// static float NormalCdf(float x) { const float a1 = 0.254829592f; const float a2 = -0.284496736f; const float a3 = 1.421413741f; const float a4 = -1.453152027f; const float a5 = 1.061405429f; const float p = 0.3275911f; var sign = 1; if (x < 0) sign = -1; x = math.abs(x) / math.sqrt(2.0f); var t = 1.0f / (1.0f + p*x); var y = 1.0f - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t * math.exp(-x*x); return 0.5f * (1.0f + sign*y); } /// /// Source: https://www.johndcook.com/blog/csharp_phi_inverse/ /// static float RationalApproximation(float t) { const float c0 = 2.515517f; const float c1 = 0.802853f; const float c2 = 0.010328f; const float d0 = 1.432788f; const float d1 = 0.189269f; const float d2 = 0.001308f; return t - ((c2*t + c1)*t + c0) / (((d2*t + d1)*t + d0)*t + 1.0f); } /// /// Source: https://www.johndcook.com/blog/csharp_phi_inverse/ /// Note: generates NaN values for values 0 and 1 /// /// A uniform sample value between the range (0, 1) static float NormalCdfInverse(float uniformSample) { return uniformSample < 0.5f ? -RationalApproximation(math.sqrt(-2.0f * math.log(uniformSample))) : RationalApproximation(math.sqrt(-2.0f * math.log(1.0f - uniformSample))); } /// /// Generates samples from a truncated normal distribution. /// Further reading about this distribution can be found here: /// https://en.wikipedia.org/wiki/Truncated_normal_distribution /// /// A sample value between 0 and 1 generated from a uniform distribution /// The minimum possible value to generate /// The maximum possible value to generate /// The mean of the normal distribution /// The standard deviation of the normal distribution /// A value sampled from a truncated normal distribution /// public static float TruncatedNormalSample(float uniformSample, float min, float max, float mean, float stdDev) { if (min > max) throw new ArgumentException("Invalid range"); if (uniformSample == 0f) return min; if (uniformSample == 1f) return max; if (stdDev == 0f) return math.clamp(mean, min, max); var a = NormalCdf((min - mean) / stdDev); var b = NormalCdf((max - mean) / stdDev); var c = math.lerp(a, b, uniformSample); if (c == 0f) return max; if (c == 1f) return min; var stdTruncNorm = NormalCdfInverse(c); return stdTruncNorm * stdDev + mean; } } }