Skip to content

Commit 4826945

Browse files
Add deterministic seeding for generation (#13)
1 parent 6b3d8e3 commit 4826945

5 files changed

Lines changed: 124 additions & 29 deletions

File tree

LOGIC-MAP.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,16 @@ This document explains the logic chain for updating `UPDATES_STRATEGY.md` with s
8484
- **Why:** Verb transitivity must guide whether direct objects appear; adverbs must originate from the lexicon; lyric corpus should load real files when available.
8585
- **Invariant:** Transitive verbs prefer `V NP`, intransitives avoid object insertion; adverbs are derived from `lexicon.Adv`; corpus loading resolves `/lyrics/*.txt` entries before falling back to embedded lines.
8686
- **Proof Sketch:** The VP generator branches on `feats.trans` with explicit paths, and `import.meta.glob` enumerates real assets, ensuring coverage without mock fillers.
87+
88+
## Logic Chain: Deterministic Seeding & RNG Propagation (Steps 1–3)
89+
1. **Identify nondeterministic sources**
90+
- **Why:** Kernel PCA initialization, TD(λ) weights, and grammar generation relied on implicit randomness, making runs irreproducible.
91+
- **Invariant:** All stochastic paths must consume a single seeded RNG when provided.
92+
2. **Centralize seeded RNG consumption**
93+
- **Why:** A single RNG source enables reproducible training and generation across components.
94+
- **Invariant:** KernelPCA, TDValueEstimator, and ConstraintGrammar call the same RNG instance or function.
95+
- **Proof Sketch:** Each component accepts a `rng` dependency and defers calls to `rng()` or `rng.next()`, so the RNG state advances deterministically in a single sequence.
96+
3. **Define deterministic seed resolution**
97+
- **Why:** Users need a stable way to set seeds without UI changes.
98+
- **Invariant:** When `?seed=` is provided, the engine seed is deterministic for both numeric and string inputs; otherwise, default entropy is used.
99+
- **Proof Sketch:** Numeric seeds map directly; string seeds use a deterministic hash (`fnv-1a`), so identical inputs always map to the same 32-bit seed.

TESTING.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,21 @@ Each test uses real logic paths without mock data. Run the generation/analysis m
7373
## Execution Notes
7474
- Use the existing UI controls to trigger sentence generation and analysis for cases 1–8.
7575
- For corpus validation (cases 9–12), temporarily log the returned array length and sample entries after invoking `loadLyricsCorpus`.
76+
77+
## Test Matrix: Deterministic Seeding & RNG Propagation
78+
Run generation with `?seed=` in the URL and confirm reproducible output across reloads.
79+
80+
- **-1:** `?seed=` empty string should fall back to default entropy (non-deterministic across reloads).
81+
- **0:** `?seed=0` yields a stable poem across reloads.
82+
- **1:** `?seed=1` yields a stable poem across reloads.
83+
- **2:** `?seed=2` yields a stable poem across reloads.
84+
- **3:** `?seed=3` yields a stable poem across reloads.
85+
- **4:** `?seed=-42` yields a stable poem across reloads.
86+
- **5:** `?seed=3.14` is floored to `3` and yields stable output.
87+
- **6:** `?seed=00012` resolves to `12` and yields stable output.
88+
- **7:** `?seed=alpha` hashes to a deterministic 32-bit seed and yields stable output.
89+
- **8:** `?seed=alpha` matches identical output across browsers (same seed string).
90+
- **9:** `?seed=alpha-beta` yields a different but stable output from `alpha`.
91+
- **10:** `?seed=9999999999` yields stable output without overflow errors.
92+
- **11:** Remove `?seed=` after using a deterministic seed to confirm non-deterministic default again.
93+
- **12:** Load a checkpoint after setting `?seed=` and confirm continued deterministic generation.

WE-CHOSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,24 @@ Document the three-perspective planning approach for the formatting adjustment i
7777
- **End Customer:** Higher phonetic and grammatical coherence improves usability.
7878

7979
**Mapped logic chain reference:** LOGIC-MAP.md (Steps 1–3, Linguistic Engine & Corpus Loading).
80+
81+
# Perspective Selection: Deterministic Seeding & RNG Propagation
82+
83+
### CEO Perspective
84+
- **Goal:** Provide repeatable outcomes for demos, testing, and stakeholder review.
85+
- **Choice:** Single seeded RNG enables consistent outputs when `?seed=` is supplied.
86+
87+
### Junior Developer Perspective
88+
- **Goal:** Make randomness easy to reason about and debug.
89+
- **Choice:** Central RNG injection removes hidden `Math.random()` usage and simplifies tracing.
90+
91+
### End Customer Perspective
92+
- **Goal:** Allow users to reproduce poems or training runs reliably.
93+
- **Choice:** Seed parsing accepts numeric and string values without requiring new UI controls.
94+
95+
### Combined Choice Justification
96+
- **CEO:** Repeatable results reduce demo risk and simplify acceptance validation.
97+
- **Junior Dev:** Shared RNG path avoids fragmented randomness sources.
98+
- **End Customer:** Reproducible outputs improve trust and usability.
99+
100+
**Mapped logic chain reference:** LOGIC-MAP.md (Steps 1–3, Deterministic Seeding & RNG Propagation).

src/App.jsx

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,39 @@ import { UniversalLinguisticEngine } from './UniversalLinguisticEngine';
2323
// CORE ALGORITHM IMPLEMENTATIONS
2424
// ============================================================================
2525

26+
const hashSeedString = (value) => {
27+
let hash = 2166136261;
28+
for (let i = 0; i < value.length; i++) {
29+
hash ^= value.charCodeAt(i);
30+
hash = Math.imul(hash, 16777619);
31+
}
32+
return hash >>> 0;
33+
};
34+
35+
const resolveSeedFromLocation = () => {
36+
if (typeof window === 'undefined') return null;
37+
const params = new URLSearchParams(window.location.search);
38+
const seedParam = params.get('seed');
39+
if (seedParam === null) return null;
40+
const normalized = seedParam.trim();
41+
if (!normalized) return null;
42+
const parsed = Number(normalized);
43+
if (Number.isFinite(parsed)) return Math.floor(parsed);
44+
return hashSeedString(normalized);
45+
};
46+
2647
/**
2748
* Kernel PCA for Non-linear Theme Embedding
2849
* Transforms word embeddings into emotional/thematic space using polynomial kernel
2950
*/
3051
class KernelPCA {
31-
constructor(nComponents = 12, degree = 3) {
52+
constructor(nComponents = 12, degree = 3, rng = Math.random) {
3253
this.nComponents = nComponents;
3354
this.degree = degree;
3455
this.eigenvectors = null;
3556
this.eigenvalues = null;
3657
this.X_fit = null;
58+
this.rng = rng;
3759
}
3860

3961
/**
@@ -90,8 +112,8 @@ class KernelPCA {
90112

91113
for (let k = 0; k < Math.min(this.nComponents, n); k++) {
92114
// Initialize random vector for PCA eigenvalue decomposition
93-
// Math.random() is acceptable here: used for ML initialization, not cryptographic purposes
94-
let v = Array(n).fill().map(() => Math.random() - 0.5);
115+
// Non-cryptographic RNG allows deterministic seeding when provided.
116+
let v = Array(n).fill().map(() => this.rng() - 0.5);
95117

96118
// Gram-Schmidt orthogonalization against previous eigenvectors
97119
for (let i = 0; i < eigenvectors.length; i++) {
@@ -178,9 +200,9 @@ class KernelPCA {
178200
* Estimates aesthetic value of poetic states for reinforcement learning
179201
*/
180202
class TDValueEstimator {
181-
constructor(nFeatures = 24, alpha = 0.01, gamma = 0.95, lambda = 0.8) {
182-
// Math.random() is acceptable here: used for neural network weight initialization, not cryptographic purposes
183-
this.weights = Array(nFeatures).fill(0).map(() => Math.random() * 0.01);
203+
constructor(nFeatures = 24, alpha = 0.01, gamma = 0.95, lambda = 0.8, rng = Math.random) {
204+
// Non-cryptographic RNG allows deterministic seeding when provided.
205+
this.weights = Array(nFeatures).fill(0).map(() => rng() * 0.01);
184206
this.alpha = alpha;
185207
this.gamma = gamma;
186208
this.lambda = lambda;
@@ -477,12 +499,13 @@ class FFTMeterAnalyzer {
477499
* Orchestrates all algorithms for multi-objective poetry generation
478500
*/
479501
class AGTuneEngine {
480-
constructor() {
481-
this.kpca = new KernelPCA(12, 3);
482-
this.valueEstimator = new TDValueEstimator(24, 0.01, 0.95, 0.8);
483-
this.rng = new LaggedFibonacciGenerator();
502+
constructor(seed = Date.now()) {
503+
this.rng = new LaggedFibonacciGenerator(seed);
504+
const rng = () => this.rng.next();
505+
this.kpca = new KernelPCA(12, 3, rng);
506+
this.valueEstimator = new TDValueEstimator(24, 0.01, 0.95, 0.8, rng);
484507
this.rete = new ReteEngine();
485-
this.ule = new UniversalLinguisticEngine(); // Integration of Production-Grade Logic
508+
this.ule = new UniversalLinguisticEngine({ rng: this.rng }); // Integration of Production-Grade Logic
486509
this.parser = new CYKParser(this.ule.getGrammar());
487510
this.vocabulary = new Set();
488511
this.embeddings = new Map();
@@ -737,7 +760,11 @@ class AGTuneEngine {
737760
}
738761

739762
// Restore KPCA (defensively sanitize numeric content)
740-
this.kpca = new KernelPCA(data.kpca?.nComponents || 8, data.kpca?.degree || 3);
763+
this.kpca = new KernelPCA(
764+
data.kpca?.nComponents || 8,
765+
data.kpca?.degree || 3,
766+
() => this.rng.next()
767+
);
741768
this.kpca.eigenvectors = this._asNumberMatrix(data.kpca?.eigenvectors);
742769
this.kpca.eigenvalues = this._asNumberArray(data.kpca?.eigenvalues);
743770
this.kpca.X_fit = this._asNumberMatrix(data.kpca?.X_fit);
@@ -747,7 +774,8 @@ class AGTuneEngine {
747774
data.valueEstimator?.weights?.length || 16,
748775
data.valueEstimator?.alpha ?? 0.01,
749776
data.valueEstimator?.gamma ?? 0.95,
750-
data.valueEstimator?.lambda ?? 0.8
777+
data.valueEstimator?.lambda ?? 0.8,
778+
() => this.rng.next()
751779
);
752780
this.valueEstimator.weights = this._asNumberArray(
753781
data.valueEstimator?.weights,
@@ -859,8 +887,8 @@ class AGTuneEngine {
859887

860888
const scored = candidates.map(word => {
861889
const eSpace = this.emotionalSpace.get(word);
862-
// Math.random() is acceptable here: used for fallback scoring in ML algorithm, not cryptographic purposes
863-
if (!eSpace) return { word, score: Math.random() };
890+
// Non-cryptographic RNG allows deterministic seeding when provided.
891+
if (!eSpace) return { word, score: this.rng.next() };
864892

865893
const dist = Math.sqrt(eSpace.reduce((sum, v, i) => {
866894
const diff = v - (lastESpace[i] || 0);
@@ -1029,7 +1057,10 @@ class AGTuneEngine {
10291057
* Interactive interface for training and poetry generation
10301058
*/
10311059
export default function AGTunePoet() {
1032-
const [engine] = useState(() => new AGTuneEngine());
1060+
const [engine] = useState(() => {
1061+
const seed = resolveSeedFromLocation();
1062+
return new AGTuneEngine(Number.isFinite(seed) ? seed : Date.now());
1063+
});
10331064
const [corpus, setCorpus] = useState([]);
10341065
const [trainingState, setTrainingState] = useState({
10351066
isTraining: false,

src/UniversalLinguisticEngine.js

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
* Replaces static lookup tables with algorithmic reasoning.
99
*/
1010
export class UniversalLinguisticEngine {
11-
constructor() {
11+
constructor({ rng } = {}) {
12+
this.rng = rng;
1213
this.phonetics = new PhoneticEngine();
13-
this.grammar = new ConstraintGrammar();
14+
this.grammar = new ConstraintGrammar(rng);
1415
}
1516

1617
/**
@@ -303,10 +304,21 @@ class PhoneticEngine {
303304
* Supports agreement (Number, Person, Tense) and recursive generation.
304305
*/
305306
class ConstraintGrammar {
306-
constructor() {
307+
constructor(rng) {
308+
this.rng = rng;
307309
this.lexicon = this._buildProductionLexicon();
308310
}
309311

312+
_random() {
313+
if (this.rng && typeof this.rng.next === 'function') {
314+
return this.rng.next();
315+
}
316+
if (typeof this.rng === 'function') {
317+
return this.rng();
318+
}
319+
return Math.random();
320+
}
321+
310322
_buildProductionLexicon() {
311323
// Extensive Lexicon categorized by features
312324
return {
@@ -425,11 +437,11 @@ class ConstraintGrammar {
425437
if (candidates.length === 0) {
426438
// Fallback: relax constraints if too strict (simple error recovery)
427439
if (this.lexicon[symbol].length > 0) {
428-
return this.lexicon[symbol][Math.floor(Math.random() * this.lexicon[symbol].length)];
440+
return this.lexicon[symbol][Math.floor(this._random() * this.lexicon[symbol].length)];
429441
}
430442
return null;
431443
}
432-
return candidates[Math.floor(Math.random() * candidates.length)];
444+
return candidates[Math.floor(this._random() * candidates.length)];
433445
}
434446

435447
/**
@@ -447,18 +459,18 @@ class ConstraintGrammar {
447459
case 'S':
448460
// S -> NP VP
449461
// We decide on a 'number' feature for the subject, which propagates to VP
450-
const num = Math.random() > 0.5 ? 'sg' : 'pl';
462+
const num = this._random() > 0.5 ? 'sg' : 'pl';
451463
return `${this.generate('NP', { num })} ${this.generate('VP', { num })}`;
452464

453465
case 'NP': {
454466
// NP -> Det N | Det Adj N | N (plural/abstract) | Det Adj Adj N
455-
const r = Math.random();
467+
const r = this._random();
456468

457469
// If constraint is not provided, we should decide on one internally so Det and N agree.
458470
// If external constraints exists (from S), we use them.
459471
let localConstraints = { ...constraints };
460472
if (!localConstraints.num) {
461-
localConstraints.num = Math.random() > 0.5 ? 'sg' : 'pl';
473+
localConstraints.num = this._random() > 0.5 ? 'sg' : 'pl';
462474
}
463475

464476
if (r < 0.35) {
@@ -493,13 +505,13 @@ class ConstraintGrammar {
493505

494506
// Optional Adverb prefix
495507
let prefix = "";
496-
if (Math.random() < 0.25) {
508+
if (this._random() < 0.25) {
497509
prefix = this.generate('Adv') + " ";
498510
}
499511

500512
if (trans === 'intrans') {
501513
// Intransitive: V or V PP
502-
if (Math.random() < 0.4) {
514+
if (this._random() < 0.4) {
503515
return `${prefix}${verb} ${this.generate('PP')}`;
504516
}
505517
return `${prefix}${verb}`;
@@ -508,7 +520,7 @@ class ConstraintGrammar {
508520
// Note: Object NP doesn't need to agree with Subject (constraints), so we pass empty constraints
509521
// or specific ones (like Accusative case if we had cases)
510522
// Passing empty constraints allows NP to pick its own number agreement
511-
if (Math.random() < 0.2) {
523+
if (this._random() < 0.2) {
512524
return `${prefix}${verb} ${this.generate('NP')} ${this.generate('PP')}`;
513525
}
514526
return `${prefix}${verb} ${this.generate('NP')}`;
@@ -562,10 +574,10 @@ class ConstraintGrammar {
562574
});
563575

564576
if (candidates.length > 0) {
565-
return candidates[Math.floor(Math.random() * candidates.length)];
577+
return candidates[Math.floor(this._random() * candidates.length)];
566578
}
567579
if (entries.length > 0) {
568-
return entries[Math.floor(Math.random() * entries.length)];
580+
return entries[Math.floor(this._random() * entries.length)];
569581
}
570582
return { word: '?' };
571583
}

0 commit comments

Comments
 (0)