peeksafe 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +78 -0
  4. package/README.md +492 -2
  5. package/RESEARCH-NOTES.md +274 -0
  6. package/dist/baseline.d.ts +45 -0
  7. package/dist/baseline.d.ts.map +1 -0
  8. package/dist/baseline.js +49 -0
  9. package/dist/baseline.js.map +1 -0
  10. package/dist/check.d.ts +64 -0
  11. package/dist/check.d.ts.map +1 -0
  12. package/dist/check.js +172 -0
  13. package/dist/check.js.map +1 -0
  14. package/dist/cluster.d.ts +328 -0
  15. package/dist/cluster.d.ts.map +1 -0
  16. package/dist/cluster.js +598 -0
  17. package/dist/cluster.js.map +1 -0
  18. package/dist/errors.d.ts +87 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +143 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/evidence.d.ts +15 -0
  23. package/dist/evidence.d.ts.map +1 -0
  24. package/dist/evidence.js +30 -0
  25. package/dist/evidence.js.map +1 -0
  26. package/dist/frontier.d.ts +269 -0
  27. package/dist/frontier.d.ts.map +1 -0
  28. package/dist/frontier.js +460 -0
  29. package/dist/frontier.js.map +1 -0
  30. package/dist/gate.d.ts +173 -0
  31. package/dist/gate.d.ts.map +1 -0
  32. package/dist/gate.js +196 -0
  33. package/dist/gate.js.map +1 -0
  34. package/dist/index.d.ts +60 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +79 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/plan.d.ts +248 -0
  39. package/dist/plan.d.ts.map +1 -0
  40. package/dist/plan.js +482 -0
  41. package/dist/plan.js.map +1 -0
  42. package/dist/rand.d.ts +17 -0
  43. package/dist/rand.d.ts.map +1 -0
  44. package/dist/rand.js +92 -0
  45. package/dist/rand.js.map +1 -0
  46. package/dist/stats.d.ts +397 -0
  47. package/dist/stats.d.ts.map +1 -0
  48. package/dist/stats.js +1245 -0
  49. package/dist/stats.js.map +1 -0
  50. package/dist/stop.d.ts +163 -0
  51. package/dist/stop.d.ts.map +1 -0
  52. package/dist/stop.js +262 -0
  53. package/dist/stop.js.map +1 -0
  54. package/package.json +64 -4
  55. package/src/baseline.ts +72 -0
  56. package/src/check.ts +238 -0
  57. package/src/cluster.ts +830 -0
  58. package/src/errors.ts +208 -0
  59. package/src/evidence.ts +62 -0
  60. package/src/frontier.ts +782 -0
  61. package/src/gate.ts +358 -0
  62. package/src/index.ts +206 -0
  63. package/src/plan.ts +778 -0
  64. package/src/rand.ts +121 -0
  65. package/src/stats.ts +1579 -0
  66. package/src/stop.ts +452 -0
