bireactive 0.2.4 → 0.3.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 (96) hide show
  1. package/dist/animation/anim.js +4 -0
  2. package/dist/coll.d.ts +7 -7
  3. package/dist/core/cell.d.ts +89 -66
  4. package/dist/core/cell.js +642 -401
  5. package/dist/core/index.d.ts +4 -14
  6. package/dist/core/index.js +4 -14
  7. package/dist/core/lenses/aggregates.d.ts +1 -1
  8. package/dist/core/lenses/aggregates.js +4 -3
  9. package/dist/core/lenses/closed-form-policies.js +6 -6
  10. package/dist/core/lenses/decompositions.js +3 -3
  11. package/dist/core/lenses/domain-aggregates.js +5 -5
  12. package/dist/core/lenses/geometry.d.ts +1 -1
  13. package/dist/core/lenses/geometry.js +6 -7
  14. package/dist/core/lenses/memory.d.ts +2 -2
  15. package/dist/core/lenses/memory.js +3 -3
  16. package/dist/core/lenses/typed-factor.js +4 -3
  17. package/dist/core/traits.d.ts +1 -0
  18. package/dist/core/values/box.js +7 -7
  19. package/dist/core/values/color.js +5 -5
  20. package/dist/core/values/field.d.ts +70 -0
  21. package/dist/core/values/field.js +230 -0
  22. package/dist/core/values/gpu.d.ts +4 -2
  23. package/dist/core/values/gpu.js +11 -4
  24. package/dist/core/values/matrix.js +7 -7
  25. package/dist/core/values/num.d.ts +1 -1
  26. package/dist/core/values/num.js +1 -1
  27. package/dist/core/values/pose.js +4 -4
  28. package/dist/core/values/range.js +6 -6
  29. package/dist/core/values/template.d.ts +1 -1
  30. package/dist/core/values/template.js +2 -1
  31. package/dist/core/values/transform.js +7 -7
  32. package/dist/core/values/tri.js +3 -3
  33. package/dist/core/values/vec.js +8 -12
  34. package/dist/ext/timeline.js +2 -2
  35. package/dist/formats/cst.d.ts +127 -0
  36. package/dist/formats/cst.js +280 -0
  37. package/dist/formats/edn.d.ts +2 -0
  38. package/dist/formats/edn.js +301 -0
  39. package/dist/formats/index.d.ts +6 -0
  40. package/dist/formats/index.js +8 -0
  41. package/dist/formats/json.d.ts +2 -0
  42. package/dist/formats/json.js +332 -0
  43. package/dist/formats/lens.d.ts +8 -0
  44. package/dist/formats/lens.js +54 -0
  45. package/dist/formats/toml.d.ts +2 -0
  46. package/dist/formats/toml.js +526 -0
  47. package/dist/formats/yaml.d.ts +2 -0
  48. package/dist/formats/yaml.js +661 -0
  49. package/dist/index.d.ts +10 -0
  50. package/dist/index.js +10 -0
  51. package/dist/learn/data.d.ts +49 -0
  52. package/dist/learn/data.js +181 -0
  53. package/dist/learn/index.d.ts +3 -0
  54. package/dist/learn/index.js +6 -0
  55. package/dist/learn/lens-net.d.ts +63 -0
  56. package/dist/learn/lens-net.js +219 -0
  57. package/dist/learn/mlp.d.ts +77 -0
  58. package/dist/learn/mlp.js +292 -0
  59. package/dist/propagators/csp.d.ts +13 -0
  60. package/dist/propagators/csp.js +52 -0
  61. package/dist/propagators/flex.d.ts +31 -0
  62. package/dist/propagators/flex.js +189 -0
  63. package/dist/propagators/graph.d.ts +73 -0
  64. package/dist/propagators/graph.js +543 -0
  65. package/dist/propagators/index.d.ts +8 -6
  66. package/dist/propagators/index.js +15 -6
  67. package/dist/propagators/lattice.d.ts +45 -0
  68. package/dist/propagators/lattice.js +113 -0
  69. package/dist/propagators/layout.d.ts +1 -27
  70. package/dist/propagators/layout.js +6 -175
  71. package/dist/propagators/numeric.d.ts +17 -0
  72. package/dist/propagators/numeric.js +93 -0
  73. package/dist/propagators/solver.d.ts +51 -0
  74. package/dist/propagators/solver.js +175 -0
  75. package/dist/schema/index.d.ts +1 -0
  76. package/dist/schema/index.js +3 -0
  77. package/dist/schema/lens.d.ts +121 -0
  78. package/dist/schema/lens.js +429 -0
  79. package/dist/shapes/annular-sector.js +4 -4
  80. package/dist/shapes/button.js +1 -1
  81. package/dist/shapes/circle.js +1 -1
  82. package/dist/shapes/handle.js +2 -2
  83. package/dist/shapes/label.js +1 -1
  84. package/dist/shapes/layout.js +2 -2
  85. package/dist/shapes/rect.js +7 -7
  86. package/dist/shapes/shape.js +8 -8
  87. package/dist/web/diagram.js +2 -2
  88. package/package.json +1 -1
  89. package/dist/propagators/network.d.ts +0 -52
  90. package/dist/propagators/network.js +0 -185
  91. package/dist/propagators/propagator.d.ts +0 -12
  92. package/dist/propagators/propagator.js +0 -16
  93. package/dist/propagators/range.d.ts +0 -45
  94. package/dist/propagators/range.js +0 -147
  95. package/dist/propagators/relations.d.ts +0 -60
  96. package/dist/propagators/relations.js +0 -343
