@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qe-libs/rena-wasm",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",
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 enadata$adjacency.matrix (column-major upper triangle order).
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, directedNodes = false) {
254
- const connectionNames = qe.connection_names(codes);
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
- // `directedNodes` (ordered networks, used by pria()) solves with libqe's
302
- // directed_node_positions, as rENA's optimize() does for ordered sets.
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, directedNodes
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.connection_names(codes);
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 (zero-network rows left at zero)
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; // leave zero-network row as zero
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
  }