package/src/rand.ts ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * rand.ts: deterministic randomness.
3
+ *
4
+ * Everything stochastic in peeksafe (the Monte Carlo checks in `test/`, and any
5
+ * simulation a caller wants to run) draws from here.
6
+ *
7
+ * The generator is a linear congruential generator feeding an integer avalanche.
8
+ * The LCG alone is not good enough: consecutive outputs of an LCG sit on a
9
+ * lattice, which visibly biases Bernoulli streams, and a Bernoulli stream is the
10
+ * only thing this library ever simulates. The mixer decorrelates without adding
11
+ * a dependency and without losing determinism.
12
+ *
13
+ * Both sets of constants are published parameters rather than anybody's source
14
+ * code, and both are named in NOTICE.
15
+ *
16
+ * The exact stream matters. The measurements in the README and in
17
+ * `test/paper.test.ts` were produced by this generator, so replacing it with a
18
+ * better one (sfc32, pcg) would change every reported figure. That is a fine
19
+ * thing to do deliberately, with the numbers re-derived and the README updated.
20
+ * It is not a fine thing to do by accident, which is why the constants are
21
+ * spelled out here rather than imported.
22
+ */
23
+ import { PeeksafeError } from "./errors.js";
24
+
25
+ export interface Rand {
26
+ (): number;
27
+ /** A single Bernoulli draw at rate `p`. */
28
+ bernoulli(p: number): boolean;
29
+ /** A uniform integer in `[0, nExclusive)`. */
30
+ int(nExclusive: number): number;
31
+ /** A uniform choice. Throws on an empty array rather than returning undefined. */
32
+ pick<T>(xs: readonly T[]): T;
33
+ /** A standard normal draw, via Box-Muller. */
34
+ normal(): number;
35
+ }
36
+
37
+ /**
38
+ * LCG with Knuth's multiplier 1664525 and increment 1013904223, modulo 2^32.
39
+ *
40
+ * These are tabulated constants (Knuth, TAOCP vol. 2), reprinted in most
41
+ * numerical texts. Exact in float64: 2^32 * 1664525 sits under 2^53, so the
42
+ * product never loses a bit before the mask.
43
+ */
44
+ function lcg(seed: number): () => number {
45
+ let s = seed >>> 0;
46
+ return () => {
47
+ s = (s * 1664525 + 1013904223) >>> 0;
48
+ return s / 0x100000000;
49
+ };
50
+ }
51
+
52
+ /**
53
+ * 32-bit avalanche: the `lowbias32` multipliers from Chris Wellons' hash
54
+ * function prospector, which he placed in the public domain. Two rounds of
55
+ * xor-shift-multiply, chosen by search for low bias rather than derived.
56
+ */
57
+ function mix32(x: number): number {
58
+ let h = x >>> 0;
59
+ h ^= h >>> 16;
60
+ h = Math.imul(h, 0x7feb352d) >>> 0;
61
+ h ^= h >>> 15;
62
+ h = Math.imul(h, 0x846ca68b) >>> 0;
63
+ h ^= h >>> 16;
64
+ return h >>> 0;
65
+ }
66
+
67
+ /** FNV-1a. Used to derive an independent seed per named stream. */
68
+ export function hash32(s: string): number {
69
+ let h = 0x811c9dc5;
70
+ for (let i = 0; i < s.length; i++) {
71
+ h ^= s.charCodeAt(i);
72
+ h = Math.imul(h, 0x01000193) >>> 0;
73
+ }
74
+ return h >>> 0;
75
+ }
76
+
77
+ export function makeRand(seed: number | string): Rand {
78
+ const s = typeof seed === "string" ? hash32(seed) : seed >>> 0;
79
+ const base = lcg(s === 0 ? 1 : s);
80
+ const next = (() => mix32(Math.floor(base() * 0x100000000)) / 0x100000000) as Rand;
81
+
82
+ next.bernoulli = (p: number) => next() < p;
83
+
84
+ next.int = (n: number) => {
85
+ if (!Number.isInteger(n) || n < 1) {
86
+ throw new PeeksafeError(
87
+ "PEEKSAFE_E_STAT_DOMAIN",
88
+ `rand.int: n must be a positive integer, got ${n}`,
89
+ {
90
+ detail: { n },
91
+ },
92
+ );
93
+ }
94
+ return Math.floor(next() * n) % n;
95
+ };
96
+
97
+ // Typed `T`, so it must never hand back `undefined`. Returning `xs[0]` for an
98
+ // empty array is `undefined` wearing a `T`, which is how a fail-open bug gets
99
+ // past the type checker.
100
+ next.pick = <T>(xs: readonly T[]): T => {
101
+ if (xs.length === 0) {
102
+ throw new PeeksafeError(
103
+ "PEEKSAFE_E_STAT_DOMAIN",
104
+ "rand.pick: cannot pick from an empty array",
105
+ );
106
+ }
107
+ return xs[next.int(xs.length)]!;
108
+ };
109
+
110
+ next.normal = () => {
111
+ const u = Math.max(1e-12, next());
112
+ const v = next();
113
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
114
+ };
115
+
116
+ return next;
117
+ }
118
+
119
+ /** A stream keyed by name, reproducible regardless of call order elsewhere. */
120
+ export const streamFor = (...parts: Array<string | number>): Rand =>
121
+ makeRand(hash32(parts.join("|")));