131 lines
4.1 KiB
Go
131 lines
4.1 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package core
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
// TestGeneratorStreamIsPinned pins the raw stream of one seed: the
|
|
// splitmix64 seeding walk and the xoshiro core must keep producing
|
|
// exactly these bits, because every recorded oracle digest downstream
|
|
// of the generator hangs on them.
|
|
func TestGeneratorStreamIsPinned(t *testing.T) {
|
|
g := NewGenerator(1)
|
|
for _, want := range []uint64{
|
|
0xcfc5d07f6f03c29b, 0xbf424132963fe08d, 0x19a37d5757aaf520, 0xbf08119f05cd56d6,
|
|
} {
|
|
if got := g.Next(); got != want {
|
|
t.Fatalf("Next() = %#016x, want %#016x", got, want)
|
|
}
|
|
}
|
|
n, err := Normal(NewGenerator(7), 3, 0, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i, want := range []float64{1.674036445441065, 0.53789816819896552, 1.2079282540944534} {
|
|
if n.FloatAt(i) != want {
|
|
t.Fatalf("Normal draw %d = %.17g, want %.17g", i, n.FloatAt(i), want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSplitmix64 pins the scalar mixer against an independent
|
|
// transcription of the finaliser and pins the state discipline: the
|
|
// returned state is the input plus the golden constant, the output is
|
|
// the mixed state, and chaining reproduces the walk.
|
|
func TestSplitmix64(t *testing.T) {
|
|
mix := func(z uint64) uint64 {
|
|
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9
|
|
z = (z ^ (z >> 27)) * 0x94D049BB133111EB
|
|
return z ^ (z >> 31)
|
|
}
|
|
const golden = 0x9E3779B97F4A7C15
|
|
state, v := Splitmix64(0)
|
|
if state != golden || v != mix(golden) {
|
|
t.Fatalf("Splitmix64(0) = (%#016x, %#016x), want (%#016x, %#016x)", state, v, uint64(golden), mix(golden))
|
|
}
|
|
for _, s := range []uint64{0, 1, 0xdeadbeef, ^uint64(0)} {
|
|
state, v := Splitmix64(s)
|
|
if state != s+golden {
|
|
t.Fatalf("Splitmix64(%#016x) advanced the state to %#016x, want %#016x", s, state, s+golden)
|
|
}
|
|
if v != mix(state) {
|
|
t.Fatalf("Splitmix64(%#016x) output %#016x, want %#016x", s, v, mix(state))
|
|
}
|
|
// Chaining from the returned state repeats the definition.
|
|
next, nv := Splitmix64(state)
|
|
if nv != mix(next) || next != state+golden {
|
|
t.Fatal("the chained step disagrees with the one-step definition")
|
|
}
|
|
}
|
|
}
|
|
|
|
// mustSub builds a substream, failing the test on a bad index.
|
|
func mustSub(t *testing.T, seed int64, index int) *Generator {
|
|
t.Helper()
|
|
g, err := Substream(seed, index)
|
|
if err != nil {
|
|
t.Fatalf("Substream(%d, %d): %v", seed, index, err)
|
|
}
|
|
return g
|
|
}
|
|
|
|
// draw returns n uniform floats from g.
|
|
func draw(t *testing.T, g *Generator, n int) []float64 {
|
|
t.Helper()
|
|
a, err := Floats(g, n)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return a.RawFloats()[:n]
|
|
}
|
|
|
|
// sameBits reports whether two draw prefixes are bit for bit equal.
|
|
func sameBits(a, b []float64) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if math.Float64bits(a[i]) != math.Float64bits(b[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// TestSubstream pins the stream family: the same seed and index
|
|
// reproduce the same draws, distinct indices give distinct draws (so
|
|
// no member is a copy of another), the family differs from the plain
|
|
// seeded generator, and a negative index is an error.
|
|
func TestSubstream(t *testing.T) {
|
|
const draws = 16
|
|
base := draw(t, mustSub(t, 42, 0), draws)
|
|
for i := 1; i < 8; i++ {
|
|
if sameBits(draw(t, mustSub(t, 42, i), draws), base) {
|
|
t.Fatalf("substream %d drew the same prefix as substream 0", i)
|
|
}
|
|
}
|
|
// Determinism: the same index redraws the same bits.
|
|
if !sameBits(draw(t, mustSub(t, 42, 3), draws), draw(t, mustSub(t, 42, 3), draws)) {
|
|
t.Fatal("the same substream drew different bits on a second construction")
|
|
}
|
|
// The family hangs off the seed, not beside the plain generator:
|
|
// substream 0 must differ from NewGenerator(42) and from the
|
|
// neighbouring seed's plain stream.
|
|
for _, seed := range []int64{42, 43} {
|
|
if sameBits(draw(t, NewGenerator(seed), draws), base) {
|
|
t.Fatalf("substream 0 coincides with NewGenerator(%d)", seed)
|
|
}
|
|
}
|
|
// A large index is as legal as a small one.
|
|
if _, err := Substream(42, 1<<40); err != nil {
|
|
t.Fatalf("Substream at index 2^40: %v", err)
|
|
}
|
|
if _, err := Substream(42, -1); err == nil {
|
|
t.Fatal("a negative index: want an error")
|
|
}
|
|
}
|