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,357 @@
1
+ // Translated unit tests for rust-kmedoids -> JS port.
2
+ // Self-contained Node test runner. Run with: node test.mjs
3
+ //
4
+ // Ported faithfully from the Rust #[cfg(test)] modules in src/*.rs.
5
+ // Each algorithm MUTATES the medoid array passed in, so every test case
6
+ // builds a FRESH med array.
7
+
8
+ import * as km from './index.mjs';
9
+ import { LowerTriangle, DenseMatrix } from './arrayadapter.mjs';
10
+
11
+ // ---- tiny harness ----------------------------------------------------------
12
+ let passed = 0;
13
+ let failed = 0;
14
+
15
+ function check(name, cond) {
16
+ if (cond) {
17
+ passed++;
18
+ console.log(`PASS ${name}`);
19
+ } else {
20
+ failed++;
21
+ console.log(`FAIL ${name}`);
22
+ }
23
+ }
24
+
25
+ function eqArr(a, b) {
26
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
27
+ if (a.length !== b.length) return false;
28
+ for (let i = 0; i < a.length; i++) {
29
+ if (a[i] !== b[i]) return false;
30
+ }
31
+ return true;
32
+ }
33
+
34
+ function near(a, b) {
35
+ return Math.abs(a - b) < 1e-9;
36
+ }
37
+
38
+ // The standard small dataset (LowerTriangle, n=5, without diagonal).
39
+ const D = new LowerTriangle(5, [1, 2, 3, 4, 5, 6, 7, 8, 9, 1]);
40
+
41
+ // ============================================================================
42
+ // FasterPAM
43
+ // ============================================================================
44
+ {
45
+ // fasterpam(D, [0,1], 10): loss=4, nSwaps=2, nIter=2, assi=[0,0,0,1,1], med=[0,3]
46
+ const med = [0, 1];
47
+ const r = km.fasterpam(D, med, 10);
48
+ const sil = km.silhouette(D, r.assi, false).sil;
49
+ check('fasterpam simple: loss', near(r.loss, 4));
50
+ check('fasterpam simple: nSwaps', r.nSwaps === 2);
51
+ check('fasterpam simple: nIter', r.nIter === 2);
52
+ check('fasterpam simple: assi', eqArr(r.assi, [0, 0, 0, 1, 1]));
53
+ check('fasterpam simple: med', eqArr(med, [0, 3]));
54
+ check('fasterpam simple: sil', near(sil, 0.7522494172494172));
55
+ }
56
+ {
57
+ // fasterpam(D, [1], 10): loss=14, nSwaps=1, nIter=1, assi=[0,0,0,0,0], med=[0]
58
+ const med = [1];
59
+ const r = km.fasterpam(D, med, 10);
60
+ const sil = km.silhouette(D, r.assi, false).sil;
61
+ check('fasterpam single-cluster: loss', near(r.loss, 14));
62
+ check('fasterpam single-cluster: nSwaps', r.nSwaps === 1);
63
+ check('fasterpam single-cluster: nIter', r.nIter === 1);
64
+ check('fasterpam single-cluster: assi', eqArr(r.assi, [0, 0, 0, 0, 0]));
65
+ check('fasterpam single-cluster: med', eqArr(med, [0]));
66
+ check('fasterpam single-cluster: sil', near(sil, 0));
67
+ }
68
+
69
+ // ============================================================================
70
+ // FastPAM1
71
+ // ============================================================================
72
+ {
73
+ // fastpam1(D, [0,1], 10): loss=4, nSwaps=1, nIter=2, assi=[0,0,0,1,1], med=[0,3]
74
+ const med = [0, 1];
75
+ const r = km.fastpam1(D, med, 10);
76
+ const sil = km.silhouette(D, r.assi, false).sil;
77
+ check('fastpam1 simple: loss', near(r.loss, 4));
78
+ check('fastpam1 simple: nSwaps', r.nSwaps === 1);
79
+ check('fastpam1 simple: nIter', r.nIter === 2);
80
+ check('fastpam1 simple: assi', eqArr(r.assi, [0, 0, 0, 1, 1]));
81
+ check('fastpam1 simple: med', eqArr(med, [0, 3]));
82
+ check('fastpam1 simple: sil', near(sil, 0.7522494172494172));
83
+ }
84
+
85
+ // ============================================================================
86
+ // Alternating
87
+ // ============================================================================
88
+ {
89
+ // alternating(D, [0,1], 10): nIter=3, loss=4, assi=[1,1,1,0,0], med=[3,0]
90
+ const med = [0, 1];
91
+ const r = km.alternating(D, med, 10);
92
+ const sil = km.silhouette(D, r.assi, false).sil;
93
+ check('alternating: nIter', r.nIter === 3);
94
+ check('alternating: loss', near(r.loss, 4));
95
+ check('alternating: assi', eqArr(r.assi, [1, 1, 1, 0, 0]));
96
+ check('alternating: med', eqArr(med, [3, 0]));
97
+ check('alternating: sil', near(sil, 0.7522494172494172));
98
+ }
99
+
100
+ // ============================================================================
101
+ // PAM
102
+ // ============================================================================
103
+ {
104
+ // pam_swap(D, [0,1], 10): loss=4, nSwaps=1, nIter=2, assi=[0,0,0,1,1], med=[0,3]
105
+ const med = [0, 1];
106
+ const r = km.pam_swap(D, med, 10);
107
+ const sil = km.silhouette(D, r.assi, false).sil;
108
+ check('pam_swap: loss', near(r.loss, 4));
109
+ check('pam_swap: nSwaps', r.nSwaps === 1);
110
+ check('pam_swap: nIter', r.nIter === 2);
111
+ check('pam_swap: assi', eqArr(r.assi, [0, 0, 0, 1, 1]));
112
+ check('pam_swap: med', eqArr(med, [0, 3]));
113
+ check('pam_swap: sil', near(sil, 0.7522494172494172));
114
+ }
115
+ {
116
+ // pam_build(D, 2): loss=4, assi=[0,0,0,1,1], meds=[0,3]
117
+ const r = km.pam_build(D, 2);
118
+ const sil = km.silhouette(D, r.assi, false).sil;
119
+ check('pam_build: loss', near(r.loss, 4));
120
+ check('pam_build: assi', eqArr(r.assi, [0, 0, 0, 1, 1]));
121
+ check('pam_build: meds', eqArr(r.meds, [0, 3]));
122
+ check('pam_build: sil', near(sil, 0.7522494172494172));
123
+ }
124
+ {
125
+ // pam(D, 2, 10): nSwaps=0, nIter=1, loss=4, assi=[0,0,0,1,1], meds=[0,3]
126
+ const r = km.pam(D, 2, 10);
127
+ const sil = km.silhouette(D, r.assi, false).sil;
128
+ check('pam: nSwaps', r.nSwaps === 0);
129
+ check('pam: nIter', r.nIter === 1);
130
+ check('pam: loss', near(r.loss, 4));
131
+ check('pam: assi', eqArr(r.assi, [0, 0, 0, 1, 1]));
132
+ check('pam: meds', eqArr(r.meds, [0, 3]));
133
+ check('pam: sil', near(sil, 0.7522494172494172));
134
+ }
135
+
136
+ // ============================================================================
137
+ // FastMSC
138
+ // ============================================================================
139
+ {
140
+ // fastmsc(D, [0,1,2], 10): loss=0.9047619047619048, nSwaps=1, nIter=2,
141
+ // assi=[0,0,2,1,1], med=[0,3,2]
142
+ const med = [0, 1, 2];
143
+ const r = km.fastmsc(D, med, 10);
144
+ const sil = km.silhouette(D, r.assi, false).sil;
145
+ const msil = km.medoid_silhouette(D, med, false).sil;
146
+ check('fastmsc k3: loss', near(r.loss, 0.9047619047619048));
147
+ check('fastmsc k3: msil', near(msil, 0.9047619047619048));
148
+ check('fastmsc k3: nSwaps', r.nSwaps === 1);
149
+ check('fastmsc k3: nIter', r.nIter === 2);
150
+ check('fastmsc k3: assi', eqArr(r.assi, [0, 0, 2, 1, 1]));
151
+ check('fastmsc k3: med', eqArr(med, [0, 3, 2]));
152
+ check('fastmsc k3: sil', near(sil, 0.5622222222222222));
153
+ }
154
+ {
155
+ // fastmsc(D, [0,1], 10): loss=0.8805555555555555, nSwaps=1, nIter=2,
156
+ // assi=[0,0,0,1,1], med=[0,4]
157
+ const med = [0, 1];
158
+ const r = km.fastmsc(D, med, 10);
159
+ const sil = km.silhouette(D, r.assi, false).sil;
160
+ const msil = km.medoid_silhouette(D, med, false).sil;
161
+ check('fastmsc k2: loss', near(r.loss, 0.8805555555555555));
162
+ check('fastmsc k2: msil', near(msil, 0.8805555555555555));
163
+ check('fastmsc k2: nSwaps', r.nSwaps === 1);
164
+ check('fastmsc k2: nIter', r.nIter === 2);
165
+ check('fastmsc k2: assi', eqArr(r.assi, [0, 0, 0, 1, 1]));
166
+ check('fastmsc k2: med', eqArr(med, [0, 4]));
167
+ check('fastmsc k2: sil', near(sil, 0.7522494172494172));
168
+ }
169
+
170
+ // ============================================================================
171
+ // FasterMSC
172
+ // ============================================================================
173
+ {
174
+ // fastermsc(D, [0,1,2], 10): loss=0.9047619047619048, nSwaps=1, nIter=2,
175
+ // assi=[0,0,2,1,1], med=[0,3,2]
176
+ const med = [0, 1, 2];
177
+ const r = km.fastermsc(D, med, 10);
178
+ const sil = km.silhouette(D, r.assi, false).sil;
179
+ const msil = km.medoid_silhouette(D, med, false).sil;
180
+ check('fastermsc k3: loss', near(r.loss, 0.9047619047619048));
181
+ check('fastermsc k3: msil', near(msil, 0.9047619047619048));
182
+ check('fastermsc k3: nSwaps', r.nSwaps === 1);
183
+ check('fastermsc k3: nIter', r.nIter === 2);
184
+ check('fastermsc k3: assi', eqArr(r.assi, [0, 0, 2, 1, 1]));
185
+ check('fastermsc k3: med', eqArr(med, [0, 3, 2]));
186
+ check('fastermsc k3: sil', near(sil, 0.5622222222222222));
187
+ }
188
+ {
189
+ // fastermsc(D, [0,1], 10): loss=0.8805555555555555, nSwaps=3, nIter=2,
190
+ // assi=[0,0,0,1,1], med=[0,4]
191
+ const med = [0, 1];
192
+ const r = km.fastermsc(D, med, 10);
193
+ const sil = km.silhouette(D, r.assi, false).sil;
194
+ const msil = km.medoid_silhouette(D, med, false).sil;
195
+ check('fastermsc k2: loss', near(r.loss, 0.8805555555555555));
196
+ check('fastermsc k2: msil', near(msil, 0.8805555555555555));
197
+ check('fastermsc k2: nSwaps', r.nSwaps === 3);
198
+ check('fastermsc k2: nIter', r.nIter === 2);
199
+ check('fastermsc k2: assi', eqArr(r.assi, [0, 0, 0, 1, 1]));
200
+ check('fastermsc k2: med', eqArr(med, [0, 4]));
201
+ check('fastermsc k2: sil', near(sil, 0.7522494172494172));
202
+ }
203
+
204
+ // ============================================================================
205
+ // PAMSIL
206
+ // ============================================================================
207
+ {
208
+ // pamsil(D, 2, 10): nSwaps=1, nIter=2, loss=0.7522494172494172,
209
+ // assi=[0,0,0,1,1], meds=[1,3]
210
+ const r = km.pamsil(D, 2, 10);
211
+ const sil = km.silhouette(D, r.assi, false).sil;
212
+ check('pamsil k2: nSwaps', r.nSwaps === 1);
213
+ check('pamsil k2: nIter', r.nIter === 2);
214
+ check('pamsil k2: loss', near(r.loss, 0.7522494172494172));
215
+ check('pamsil k2: assi', eqArr(r.assi, [0, 0, 0, 1, 1]));
216
+ check('pamsil k2: meds', eqArr(r.meds, [1, 3]));
217
+ check('pamsil k2: sil', near(sil, 0.7522494172494172));
218
+ }
219
+ {
220
+ // pamsil(D, 3, 10): nSwaps=1, nIter=2, loss=0.5622222222222222,
221
+ // assi=[0,0,2,1,1], meds=[1,3,2]
222
+ const r = km.pamsil(D, 3, 10);
223
+ const sil = km.silhouette(D, r.assi, false).sil;
224
+ check('pamsil k3: nSwaps', r.nSwaps === 1);
225
+ check('pamsil k3: nIter', r.nIter === 2);
226
+ check('pamsil k3: loss', near(r.loss, 0.5622222222222222));
227
+ check('pamsil k3: assi', eqArr(r.assi, [0, 0, 2, 1, 1]));
228
+ check('pamsil k3: meds', eqArr(r.meds, [1, 3, 2]));
229
+ check('pamsil k3: sil', near(sil, 0.5622222222222222));
230
+ }
231
+ {
232
+ // pamsil_swap(D, [0,1,2], 10): loss=0.5622222222222222, nSwaps=1, nIter=2,
233
+ // assi=[1,1,2,0,0], med=[3,1,2]
234
+ const med = [0, 1, 2];
235
+ const r = km.pamsil_swap(D, med, 10);
236
+ const sil = km.silhouette(D, r.assi, false).sil;
237
+ check('pamsil_swap: loss', near(r.loss, 0.5622222222222222));
238
+ check('pamsil_swap: nSwaps', r.nSwaps === 1);
239
+ check('pamsil_swap: nIter', r.nIter === 2);
240
+ check('pamsil_swap: assi', eqArr(r.assi, [1, 1, 2, 0, 0]));
241
+ check('pamsil_swap: med', eqArr(med, [3, 1, 2]));
242
+ check('pamsil_swap: sil', near(sil, 0.5622222222222222));
243
+ }
244
+
245
+ // ============================================================================
246
+ // PAMMEDSIL
247
+ // ============================================================================
248
+ {
249
+ // pammedsil(D, 3, 10): nSwaps=0, nIter=1, loss=0.9047619047619048,
250
+ // assi=[0,0,2,1,1], meds=[0,3,2]
251
+ const r = km.pammedsil(D, 3, 10);
252
+ const sil = km.silhouette(D, r.assi, false).sil;
253
+ const msil = km.medoid_silhouette(D, r.meds, false).sil;
254
+ check('pammedsil: nSwaps', r.nSwaps === 0);
255
+ check('pammedsil: nIter', r.nIter === 1);
256
+ check('pammedsil: loss', near(r.loss, 0.9047619047619048));
257
+ check('pammedsil: msil', near(msil, 0.9047619047619048));
258
+ check('pammedsil: assi', eqArr(r.assi, [0, 0, 2, 1, 1]));
259
+ check('pammedsil: meds', eqArr(r.meds, [0, 3, 2]));
260
+ check('pammedsil: sil', near(sil, 0.5622222222222222));
261
+ }
262
+ {
263
+ // pammedsil_swap(D, [0,1,2], 10): loss=0.9047619047619048, nSwaps=1, nIter=2,
264
+ // assi=[0,0,2,1,1], med=[0,3,2]
265
+ const med = [0, 1, 2];
266
+ const r = km.pammedsil_swap(D, med, 10);
267
+ const sil = km.silhouette(D, r.assi, false).sil;
268
+ const msil = km.medoid_silhouette(D, med, false).sil;
269
+ check('pammedsil_swap k3: loss', near(r.loss, 0.9047619047619048));
270
+ check('pammedsil_swap k3: msil', near(msil, 0.9047619047619048));
271
+ check('pammedsil_swap k3: nSwaps', r.nSwaps === 1);
272
+ check('pammedsil_swap k3: nIter', r.nIter === 2);
273
+ check('pammedsil_swap k3: assi', eqArr(r.assi, [0, 0, 2, 1, 1]));
274
+ check('pammedsil_swap k3: med', eqArr(med, [0, 3, 2]));
275
+ check('pammedsil_swap k3: sil', near(sil, 0.5622222222222222));
276
+ }
277
+ {
278
+ // pammedsil_swap(D, [0,1], 10): loss=0.8805555555555555, nSwaps=1, nIter=2,
279
+ // assi=[0,0,0,1,1], med=[0,4]
280
+ const med = [0, 1];
281
+ const r = km.pammedsil_swap(D, med, 10);
282
+ const sil = km.silhouette(D, r.assi, false).sil;
283
+ const msil = km.medoid_silhouette(D, med, false).sil;
284
+ check('pammedsil_swap k2: loss', near(r.loss, 0.8805555555555555));
285
+ check('pammedsil_swap k2: msil', near(msil, 0.8805555555555555));
286
+ check('pammedsil_swap k2: nSwaps', r.nSwaps === 1);
287
+ check('pammedsil_swap k2: nIter', r.nIter === 2);
288
+ check('pammedsil_swap k2: assi', eqArr(r.assi, [0, 0, 0, 1, 1]));
289
+ check('pammedsil_swap k2: med', eqArr(med, [0, 4]));
290
+ check('pammedsil_swap k2: sil', near(sil, 0.7522494172494172));
291
+ }
292
+
293
+ // ============================================================================
294
+ // Silhouette standalone
295
+ // ============================================================================
296
+ {
297
+ const sil = km.silhouette(D, [0, 0, 0, 1, 1], false).sil;
298
+ check('silhouette [0,0,0,1,1]', near(sil, 0.7522494172494172));
299
+ }
300
+ {
301
+ const sil = km.silhouette(D, [0, 0, 2, 1, 1], false).sil;
302
+ check('silhouette [0,0,2,1,1]', near(sil, 0.5622222222222222));
303
+ }
304
+
305
+ // ============================================================================
306
+ // DynMSC (2D dense arrays; pass a FIXED initial med for determinism)
307
+ // ============================================================================
308
+ {
309
+ const D4 = [
310
+ [0, 1, 2, 3],
311
+ [1, 0, 4, 5],
312
+ [2, 4, 0, 6],
313
+ [3, 5, 6, 0],
314
+ ];
315
+ const med = [0, 1, 2];
316
+ const r = km.dynmsc(D4, med, 2, 100);
317
+ const msil = km.medoid_silhouette(D4, r.meds, false).sil;
318
+ check('dynmsc D4: loss', near(r.loss, 0.9375));
319
+ check('dynmsc D4: meds.length', r.meds.length === 3);
320
+ check('dynmsc D4: msil', near(msil, 0.9375));
321
+ }
322
+ {
323
+ const D5 = [
324
+ [0, 1, 2, 3, 1],
325
+ [1, 0, 4, 5, 2],
326
+ [2, 4, 0, 6, 3],
327
+ [3, 5, 6, 0, 4],
328
+ [2, 1, 5, 6, 5],
329
+ ];
330
+ const med = [0, 1, 2];
331
+ const r = km.dynmsc(D5, med, 1, 100);
332
+ const msil = km.medoid_silhouette(D5, r.meds, false).sil;
333
+ check('dynmsc D5: loss', near(r.loss, 0.87));
334
+ check('dynmsc D5: meds.length', r.meds.length === 3);
335
+ check('dynmsc D5: msil', near(msil, 0.87));
336
+ }
337
+
338
+ // ============================================================================
339
+ // RNG-dependent (assert only invariants; JS PRNG != Rust StdRng)
340
+ // ============================================================================
341
+ {
342
+ const med = [0, 1];
343
+ const r = km.rand_fasterpam(D, med, 10);
344
+ check('rand_fasterpam: loss near 4', near(r.loss, 4));
345
+ }
346
+ {
347
+ const med = [0, 1];
348
+ const r = km.par_fasterpam(D, med, 10);
349
+ check('par_fasterpam: loss near 4', near(r.loss, 4));
350
+ }
351
+
352
+ // ============================================================================
353
+ // Summary
354
+ // ============================================================================
355
+ console.log('');
356
+ console.log(`Summary: ${passed} passed, ${failed} failed`);
357
+ process.exit(failed > 0 ? 1 : 0);
@@ -0,0 +1,63 @@
1
+ // Utility types and helpers, ported from src/util.rs
2
+ export const U32_MAX = 4294967295;
3
+ export const USIZE_MAX = Number.MAX_SAFE_INTEGER;
4
+
5
+ /** Object id (i) and distance (d) pair. */
6
+ export class DistancePair {
7
+ constructor(i, d) { this.i = i; this.d = d; }
8
+ clone() { return new DistancePair(this.i, this.d); }
9
+ static empty() { return new DistancePair(U32_MAX, 0); }
10
+ }
11
+
12
+ /** Per-point record: nearest + second nearest medoid. */
13
+ export class Rec {
14
+ constructor(i1, d1, i2, d2) { this.near = new DistancePair(i1, d1); this.seco = new DistancePair(i2, d2); }
15
+ clone() { return new Rec(this.near.i, this.near.d, this.seco.i, this.seco.d); }
16
+ static empty() { return new Rec(U32_MAX, 0, U32_MAX, 0); }
17
+ }
18
+
19
+ /** Per-point record: nearest + second + third nearest medoid. */
20
+ export class Reco {
21
+ constructor(i1, d1, i2, d2, i3, d3) {
22
+ this.near = new DistancePair(i1, d1);
23
+ this.seco = new DistancePair(i2, d2);
24
+ this.third = new DistancePair(i3, d3);
25
+ }
26
+ clone() { return new Reco(this.near.i, this.near.d, this.seco.i, this.seco.d, this.third.i, this.third.d); }
27
+ static empty() { return new Reco(U32_MAX, 0, U32_MAX, 0, U32_MAX, 0); }
28
+ }
29
+
30
+ /** Find the minimum (index and value) over an array of numbers. */
31
+ export function find_min(arr) {
32
+ let bi = 0, bv = arr[0];
33
+ for (let i = 1; i < arr.length; i++) { if (arr[i] < bv) { bi = i; bv = arr[i]; } }
34
+ return [bi, bv];
35
+ }
36
+
37
+ /** Find the maximum (index and value) over an array of numbers. */
38
+ export function find_max(arr) {
39
+ let bi = 0, bv = arr[0];
40
+ for (let i = 1; i < arr.length; i++) { if (arr[i] > bv) { bi = i; bv = arr[i]; } }
41
+ return [bi, bv];
42
+ }
43
+
44
+ /** Choose the best medoid within a partition. Mutates med[m]. Returns [changed, sumb]. */
45
+ export function choose_medoid_within_partition(mat, assi, med, m) {
46
+ const first = med[m];
47
+ let best = first;
48
+ let sumb = 0;
49
+ for (let i = 0; i < assi.length; i++) {
50
+ if (first !== i && assi[i] === m) sumb += mat.get(first, i);
51
+ }
52
+ for (let j = 0; j < assi.length; j++) {
53
+ if (j !== first && assi[j] === m) {
54
+ let sumj = 0;
55
+ for (let i = 0; i < assi.length; i++) {
56
+ if (i !== j && assi[i] === m) sumj += mat.get(j, i);
57
+ }
58
+ if (sumj < sumb) { best = j; sumb = sumj; }
59
+ }
60
+ }
61
+ med[m] = best;
62
+ return [best !== first, sumb];
63
+ }
@@ -0,0 +1,141 @@
1
+ import assert from 'assert'
2
+ import WCluster from '../src/WCluster.mjs'
3
+ import { jaccardBitset, packBits, jaccardBits } from '../src/jaccardBitset.mjs'
4
+
5
+
6
+ //確定性PRNG(mulberry32), 使測試資料可重現
7
+ function mulberry32(a) {
8
+ return function() {
9
+ a |= 0
10
+ a = a + 0x6D2B79F5 | 0
11
+ let t = Math.imul(a ^ a >>> 15, 1 | a)
12
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t
13
+ return ((t ^ t >>> 14) >>> 0) / 4294967296
14
+ }
15
+ }
16
+
17
+ //逐維Jaccard(0/1陣列), 作為對照基準
18
+ function jacNaive(a, b) {
19
+ let inter = 0
20
+ let uni = 0
21
+ for (let k = 0; k < a.length; k++) {
22
+ let x = a[k]
23
+ let y = b[k]
24
+ if (x & y) inter++
25
+ if (x | y) uni++
26
+ }
27
+ return uni === 0 ? 0 : 1 - inter / uni
28
+ }
29
+
30
+ //產生G個分離良好的二元blob(交錯排列), 回傳{data, truth}
31
+ function genBinaryBlobs(G, m, D, active, flips, seed) {
32
+ let rnd = mulberry32(seed)
33
+ let bases = []
34
+ for (let g = 0; g < G; g++) {
35
+ let base = new Array(D).fill(0)
36
+ let s = new Set()
37
+ while (s.size < active) {
38
+ s.add(Math.floor(rnd() * D))
39
+ }
40
+ s.forEach((i) => {
41
+ base[i] = 1
42
+ })
43
+ bases.push(base)
44
+ }
45
+ let data = []
46
+ let truth = []
47
+ for (let i = 0; i < m; i++) {
48
+ for (let g = 0; g < G; g++) {
49
+ let v = bases[g].slice()
50
+ for (let f = 0; f < flips; f++) {
51
+ let p = Math.floor(rnd() * D)
52
+ v[p] = v[p] ? 0 : 1
53
+ }
54
+ data.push(v)
55
+ truth.push(g)
56
+ }
57
+ }
58
+ return { data, truth }
59
+ }
60
+
61
+ //cluster purity(各分群以多數ground-truth標籤計, 全對為1)
62
+ function purity(ginds, truth) {
63
+ let correct = 0
64
+ for (let g of ginds) {
65
+ let cnt = {}
66
+ for (let i of g) {
67
+ cnt[truth[i]] = (cnt[truth[i]] || 0) + 1
68
+ }
69
+ correct += Math.max(...Object.values(cnt))
70
+ }
71
+ return correct / truth.length
72
+ }
73
+
74
+ //flat是否恰好涵蓋0..n-1各一次
75
+ function coversAll(ginds, n) {
76
+ let flat = ginds.flat().sort((a, b) => a - b)
77
+ if (flat.length !== n) {
78
+ return false
79
+ }
80
+ for (let i = 0; i < n; i++) {
81
+ if (flat[i] !== i) {
82
+ return false
83
+ }
84
+ }
85
+ return true
86
+ }
87
+
88
+
89
+ describe(`jaccardBitset`, function() {
90
+
91
+ this.timeout(60000)
92
+
93
+ it(`jaccardBits 對已知小案例正確`, function() {
94
+ //a=[1,1,0,0], b=[1,0,1,0] → 交集1, 聯集3 → 1-1/3
95
+ assert.strict.strictEqual(jaccardBits(packBits([1, 1, 0, 0]), packBits([1, 0, 1, 0])), 1 - 1 / 3)
96
+ //相同列 → 0
97
+ assert.strict.strictEqual(jaccardBits(packBits([1, 0, 1]), packBits([1, 0, 1])), 0)
98
+ //兩列全0(聯集0) → 視為相同 0
99
+ assert.strict.strictEqual(jaccardBits(packBits([0, 0, 0]), packBits([0, 0, 0])), 0)
100
+ //完全不相交 → 1
101
+ assert.strict.strictEqual(jaccardBits(packBits([1, 0]), packBits([0, 1])), 1)
102
+ })
103
+
104
+ it(`jaccardBits 與逐維Jaccard對隨機二元向量數值一致(含D非32倍數)`, function() {
105
+ let rnd = mulberry32(123)
106
+ let allEqual = true
107
+ for (let D of [16, 33, 50, 64, 100, 200]) {
108
+ let rows = Array.from({ length: 12 }, () => Array.from({ length: D }, () => (rnd() < 0.3 ? 1 : 0)))
109
+ for (let i = 0; i < rows.length; i++) {
110
+ for (let j = i + 1; j < rows.length; j++) {
111
+ let a = jaccardBits(packBits(rows[i]), packBits(rows[j]))
112
+ let b = jacNaive(rows[i], rows[j])
113
+ if (Math.abs(a - b) > 1e-12) {
114
+ allEqual = false
115
+ }
116
+ }
117
+ }
118
+ }
119
+ assert.strict.strictEqual(allEqual, true)
120
+ })
121
+
122
+ it(`經 WCluster.cluster: helper(fasterPAM) ginds 與逐維版完全相同`, async function() {
123
+ let { data: rows } = genBinaryBlobs(6, 40, 300, 45, 6, 42)
124
+ let rNaive = await WCluster.cluster(rows, { mode: 'k-medoids', kNumber: 6, usePCA: false, funDist: jacNaive, useMethod: 'fasterPAM' })
125
+ let { data, funDist } = jaccardBitset(rows)
126
+ let rBits = await WCluster.cluster(data, { mode: 'k-medoids', kNumber: 6, usePCA: false, funDist, useMethod: 'fasterPAM' })
127
+ assert.strict.deepStrictEqual(rBits.ginds, rNaive.ginds)
128
+ })
129
+
130
+ it(`helper 在分離良好的二元blob上 purity=1 且涵蓋全部 index`, async function() {
131
+ let G = 6
132
+ let m = 40
133
+ let n = G * m
134
+ let { data: rows, truth } = genBinaryBlobs(G, m, 300, 45, 6, 42)
135
+ let { data, funDist } = jaccardBitset(rows)
136
+ let r = await WCluster.cluster(data, { mode: 'k-medoids', kNumber: G, usePCA: false, funDist, useMethod: 'fasterPAM' })
137
+ assert.strict.strictEqual(coversAll(r.ginds, n), true)
138
+ assert.strict.strictEqual(purity(r.ginds, truth), 1)
139
+ })
140
+
141
+ })