flodr 0.1.0__py3-none-any.whl

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.
flodr/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ from .data import (
2
+ find_ab,
3
+ fuzzy_knn_graph,
4
+ global_pairs,
5
+ knn_search,
6
+ preprocess,
7
+ raw_coords,
8
+ )
9
+ from .estimator import FloDR
10
+ from .model import Coupling, Flow
11
+ from .train import (
12
+ TrainConfig,
13
+ default_device,
14
+ native_bf16,
15
+ perf_cores,
16
+ raw_recipe,
17
+ train_flodr,
18
+ )
19
+ from . import viz
20
+
21
+ __all__ = [
22
+ "FloDR",
23
+ "Flow",
24
+ "Coupling",
25
+ "TrainConfig",
26
+ "train_flodr",
27
+ "raw_recipe",
28
+ "default_device",
29
+ "native_bf16",
30
+ "perf_cores",
31
+ "preprocess",
32
+ "raw_coords",
33
+ "fuzzy_knn_graph",
34
+ "knn_search",
35
+ "global_pairs",
36
+ "find_ab",
37
+ "viz",
38
+ ]
flodr/data.py ADDED
@@ -0,0 +1,238 @@
1
+ import warnings
2
+
3
+ import numpy as np
4
+ from scipy.linalg import null_space
5
+ from scipy.optimize import curve_fit
6
+ from scipy.sparse import coo_matrix
7
+ from scipy.sparse.csgraph import laplacian
8
+ from scipy.sparse.linalg import eigsh
9
+ from scipy.spatial import cKDTree
10
+ from sklearn.decomposition import PCA
11
+ from sklearn.neighbors import NearestNeighbors
12
+ from usearch.index import Index
13
+
14
+ warnings.filterwarnings("ignore")
15
+
16
+
17
+ def preprocess(
18
+ X, n_components=50, seed=0, return_inverse=False, return_transform=False, whiten=1.0
19
+ ):
20
+ n_pcs = min(n_components, X.shape[1])
21
+ pca = PCA(n_components=n_pcs, random_state=seed).fit(X.astype(np.float64))
22
+ proj = pca.transform(X.astype(np.float64))
23
+ mu, scale = proj.mean(0), (proj.std(0) + 1e-9) ** whiten
24
+ X_std = ((proj - mu) / scale).astype(np.float32)
25
+ out = [X_std]
26
+
27
+ if return_inverse:
28
+ out.append(lambda Z: pca.inverse_transform(np.asarray(Z) * scale + mu))
29
+
30
+ if return_transform:
31
+ out.append(
32
+ lambda Xnew: (
33
+ (pca.transform(np.asarray(Xnew).astype(np.float64)) - mu) / scale
34
+ ).astype(np.float32)
35
+ )
36
+
37
+ return out[0] if len(out) == 1 else tuple(out)
38
+
39
+
40
+ # the fitted map between inputs and the whitened coordinates the flow sees. an object rather
41
+ # than closures, so a fitted model pickles
42
+ class Whitening:
43
+ def __init__(self, pca, n_head, head_mu, head_sd, tail_v, tail_sd):
44
+ self.pca, self.n_head = pca, n_head
45
+ self.head_mu, self.head_sd = head_mu, head_sd
46
+ self.tail_v, self.tail_sd = tail_v, tail_sd
47
+
48
+ def to_raw(self, Zn):
49
+ Zn = np.asarray(Zn, dtype=np.float32)
50
+ head = Zn[:, : self.n_head] * self.head_sd + self.head_mu
51
+ tail = Zn[:, self.n_head :] * self.tail_sd
52
+
53
+ return (
54
+ self.pca.mean_ + head @ self.pca.components_ + tail @ self.tail_v.T
55
+ ).astype(np.float32)
56
+
57
+ def to_coords(self, Xn):
58
+ Xn = np.asarray(Xn, dtype=np.float32)
59
+ head = (self.pca.transform(Xn) - self.head_mu) / self.head_sd
60
+ tail = ((Xn - self.pca.mean_) @ self.tail_v) / self.tail_sd
61
+
62
+ return np.hstack([head, tail]).astype(np.float32)
63
+
64
+
65
+ def raw_coords(X, n_components=50, seed=0):
66
+ X = np.asarray(X, dtype=np.float32)
67
+
68
+ # PCA cannot return more components than it has samples
69
+ n_head = min(n_components, X.shape[1] - 1, X.shape[0] - 1)
70
+
71
+ pca = PCA(n_components=n_head, svd_solver="randomized", random_state=seed).fit(X)
72
+
73
+ tail_v = null_space(pca.components_).astype(np.float32)
74
+ proj = pca.transform(X)
75
+
76
+ tail = (X - pca.mean_) @ tail_v
77
+
78
+ head_mu, head_sd = proj.mean(0), proj.std(0) + 1e-9
79
+ tail_sd = np.maximum(tail.std(0), 1e-2 * tail.std(0).max() + 1e-12)
80
+
81
+ Z = np.hstack([(proj - head_mu) / head_sd, tail / tail_sd]).astype(np.float32)
82
+ wh = Whitening(pca, n_head, head_mu, head_sd, tail_v, tail_sd)
83
+
84
+ return Z, wh.to_raw, wh.to_coords
85
+
86
+
87
+ def spectral_init(ei, ej, w, n, scale=1.0, seed=0):
88
+ graph = coo_matrix(
89
+ (np.asarray(w, dtype=np.float64), (ei, ej)), shape=(n, n)
90
+ ).tocsr()
91
+ graph = graph.maximum(graph.T)
92
+
93
+ lap = laplacian(graph, normed=True)
94
+
95
+ try:
96
+ k = 3
97
+ _, vecs = eigsh(
98
+ lap, k=k, sigma=-1e-3, which="LM", v0=np.ones(n) / np.sqrt(n), maxiter=n * 5
99
+ )
100
+ layout = vecs[:, 1:3]
101
+ except Exception:
102
+ layout = np.random.default_rng(seed).standard_normal((n, 2))
103
+
104
+ layout = np.asarray(layout, dtype=np.float32)
105
+ layout = layout - layout.mean(0)
106
+
107
+ return (layout / (layout.std(0) + 1e-12) * scale).astype(np.float32)
108
+
109
+
110
+ EXACT_KNN_MAX = 50_000
111
+
112
+
113
+ def knn_query(Xref, Xq, k, metric="euclid"):
114
+ self_q = Xq is None
115
+
116
+ if metric == "jaccard":
117
+ nn = NearestNeighbors(n_neighbors=k + self_q, metric="jaccard").fit(
118
+ np.asarray(Xref, dtype=bool)
119
+ )
120
+ dist, idx = nn.kneighbors(None if self_q else np.asarray(Xq, dtype=bool))
121
+ else:
122
+ dist, idx = cKDTree(Xref).query(Xref if self_q else Xq, k=k + self_q)
123
+
124
+ if self_q:
125
+ dist, idx = dist[:, 1:], idx[:, 1:]
126
+
127
+ return np.asarray(dist, dtype=np.float64), np.asarray(idx, dtype=np.int64)
128
+
129
+
130
+ def knn_search(Xp, k, exact=None):
131
+ X_f32 = np.ascontiguousarray(Xp, dtype=np.float32)
132
+ n, n_dim = X_f32.shape
133
+
134
+ if exact is None:
135
+ exact = n <= EXACT_KNN_MAX
136
+
137
+ if exact:
138
+ try:
139
+ # exact and deterministic even multithreaded
140
+ import faiss
141
+
142
+ index = faiss.IndexFlatL2(n_dim)
143
+ index.add(X_f32)
144
+ dist2, keys = index.search(X_f32, k + 1)
145
+ keys, dist2 = keys.astype(np.int64), dist2.astype(np.float64)
146
+ order = np.argsort(keys == np.arange(n)[:, None], axis=1, kind="stable")
147
+ keys = np.take_along_axis(keys, order, axis=1)[:, :k]
148
+ dist2 = np.take_along_axis(dist2, order, axis=1)[:, :k]
149
+ return np.sqrt(np.clip(dist2, 0, None)), keys
150
+ except ImportError:
151
+ pass
152
+
153
+ index = Index(ndim=n_dim, metric="l2sq", dtype="f32")
154
+
155
+ # HNSW is nondeterministic across threads, the exact path is not and may use all cores
156
+ index.add(np.arange(n), X_f32, threads=1 if not exact else 0)
157
+ res = index.search(X_f32, k + 1, threads=1 if not exact else 0, exact=exact)
158
+ keys, dist2 = res.keys.astype(np.int64), res.distances.astype(np.float64)
159
+
160
+ if exact:
161
+ # canonical (distance, index) tie order, since multithreaded exact search returns ties
162
+ # in whatever order it finds them, which flips graph edges run-to-run
163
+ order = np.lexsort((keys, dist2), axis=1)
164
+ keys = np.take_along_axis(keys, order, axis=1)
165
+ dist2 = np.take_along_axis(dist2, order, axis=1)
166
+
167
+ # push self last
168
+ order = np.argsort(keys == np.arange(n)[:, None], axis=1, kind="stable")
169
+ keys = np.take_along_axis(keys, order, axis=1)[:, :k]
170
+ dist2 = np.take_along_axis(dist2, order, axis=1)[:, :k]
171
+
172
+ return np.sqrt(dist2), keys
173
+
174
+
175
+ def fuzzy_knn_graph(Xp, k, n_iter=64, precomputed=None, return_dist=False):
176
+ n = len(Xp)
177
+
178
+ if precomputed is not None:
179
+ dist, idx = precomputed
180
+ if dist.shape != (n, k) or idx.shape != (n, k):
181
+ raise ValueError(
182
+ f"precomputed must be two ({n},{k}) arrays, got "
183
+ f"{dist.shape} and {idx.shape}"
184
+ )
185
+ else:
186
+ dist, idx = knn_search(Xp, k)
187
+
188
+ rho = dist[:, 0]
189
+ d_clip = np.clip(dist - rho[:, None], 0, None)
190
+ target = np.log2(k)
191
+ lo, hi, sigma = np.zeros(n), np.full(n, np.inf), np.ones(n)
192
+
193
+ for _ in range(n_iter):
194
+ over = np.exp(-d_clip / sigma[:, None]).sum(1) > target
195
+ hi[over], lo[~over] = sigma[over], sigma[~over]
196
+ sigma = np.where(over | np.isfinite(hi), (lo + hi) / 2, sigma * 2)
197
+
198
+ w_attr = np.exp(-d_clip / sigma[:, None])
199
+ rows = np.repeat(np.arange(n), k)
200
+ A = coo_matrix((w_attr.ravel(), (rows, idx.ravel())), shape=(n, n)).tocsr()
201
+ B = (A + A.T - A.multiply(A.T)).tocoo()
202
+
203
+ # unique undirected edges
204
+ mask = B.row < B.col
205
+ out = (
206
+ idx,
207
+ B.row[mask].astype(np.int64),
208
+ B.col[mask].astype(np.int64),
209
+ B.data[mask].astype(np.float32),
210
+ )
211
+
212
+ return out + (dist,) if return_dist else out
213
+
214
+
215
+ def find_ab(min_dist=0.1, spread=1.0):
216
+ x_grid = np.linspace(0, spread * 3, 300)
217
+ y_prof = np.where(x_grid < min_dist, 1.0, np.exp(-(x_grid - min_dist) / spread))
218
+ (a, b), _ = curve_fit(
219
+ lambda x, a, b: 1.0 / (1.0 + a * x ** (2 * b)),
220
+ x_grid,
221
+ y_prof,
222
+ p0=[1.0, 1.0],
223
+ maxfev=10000,
224
+ )
225
+
226
+ return float(a), float(b)
227
+
228
+
229
+ def global_pairs(Xp, count, rng):
230
+ n = len(Xp)
231
+ pair_i = rng.integers(0, n, count)
232
+ pair_j = rng.integers(0, n, count)
233
+
234
+ mask = pair_i != pair_j
235
+ pair_i, pair_j = pair_i[mask], pair_j[mask]
236
+ dist_hd = np.sqrt(((Xp[pair_i] - Xp[pair_j]) ** 2).sum(1))
237
+
238
+ return pair_i, pair_j, dist_hd