memlnaut-nisps/manifold/src/feedback/rng.ts

65 lines
2.5 KiB
TypeScript
Raw Normal View History

/**
* Deterministic seeded RNG for the feedback controller's hot path.
*
* The rl-feedback-design (§6) mandates: "every new operation is deterministic
* f32 arithmetic on the per-instance `nisps::Rng` (no libc `rand()` anywhere)".
* In the C++ core the controller owns a `nisps::Rng` seeded from
* `kSeed ^ kFeedbackSalt`. This TS prototype mirrors that discipline so that the
* `nudge` perturbation is reproducible run-to-run (no `Math.random` in the
* core path see the task CONSTRAINTS).
*
* Implementation: a small splitmix64-style integer generator reduced to f32.
* This is NOT bit-identical to the C++ `nisps::Rng` when the geometric push /
* nudge becomes a C++ core primitive (rl-feedback-design §4), the seeded stream
* must come from `nisps::Rng` so native==WASM parity holds. Here it only needs
* to be deterministic *within* the prototype.
*
* --- C++ GAP -------------------------------------------------------------
* The true firmware nudge perturbs weights with `move_weights(speed, spread)`
* driven by the controller's `nisps::Rng`. This TS RNG is a stand-in so the
* prototype is reproducible; it will be REPLACED by the engine's own Rng stream
* once `nisps_ml_feedback_nudge` exists (rl-feedback-design §4 "TS").
* ------------------------------------------------------------------------
*/
export class SeededRng {
// 64-bit state held as two 32-bit halves (BigInt would be cleaner but we keep
// to plain number maths to avoid any per-call BigInt allocation in the hot
// nudge loop).
private state: number;
constructor(seed: number) {
// Fold the seed into a non-zero 32-bit state.
this.state = (seed ^ 0x9e3779b9) >>> 0;
if (this.state === 0) this.state = 0x1234567;
}
/** Next uniform float in [0, 1). xorshift32 — deterministic, allocation-free. */
nextFloat(): number {
let x = this.state;
x ^= x << 13;
x >>>= 0;
x ^= x >>> 17;
x ^= x << 5;
x >>>= 0;
this.state = x;
// Map to [0,1) using the top 24 bits for a clean float mantissa.
return (x >>> 8) / 0x01000000;
}
/** Next uniform float in [-1, 1). */
nextFloatSigned(): number {
return this.nextFloat() * 2 - 1;
}
/**
* Approximate gaussian via the sum-of-three-uniforms method the nisps core
* uses (`gen_randn` in MEMORY.md: sum of 3 uniforms). Mean 0, the given
* standard deviation. Allocation-free.
*/
nextGaussian(stddev: number): number {
const u = this.nextFloatSigned() + this.nextFloatSigned() + this.nextFloatSigned();
return u * stddev;
}
}