w-cluster 1.0.20 → 1.0.21

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.
Files changed (72) hide show
  1. package/.github/workflows/ci-test.yml +3 -3
  2. package/README.md +4 -6
  3. package/babel.config.js +4 -3
  4. package/dist/w-cluster.umd.js +2 -2
  5. package/dist/w-cluster.umd.js.map +1 -1
  6. package/dist/w-cluster.wk.umd.js +1 -1
  7. package/docs/DenseMatrix.html +202 -0
  8. package/docs/DistancePair.html +202 -0
  9. package/docs/LowerTriangle.html +202 -0
  10. package/docs/Rec.html +202 -0
  11. package/docs/Reco.html +202 -0
  12. package/docs/WCluster.mjs.html +8 -4
  13. package/docs/examples/ex-PCA.html +1 -1
  14. package/docs/examples/ex-cluster-webworker.html +1 -1
  15. package/docs/examples/ex-cluster.html +1 -1
  16. package/docs/global.html +6799 -164
  17. package/docs/index.html +2 -2
  18. package/docs/k-medoids_alternating.mjs.html +144 -0
  19. package/docs/k-medoids_arrayadapter.mjs.html +107 -0
  20. package/docs/k-medoids_dynmsc.mjs.html +255 -0
  21. package/docs/k-medoids_fastermsc.mjs.html +411 -0
  22. package/docs/k-medoids_fasterpam.mjs.html +305 -0
  23. package/docs/k-medoids_fastmsc.mjs.html +166 -0
  24. package/docs/k-medoids_fastpam1.mjs.html +132 -0
  25. package/docs/k-medoids_initialization.mjs.html +107 -0
  26. package/docs/k-medoids_pam.mjs.html +275 -0
  27. package/docs/k-medoids_pammedsil.mjs.html +307 -0
  28. package/docs/k-medoids_pamsil.mjs.html +225 -0
  29. package/docs/k-medoids_par_fasterpam.mjs.html +134 -0
  30. package/docs/k-medoids_par_silhouette.mjs.html +126 -0
  31. package/docs/k-medoids_silhouette.mjs.html +193 -0
  32. package/docs/k-medoids_util.mjs.html +135 -0
  33. package/g-PCA-nodeworker.mjs +1 -1
  34. package/g-PCA.mjs +1 -1
  35. package/g-cluster-kMeans-large.mjs +83 -0
  36. package/g-cluster-kMeans-nodeworker.mjs +181 -0
  37. package/g-cluster-kMeans.mjs +178 -0
  38. package/g-cluster-kMedoids-large-suggest.mjs +95 -0
  39. package/g-cluster-kMedoids-large.mjs +104 -0
  40. package/{g-cluster-nodeworker.mjs → g-cluster-kMedoids-nodeworker.mjs} +1 -2
  41. package/{g-cluster.mjs → g-cluster-kMedoids-simple.mjs} +1 -1
  42. package/package.json +7 -7
  43. package/script.txt +5 -7
  44. package/src/WCluster.mjs +6 -2
  45. package/src/WClusterCore.mjs +30 -21
  46. package/src/WClusterMat.mjs +98 -24
  47. package/src/jaccardBitset.mjs +63 -0
  48. package/src/k-medoids/README.md +124 -0
  49. package/src/k-medoids/alternating.mjs +72 -0
  50. package/src/k-medoids/arrayadapter.mjs +35 -0
  51. package/src/k-medoids/dynmsc.mjs +183 -0
  52. package/src/k-medoids/fastermsc.mjs +339 -0
  53. package/src/k-medoids/fasterpam.mjs +233 -0
  54. package/src/k-medoids/fastmsc.mjs +94 -0
  55. package/src/k-medoids/fastpam1.mjs +60 -0
  56. package/src/k-medoids/index.mjs +37 -0
  57. package/src/k-medoids/initialization.mjs +35 -0
  58. package/src/k-medoids/package.json +8 -0
  59. package/src/k-medoids/pam.mjs +203 -0
  60. package/src/k-medoids/pammedsil.mjs +235 -0
  61. package/src/k-medoids/pamsil.mjs +153 -0
  62. package/src/k-medoids/par_fasterpam.mjs +62 -0
  63. package/src/k-medoids/par_silhouette.mjs +54 -0
  64. package/src/k-medoids/silhouette.mjs +121 -0
  65. package/src/k-medoids/test.mjs +357 -0
  66. package/src/k-medoids/util.mjs +63 -0
  67. package/test/jaccardBitset.test.mjs +141 -0
  68. package/test/kMeans.test.mjs +278 -0
  69. package/test/kMedoids-large-suggest.test.mjs +140 -0
  70. package/test/kMedoids-large.test.mjs +142 -0
  71. package/test/{cluster.test.mjs → kMedoids-simple.test.mjs} +5 -5
  72. package/toolg/gDocsExams.mjs +1 -1