@@ -0,0 +1,49 @@
1
+ import { type Sample } from "./mlp.js";
2
+ /** A 2D point dataset: each sample's `x` is `[px, py]`, `y` is `0|1`. */
3
+ export type Points = Sample[];
4
+ /** Two interleaving half-moons (the reliable "is it learning" classic). */
5
+ export declare function moons(n: number, opts?: {
6
+ seed?: number;
7
+ noise?: number;
8
+ }): Points;
9
+ /** Concentric rings: inner blob (class 0) inside an outer ring (class 1). */
10
+ export declare function circles(n: number, opts?: {
11
+ seed?: number;
12
+ noise?: number;
13
+ }): Points;
14
+ /** Four quadrant clusters; class 1 where the coordinate signs differ. */
15
+ export declare function xor(n: number, opts?: {
16
+ seed?: number;
17
+ noise?: number;
18
+ }): Points;
19
+ /** Two intertwined spirals — the hard one (may need more steps/capacity). */
20
+ export declare function spirals(n: number, opts?: {
21
+ seed?: number;
22
+ noise?: number;
23
+ }): Points;
24
+ /** A 2D dataset family selectable in the demo. */
25
+ export type PointsKind = "moons" | "circles" | "xor" | "spirals";
26
+ /** Build a 2D dataset by name. */
27
+ export declare function points(kind: PointsKind, n: number, opts?: {
28
+ seed?: number;
29
+ noise?: number;
30
+ }): Points;
31
+ /** A rasterisable shape. The binary task is `circle` (class 1) vs the rest. */
32
+ export type ShapeKind = "circle" | "square" | "triangle";
33
+ /** Placement of a shape in normalised `[0,1]²` grid space. */
34
+ export interface ShapePose {
35
+ cx: number;
36
+ cy: number;
37
+ r: number;
38
+ rot: number;
39
+ }
40
+ /** Rasterise a posed shape onto a `grid×grid` coverage buffer (0..1) via
41
+ * 3×3 supersampling. */
42
+ export declare function rasterShape(kind: ShapeKind, grid: number, pose: ShapePose): Float64Array;
43
+ /** Random pose for a roughly-centred shape (small jitter, moderate size,
44
+ * free rotation) — learnable from raw pixels and legible when drawn. */
45
+ export declare function randomPose(r: () => number): ShapePose;
46
+ /** One labelled pixel sample: `circle` → class 1, `square`/`triangle` → 0. */
47
+ export declare function shapeSample(grid: number, r: () => number, noise?: number): Sample;
48
+ /** A batch of `n` fresh pixel samples. */
49
+ export declare function shapeBatch(grid: number, n: number, r: () => number): Sample[];
@@ -0,0 +1,181 @@
1
+ // data.ts — synthetic, reproducible datasets for the learning demos.
2
+ //
3
+ // Two families: 2D point clouds (moons / circles / xor / spirals) where the
4
+ // learned decision boundary is the visual; and rasterised shapes on a small
5
+ // pixel grid (circle vs square/triangle) generated on the fly, so training
6
+ // data is endless and the label is whatever the generator drew.
7
+ import { gaussian, rng } from "./mlp.js";
8
+ /** Two interleaving half-moons (the reliable "is it learning" classic). */
9
+ export function moons(n, opts = {}) {
10
+ const r = rng(opts.seed ?? 7);
11
+ const noise = opts.noise ?? 0.12;
12
+ const out = [];
13
+ for (let i = 0; i < n; i++) {
14
+ const top = i % 2 === 0;
15
+ const a = r() * Math.PI;
16
+ let px;
17
+ let py;
18
+ if (top) {
19
+ px = Math.cos(a);
20
+ py = Math.sin(a) - 0.25;
21
+ }
22
+ else {
23
+ px = 1 - Math.cos(a);
24
+ py = 0.25 - Math.sin(a);
25
+ }
26
+ out.push({
27
+ x: [px - 0.5 + gaussian(r) * noise, py + gaussian(r) * noise],
28
+ y: top ? 0 : 1,
29
+ });
30
+ }
31
+ return out;
32
+ }
33
+ /** Concentric rings: inner blob (class 0) inside an outer ring (class 1). */
34
+ export function circles(n, opts = {}) {
35
+ const r = rng(opts.seed ?? 7);
36
+ const noise = opts.noise ?? 0.1;
37
+ const out = [];
38
+ for (let i = 0; i < n; i++) {
39
+ const inner = i % 2 === 0;
40
+ const rad = inner ? 0.35 : 0.9;
41
+ const a = r() * 2 * Math.PI;
42
+ out.push({
43
+ x: [Math.cos(a) * rad + gaussian(r) * noise, Math.sin(a) * rad + gaussian(r) * noise],
44
+ y: inner ? 0 : 1,
45
+ });
46
+ }
47
+ return out;
48
+ }
49
+ /** Four quadrant clusters; class 1 where the coordinate signs differ. */
50
+ export function xor(n, opts = {}) {
51
+ const r = rng(opts.seed ?? 7);
52
+ const noise = opts.noise ?? 0.18;
53
+ const out = [];
54
+ for (let i = 0; i < n; i++) {
55
+ const sx = i & 1 ? 1 : -1;
56
+ const sy = i & 2 ? 1 : -1;
57
+ out.push({
58
+ x: [sx * 0.6 + gaussian(r) * noise, sy * 0.6 + gaussian(r) * noise],
59
+ y: sx === sy ? 0 : 1,
60
+ });
61
+ }
62
+ return out;
63
+ }
64
+ /** Two intertwined spirals — the hard one (may need more steps/capacity). */
65
+ export function spirals(n, opts = {}) {
66
+ const r = rng(opts.seed ?? 7);
67
+ const noise = opts.noise ?? 0.06;
68
+ const out = [];
69
+ const per = Math.ceil(n / 2);
70
+ for (let c = 0; c < 2; c++) {
71
+ for (let i = 0; i < per; i++) {
72
+ const t = (i / per) * 3.2;
73
+ const a = t * Math.PI + c * Math.PI;
74
+ const rad = 0.15 + t * 0.26;
75
+ out.push({
76
+ x: [Math.cos(a) * rad + gaussian(r) * noise, Math.sin(a) * rad + gaussian(r) * noise],
77
+ y: c,
78
+ });
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+ /** Build a 2D dataset by name. */
84
+ export function points(kind, n, opts = {}) {
85
+ switch (kind) {
86
+ case "moons":
87
+ return moons(n, opts);
88
+ case "circles":
89
+ return circles(n, opts);
90
+ case "xor":
91
+ return xor(n, opts);
92
+ default:
93
+ return spirals(n, opts);
94
+ }
95
+ }
96
+ // Point-in-shape test in normalised coords.
97
+ function inside(kind, p, x, y) {
98
+ const dx = x - p.cx;
99
+ const dy = y - p.cy;
100
+ if (kind === "circle")
101
+ return dx * dx + dy * dy <= p.r * p.r;
102
+ const cs = Math.cos(-p.rot);
103
+ const sn = Math.sin(-p.rot);
104
+ const rx = dx * cs - dy * sn;
105
+ const ry = dx * sn + dy * cs;
106
+ if (kind === "square") {
107
+ const s = p.r * 0.86;
108
+ return Math.abs(rx) <= s && Math.abs(ry) <= s;
109
+ }
110
+ // Equilateral triangle, circumradius r, vertices at 90°/210°/330°.
111
+ for (let k = 0; k < 3; k++) {
112
+ const a = (Math.PI / 2) * -1 + (k * 2 * Math.PI) / 3;
113
+ // Inward normal of the edge opposite vertex k points toward center;
114
+ // test the half-plane through the two other vertices.
115
+ const a1 = -Math.PI / 2 + (((k + 1) % 3) * 2 * Math.PI) / 3;
116
+ const a2 = -Math.PI / 2 + (((k + 2) % 3) * 2 * Math.PI) / 3;
117
+ const x1 = Math.cos(a1) * p.r;
118
+ const y1 = Math.sin(a1) * p.r;
119
+ const x2 = Math.cos(a2) * p.r;
120
+ const y2 = Math.sin(a2) * p.r;
121
+ const ex = x2 - x1;
122
+ const ey = y2 - y1;
123
+ // Cross product sign: center (0,0) must be on the same side as (rx,ry).
124
+ const side = ex * (ry - y1) - ey * (rx - x1);
125
+ const cside = ex * (0 - y1) - ey * (0 - x1);
126
+ if (Math.sign(side) !== Math.sign(cside) && side !== 0)
127
+ return false;
128
+ void a;
129
+ }
130
+ return true;
131
+ }
132
+ /** Rasterise a posed shape onto a `grid×grid` coverage buffer (0..1) via
133
+ * 3×3 supersampling. */
134
+ export function rasterShape(kind, grid, pose) {
135
+ const out = new Float64Array(grid * grid);
136
+ const S = 3;
137
+ for (let gy = 0; gy < grid; gy++) {
138
+ for (let gx = 0; gx < grid; gx++) {
139
+ let hit = 0;
140
+ for (let sy = 0; sy < S; sy++) {
141
+ for (let sx = 0; sx < S; sx++) {
142
+ const x = (gx + (sx + 0.5) / S) / grid;
143
+ const y = (gy + (sy + 0.5) / S) / grid;
144
+ if (inside(kind, pose, x, y))
145
+ hit++;
146
+ }
147
+ }
148
+ out[gy * grid + gx] = hit / (S * S);
149
+ }
150
+ }
151
+ return out;
152
+ }
153
+ /** Random pose for a roughly-centred shape (small jitter, moderate size,
154
+ * free rotation) — learnable from raw pixels and legible when drawn. */
155
+ export function randomPose(r) {
156
+ return {
157
+ cx: 0.5 + (r() - 0.5) * 0.16,
158
+ cy: 0.5 + (r() - 0.5) * 0.16,
159
+ r: 0.26 + r() * 0.12,
160
+ rot: r() * Math.PI * 2,
161
+ };
162
+ }
163
+ /** One labelled pixel sample: `circle` → class 1, `square`/`triangle` → 0. */
164
+ export function shapeSample(grid, r, noise = 0.04) {
165
+ const kind = r() < 0.5 ? "circle" : r() < 0.5 ? "square" : "triangle";
166
+ const buf = rasterShape(kind, grid, randomPose(r));
167
+ if (noise > 0)
168
+ for (let i = 0; i < buf.length; i++)
169
+ buf[i] = clamp01(buf[i] + gaussian(r) * noise);
170
+ return { x: buf, y: kind === "circle" ? 1 : 0 };
171
+ }
172
+ /** A batch of `n` fresh pixel samples. */
173
+ export function shapeBatch(grid, n, r) {
174
+ const out = [];
175
+ for (let i = 0; i < n; i++)
176
+ out.push(shapeSample(grid, r));
177
+ return out;
178
+ }
179
+ function clamp01(v) {
180
+ return v < 0 ? 0 : v > 1 ? 1 : v;
181
+ }
@@ -0,0 +1,3 @@
1
+ export { circles, moons, type Points, type PointsKind, points, randomPose, rasterShape, type ShapeKind, type ShapePose, shapeBatch, shapeSample, spirals, xor, } from "./data.js";
2
+ export { accuracyOf, classifyOf, inputGradient, type LayerParams, type LensLayer, type LensNet, type LensNetCfg, lensNet, logitsOf, meanLossOf, probsOf, trainEpoch, trainExample, } from "./lens-net.js";
3
+ export { type Activation, gaussian, rng, type Sample } from "./mlp.js";
@@ -0,0 +1,6 @@
1
+ // learn — a tiny, dependency-free MLP framed as a stack of parametric lenses,
2
+ // plus reproducible datasets for the classification demos. Imported by the
3
+ // site via "@bireactive/learn"; not part of the main barrel.
4
+ export { circles, moons, points, randomPose, rasterShape, shapeBatch, shapeSample, spirals, xor, } from "./data.js";
5
+ export { accuracyOf, classifyOf, inputGradient, lensNet, logitsOf, meanLossOf, probsOf, trainEpoch, trainExample, } from "./lens-net.js";
6
+ export { gaussian, rng } from "./mlp.js";
@@ -0,0 +1,63 @@
1
+ import { type Cell, type Writable } from "../core/index.js";
2
+ import { type Activation, type Sample } from "./mlp.js";
3
+ /** Dense-layer parameters: row-major `out×in` weights + `out` biases. */
4
+ export interface LayerParams {
5
+ W: Float64Array;
6
+ b: Float64Array;
7
+ }
8
+ /** Training knobs read live by every layer's backward map. */
9
+ export interface LensNetCfg {
10
+ /** SGD learning rate for the weight step. */
11
+ lr: number;
12
+ /** Freeze the weights: their backward update is `SKIP`ped and only the input
13
+ * cotangent flows — the inversion ("dream") leg. */
14
+ frozen: boolean;
15
+ }
16
+ /** One dense layer as a lens: `out = act(W·in + b)`, `params` its weight source. */
17
+ export interface LensLayer {
18
+ inDim: number;
19
+ outDim: number;
20
+ act: Activation;
21
+ params: Writable<Cell<LayerParams>>;
22
+ out: Writable<Cell<Float64Array>>;
23
+ }
24
+ /** A net wired as a lens DAG `input → layer → … → logits`. Train by writing the
25
+ * output cotangent to `logits`; the engine backpropagates to the sources. */
26
+ export interface LensNet {
27
+ input: Writable<Cell<Float64Array>>;
28
+ layers: LensLayer[];
29
+ logits: Writable<Cell<Float64Array>>;
30
+ cfg: LensNetCfg;
31
+ dims: readonly number[];
32
+ }
33
+ /** Build a net as a lens DAG. `dims` is `[in, h1, …, out]`; hidden layers use
34
+ * `hidden` activation, the output is `linear` (the squash folds into the loss). */
35
+ export declare function lensNet(dims: readonly number[], opts?: {
36
+ seed?: number;
37
+ hidden?: Activation;
38
+ lr?: number;
39
+ }): LensNet;
40
+ /** Logits for `x`, reading the current weight cells (no training, untracked).
41
+ * Applies any pending backward write first, so it always sees fresh weights. */
42
+ export declare function logitsOf(net: LensNet, x: ArrayLike<number>): Float64Array;
43
+ /** Class probabilities: sigmoid for a 1-logit (binary) net, else softmax. */
44
+ export declare function probsOf(net: LensNet, x: ArrayLike<number>): Float64Array;
45
+ /** Argmax class for a multi-logit net, or `P ≥ 0.5` for a binary net. */
46
+ export declare function classifyOf(net: LensNet, x: ArrayLike<number>): number;
47
+ /** Fraction of `data` classified correctly. */
48
+ export declare function accuracyOf(net: LensNet, data: readonly Sample[]): number;
49
+ /** Mean cross-entropy over a dataset (no update) — for monitoring/tests. */
50
+ export declare function meanLossOf(net: LensNet, data: readonly Sample[]): number;
51
+ /** Train one example with a single backward write: pin the input, read the
52
+ * prediction (the forward pull), then write the output cotangent to `logits`.
53
+ * The engine backpropagates and lands an SGD step on every weight cell. The
54
+ * step is forced before returning (while the input still holds this example),
55
+ * so callers may move on immediately. Returns the loss before the step. */
56
+ export declare function trainExample(net: LensNet, x: ArrayLike<number>, y: number): number;
57
+ /** Train one shuffled pass — one backward write per example (online SGD).
58
+ * Returns the mean loss over the epoch. */
59
+ export declare function trainEpoch(net: LensNet, data: readonly Sample[], r: () => number): number;
60
+ /** Input-space gradient toward raising logit `cls`, by one frozen-weight
61
+ * backward write: with the weights held fixed the cotangent flows past them to
62
+ * the input cell, which then holds dL/dInput. Drives the "dream" / saliency. */
63
+ export declare function inputGradient(net: LensNet, x: ArrayLike<number>, cls?: number): Float64Array;
@@ -0,0 +1,219 @@
1
+ // lens-net.ts — the MLP of `mlp.ts`, but wired as an actual lens DAG so that
2
+ // *training is a backward write*.
3
+ //
4
+ // Each dense layer is a multi-parent stateful lens over `[paramsCell, inputCell]`:
5
+ // fwd computes the activation `act(W·x + b)`,
6
+ // bwd receives the cotangent dL/da, deposits an SGD step on the weight cell,
7
+ // and passes dL/dx up to the previous layer.
8
+ // Composing the layers composes their backward passes in reverse, so writing
9
+ // the output cotangent to the `logits` cell makes the engine run reverse-mode
10
+ // backprop down the whole chain and land an update on every weight source.
11
+ // There is no optimizer object and no training loop inside the net: the
12
+ // statement `logits.value = cotangent` *is* one gradient step.
13
+ //
14
+ // The very same lens, run with the weights frozen (`cfg.frozen`), inverts
15
+ // instead of fits — the cotangent still flows to the input, so the input cell
16
+ // receives dL/dx. That is the gradient the "dream" ascends to paint a class
17
+ // prototype: inference-time inversion is just the backward leg with the
18
+ // parameters held fixed.
19
+ //
20
+ // `mlp.ts` stays the flat, fast reference (and the offline ground truth); this
21
+ // is the reactive realization the demos drive.
22
+ import { cell, lens, SKIP } from "../core/index.js";
23
+ import { actGrad, applyAct, gaussian, rng, softmax } from "./mlp.js";
24
+ const toF64 = (x) => x instanceof Float64Array ? x : Float64Array.from(x);
25
+ // Forward of one dense layer (the lens `fwd`).
26
+ function denseForward(p, inDim, outDim, act, x) {
27
+ const y = new Float64Array(outDim);
28
+ for (let o = 0; o < outDim; o++) {
29
+ let z = p.b[o];
30
+ const base = o * inDim;
31
+ for (let i = 0; i < inDim; i++)
32
+ z += p.W[base + i] * x[i];
33
+ y[o] = applyAct(act, z);
34
+ }
35
+ return y;
36
+ }
37
+ // Backward of one dense layer (the lens `bwd` math): given dL/da, return the
38
+ // input cotangent dL/dx and the parameter gradients gW/gb. Shared by the
39
+ // engine-routed training step and the frozen inversion pass so they can't drift.
40
+ function denseBackward(p, inDim, outDim, act, x, dOut) {
41
+ const dIn = new Float64Array(inDim);
42
+ const gW = new Float64Array(outDim * inDim);
43
+ const gb = new Float64Array(outDim);
44
+ for (let o = 0; o < outDim; o++) {
45
+ let z = p.b[o];
46
+ const base = o * inDim;
47
+ for (let i = 0; i < inDim; i++)
48
+ z += p.W[base + i] * x[i];
49
+ const dz = dOut[o] * actGrad(act, applyAct(act, z));
50
+ gb[o] = dz;
51
+ for (let i = 0; i < inDim; i++) {
52
+ gW[base + i] = dz * x[i];
53
+ dIn[i] = dIn[i] + p.W[base + i] * dz;
54
+ }
55
+ }
56
+ return { dIn, gW, gb };
57
+ }
58
+ // Build one dense layer-lens over its weight source and input cell. Forward is
59
+ // the activation; backward steps the weights (unless frozen) and always returns
60
+ // dL/dx for the input parent — for a hidden layer that propagates the gradient
61
+ // up the chain; for the first layer it lands on the input cell (where the
62
+ // inversion pass reads it).
63
+ function denseLens(params, input, inDim, outDim, act, cfg) {
64
+ const parents = [params, input];
65
+ return lens(parents, {
66
+ init: () => null,
67
+ step: (_s, c) => c,
68
+ fwd: (s) => denseForward(s[0], inDim, outDim, act, s[1]),
69
+ bwd: (cot, s, c) => {
70
+ const { dIn, gW, gb } = denseBackward(s[0], inDim, outDim, act, s[1], cot);
71
+ let pUpd;
72
+ if (cfg.frozen) {
73
+ pUpd = SKIP;
74
+ }
75
+ else {
76
+ const W = new Float64Array(s[0].W);
77
+ const b = new Float64Array(s[0].b);
78
+ const lr = cfg.lr;
79
+ for (let k = 0; k < W.length; k++)
80
+ W[k] = W[k] - lr * gW[k];
81
+ for (let o = 0; o < b.length; o++)
82
+ b[o] = b[o] - lr * gb[o];
83
+ pUpd = { W, b };
84
+ }
85
+ return { updates: [pUpd, dIn], complement: c };
86
+ },
87
+ });
88
+ }
89
+ /** Build a net as a lens DAG. `dims` is `[in, h1, …, out]`; hidden layers use
90
+ * `hidden` activation, the output is `linear` (the squash folds into the loss). */
91
+ export function lensNet(dims, opts = {}) {
92
+ const hidden = opts.hidden ?? "tanh";
93
+ const r = rng(opts.seed ?? 1);
94
+ const cfg = { lr: opts.lr ?? 0.05, frozen: false };
95
+ const input = cell(new Float64Array(dims[0]));
96
+ const layers = [];
97
+ let x = input;
98
+ for (let i = 0; i + 1 < dims.length; i++) {
99
+ const inDim = dims[i];
100
+ const outDim = dims[i + 1];
101
+ const act = i + 2 < dims.length ? hidden : "linear";
102
+ const scale = act === "relu" ? Math.sqrt(2 / inDim) : Math.sqrt(1 / inDim);
103
+ const W = new Float64Array(outDim * inDim);
104
+ for (let k = 0; k < W.length; k++)
105
+ W[k] = gaussian(r) * scale;
106
+ const params = cell({ W, b: new Float64Array(outDim) });
107
+ const out = denseLens(params, x, inDim, outDim, act, cfg);
108
+ layers.push({ inDim, outDim, act, params, out });
109
+ x = out;
110
+ }
111
+ return { input, layers, logits: x, cfg, dims };
112
+ }
113
+ /** Logits for `x`, reading the current weight cells (no training, untracked).
114
+ * Applies any pending backward write first, so it always sees fresh weights. */
115
+ export function logitsOf(net, x) {
116
+ let a = toF64(x);
117
+ for (const L of net.layers)
118
+ a = denseForward(L.params.peek(), L.inDim, L.outDim, L.act, a);
119
+ return a;
120
+ }
121
+ /** Class probabilities: sigmoid for a 1-logit (binary) net, else softmax. */
122
+ export function probsOf(net, x) {
123
+ const z = logitsOf(net, x);
124
+ return z.length === 1 ? Float64Array.of(1 / (1 + Math.exp(-z[0]))) : softmax(z);
125
+ }
126
+ /** Argmax class for a multi-logit net, or `P ≥ 0.5` for a binary net. */
127
+ export function classifyOf(net, x) {
128
+ const p = probsOf(net, x);
129
+ if (p.length === 1)
130
+ return p[0] >= 0.5 ? 1 : 0;
131
+ let best = 0;
132
+ for (let i = 1; i < p.length; i++)
133
+ if (p[i] > p[best])
134
+ best = i;
135
+ return best;
136
+ }
137
+ /** Fraction of `data` classified correctly. */
138
+ export function accuracyOf(net, data) {
139
+ let ok = 0;
140
+ for (const s of data)
141
+ if (classifyOf(net, s.x) === s.y)
142
+ ok++;
143
+ return ok / Math.max(1, data.length);
144
+ }
145
+ // Cross-entropy loss + the cotangent dL/dlogits (the seed of the backward
146
+ // pass). Binary: BCE-with-logits, dz = σ(z) − y. Multi: softmax CE, dz = p − e_y.
147
+ function lossCotangent(z, y) {
148
+ if (z.length === 1) {
149
+ const p = 1 / (1 + Math.exp(-z[0]));
150
+ const eps = 1e-12;
151
+ return {
152
+ loss: -(y * Math.log(p + eps) + (1 - y) * Math.log(1 - p + eps)),
153
+ cot: Float64Array.of(p - y),
154
+ };
155
+ }
156
+ const p = softmax(z);
157
+ const cot = new Float64Array(z.length);
158
+ for (let k = 0; k < z.length; k++)
159
+ cot[k] = p[k] - (k === y ? 1 : 0);
160
+ return { loss: -Math.log(p[y] + 1e-12), cot };
161
+ }
162
+ /** Mean cross-entropy over a dataset (no update) — for monitoring/tests. */
163
+ export function meanLossOf(net, data) {
164
+ let total = 0;
165
+ for (const s of data)
166
+ total += lossCotangent(logitsOf(net, s.x), s.y).loss;
167
+ return total / Math.max(1, data.length);
168
+ }
169
+ /** Train one example with a single backward write: pin the input, read the
170
+ * prediction (the forward pull), then write the output cotangent to `logits`.
171
+ * The engine backpropagates and lands an SGD step on every weight cell. The
172
+ * step is forced before returning (while the input still holds this example),
173
+ * so callers may move on immediately. Returns the loss before the step. */
174
+ export function trainExample(net, x, y) {
175
+ net.cfg.frozen = false;
176
+ net.input.value = toF64(x);
177
+ const { loss, cot } = lossCotangent(net.logits.value, y);
178
+ net.logits.value = cot;
179
+ void net.logits.peek(); // force the backward now, while input === this example
180
+ return loss;
181
+ }
182
+ // Fisher–Yates over [0, n) using the provided uniform source.
183
+ function shuffled(n, r) {
184
+ const a = Array.from({ length: n }, (_, i) => i);
185
+ for (let i = n - 1; i > 0; i--) {
186
+ const j = Math.floor(r() * (i + 1));
187
+ const t = a[i];
188
+ a[i] = a[j];
189
+ a[j] = t;
190
+ }
191
+ return a;
192
+ }
193
+ /** Train one shuffled pass — one backward write per example (online SGD).
194
+ * Returns the mean loss over the epoch. */
195
+ export function trainEpoch(net, data, r) {
196
+ if (data.length === 0)
197
+ return 0;
198
+ let total = 0;
199
+ for (const idx of shuffled(data.length, r)) {
200
+ const s = data[idx];
201
+ total += trainExample(net, s.x, s.y);
202
+ }
203
+ return total / data.length;
204
+ }
205
+ /** Input-space gradient toward raising logit `cls`, by one frozen-weight
206
+ * backward write: with the weights held fixed the cotangent flows past them to
207
+ * the input cell, which then holds dL/dInput. Drives the "dream" / saliency. */
208
+ export function inputGradient(net, x, cls = 0) {
209
+ net.cfg.frozen = true;
210
+ net.input.value = toF64(x);
211
+ const z = net.logits.value; // forward
212
+ const seed = new Float64Array(z.length);
213
+ seed[Math.min(cls, z.length - 1)] = 1;
214
+ net.logits.value = seed;
215
+ void net.logits.peek(); // force the backward; the gradient lands on `input`
216
+ const g = net.input.peek();
217
+ net.cfg.frozen = false;
218
+ return g;
219
+ }
@@ -0,0 +1,77 @@
1
+ /** Hidden/output nonlinearity. The output layer is `linear`; its squashing
2
+ * (sigmoid/softmax) is folded into the loss for numerically-stable grads. */
3
+ export type Activation = "tanh" | "relu" | "sigmoid" | "linear";
4
+ /** Deterministic PRNG (mulberry32) so init and data are reproducible. */
5
+ export declare function rng(seed: number): () => number;
6
+ /** Standard normal via Box–Muller, driven by a uniform source. */
7
+ export declare function gaussian(r: () => number): number;
8
+ interface Layer {
9
+ inDim: number;
10
+ outDim: number;
11
+ act: Activation;
12
+ W: Float64Array;
13
+ b: Float64Array;
14
+ gW: Float64Array;
15
+ gb: Float64Array;
16
+ mW: Float64Array;
17
+ vW: Float64Array;
18
+ mb: Float64Array;
19
+ vb: Float64Array;
20
+ inBuf: Float64Array;
21
+ preBuf: Float64Array;
22
+ outBuf: Float64Array;
23
+ }
24
+ /** A multilayer perceptron: a `pipe` of dense layers plus an Adam step clock. */
25
+ export interface MLP {
26
+ layers: Layer[];
27
+ /** Adam timestep (for bias correction). */
28
+ t: number;
29
+ lr: number;
30
+ beta1: number;
31
+ beta2: number;
32
+ l2: number;
33
+ }
34
+ /** Build an MLP. `dims` is `[in, h1, …, out]`; hidden layers use `hidden`
35
+ * activation, the output layer is `linear` (loss folds in the squashing). */
36
+ export declare function mlp(dims: readonly number[], opts?: {
37
+ seed?: number;
38
+ hidden?: Activation;
39
+ lr?: number;
40
+ l2?: number;
41
+ }): MLP;
42
+ /** Pointwise activation `a = σ(z)`. */
43
+ export declare function applyAct(act: Activation, z: number): number;
44
+ /** Activation derivative `σ'`, given the *output* `a` (cheap for tanh/sigmoid). */
45
+ export declare function actGrad(act: Activation, a: number): number;
46
+ /** Forward pass: logits (pre-squash output) for one input vector. */
47
+ export declare function forward(net: MLP, x: Float64Array | number[]): Float64Array;
48
+ /** Softmax of a logit vector (numerically stabilised). */
49
+ export declare function softmax(logits: Float64Array): Float64Array;
50
+ /** Class probabilities: sigmoid for a 1-logit (binary) net, else softmax.
51
+ * A binary net returns `[P(class 1)]`. */
52
+ export declare function predict(net: MLP, x: Float64Array | number[]): Float64Array;
53
+ /** Argmax class for a multi-logit net, or `prob ≥ 0.5` for a binary net. */
54
+ export declare function classify(net: MLP, x: Float64Array | number[]): number;
55
+ /** A labelled example: input vector + integer class. */
56
+ export interface Sample {
57
+ x: Float64Array | number[];
58
+ y: number;
59
+ }
60
+ /** One full-batch gradient step over `batch`. Returns the mean loss before
61
+ * the update. This is the dynamical step on the weights — call it from a
62
+ * clock to train. */
63
+ export declare function trainStep(net: MLP, batch: readonly Sample[]): number;
64
+ /** Mean cross-entropy over a dataset (no update) — for monitoring/tests. */
65
+ export declare function meanLoss(net: MLP, data: readonly Sample[]): number;
66
+ /** Fraction of `data` classified correctly. */
67
+ export declare function accuracy(net: MLP, data: readonly Sample[]): number;
68
+ /** Flattened parameter buffers `[W0, b0, W1, b1, …]` (live views). */
69
+ export declare function parameters(net: MLP): Float64Array[];
70
+ /** Mean-loss gradients over `batch` with no update and no weight decay,
71
+ * aligned with `parameters(net)`. For gradient checking / inspection. */
72
+ export declare function gradients(net: MLP, batch: readonly Sample[]): Float64Array[];
73
+ /** Input-space gradient of a chosen logit, by one backward pass with a unit
74
+ * seed. Drives the "dream" view (ascend pixels toward a class) and saliency.
75
+ * Leaves parameter-gradient accumulators dirty; not for use mid-train-step. */
76
+ export declare function inputGradient(net: MLP, x: Float64Array | number[], cls?: number): Float64Array;
77
+ export {};