bireactive 0.2.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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/animation/anim.d.ts +57 -0
- package/dist/animation/anim.js +318 -0
- package/dist/animation/combinators.d.ts +39 -0
- package/dist/animation/combinators.js +113 -0
- package/dist/animation/easings.d.ts +5 -0
- package/dist/animation/easings.js +5 -0
- package/dist/animation/index.d.ts +3 -0
- package/dist/animation/index.js +3 -0
- package/dist/assert/algebra.d.ts +20 -0
- package/dist/assert/algebra.js +79 -0
- package/dist/assert/claim.d.ts +40 -0
- package/dist/assert/claim.js +129 -0
- package/dist/assert/index.d.ts +7 -0
- package/dist/assert/index.js +19 -0
- package/dist/assert/predicates.d.ts +18 -0
- package/dist/assert/predicates.js +43 -0
- package/dist/assert/record.d.ts +20 -0
- package/dist/assert/record.js +78 -0
- package/dist/assert/scope.d.ts +42 -0
- package/dist/assert/scope.js +233 -0
- package/dist/assert/span.d.ts +37 -0
- package/dist/assert/span.js +68 -0
- package/dist/assert/tree.d.ts +22 -0
- package/dist/assert/tree.js +65 -0
- package/dist/code/code.d.ts +70 -0
- package/dist/code/code.js +361 -0
- package/dist/code/index.d.ts +2 -0
- package/dist/code/index.js +9 -0
- package/dist/code/morph.d.ts +5 -0
- package/dist/code/morph.js +194 -0
- package/dist/code/tokenize.d.ts +8 -0
- package/dist/code/tokenize.js +51 -0
- package/dist/constraints/cluster.d.ts +83 -0
- package/dist/constraints/cluster.js +213 -0
- package/dist/constraints/drivers.d.ts +15 -0
- package/dist/constraints/drivers.js +40 -0
- package/dist/constraints/factories.d.ts +73 -0
- package/dist/constraints/factories.js +248 -0
- package/dist/constraints/index.d.ts +11 -0
- package/dist/constraints/index.js +39 -0
- package/dist/constraints/interaction.d.ts +21 -0
- package/dist/constraints/interaction.js +148 -0
- package/dist/constraints/linalg.d.ts +18 -0
- package/dist/constraints/linalg.js +141 -0
- package/dist/constraints/phases.d.ts +21 -0
- package/dist/constraints/phases.js +60 -0
- package/dist/constraints/physics.d.ts +34 -0
- package/dist/constraints/physics.js +128 -0
- package/dist/constraints/rigid.d.ts +210 -0
- package/dist/constraints/rigid.js +835 -0
- package/dist/constraints/solver.d.ts +107 -0
- package/dist/constraints/solver.js +510 -0
- package/dist/constraints/term.d.ts +50 -0
- package/dist/constraints/term.js +80 -0
- package/dist/constraints/terms.d.ts +80 -0
- package/dist/constraints/terms.js +302 -0
- package/dist/constraints/world.d.ts +31 -0
- package/dist/constraints/world.js +245 -0
- package/dist/core/aggregates.d.ts +64 -0
- package/dist/core/aggregates.js +198 -0
- package/dist/core/anim.d.ts +84 -0
- package/dist/core/anim.js +301 -0
- package/dist/core/index.d.ts +38 -0
- package/dist/core/index.js +38 -0
- package/dist/core/introspect.d.ts +5 -0
- package/dist/core/introspect.js +31 -0
- package/dist/core/lenses/closed-form-policies.d.ts +64 -0
- package/dist/core/lenses/closed-form-policies.js +452 -0
- package/dist/core/lenses/domain-aggregates.d.ts +54 -0
- package/dist/core/lenses/domain-aggregates.js +259 -0
- package/dist/core/lenses/factor-lens.d.ts +42 -0
- package/dist/core/lenses/factor-lens.js +419 -0
- package/dist/core/lenses/index.d.ts +5 -0
- package/dist/core/lenses/index.js +16 -0
- package/dist/core/lenses/memory.d.ts +47 -0
- package/dist/core/lenses/memory.js +102 -0
- package/dist/core/lenses/typed-factor.d.ts +45 -0
- package/dist/core/lenses/typed-factor.js +376 -0
- package/dist/core/network-utils.d.ts +14 -0
- package/dist/core/network-utils.js +62 -0
- package/dist/core/new-primitives.d.ts +33 -0
- package/dist/core/new-primitives.js +113 -0
- package/dist/core/signal.d.ts +254 -0
- package/dist/core/signal.js +1349 -0
- package/dist/core/traits.d.ts +61 -0
- package/dist/core/traits.js +56 -0
- package/dist/core/tree.d.ts +23 -0
- package/dist/core/tree.js +62 -0
- package/dist/core/values/anchor.d.ts +23 -0
- package/dist/core/values/anchor.js +23 -0
- package/dist/core/values/audio.d.ts +33 -0
- package/dist/core/values/audio.js +107 -0
- package/dist/core/values/bool.d.ts +37 -0
- package/dist/core/values/bool.js +75 -0
- package/dist/core/values/box.d.ts +77 -0
- package/dist/core/values/box.js +211 -0
- package/dist/core/values/canvas.d.ts +71 -0
- package/dist/core/values/canvas.js +495 -0
- package/dist/core/values/color.d.ts +49 -0
- package/dist/core/values/color.js +106 -0
- package/dist/core/values/flags.d.ts +18 -0
- package/dist/core/values/flags.js +50 -0
- package/dist/core/values/gpu.d.ts +74 -0
- package/dist/core/values/gpu.js +426 -0
- package/dist/core/values/matrix.d.ts +53 -0
- package/dist/core/values/matrix.js +140 -0
- package/dist/core/values/num.d.ts +62 -0
- package/dist/core/values/num.js +166 -0
- package/dist/core/values/pose.d.ts +31 -0
- package/dist/core/values/pose.js +83 -0
- package/dist/core/values/range.d.ts +83 -0
- package/dist/core/values/range.js +167 -0
- package/dist/core/values/str.d.ts +76 -0
- package/dist/core/values/str.js +346 -0
- package/dist/core/values/template.d.ts +49 -0
- package/dist/core/values/template.js +148 -0
- package/dist/core/values/transform.d.ts +49 -0
- package/dist/core/values/transform.js +115 -0
- package/dist/core/values/tri.d.ts +31 -0
- package/dist/core/values/tri.js +95 -0
- package/dist/core/values/vec.d.ts +72 -0
- package/dist/core/values/vec.js +219 -0
- package/dist/core/writable.d.ts +15 -0
- package/dist/core/writable.js +29 -0
- package/dist/ext/events.d.ts +10 -0
- package/dist/ext/events.js +31 -0
- package/dist/ext/index.d.ts +4 -0
- package/dist/ext/index.js +4 -0
- package/dist/ext/snapshot.d.ts +8 -0
- package/dist/ext/snapshot.js +29 -0
- package/dist/ext/timeline.d.ts +56 -0
- package/dist/ext/timeline.js +94 -0
- package/dist/ext/waapi.d.ts +25 -0
- package/dist/ext/waapi.js +198 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/propagators/index.d.ts +6 -0
- package/dist/propagators/index.js +6 -0
- package/dist/propagators/layout.d.ts +68 -0
- package/dist/propagators/layout.js +336 -0
- package/dist/propagators/network.d.ts +52 -0
- package/dist/propagators/network.js +185 -0
- package/dist/propagators/propagator.d.ts +12 -0
- package/dist/propagators/propagator.js +16 -0
- package/dist/propagators/range.d.ts +45 -0
- package/dist/propagators/range.js +147 -0
- package/dist/propagators/relations.d.ts +60 -0
- package/dist/propagators/relations.js +343 -0
- package/dist/shapes/annular-sector.d.ts +15 -0
- package/dist/shapes/annular-sector.js +64 -0
- package/dist/shapes/button.d.ts +14 -0
- package/dist/shapes/button.js +31 -0
- package/dist/shapes/choreographers.d.ts +22 -0
- package/dist/shapes/choreographers.js +69 -0
- package/dist/shapes/circle.d.ts +17 -0
- package/dist/shapes/circle.js +57 -0
- package/dist/shapes/clip.d.ts +5 -0
- package/dist/shapes/clip.js +31 -0
- package/dist/shapes/connect.d.ts +16 -0
- package/dist/shapes/connect.js +70 -0
- package/dist/shapes/curve.d.ts +60 -0
- package/dist/shapes/curve.js +285 -0
- package/dist/shapes/dashed.d.ts +16 -0
- package/dist/shapes/dashed.js +142 -0
- package/dist/shapes/debug.d.ts +43 -0
- package/dist/shapes/debug.js +97 -0
- package/dist/shapes/group.d.ts +5 -0
- package/dist/shapes/group.js +10 -0
- package/dist/shapes/handle.d.ts +32 -0
- package/dist/shapes/handle.js +88 -0
- package/dist/shapes/index.d.ts +23 -0
- package/dist/shapes/index.js +23 -0
- package/dist/shapes/interaction.d.ts +32 -0
- package/dist/shapes/interaction.js +187 -0
- package/dist/shapes/label.d.ts +20 -0
- package/dist/shapes/label.js +42 -0
- package/dist/shapes/layout.d.ts +29 -0
- package/dist/shapes/layout.js +74 -0
- package/dist/shapes/line.d.ts +21 -0
- package/dist/shapes/line.js +79 -0
- package/dist/shapes/list.d.ts +18 -0
- package/dist/shapes/list.js +51 -0
- package/dist/shapes/mount.d.ts +7 -0
- package/dist/shapes/mount.js +10 -0
- package/dist/shapes/path.d.ts +77 -0
- package/dist/shapes/path.js +227 -0
- package/dist/shapes/rect.d.ts +30 -0
- package/dist/shapes/rect.js +131 -0
- package/dist/shapes/shape.d.ts +132 -0
- package/dist/shapes/shape.js +306 -0
- package/dist/shapes/text.d.ts +24 -0
- package/dist/shapes/text.js +53 -0
- package/dist/shapes/tokens.d.ts +28 -0
- package/dist/shapes/tokens.js +27 -0
- package/dist/shapes/transitions.d.ts +23 -0
- package/dist/shapes/transitions.js +62 -0
- package/dist/tex/decorations.d.ts +26 -0
- package/dist/tex/decorations.js +116 -0
- package/dist/tex/index.d.ts +5 -0
- package/dist/tex/index.js +5 -0
- package/dist/tex/marker.d.ts +17 -0
- package/dist/tex/marker.js +63 -0
- package/dist/tex/motion.d.ts +43 -0
- package/dist/tex/motion.js +290 -0
- package/dist/tex/parts.d.ts +65 -0
- package/dist/tex/parts.js +149 -0
- package/dist/tex/tex.d.ts +45 -0
- package/dist/tex/tex.js +244 -0
- package/dist/web/attr.d.ts +16 -0
- package/dist/web/attr.js +98 -0
- package/dist/web/diagram.d.ts +49 -0
- package/dist/web/diagram.js +260 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +6 -0
- package/dist/web/md-marker.d.ts +6 -0
- package/dist/web/md-marker.js +39 -0
- package/dist/web/md-tex.d.ts +6 -0
- package/dist/web/md-tex.js +61 -0
- package/dist/web/raf.d.ts +6 -0
- package/dist/web/raf.js +24 -0
- package/dist/web/viewport.d.ts +7 -0
- package/dist/web/viewport.js +13 -0
- package/package.json +87 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// factor-lens.ts — N→M lens prototypes (Vec-specific monoliths).
|
|
2
|
+
//
|
|
3
|
+
// N inputs → M coupled writable outputs, where writing one output
|
|
4
|
+
// preserves the readings of the other M−1 (cross-channel invariance).
|
|
5
|
+
// Two regimes:
|
|
6
|
+
//
|
|
7
|
+
// 1. Numerical (Jacobian-LSQ): `factorLens` builds an M×N Jacobian by
|
|
8
|
+
// finite differences and solves `(J W Jᵀ + λI) k = δy` for `k`,
|
|
9
|
+
// then writes `δx = W Jᵀ k`. δy is sparse (only the written
|
|
10
|
+
// channel), so the solve leaves other channels near-stationary.
|
|
11
|
+
// Invariance is approximate, set by the local condition number.
|
|
12
|
+
//
|
|
13
|
+
// 2. Closed-form (geometric): `procrustesLens`, `bboxLens`,
|
|
14
|
+
// `meanDiffLens` — hand-rolled bwd via the right group action, so
|
|
15
|
+
// cross-channel invariance is EXACT. Cost: crafted per topology.
|
|
16
|
+
//
|
|
17
|
+
// `bundleLens` sketches the 1→M dual (single source → M coupled views).
|
|
18
|
+
import { Num, Vec } from "../index.js";
|
|
19
|
+
export function factorLens(inputs, forwards, opts = {}) {
|
|
20
|
+
const N = inputs.length;
|
|
21
|
+
const M = forwards.length;
|
|
22
|
+
if (M === 0)
|
|
23
|
+
return [];
|
|
24
|
+
const w = opts.inputWeights ?? new Array(N).fill(1);
|
|
25
|
+
const eps = opts.eps ?? 1e-5;
|
|
26
|
+
const lambda = opts.damping ?? 1e-6;
|
|
27
|
+
// Per-call (NOT per-cell) scratch; safe because writes execute
|
|
28
|
+
// synchronously inside `_setWithExclusion`.
|
|
29
|
+
const J = new Array(M * N);
|
|
30
|
+
const A = new Array(M * M);
|
|
31
|
+
const Ainv = new Array(M * M);
|
|
32
|
+
const ys = new Array(M);
|
|
33
|
+
const dy = new Array(M);
|
|
34
|
+
const kvec = new Array(M);
|
|
35
|
+
const outputs = [];
|
|
36
|
+
for (let outIdx = 0; outIdx < M; outIdx++) {
|
|
37
|
+
const idx = outIdx;
|
|
38
|
+
const out = new Array(N);
|
|
39
|
+
const cell = Num.lens(inputs, (vals) => forwards[idx](vals), (target, valsReadonly) => {
|
|
40
|
+
// Snapshot inputs into a mutable scratch so FD perturbations
|
|
41
|
+
// don't leak into upstream cell state.
|
|
42
|
+
const xs = valsReadonly;
|
|
43
|
+
const xsm = xs.slice();
|
|
44
|
+
for (let j = 0; j < M; j++)
|
|
45
|
+
ys[j] = forwards[j](xsm);
|
|
46
|
+
for (let j = 0; j < M; j++)
|
|
47
|
+
dy[j] = 0;
|
|
48
|
+
dy[idx] = target - ys[idx];
|
|
49
|
+
// Build Jacobian column-by-column.
|
|
50
|
+
for (let i = 0; i < N; i++) {
|
|
51
|
+
const saved = xsm[i];
|
|
52
|
+
xsm[i] = saved + eps;
|
|
53
|
+
for (let j = 0; j < M; j++) {
|
|
54
|
+
J[j * N + i] = (forwards[j](xsm) - ys[j]) / eps;
|
|
55
|
+
}
|
|
56
|
+
xsm[i] = saved;
|
|
57
|
+
}
|
|
58
|
+
// A = J W Jᵀ + λI
|
|
59
|
+
for (let r = 0; r < M; r++) {
|
|
60
|
+
for (let c = 0; c < M; c++) {
|
|
61
|
+
let s = 0;
|
|
62
|
+
for (let i = 0; i < N; i++)
|
|
63
|
+
s += J[r * N + i] * w[i] * J[c * N + i];
|
|
64
|
+
A[r * M + c] = s + (r === c ? lambda : 0);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!invertMatrix(A, M, Ainv)) {
|
|
68
|
+
// Singular — leave inputs unchanged.
|
|
69
|
+
for (let i = 0; i < N; i++)
|
|
70
|
+
out[i] = undefined;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
for (let r = 0; r < M; r++) {
|
|
74
|
+
let s = 0;
|
|
75
|
+
for (let c = 0; c < M; c++)
|
|
76
|
+
s += Ainv[r * M + c] * dy[c];
|
|
77
|
+
kvec[r] = s;
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < N; i++) {
|
|
80
|
+
let dxi = 0;
|
|
81
|
+
for (let r = 0; r < M; r++)
|
|
82
|
+
dxi += J[r * N + i] * kvec[r];
|
|
83
|
+
out[i] = xsm[i] + w[i] * dxi;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
});
|
|
87
|
+
outputs.push(cell);
|
|
88
|
+
}
|
|
89
|
+
return outputs;
|
|
90
|
+
}
|
|
91
|
+
/** Gauss-Jordan inverse of a row-major M×M matrix. Returns false if
|
|
92
|
+
* singular (pivot below 1e-14). Allocates one 2M-wide row buffer.
|
|
93
|
+
* For M ≤ ~10 this is competitive with LAPACK and avoids the dep. */
|
|
94
|
+
function invertMatrix(A, M, out) {
|
|
95
|
+
const W = 2 * M;
|
|
96
|
+
const aug = new Array(M * W);
|
|
97
|
+
for (let r = 0; r < M; r++) {
|
|
98
|
+
for (let c = 0; c < M; c++)
|
|
99
|
+
aug[r * W + c] = A[r * M + c];
|
|
100
|
+
for (let c = 0; c < M; c++)
|
|
101
|
+
aug[r * W + M + c] = r === c ? 1 : 0;
|
|
102
|
+
}
|
|
103
|
+
for (let i = 0; i < M; i++) {
|
|
104
|
+
let p = i;
|
|
105
|
+
let pv = Math.abs(aug[i * W + i]);
|
|
106
|
+
for (let r = i + 1; r < M; r++) {
|
|
107
|
+
const v = Math.abs(aug[r * W + i]);
|
|
108
|
+
if (v > pv) {
|
|
109
|
+
pv = v;
|
|
110
|
+
p = r;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (pv < 1e-14)
|
|
114
|
+
return false;
|
|
115
|
+
if (p !== i) {
|
|
116
|
+
for (let c = 0; c < W; c++) {
|
|
117
|
+
const t = aug[i * W + c];
|
|
118
|
+
aug[i * W + c] = aug[p * W + c];
|
|
119
|
+
aug[p * W + c] = t;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const inv = 1 / aug[i * W + i];
|
|
123
|
+
for (let c = 0; c < W; c++)
|
|
124
|
+
aug[i * W + c] = aug[i * W + c] * inv;
|
|
125
|
+
for (let r = 0; r < M; r++) {
|
|
126
|
+
if (r === i)
|
|
127
|
+
continue;
|
|
128
|
+
const f = aug[r * W + i];
|
|
129
|
+
if (f === 0)
|
|
130
|
+
continue;
|
|
131
|
+
for (let c = 0; c < W; c++)
|
|
132
|
+
aug[r * W + c] = aug[r * W + c] - f * aug[i * W + c];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (let r = 0; r < M; r++) {
|
|
136
|
+
for (let c = 0; c < M; c++)
|
|
137
|
+
out[r * M + c] = aug[r * W + M + c];
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
// meanDiffLens — M=2 isomorphism baseline.
|
|
142
|
+
//
|
|
143
|
+
// (a, b) → ((a+b)/2, a−b). Square full-rank linear lens; bwd is the
|
|
144
|
+
// inverse change of basis — exact, cross-channel invariant. Sanity
|
|
145
|
+
// baseline for the property tests.
|
|
146
|
+
export function meanDiffLens(a, b) {
|
|
147
|
+
const mean = Num.lens([a, b], vals => (vals[0] + vals[1]) / 2, (target, vals) => {
|
|
148
|
+
const d = vals[0] - vals[1];
|
|
149
|
+
return [target + d / 2, target - d / 2];
|
|
150
|
+
});
|
|
151
|
+
const diff = Num.lens([a, b], vals => vals[0] - vals[1], (target, vals) => {
|
|
152
|
+
const m = (vals[0] + vals[1]) / 2;
|
|
153
|
+
return [m + target / 2, m - target / 2];
|
|
154
|
+
});
|
|
155
|
+
return { mean, diff };
|
|
156
|
+
}
|
|
157
|
+
// procrustesLens — closed-form similarity (the showcase).
|
|
158
|
+
//
|
|
159
|
+
// K writable Vecs → {centroid, rotation (angle of point[0] about
|
|
160
|
+
// centroid), scale (its distance from centroid)}. Each bwd is a
|
|
161
|
+
// closed-form transform about the centroid:
|
|
162
|
+
// write centroid → translate every point by (c − old c)
|
|
163
|
+
// write rotation → rotate every point about centroid by (θ − old θ)
|
|
164
|
+
// write scale → scale every point about centroid by (s / old s)
|
|
165
|
+
// These commute on the cluster's similarity orbit, so the three outputs
|
|
166
|
+
// have EXACT cross-channel invariance (cf. `procrustesJacobianLens`'s
|
|
167
|
+
// approximate version). Degenerate: K < 2 leaves rotation/scale
|
|
168
|
+
// undefined; a collapsed cluster (scale → 0) makes rotation singular and
|
|
169
|
+
// scale writes no-ops; target scale = 0 collapses to the centroid.
|
|
170
|
+
export function procrustesLens(points) {
|
|
171
|
+
const K = points.length;
|
|
172
|
+
if (K < 2)
|
|
173
|
+
throw new Error("procrustesLens: need ≥ 2 points");
|
|
174
|
+
const centroid = Vec.lens(points, (vals) => {
|
|
175
|
+
let sx = 0;
|
|
176
|
+
let sy = 0;
|
|
177
|
+
for (let i = 0; i < K; i++) {
|
|
178
|
+
sx += vals[i].x;
|
|
179
|
+
sy += vals[i].y;
|
|
180
|
+
}
|
|
181
|
+
return { x: sx / K, y: sy / K };
|
|
182
|
+
}, (target, vals) => {
|
|
183
|
+
let sx = 0;
|
|
184
|
+
let sy = 0;
|
|
185
|
+
for (let i = 0; i < K; i++) {
|
|
186
|
+
sx += vals[i].x;
|
|
187
|
+
sy += vals[i].y;
|
|
188
|
+
}
|
|
189
|
+
const dx = target.x - sx / K;
|
|
190
|
+
const dy = target.y - sy / K;
|
|
191
|
+
const out = new Array(K);
|
|
192
|
+
for (let i = 0; i < K; i++)
|
|
193
|
+
out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
|
|
194
|
+
return out;
|
|
195
|
+
});
|
|
196
|
+
const rotation = Num.lens(points, (vals) => {
|
|
197
|
+
let sx = 0;
|
|
198
|
+
let sy = 0;
|
|
199
|
+
for (let i = 0; i < K; i++) {
|
|
200
|
+
sx += vals[i].x;
|
|
201
|
+
sy += vals[i].y;
|
|
202
|
+
}
|
|
203
|
+
const cx = sx / K;
|
|
204
|
+
const cy = sy / K;
|
|
205
|
+
return Math.atan2(vals[0].y - cy, vals[0].x - cx);
|
|
206
|
+
}, (target, vals) => {
|
|
207
|
+
let sx = 0;
|
|
208
|
+
let sy = 0;
|
|
209
|
+
for (let i = 0; i < K; i++) {
|
|
210
|
+
sx += vals[i].x;
|
|
211
|
+
sy += vals[i].y;
|
|
212
|
+
}
|
|
213
|
+
const cx = sx / K;
|
|
214
|
+
const cy = sy / K;
|
|
215
|
+
const rx0 = vals[0].x - cx;
|
|
216
|
+
const ry0 = vals[0].y - cy;
|
|
217
|
+
if (rx0 * rx0 + ry0 * ry0 < 1e-24) {
|
|
218
|
+
// Collapsed cluster; no angle to rotate from.
|
|
219
|
+
return vals.map(() => undefined);
|
|
220
|
+
}
|
|
221
|
+
const oldθ = Math.atan2(ry0, rx0);
|
|
222
|
+
const dθ = target - oldθ;
|
|
223
|
+
const cos = Math.cos(dθ);
|
|
224
|
+
const sin = Math.sin(dθ);
|
|
225
|
+
const out = new Array(K);
|
|
226
|
+
for (let i = 0; i < K; i++) {
|
|
227
|
+
const rx = vals[i].x - cx;
|
|
228
|
+
const ry = vals[i].y - cy;
|
|
229
|
+
out[i] = { x: cx + cos * rx - sin * ry, y: cy + sin * rx + cos * ry };
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
});
|
|
233
|
+
const centroidOf = (vals) => {
|
|
234
|
+
let sx = 0;
|
|
235
|
+
let sy = 0;
|
|
236
|
+
for (let i = 0; i < K; i++) {
|
|
237
|
+
sx += vals[i].x;
|
|
238
|
+
sy += vals[i].y;
|
|
239
|
+
}
|
|
240
|
+
return { x: sx / K, y: sy / K };
|
|
241
|
+
};
|
|
242
|
+
const refreshDevs = (devs, vals) => {
|
|
243
|
+
const c = centroidOf(vals);
|
|
244
|
+
return devs.map((d, i) => {
|
|
245
|
+
const dx = vals[i].x - c.x;
|
|
246
|
+
const dy = vals[i].y - c.y;
|
|
247
|
+
return dx * dx + dy * dy > 1e-18 ? { x: dx, y: dy } : d;
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
const scale = Num.lens(points, {
|
|
251
|
+
init: (vals) => {
|
|
252
|
+
const c = centroidOf(vals);
|
|
253
|
+
return { devs: vals.map(v => ({ x: v.x - c.x, y: v.y - c.y })) };
|
|
254
|
+
},
|
|
255
|
+
step: (vals, c) => ({ devs: refreshDevs(c.devs, vals) }),
|
|
256
|
+
fwd: (vals) => {
|
|
257
|
+
const c = centroidOf(vals);
|
|
258
|
+
return Math.hypot(vals[0].x - c.x, vals[0].y - c.y);
|
|
259
|
+
},
|
|
260
|
+
bwd: (target, vals, c) => {
|
|
261
|
+
const cen = centroidOf(vals);
|
|
262
|
+
const d0 = c.devs[0];
|
|
263
|
+
const r0 = Math.hypot(d0.x, d0.y);
|
|
264
|
+
if (r0 < 1e-12)
|
|
265
|
+
return { updates: vals.map(() => undefined), complement: c };
|
|
266
|
+
const k = target / r0;
|
|
267
|
+
const out = c.devs.map(d => ({ x: cen.x + k * d.x, y: cen.y + k * d.y }));
|
|
268
|
+
return { updates: out, complement: c };
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
return { centroid, rotation, scale };
|
|
272
|
+
}
|
|
273
|
+
// bboxLens — closed-form axis-aligned bounding box.
|
|
274
|
+
//
|
|
275
|
+
// K Vecs → {center, size}. Forward is min/max (piecewise-constant
|
|
276
|
+
// Jacobian — fatal for FD), but the closed-form bwd is exact:
|
|
277
|
+
// write center → translate all points by (c − old c)
|
|
278
|
+
// write size → scale all about center by component-wise ratio
|
|
279
|
+
// Center↔size invariance is exact. Degenerate axes (size = 0) write
|
|
280
|
+
// as no-ops; negative size reflects (kept permissive).
|
|
281
|
+
export function bboxLens(points) {
|
|
282
|
+
const K = points.length;
|
|
283
|
+
if (K < 1)
|
|
284
|
+
throw new Error("bboxLens: need ≥ 1 point");
|
|
285
|
+
const computeBox = (vals) => {
|
|
286
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
287
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
288
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
289
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
290
|
+
for (let i = 0; i < K; i++) {
|
|
291
|
+
const x = vals[i].x;
|
|
292
|
+
const y = vals[i].y;
|
|
293
|
+
if (x < minX)
|
|
294
|
+
minX = x;
|
|
295
|
+
if (x > maxX)
|
|
296
|
+
maxX = x;
|
|
297
|
+
if (y < minY)
|
|
298
|
+
minY = y;
|
|
299
|
+
if (y > maxY)
|
|
300
|
+
maxY = y;
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
cx: (minX + maxX) / 2,
|
|
304
|
+
cy: (minY + maxY) / 2,
|
|
305
|
+
sx: maxX - minX,
|
|
306
|
+
sy: maxY - minY,
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
const center = Vec.lens(points, (vals) => {
|
|
310
|
+
const b = computeBox(vals);
|
|
311
|
+
return { x: b.cx, y: b.cy };
|
|
312
|
+
}, (target, vals) => {
|
|
313
|
+
const b = computeBox(vals);
|
|
314
|
+
const dx = target.x - b.cx;
|
|
315
|
+
const dy = target.y - b.cy;
|
|
316
|
+
const out = new Array(K);
|
|
317
|
+
for (let i = 0; i < K; i++)
|
|
318
|
+
out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
|
|
319
|
+
return out;
|
|
320
|
+
});
|
|
321
|
+
const refreshFracs = (fracs, vals) => {
|
|
322
|
+
const b = computeBox(vals);
|
|
323
|
+
const hx = b.sx > 1e-12 ? b.sx / 2 : 0;
|
|
324
|
+
const hy = b.sy > 1e-12 ? b.sy / 2 : 0;
|
|
325
|
+
return fracs.map((f, i) => ({
|
|
326
|
+
x: hx > 0 ? (vals[i].x - b.cx) / hx : f.x,
|
|
327
|
+
y: hy > 0 ? (vals[i].y - b.cy) / hy : f.y,
|
|
328
|
+
}));
|
|
329
|
+
};
|
|
330
|
+
const size = Vec.lens(points, {
|
|
331
|
+
init: (vals) => {
|
|
332
|
+
const b = computeBox(vals);
|
|
333
|
+
const halfX0 = b.sx > 1e-12 ? b.sx / 2 : 1;
|
|
334
|
+
const halfY0 = b.sy > 1e-12 ? b.sy / 2 : 1;
|
|
335
|
+
return {
|
|
336
|
+
fracs: vals.map(v => ({
|
|
337
|
+
x: b.sx > 1e-12 ? (v.x - b.cx) / halfX0 : 0,
|
|
338
|
+
y: b.sy > 1e-12 ? (v.y - b.cy) / halfY0 : 0,
|
|
339
|
+
})),
|
|
340
|
+
};
|
|
341
|
+
},
|
|
342
|
+
step: (vals, c) => ({ fracs: refreshFracs(c.fracs, vals) }),
|
|
343
|
+
fwd: (vals) => {
|
|
344
|
+
const b = computeBox(vals);
|
|
345
|
+
return { x: b.sx, y: b.sy };
|
|
346
|
+
},
|
|
347
|
+
bwd: (target, vals, c) => {
|
|
348
|
+
const b = computeBox(vals);
|
|
349
|
+
const halfTx = target.x / 2;
|
|
350
|
+
const halfTy = target.y / 2;
|
|
351
|
+
const out = c.fracs.map(f => ({ x: b.cx + f.x * halfTx, y: b.cy + f.y * halfTy }));
|
|
352
|
+
return { updates: out, complement: c };
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
return { center, size };
|
|
356
|
+
}
|
|
357
|
+
// procrustesJacobianLens — comparison point.
|
|
358
|
+
//
|
|
359
|
+
// Same forward map as `procrustesLens` but with the generic Jacobian-LSQ
|
|
360
|
+
// bwd, to quantify the numerical path vs. the closed-form one.
|
|
361
|
+
export function procrustesJacobianLens(points) {
|
|
362
|
+
const K = points.length;
|
|
363
|
+
if (K < 2)
|
|
364
|
+
throw new Error("procrustesJacobianLens: need ≥ 2 points");
|
|
365
|
+
// Flatten K Vecs into 2K scalar field lenses.
|
|
366
|
+
const xs = [];
|
|
367
|
+
const ys = [];
|
|
368
|
+
for (const p of points) {
|
|
369
|
+
xs.push(Num.lens([p], v => v[0].x, (t, v) => [{ x: t, y: v[0].y }]));
|
|
370
|
+
ys.push(Num.lens([p], v => v[0].y, (t, v) => [{ x: v[0].x, y: t }]));
|
|
371
|
+
}
|
|
372
|
+
// factorLens wants a flat input array.
|
|
373
|
+
const flat = [];
|
|
374
|
+
for (let i = 0; i < K; i++) {
|
|
375
|
+
flat.push(xs[i], ys[i]);
|
|
376
|
+
}
|
|
377
|
+
// Indexing helpers
|
|
378
|
+
const xAt = (a, i) => a[2 * i];
|
|
379
|
+
const yAt = (a, i) => a[2 * i + 1];
|
|
380
|
+
const fwdCx = (a) => {
|
|
381
|
+
let s = 0;
|
|
382
|
+
for (let i = 0; i < K; i++)
|
|
383
|
+
s += xAt(a, i);
|
|
384
|
+
return s / K;
|
|
385
|
+
};
|
|
386
|
+
const fwdCy = (a) => {
|
|
387
|
+
let s = 0;
|
|
388
|
+
for (let i = 0; i < K; i++)
|
|
389
|
+
s += yAt(a, i);
|
|
390
|
+
return s / K;
|
|
391
|
+
};
|
|
392
|
+
const fwdRot = (a) => {
|
|
393
|
+
return Math.atan2(yAt(a, 0) - fwdCy(a), xAt(a, 0) - fwdCx(a));
|
|
394
|
+
};
|
|
395
|
+
const fwdScale = (a) => {
|
|
396
|
+
return Math.hypot(xAt(a, 0) - fwdCx(a), yAt(a, 0) - fwdCy(a));
|
|
397
|
+
};
|
|
398
|
+
const [centroidX, centroidY, rotation, scale] = factorLens(flat, [fwdCx, fwdCy, fwdRot, fwdScale], { damping: 1e-4 });
|
|
399
|
+
return { centroidX, centroidY, rotation, scale };
|
|
400
|
+
}
|
|
401
|
+
export function bundleLens(pose, rotateAbout) {
|
|
402
|
+
const position = Vec.lens([pose], (v) => ({ x: v[0].x, y: v[0].y }), (target, v) => [{ ...v[0], x: target.x, y: target.y }]);
|
|
403
|
+
const rotation = Num.lens([pose], (v) => v[0].theta, (target, v) => {
|
|
404
|
+
const cur = v[0];
|
|
405
|
+
const dθ = target - cur.theta;
|
|
406
|
+
const cos = Math.cos(dθ);
|
|
407
|
+
const sin = Math.sin(dθ);
|
|
408
|
+
const rx = cur.x - rotateAbout.x;
|
|
409
|
+
const ry = cur.y - rotateAbout.y;
|
|
410
|
+
return [
|
|
411
|
+
{
|
|
412
|
+
x: rotateAbout.x + cos * rx - sin * ry,
|
|
413
|
+
y: rotateAbout.y + sin * rx + cos * ry,
|
|
414
|
+
theta: target,
|
|
415
|
+
},
|
|
416
|
+
];
|
|
417
|
+
});
|
|
418
|
+
return { position, rotation };
|
|
419
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { bestFitCircleLens, bestFitLineLens, pcaLens, procrustesViaBuildingBlocks, rigidTranslate, rotateAbout, scaleAbout, scaleAboutXY, totalLens, } from "./closed-form-policies.js";
|
|
2
|
+
export { bezierGestaltLens, crossfade, meanColor, meanOf, mix, paletteLens, rigidTranslateOf, select, spreadOf, timeSeriesLens, } from "./domain-aggregates.js";
|
|
3
|
+
export { bboxLens, bundleLens, type FactorLensOpts, factorLens, meanDiffLens, procrustesJacobianLens, procrustesLens, } from "./factor-lens.js";
|
|
4
|
+
export { type ContinuousOpts, continuous, type RememberOpts, remember, } from "./memory.js";
|
|
5
|
+
export { bundle, type FactorOpts, type FactorResult, factor, factorTuple, type OutputSpec, type PackedInput, procrustesTyped, } from "./typed-factor.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// core/lenses/index.ts — N→M and 1→M bidirectional lens primitives.
|
|
2
|
+
//
|
|
3
|
+
// Three layers:
|
|
4
|
+
// 1. NUMERICAL — `factor`, `factorTuple`, `bundle`: generic
|
|
5
|
+
// Jacobian-LSQ, the escape hatch when no closed-form policy fits.
|
|
6
|
+
// 2. CLOSED-FORM POLICIES — `rigidTranslate`, `rotateAbout`,
|
|
7
|
+
// `scaleAbout`, `scaleAboutXY`: exact group-action primitives.
|
|
8
|
+
// 3. DECOMPOSITIONS — `procrustesLens`, `bboxLens`, `bestFitLine`,
|
|
9
|
+
// etc.: composed M-output views over the policies and aggregates.
|
|
10
|
+
//
|
|
11
|
+
// See BIDIRECTIONAL-LENSES.md for the engine substrate.
|
|
12
|
+
export { bestFitCircleLens, bestFitLineLens, pcaLens, procrustesViaBuildingBlocks, rigidTranslate, rotateAbout, scaleAbout, scaleAboutXY, totalLens, } from "./closed-form-policies.js";
|
|
13
|
+
export { bezierGestaltLens, crossfade, meanColor, meanOf, mix, paletteLens, rigidTranslateOf, select, spreadOf, timeSeriesLens, } from "./domain-aggregates.js";
|
|
14
|
+
export { bboxLens, bundleLens, factorLens, meanDiffLens, procrustesJacobianLens, procrustesLens, } from "./factor-lens.js";
|
|
15
|
+
export { continuous, remember, } from "./memory.js";
|
|
16
|
+
export { bundle, factor, factorTuple, procrustesTyped, } from "./typed-factor.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Cell, Num, type Traits, type Writable } from "../index.js";
|
|
2
|
+
/** Options for {@link remember}. `anchor` is the fixed point sources scale
|
|
3
|
+
* about (a pivot, or the live centroid); `feature` is the writable scalar
|
|
4
|
+
* (a radius, mean distance, or sum). */
|
|
5
|
+
export interface RememberOpts<T> {
|
|
6
|
+
/** Point sources scale about — a constant pivot or a derived centroid. */
|
|
7
|
+
anchor: (vals: readonly T[]) => T;
|
|
8
|
+
/** The scalar view (the forward): radius, mean radius, spread, sum, … */
|
|
9
|
+
feature: (vals: readonly T[], anchor: T) => number;
|
|
10
|
+
/** Feature is non-negative ⇒ a same-magnitude write (e.g. `-r`) is a
|
|
11
|
+
* no-op (PutGet on a magnitude). Default `true`; set `false` for a
|
|
12
|
+
* signed total. */
|
|
13
|
+
magnitude?: boolean;
|
|
14
|
+
/** Degeneracy threshold; below it the cluster is "collapsed" and the
|
|
15
|
+
* stored shape drives reinflation. Default `1e-9`. */
|
|
16
|
+
eps?: number;
|
|
17
|
+
/** Normalized shape to seed when the feature is degenerate at init and
|
|
18
|
+
* no prior good shape exists. Default: zero deviations (stays
|
|
19
|
+
* collapsed). `totalLens` passes uniform `1/K` for an even split. */
|
|
20
|
+
seed?: (vals: readonly T[]) => T[];
|
|
21
|
+
}
|
|
22
|
+
/** Scalar shape-memory lens. Reads `feature(sources)`; writing it scales
|
|
23
|
+
* the cluster about `anchor` so the new feature matches, reinflating the
|
|
24
|
+
* remembered shape when the cluster has collapsed onto the anchor. The
|
|
25
|
+
* complement is the per-source deviation normalized by the feature. */
|
|
26
|
+
export declare function remember<T, S extends Cell<T> & Traits<T, "linear">>(sources: readonly Writable<S>[], opts: RememberOpts<T>): Writable<Num>;
|
|
27
|
+
/** Options for {@link continuous}. `raw` reads the cyclic value (mod
|
|
28
|
+
* `period`) and whether it's defined; `apply` realizes an unwrapped
|
|
29
|
+
* target back onto the sources given the current unwrapped reading. */
|
|
30
|
+
export interface ContinuousOpts<T> {
|
|
31
|
+
/** Cycle length: `2π` for a full angle, `π` for an axis (sign-free). */
|
|
32
|
+
period: number;
|
|
33
|
+
/** Raw cyclic reading. `defined: false` (e.g. a collapsed cloud has no
|
|
34
|
+
* axis) holds the last emitted value and freezes the sources. */
|
|
35
|
+
raw: (vals: readonly T[]) => {
|
|
36
|
+
value: number;
|
|
37
|
+
defined: boolean;
|
|
38
|
+
};
|
|
39
|
+
/** Realize `target` (already unwrapped, absolute) onto the sources,
|
|
40
|
+
* given the `current` unwrapped reading (for a delta). */
|
|
41
|
+
apply: (target: number, vals: readonly T[], current: number) => readonly (T | undefined)[];
|
|
42
|
+
}
|
|
43
|
+
/** Continuous (winding-aware) lens over a cyclic reading. The complement
|
|
44
|
+
* tracks the last emitted value and unwraps each raw reading to the
|
|
45
|
+
* nearest representative modulo `period`, so the view follows the source
|
|
46
|
+
* across branch cuts instead of jumping a full period. */
|
|
47
|
+
export declare function continuous<T>(sources: readonly Writable<Cell<T>>[], opts: ContinuousOpts<T>): Writable<Num>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// memory.ts — path-dependent (stateful-complement) lens combinators.
|
|
2
|
+
//
|
|
3
|
+
// Two backward directions are not functions of the current source alone;
|
|
4
|
+
// they depend on where the value has BEEN. Both were hand-rolled per site
|
|
5
|
+
// across the closed-form-policy and aggregate lenses, each with its own
|
|
6
|
+
// degeneracy threshold. They factor into two combinators:
|
|
7
|
+
//
|
|
8
|
+
// remember — a magnitude/total view of a cluster: sources live at
|
|
9
|
+
// `anchorᵢ + shapeᵢ · scale`, `scale` is the writable
|
|
10
|
+
// scalar. The complement holds the normalized shape so a
|
|
11
|
+
// collapse to the anchor (scale → 0) reinflates it. Covers
|
|
12
|
+
// scaleAbout, bestFitCircle.radius, spreadOf, totalLens.
|
|
13
|
+
// continuous — a cyclic (mod-`period`) view lifted to its universal
|
|
14
|
+
// cover: the complement tracks the last emitted value and
|
|
15
|
+
// unwraps the raw reading to the nearest sheet, so the
|
|
16
|
+
// view never tears at a branch cut. Covers the eigenvector
|
|
17
|
+
// angle of bestFitLine; the primitive behind winding.
|
|
18
|
+
import { Num } from "../index.js";
|
|
19
|
+
/** Scalar shape-memory lens. Reads `feature(sources)`; writing it scales
|
|
20
|
+
* the cluster about `anchor` so the new feature matches, reinflating the
|
|
21
|
+
* remembered shape when the cluster has collapsed onto the anchor. The
|
|
22
|
+
* complement is the per-source deviation normalized by the feature. */
|
|
23
|
+
export function remember(sources, opts) {
|
|
24
|
+
const K = sources.length;
|
|
25
|
+
if (K < 1)
|
|
26
|
+
throw new Error("remember: need ≥ 1 source");
|
|
27
|
+
// biome-ignore lint/suspicious/noExplicitAny: dynamic class/trait lookup
|
|
28
|
+
const Cls = sources[0].constructor;
|
|
29
|
+
const lin = Cls.traits?.linear;
|
|
30
|
+
if (!lin)
|
|
31
|
+
throw new Error(`remember: ${Cls.name ?? "?"} has no traits.linear`);
|
|
32
|
+
const { anchor, feature, seed } = opts;
|
|
33
|
+
const magnitude = opts.magnitude ?? true;
|
|
34
|
+
const eps = opts.eps ?? 1e-9;
|
|
35
|
+
const zero = (vals, a) => vals.map(v => lin.scale(lin.sub(v, a), 0));
|
|
36
|
+
// Normalized deviations `(vᵢ − anchor) / feature`, refreshed whole while
|
|
37
|
+
// the feature is non-degenerate; below `eps` the prior shape (or the
|
|
38
|
+
// seed) is held so a collapse reinflates it.
|
|
39
|
+
const shapeOf = (vals, a, f, prev) => {
|
|
40
|
+
if (f <= eps)
|
|
41
|
+
return prev ?? seed?.(vals) ?? zero(vals, a);
|
|
42
|
+
const inv = 1 / f;
|
|
43
|
+
return vals.map(v => lin.scale(lin.sub(v, a), inv));
|
|
44
|
+
};
|
|
45
|
+
// biome-ignore lint/suspicious/noExplicitAny: spec is checked structurally
|
|
46
|
+
return Num.lens(sources, {
|
|
47
|
+
init: (vals) => {
|
|
48
|
+
const a = anchor(vals);
|
|
49
|
+
return { shape: shapeOf(vals, a, feature(vals, a), null) };
|
|
50
|
+
},
|
|
51
|
+
step: (vals, c) => {
|
|
52
|
+
const a = anchor(vals);
|
|
53
|
+
return { shape: shapeOf(vals, a, feature(vals, a), c.shape) };
|
|
54
|
+
},
|
|
55
|
+
fwd: (vals) => feature(vals, anchor(vals)),
|
|
56
|
+
bwd: (target, vals, c) => {
|
|
57
|
+
const a = anchor(vals);
|
|
58
|
+
const f = feature(vals, a);
|
|
59
|
+
// Magnitude is lossy (|−f| = f): a same-magnitude target re-projects
|
|
60
|
+
// to the current feature, so the cluster is left put.
|
|
61
|
+
if (magnitude && Math.abs(target) === f) {
|
|
62
|
+
return { updates: vals.map(() => undefined), complement: c };
|
|
63
|
+
}
|
|
64
|
+
if (f > eps) {
|
|
65
|
+
const k = target / f;
|
|
66
|
+
return { updates: vals.map(v => lin.add(a, lin.scale(lin.sub(v, a), k))), complement: c };
|
|
67
|
+
}
|
|
68
|
+
return { updates: c.shape.map(s => lin.add(a, lin.scale(s, target))), complement: c };
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Continuous (winding-aware) lens over a cyclic reading. The complement
|
|
73
|
+
* tracks the last emitted value and unwraps each raw reading to the
|
|
74
|
+
* nearest representative modulo `period`, so the view follows the source
|
|
75
|
+
* across branch cuts instead of jumping a full period. */
|
|
76
|
+
export function continuous(sources, opts) {
|
|
77
|
+
const { period, raw, apply } = opts;
|
|
78
|
+
const wrap = (x) => x - period * Math.round(x / period);
|
|
79
|
+
const unwrap = (rawv, prev) => prev + wrap(rawv - prev);
|
|
80
|
+
// biome-ignore lint/suspicious/noExplicitAny: spec is checked structurally
|
|
81
|
+
return Num.lens(sources, {
|
|
82
|
+
init: (vals) => {
|
|
83
|
+
const r = raw(vals);
|
|
84
|
+
return { prev: r.defined ? r.value : 0 };
|
|
85
|
+
},
|
|
86
|
+
step: (vals, c) => {
|
|
87
|
+
const r = raw(vals);
|
|
88
|
+
return r.defined ? { prev: unwrap(r.value, c.prev) } : c;
|
|
89
|
+
},
|
|
90
|
+
fwd: (vals, c) => {
|
|
91
|
+
const r = raw(vals);
|
|
92
|
+
return r.defined ? unwrap(r.value, c.prev) : c.prev;
|
|
93
|
+
},
|
|
94
|
+
bwd: (target, vals, c) => {
|
|
95
|
+
const r = raw(vals);
|
|
96
|
+
if (!r.defined)
|
|
97
|
+
return { updates: vals.map(() => undefined), complement: { prev: target } };
|
|
98
|
+
const current = unwrap(r.value, c.prev);
|
|
99
|
+
return { updates: apply(target, vals, current), complement: { prev: target } };
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Cell, type Inner, Num, type Read, type Traits, Vec, type Writable } from "../index.js";
|
|
2
|
+
/** Input cell: writable cell whose value class declares the `pack`
|
|
3
|
+
* trait. Vec, Num, Pose, Box, Color, Range all satisfy this. */
|
|
4
|
+
export type PackedInput<T = any> = Writable<Read<T> & Traits<T, "pack">>;
|
|
5
|
+
/** Output specification: a target class + a fwd from typed inputs to
|
|
6
|
+
* the value the class wraps. Optional analytical Jacobian skips FD. */
|
|
7
|
+
export interface OutputSpec<C extends new (...args: never[]) => Cell<any>> {
|
|
8
|
+
Cls: C;
|
|
9
|
+
fwd: (inputs: ReadonlyArray<any>) => Inner<InstanceType<C>>;
|
|
10
|
+
/** Optional analytical Jacobian. Returns dim(Cls) rows, each of
|
|
11
|
+
* length `sum(input pack dims)`. If supplied for ALL outputs, FD
|
|
12
|
+
* is skipped entirely → faster AND exact (no eps drift). */
|
|
13
|
+
jacobian?: (inputs: ReadonlyArray<any>) => readonly (readonly number[])[];
|
|
14
|
+
}
|
|
15
|
+
/** Result type: writable cell per output key, typed by the spec's Cls. */
|
|
16
|
+
export type FactorResult<O extends Record<string, OutputSpec<any>>> = {
|
|
17
|
+
[K in keyof O]: Writable<InstanceType<O[K]["Cls"]>>;
|
|
18
|
+
};
|
|
19
|
+
export interface FactorOpts {
|
|
20
|
+
/** Per-input mobility weights. 0 = pinned. Defaults to 1 for all. */
|
|
21
|
+
inputWeights?: readonly number[];
|
|
22
|
+
/** Levenberg-Marquardt damping. Default 1e-6. */
|
|
23
|
+
damping?: number;
|
|
24
|
+
/** Finite-difference epsilon. Default 1e-5. */
|
|
25
|
+
eps?: number;
|
|
26
|
+
/** Auto-iterate the bwd until the written channel's reading is
|
|
27
|
+
* within `tol` of target (or `maxIters` exhausted). Cheap when
|
|
28
|
+
* forwards are linear (1 iter); needed for non-linear forwards
|
|
29
|
+
* to land exactly without user-side loops. Default `false`. */
|
|
30
|
+
converge?: boolean;
|
|
31
|
+
/** Max iters when `converge: true`. Default 10. */
|
|
32
|
+
maxIters?: number;
|
|
33
|
+
/** Convergence tolerance (per-channel L2). Default 1e-4. */
|
|
34
|
+
tol?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare function factor<O extends Record<string, OutputSpec<any>>>(inputs: readonly PackedInput[], outputs: O, opts?: FactorOpts): FactorResult<O>;
|
|
37
|
+
export declare function factorTuple<T extends readonly OutputSpec<any>[]>(inputs: readonly PackedInput[], outputs: readonly [...T], opts?: FactorOpts): {
|
|
38
|
+
[K in keyof T]: Writable<InstanceType<T[K]["Cls"]>>;
|
|
39
|
+
};
|
|
40
|
+
export declare function bundle<T, O extends Record<string, OutputSpec<any>>>(source: Writable<Read<T> & Traits<T, "pack">>, views: O, opts?: FactorOpts): FactorResult<O>;
|
|
41
|
+
export declare function procrustesTyped(points: readonly PackedInput<Inner<Vec>>[]): {
|
|
42
|
+
centroid: Writable<Vec>;
|
|
43
|
+
rotation: Writable<Num>;
|
|
44
|
+
scale: Writable<Num>;
|
|
45
|
+
};
|