(vectors: number[][], maxClusters: number = 20)
| 135 | } |
| 136 | |
| 137 | export function spectralCluster(vectors: number[][], maxClusters: number = 20): ClusterResult[] { |
| 138 | const n = vectors.length; |
| 139 | if (n <= 1) return [{ indices: Array.from({ length: n }, (_, i) => i) }]; |
| 140 | if (n <= maxClusters) return vectors.map((_, i) => ({ indices: [i] })); |
| 141 | |
| 142 | const affinity = buildAffinityMatrix(vectors); |
| 143 | const laplacian = normalizedLaplacian(affinity); |
| 144 | const eigen = new EigenvalueDecomposition(laplacian); |
| 145 | |
| 146 | const eigenvalues = eigen.realEigenvalues; |
| 147 | const eigenvectors = eigen.eigenvectorMatrix; |
| 148 | |
| 149 | const k = findOptimalK(eigenvalues, Math.min(maxClusters, Math.max(2, Math.floor(Math.sqrt(n))))); |
| 150 | |
| 151 | const sortedIndices = eigenvalues |
| 152 | .map((v: number, i: number) => ({ v, i })) |
| 153 | .sort((a: { v: number }, b: { v: number }) => a.v - b.v) |
| 154 | .map((x: { i: number }) => x.i); |
| 155 | |
| 156 | const embedding: number[][] = []; |
| 157 | for (let i = 0; i < n; i++) { |
| 158 | const row: number[] = []; |
| 159 | for (let j = 0; j < k; j++) row.push(eigenvectors.get(i, sortedIndices[j])); |
| 160 | const norm = Math.sqrt(row.reduce((s, v) => s + v * v, 0)); |
| 161 | if (norm > 1e-10) for (let d = 0; d < row.length; d++) row[d] /= norm; |
| 162 | embedding.push(row); |
| 163 | } |
| 164 | |
| 165 | const assignments = kMeans(embedding, k); |
| 166 | |
| 167 | const clusters: Map<number, number[]> = new Map(); |
| 168 | for (let i = 0; i < n; i++) { |
| 169 | const c = assignments[i]; |
| 170 | if (!clusters.has(c)) clusters.set(c, []); |
| 171 | clusters.get(c)!.push(i); |
| 172 | } |
| 173 | |
| 174 | return Array.from(clusters.values()).map((indices) => ({ indices })); |
| 175 | } |
| 176 | |
| 177 | export function findPathPattern(paths: string[]): string | null { |
| 178 | if (paths.length <= 1) return null; |
no test coverage detected