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 +38 -0
- flodr/data.py +238 -0
- flodr/estimator.py +860 -0
- flodr/model.py +762 -0
- flodr/train.py +982 -0
- flodr/viz.py +497 -0
- flodr-0.1.0.dist-info/METADATA +240 -0
- flodr-0.1.0.dist-info/RECORD +10 -0
- flodr-0.1.0.dist-info/WHEEL +4 -0
- flodr-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|