w-cluster 1.0.19 → 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 (73) 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 +18 -0
  44. package/src/WCluster.mjs +6 -2
  45. package/src/WClusterCore.mjs +30 -21
  46. package/src/WClusterMat.mjs +98 -24
  47. package/src/WPCAMat.mjs +2 -2
  48. package/src/jaccardBitset.mjs +63 -0
  49. package/src/k-medoids/README.md +124 -0
  50. package/src/k-medoids/alternating.mjs +72 -0
  51. package/src/k-medoids/arrayadapter.mjs +35 -0
  52. package/src/k-medoids/dynmsc.mjs +183 -0
  53. package/src/k-medoids/fastermsc.mjs +339 -0
  54. package/src/k-medoids/fasterpam.mjs +233 -0
  55. package/src/k-medoids/fastmsc.mjs +94 -0
  56. package/src/k-medoids/fastpam1.mjs +60 -0
  57. package/src/k-medoids/index.mjs +37 -0
  58. package/src/k-medoids/initialization.mjs +35 -0
  59. package/src/k-medoids/package.json +8 -0
  60. package/src/k-medoids/pam.mjs +203 -0
  61. package/src/k-medoids/pammedsil.mjs +235 -0
  62. package/src/k-medoids/pamsil.mjs +153 -0
  63. package/src/k-medoids/par_fasterpam.mjs +62 -0
  64. package/src/k-medoids/par_silhouette.mjs +54 -0
  65. package/src/k-medoids/silhouette.mjs +121 -0
  66. package/src/k-medoids/test.mjs +357 -0
  67. package/src/k-medoids/util.mjs +63 -0
  68. package/test/jaccardBitset.test.mjs +141 -0
  69. package/test/kMeans.test.mjs +278 -0
  70. package/test/kMedoids-large-suggest.test.mjs +140 -0
  71. package/test/kMedoids-large.test.mjs +142 -0
  72. package/test/{cluster.test.mjs → kMedoids-simple.test.mjs} +5 -5
  73. package/toolg/gDocsExams.mjs +1 -1
