New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Created PCG random number generator #82
Merged
Merged
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
5973ce6
Created PCG random number generator
peastman 08f3dc0
Fixed an error
peastman 8efde54
Changes based on feedback
peastman 20d6cb6
Removed an unused import
peastman f14f70f
Repeat distribution tests with lots of fixed seeds
peastman 687248e
Optimized creating random arrays
peastman 20ecb85
Discard cached gaussian when setting seed
peastman File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,238 @@ | ||
/** | ||
* This file implements random number generation using a PCG-XSH-RR generation. Some elements of this file are based | ||
* on the pcg-c-basic library (https://github.com/imneme/pcg-c-basic), which is also distributed under the Apache 2.0 | ||
* license. | ||
*/ | ||
|
||
package koma.internal | ||
|
||
// We really want a non-internal `expect val rng: KomaRandom` below, but | ||
// kotlin/native breaks currently. This is a workaround. | ||
import kotlin.math.ln | ||
import kotlin.math.sqrt | ||
|
||
private val rng = KomaRandom(0, 0x4B4F4D41) | ||
|
||
/** | ||
* Get the default random number generator. | ||
*/ | ||
fun getRng() = rng | ||
|
||
expect internal val rng: KomaRandom | ||
/** | ||
* This class implements random number generation using the PCG-XSH-RR algorithm. It has excellent statistical | ||
* properties and a period of 2^64. | ||
*/ | ||
class KomaRandom { | ||
private val inc1: Long | ||
private val inc2: Long | ||
private var state1: Long | ||
private var state2: Long | ||
private var gaussianValue = 0.0 | ||
private var gaussianIsValid = false | ||
|
||
private val MULTIPLIER = 6364136223846793005L | ||
private val FLOAT_SCALE = 1.0f/(1 shl 24) | ||
private val DOUBLE_SCALE = 1.0/(1L shl 53) | ||
|
||
/** | ||
* Create a new random number generator. | ||
* | ||
* @param seed the seed value to use. This determines the initial position of the generator within | ||
* the random sequence. It can be changed later by calling [setSeed]. | ||
* @param streamID every stream ID produces a different sequence. This cannot be negative. | ||
*/ | ||
constructor(seed: Long, streamID: Long) { | ||
// The highest bit of the stream ID is ignored. To avoid a risk of someone thinking two streams are distinct | ||
// when they really aren't, throw an exception if it's set. | ||
|
||
if (streamID < 0) | ||
throw IllegalArgumentException("streamID cannot be negative") | ||
kyonifer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// We actually create two different streams. When generating 64 bit values, we use the second stream to | ||
// generate the upper 32 bits. Create a second stream ID by inverting some of the bits. | ||
|
||
inc1 = (streamID shl 1) or 1 | ||
inc2 = inc1 xor 0xAAAAAAAA | ||
state1 = 0 | ||
state2 = 0 | ||
setSeed(seed) | ||
} | ||
|
||
/** | ||
* Reset the position of the generator within its random sequence based on a seed value. | ||
*/ | ||
fun setSeed(seed: Long) { | ||
synchronized(this) { | ||
state1 = 0 | ||
state2 = 0 | ||
nextLongUnsafe() | ||
state1 += seed | ||
state2 += seed | ||
nextLongUnsafe() | ||
} | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Int between Int.MIN_VALUE and Int.MAX_VALUE inclusive. | ||
*/ | ||
fun nextInt(): Int { | ||
synchronized(this) { | ||
return nextIntUnsafe() | ||
} | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Int between 0 (inclusive) and [bound] (exclusive). | ||
*/ | ||
fun nextInt(bound: Int): Int { | ||
synchronized(this) { | ||
return nextIntUnsafe(bound) | ||
} | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Long between Long.MIN_VALUE and Long.MAX_VALUE inclusive. | ||
*/ | ||
fun nextLong(): Long { | ||
synchronized(this) { | ||
return nextLongUnsafe() | ||
} | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Long between 0 (inclusive) and [bound] (exclusive). | ||
*/ | ||
fun nextLong(bound: Long): Long { | ||
synchronized(this) { | ||
return nextLongUnsafe(bound) | ||
} | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Float between 0 (inclusive) and 1 (exclusive). | ||
*/ | ||
fun nextFloat(): Float { | ||
synchronized(this) { | ||
return nextFloatUnsafe() | ||
} | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Double between 0 (inclusive) and 1 (exclusive). | ||
*/ | ||
fun nextDouble(): Double { | ||
synchronized(this) { | ||
return nextDoubleUnsafe() | ||
} | ||
} | ||
|
||
/** | ||
* Get a normally distributed Double with min 0 and standard deviation 1. | ||
*/ | ||
fun nextGaussian(): Double { | ||
synchronized(this) { | ||
return nextGaussianUnsafe() | ||
} | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Int between Int.MIN_VALUE and Int.MAX_VALUE inclusive. | ||
* This method is not thread safe. Only call it if you are holding the lock on this object! | ||
*/ | ||
internal fun nextIntUnsafe(): Int { | ||
val oldState = state1 | ||
state1 = oldState * MULTIPLIER + inc1 | ||
val xorShifted = (((oldState ushr 18) xor oldState) ushr 27).toInt() | ||
val rot = (oldState ushr 59).toInt() | ||
return xorShifted ushr rot or (xorShifted shl (-rot and 31)) | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Int between 0 (inclusive) and [bound] (exclusive). | ||
* This method is not thread safe. Only call it if you are holding the lock on this object! | ||
*/ | ||
internal fun nextIntUnsafe(bound: Int): Int { | ||
val threshold = -bound % bound | ||
while (true) { | ||
val r = nextIntUnsafe() | ||
if (r >= threshold) | ||
return r % bound; | ||
} | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Long between Long.MIN_VALUE and Long.MAX_VALUE inclusive. | ||
* This method is not thread safe. Only call it if you are holding the lock on this object! | ||
*/ | ||
internal fun nextLongUnsafe(): Long { | ||
// Compute the lower half of the result. | ||
|
||
val oldState1 = state1 | ||
state1 = oldState1 * MULTIPLIER + inc1 | ||
val xorShifted1 = (((oldState1 ushr 18) xor oldState1) ushr 27).toInt() | ||
val rot1 = (oldState1 ushr 59).toInt() | ||
val result1 = xorShifted1 ushr rot1 or (xorShifted1 shl (-rot1 and 31)) | ||
|
||
// Now use the second stream to compute the upper half. | ||
|
||
val oldState2 = state2 | ||
state2 = oldState2 * MULTIPLIER + inc2 | ||
val xorShifted2 = (((oldState2 ushr 18) xor oldState2) ushr 27).toInt() | ||
val rot2 = (oldState2 ushr 59).toInt() | ||
val result2 = xorShifted2 ushr rot2 or (xorShifted2 shl (-rot2 and 31)) | ||
return (result2.toLong() shl 32) + result1 | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Long between 0 (inclusive) and [bound] (exclusive). | ||
* This method is not thread safe. Only call it if you are holding the lock on this object! | ||
*/ | ||
internal fun nextLongUnsafe(bound: Long): Long { | ||
val threshold = -bound % bound | ||
while (true) { | ||
val r = nextLongUnsafe() | ||
if (r >= threshold) | ||
return r % bound; | ||
} | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Float between 0 (inclusive) and 1 (exclusive). | ||
* This method is not thread safe. Only call it if you are holding the lock on this object! | ||
*/ | ||
internal fun nextFloatUnsafe(): Float { | ||
return nextIntUnsafe().ushr(8) * FLOAT_SCALE | ||
} | ||
|
||
/** | ||
* Get a uniformly distributed Double between 0 (inclusive) and 1 (exclusive). | ||
* This method is not thread safe. Only call it if you are holding the lock on this object! | ||
*/ | ||
internal fun nextDoubleUnsafe(): Double { | ||
return nextLongUnsafe().ushr(11) * DOUBLE_SCALE | ||
} | ||
|
||
interface KomaRandom { | ||
fun setSeed(seed: Long) | ||
fun nextGaussian(): Double | ||
fun nextDouble(): Double | ||
/** | ||
* Get a normally distributed Double with min 0 and standard deviation 1. | ||
* This method is not thread safe. Only call it if you are holding the lock on this object! | ||
*/ | ||
internal fun nextGaussianUnsafe(): Double { | ||
if (gaussianIsValid) { | ||
gaussianIsValid = false | ||
return gaussianValue | ||
} | ||
|
||
// Compute two new values using the Box-Muller transform. | ||
|
||
var x: Double | ||
var y: Double | ||
var r2: Double | ||
do { | ||
x = 2*nextDoubleUnsafe() - 1.0 | ||
y = 2*nextDoubleUnsafe() - 1.0 | ||
r2 = x*x + y*y | ||
} while (r2 >= 1.0 || r2 == 0.0) | ||
val multiplier = sqrt((-2.0*ln(r2))/r2); | ||
gaussianValue = y*multiplier; | ||
gaussianIsValid = true; | ||
return x*multiplier | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IANAL but I believe the APL 2.0 requires us to retain the original copyright notices that were posted in the original source code, along with a notice that we've modified it. In particular:
My understanding is that translations to another language like this count as a "derivative work" (see https://www.rosenlaw.com/lj19.htm for a discussion).
I've also been trying to keep a running list of files that are owned by other projects at the end of the LICENSE file as I expect that list to grow.
I'd rather be too careful than step on anybody's toes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I'll fix that.