@@ -0,0 +1,183 @@
1
+ // Ported 1:1 from src/dynmsc.rs (DynMSC algorithm).
2
+ import { arrayAdapter } from './arrayadapter.mjs';
3
+ import { Reco, DistancePair, U32_MAX, USIZE_MAX, choose_medoid_within_partition } from './util.mjs';
4
+ import { _loss, initial_assignment, update_removal_loss, find_best_swap, do_swap, fastermsc_k2 } from './fastermsc.mjs';
5
+
6
+ /**
7
+ * Run the DynMSC algorithm.
8
+ *
9
+ * We begin with a maximum number of clusters, optimize the Average Medoid Silhouette,
10
+ * then decrease the number of clusters by one, and repeat until we have reached a
11
+ * minimum number of clusters. During this process, we store the solution with the
12
+ * highest AMS to return later.
13
+ *
14
+ * @param {object} mat - a pairwise distance matrix
15
+ * @param {number[]} med - the list of medoids
16
+ * @param {number} mink - the minimum number of clusters
17
+ * @param {number} maxiter - the maximum number of iterations allowed
18
+ * returns { loss, assi, nIter, nSwaps, meds, losses }
19
+ */
20
+ export function dynmsc(mat, med, mink, maxiter) {
21
+ mat = arrayAdapter(mat);
22
+ med = med.slice(); // Rust: let mut med = med.to_owned();
23
+ const n = mat.len();
24
+ let k = med.length;
25
+ const minimum_k = Math.min(Math.max(mink, 1), k);
26
+ if (k === 1) {
27
+ const return_loss = new Array(1).fill(0);
28
+ const assi = new Array(n).fill(0);
29
+ const [swapped, loss] = choose_medoid_within_partition(mat, assi, med, 0);
30
+ return_loss[0] = loss;
31
+ const return_meds = med.slice();
32
+ return { loss, assi, nIter: 1, nSwaps: swapped ? 1 : 0, meds: return_meds, losses: return_loss };
33
+ }
34
+ let [loss, data] = initial_assignment(mat, med);
35
+
36
+ const return_loss = new Array(k - minimum_k + 1).fill(0);
37
+ let best_loss = 0;
38
+ let return_assi = [0, n]; // Rust: vec![0, n]; overwritten before being meaningfully used
39
+ let return_iter = 0;
40
+ let return_swaps = 0;
41
+ let lastswap, n_swaps, iter;
42
+ let removal_loss = new Array(k).fill(0);
43
+ let return_meds = med.slice();
44
+ while (k >= 3 && k >= minimum_k) {
45
+ update_removal_loss(data, removal_loss);
46
+ lastswap = n;
47
+ n_swaps = 0;
48
+ iter = 0;
49
+ while (iter < maxiter) {
50
+ iter += 1;
51
+ const swaps_before = n_swaps, lastloss = loss;
52
+ for (let j = 0; j < n; j++) {
53
+ if (j === lastswap) {
54
+ break;
55
+ }
56
+ if (j === med[data[j].near.i]) {
57
+ continue; // This already is a medoid
58
+ }
59
+ const [change, b] = find_best_swap(mat, removal_loss, data, j);
60
+ if (change <= 0) {
61
+ continue; // No improvement
62
+ }
63
+ n_swaps += 1;
64
+ lastswap = j;
65
+ // perform the swap
66
+ loss = do_swap(mat, med, data, b, j);
67
+ update_removal_loss(data, removal_loss);
68
+ }
69
+ if (n_swaps === swaps_before || loss >= lastloss) {
70
+ break; // converged
71
+ }
72
+ }
73
+ let r = [0, USIZE_MAX];
74
+ for (let o = 0; o < removal_loss.length; o++) {
75
+ const remloss = removal_loss[o];
76
+ if (o === 0 || remloss < r[0]) {
77
+ r = [remloss, o];
78
+ }
79
+ }
80
+ loss = 1 - loss / n;
81
+ return_loss[k - minimum_k] = loss;
82
+ const assi = data.map((x) => x.near.i);
83
+ if (loss > best_loss) {
84
+ best_loss = loss;
85
+ return_assi = assi;
86
+ return_meds = med.slice();
87
+ }
88
+ return_swaps += n_swaps;
89
+ return_iter += iter;
90
+ loss = remove_med(mat, med, data, r[1]);
91
+ removal_loss.splice(r[1], 1);
92
+ k = med.length;
93
+ }
94
+ if (minimum_k <= 2) {
95
+ const { loss: loss2, assi: assi2, nIter: iter2, nSwaps: n_swaps2 } = fastermsc_k2(mat, med, maxiter);
96
+ return_loss[2 - minimum_k] = loss2;
97
+ if (loss2 > best_loss) {
98
+ best_loss = loss2;
99
+ return_meds = med.slice();
100
+ return_assi = assi2;
101
+ }
102
+ return_swaps += n_swaps2;
103
+ return_iter += iter2;
104
+ }
105
+ if (minimum_k <= 1) {
106
+ return_loss[0] = 0;
107
+ }
108
+ return { loss: best_loss, assi: return_assi, nIter: return_iter, nSwaps: return_swaps, meds: return_meds, losses: return_loss };
109
+ }
110
+
111
+ /**
112
+ * Update the third nearest medoid information. Called after each swap.
113
+ * Returns a fresh DistancePair.
114
+ */
115
+ export function update_third_nearest_without_new(mat, med, n, s, b, o) {
116
+ let dist = new DistancePair(b, 0);
117
+ for (let i = 0; i < med.length; i++) {
118
+ const mi = med[i];
119
+ if (i === n || i === s) {
120
+ continue;
121
+ }
122
+ const d = mat.get(o, mi);
123
+ if (dist.i === b || d < dist.d) {
124
+ dist = new DistancePair(i, d);
125
+ }
126
+ }
127
+ return dist;
128
+ }
129
+
130
+ /** Remove one medoid. Returns RAW loss. */
131
+ export function remove_med(mat, med, data, b) {
132
+ const l = med.length - 1;
133
+ if (!(b < med.length)) throw new Error('invalid medoid number');
134
+ med[b] = med[l];
135
+ med.splice(l, 1);
136
+ let acc = 0;
137
+ for (let o = 0; o < data.length; o++) {
138
+ const reco = data[o];
139
+ if (reco.near.i === b) {
140
+ // nearest medoid is gone
141
+ if (reco.seco.i === l) {
142
+ reco.seco.i = b;
143
+ }
144
+ if (reco.third.i === l) {
145
+ reco.third.i = b;
146
+ }
147
+ reco.near = reco.seco.clone();
148
+ reco.seco = reco.third.clone();
149
+ reco.third = update_third_nearest_without_new(mat, med, reco.near.i, reco.seco.i, b, o);
150
+ } else if (reco.seco.i === b) {
151
+ // second nearest is gone
152
+ if (reco.near.i === l) {
153
+ reco.near.i = b;
154
+ }
155
+ if (reco.third.i === l) {
156
+ reco.third.i = b;
157
+ }
158
+ reco.seco = reco.third.clone();
159
+ reco.third = update_third_nearest_without_new(mat, med, reco.near.i, reco.seco.i, b, o);
160
+ } else if (reco.third.i === b) {
161
+ // third nearest is gone
162
+ if (reco.near.i === l) {
163
+ reco.near.i = b;
164
+ }
165
+ if (reco.seco.i === l) {
166
+ reco.seco.i = b;
167
+ }
168
+ reco.third = update_third_nearest_without_new(mat, med, reco.near.i, reco.seco.i, b, o);
169
+ } else {
170
+ if (reco.near.i === l) {
171
+ reco.near.i = b;
172
+ }
173
+ if (reco.seco.i === l) {
174
+ reco.seco.i = b;
175
+ }
176
+ if (reco.third.i === l) {
177
+ reco.third.i = b;
178
+ }
179
+ }
180
+ acc += _loss(reco.near.d, reco.seco.d);
181
+ }
182
+ return acc;
183
+ }
@@ -0,0 +1,339 @@
1
+ // Ported 1:1 from src/fastermsc.rs (FasterMSC algorithm).
2
+ import { arrayAdapter } from './arrayadapter.mjs';
3
+ import { Reco, DistancePair, U32_MAX, find_min, find_max, choose_medoid_within_partition } from './util.mjs';
4
+
5
+ // _loss(a, b): 0 if a or b is zero, else a / b.
6
+ export function _loss(a, b) {
7
+ if (a === 0 || b === 0) { return 0; } else { return a / b; }
8
+ }
9
+
10
+ /**
11
+ * Run the FasterMSC algorithm.
12
+ * @param {object} mat - pairwise distance matrix (wrapped)
13
+ * @param {number[]} med - the list of medoids (mutated in place)
14
+ * @param {number} maxiter - maximum number of iterations
15
+ * returns { loss, assi, nIter, nSwaps }
16
+ */
17
+ export function fastermsc(mat, med, maxiter) {
18
+ mat = arrayAdapter(mat);
19
+ const n = mat.len(), k = med.length;
20
+ if (k === 1) {
21
+ const assi = new Array(n).fill(0);
22
+ const [swapped, loss] = choose_medoid_within_partition(mat, assi, med, 0);
23
+ return { loss, assi, nIter: 1, nSwaps: swapped ? 1 : 0 };
24
+ }
25
+ if (k === 2) { // special hadling, as there is no third
26
+ return fastermsc_k2(mat, med, maxiter);
27
+ }
28
+ let [loss, data] = initial_assignment(mat, med);
29
+
30
+ let removal_loss = new Array(k).fill(0);
31
+ update_removal_loss(data, removal_loss);
32
+ let lastswap = n, n_swaps = 0, iter = 0;
33
+ while (iter < maxiter) {
34
+ iter += 1;
35
+ const swaps_before = n_swaps, lastloss = loss;
36
+ for (let j = 0; j < n; j++) {
37
+ if (j === lastswap) {
38
+ break;
39
+ }
40
+ if (j === med[data[j].near.i]) {
41
+ continue; // This already is a medoid
42
+ }
43
+ const [change, b] = find_best_swap(mat, removal_loss, data, j);
44
+ if (change <= 0) {
45
+ continue; // No improvement
46
+ }
47
+ n_swaps += 1;
48
+ lastswap = j;
49
+ // perform the swap
50
+ loss = do_swap(mat, med, data, b, j);
51
+ update_removal_loss(data, removal_loss);
52
+ }
53
+ if (n_swaps === swaps_before || loss >= lastloss) {
54
+ break; // converged
55
+ }
56
+ }
57
+ const assi = data.map((x) => x.near.i);
58
+ loss = 1 - loss / n;
59
+ return { loss, assi, nIter: iter, nSwaps: n_swaps };
60
+ }
61
+
62
+ /** Perform the initial assignment to medoids. Returns [loss, data] (data = Reco[]). */
63
+ export function initial_assignment(mat, med) {
64
+ const n = mat.len(), k = med.length;
65
+ if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
66
+ if (!(n <= U32_MAX)) throw new Error('N is too large');
67
+ if (!(k > 0 && k < U32_MAX)) throw new Error('invalid N');
68
+ if (!(k <= n)) throw new Error('k must be at most N');
69
+ const data = new Array(mat.len());
70
+ for (let _i = 0; _i < data.length; _i++) data[_i] = Reco.empty();
71
+ const firstcenter = med[0];
72
+ let loss = 0;
73
+ for (let i = 0; i < data.length; i++) {
74
+ // Rust: *cur = Reco::new(...) overwrites the slot in place.
75
+ const cur = new Reco(0, mat.get(i, firstcenter), U32_MAX, 0, U32_MAX, 0);
76
+ data[i] = cur;
77
+ for (let m = 1; m < med.length; m++) {
78
+ const me = med[m];
79
+ const d = mat.get(i, me);
80
+ if (d < cur.near.d || i === me) {
81
+ cur.third = cur.seco.clone();
82
+ cur.seco = cur.near.clone();
83
+ cur.near = new DistancePair(m, d);
84
+ } else if (cur.seco.i === U32_MAX || d < cur.seco.d) {
85
+ cur.third = cur.seco.clone();
86
+ cur.seco = new DistancePair(m, d);
87
+ } else if (cur.third.i === U32_MAX || d < cur.third.d) {
88
+ cur.third = new DistancePair(m, d);
89
+ }
90
+ }
91
+ loss += _loss(cur.near.d, cur.seco.d);
92
+ }
93
+ return [loss, data];
94
+ }
95
+
96
+ /** Find the best swap for object j - FastMSC version. Returns [change, b]. */
97
+ export function find_best_swap(mat, removal_loss, data, j) {
98
+ const ploss = removal_loss.slice();
99
+ // Improvement from the journal version:
100
+ let acc = 0;
101
+ for (let o = 0; o < data.length; o++) {
102
+ const reco = data[o];
103
+ const doj = mat.get(o, j);
104
+ if (doj < reco.near.d) {
105
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(doj, reco.near.d);
106
+ // loss already includes (dt - ds) - (ds - dn), remove
107
+ ploss[reco.near.i] += _loss(doj, reco.near.d) + _loss(reco.seco.d, reco.third.d) - _loss(reco.near.d + doj, reco.seco.d);
108
+ ploss[reco.seco.i] += _loss(reco.near.d, reco.third.d) - _loss(reco.near.d, reco.seco.d);
109
+ } else if (doj < reco.seco.d) {
110
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.near.d, doj);
111
+ ploss[reco.near.i] += _loss(reco.near.d, doj) + _loss(reco.seco.d, reco.third.d) - _loss(reco.near.d + doj, reco.seco.d);
112
+ // loss already includes (dt - ds) - (ds - dn), adjust to 2*d(xo) - ds - dt
113
+ // loss already includes (dt - ds), adjust to 2*d(xo) - ds - dt
114
+ ploss[reco.seco.i] += _loss(reco.near.d, reco.third.d) - _loss(reco.near.d, reco.seco.d);
115
+ } else if (doj < reco.third.d) {
116
+ // loss already includes (dt - ds) - (ds - dn), adjust to d(xo)- dt
117
+ ploss[reco.near.i] += _loss(reco.seco.d, reco.third.d) - _loss(reco.seco.d, doj);
118
+ // loss already includes (dt - ds), adjust to d(xo)- dt
119
+ ploss[reco.seco.i] += _loss(reco.near.d, reco.third.d) - _loss(reco.near.d, doj);
120
+ }
121
+ }
122
+ const [b, bloss] = find_max(ploss);
123
+ return [bloss + acc, b]; // add the shared accumulator
124
+ }
125
+
126
+ /** Update the loss when removing each medoid. Mutates loss in place. */
127
+ export function update_removal_loss(data, loss) {
128
+ loss.fill(0); // stable since 1.50
129
+ for (let r = 0; r < data.length; r++) {
130
+ const rec = data[r];
131
+ loss[rec.near.i] += _loss(rec.near.d, rec.seco.d) - _loss(rec.seco.d, rec.third.d);
132
+ loss[rec.seco.i] += _loss(rec.near.d, rec.seco.d) - _loss(rec.near.d, rec.third.d);
133
+ // as N might be unsigned
134
+ }
135
+ }
136
+
137
+ /** Update the third nearest medoid information. Called after each swap. Returns fresh DistancePair. */
138
+ export function update_third_nearest(mat, med, n, s, b, o, doj) {
139
+ let dist = new DistancePair(b, doj);
140
+ for (let i = 0; i < med.length; i++) {
141
+ const mi = med[i];
142
+ if (i === n || i === b || i === s) {
143
+ continue;
144
+ }
145
+ const d = mat.get(o, mi);
146
+ if (d < dist.d) {
147
+ dist = new DistancePair(i, d);
148
+ }
149
+ }
150
+ return dist;
151
+ }
152
+
153
+ /** Perform a single swap. Returns RAW loss. */
154
+ export function do_swap(mat, med, data, b, j) {
155
+ const n = mat.len();
156
+ if (!(b < med.length)) throw new Error('invalid medoid number');
157
+ if (!(j < n)) throw new Error('invalid object number');
158
+ med[b] = j;
159
+ let acc = 0;
160
+ for (let o = 0; o < data.length; o++) {
161
+ const reco = data[o];
162
+ if (o === j) {
163
+ if (reco.near.i !== b) {
164
+ if (reco.seco.i !== b) {
165
+ reco.third = reco.seco.clone();
166
+ }
167
+ reco.seco = reco.near.clone();
168
+ }
169
+ reco.near = new DistancePair(b, 0);
170
+ acc += 0;
171
+ continue;
172
+ }
173
+ const doj = mat.get(o, j);
174
+ // Nearest medoid is gone:
175
+ if (reco.near.i === b) {
176
+ if (doj < reco.seco.d) {
177
+ reco.near = new DistancePair(b, doj);
178
+ } else if (reco.third.i === U32_MAX || doj < reco.third.d) {
179
+ reco.near = reco.seco.clone();
180
+ reco.seco = new DistancePair(b, doj);
181
+ } else {
182
+ reco.near = reco.seco.clone();
183
+ reco.seco = reco.third.clone();
184
+ reco.third = update_third_nearest(mat, med, reco.near.i, reco.seco.i, b, o, doj);
185
+ }
186
+ } else if (reco.seco.i === b) {
187
+ // second nearest was replaced
188
+ if (doj < reco.near.d) {
189
+ reco.seco = reco.near.clone();
190
+ reco.near = new DistancePair(b, doj);
191
+ } else if (reco.third.i === U32_MAX || doj < reco.third.d) {
192
+ reco.seco = new DistancePair(b, doj);
193
+ } else {
194
+ reco.seco = reco.third.clone();
195
+ reco.third = update_third_nearest(mat, med, reco.near.i, reco.seco.i, b, o, doj);
196
+ }
197
+ } else {
198
+ // nearest not removed
199
+ if (doj < reco.near.d) {
200
+ reco.third = reco.seco.clone();
201
+ reco.seco = reco.near.clone();
202
+ reco.near = new DistancePair(b, doj);
203
+ } else if (doj < reco.seco.d) {
204
+ reco.third = reco.seco.clone();
205
+ reco.seco = new DistancePair(b, doj);
206
+ } else if (reco.third.i === U32_MAX || doj < reco.third.d) {
207
+ reco.third = new DistancePair(b, doj);
208
+ } else if (reco.third.i === b) {
209
+ reco.third = update_third_nearest(mat, med, reco.near.i, reco.seco.i, b, o, doj);
210
+ }
211
+ }
212
+ acc += _loss(reco.near.d, reco.seco.d);
213
+ }
214
+ return acc;
215
+ }
216
+
217
+ /** Special case k=2 of the FasterMSC algorithm. Returns { loss, assi, nIter, nSwaps }. */
218
+ export function fastermsc_k2(mat, med, maxiter) {
219
+ const n = mat.len(), k = med.length;
220
+ if (!(k === 2)) throw new Error('Only valid for k=2');
221
+ let [loss, assi, data] = initial_assignment_k2(mat, med);
222
+ let lastswap = n, n_swaps = 0, iter = 0;
223
+ while (iter < maxiter) {
224
+ iter += 1;
225
+ const swaps_before = n_swaps, lastloss = loss;
226
+ for (let j = 0; j < n; j++) {
227
+ if (j === lastswap) {
228
+ break;
229
+ }
230
+ if (j === med[assi[j]]) {
231
+ continue; // This already is a medoid
232
+ }
233
+ const [newloss, b] = find_best_swap_k2(mat, data, j); // assi not used, see below
234
+ if (newloss >= loss) {
235
+ continue; // No improvement
236
+ }
237
+ n_swaps += 1;
238
+ lastswap = j;
239
+ // perform the swap
240
+ loss = do_swap_k2(mat, med, assi, data, b, j);
241
+ }
242
+ if (n_swaps === swaps_before || loss >= lastloss) {
243
+ break; // converged
244
+ }
245
+ }
246
+ loss = 1 - loss / n;
247
+ return { loss, assi, nIter: iter, nSwaps: n_swaps };
248
+ }
249
+
250
+ /** Perform the initial assignment to medoids, for k=2 only. Returns [loss, assi, data] (data = [d0,d1] pairs). */
251
+ export function initial_assignment_k2(mat, med) {
252
+ const n = mat.len(), k = med.length;
253
+ if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
254
+ if (!(n <= U32_MAX)) throw new Error('N is too large');
255
+ if (!(k === 2)) throw new Error('k must be 2');
256
+ const assi = new Array(mat.len()).fill(0);
257
+ const data = new Array(mat.len());
258
+ for (let _i = 0; _i < data.length; _i++) data[_i] = [0, 0];
259
+ let loss = 0;
260
+ for (let i = 0; i < data.length; i++) {
261
+ const d = data[i];
262
+ d[0] = mat.get(i, med[0]);
263
+ d[1] = mat.get(i, med[1]);
264
+ if (d[0] < d[1]) {
265
+ assi[i] = 0;
266
+ loss += _loss(d[0], d[1]); // return
267
+ } else {
268
+ assi[i] = 1;
269
+ loss += _loss(d[1], d[0]); // return
270
+ }
271
+ }
272
+ return [loss, assi, data];
273
+ }
274
+
275
+ /** Find the best swap for object j - FastMSC version, k=2. Returns [loss, b]. */
276
+ export function find_best_swap_k2(mat, data, j) {
277
+ const ploss = [0, 0];
278
+ for (let o = 0; o < data.length; o++) {
279
+ const d = data[o];
280
+ const doj = mat.get(o, j);
281
+ // We do not use the assignment here, because we stored d0/d1 by medoid position, not closeness
282
+ ploss[0] += (doj < d[1]) ? _loss(doj, d[1]) : _loss(d[1], doj);
283
+ ploss[1] += (doj < d[0]) ? _loss(doj, d[0]) : _loss(d[0], doj);
284
+ }
285
+ const [b, bloss] = find_min(ploss);
286
+ return [bloss, b];
287
+ }
288
+
289
+ /** Perform a single swap, k=2. Returns RAW loss. */
290
+ export function do_swap_k2(mat, med, assi, data, b, j) {
291
+ const n = mat.len();
292
+ if (!(b < med.length)) throw new Error('invalid medoid number');
293
+ if (!(j < n)) throw new Error('invalid object number');
294
+ med[b] = j;
295
+ // Its nicer to have the if outside, even though this looks duplicated
296
+ if (b === 0) {
297
+ let acc = 0;
298
+ for (let o = 0; o < data.length; o++) {
299
+ const d = data[o];
300
+ if (o === j) {
301
+ assi[o] = 0;
302
+ d[0] = 0;
303
+ acc += 0;
304
+ continue;
305
+ }
306
+ const doj = mat.get(o, j);
307
+ d[0] = doj;
308
+ if (doj < d[1] || (doj === d[1] && assi[o] === 0)) {
309
+ assi[o] = 0;
310
+ acc += _loss(doj, d[1]); // return
311
+ } else {
312
+ assi[o] = 1;
313
+ acc += _loss(d[1], doj); // return
314
+ }
315
+ }
316
+ return acc;
317
+ } else { // b == 1
318
+ let acc = 0;
319
+ for (let o = 0; o < data.length; o++) {
320
+ const d = data[o];
321
+ if (o === j) {
322
+ assi[o] = 1;
323
+ d[1] = 0;
324
+ acc += 0;
325
+ continue;
326
+ }
327
+ const doj = mat.get(o, j);
328
+ d[1] = doj;
329
+ if (doj < d[0] || (doj === d[0] && assi[o] === 1)) {
330
+ assi[o] = 1;
331
+ acc += _loss(doj, d[0]); // return
332
+ } else {
333
+ assi[o] = 0;
334
+ acc += _loss(d[0], doj); // return
335
+ }
336
+ }
337
+ return acc;
338
+ }
339
+ }