@@ -0,0 +1,235 @@
1
+ // Ported 1:1 from src/pammedsil.rs (PAMMEDSIL 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, do_swap } from './fastermsc.mjs';
5
+
6
+ /**
7
+ * Run the original PAMMEDSIL SWAP algorithm (no initialization, but given initial medoids).
8
+ * @param {object} mat - pairwise distance matrix
9
+ * @param {number[]} med - the list of medoids (mutated in place)
10
+ * @param {number} maxiter - the maximum number of iterations allowed
11
+ * returns { loss, assi, nIter, nSwaps }
12
+ */
13
+ export function pammedsil_swap(mat, med, maxiter) {
14
+ mat = arrayAdapter(mat);
15
+ const [loss, data] = initial_assignment(mat, med);
16
+ return pammedsil_optimize(mat, med, data, maxiter, loss);
17
+ }
18
+
19
+ /**
20
+ * Run the original PAM BUILD algorithm combined with the PAMMEDSIL SWAP.
21
+ * @param {object} mat - pairwise distance matrix
22
+ * @param {number} k - the number of medoids to pick
23
+ * @param {number} maxiter - the maximum number of iterations allowed
24
+ * returns { loss, assi, meds, nIter, nSwaps }
25
+ */
26
+ export function pammedsil(mat, k, maxiter) {
27
+ mat = arrayAdapter(mat);
28
+ const n = mat.len();
29
+ if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
30
+ if (!(n <= U32_MAX)) throw new Error('N is too large');
31
+ if (!(k > 0 && k < U32_MAX)) throw new Error('invalid N');
32
+ if (!(k <= n)) throw new Error('k must be at most N');
33
+ const meds = [];
34
+ const data = [];
35
+ const loss = pammedsil_build_initialize(mat, meds, data, k);
36
+ const { loss: nloss, assi, nIter, nSwaps } = pammedsil_optimize(mat, meds, data, maxiter, loss);
37
+ return { loss: nloss, assi, meds, nIter, nSwaps }; // also return medoids
38
+ }
39
+
40
+ /** Main optimization function of PAMMEDSIL, not exposed (use pammedsil_swap or pammedsil) */
41
+ function pammedsil_optimize(mat, med, data, maxiter, loss) {
42
+ const n = mat.len(), k = med.length;
43
+ if (k === 1) {
44
+ const assi = new Array(n).fill(0);
45
+ const [swapped, lloss] = choose_medoid_within_partition(mat, assi, med, 0);
46
+ return { loss: lloss, assi, nIter: 1, nSwaps: swapped ? 1 : 0 };
47
+ }
48
+ let n_swaps = 0, iter = 0;
49
+ while (iter < maxiter) {
50
+ iter += 1;
51
+ let best = [0, k, USIZE_MAX];
52
+ for (let j = 0; j < n; j++) {
53
+ if (j === med[data[j].near.i]) {
54
+ continue; // This already is a medoid
55
+ }
56
+ const [change, b] = (k === 2)
57
+ ? find_best_swap_pammedsil_k2(mat, med, data, j)
58
+ : find_best_swap_pammedsil(mat, med, data, j);
59
+ if (change <= best[0]) {
60
+ continue; // No improvement
61
+ }
62
+ best = [change, b, j];
63
+ }
64
+ if (best[0] > 0) {
65
+ n_swaps += 1;
66
+ // perform the swap
67
+ const newloss = do_swap(mat, med, data, best[1], best[2]);
68
+ if (newloss >= loss) {
69
+ break; // Probably numerically unstable now.
70
+ }
71
+ loss = newloss;
72
+ } else {
73
+ break; // No improvement, or NaN.
74
+ }
75
+ }
76
+ const assi = data.map((x) => x.near.i);
77
+ loss = 1 - loss / n;
78
+ return { loss, assi, nIter: iter, nSwaps: n_swaps };
79
+ }
80
+
81
+ /** Find the best swap for object j. Returns [change, b]. */
82
+ function find_best_swap_pammedsil(mat, med, data, j) {
83
+ const recj = data[j];
84
+ let best = [0, USIZE_MAX];
85
+ for (let m = 0; m < med.length; m++) {
86
+ let acc = _loss(recj.near.d, recj.seco.d); // j becomes medoid
87
+ for (let o = 0; o < data.length; o++) {
88
+ const reco = data[o];
89
+ if (o === j) {
90
+ continue;
91
+ }
92
+ const doj = mat.get(o, j);
93
+ // Current medoid is being replaced:
94
+ if (reco.near.i === m) {
95
+ if (doj < reco.seco.d) {
96
+ // Assign to new medoid:
97
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(doj, reco.seco.d);
98
+ } else if (doj < reco.third.d) {
99
+ // Assign to second nearest instead:
100
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.seco.d, doj);
101
+ } else {
102
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.seco.d, reco.third.d);
103
+ }
104
+ } else if (reco.seco.i === m) {
105
+ if (doj < reco.near.d) {
106
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(doj, reco.near.d);
107
+ } else if (doj < reco.third.d) {
108
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.near.d, doj);
109
+ } else {
110
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.near.d, reco.third.d);
111
+ }
112
+ } else {
113
+ if (doj < reco.near.d) {
114
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(doj, reco.near.d);
115
+ } else if (doj < reco.seco.d) {
116
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.near.d, doj);
117
+ }
118
+ }
119
+ }
120
+ if (acc > best[0]) {
121
+ best = [acc, m];
122
+ }
123
+ }
124
+ return best;
125
+ }
126
+
127
+ /** Find the best swap for object j (k=2 variant). Returns [change, b]. */
128
+ function find_best_swap_pammedsil_k2(mat, med, data, j) {
129
+ const recj = data[j];
130
+ let best = [0, USIZE_MAX];
131
+ for (let m = 0; m < med.length; m++) {
132
+ let acc = _loss(recj.near.d, recj.seco.d); // j becomes medoid
133
+ for (let o = 0; o < data.length; o++) {
134
+ const reco = data[o];
135
+ if (o === j) {
136
+ continue;
137
+ }
138
+ const doj = mat.get(o, j);
139
+ // Current medoid is being replaced:
140
+ if (reco.near.i === m) {
141
+ if (doj < reco.seco.d) {
142
+ // Assign to new medoid:
143
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(doj, reco.seco.d);
144
+ } else {
145
+ // Assign to second nearest instead:
146
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.seco.d, doj);
147
+ }
148
+ } else if (reco.seco.i === m) {
149
+ if (doj < reco.near.d) {
150
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(doj, reco.near.d);
151
+ } else {
152
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.near.d, doj);
153
+ }
154
+ } else {
155
+ if (doj < reco.near.d) {
156
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(doj, reco.near.d);
157
+ } else if (doj < reco.seco.d) {
158
+ acc += _loss(reco.near.d, reco.seco.d) - _loss(reco.near.d, doj);
159
+ }
160
+ }
161
+ }
162
+ if (acc > best[0]) {
163
+ best = [acc, m];
164
+ }
165
+ }
166
+ return best;
167
+ }
168
+
169
+ /** Not exposed. Use pammedsil_build or pammedsil. Pushes into meds & data. Returns loss. */
170
+ function pammedsil_build_initialize(mat, meds, data, k) {
171
+ const n = mat.len();
172
+ // choose first medoid
173
+ let best = [0, k];
174
+ for (let i = 0; i < n; i++) {
175
+ let sum = 0;
176
+ for (let j = 0; j < n; j++) {
177
+ if (j !== i) {
178
+ sum += mat.get(j, i);
179
+ }
180
+ }
181
+ if (i === 0 || sum < best[0]) {
182
+ best = [sum, i];
183
+ }
184
+ }
185
+ let loss = best[0];
186
+ meds.push(best[1]);
187
+ for (let j = 0; j < n; j++) {
188
+ data.push(new Reco(0, mat.get(j, best[1]), U32_MAX, 0, U32_MAX, 0));
189
+ }
190
+ // choose remaining medoids
191
+ for (let l = 1; l < k; l++) {
192
+ best = [0, k];
193
+ for (let i = 1; i < data.length; i++) {
194
+ let sum = -data[i].near.d;
195
+ for (let jj = 0; jj < data.length; jj++) {
196
+ const dj = data[jj];
197
+ if (jj !== i) {
198
+ const d = mat.get(jj, i);
199
+ if (d < dj.near.d) {
200
+ sum += d - dj.near.d;
201
+ }
202
+ }
203
+ }
204
+ if (i === 0 || sum < best[0]) {
205
+ best = [sum, i];
206
+ }
207
+ }
208
+ if (best[0] >= 0) { break; } // No more improvement, duplicates
209
+ // Update assignments:
210
+ loss = 0;
211
+ for (let jj = 0; jj < data.length; jj++) {
212
+ const recj = data[jj];
213
+ if (jj === best[1]) {
214
+ recj.third = recj.seco.clone();
215
+ recj.seco = recj.near.clone();
216
+ recj.near = new DistancePair(l, 0);
217
+ } else {
218
+ const dj = mat.get(jj, best[1]);
219
+ if (dj < recj.near.d) {
220
+ recj.third = recj.seco.clone();
221
+ recj.seco = recj.near.clone();
222
+ recj.near = new DistancePair(l, dj);
223
+ } else if (recj.seco.i === U32_MAX || dj < recj.seco.d) {
224
+ recj.third = recj.seco.clone();
225
+ recj.seco = new DistancePair(l, dj);
226
+ } else if (recj.third.i === U32_MAX || dj < recj.third.d) {
227
+ recj.third = new DistancePair(l, dj);
228
+ }
229
+ }
230
+ loss += _loss(recj.near.d, recj.seco.d);
231
+ }
232
+ meds.push(best[1]);
233
+ }
234
+ return loss;
235
+ }
@@ -0,0 +1,153 @@
1
+ // PAMSIL: PAM combined with direct Silhouette optimization.
2
+ // Ported 1:1 from src/pamsil.rs
3
+
4
+ import { arrayAdapter } from './arrayadapter.mjs';
5
+ import { USIZE_MAX, choose_medoid_within_partition } from './util.mjs';
6
+ import { assign_nearest } from './alternating.mjs';
7
+ import { silhouette } from './silhouette.mjs';
8
+
9
+ /**
10
+ * Run the original PAMSIL SWAP algorithm (no BUILD, but given initial medoids).
11
+ *
12
+ * @param {*} mat - a pairwise distance matrix
13
+ * @param {number[]} med - the list of medoids (mutated in place)
14
+ * @param {number} maxiter - the maximum number of iterations allowed
15
+ * @returns {{ loss: number, assi: number[], nIter: number, nSwaps: number }}
16
+ */
17
+ export function pamsil(mat, k, maxiter) {
18
+ mat = arrayAdapter(mat);
19
+ const n = mat.len();
20
+ if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
21
+ if (!(n <= 4294967295)) throw new Error('N is too large');
22
+ if (!(k > 0 && k < 4294967295)) throw new Error('invalid N');
23
+ if (!(k <= n)) throw new Error('k must be at most N');
24
+ const meds = [];
25
+ const assi = new Array(n).fill(0);
26
+ pamsil_build_initialize(mat, meds, assi, k);
27
+ const [nloss, n_iter, n_swap] = pamsil_optimize(mat, meds, assi, maxiter);
28
+ return { loss: nloss, assi, meds, nIter: n_iter, nSwaps: n_swap }; // also return medoids
29
+ }
30
+
31
+ /**
32
+ * Run the original PAMSIL SWAP algorithm (no BUILD, but given initial medoids).
33
+ *
34
+ * @param {*} mat - a pairwise distance matrix
35
+ * @param {number[]} med - the list of medoids (mutated in place)
36
+ * @param {number} maxiter - the maximum number of iterations allowed
37
+ * @returns {{ loss: number, assi: number[], nIter: number, nSwaps: number }}
38
+ */
39
+ export function pamsil_swap(mat, med, maxiter) {
40
+ mat = arrayAdapter(mat);
41
+ const n = mat.len();
42
+ const assi = new Array(n).fill(0);
43
+ assign_nearest(mat, med, assi);
44
+ const [nloss, n_iter, n_swap] = pamsil_optimize(mat, med, assi, maxiter);
45
+ return { loss: nloss, assi, nIter: n_iter, nSwaps: n_swap };
46
+ }
47
+
48
+ /**
49
+ * Main optimization function of PAMSIL, not exposed (use pamsil_swap or pamsil).
50
+ * @returns {[number, number, number]} [loss, n_iter, n_swaps]
51
+ */
52
+ function pamsil_optimize(mat, med, assi, maxiter) {
53
+ const n = mat.len(), k = med.length;
54
+ if (k === 1) {
55
+ const [swapped, loss] = choose_medoid_within_partition(mat, assi, med, 0);
56
+ return [loss, 1, swapped ? 1 : 0];
57
+ }
58
+ let n_swaps = 0, iter = 0;
59
+ let sil = silhouette(mat, assi, false).sil;
60
+ while (iter < maxiter) {
61
+ iter += 1;
62
+ let best = [0, k, USIZE_MAX];
63
+ for (let m = 0; m < k; m++) {
64
+ const medm = med[m]; // preseve previous value
65
+ for (let j = 0; j < n; j++) {
66
+ if (j === medm || j === med[assi[j]]) {
67
+ continue; // This already is a medoid
68
+ }
69
+ med[m] = j; // replace
70
+ assign_nearest(mat, med, assi);
71
+ const siltemp = silhouette(mat, assi, false).sil;
72
+ if (siltemp <= best[0]) {
73
+ continue; // No improvement
74
+ }
75
+ best = [siltemp, m, j];
76
+ }
77
+ med[m] = medm; // restore
78
+ }
79
+ if (best[0] <= sil) {
80
+ break; // no improvement
81
+ }
82
+ n_swaps += 1;
83
+ med[best[1]] = best[2];
84
+ sil = best[0];
85
+ }
86
+ assign_nearest(mat, med, assi);
87
+ return [sil, iter, n_swaps];
88
+ }
89
+
90
+ /**
91
+ * Not exposed. Use pamsil_build or pamsil.
92
+ * Standard PAM BUILD storing nearest distance per point in a plain number array `data`.
93
+ * @returns {number} loss
94
+ */
95
+ function pamsil_build_initialize(mat, meds, assi, k) {
96
+ const n = mat.len();
97
+ // choose first medoid
98
+ let best = [0, k];
99
+ for (let i = 0; i < n; i++) {
100
+ let sum = 0;
101
+ for (let j = 0; j < n; j++) {
102
+ if (j !== i) {
103
+ sum += mat.get(j, i);
104
+ }
105
+ }
106
+ if (i === 0 || sum < best[0]) {
107
+ best = [sum, i];
108
+ }
109
+ }
110
+ let loss = best[0];
111
+ meds.push(best[1]);
112
+ const data = [];
113
+ assi.fill(0);
114
+ for (let j = 0; j < n; j++) {
115
+ data.push(mat.get(j, best[1]));
116
+ }
117
+ // choose remaining medoids
118
+ for (let _ = 1; _ < k; _++) {
119
+ best = [0, k];
120
+ for (let i = 0; i < data.length; i++) {
121
+ const di = data[i];
122
+ let sum = -di;
123
+ for (let j = 0; j < data.length; j++) {
124
+ const dnear = data[j];
125
+ if (j !== i) {
126
+ const d = mat.get(j, i);
127
+ if (d < dnear) {
128
+ sum += d - dnear;
129
+ }
130
+ }
131
+ }
132
+ if (i === 0 || sum < best[0]) {
133
+ best = [sum, i];
134
+ }
135
+ }
136
+ if (!(best[0] <= 0)) throw new Error('assertion failed: best.0 <= L::zero()');
137
+ // Update assignments:
138
+ loss = 0;
139
+ for (let j = 0; j < data.length; j++) {
140
+ if (j === best[1]) {
141
+ data[j] = 0;
142
+ continue;
143
+ }
144
+ const dj = mat.get(j, best[1]);
145
+ if (dj < data[j]) {
146
+ data[j] = dj;
147
+ }
148
+ loss += data[j];
149
+ }
150
+ meds.push(best[1]);
151
+ }
152
+ return loss;
153
+ }
@@ -0,0 +1,62 @@
1
+ // Parallel FasterPAM algorithm, ported 1:1 from src/par_fasterpam.rs
2
+ // JS is single-threaded; parallelism is collapsed to a sequential equivalent
3
+ // by delegating to fasterpam.mjs helpers (result-identical to the Rust parallel internals).
4
+ import { arrayAdapter } from './arrayadapter.mjs';
5
+ import { choose_medoid_within_partition } from './util.mjs';
6
+ import { initial_assignment, find_best_swap, do_swap, update_removal_loss } from './fasterpam.mjs';
7
+ import { shuffle } from './initialization.mjs';
8
+
9
+ /**
10
+ * Run the FasterPAM algorithm (parallel version).
11
+ *
12
+ * For small data sets (n<1000) it is usually faster to use the non-parallel version.
13
+ *
14
+ * @param mat - a pairwise distance matrix
15
+ * @param med - the list of medoids (mutated in place)
16
+ * @param maxiter - the maximum number of iterations allowed
17
+ * @param rng - random number generator for shuffling the input data
18
+ * @returns { loss, assi, nIter, nSwaps }
19
+ *
20
+ * Panics (throws) when the dissimilarity matrix is not square, or k is 0 or larger than N.
21
+ */
22
+ export function par_fasterpam(mat, med, maxiter, rng = Math.random) {
23
+ mat = arrayAdapter(mat);
24
+ const n = mat.len(), k = med.length;
25
+ if (k === 1) {
26
+ const assi = new Array(n).fill(0);
27
+ const [swapped, loss] = choose_medoid_within_partition(mat, assi, med, 0);
28
+ return { loss, assi, nIter: 1, nSwaps: swapped ? 1 : 0 };
29
+ }
30
+ let [loss, data] = initial_assignment(mat, med);
31
+
32
+ let removal_loss = new Array(k).fill(0);
33
+ update_removal_loss(data, removal_loss);
34
+ let lastswap = n, n_swaps = 0, iter = 0;
35
+ const seq = shuffle(rng, n); // random shuffling
36
+ while (iter < maxiter) {
37
+ iter += 1;
38
+ const swaps_before = n_swaps, lastloss = loss;
39
+ for (const j of seq) {
40
+ if (j === lastswap) {
41
+ break;
42
+ }
43
+ if (j === med[data[j].near.i]) {
44
+ continue; // This already is a medoid
45
+ }
46
+ const [change, b] = find_best_swap(mat, removal_loss, data, j);
47
+ if (change >= 0) {
48
+ continue; // No improvement
49
+ }
50
+ n_swaps += 1;
51
+ lastswap = j;
52
+ // perform the swap
53
+ loss = do_swap(mat, med, data, b, j);
54
+ update_removal_loss(data, removal_loss);
55
+ }
56
+ if (n_swaps === swaps_before || loss >= lastloss) {
57
+ break; // converged
58
+ }
59
+ }
60
+ const assi = data.map((x) => x.near.i);
61
+ return { loss, assi, nIter: iter, nSwaps: n_swaps };
62
+ }
@@ -0,0 +1,54 @@
1
+ // Parallelism (rayon) collapsed to sequential equivalent for JS.
2
+ import { arrayAdapter } from './arrayadapter.mjs';
3
+ import { checked_div } from './silhouette.mjs';
4
+
5
+ /**
6
+ * Compute the Silhouette of a strict partitional clustering (sequential JS port of parallel Rust impl).
7
+ *
8
+ * @param {object|Array} mat - pairwise distance matrix (will be wrapped by arrayAdapter)
9
+ * @param {number[]} assi - cluster assignment for each point
10
+ * @returns {number} the average silhouette value
11
+ */
12
+ export function par_silhouette(mat, assi) {
13
+ mat = arrayAdapter(mat);
14
+ if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
15
+ let lsum = 0;
16
+ for (let i = 0; i < assi.length; i++) {
17
+ const ai = assi[i];
18
+ // buf[c] = [count, sum] for cluster c
19
+ const buf = [];
20
+ for (let j = 0; j < assi.length; j++) {
21
+ const aj = assi[j];
22
+ while (aj >= buf.length) {
23
+ buf.push([0, 0]);
24
+ }
25
+ if (i !== j) {
26
+ buf[aj][0] += 1;
27
+ buf[aj][1] += mat.get(i, j);
28
+ }
29
+ }
30
+ if (buf[ai][0] > 0) {
31
+ const a = checked_div(buf[ai][1], buf[ai][0]);
32
+ // find minimum average distance to any other cluster (b)
33
+ // mirrors Rust: tmp.next().unwrap_or_else(L::zero) seeds the fold
34
+ let tmp2 = 0;
35
+ let foundFirst = false;
36
+ let b = 0;
37
+ for (let c = 0; c < buf.length; c++) {
38
+ if (c === ai) continue;
39
+ const avg = checked_div(buf[c][1], buf[c][0]);
40
+ if (!foundFirst) {
41
+ tmp2 = avg;
42
+ foundFirst = true;
43
+ b = tmp2;
44
+ } else {
45
+ b = avg < b ? avg : b;
46
+ }
47
+ }
48
+ // if no other cluster exists, foundFirst is false → b = tmp2 = 0
49
+ lsum += checked_div(b - a, a > b ? a : b);
50
+ }
51
+ // else: singleton, contributes 0 (lsum unchanged)
52
+ }
53
+ return lsum / assi.length;
54
+ }
@@ -0,0 +1,121 @@
1
+ // Silhouette evaluation measures, ported from src/silhouette.rs
2
+ import { arrayAdapter } from './arrayadapter.mjs';
3
+
4
+ /**
5
+ * Compute the Silhouette of a strict partitional clustering.
6
+ *
7
+ * The Silhouette, proposed by Peter Rousseeuw in 1987, is a popular internal
8
+ * evaluation measure for clusterings.
9
+ *
10
+ * @param mat - a pairwise distance matrix
11
+ * @param assi - the cluster assignment
12
+ * @param samples - whether to keep the individual samples, or not
13
+ * @returns { sil, samples } where sil is the average silhouette and samples are
14
+ * the individual silhouette values (empty [] if samples = false)
15
+ */
16
+ export function silhouette(mat, assi, samples = false) {
17
+ mat = arrayAdapter(mat);
18
+ if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
19
+ let sil = new Array(samples ? assi.length : 0).fill(0);
20
+ let lsum = 0;
21
+ let buf = []; // array of [count(u32), sum(L)] pairs
22
+ for (let i = 0; i < assi.length; i++) {
23
+ const ai = assi[i];
24
+ buf.length = 0;
25
+ for (let j = 0; j < assi.length; j++) {
26
+ const aj = assi[j];
27
+ while (aj >= buf.length) {
28
+ buf.push([0, 0]);
29
+ }
30
+ if (i !== j) {
31
+ buf[aj][0] += 1;
32
+ buf[aj][1] += mat.get(i, j);
33
+ }
34
+ }
35
+ if (buf.length === 1) {
36
+ return { sil: 0, samples: sil };
37
+ }
38
+ let s;
39
+ if (buf[ai][0] > 0) {
40
+ const a = checked_div(buf[ai][1], buf[ai][0]);
41
+ // Ugly hack to get the min():
42
+ let started = false;
43
+ let b = 0;
44
+ for (let k = 0; k < buf.length; k++) {
45
+ if (k === ai) continue;
46
+ const p = buf[k];
47
+ const y = checked_div(p[1], p[0]);
48
+ if (!started) {
49
+ b = y; // tmp.next().unwrap_or_else(L::zero)
50
+ started = true;
51
+ } else {
52
+ b = y < b ? y : b; // tmp.fold(tmp2, |x, y| if y < x { y } else { x })
53
+ }
54
+ }
55
+ // tmp2 = tmp.next().unwrap_or_else(L::zero): if no element, b stays 0
56
+ s = checked_div(b - a, a > b ? a : b);
57
+ } else {
58
+ s = 0; // singleton
59
+ }
60
+ if (samples) {
61
+ sil[i] = s;
62
+ }
63
+ lsum += s;
64
+ }
65
+ return { sil: lsum / assi.length, samples: sil };
66
+ }
67
+
68
+ /**
69
+ * Compute the Medoid Silhouette of a clustering.
70
+ *
71
+ * The Medoid Silhouette is an approximation to the original Silhouette where the
72
+ * distance to the cluster medoid is used instead of the average distance.
73
+ *
74
+ * @param mat - a pairwise distance matrix
75
+ * @param meds - the medoid list
76
+ * @param samples - whether to keep the individual samples, or not
77
+ * @returns { sil, samples } where sil is the average medoid silhouette and
78
+ * samples are the individual values (empty [] if samples = false)
79
+ */
80
+ export function medoid_silhouette(mat, meds, samples = false) {
81
+ mat = arrayAdapter(mat);
82
+ const n = mat.len(), k = meds.length;
83
+ if (!mat.isSquare()) throw new Error('Dissimilarity matrix is not square');
84
+ if (!(n <= U32_MAX_GUARD)) throw new Error('N is too large');
85
+ let sil = new Array(samples ? n : 0).fill(1);
86
+ if (k === 1) { return { sil: 1, samples: sil }; } // not really well-defined
87
+ if (!(k >= 2 && k <= n)) throw new Error('invalid k, must be over 1 and at most N');
88
+ let loss = 0;
89
+ for (let i = 0; i < n; i++) {
90
+ const d1 = mat.get(i, meds[0]), d2 = mat.get(i, meds[1]);
91
+ let best = d1 < d2 ? [d1, d2] : [d2, d1];
92
+ for (let s2 = 2; s2 < meds.length; s2++) {
93
+ const m = meds[s2];
94
+ const d = mat.get(i, m);
95
+ if (d < best[0]) {
96
+ best = [d, best[0]];
97
+ } else if (d < best[1]) {
98
+ best = [best[0], d];
99
+ }
100
+ }
101
+ if (best[0] !== 0) {
102
+ const s = best[0] / best[1];
103
+ if (samples) { sil[i] = 1 - s; }
104
+ loss += s;
105
+ }
106
+ }
107
+ loss = 1 - loss / n;
108
+ return { sil: loss, samples: sil };
109
+ }
110
+
111
+ // u32::MAX, used only for the "N is too large" assertion.
112
+ const U32_MAX_GUARD = 4294967295;
113
+
114
+ // helper function, returns 0 on division by 0
115
+ export function checked_div(x, y) {
116
+ if (y > 0) {
117
+ return x / y;
118
+ } else {
119
+ return 0;
120
+ }
121
+ }