@qe-libs/rena-wasm 0.1.6 → 0.1.8
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 -1
- package/package.json +1 -1
- package/src/data.js +15 -4
- package/src/index.js +68 -23
- package/src/pipeline.js +7 -3
- package/src/tensor.js +4 -1
package/README.md
CHANGED
|
@@ -91,6 +91,26 @@ const model = ena.fit(rows, { codes, units, conversations, window: 4, weightMode
|
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
|
94
|
+
## Modeling a Subset of Units
|
|
95
|
+
|
|
96
|
+
`unitsUsed` (= R's `units.used`) lists the units to model, as unit keys (the
|
|
97
|
+
unit column values joined with `__`, as in `model.unitLabels`). Every row
|
|
98
|
+
stays in the data: rows of other units still count as context in the modeled
|
|
99
|
+
units' windows, but those units get no network and never reach sphere
|
|
100
|
+
normalization, centering or rotation. Omit it to model every unit.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
// Students' connections to what teachers said are kept; teachers aren't units.
|
|
104
|
+
const model = ena.fit(rows, { codes, units: ['Role', 'Name'], conversations,
|
|
105
|
+
window: 4, unitsUsed: studentKeys });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`unitsUsed` works in `fit()`, `accumulate()`, `tuneWindowSize()` and `pria()`.
|
|
109
|
+
This differs from dropping the other units' rows, which also removes them from
|
|
110
|
+
everyone's windows.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
94
114
|
## Reduced-Code Search (PRIA)
|
|
95
115
|
|
|
96
116
|
`pria()` finds the largest set of codes (up to `removeNum`, never leaving
|
|
@@ -136,7 +156,13 @@ columns in the data.
|
|
|
136
156
|
// dims = [nRoleValues, 2] → Teacher uses window=4, Student uses window=2.
|
|
137
157
|
const model = ena.fit(rows, {
|
|
138
158
|
codes, units, conversations,
|
|
139
|
-
ordered: true, // directed (n² connections); also works without a tensor
|
|
159
|
+
ordered: true, // directed (n² connections); also works without a tensor.
|
|
160
|
+
// Ordered models follow ona::model(): zero-network units are
|
|
161
|
+
// left out of the centring mean but shifted by it, nodes use
|
|
162
|
+
// directed positions, and points/nodes are centred on the origin.
|
|
163
|
+
// connectionNames / rotation.adjacencyKey list all n² directed
|
|
164
|
+
// connections as R names them: column j*n + i is
|
|
165
|
+
// "codes[i] & codes[j]", ground codes[i] → response codes[j].
|
|
140
166
|
tensor: {
|
|
141
167
|
dims: [2, 2], // [nRoleValues=2, weight/window=2]
|
|
142
168
|
dimsSender: [0], // axis 0 is a sender factor
|
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -24,6 +24,10 @@ export function rowKey(row, cols) {
|
|
|
24
24
|
* @param {string[]} codes - Code column names
|
|
25
25
|
* @param {string[]} unitCols - Column(s) that identify a unit (e.g. ['UserName', 'Condition'])
|
|
26
26
|
* @param {string[]} convoCols - Column(s) that identify a conversation (e.g. ['Condition', 'GroupName'])
|
|
27
|
+
* @param {string[]|null} [unitsUsed=null] - Unit keys (rowKey format) to model
|
|
28
|
+
* (= R's `units.used`). Rows of other units stay in their conversations, so
|
|
29
|
+
* they still count as context in the included units' windows, but they get
|
|
30
|
+
* no unit (unitOf = -1) and so produce no network. null = every unit.
|
|
27
31
|
*
|
|
28
32
|
* @returns {{
|
|
29
33
|
* codeMatrix: Float64Array, // n_rows × n_codes, row-major
|
|
@@ -31,14 +35,14 @@ export function rowKey(row, cols) {
|
|
|
31
35
|
* nCodes: number,
|
|
32
36
|
* nUnits: number,
|
|
33
37
|
* unitLabels: string[], // unit key for each unit index
|
|
34
|
-
* unitOf: Int32Array, // unit index for each row
|
|
38
|
+
* unitOf: Int32Array, // unit index for each row (-1 = not a modeled unit)
|
|
35
39
|
* convoOf: Int32Array, // conversation index for each row
|
|
36
40
|
* convoGroups: Map<number, number[]>, // convoIdx → [rowIdx, ...]
|
|
37
41
|
* metaData: Object[], // one metadata object per unit (first-row representative)
|
|
38
42
|
* metaCols: string[], // names of metadata columns
|
|
39
43
|
* }}
|
|
40
44
|
*/
|
|
41
|
-
export function parseData(rows, codes, unitCols, convoCols) {
|
|
45
|
+
export function parseData(rows, codes, unitCols, convoCols, unitsUsed = null) {
|
|
42
46
|
const nRows = rows.length;
|
|
43
47
|
const nCodes = codes.length;
|
|
44
48
|
|
|
@@ -58,6 +62,8 @@ export function parseData(rows, codes, unitCols, convoCols) {
|
|
|
58
62
|
// Per-unit metadata rows — populated on first occurrence of each unit.
|
|
59
63
|
const metaData = [];
|
|
60
64
|
|
|
65
|
+
const used = unitsUsed ? new Set(unitsUsed.map(String)) : null;
|
|
66
|
+
|
|
61
67
|
for (let i = 0; i < nRows; i++) {
|
|
62
68
|
const row = rows[i];
|
|
63
69
|
|
|
@@ -68,7 +74,9 @@ export function parseData(rows, codes, unitCols, convoCols) {
|
|
|
68
74
|
|
|
69
75
|
// Unit index
|
|
70
76
|
const uKey = rowKey(row, unitCols);
|
|
71
|
-
if (!
|
|
77
|
+
if (used && !used.has(uKey)) {
|
|
78
|
+
unitOf[i] = -1;
|
|
79
|
+
} else if (!unitIndex.has(uKey)) {
|
|
72
80
|
const uIdx = unitIndex.size;
|
|
73
81
|
unitIndex.set(uKey, uIdx);
|
|
74
82
|
// First row for this unit → representative metadata
|
|
@@ -76,7 +84,7 @@ export function parseData(rows, codes, unitCols, convoCols) {
|
|
|
76
84
|
for (const col of metaCols) meta[col] = row[col];
|
|
77
85
|
metaData[uIdx] = meta;
|
|
78
86
|
}
|
|
79
|
-
unitOf[i] = unitIndex.get(uKey);
|
|
87
|
+
if (unitOf[i] !== -1) unitOf[i] = unitIndex.get(uKey);
|
|
80
88
|
|
|
81
89
|
// Conversation index
|
|
82
90
|
const cKey = rowKey(row, convoCols);
|
|
@@ -90,6 +98,9 @@ export function parseData(rows, codes, unitCols, convoCols) {
|
|
|
90
98
|
|
|
91
99
|
const unitLabels = Array.from(unitIndex.keys());
|
|
92
100
|
const nUnits = unitLabels.length;
|
|
101
|
+
if (used && nUnits === 0) {
|
|
102
|
+
throw new Error('opts.unitsUsed matched no units in the data');
|
|
103
|
+
}
|
|
93
104
|
|
|
94
105
|
return { codeMatrix, nRows, nCodes, nUnits, unitLabels, unitOf, convoOf, convoGroups,
|
|
95
106
|
metaData, metaCols };
|
package/src/index.js
CHANGED
|
@@ -90,19 +90,45 @@ function computeVariance(points, nUnits, dims) {
|
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Build the adjacency key — list of [codeA, codeB] pairs for each connection.
|
|
93
|
-
* Matches R's
|
|
93
|
+
* Matches R's rotation$adjacency.key.
|
|
94
|
+
*
|
|
95
|
+
* Unordered: the upper triangle in column-major order (choose_two(n) pairs).
|
|
96
|
+
* Ordered: all n² directed connections in column-major order — column
|
|
97
|
+
* j*n + i is [codes[i], codes[j]], ground codes[i] → response codes[j]
|
|
98
|
+
* (as R names it "codes[i] & codes[j]").
|
|
94
99
|
*
|
|
95
100
|
* @param {string[]} codes
|
|
101
|
+
* @param {boolean} [ordered=false]
|
|
96
102
|
* @returns {string[][]} length nConnections, each entry is [codeI, codeJ]
|
|
97
103
|
*/
|
|
98
|
-
function buildAdjacencyKey(codes) {
|
|
104
|
+
function buildAdjacencyKey(codes, ordered = false) {
|
|
99
105
|
const key = [];
|
|
106
|
+
if (ordered) {
|
|
107
|
+
for (let j = 0; j < codes.length; j++)
|
|
108
|
+
for (let i = 0; i < codes.length; i++)
|
|
109
|
+
key.push([codes[i], codes[j]]);
|
|
110
|
+
return key;
|
|
111
|
+
}
|
|
100
112
|
for (let j = 1; j < codes.length; j++)
|
|
101
113
|
for (let i = 0; i < j; i++)
|
|
102
114
|
key.push([codes[i], codes[j]]);
|
|
103
115
|
return key;
|
|
104
116
|
}
|
|
105
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Connection names, "codeA & codeB" per connection, in network column order
|
|
120
|
+
* (= R's colnames(set$connection.counts)).
|
|
121
|
+
*
|
|
122
|
+
* @param {object} qe
|
|
123
|
+
* @param {string[]} codes
|
|
124
|
+
* @param {boolean} [ordered=false]
|
|
125
|
+
* @returns {string[]}
|
|
126
|
+
*/
|
|
127
|
+
function connectionNamesFor(qe, codes, ordered = false) {
|
|
128
|
+
if (!ordered) return qe.connection_names(codes);
|
|
129
|
+
return buildAdjacencyKey(codes, true).map(([a, b]) => `${a} & ${b}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
106
132
|
// ── ENA result object ─────────────────────────────────────────────────────────
|
|
107
133
|
|
|
108
134
|
class ENAModel {
|
|
@@ -250,16 +276,18 @@ function applyCodeMask(networks, codeMask, nCodes, nUnits, nConnections, ordered
|
|
|
250
276
|
|
|
251
277
|
function runPipeline(qe, rawNetworks, nUnits, nConnections, codes, unitLabels,
|
|
252
278
|
metaData, rotMethod, groupA, groupB, dims, gParams,
|
|
253
|
-
rowConnectionCounts = null,
|
|
254
|
-
const connectionNames = qe
|
|
279
|
+
rowConnectionCounts = null, ordered = false) {
|
|
280
|
+
const connectionNames = connectionNamesFor(qe, codes, ordered);
|
|
255
281
|
|
|
256
282
|
// Sphere norm → lineWeights (= R's set$line.weights)
|
|
257
283
|
const lineWeights = sphereNorm(qe, rawNetworks, nUnits, nConnections);
|
|
258
284
|
|
|
259
285
|
// Center → pointsForProjection (= R's model$points.for.projection)
|
|
260
286
|
// centerVec (= R's rotation$center.vec)
|
|
287
|
+
// Ordered (ONA) models follow ona::model(): zero-network units are left
|
|
288
|
+
// out of the mean but still shifted by it.
|
|
261
289
|
const { centered: pointsForProjection, centerVec } =
|
|
262
|
-
center(qe, lineWeights, nUnits, nConnections);
|
|
290
|
+
center(qe, lineWeights, nUnits, nConnections, ordered);
|
|
263
291
|
|
|
264
292
|
// Rotate
|
|
265
293
|
let rot;
|
|
@@ -298,17 +326,30 @@ function runPipeline(qe, rawNetworks, nUnits, nConnections, codes, unitLabels,
|
|
|
298
326
|
// Matches rENA's lws.positions.sq, which regresses the projected points onto
|
|
299
327
|
// the SPHERE-normed line weights (enaset$line.weights) — not the centered
|
|
300
328
|
// networks. Verified node-for-node against R rENA on rs.data.new.csv.
|
|
301
|
-
//
|
|
302
|
-
//
|
|
329
|
+
// Ordered (ONA) models use libqe's directed_node_positions, as
|
|
330
|
+
// ona::model() does (via rENA's optimize()).
|
|
303
331
|
const { nodes, centroids } = nodePositions(
|
|
304
|
-
qe, lineWeights, nUnits, nConnections, points, dims,
|
|
332
|
+
qe, lineWeights, nUnits, nConnections, points, dims, ordered
|
|
305
333
|
);
|
|
306
334
|
|
|
335
|
+
// ONA: translate points and nodes so the points' mean is at the origin
|
|
336
|
+
// (ona::model's center_to_origin; centroids are left as computed, as in R).
|
|
337
|
+
if (ordered) {
|
|
338
|
+
const nCodes = nodes.length / dims;
|
|
339
|
+
for (let d = 0; d < dims; d++) {
|
|
340
|
+
let mean = 0;
|
|
341
|
+
for (let u = 0; u < nUnits; u++) mean += points[u * dims + d];
|
|
342
|
+
mean /= nUnits;
|
|
343
|
+
for (let u = 0; u < nUnits; u++) points[u * dims + d] -= mean;
|
|
344
|
+
for (let c = 0; c < nCodes; c++) nodes[c * dims + d] -= mean;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
307
348
|
// Variance explained (= R's model$variance)
|
|
308
349
|
const variance = computeVariance(points, nUnits, dims);
|
|
309
350
|
|
|
310
351
|
// Adjacency key (= R's rotation$adjacency.key)
|
|
311
|
-
const adjacencyKey = buildAdjacencyKey(codes);
|
|
352
|
+
const adjacencyKey = buildAdjacencyKey(codes, ordered);
|
|
312
353
|
|
|
313
354
|
return new ENAModel({
|
|
314
355
|
// top-level
|
|
@@ -366,6 +407,8 @@ export default async function loadENA() {
|
|
|
366
407
|
* @param {number[]} [opts.groupB] - Unit indices for means rotation group B
|
|
367
408
|
* @param {object} [opts.gParams] - Pre-built GMR parameters for 'generalized' rotation
|
|
368
409
|
* @param {number} [opts.dims=2] - Number of dimensions to return
|
|
410
|
+
* @param {string[]} [opts.unitsUsed] - Unit keys to model (= R's units.used);
|
|
411
|
+
* other units' rows remain as context
|
|
369
412
|
*
|
|
370
413
|
* @returns {ENAModel}
|
|
371
414
|
*/
|
|
@@ -385,6 +428,7 @@ export default async function loadENA() {
|
|
|
385
428
|
dims = 2,
|
|
386
429
|
codeMask,
|
|
387
430
|
weightModel,
|
|
431
|
+
unitsUsed,
|
|
388
432
|
} = opts;
|
|
389
433
|
|
|
390
434
|
if (!codes?.length) throw new Error('opts.codes is required');
|
|
@@ -401,7 +445,7 @@ export default async function loadENA() {
|
|
|
401
445
|
|
|
402
446
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
403
447
|
unitOf, convoGroups, metaData } =
|
|
404
|
-
parseData(rows, codes, units, conversations);
|
|
448
|
+
parseData(rows, codes, units, conversations, unitsUsed);
|
|
405
449
|
|
|
406
450
|
// Every model accumulates through the tensor path; a plain moving
|
|
407
451
|
// window is expressed as defaultTensor(window).
|
|
@@ -416,7 +460,7 @@ export default async function loadENA() {
|
|
|
416
460
|
|
|
417
461
|
return runPipeline(qe, rawNetworks, nUnits, nConnections, codes,
|
|
418
462
|
unitLabels, metaData, rotMethod, groupA, groupB,
|
|
419
|
-
dims, gParams, rowConnectionCounts);
|
|
463
|
+
dims, gParams, rowConnectionCounts, ordered);
|
|
420
464
|
},
|
|
421
465
|
|
|
422
466
|
/**
|
|
@@ -445,11 +489,12 @@ export default async function loadENA() {
|
|
|
445
489
|
tensor: tensorDef,
|
|
446
490
|
codeMask,
|
|
447
491
|
weightModel,
|
|
492
|
+
unitsUsed,
|
|
448
493
|
} = opts;
|
|
449
494
|
|
|
450
495
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
451
496
|
unitOf, convoGroups, metaData } =
|
|
452
|
-
parseData(rows, codes, units, conversations);
|
|
497
|
+
parseData(rows, codes, units, conversations, unitsUsed);
|
|
453
498
|
|
|
454
499
|
const { networks, rowConnectionCounts } = accumulateTensor(
|
|
455
500
|
qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
@@ -460,7 +505,7 @@ export default async function loadENA() {
|
|
|
460
505
|
|
|
461
506
|
applyCodeMask(networks, codeMask, codes.length, nUnits, nConnections, ordered);
|
|
462
507
|
|
|
463
|
-
const connectionNames = qe
|
|
508
|
+
const connectionNames = connectionNamesFor(qe, codes, ordered);
|
|
464
509
|
return {
|
|
465
510
|
connectionCounts: networks, rowConnectionCounts, unitLabels, connectionNames,
|
|
466
511
|
metaData, nUnits, nConnections,
|
|
@@ -468,7 +513,7 @@ export default async function loadENA() {
|
|
|
468
513
|
// (= R's ENAAccumulation$`_function.call`).
|
|
469
514
|
_call: { rows, codes, units, conversations,
|
|
470
515
|
window: windowSize, binary, ordered, tensor: tensorDef,
|
|
471
|
-
weightModel },
|
|
516
|
+
weightModel, unitsUsed },
|
|
472
517
|
};
|
|
473
518
|
},
|
|
474
519
|
|
|
@@ -506,12 +551,12 @@ export default async function loadENA() {
|
|
|
506
551
|
'maxSize must be greater than minSize to compare windows.'
|
|
507
552
|
);
|
|
508
553
|
|
|
509
|
-
const { rows, codes, units, conversations, binary, ordered, weightModel } = call;
|
|
554
|
+
const { rows, codes, units, conversations, binary, ordered, weightModel, unitsUsed } = call;
|
|
510
555
|
|
|
511
556
|
// Parse once; only the window size changes between iterations.
|
|
512
557
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
513
558
|
unitOf, convoGroups, metaData } =
|
|
514
|
-
parseData(rows, codes, units, conversations);
|
|
559
|
+
parseData(rows, codes, units, conversations, unitsUsed);
|
|
515
560
|
const nConnections = ordered ? nCodes * nCodes : qe.choose_two(nCodes);
|
|
516
561
|
|
|
517
562
|
// 1. Rebuild + fit at each window, collecting unit points.
|
|
@@ -525,7 +570,8 @@ export default async function loadENA() {
|
|
|
525
570
|
);
|
|
526
571
|
const model = runPipeline(
|
|
527
572
|
qe, raw, nUnits, nConnections, codes,
|
|
528
|
-
unitLabels, metaData, 'svd', undefined, undefined, dims
|
|
573
|
+
unitLabels, metaData, 'svd', undefined, undefined, dims,
|
|
574
|
+
undefined, null, ordered
|
|
529
575
|
);
|
|
530
576
|
allPoints.push(model.points);
|
|
531
577
|
}
|
|
@@ -548,7 +594,7 @@ export default async function loadENA() {
|
|
|
548
594
|
// 4. Rebuild the accumulation at the selected window size.
|
|
549
595
|
return api.accumulate(rows, {
|
|
550
596
|
codes, units, conversations, window: bestWindow, binary, ordered,
|
|
551
|
-
weightModel,
|
|
597
|
+
weightModel, unitsUsed,
|
|
552
598
|
});
|
|
553
599
|
},
|
|
554
600
|
|
|
@@ -680,7 +726,8 @@ export default async function loadENA() {
|
|
|
680
726
|
* @param {Object[]} rows
|
|
681
727
|
* @param {object} opts - fit() options (codes, units, conversations,
|
|
682
728
|
* window, binary, ordered, tensor, weightModel,
|
|
683
|
-
* codeMask, rotation, groupA, groupB, gParams
|
|
729
|
+
* codeMask, rotation, groupA, groupB, gParams,
|
|
730
|
+
* unitsUsed)
|
|
684
731
|
* plus removeNum (default 3) and threshold
|
|
685
732
|
* (default 0.95). `dims` is ignored: scoring
|
|
686
733
|
* uses dims 1:2 and the variance uses all dims.
|
|
@@ -693,7 +740,7 @@ export default async function loadENA() {
|
|
|
693
740
|
window: windowSize = 4, binary = true, ordered = false,
|
|
694
741
|
tensor: tensorDef, weightModel, codeMask,
|
|
695
742
|
rotation = 'svd', gParams, groupA, groupB,
|
|
696
|
-
removeNum = 3, threshold = 0.95,
|
|
743
|
+
removeNum = 3, threshold = 0.95, unitsUsed,
|
|
697
744
|
} = opts;
|
|
698
745
|
if (!codes?.length) throw new Error('opts.codes is required');
|
|
699
746
|
if (!units?.length) throw new Error('opts.units is required');
|
|
@@ -707,7 +754,7 @@ export default async function loadENA() {
|
|
|
707
754
|
// Accumulate once, exactly as fit() would for these options.
|
|
708
755
|
const { codeMatrix, nRows, nCodes, nUnits, unitLabels,
|
|
709
756
|
unitOf, convoGroups, metaData } =
|
|
710
|
-
parseData(rows, codes, units, conversations);
|
|
757
|
+
parseData(rows, codes, units, conversations, unitsUsed);
|
|
711
758
|
const { networks: raw } = accumulateTensor(
|
|
712
759
|
qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
713
760
|
unitOf, convoGroups, tensorDef ?? defaultTensor(windowSize),
|
|
@@ -745,8 +792,6 @@ export default async function loadENA() {
|
|
|
745
792
|
networks[u * nConn + c] = raw[u * nConnections + cols[c]];
|
|
746
793
|
keptCodes = codes.filter((_, i) => keep(i));
|
|
747
794
|
}
|
|
748
|
-
// Ordered models are scored with directed node positions (R's
|
|
749
|
-
// ordered pipeline), independent of how fit() places nodes.
|
|
750
795
|
return runPipeline(qe, networks, nUnits, nConn, keptCodes,
|
|
751
796
|
unitLabels, metaData, rotation, groupA, groupB,
|
|
752
797
|
nConn, gParams, null, ordered);
|
package/src/pipeline.js
CHANGED
|
@@ -133,11 +133,15 @@ export function sphereNorm(qe, networks, nUnits, nConnections) {
|
|
|
133
133
|
* Subtract column means (center the network space).
|
|
134
134
|
* Excludes all-zero rows from the mean calculation (zero-network exclusion).
|
|
135
135
|
*
|
|
136
|
+
* @param {boolean} [shiftZeroRows=false] Also subtract the mean from all-zero
|
|
137
|
+
* rows, as rENA's center(exclude_zero_networks = TRUE) does -- the ONA
|
|
138
|
+
* convention (ona::model). By default zero-network rows stay at zero, as in
|
|
139
|
+
* rENA's ena.make.set().
|
|
136
140
|
* @returns {{ centered: Float64Array, centerVec: Float64Array }}
|
|
137
|
-
* centered — mean-subtracted networks
|
|
141
|
+
* centered — mean-subtracted networks
|
|
138
142
|
* centerVec — the column means used for centering (= R's rotation$center.vec)
|
|
139
143
|
*/
|
|
140
|
-
export function center(qe, networks, nUnits, nConnections) {
|
|
144
|
+
export function center(qe, networks, nUnits, nConnections, shiftZeroRows = false) {
|
|
141
145
|
// Identify non-zero rows
|
|
142
146
|
const active = [];
|
|
143
147
|
for (let u = 0; u < nUnits; u++) {
|
|
@@ -160,7 +164,7 @@ export function center(qe, networks, nUnits, nConnections) {
|
|
|
160
164
|
const activeSet = new Set(active);
|
|
161
165
|
const centered = new Float64Array(networks.length);
|
|
162
166
|
for (let u = 0; u < nUnits; u++) {
|
|
163
|
-
if (!activeSet.has(u)) continue;
|
|
167
|
+
if (!shiftZeroRows && !activeSet.has(u)) continue; // leave zero-network row as zero
|
|
164
168
|
for (let c = 0; c < nConnections; c++) {
|
|
165
169
|
centered[u * nConnections + c] = networks[u * nConnections + c] - means[c];
|
|
166
170
|
}
|
package/src/tensor.js
CHANGED
|
@@ -197,10 +197,13 @@ export function accumulateTensor(qe, rows, codeMatrix, nRows, nCodes, nUnits,
|
|
|
197
197
|
? buildContextLookupWithTimes(rows, rowIndices, factors, factorLevels, timesCol)
|
|
198
198
|
: buildContextLookup(rows, rowIndices, factors, factorLevels);
|
|
199
199
|
|
|
200
|
-
// Group response rows by unit (within this conversation)
|
|
200
|
+
// Group response rows by unit (within this conversation). Rows with
|
|
201
|
+
// no unit (unitOf = -1, excluded via unitsUsed) are never responses,
|
|
202
|
+
// but stay in convoCodes as context for the other units' windows.
|
|
201
203
|
const unitConvoRows = new Map();
|
|
202
204
|
for (let r = 0; r < nConvo; r++) {
|
|
203
205
|
const unit = unitOf[rowIndices[r]];
|
|
206
|
+
if (unit < 0) continue;
|
|
204
207
|
if (!unitConvoRows.has(unit)) unitConvoRows.set(unit, []);
|
|
205
208
|
unitConvoRows.get(unit).push(r); // local (conversation-relative) index
|
|
206
209
|
}
|