bireactive 0.2.3 → 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.
- package/dist/animation/anim.js +4 -0
- package/dist/coll.d.ts +7 -7
- package/dist/coll.js +3 -1
- package/dist/core/cell.d.ts +89 -66
- package/dist/core/cell.js +642 -401
- package/dist/core/index.d.ts +4 -14
- package/dist/core/index.js +4 -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 +6 -6
- 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/memory.d.ts +2 -2
- package/dist/core/lenses/memory.js +3 -3
- package/dist/core/lenses/typed-factor.js +4 -3
- package/dist/core/traits.d.ts +1 -0
- package/dist/core/values/box.js +7 -7
- 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/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 +54 -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/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/handle.js +2 -2
- package/dist/shapes/label.js +1 -1
- package/dist/shapes/layout.js +2 -2
- package/dist/shapes/rect.js +7 -7
- package/dist/shapes/shape.js +8 -8
- package/dist/tex/tex.js +9 -2
- package/dist/web/diagram.js +2 -2
- package/package.json +9 -19
- 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,292 @@
|
|
|
1
|
+
// mlp.ts — a tiny dense neural net, written as a stack of parametric lenses.
|
|
2
|
+
//
|
|
3
|
+
// Backprop is the lens pattern: each layer is a forward map (compute the
|
|
4
|
+
// activation) paired with a backward map (pull a gradient back to the input
|
|
5
|
+
// and deposit gradients on the parameters). Composing layers composes their
|
|
6
|
+
// backward passes in reverse — reverse-mode autodiff *is* lens composition,
|
|
7
|
+
// exactly the `pipe` of the schema kit but over differentiable maps. The
|
|
8
|
+
// "complement" a layer needs for its backward pass is the cached forward
|
|
9
|
+
// activation, stashed on the layer during `forward`.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately coarse-grained: one layer = one matmul over Float64Arrays, not
|
|
12
|
+
// a cell per scalar. The reactive/bidirectional payoff lives in the demos
|
|
13
|
+
// (live data, watch-it-learn); this core stays plain and fast so it is cheap
|
|
14
|
+
// to run and easy to test offline.
|
|
15
|
+
/** Deterministic PRNG (mulberry32) so init and data are reproducible. */
|
|
16
|
+
export function rng(seed) {
|
|
17
|
+
let a = seed >>> 0;
|
|
18
|
+
return () => {
|
|
19
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
20
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
21
|
+
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
|
|
22
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Standard normal via Box–Muller, driven by a uniform source. */
|
|
26
|
+
export function gaussian(r) {
|
|
27
|
+
let u = 0;
|
|
28
|
+
let v = 0;
|
|
29
|
+
while (u === 0)
|
|
30
|
+
u = r();
|
|
31
|
+
while (v === 0)
|
|
32
|
+
v = r();
|
|
33
|
+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
|
|
34
|
+
}
|
|
35
|
+
/** Build an MLP. `dims` is `[in, h1, …, out]`; hidden layers use `hidden`
|
|
36
|
+
* activation, the output layer is `linear` (loss folds in the squashing). */
|
|
37
|
+
export function mlp(dims, opts = {}) {
|
|
38
|
+
const hidden = opts.hidden ?? "tanh";
|
|
39
|
+
const r = rng(opts.seed ?? 1);
|
|
40
|
+
const layers = [];
|
|
41
|
+
for (let i = 0; i + 1 < dims.length; i++) {
|
|
42
|
+
const inDim = dims[i];
|
|
43
|
+
const outDim = dims[i + 1];
|
|
44
|
+
const act = i + 2 < dims.length ? hidden : "linear";
|
|
45
|
+
// He for relu, Xavier otherwise — keeps early activations well-scaled.
|
|
46
|
+
const scale = act === "relu" ? Math.sqrt(2 / inDim) : Math.sqrt(1 / inDim);
|
|
47
|
+
const W = new Float64Array(outDim * inDim);
|
|
48
|
+
for (let k = 0; k < W.length; k++)
|
|
49
|
+
W[k] = gaussian(r) * scale;
|
|
50
|
+
layers.push({
|
|
51
|
+
inDim,
|
|
52
|
+
outDim,
|
|
53
|
+
act,
|
|
54
|
+
W,
|
|
55
|
+
b: new Float64Array(outDim),
|
|
56
|
+
gW: new Float64Array(outDim * inDim),
|
|
57
|
+
gb: new Float64Array(outDim),
|
|
58
|
+
mW: new Float64Array(outDim * inDim),
|
|
59
|
+
vW: new Float64Array(outDim * inDim),
|
|
60
|
+
mb: new Float64Array(outDim),
|
|
61
|
+
vb: new Float64Array(outDim),
|
|
62
|
+
inBuf: new Float64Array(inDim),
|
|
63
|
+
preBuf: new Float64Array(outDim),
|
|
64
|
+
outBuf: new Float64Array(outDim),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
layers,
|
|
69
|
+
t: 0,
|
|
70
|
+
lr: opts.lr ?? 0.02,
|
|
71
|
+
beta1: 0.9,
|
|
72
|
+
beta2: 0.999,
|
|
73
|
+
l2: opts.l2 ?? 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Pointwise activation `a = σ(z)`. */
|
|
77
|
+
export function applyAct(act, z) {
|
|
78
|
+
switch (act) {
|
|
79
|
+
case "tanh":
|
|
80
|
+
return Math.tanh(z);
|
|
81
|
+
case "relu":
|
|
82
|
+
return z > 0 ? z : 0;
|
|
83
|
+
case "sigmoid":
|
|
84
|
+
return 1 / (1 + Math.exp(-z));
|
|
85
|
+
default:
|
|
86
|
+
return z;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Activation derivative `σ'`, given the *output* `a` (cheap for tanh/sigmoid). */
|
|
90
|
+
export function actGrad(act, a) {
|
|
91
|
+
switch (act) {
|
|
92
|
+
case "tanh":
|
|
93
|
+
return 1 - a * a;
|
|
94
|
+
case "relu":
|
|
95
|
+
return a > 0 ? 1 : 0;
|
|
96
|
+
case "sigmoid":
|
|
97
|
+
return a * (1 - a);
|
|
98
|
+
default:
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Forward through one layer (the lens `fwd`): caches input + output as the
|
|
103
|
+
// complement, returns the activation buffer (reused — copy if you must keep).
|
|
104
|
+
function forwardLayer(L, x) {
|
|
105
|
+
L.inBuf.set(x);
|
|
106
|
+
for (let o = 0; o < L.outDim; o++) {
|
|
107
|
+
let z = L.b[o];
|
|
108
|
+
const base = o * L.inDim;
|
|
109
|
+
for (let i = 0; i < L.inDim; i++)
|
|
110
|
+
z += L.W[base + i] * x[i];
|
|
111
|
+
L.preBuf[o] = z;
|
|
112
|
+
L.outBuf[o] = applyAct(L.act, z);
|
|
113
|
+
}
|
|
114
|
+
return L.outBuf;
|
|
115
|
+
}
|
|
116
|
+
// Backward through one layer (the lens `bwd`): given dL/da, accumulate the
|
|
117
|
+
// parameter gradients and return dL/dx for the previous layer.
|
|
118
|
+
function backwardLayer(L, dOut) {
|
|
119
|
+
const dIn = new Float64Array(L.inDim);
|
|
120
|
+
for (let o = 0; o < L.outDim; o++) {
|
|
121
|
+
const dz = dOut[o] * actGrad(L.act, L.outBuf[o]);
|
|
122
|
+
L.gb[o] = L.gb[o] + dz;
|
|
123
|
+
const base = o * L.inDim;
|
|
124
|
+
for (let i = 0; i < L.inDim; i++) {
|
|
125
|
+
L.gW[base + i] = L.gW[base + i] + dz * L.inBuf[i];
|
|
126
|
+
dIn[i] = dIn[i] + L.W[base + i] * dz;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return dIn;
|
|
130
|
+
}
|
|
131
|
+
/** Forward pass: logits (pre-squash output) for one input vector. */
|
|
132
|
+
export function forward(net, x) {
|
|
133
|
+
let a = x instanceof Float64Array ? x : Float64Array.from(x);
|
|
134
|
+
for (const L of net.layers)
|
|
135
|
+
a = forwardLayer(L, a);
|
|
136
|
+
return a;
|
|
137
|
+
}
|
|
138
|
+
/** Softmax of a logit vector (numerically stabilised). */
|
|
139
|
+
export function softmax(logits) {
|
|
140
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
141
|
+
for (const z of logits)
|
|
142
|
+
if (z > max)
|
|
143
|
+
max = z;
|
|
144
|
+
const out = new Float64Array(logits.length);
|
|
145
|
+
let sum = 0;
|
|
146
|
+
for (let i = 0; i < logits.length; i++) {
|
|
147
|
+
const e = Math.exp(logits[i] - max);
|
|
148
|
+
out[i] = e;
|
|
149
|
+
sum += e;
|
|
150
|
+
}
|
|
151
|
+
for (let i = 0; i < out.length; i++)
|
|
152
|
+
out[i] = out[i] / sum;
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/** Class probabilities: sigmoid for a 1-logit (binary) net, else softmax.
|
|
156
|
+
* A binary net returns `[P(class 1)]`. */
|
|
157
|
+
export function predict(net, x) {
|
|
158
|
+
const logits = forward(net, x);
|
|
159
|
+
if (logits.length === 1)
|
|
160
|
+
return Float64Array.of(1 / (1 + Math.exp(-logits[0])));
|
|
161
|
+
return softmax(logits);
|
|
162
|
+
}
|
|
163
|
+
/** Argmax class for a multi-logit net, or `prob ≥ 0.5` for a binary net. */
|
|
164
|
+
export function classify(net, x) {
|
|
165
|
+
const p = predict(net, x);
|
|
166
|
+
if (p.length === 1)
|
|
167
|
+
return p[0] >= 0.5 ? 1 : 0;
|
|
168
|
+
let best = 0;
|
|
169
|
+
for (let i = 1; i < p.length; i++)
|
|
170
|
+
if (p[i] > p[best])
|
|
171
|
+
best = i;
|
|
172
|
+
return best;
|
|
173
|
+
}
|
|
174
|
+
// Cross-entropy loss + the gradient on the logits, written into `dLogit`.
|
|
175
|
+
// Binary (1 logit): BCE-with-logits, dz = sigmoid(z) − y.
|
|
176
|
+
// Multi (K logits): softmax CE, dz = softmax(z) − onehot(y).
|
|
177
|
+
function lossAndGrad(logits, y, dLogit) {
|
|
178
|
+
if (logits.length === 1) {
|
|
179
|
+
const z = logits[0];
|
|
180
|
+
const p = 1 / (1 + Math.exp(-z));
|
|
181
|
+
dLogit[0] = p - y;
|
|
182
|
+
const eps = 1e-12;
|
|
183
|
+
return -(y * Math.log(p + eps) + (1 - y) * Math.log(1 - p + eps));
|
|
184
|
+
}
|
|
185
|
+
const p = softmax(logits);
|
|
186
|
+
for (let k = 0; k < logits.length; k++)
|
|
187
|
+
dLogit[k] = p[k] - (k === y ? 1 : 0);
|
|
188
|
+
return -Math.log(p[y] + 1e-12);
|
|
189
|
+
}
|
|
190
|
+
function adamStep(net, param, g, m, v) {
|
|
191
|
+
const { lr, beta1, beta2, t } = net;
|
|
192
|
+
const bc1 = 1 - beta1 ** t;
|
|
193
|
+
const bc2 = 1 - beta2 ** t;
|
|
194
|
+
for (let i = 0; i < param.length; i++) {
|
|
195
|
+
const gi = g[i];
|
|
196
|
+
const mi = (m[i] = beta1 * m[i] + (1 - beta1) * gi);
|
|
197
|
+
const vi = (v[i] = beta2 * v[i] + (1 - beta2) * gi * gi);
|
|
198
|
+
param[i] = param[i] - (lr * (mi / bc1)) / (Math.sqrt(vi / bc2) + 1e-8);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** One full-batch gradient step over `batch`. Returns the mean loss before
|
|
202
|
+
* the update. This is the dynamical step on the weights — call it from a
|
|
203
|
+
* clock to train. */
|
|
204
|
+
export function trainStep(net, batch) {
|
|
205
|
+
for (const L of net.layers) {
|
|
206
|
+
L.gW.fill(0);
|
|
207
|
+
L.gb.fill(0);
|
|
208
|
+
}
|
|
209
|
+
const outDim = net.layers[net.layers.length - 1].outDim;
|
|
210
|
+
const dLogit = new Float64Array(outDim);
|
|
211
|
+
let total = 0;
|
|
212
|
+
for (const s of batch) {
|
|
213
|
+
const logits = forward(net, s.x);
|
|
214
|
+
total += lossAndGrad(logits, s.y, dLogit);
|
|
215
|
+
let g = dLogit;
|
|
216
|
+
for (let li = net.layers.length - 1; li >= 0; li--)
|
|
217
|
+
g = backwardLayer(net.layers[li], g);
|
|
218
|
+
}
|
|
219
|
+
const inv = 1 / Math.max(1, batch.length);
|
|
220
|
+
net.t += 1;
|
|
221
|
+
for (const L of net.layers) {
|
|
222
|
+
for (let i = 0; i < L.gW.length; i++)
|
|
223
|
+
L.gW[i] = L.gW[i] * inv + net.l2 * L.W[i];
|
|
224
|
+
for (let i = 0; i < L.gb.length; i++)
|
|
225
|
+
L.gb[i] = L.gb[i] * inv;
|
|
226
|
+
adamStep(net, L.W, L.gW, L.mW, L.vW);
|
|
227
|
+
adamStep(net, L.b, L.gb, L.mb, L.vb);
|
|
228
|
+
}
|
|
229
|
+
return total * inv;
|
|
230
|
+
}
|
|
231
|
+
/** Mean cross-entropy over a dataset (no update) — for monitoring/tests. */
|
|
232
|
+
export function meanLoss(net, data) {
|
|
233
|
+
const outDim = net.layers[net.layers.length - 1].outDim;
|
|
234
|
+
const dLogit = new Float64Array(outDim);
|
|
235
|
+
let total = 0;
|
|
236
|
+
for (const s of data)
|
|
237
|
+
total += lossAndGrad(forward(net, s.x), s.y, dLogit);
|
|
238
|
+
return total / Math.max(1, data.length);
|
|
239
|
+
}
|
|
240
|
+
/** Fraction of `data` classified correctly. */
|
|
241
|
+
export function accuracy(net, data) {
|
|
242
|
+
let ok = 0;
|
|
243
|
+
for (const s of data)
|
|
244
|
+
if (classify(net, s.x) === s.y)
|
|
245
|
+
ok++;
|
|
246
|
+
return ok / Math.max(1, data.length);
|
|
247
|
+
}
|
|
248
|
+
/** Flattened parameter buffers `[W0, b0, W1, b1, …]` (live views). */
|
|
249
|
+
export function parameters(net) {
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const L of net.layers) {
|
|
252
|
+
out.push(L.W);
|
|
253
|
+
out.push(L.b);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
/** Mean-loss gradients over `batch` with no update and no weight decay,
|
|
258
|
+
* aligned with `parameters(net)`. For gradient checking / inspection. */
|
|
259
|
+
export function gradients(net, batch) {
|
|
260
|
+
for (const L of net.layers) {
|
|
261
|
+
L.gW.fill(0);
|
|
262
|
+
L.gb.fill(0);
|
|
263
|
+
}
|
|
264
|
+
const outDim = net.layers[net.layers.length - 1].outDim;
|
|
265
|
+
const dLogit = new Float64Array(outDim);
|
|
266
|
+
for (const s of batch) {
|
|
267
|
+
lossAndGrad(forward(net, s.x), s.y, dLogit);
|
|
268
|
+
let g = dLogit;
|
|
269
|
+
for (let li = net.layers.length - 1; li >= 0; li--)
|
|
270
|
+
g = backwardLayer(net.layers[li], g);
|
|
271
|
+
}
|
|
272
|
+
const inv = 1 / Math.max(1, batch.length);
|
|
273
|
+
const out = [];
|
|
274
|
+
for (const L of net.layers) {
|
|
275
|
+
out.push(L.gW.map(v => v * inv));
|
|
276
|
+
out.push(L.gb.map(v => v * inv));
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
/** Input-space gradient of a chosen logit, by one backward pass with a unit
|
|
281
|
+
* seed. Drives the "dream" view (ascend pixels toward a class) and saliency.
|
|
282
|
+
* Leaves parameter-gradient accumulators dirty; not for use mid-train-step. */
|
|
283
|
+
export function inputGradient(net, x, cls = 0) {
|
|
284
|
+
forward(net, x);
|
|
285
|
+
const outDim = net.layers[net.layers.length - 1].outDim;
|
|
286
|
+
const seed = new Float64Array(outDim);
|
|
287
|
+
seed[Math.min(cls, outDim - 1)] = 1;
|
|
288
|
+
let g = seed;
|
|
289
|
+
for (let li = net.layers.length - 1; li >= 0; li--)
|
|
290
|
+
g = backwardLayer(net.layers[li], g);
|
|
291
|
+
return g;
|
|
292
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type LatticeCell } from "./lattice.js";
|
|
2
|
+
import { type Propagator } from "./solver.js";
|
|
3
|
+
type S<E> = LatticeCell<ReadonlySet<E>>;
|
|
4
|
+
/** "These cells hold DIFFERENT values." When one collapses to a
|
|
5
|
+
* singleton `{v}`, eliminate `v` from the others (naked-single
|
|
6
|
+
* propagation). N(N−1) narrowers. */
|
|
7
|
+
export declare function allDifferent<E>(...cells: S<E>[]): Propagator[];
|
|
8
|
+
/** `a` and `b` hold the same value: intersect both candidate sets.
|
|
9
|
+
* Unification, lifted to sets. */
|
|
10
|
+
export declare function same<E>(a: S<E>, b: S<E>): Propagator[];
|
|
11
|
+
/** Restrict `cell` to `allowed` (intersect). Self-applying. */
|
|
12
|
+
export declare function restrict<E>(cell: S<E>, allowed: Iterable<E>): Propagator;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// csp.ts — set-narrowing relations over candidate-set cells.
|
|
2
|
+
//
|
|
3
|
+
// The discrete sibling of `numeric.ts`: same monotone-narrowing model,
|
|
4
|
+
// finite-set lattice. Height is bounded by the universe size, so these
|
|
5
|
+
// terminate structurally — sudoku, map colouring, type unification.
|
|
6
|
+
import { merge } from "./lattice.js";
|
|
7
|
+
import { propagator } from "./solver.js";
|
|
8
|
+
/** "These cells hold DIFFERENT values." When one collapses to a
|
|
9
|
+
* singleton `{v}`, eliminate `v` from the others (naked-single
|
|
10
|
+
* propagation). N(N−1) narrowers. */
|
|
11
|
+
export function allDifferent(...cells) {
|
|
12
|
+
const props = [];
|
|
13
|
+
for (let i = 0; i < cells.length; i++) {
|
|
14
|
+
for (let j = 0; j < cells.length; j++) {
|
|
15
|
+
if (i === j)
|
|
16
|
+
continue;
|
|
17
|
+
const src = cells[i];
|
|
18
|
+
const dst = cells[j];
|
|
19
|
+
props.push(propagator([src], [dst], () => {
|
|
20
|
+
const sv = src.value;
|
|
21
|
+
if (sv.size !== 1)
|
|
22
|
+
return;
|
|
23
|
+
const [only] = sv;
|
|
24
|
+
if (!dst.value.has(only))
|
|
25
|
+
return;
|
|
26
|
+
const next = new Set(dst.value);
|
|
27
|
+
next.delete(only);
|
|
28
|
+
merge(dst, next);
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return props;
|
|
33
|
+
}
|
|
34
|
+
/** `a` and `b` hold the same value: intersect both candidate sets.
|
|
35
|
+
* Unification, lifted to sets. */
|
|
36
|
+
export function same(a, b) {
|
|
37
|
+
return [
|
|
38
|
+
propagator([a], [b], () => {
|
|
39
|
+
merge(b, a.value);
|
|
40
|
+
}),
|
|
41
|
+
propagator([b], [a], () => {
|
|
42
|
+
merge(a, b.value);
|
|
43
|
+
}),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
/** Restrict `cell` to `allowed` (intersect). Self-applying. */
|
|
47
|
+
export function restrict(cell, allowed) {
|
|
48
|
+
const allow = new Set(allowed);
|
|
49
|
+
return propagator([cell], [cell], () => {
|
|
50
|
+
merge(cell, allow);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Box, type Num as NumClass, type Read, type Writable } from "../core/index.js";
|
|
2
|
+
import { type Propagator } from "./solver.js";
|
|
3
|
+
type ValOrSig = number | Read<number>;
|
|
4
|
+
/** A flex child. A bare `Box` takes defaults (grow 1, shrink 1, no
|
|
5
|
+
* bounds); tag it to set per-item flex. `basis` seeds the size the
|
|
6
|
+
* distribution grows/shrinks from (defaults to the box's current main
|
|
7
|
+
* size). */
|
|
8
|
+
export type Item = Box | {
|
|
9
|
+
box: Box;
|
|
10
|
+
grow?: number;
|
|
11
|
+
shrink?: number;
|
|
12
|
+
min?: number;
|
|
13
|
+
max?: number;
|
|
14
|
+
basis?: number;
|
|
15
|
+
};
|
|
16
|
+
export interface FlexOpts {
|
|
17
|
+
/** Space between adjacent items. Default 0. */
|
|
18
|
+
gap?: ValOrSig;
|
|
19
|
+
/** Padding inside the container on every side. Default 0. */
|
|
20
|
+
padding?: ValOrSig;
|
|
21
|
+
/** Cross-axis placement. Default "stretch". */
|
|
22
|
+
align?: "start" | "center" | "end" | "stretch";
|
|
23
|
+
/** Set to `true` on the frame(s) where the content can't fit (Σmin >
|
|
24
|
+
* content). Drives "this layout is impossible" UI. */
|
|
25
|
+
report?: Writable<NumClass> | ((infeasible: boolean) => void);
|
|
26
|
+
}
|
|
27
|
+
/** Horizontal flex line over plain `Box` cells. */
|
|
28
|
+
export declare function row(c: Box, items: readonly Item[], opts?: FlexOpts): Propagator;
|
|
29
|
+
/** Vertical flex line over plain `Box` cells. */
|
|
30
|
+
export declare function col(c: Box, items: readonly Item[], opts?: FlexOpts): Propagator;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// flex.ts — flexbox as interval propagation.
|
|
2
|
+
//
|
|
3
|
+
// A flex line is two phases, both grounded in the interval lattice:
|
|
4
|
+
//
|
|
5
|
+
// 1. FEASIBILITY (narrowing). Given the container's content size and
|
|
6
|
+
// each item's [min, max], the `total` relation narrows every item
|
|
7
|
+
// to the band it could occupy: `wᵢ ∈ content − Σ(others)`. If any
|
|
8
|
+
// band comes back empty the line can't fit — that's a real lattice
|
|
9
|
+
// contradiction, reported, not a silent overflow.
|
|
10
|
+
//
|
|
11
|
+
// 2. RESOLUTION (point pick). Within the feasible bands, grow/shrink
|
|
12
|
+
// weights distribute the slack to a single size per item. This is
|
|
13
|
+
// the one place a *preference* (not a constraint) enters, so it's
|
|
14
|
+
// kept out of the narrowing.
|
|
15
|
+
//
|
|
16
|
+
// The line operates on plain `Box` cells — zero ceremony, nothing is
|
|
17
|
+
// coloured. Nesting composes through the ordinary reactive graph: a
|
|
18
|
+
// child container is just an item of its parent, so resizing the root
|
|
19
|
+
// re-runs each line top-down. `row`/`col` are the only public surface;
|
|
20
|
+
// everything else is the two phases above.
|
|
21
|
+
import { isCell, readNow, } from "../core/index.js";
|
|
22
|
+
import { interval } from "./lattice.js";
|
|
23
|
+
import { propagator } from "./solver.js";
|
|
24
|
+
const asW = (n) => n;
|
|
25
|
+
function readDeps(...vs) {
|
|
26
|
+
return vs.filter(isCell);
|
|
27
|
+
}
|
|
28
|
+
const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
29
|
+
function specs(items) {
|
|
30
|
+
return items.map(it => "box" in it
|
|
31
|
+
? {
|
|
32
|
+
box: it.box,
|
|
33
|
+
grow: it.grow ?? 1,
|
|
34
|
+
shrink: it.shrink ?? 1,
|
|
35
|
+
min: it.min ?? 0,
|
|
36
|
+
max: it.max ?? Number.POSITIVE_INFINITY,
|
|
37
|
+
basis: it.basis,
|
|
38
|
+
}
|
|
39
|
+
: { box: it, grow: 1, shrink: 1, min: 0, max: Number.POSITIVE_INFINITY });
|
|
40
|
+
}
|
|
41
|
+
/** Horizontal flex line over plain `Box` cells. */
|
|
42
|
+
export function row(c, items, opts = {}) {
|
|
43
|
+
return line(c, items, opts, "x");
|
|
44
|
+
}
|
|
45
|
+
/** Vertical flex line over plain `Box` cells. */
|
|
46
|
+
export function col(c, items, opts = {}) {
|
|
47
|
+
return line(c, items, opts, "y");
|
|
48
|
+
}
|
|
49
|
+
function line(c, rawItems, opts, main) {
|
|
50
|
+
const items = specs(rawItems);
|
|
51
|
+
const horizontal = main === "x";
|
|
52
|
+
const mainPos = (b) => asW(horizontal ? b.x : b.y);
|
|
53
|
+
const mainSize = (b) => asW(horizontal ? b.w : b.h);
|
|
54
|
+
const crossPos = (b) => asW(horizontal ? b.y : b.x);
|
|
55
|
+
const crossSize = (b) => asW(horizontal ? b.h : b.w);
|
|
56
|
+
const align = opts.align ?? "stretch";
|
|
57
|
+
const reads = [
|
|
58
|
+
mainPos(c),
|
|
59
|
+
mainSize(c),
|
|
60
|
+
crossPos(c),
|
|
61
|
+
crossSize(c),
|
|
62
|
+
...items.map(it => crossSize(it.box)),
|
|
63
|
+
...readDeps(opts.gap ?? 0, opts.padding ?? 0),
|
|
64
|
+
];
|
|
65
|
+
const writes = [];
|
|
66
|
+
for (const it of items) {
|
|
67
|
+
writes.push(mainPos(it.box), mainSize(it.box), crossPos(it.box));
|
|
68
|
+
if (align === "stretch")
|
|
69
|
+
writes.push(crossSize(it.box));
|
|
70
|
+
}
|
|
71
|
+
if (typeof opts.report !== "function" && opts.report)
|
|
72
|
+
writes.push(opts.report);
|
|
73
|
+
return propagator(reads, writes, () => {
|
|
74
|
+
const gap = readNow(opts.gap ?? 0);
|
|
75
|
+
const pad = readNow(opts.padding ?? 0);
|
|
76
|
+
const n = items.length;
|
|
77
|
+
if (n === 0)
|
|
78
|
+
return;
|
|
79
|
+
const content = mainSize(c).value - 2 * pad - (n - 1) * gap;
|
|
80
|
+
const mins = items.map(it => it.min);
|
|
81
|
+
const maxs = items.map(it => it.max);
|
|
82
|
+
const sumMin = mins.reduce((a, b) => a + b, 0);
|
|
83
|
+
const sumMax = maxs.reduce((a, b) => a + b, 0);
|
|
84
|
+
// Phase 1 — feasibility. Per-item band from the `total` relation:
|
|
85
|
+
// wᵢ ∈ [content − Σmax(others), content − Σmin(others)] ∩ [minᵢ, maxᵢ].
|
|
86
|
+
const bands = items.map((_, i) => {
|
|
87
|
+
const lo = content - (sumMax - maxs[i]);
|
|
88
|
+
const hi = content - (sumMin - mins[i]);
|
|
89
|
+
return interval.meet([Math.max(lo, mins[i]), Math.min(hi, maxs[i])], [mins[i], maxs[i]]);
|
|
90
|
+
});
|
|
91
|
+
const infeasible = content < sumMin - 1e-9 || bands.some(interval.isBottom);
|
|
92
|
+
report(opts, infeasible);
|
|
93
|
+
// Phase 2 — resolution. Distribute slack from the basis sizes,
|
|
94
|
+
// weighted by grow (free > 0) / shrink (free < 0), clamped into the
|
|
95
|
+
// feasible band each pass and redistributed.
|
|
96
|
+
const base = items.map((it, i) => clamp(it.basis ?? mainSize(it.box).value, mins[i], maxs[i]));
|
|
97
|
+
const sizes = distribute(base, items, mins, maxs, content);
|
|
98
|
+
let cursor = mainPos(c).value + pad;
|
|
99
|
+
for (let i = 0; i < n; i++) {
|
|
100
|
+
mainPos(items[i].box).value = cursor;
|
|
101
|
+
mainSize(items[i].box).value = sizes[i];
|
|
102
|
+
cursor += sizes[i] + gap;
|
|
103
|
+
}
|
|
104
|
+
const cBase = crossPos(c).value + pad;
|
|
105
|
+
const cAvail = crossSize(c).value - 2 * pad;
|
|
106
|
+
for (const it of items) {
|
|
107
|
+
const itSize = crossSize(it.box).value;
|
|
108
|
+
switch (align) {
|
|
109
|
+
case "start":
|
|
110
|
+
crossPos(it.box).value = cBase;
|
|
111
|
+
break;
|
|
112
|
+
case "center":
|
|
113
|
+
crossPos(it.box).value = cBase + (cAvail - itSize) / 2;
|
|
114
|
+
break;
|
|
115
|
+
case "end":
|
|
116
|
+
crossPos(it.box).value = cBase + cAvail - itSize;
|
|
117
|
+
break;
|
|
118
|
+
case "stretch":
|
|
119
|
+
crossPos(it.box).value = cBase;
|
|
120
|
+
crossSize(it.box).value = cAvail;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** CSS-flex slack distribution, clamped into [min, max] with overflow
|
|
127
|
+
* redistributed across still-eligible items. */
|
|
128
|
+
function distribute(base, items, mins, maxs, content) {
|
|
129
|
+
const n = base.length;
|
|
130
|
+
const sizes = base.slice();
|
|
131
|
+
const free = content - base.reduce((a, b) => a + b, 0);
|
|
132
|
+
if (free > 1e-9) {
|
|
133
|
+
let remaining = free;
|
|
134
|
+
const eligible = new Set();
|
|
135
|
+
for (let i = 0; i < n; i++)
|
|
136
|
+
if (items[i].grow > 0 && sizes[i] < maxs[i])
|
|
137
|
+
eligible.add(i);
|
|
138
|
+
while (remaining > 1e-9 && eligible.size > 0) {
|
|
139
|
+
let weight = 0;
|
|
140
|
+
for (const i of eligible)
|
|
141
|
+
weight += items[i].grow;
|
|
142
|
+
if (weight === 0)
|
|
143
|
+
break;
|
|
144
|
+
let absorbed = 0;
|
|
145
|
+
for (const i of [...eligible]) {
|
|
146
|
+
const next = Math.min(sizes[i] + (remaining * items[i].grow) / weight, maxs[i]);
|
|
147
|
+
absorbed += next - sizes[i];
|
|
148
|
+
sizes[i] = next;
|
|
149
|
+
if (next >= maxs[i])
|
|
150
|
+
eligible.delete(i);
|
|
151
|
+
}
|
|
152
|
+
if (absorbed < 1e-9)
|
|
153
|
+
break;
|
|
154
|
+
remaining -= absorbed;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (free < -1e-9) {
|
|
158
|
+
let remaining = -free;
|
|
159
|
+
const eligible = new Set();
|
|
160
|
+
for (let i = 0; i < n; i++)
|
|
161
|
+
if (items[i].shrink > 0 && sizes[i] > mins[i])
|
|
162
|
+
eligible.add(i);
|
|
163
|
+
while (remaining > 1e-9 && eligible.size > 0) {
|
|
164
|
+
let weight = 0;
|
|
165
|
+
for (const i of eligible)
|
|
166
|
+
weight += items[i].shrink;
|
|
167
|
+
if (weight === 0)
|
|
168
|
+
break;
|
|
169
|
+
let absorbed = 0;
|
|
170
|
+
for (const i of [...eligible]) {
|
|
171
|
+
const next = Math.max(sizes[i] - (remaining * items[i].shrink) / weight, mins[i]);
|
|
172
|
+
absorbed += sizes[i] - next;
|
|
173
|
+
sizes[i] = next;
|
|
174
|
+
if (next <= mins[i])
|
|
175
|
+
eligible.delete(i);
|
|
176
|
+
}
|
|
177
|
+
if (absorbed < 1e-9)
|
|
178
|
+
break;
|
|
179
|
+
remaining -= absorbed;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return sizes;
|
|
183
|
+
}
|
|
184
|
+
function report(opts, infeasible) {
|
|
185
|
+
if (typeof opts.report === "function")
|
|
186
|
+
opts.report(infeasible);
|
|
187
|
+
else if (opts.report)
|
|
188
|
+
asW(opts.report).value = infeasible ? 1 : 0;
|
|
189
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** A directed graph. Edges are `[from, to]`; nodes carry any identity. */
|
|
2
|
+
export interface Graph<N> {
|
|
3
|
+
readonly nodes: readonly N[];
|
|
4
|
+
readonly edges: readonly (readonly [N, N])[];
|
|
5
|
+
}
|
|
6
|
+
export type Direction = "TB" | "BT" | "LR" | "RL";
|
|
7
|
+
/** Intrinsic size of a node's box. */
|
|
8
|
+
export interface Size {
|
|
9
|
+
w: number;
|
|
10
|
+
h: number;
|
|
11
|
+
}
|
|
12
|
+
/** Top-left placement of a node's box. */
|
|
13
|
+
export interface Placement {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
w: number;
|
|
17
|
+
h: number;
|
|
18
|
+
}
|
|
19
|
+
export interface LayeredOpts<N> {
|
|
20
|
+
direction?: Direction;
|
|
21
|
+
/** Centre-to-centre distance between adjacent layers. Default 90. */
|
|
22
|
+
layerGap?: number;
|
|
23
|
+
/** Minimum edge-to-edge distance within a layer. Default 28. */
|
|
24
|
+
nodeGap?: number;
|
|
25
|
+
/** Intrinsic node size. Default 46×34. */
|
|
26
|
+
sizeOf?: (n: N) => Size;
|
|
27
|
+
/** Crossing-reduction sweeps. Default 6. */
|
|
28
|
+
sweeps?: number;
|
|
29
|
+
/** Barycenter source: "both" (DAG), "down" (parents over children,
|
|
30
|
+
* the tree look), "up". Default "both". */
|
|
31
|
+
align?: "both" | "down" | "up";
|
|
32
|
+
}
|
|
33
|
+
/** Assign each node an integer layer so every edge increases layer by
|
|
34
|
+
* ≥1. Cycles are broken (back edges reversed for ranking only). The
|
|
35
|
+
* value is the longest path from a source — computed as the lower
|
|
36
|
+
* bound the `order` atoms narrow each layer cell to. */
|
|
37
|
+
export declare function rank<N>(g: Graph<N>): Map<N, number>;
|
|
38
|
+
/** Strongly-connected components (Tarjan), each a list of mutually
|
|
39
|
+
* reachable nodes, in reverse-topological order of the condensation.
|
|
40
|
+
* Singletons are size-1 components; a self-loop still reports size 1.
|
|
41
|
+
* The cyclic cores of a graph are exactly the components of size > 1. */
|
|
42
|
+
export declare function scc<N>(g: Graph<N>): N[][];
|
|
43
|
+
/** Count edge crossings given per-layer orderings (for tests / quality). */
|
|
44
|
+
export declare function crossings<N>(g: Graph<N>, layer: Map<N, number>, layers: N[][]): number;
|
|
45
|
+
/** Full layered layout → top-left placement per node. */
|
|
46
|
+
export declare function layered<N>(g: Graph<N>, opts?: LayeredOpts<N>): Map<N, Placement>;
|
|
47
|
+
/** Tree layout: parents centred over their children. A `layered` with
|
|
48
|
+
* downward barycenter and tighter defaults. */
|
|
49
|
+
export declare function tree<N>(g: Graph<N>, opts?: LayeredOpts<N>): Map<N, Placement>;
|
|
50
|
+
/** Radial layout: layers become concentric rings, cross-axis position
|
|
51
|
+
* becomes angle. Reads a `layered` (TB) result and re-maps to polar. */
|
|
52
|
+
export declare function radial<N>(g: Graph<N>, opts?: LayeredOpts<N>): Map<N, Placement>;
|
|
53
|
+
/** Recurrent-hierarchy layout: decompose into strongly-connected
|
|
54
|
+
* components, lay the condensation (a DAG of SCCs) out hierarchically,
|
|
55
|
+
* and draw every cyclic component as a ring centred on its condensation
|
|
56
|
+
* slot. The cycle closes by going around the circle — no backward edge.
|
|
57
|
+
* Singleton components are placed as ordinary nodes. */
|
|
58
|
+
export declare function recurrent<N>(g: Graph<N>, opts?: LayeredOpts<N>): Map<N, Placement>;
|
|
59
|
+
export interface LanesOpts<N> {
|
|
60
|
+
/** Row height (topological order axis). Default 56. */
|
|
61
|
+
rowGap?: number;
|
|
62
|
+
/** Lane width (column axis). Default 46. */
|
|
63
|
+
laneGap?: number;
|
|
64
|
+
sizeOf?: (n: N) => Size;
|
|
65
|
+
}
|
|
66
|
+
/** Git-style lane packing: nodes flow top-to-bottom in topological
|
|
67
|
+
* order (one row each), each assigned the first free column ("lane")
|
|
68
|
+
* that no live edge occupies. Branch points open lanes, merges free
|
|
69
|
+
* them. Expects a DAG whose `nodes` are in commit order (parents
|
|
70
|
+
* before children). */
|
|
71
|
+
export declare function lanes<N>(g: Graph<N>, opts?: LanesOpts<N>): Map<N, Placement>;
|
|
72
|
+
/** Bounding size of a placement map. */
|
|
73
|
+
export declare function extent<N>(p: Map<N, Placement>): Size;
|