@graphty/algorithms 1.7.0 → 1.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphty/algorithms",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Graph algorithms library for browser environments implemented in TypeScript",
5
5
  "author": "Adam Powers <apowers@ato.ms>",
6
6
  "main": "dist/algorithms.js",
@@ -1,6 +1,6 @@
1
1
  import type { Graph } from "../core/graph.js";
2
2
  import type { NodeId } from "../types/index.js";
3
- import { euclideanDistance } from "../utils/math-utilities.js";
3
+ import { euclideanDistance, SeededRandom } from "../utils/math-utilities.js";
4
4
 
5
5
  /**
6
6
  * Spectral Clustering implementation
@@ -17,6 +17,7 @@ export interface SpectralClusteringOptions {
17
17
  laplacianType?: "unnormalized" | "normalized" | "randomWalk"; // Type of Laplacian
18
18
  maxIterations?: number; // Max iterations for k-means (default: 100)
19
19
  tolerance?: number; // Convergence tolerance (default: 1e-4)
20
+ seed?: number; // Random seed for reproducible results
20
21
  }
21
22
 
22
23
  export interface SpectralClusteringResult {
@@ -41,7 +42,10 @@ export interface SpectralClusteringResult {
41
42
  * @returns Spectral clustering result with communities and cluster assignments
42
43
  */
