bireactive 0.2.4 → 0.3.1
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.
- package/dist/animation/anim.js +4 -0
- package/dist/automerge/doc-cell.d.ts +20 -0
- package/dist/automerge/doc-cell.js +80 -0
- package/dist/automerge/index.d.ts +3 -0
- package/dist/automerge/index.js +12 -0
- package/dist/automerge/reconcile.d.ts +5 -0
- package/dist/automerge/reconcile.js +63 -0
- package/dist/coll.d.ts +7 -7
- package/dist/core/_counts.d.ts +48 -0
- package/dist/core/_counts.js +58 -0
- package/dist/core/cell.d.ts +182 -123
- package/dist/core/cell.js +1140 -721
- package/dist/core/debug.d.ts +25 -0
- package/dist/core/debug.js +121 -0
- package/dist/core/index.d.ts +9 -14
- package/dist/core/index.js +9 -14
- package/dist/core/lenses/aggregates.d.ts +1 -1
- package/dist/core/lenses/aggregates.js +4 -3
- package/dist/core/lenses/closed-form-policies.js +14 -9
- package/dist/core/lenses/decompositions.js +3 -3
- package/dist/core/lenses/domain-aggregates.js +5 -5
- package/dist/core/lenses/geometry.d.ts +1 -1
- package/dist/core/lenses/geometry.js +6 -7
- package/dist/core/lenses/index.d.ts +1 -0
- package/dist/core/lenses/index.js +1 -0
- package/dist/core/lenses/memory.d.ts +2 -2
- package/dist/core/lenses/memory.js +3 -3
- package/dist/core/lenses/snap.d.ts +18 -0
- package/dist/core/lenses/snap.js +145 -0
- package/dist/core/lenses/typed-factor.js +4 -3
- package/dist/core/optic.d.ts +13 -0
- package/dist/core/optic.js +44 -0
- package/dist/core/optics.d.ts +10 -0
- package/dist/core/optics.js +30 -0
- package/dist/core/store.d.ts +10 -0
- package/dist/core/store.js +85 -0
- package/dist/core/traits.d.ts +1 -0
- package/dist/core/values/audio.js +4 -5
- package/dist/core/values/box.js +7 -7
- package/dist/core/values/canvas.js +15 -18
- package/dist/core/values/color.js +5 -5
- package/dist/core/values/field.d.ts +70 -0
- package/dist/core/values/field.js +230 -0
- package/dist/core/values/gpu.d.ts +4 -2
- package/dist/core/values/gpu.js +11 -4
- package/dist/core/values/matrix.js +7 -7
- package/dist/core/values/num.d.ts +1 -1
- package/dist/core/values/num.js +1 -1
- package/dist/core/values/pose.js +4 -4
- package/dist/core/values/range.js +6 -6
- package/dist/core/values/str.js +8 -8
- package/dist/core/values/template.d.ts +1 -1
- package/dist/core/values/template.js +2 -1
- package/dist/core/values/transform.js +7 -7
- package/dist/core/values/tri.js +3 -3
- package/dist/core/values/vec.js +8 -12
- package/dist/ext/timeline.js +2 -2
- package/dist/formats/cst.d.ts +127 -0
- package/dist/formats/cst.js +280 -0
- package/dist/formats/edn.d.ts +2 -0
- package/dist/formats/edn.js +301 -0
- package/dist/formats/index.d.ts +6 -0
- package/dist/formats/index.js +8 -0
- package/dist/formats/json.d.ts +2 -0
- package/dist/formats/json.js +332 -0
- package/dist/formats/lens.d.ts +8 -0
- package/dist/formats/lens.js +51 -0
- package/dist/formats/toml.d.ts +2 -0
- package/dist/formats/toml.js +526 -0
- package/dist/formats/yaml.d.ts +2 -0
- package/dist/formats/yaml.js +661 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +5 -0
- package/dist/jsx-runtime.d.ts +54 -0
- package/dist/jsx-runtime.js +219 -0
- package/dist/learn/data.d.ts +49 -0
- package/dist/learn/data.js +181 -0
- package/dist/learn/index.d.ts +3 -0
- package/dist/learn/index.js +6 -0
- package/dist/learn/lens-net.d.ts +63 -0
- package/dist/learn/lens-net.js +219 -0
- package/dist/learn/mlp.d.ts +77 -0
- package/dist/learn/mlp.js +292 -0
- package/dist/propagators/csp.d.ts +13 -0
- package/dist/propagators/csp.js +52 -0
- package/dist/propagators/flex.d.ts +31 -0
- package/dist/propagators/flex.js +189 -0
- package/dist/propagators/graph.d.ts +73 -0
- package/dist/propagators/graph.js +543 -0
- package/dist/propagators/index.d.ts +8 -6
- package/dist/propagators/index.js +15 -6
- package/dist/propagators/lattice.d.ts +45 -0
- package/dist/propagators/lattice.js +113 -0
- package/dist/propagators/layout.d.ts +1 -27
- package/dist/propagators/layout.js +6 -175
- package/dist/propagators/numeric.d.ts +17 -0
- package/dist/propagators/numeric.js +93 -0
- package/dist/propagators/solver.d.ts +51 -0
- package/dist/propagators/solver.js +175 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/lens.d.ts +121 -0
- package/dist/schema/lens.js +429 -0
- package/dist/shapes/annular-sector.js +4 -4
- package/dist/shapes/button.js +1 -1
- package/dist/shapes/circle.js +1 -1
- package/dist/shapes/drag-behaviors.d.ts +56 -0
- package/dist/shapes/drag-behaviors.js +102 -0
- package/dist/shapes/drag-spec.d.ts +52 -0
- package/dist/shapes/drag-spec.js +112 -0
- package/dist/shapes/handle.js +2 -2
- package/dist/shapes/index.d.ts +3 -1
- package/dist/shapes/index.js +3 -1
- package/dist/shapes/interaction.d.ts +2 -3
- package/dist/shapes/interaction.js +77 -56
- package/dist/shapes/label.js +7 -1
- package/dist/shapes/layout.d.ts +47 -1
- package/dist/shapes/layout.js +60 -2
- package/dist/shapes/rect.js +7 -7
- package/dist/shapes/shape.js +8 -8
- package/dist/web/diagram.js +2 -2
- package/package.json +24 -2
- package/dist/propagators/network.d.ts +0 -52
- package/dist/propagators/network.js +0 -185
- package/dist/propagators/propagator.d.ts +0 -12
- package/dist/propagators/propagator.js +0 -16
- package/dist/propagators/range.d.ts +0 -45
- package/dist/propagators/range.js +0 -147
- package/dist/propagators/relations.d.ts +0 -60
- package/dist/propagators/relations.js +0 -343
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Development entry for the automatic JSX runtime. esbuild/tsc import this when
|
|
2
|
+
// `jsxDev` is enabled; the dev signature carries extra debug args we ignore, so
|
|
3
|
+
// `jsxDEV` just forwards to `jsx`.
|
|
4
|
+
export * from "./jsx-runtime.js";
|
|
5
|
+
export { jsx as jsxDEV } from "./jsx-runtime.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Cell } from "./core/cell.js";
|
|
2
|
+
/** Marker for `<>…</>`; lowered to `jsx(Fragment, …)`. */
|
|
3
|
+
export declare const Fragment: unique symbol;
|
|
4
|
+
type Disposer = () => void;
|
|
5
|
+
/** Register a teardown with the active scope (`mount` / `scope` / an `each`
|
|
6
|
+
* item) — for raw `effect`s or listeners created in a component body, which
|
|
7
|
+
* the JSX helpers otherwise track for you. No-op outside a scope. */
|
|
8
|
+
export declare function onCleanup(fn: Disposer): void;
|
|
9
|
+
type Props = Record<string, unknown> & {
|
|
10
|
+
children?: unknown;
|
|
11
|
+
};
|
|
12
|
+
type Component = (props: Props) => Node;
|
|
13
|
+
/** Build a DOM node for one JSX element. */
|
|
14
|
+
export declare function jsx(type: string | symbol | Component, props?: Props): Node;
|
|
15
|
+
/** Static-children variant; behaviourally identical in a runtime builder. */
|
|
16
|
+
export declare const jsxs: typeof jsx;
|
|
17
|
+
/** Render `component` into `host`, collecting reactive teardowns. The returned
|
|
18
|
+
* disposer releases them — call it on unmount (e.g. `disconnectedCallback`). */
|
|
19
|
+
export declare function mount(component: () => Node, host: Node): Disposer;
|
|
20
|
+
/** Run `fn` under a fresh reactive scope, returning its result and a disposer
|
|
21
|
+
* for every effect created during it — `mount` without a host. `each` gives
|
|
22
|
+
* each keyed item its own scope so its effects die when the item leaves. */
|
|
23
|
+
export declare function scope<T>(fn: () => T): [T, Disposer];
|
|
24
|
+
/** Keyed list rendering: keep `parent`'s children in sync with a reactive array,
|
|
25
|
+
* reusing and reordering nodes by key, disposing those that leave. Each item is
|
|
26
|
+
* rendered in its own `scope` (untracked from the list effect, so item-internal
|
|
27
|
+
* reads don't retrigger the whole list). Attach via `ref`:
|
|
28
|
+
* `<div ref={el => each(el, items, s => s.id, render)} />`. */
|
|
29
|
+
export declare function each<T>(parent: Element, items: Cell<T[]> | (() => readonly T[]), key: (item: T, index: number) => string, render: (item: T, index: number) => Node): void;
|
|
30
|
+
type Reactive<T> = T | Cell<T> | (() => T);
|
|
31
|
+
export declare namespace JSX {
|
|
32
|
+
export type Element = Node;
|
|
33
|
+
export interface ElementChildrenAttribute {
|
|
34
|
+
children: unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface IntrinsicAttributes {
|
|
37
|
+
children?: unknown;
|
|
38
|
+
}
|
|
39
|
+
export interface CommonProps {
|
|
40
|
+
class?: Reactive<string>;
|
|
41
|
+
style?: Reactive<string | Partial<CSSStyleDeclaration>>;
|
|
42
|
+
id?: Reactive<string>;
|
|
43
|
+
lens?: Cell<any>;
|
|
44
|
+
ref?: (el: Element) => void;
|
|
45
|
+
children?: unknown;
|
|
46
|
+
[attr: string]: any;
|
|
47
|
+
}
|
|
48
|
+
type Tag = keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap;
|
|
49
|
+
export type IntrinsicElements = {
|
|
50
|
+
[K in Tag]: CommonProps;
|
|
51
|
+
};
|
|
52
|
+
export {};
|
|
53
|
+
}
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// jsx-runtime.ts — minimal runtime JSX for bireactive (no compiler step).
|
|
2
|
+
//
|
|
3
|
+
// esbuild's automatic runtime (`jsxImportSource: "@bireactive"`) lowers JSX to
|
|
4
|
+
// `jsx`/`jsxs`/`Fragment` calls; this module builds real DOM nodes and wires
|
|
5
|
+
// reactive props and children through `effect`. The one binding with no React
|
|
6
|
+
// analogue is the `lens` prop: a single bidirectional terminal — the dual of
|
|
7
|
+
// the value-plus-onInput pair — that reads a writable cell forward into the
|
|
8
|
+
// control and writes edits back, so a chain of lenses can be driven from the
|
|
9
|
+
// leaf. Components are plain `props → Node`; reactive expressions are passed as
|
|
10
|
+
// thunks (`{() => expr}`) or cells, since there is no compile step to wrap them.
|
|
11
|
+
import { Cell, effect, untracked } from "./core/cell.js";
|
|
12
|
+
/** Marker for `<>…</>`; lowered to `jsx(Fragment, …)`. */
|
|
13
|
+
export const Fragment = Symbol.for("bireactive.jsx.Fragment");
|
|
14
|
+
// The active mount scope: reactive teardowns created while building a tree are
|
|
15
|
+
// collected here so `mount` can release them all on unmount. Non-reentrant —
|
|
16
|
+
// `mount` saves/restores the previous owner around the render.
|
|
17
|
+
let currentOwner = null;
|
|
18
|
+
function track(d) {
|
|
19
|
+
currentOwner?.push(d);
|
|
20
|
+
}
|
|
21
|
+
/** Register a teardown with the active scope (`mount` / `scope` / an `each`
|
|
22
|
+
* item) — for raw `effect`s or listeners created in a component body, which
|
|
23
|
+
* the JSX helpers otherwise track for you. No-op outside a scope. */
|
|
24
|
+
export function onCleanup(fn) {
|
|
25
|
+
track(fn);
|
|
26
|
+
}
|
|
27
|
+
/** Build a DOM node for one JSX element. */
|
|
28
|
+
export function jsx(type, props) {
|
|
29
|
+
if (typeof type === "function")
|
|
30
|
+
return type(props ?? {});
|
|
31
|
+
if (type === Fragment) {
|
|
32
|
+
const frag = document.createDocumentFragment();
|
|
33
|
+
append(frag, props?.children);
|
|
34
|
+
return frag;
|
|
35
|
+
}
|
|
36
|
+
const el = document.createElement(type);
|
|
37
|
+
if (props)
|
|
38
|
+
for (const key in props)
|
|
39
|
+
applyProp(el, key, props[key]);
|
|
40
|
+
return el;
|
|
41
|
+
}
|
|
42
|
+
/** Static-children variant; behaviourally identical in a runtime builder. */
|
|
43
|
+
export const jsxs = jsx;
|
|
44
|
+
function applyProp(el, key, value) {
|
|
45
|
+
if (key === "children")
|
|
46
|
+
return append(el, value);
|
|
47
|
+
if (key === "ref") {
|
|
48
|
+
if (typeof value === "function")
|
|
49
|
+
value(el);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (key === "lens")
|
|
53
|
+
return bindLens(el, value);
|
|
54
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
55
|
+
el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (value instanceof Cell) {
|
|
59
|
+
track(effect(() => setProp(el, key, value.value)));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "function") {
|
|
63
|
+
track(effect(() => setProp(el, key, value())));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setProp(el, key, value);
|
|
67
|
+
}
|
|
68
|
+
function setProp(el, key, value) {
|
|
69
|
+
if (key === "class" || key === "className") {
|
|
70
|
+
el.setAttribute("class", value == null ? "" : String(value));
|
|
71
|
+
}
|
|
72
|
+
else if (key === "style") {
|
|
73
|
+
if (value && typeof value === "object")
|
|
74
|
+
Object.assign(el.style, value);
|
|
75
|
+
else
|
|
76
|
+
el.setAttribute("style", value == null ? "" : String(value));
|
|
77
|
+
}
|
|
78
|
+
else if (key === "value") {
|
|
79
|
+
el.value = value == null ? "" : String(value);
|
|
80
|
+
}
|
|
81
|
+
else if (key === "checked" || key === "disabled" || key === "selected") {
|
|
82
|
+
// biome-ignore lint/suspicious/noExplicitAny: boolean DOM properties
|
|
83
|
+
el[key] = !!value;
|
|
84
|
+
}
|
|
85
|
+
else if (value == null || value === false) {
|
|
86
|
+
el.removeAttribute(key);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
el.setAttribute(key, value === true ? "" : String(value));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Append a child (array / Node / text / cell / thunk) to `parent`. */
|
|
93
|
+
function append(parent, child) {
|
|
94
|
+
if (Array.isArray(child)) {
|
|
95
|
+
for (const c of child)
|
|
96
|
+
append(parent, c);
|
|
97
|
+
}
|
|
98
|
+
else if (child instanceof Node) {
|
|
99
|
+
parent.appendChild(child);
|
|
100
|
+
}
|
|
101
|
+
else if (child instanceof Cell) {
|
|
102
|
+
parent.appendChild(reactiveText(() => child.value));
|
|
103
|
+
}
|
|
104
|
+
else if (typeof child === "function") {
|
|
105
|
+
parent.appendChild(reactiveText(child));
|
|
106
|
+
}
|
|
107
|
+
else if (child != null && child !== false && child !== true) {
|
|
108
|
+
parent.appendChild(document.createTextNode(String(child)));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** A text node whose content tracks `get()`. Primitive children only — the
|
|
112
|
+
* minimal runtime does not reconcile dynamic element children. */
|
|
113
|
+
function reactiveText(get) {
|
|
114
|
+
const node = document.createTextNode("");
|
|
115
|
+
track(effect(() => {
|
|
116
|
+
const v = get();
|
|
117
|
+
node.data = v == null ? "" : String(v);
|
|
118
|
+
}));
|
|
119
|
+
return node;
|
|
120
|
+
}
|
|
121
|
+
/** Two-way bind a form control to a writable cell: read forward into the
|
|
122
|
+
* control, write back on input. The forward write is skipped while the
|
|
123
|
+
* control is focused, so a live edit is never clobbered mid-drag (the
|
|
124
|
+
* controlled-input focus guard, written once). */
|
|
125
|
+
function bindLens(el, lens) {
|
|
126
|
+
const checkbox = el.type === "checkbox";
|
|
127
|
+
track(effect(() => {
|
|
128
|
+
const v = lens.value;
|
|
129
|
+
if (checkbox) {
|
|
130
|
+
el.checked = !!v;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const next = v == null ? "" : String(v);
|
|
134
|
+
const root = el.getRootNode();
|
|
135
|
+
if (root.activeElement !== el && el.value !== next)
|
|
136
|
+
el.value = next;
|
|
137
|
+
}));
|
|
138
|
+
const evt = checkbox || el.tagName === "SELECT" ? "change" : "input";
|
|
139
|
+
el.addEventListener(evt, () => {
|
|
140
|
+
lens.value = checkbox
|
|
141
|
+
? el.checked
|
|
142
|
+
: el.type === "range" || el.type === "number"
|
|
143
|
+
? Number(el.value)
|
|
144
|
+
: el.value;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Render `component` into `host`, collecting reactive teardowns. The returned
|
|
148
|
+
* disposer releases them — call it on unmount (e.g. `disconnectedCallback`). */
|
|
149
|
+
export function mount(component, host) {
|
|
150
|
+
const [node, dispose] = scope(component);
|
|
151
|
+
host.appendChild(node);
|
|
152
|
+
return dispose;
|
|
153
|
+
}
|
|
154
|
+
/** Run `fn` under a fresh reactive scope, returning its result and a disposer
|
|
155
|
+
* for every effect created during it — `mount` without a host. `each` gives
|
|
156
|
+
* each keyed item its own scope so its effects die when the item leaves. */
|
|
157
|
+
export function scope(fn) {
|
|
158
|
+
const prev = currentOwner;
|
|
159
|
+
const owner = [];
|
|
160
|
+
currentOwner = owner;
|
|
161
|
+
try {
|
|
162
|
+
return [
|
|
163
|
+
fn(),
|
|
164
|
+
() => {
|
|
165
|
+
for (const d of owner)
|
|
166
|
+
d();
|
|
167
|
+
owner.length = 0;
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
currentOwner = prev;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Keyed list rendering: keep `parent`'s children in sync with a reactive array,
|
|
176
|
+
* reusing and reordering nodes by key, disposing those that leave. Each item is
|
|
177
|
+
* rendered in its own `scope` (untracked from the list effect, so item-internal
|
|
178
|
+
* reads don't retrigger the whole list). Attach via `ref`:
|
|
179
|
+
* `<div ref={el => each(el, items, s => s.id, render)} />`. */
|
|
180
|
+
export function each(parent, items, key, render) {
|
|
181
|
+
const read = typeof items === "function" ? items : () => items.value;
|
|
182
|
+
const cache = new Map();
|
|
183
|
+
const stop = effect(() => {
|
|
184
|
+
const arr = read();
|
|
185
|
+
const seen = new Set();
|
|
186
|
+
const nodes = [];
|
|
187
|
+
arr.forEach((item, i) => {
|
|
188
|
+
const k = key(item, i);
|
|
189
|
+
seen.add(k);
|
|
190
|
+
let entry = cache.get(k);
|
|
191
|
+
if (entry === undefined) {
|
|
192
|
+
const [node, dispose] = untracked(() => scope(() => render(item, i)));
|
|
193
|
+
entry = { node, dispose };
|
|
194
|
+
cache.set(k, entry);
|
|
195
|
+
}
|
|
196
|
+
nodes.push(entry.node);
|
|
197
|
+
});
|
|
198
|
+
for (const [k, entry] of cache) {
|
|
199
|
+
if (!seen.has(k)) {
|
|
200
|
+
entry.dispose();
|
|
201
|
+
cache.delete(k);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Only touch the DOM when the ordered node set actually changed — re-inserting
|
|
205
|
+
// identical children mid-interaction would steal focus and reset clicks.
|
|
206
|
+
const cur = parent.childNodes;
|
|
207
|
+
let same = cur.length === nodes.length;
|
|
208
|
+
for (let i = 0; same && i < nodes.length; i++)
|
|
209
|
+
same = cur[i] === nodes[i];
|
|
210
|
+
if (!same)
|
|
211
|
+
parent.replaceChildren(...nodes);
|
|
212
|
+
});
|
|
213
|
+
track(() => {
|
|
214
|
+
stop();
|
|
215
|
+
for (const entry of cache.values())
|
|
216
|
+
entry.dispose();
|
|
217
|
+
cache.clear();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
@@ -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;
|