|
26 | 26 | from collections.abc import Callable |
27 | 27 | from dataclasses import dataclass, field |
28 | 28 |
|
| 29 | +from .._fastmath import batch_dot |
29 | 30 | from ..cso.types import Fact |
30 | 31 | from ..semantic.base import EmbeddingProvider |
31 | | -from ..semantic.index import _dot |
32 | 32 |
|
33 | 33 |
|
34 | 34 | @dataclass |
@@ -97,25 +97,75 @@ def consolidate( |
97 | 97 | f.id: vec for f, vec in zip(targets, vectors, strict=True) |
98 | 98 | } |
99 | 99 |
|
100 | | - # Greedy clustering: walk facts, group each into the first cluster |
| 100 | + # Greedy clustering: walk facts, group each into the best cluster |
101 | 101 | # whose centroid is within the threshold; else start a new cluster. |
| 102 | + # When numpy is present we keep the centroid matrix as a live ndarray |
| 103 | + # and append rows in place, so we never re-coerce a growing python |
| 104 | + # list to an array on every fact (that reconversion was the dominant |
| 105 | + # cost at scale). Centroids use an incremental running sum / count. |
| 106 | + from .._fastmath import HAS_NUMPY |
| 107 | + |
102 | 108 | clusters: list[list[Fact]] = [] |
103 | | - centroids: list[list[float]] = [] |
104 | | - |
105 | | - for fact in targets: |
106 | | - vec = fact_vectors[fact.id] |
107 | | - assigned = False |
108 | | - for i, centroid in enumerate(centroids): |
109 | | - if _dot(vec, centroid) >= config.similarity_threshold: |
110 | | - clusters[i].append(fact) |
111 | | - # update centroid as running mean (re-normalise lazily) |
112 | | - cluster_vecs = [fact_vectors[m.id] for m in clusters[i]] |
113 | | - centroids[i] = _mean_vector(cluster_vecs) |
114 | | - assigned = True |
115 | | - break |
116 | | - if not assigned: |
117 | | - clusters.append([fact]) |
118 | | - centroids.append(vec) |
| 109 | + centroid_sums: list[list[float]] = [] |
| 110 | + centroid_counts: list[int] = [] |
| 111 | + |
| 112 | + if HAS_NUMPY: |
| 113 | + import numpy as _np |
| 114 | + |
| 115 | + dim = len(fact_vectors[targets[0].id]) |
| 116 | + # Pre-allocate; grow geometrically to amortise reallocation. |
| 117 | + cap = 64 |
| 118 | + cmat = _np.empty((cap, dim), dtype=float) |
| 119 | + n_centroids = 0 |
| 120 | + |
| 121 | + for fact in targets: |
| 122 | + vec = fact_vectors[fact.id] |
| 123 | + varr = _np.asarray(vec, dtype=float) |
| 124 | + assigned = False |
| 125 | + if n_centroids > 0: |
| 126 | + scores = cmat[:n_centroids] @ varr |
| 127 | + best_i = int(scores.argmax()) |
| 128 | + if scores[best_i] >= config.similarity_threshold: |
| 129 | + clusters[best_i].append(fact) |
| 130 | + csum = centroid_sums[best_i] |
| 131 | + for k in range(dim): |
| 132 | + csum[k] += vec[k] |
| 133 | + centroid_counts[best_i] += 1 |
| 134 | + cnt = centroid_counts[best_i] |
| 135 | + cmat[best_i] = [s / cnt for s in csum] |
| 136 | + assigned = True |
| 137 | + if not assigned: |
| 138 | + if n_centroids >= cap: |
| 139 | + cap *= 2 |
| 140 | + newmat = _np.empty((cap, dim), dtype=float) |
| 141 | + newmat[:n_centroids] = cmat[:n_centroids] |
| 142 | + cmat = newmat |
| 143 | + cmat[n_centroids] = varr |
| 144 | + n_centroids += 1 |
| 145 | + clusters.append([fact]) |
| 146 | + centroid_sums.append(list(vec)) |
| 147 | + centroid_counts.append(1) |
| 148 | + else: |
| 149 | + centroids: list[list[float]] = [] |
| 150 | + for fact in targets: |
| 151 | + vec = fact_vectors[fact.id] |
| 152 | + assigned = False |
| 153 | + for i, centroid in enumerate(centroids): |
| 154 | + if batch_dot(vec, [centroid])[0] >= config.similarity_threshold: |
| 155 | + clusters[i].append(fact) |
| 156 | + csum = centroid_sums[i] |
| 157 | + for k in range(len(csum)): |
| 158 | + csum[k] += vec[k] |
| 159 | + centroid_counts[i] += 1 |
| 160 | + cnt = centroid_counts[i] |
| 161 | + centroids[i] = [s / cnt for s in csum] |
| 162 | + assigned = True |
| 163 | + break |
| 164 | + if not assigned: |
| 165 | + clusters.append([fact]) |
| 166 | + centroids.append(list(vec)) |
| 167 | + centroid_sums.append(list(vec)) |
| 168 | + centroid_counts.append(1) |
119 | 169 |
|
120 | 170 | # Process non-singleton clusters |
121 | 171 | for cluster in clusters: |
|
0 commit comments