43
44
  export function spectralClustering(graph: Graph, options: SpectralClusteringOptions): SpectralClusteringResult {
44
- const { k, laplacianType = "normalized", maxIterations = 100, tolerance = 1e-4 } = options;
45
+ const { k, laplacianType = "normalized", maxIterations = 100, tolerance = 1e-4, seed } = options;
46
+
47
+ // Create random function - use seeded PRNG if seed provided, otherwise Math.random
48
+ const random = seed !== undefined ? SeededRandom.createGenerator(seed) : Math.random;
45
49
 
46
50
  // Input validation
47
51
  if (k < 1 || !Number.isInteger(k)) {
@@ -67,7 +71,7 @@ export function spectralClustering(graph: Graph, options: SpectralClusteringOpti
67
71
  const laplacianMatrix = buildLaplacianMatrix(adjacencyMatrix, laplacianType);
68
72
 
69
73
  // Find k smallest eigenvectors
70
- const eigenResult = findSmallestEigenvectors(laplacianMatrix, k);
74
+ const eigenResult = findSmallestEigenvectors(laplacianMatrix, k, random);
71
75
 
72
76
  // Perform k-means clustering on the eigenvectors
73
77
  // For spectral clustering, we need to transpose the eigenvector matrix
@@ -89,7 +93,7 @@ export function spectralClustering(graph: Graph, options: SpectralClusteringOpti
89
93
  normalizeRows(dataPoints);
90
94
  }
91
95
 
92
- const kmeans = kMeansClustering(dataPoints, k, maxIterations, tolerance);
96
+ const kmeans = kMeansClustering(dataPoints, k, maxIterations, tolerance, random);
93
97
 
94
98
  // Build communities
95
99
  const communities: NodeId[][] = Array.from({ length: k }, () => []);
@@ -293,11 +297,13 @@ function buildLaplacianMatrix(adjacency: number[][], type: string): number[][] {
293
297
  * This is a simplified implementation - in practice, you'd use LAPACK or similar
294
298
  * @param matrix - The Laplacian matrix for eigendecomposition
295
299
  * @param k - Number of smallest eigenvectors to find
300
+ * @param random - Random number generator function
296
301
  * @returns Object containing eigenvalues and corresponding eigenvectors
297
302
  */
298
303
  function findSmallestEigenvectors(
299
304
  matrix: number[][],
300
305
  k: number,
306
+ random: () => number,
301
307
  ): {
302
308
  eigenvalues: number[];
303
309
  eigenvectors: number[][];
@@ -318,7 +324,7 @@ function findSmallestEigenvectors(
318
324
  // For spectral clustering, we need proper eigenvectors
319
325
  // Special handling for small k values which are common in clustering
320
326
  if (k <= 3 && n > k) {
321
- return computeSmallestEigenvectorsSimple(matrix, k, n);
327
+ return computeSmallestEigenvectorsSimple(matrix, k, n, random);
322
328
  }
323
329
 
324
330
  // For larger k, use power iteration
@@ -330,7 +336,7 @@ function findSmallestEigenvectors(
330
336
  // Initialize random vector
331
337
  let vector = Array(n)
332
338
  .fill(0)
333
- .map(() => Math.random() - 0.5);
339
+ .map(() => random() - 0.5);
334
340
 
335
341
  // Normalize initial vector
336
342
  const initNorm = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
@@ -447,6 +453,7 @@ function normalizeRows(matrix: number[][]): void {
447
453
  * @param k - Number of clusters to form
448
454
  * @param maxIterations - Maximum number of iterations
449
455
  * @param tolerance - Convergence tolerance for centroid movement
456
+ * @param random - Random number generator function
450
457
  * @returns Object containing cluster assignments and final centroids
451
458
  */
452
459
  function kMeansClustering(
@@ -454,6 +461,7 @@ function kMeansClustering(
454
461
  k: number,
455
462
  maxIterations: number,
456
463
  tolerance = 1e-4,
464
+ random: () => number = Math.random,
457
465
  ): { assignments: number[]; centroids: number[][] } {
458
466
  const n = data.length;
459
467
  const d = data[0]?.length ?? 0;
@@ -476,7 +484,7 @@ function kMeansClustering(
476
484
  const selectedIndices = new Set<number>();
477
485
 
478
486
  while (centroids.length < k && selectedIndices.size < n) {
479
- const idx = Math.floor(Math.random() * n);
487
+ const idx = Math.floor(random() * n);
480
488
  if (!selectedIndices.has(idx) && data[idx]) {
481
489
  selectedIndices.add(idx);
482
490
  centroids.push([...data[idx]]);
@@ -487,7 +495,7 @@ function kMeansClustering(
487
495
  while (centroids.length < k) {
488
496
  const centroid = Array(d).fill(0) as number[];
489
497
  for (let j = 0; j < d; j++) {
490
- centroid[j] = Math.random() - 0.5;
498
+ centroid[j] = random() - 0.5;
491
499
  }
492
500
  centroids.push(centroid);
493
501
  }
@@ -611,12 +619,14 @@ function kMeansClustering(
611
619
  * @param matrix - The Laplacian matrix
612
620
  * @param k - Number of eigenvectors to compute (1-3)
613
621
  * @param n - Size of the matrix
622
+ * @param random - Random number generator function
614
623
  * @returns Object containing eigenvalues and eigenvectors
615
624
  */
616
625
  function computeSmallestEigenvectorsSimple(
617
626
  matrix: number[][],
618
627
  k: number,
619
628
  n: number,
629
+ random: () => number,
620
630
  ): {
621
631
  eigenvalues: number[];
622
632
  eigenvectors: number[][];
@@ -635,7 +645,7 @@ function computeSmallestEigenvectorsSimple(
635
645
  const maxEig = 2; // For normalized Laplacian, max eigenvalue <= 2
636
646
  let vector = Array(n)
637
647
  .fill(0)
638
- .map(() => Math.random() - 0.5);
648
+ .map(() => random() - 0.5);
639
649
 
640
650
  // Make orthogonal to first eigenvector
641
651
  const dot1 = vector.reduce((sum, val) => sum + val / Math.sqrt(n), 0);
@@ -681,7 +691,7 @@ function computeSmallestEigenvectorsSimple(
681
691
  if (k >= 3) {
682
692
  let vector = Array(n)
683
693
  .fill(0)
684
- .map(() => Math.random() - 0.5);
694
+ .map(() => random() - 0.5);
685
695
 
686
696
  // Orthogonalize against previous eigenvectors
687
697
  for (const prev of eigenvectors) {