From 33d40a18ea203922e78a560521f7a7ae0067046f Mon Sep 17 00:00:00 2001 From: Justin Visser Date: Wed, 10 Jun 2026 11:57:39 +0200 Subject: [PATCH] engine: seeded RNG helper, first tests math/rand/v2 (Go 1.22+) replaces the old math/rand: PCG generator, no global Seed(), and rand.New(rand.NewPCG(seed, 0)) gives an isolated deterministic stream. Each randomness consumer (graph, thresholds, education sampling) will get its own stream so levers vary independently. Tests live next to the code as *_test.go; 'go test ./...' runs them all. 'for i := range 100' is Go 1.22 range-over-int. --- internal/engine/rng.go | 16 ++++++++++++++++ internal/engine/rng_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 internal/engine/rng.go create mode 100644 internal/engine/rng_test.go diff --git a/internal/engine/rng.go b/internal/engine/rng.go new file mode 100644 index 0000000..009148c --- /dev/null +++ b/internal/engine/rng.go @@ -0,0 +1,16 @@ +// Package engine implements the agent-based spread model: build a social +// network, run an independent cascade over it, and measure the effect of +// education strategies. It is pure: no I/O, no web dependencies, and all +// randomness flows from seeds in the config, so identical inputs always +// produce identical runs. +package engine + +import "math/rand/v2" + +// newRand returns a deterministic random stream for the given seed. The +// engine never touches global random state; every source of randomness +// (graph build, edge thresholds, education sampling) gets its own seeded +// stream so each can be varied independently. +func newRand(seed uint64) *rand.Rand { + return rand.New(rand.NewPCG(seed, 0)) +} diff --git a/internal/engine/rng_test.go b/internal/engine/rng_test.go new file mode 100644 index 0000000..0ba8de4 --- /dev/null +++ b/internal/engine/rng_test.go @@ -0,0 +1,26 @@ +package engine + +import "testing" + +// Determinism is a locked project decision: the same seed must always yield +// the same stream, and different seeds must not collide. With fixed seeds +// these assertions are exact, not probabilistic. + +func TestNewRandSameSeedSameStream(t *testing.T) { + a, b := newRand(17), newRand(17) + for i := range 100 { + if got, want := a.Float64(), b.Float64(); got != want { + t.Fatalf("draw %d: streams diverged: %v != %v", i, got, want) + } + } +} + +func TestNewRandDifferentSeedsDiffer(t *testing.T) { + a, b := newRand(17), newRand(18) + for range 100 { + if a.Float64() != b.Float64() { + return // diverged, as expected + } + } + t.Fatal("seeds 17 and 18 produced 100 identical draws") +}