@qe-libs/rena-wasm 0.1.4 → 0.1.5
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/README.md +27 -3
- package/package.json +2 -2
- package/src/index.js +27 -25
- package/src/tensor.js +24 -67
package/README.md
CHANGED
|
@@ -34,8 +34,8 @@ const model = ena.fit(rows, {
|
|
|
34
34
|
|
|
35
35
|
model.model.centroids // Float64Array nUnits × dims
|
|
36
36
|
model.lineWeights // Float64Array nUnits × nConnections (normed)
|
|
37
|
-
model.connectionCounts // Float64Array nUnits × nConnections (
|
|
38
|
-
model.rowConnectionCounts // Float64Array nRows × nConnections (
|
|
37
|
+
model.connectionCounts // Float64Array nUnits × nConnections (unit counts, before normalisation)
|
|
38
|
+
model.rowConnectionCounts // Float64Array nRows × nConnections (each row's counts after binarize / weight; rows sum to connectionCounts)
|
|
39
39
|
model.rotation.nodes // Float64Array nCodes × dims
|
|
40
40
|
model.connectionNames // ['Data & Technical.Constraints', ...]
|
|
41
41
|
model.model.unitLabels // ['UserName1_ConditionA', ...]
|
|
@@ -67,6 +67,30 @@ const model = ena.fit(rows, {
|
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
70
|
+
## Weight Models
|
|
71
|
+
|
|
72
|
+
`weightModel` (= R's `weight.by` / tma's `weight_by`) is applied to each line's
|
|
73
|
+
co-occurrence counts **before** they are summed into the unit network, by
|
|
74
|
+
libqe's shared `finalize_row_connections` kernel — the same stage and results
|
|
75
|
+
as rENA's `ena.accumulate.data()` and `tma::accumulate()`.
|
|
76
|
+
|
|
77
|
+
| `weightModel` | Per line |
|
|
78
|
+
|---|---|
|
|
79
|
+
| — (default) | binary: each positive count becomes 1 (`binary: false` keeps raw counts) |
|
|
80
|
+
| `'product'` | the raw, non-binarized counts |
|
|
81
|
+
| `'sqrt'` | square root of each line's count |
|
|
82
|
+
| `'log'` (alias `'log1p'`) | `log(1 + x)` of each line's count |
|
|
83
|
+
|
|
84
|
+
For ordered (directed) networks the weight is applied to each directed cell,
|
|
85
|
+
and the default keeps the raw directed counts. `weightModel` works in `fit()`,
|
|
86
|
+
`accumulate()` and `tuneWindowSize()`.
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
const model = ena.fit(rows, { codes, units, conversations, window: 4, weightModel: 'sqrt' });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
70
94
|
## Accumulation Only
|
|
71
95
|
|
|
72
96
|
Returns raw (un-normalised) network vectors without running the full pipeline.
|
|
@@ -92,7 +116,7 @@ columns in the data.
|
|
|
92
116
|
// dims = [nRoleValues, 2] → Teacher uses window=4, Student uses window=2.
|
|
93
117
|
const model = ena.fit(rows, {
|
|
94
118
|
codes, units, conversations,
|
|
95
|
-
ordered: true, // directed (n² connections)
|
|
119
|
+
ordered: true, // directed (n² connections); also works without a tensor
|
|
96
120
|
tensor: {
|
|
97
121
|
dims: [2, 2], // [nRoleValues=2, weight/window=2]
|
|
98
122
|
dimsSender: [0], // axis 0 is a sender factor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qe-libs/rena-wasm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "JavaScript/WebAssembly ENA pipeline — thin orchestration layer over @qe-libs/libqe-wasm",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"resolver": "<rootDir>/jest-resolver.cjs"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@qe-libs/libqe-wasm": "^0.1.
|
|
34
|
+
"@qe-libs/libqe-wasm": "^0.1.5"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"jest": "^29.0.0"
|
package/src/index.js
CHANGED
|
@@ -190,21 +190,22 @@ class ENAModel {
|
|
|
190
190
|
// ── weight models (= R's `weight.by`) ────────────────────────────────────────
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
* Map a weight-model name to
|
|
194
|
-
*
|
|
195
|
-
* 'sqrt'
|
|
196
|
-
* 'log'
|
|
197
|
-
* 'product'
|
|
198
|
-
*
|
|
199
|
-
*
|
|
193
|
+
* Map a rena-wasm weight-model name to the libqe weight model applied per line
|
|
194
|
+
* (before the per-unit sum) by libqe's finalize_row_connections:
|
|
195
|
+
* 'sqrt' → 'sqrt' (R weight.by = sqrt)
|
|
196
|
+
* 'log' | 'log1p' → 'log1p' (log(x+1); guards log(0))
|
|
197
|
+
* 'product' → 'product' (the raw, non-binarized line counts)
|
|
198
|
+
* A falsy value, 'binary' or an unknown name returns null (binary / `binary`
|
|
199
|
+
* flag accumulation, no weight model).
|
|
200
200
|
* @param {string|false|undefined} name
|
|
201
|
-
* @returns {
|
|
201
|
+
* @returns {'sqrt'|'log1p'|'product'|null}
|
|
202
202
|
*/
|
|
203
|
-
function
|
|
203
|
+
function weightModelName(name) {
|
|
204
204
|
switch (name) {
|
|
205
|
-
case 'sqrt': return
|
|
206
|
-
case 'log':
|
|
207
|
-
case '
|
|
205
|
+
case 'sqrt': return 'sqrt';
|
|
206
|
+
case 'log':
|
|
207
|
+
case 'log1p': return 'log1p';
|
|
208
|
+
case 'product': return 'product';
|
|
208
209
|
default: return null; // binary / off / unknown
|
|
209
210
|
}
|
|
210
211
|
}
|
|
@@ -318,7 +319,7 @@ export default async function loadENA() {
|
|
|
318
319
|
* @param {boolean} [opts.binary=true] - Binarise each line's co-occurrences (unordered; ignored when weightModel is set)
|
|
319
320
|
* @param {boolean} [opts.ordered=false] - Directed networks
|
|
320
321
|
* @param {object} [opts.tensor] - Context tensor definition (overrides window)
|
|
321
|
-
* @param {string} [opts.weightModel] - 'product' | 'sqrt' | 'log' (per
|
|
322
|
+
* @param {string} [opts.weightModel] - 'product' | 'sqrt' | 'log' (alias 'log1p'); per line, before the unit sum
|
|
322
323
|
* @param {string} [opts.rotation='svd'] - 'svd', 'mean', or 'generalized'
|
|
323
324
|
* @param {number[]} [opts.groupA] - Unit indices for means rotation group A
|
|
324
325
|
* @param {number[]} [opts.groupB] - Unit indices for means rotation group B
|
|
@@ -353,12 +354,9 @@ export default async function loadENA() {
|
|
|
353
354
|
// "product" co-occurrence counts) BEFORE summing per unit, mirroring
|
|
354
355
|
// accumulate.data.R (lapply(.SD, weight.by) over the per-line
|
|
355
356
|
// co-occurrence table, then per-unit aggregation). Because
|
|
356
|
-
// sqrt(Σ) ≠ Σsqrt,
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
// any weight model therefore forces non-binary accumulation.
|
|
360
|
-
const weightFn = weightModelTransform(weightModel);
|
|
361
|
-
const effBinary = weightFn ? false : binary;
|
|
357
|
+
// sqrt(Σ) ≠ Σsqrt, libqe applies it to each row's counts inside
|
|
358
|
+
// accumulateTensor, not to the unit-summed network.
|
|
359
|
+
const weight = weightModelName(weightModel);
|
|
362
360
|
|
|
363
361
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
364
362
|
unitOf, convoGroups, metaData } =
|
|
@@ -369,7 +367,7 @@ export default async function loadENA() {
|
|
|
369
367
|
const { networks: rawNetworks, rowConnectionCounts } = accumulateTensor(
|
|
370
368
|
qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
371
369
|
unitOf, convoGroups, tensorDef ?? defaultTensor(windowSize),
|
|
372
|
-
ordered,
|
|
370
|
+
ordered, binary, weight
|
|
373
371
|
);
|
|
374
372
|
const nConnections = ordered ? nCodes * nCodes : qe.choose_two(nCodes);
|
|
375
373
|
|
|
@@ -412,7 +410,7 @@ export default async function loadENA() {
|
|
|
412
410
|
* Returns raw (un-normalised) network vectors.
|
|
413
411
|
*
|
|
414
412
|
* @param {Object[]} rows
|
|
415
|
-
* @param {object} opts - codes, units, conversations, window, binary, ordered, tensor
|
|
413
|
+
* @param {object} opts - codes, units, conversations, window, binary, ordered, tensor, weightModel
|
|
416
414
|
* @returns {{
|
|
417
415
|
* connectionCounts: Float64Array,
|
|
418
416
|
* rowConnectionCounts: Float64Array | null,
|
|
@@ -432,6 +430,7 @@ export default async function loadENA() {
|
|
|
432
430
|
ordered = false,
|
|
433
431
|
tensor: tensorDef,
|
|
434
432
|
codeMask,
|
|
433
|
+
weightModel,
|
|
435
434
|
} = opts;
|
|
436
435
|
|
|
437
436
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
@@ -441,7 +440,7 @@ export default async function loadENA() {
|
|
|
441
440
|
const { networks, rowConnectionCounts } = accumulateTensor(
|
|
442
441
|
qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
443
442
|
unitOf, convoGroups, tensorDef ?? defaultTensor(windowSize),
|
|
444
|
-
ordered, binary
|
|
443
|
+
ordered, binary, weightModelName(weightModel)
|
|
445
444
|
);
|
|
446
445
|
const nConnections = ordered ? nCodes * nCodes : qe.choose_two(nCodes);
|
|
447
446
|
|
|
@@ -481,7 +480,8 @@ export default async function loadENA() {
|
|
|
481
480
|
// Retained so tuneWindowSize() can rebuild at other window sizes
|
|
482
481
|
// (= R's ENAAccumulation$`_function.call`).
|
|
483
482
|
_call: { rows, codes, units, conversations,
|
|
484
|
-
window: windowSize, binary, ordered, tensor: tensorDef
|
|
483
|
+
window: windowSize, binary, ordered, tensor: tensorDef,
|
|
484
|
+
weightModel },
|
|
485
485
|
};
|
|
486
486
|
},
|
|
487
487
|
|
|
@@ -519,7 +519,7 @@ export default async function loadENA() {
|
|
|
519
519
|
'maxSize must be greater than minSize to compare windows.'
|
|
520
520
|
);
|
|
521
521
|
|
|
522
|
-
const { rows, codes, units, conversations, binary, ordered } = call;
|
|
522
|
+
const { rows, codes, units, conversations, binary, ordered, weightModel } = call;
|
|
523
523
|
|
|
524
524
|
// Parse once; only the window size changes between iterations.
|
|
525
525
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
@@ -533,7 +533,8 @@ export default async function loadENA() {
|
|
|
533
533
|
for (const w of windowRange) {
|
|
534
534
|
const { networks: raw } = accumulateTensor(
|
|
535
535
|
qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
536
|
-
unitOf, convoGroups, defaultTensor(w), ordered, binary
|
|
536
|
+
unitOf, convoGroups, defaultTensor(w), ordered, binary,
|
|
537
|
+
weightModelName(weightModel)
|
|
537
538
|
);
|
|
538
539
|
const model = runPipeline(
|
|
539
540
|
qe, raw, nUnits, nConnections, codes,
|
|
@@ -560,6 +561,7 @@ export default async function loadENA() {
|
|
|
560
561
|
// 4. Rebuild the accumulation at the selected window size.
|
|
561
562
|
return api.accumulate(rows, {
|
|
562
563
|
codes, units, conversations, window: bestWindow, binary, ordered,
|
|
564
|
+
weightModel,
|
|
563
565
|
});
|
|
564
566
|
},
|
|
565
567
|
|
package/src/tensor.js
CHANGED
|
@@ -119,54 +119,6 @@ export function inferFactorLevels(rows, factors) {
|
|
|
119
119
|
return levels;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
* Weighted aggregation of a unit's per-response-row directed connection counts.
|
|
124
|
-
*
|
|
125
|
-
* Mirrors the kernel's aggregate_row_connections but applies a weight-model
|
|
126
|
-
* transform (R's weight.by) per LINE before the per-unit sum, so weight models
|
|
127
|
-
* behave identically on the tensor path and the windowed path. Non-binary by
|
|
128
|
-
* construction (the weight replaces the binarize step).
|
|
129
|
-
*
|
|
130
|
-
* unordered: fold each directed row (m + mᵀ, upper triangle, column-major —
|
|
131
|
-
* the same order as choose_two/connection_names), weight each
|
|
132
|
-
* folded cell, then sum across rows.
|
|
133
|
-
* ordered: weight each directed cell, then sum across rows.
|
|
134
|
-
*
|
|
135
|
-
* @param {Float64Array|number[]} data Row-major (nRccRows × nCodes²) counts.
|
|
136
|
-
* @param {number} nRccRows Number of response rows for this unit.
|
|
137
|
-
* @param {number} nCodes
|
|
138
|
-
* @param {boolean} ordered
|
|
139
|
-
* @param {(x:number)=>number} weightFn
|
|
140
|
-
* @returns {Float64Array} length nCodes² (ordered) or choose_two(nCodes).
|
|
141
|
-
*/
|
|
142
|
-
function aggregateRowConnectionsWeighted(data, nRccRows, nCodes, ordered, weightFn) {
|
|
143
|
-
const nSq = nCodes * nCodes;
|
|
144
|
-
if (ordered) {
|
|
145
|
-
const out = new Float64Array(nSq);
|
|
146
|
-
for (let r = 0; r < nRccRows; r++) {
|
|
147
|
-
const base = r * nSq;
|
|
148
|
-
for (let c = 0; c < nSq; c++) out[c] += weightFn(data[base + c]);
|
|
149
|
-
}
|
|
150
|
-
return out;
|
|
151
|
-
}
|
|
152
|
-
const nTri = (nCodes * (nCodes - 1)) / 2;
|
|
153
|
-
const out = new Float64Array(nTri);
|
|
154
|
-
for (let r = 0; r < nRccRows; r++) {
|
|
155
|
-
const base = r * nSq;
|
|
156
|
-
let k = 0;
|
|
157
|
-
for (let col = 1; col < nCodes; col++) {
|
|
158
|
-
for (let row = 0; row < col; row++) {
|
|
159
|
-
// fold: m(row,col) + m(col,row), column-major reshape of the row
|
|
160
|
-
const folded = data[base + col * nCodes + row] +
|
|
161
|
-
data[base + row * nCodes + col];
|
|
162
|
-
out[k] += weightFn(folded);
|
|
163
|
-
k++;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return out;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
122
|
/**
|
|
171
123
|
* Accumulate tensor networks for all units across all conversations.
|
|
172
124
|
*
|
|
@@ -184,8 +136,11 @@ function aggregateRowConnectionsWeighted(data, nRccRows, nCodes, ordered, weight
|
|
|
184
136
|
* @param {Map} convoGroups convoIdx → [rowIdx, ...]
|
|
185
137
|
* @param {object} tensorDef Tensor definition (see module docstring)
|
|
186
138
|
* @param {boolean} ordered true → directed (n²); false → undirected
|
|
187
|
-
* @param {boolean} [binary=true] binarize each folded row (unordered only)
|
|
188
|
-
*
|
|
139
|
+
* @param {boolean} [binary=true] binarize each folded row (unordered only);
|
|
140
|
+
* used when no weight model is given
|
|
141
|
+
* @param {string|null} [weight=null] libqe weight model applied per line before
|
|
142
|
+
* the unit sum: 'binary' | 'product' |
|
|
143
|
+
* 'sqrt' | 'log1p'
|
|
189
144
|
*
|
|
190
145
|
* @returns {{ networks: Float64Array, rowConnectionCounts: Float64Array }}
|
|
191
146
|
* networks nUnits × nConnections, row-major
|
|
@@ -195,7 +150,7 @@ function aggregateRowConnectionsWeighted(data, nRccRows, nCodes, ordered, weight
|
|
|
195
150
|
*/
|
|
196
151
|
export function accumulateTensor(qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
197
152
|
unitOf, convoGroups, tensorDef, ordered = false,
|
|
198
|
-
binary = true,
|
|
153
|
+
binary = true, weight = null) {
|
|
199
154
|
const {
|
|
200
155
|
dims,
|
|
201
156
|
dimsSender = [],
|
|
@@ -216,6 +171,9 @@ export function accumulateTensor(qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
|
216
171
|
const networks = new Float64Array(nUnits * nConnections);
|
|
217
172
|
const rowConnectionCounts = new Float64Array(nRows * nConnections);
|
|
218
173
|
|
|
174
|
+
// libqe weight argument: a weight-model name, or the legacy binary flag.
|
|
175
|
+
const kernelWeight = weight ?? binary;
|
|
176
|
+
|
|
219
177
|
const dimsArr = new Int32Array(dims);
|
|
220
178
|
const senderArr = new Int32Array(dimsSender);
|
|
221
179
|
const receiverArr = new Int32Array(dimsReceiver);
|
|
@@ -267,24 +225,23 @@ export function accumulateTensor(qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
|
267
225
|
true
|
|
268
226
|
);
|
|
269
227
|
|
|
270
|
-
// Finalise each raw per-response-row connection vector
|
|
271
|
-
// tma does in R: fold to the upper
|
|
272
|
-
// (unordered)
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
// through the libqe kernel so fold/binarize semantics are shared
|
|
276
|
-
// across every binding. Row i of rcc is local row localRows[i].
|
|
228
|
+
// Finalise each raw per-response-row connection vector with libqe's
|
|
229
|
+
// shared kernel, exactly as tma does in R: fold to the upper
|
|
230
|
+
// triangle (unordered) or keep the directed row (ordered), then
|
|
231
|
+
// apply the weight model per line (binary clamp, product, sqrt,
|
|
232
|
+
// log1p) before the per-unit sum. Row i is local row localRows[i].
|
|
277
233
|
const rcc = result.row_connection_counts;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const rowOff
|
|
284
|
-
const
|
|
234
|
+
const fin = qe.finalize_row_connections(
|
|
235
|
+
rcc.data, rcc.rows, rcc.cols, nCodes, ordered, kernelWeight
|
|
236
|
+
);
|
|
237
|
+
const unitOff = unit * nConnections;
|
|
238
|
+
for (let i = 0; i < fin.rows; i++) {
|
|
239
|
+
const rowOff = rowIndices[localRows[i]] * nConnections;
|
|
240
|
+
const finOff = i * fin.cols;
|
|
285
241
|
for (let c = 0; c < nConnections; c++) {
|
|
286
|
-
|
|
287
|
-
|
|
242
|
+
const v = fin.data[finOff + c];
|
|
243
|
+
rowConnectionCounts[rowOff + c] = v;
|
|
244
|
+
networks[unitOff + c] += v;
|
|
288
245
|
}
|
|
289
246
|
}
|
|
290
247
|
}
|