@qe-libs/rena-wasm 0.1.6 → 0.1.7
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 +7 -1
- package/package.json +1 -1
- package/src/index.js +54 -14
- package/src/pipeline.js +7 -3
package/README.md
CHANGED
|
@@ -136,7 +136,13 @@ columns in the data.
|
|
|
136
136
|
// dims = [nRoleValues, 2] → Teacher uses window=4, Student uses window=2.
|
|
137
137
|
const model = ena.fit(rows, {
|
|
138
138
|
codes, units, conversations,
|
|
139
|
-
ordered: true, // directed (n² connections); also works without a tensor
|
|
139
|
+
ordered: true, // directed (n² connections); also works without a tensor.
|
|
140
|
+
// Ordered models follow ona::model(): zero-network units are
|
|
141
|
+
// left out of the centring mean but shifted by it, nodes use
|
|
142
|
+
// directed positions, and points/nodes are centred on the origin.
|
|
143
|
+
// connectionNames / rotation.adjacencyKey list all n² directed
|
|
144
|
+
// connections as R names them: column j*n + i is
|
|
145
|
+
// "codes[i] & codes[j]", ground codes[i] → response codes[j].
|
|
140
146
|
tensor: {
|
|
141
147
|
dims: [2, 2], // [nRoleValues=2, weight/window=2]
|
|
142
148
|
dimsSender: [0], // axis 0 is a sender factor
|
package/package.json
CHANGED
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
|
|
@@ -416,7 +457,7 @@ export default async function loadENA() {
|
|
|
416
457
|
|
|
417
458
|
return runPipeline(qe, rawNetworks, nUnits, nConnections, codes,
|
|
418
459
|
unitLabels, metaData, rotMethod, groupA, groupB,
|
|
419
|
-
dims, gParams, rowConnectionCounts);
|
|
460
|
+
dims, gParams, rowConnectionCounts, ordered);
|
|
420
461
|
},
|
|
421
462
|
|
|
422
463
|
/**
|
|
@@ -460,7 +501,7 @@ export default async function loadENA() {
|
|
|
460
501
|
|
|
461
502
|
applyCodeMask(networks, codeMask, codes.length, nUnits, nConnections, ordered);
|
|
462
503
|
|
|
463
|
-
const connectionNames = qe
|
|
504
|
+
const connectionNames = connectionNamesFor(qe, codes, ordered);
|
|
464
505
|
return {
|
|
465
506
|
connectionCounts: networks, rowConnectionCounts, unitLabels, connectionNames,
|
|
466
507
|
metaData, nUnits, nConnections,
|
|
@@ -525,7 +566,8 @@ export default async function loadENA() {
|
|
|
525
566
|
);
|
|
526
567
|
const model = runPipeline(
|
|
527
568
|
qe, raw, nUnits, nConnections, codes,
|
|
528
|
-
unitLabels, metaData, 'svd', undefined, undefined, dims
|
|
569
|
+
unitLabels, metaData, 'svd', undefined, undefined, dims,
|
|
570
|
+
undefined, null, ordered
|
|
529
571
|
);
|
|
530
572
|
allPoints.push(model.points);
|
|
531
573
|
}
|
|
@@ -745,8 +787,6 @@ export default async function loadENA() {
|
|
|
745
787
|
networks[u * nConn + c] = raw[u * nConnections + cols[c]];
|
|
746
788
|
keptCodes = codes.filter((_, i) => keep(i));
|
|
747
789
|
}
|
|
748
|
-
// Ordered models are scored with directed node positions (R's
|
|
749
|
-
// ordered pipeline), independent of how fit() places nodes.
|
|
750
790
|
return runPipeline(qe, networks, nUnits, nConn, keptCodes,
|
|
751
791
|
unitLabels, metaData, rotation, groupA, groupB,
|
|
752
792
|
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
|
}
|