@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 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 (raw unit counts)
38
- model.rowConnectionCounts // Float64Array nRows × nConnections (raw row counts)
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) when using tensors
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.4",
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.4"
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 its element-wise transform, matching the webtool's
194
- * server.js mapping to R weight.by functions:
195
- * 'sqrt' → Math.sqrt (R "sqrt")
196
- * 'log' → Math.log1p (R "log1p", i.e. log(x+1); guards log(0))
197
- * 'product' → identity (raw non-binary counts; R's "product" string
198
- * is not a function, so no transform)
199
- * A falsy value or 'binary' returns null (binary accumulation, no transform).
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 {((x:number)=>number)|null}
201
+ * @returns {'sqrt'|'log1p'|'product'|null}
202
202
  */
203
- function weightModelTransform(name) {
203
+ function weightModelName(name) {
204
204
  switch (name) {
205
- case 'sqrt': return Math.sqrt;
206
- case 'log': return Math.log1p;
207
- case 'product': return (x) => x;
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-line, before the unit sum)
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, the transform is applied to each row's counts
357
- // inside accumulateTensor, not to the unit-summed network.
358
- // `product` is the non-binarized row counts themselves (identity);
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, effBinary, weightFn
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
- * @param {((x:number)=>number)|null} [weightFn=null] per-line weight transform
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, weightFn = null) {
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 exactly as
271
- // tma does in R: fold to the upper triangle and binarize per row
272
- // (unordered), or keep the directed row (ordered). A weight model
273
- // (R's weight.by) replaces the binarize step with its transform,
274
- // applied per line before the per-unit sum. Unweighted rows go
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
- for (let i = 0; i < rcc.rows; i++) {
279
- const one = rcc.data.slice(i * rcc.cols, (i + 1) * rcc.cols);
280
- const rowVec = weightFn
281
- ? aggregateRowConnectionsWeighted(one, 1, nCodes, ordered, weightFn)
282
- : qe.aggregate_row_connections(one, 1, rcc.cols, nCodes, ordered, binary);
283
- const rowOff = rowIndices[localRows[i]] * nConnections;
284
- const unitOff = unit * nConnections;
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
- rowConnectionCounts[rowOff + c] = rowVec[c];
287
- networks[unitOff + c] += rowVec[c];
242
+ const v = fin.data[finOff + c];
243
+ rowConnectionCounts[rowOff + c] = v;
244
+ networks[unitOff + c] += v;
288
245
  }
289
246
  }
290
247
  }