multipers 1.2.2__cp310-cp310-macosx_10_13_x86_64.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.
Potentially problematic release.
This version of multipers might be problematic. Click here for more details.
- multipers/.dylibs/libc++.1.0.dylib +0 -0
- multipers/.dylibs/libtbb.12.12.dylib +0 -0
- multipers/.dylibs/libtbbmalloc.2.12.dylib +0 -0
- multipers/__init__.py +11 -0
- multipers/_signed_measure_meta.py +268 -0
- multipers/_slicer_meta.py +171 -0
- multipers/data/MOL2.py +350 -0
- multipers/data/UCR.py +18 -0
- multipers/data/__init__.py +1 -0
- multipers/data/graphs.py +466 -0
- multipers/data/immuno_regions.py +27 -0
- multipers/data/minimal_presentation_to_st_bf.py +0 -0
- multipers/data/pytorch2simplextree.py +91 -0
- multipers/data/shape3d.py +101 -0
- multipers/data/synthetic.py +68 -0
- multipers/distances.py +198 -0
- multipers/euler_characteristic.pyx +132 -0
- multipers/filtration_conversions.pxd +229 -0
- multipers/filtrations.pxd +225 -0
- multipers/function_rips.cpython-310-darwin.so +0 -0
- multipers/function_rips.pyx +105 -0
- multipers/grids.cpython-310-darwin.so +0 -0
- multipers/grids.pyx +281 -0
- multipers/hilbert_function.pyi +46 -0
- multipers/hilbert_function.pyx +153 -0
- multipers/io.cpython-310-darwin.so +0 -0
- multipers/io.pyx +571 -0
- multipers/ml/__init__.py +0 -0
- multipers/ml/accuracies.py +90 -0
- multipers/ml/convolutions.py +532 -0
- multipers/ml/invariants_with_persistable.py +79 -0
- multipers/ml/kernels.py +176 -0
- multipers/ml/mma.py +659 -0
- multipers/ml/one.py +472 -0
- multipers/ml/point_clouds.py +238 -0
- multipers/ml/signed_betti.py +50 -0
- multipers/ml/signed_measures.py +1542 -0
- multipers/ml/sliced_wasserstein.py +461 -0
- multipers/ml/tools.py +113 -0
- multipers/mma_structures.cpython-310-darwin.so +0 -0
- multipers/mma_structures.pxd +127 -0
- multipers/mma_structures.pyx +2433 -0
- multipers/multiparameter_edge_collapse.py +41 -0
- multipers/multiparameter_module_approximation.cpython-310-darwin.so +0 -0
- multipers/multiparameter_module_approximation.pyx +211 -0
- multipers/pickle.py +53 -0
- multipers/plots.py +326 -0
- multipers/point_measure_integration.cpython-310-darwin.so +0 -0
- multipers/point_measure_integration.pyx +139 -0
- multipers/rank_invariant.cpython-310-darwin.so +0 -0
- multipers/rank_invariant.pyx +229 -0
- multipers/simplex_tree_multi.cpython-310-darwin.so +0 -0
- multipers/simplex_tree_multi.pxd +129 -0
- multipers/simplex_tree_multi.pyi +715 -0
- multipers/simplex_tree_multi.pyx +4655 -0
- multipers/slicer.cpython-310-darwin.so +0 -0
- multipers/slicer.pxd +781 -0
- multipers/slicer.pyx +3393 -0
- multipers/tensor.pxd +13 -0
- multipers/test.pyx +44 -0
- multipers/tests/__init__.py +40 -0
- multipers/tests/old_test_rank_invariant.py +91 -0
- multipers/tests/test_diff_helper.py +74 -0
- multipers/tests/test_hilbert_function.py +82 -0
- multipers/tests/test_mma.py +51 -0
- multipers/tests/test_point_clouds.py +59 -0
- multipers/tests/test_python-cpp_conversion.py +82 -0
- multipers/tests/test_signed_betti.py +181 -0
- multipers/tests/test_simplextreemulti.py +98 -0
- multipers/tests/test_slicer.py +63 -0
- multipers/torch/__init__.py +1 -0
- multipers/torch/diff_grids.py +217 -0
- multipers/torch/rips_density.py +257 -0
- multipers-1.2.2.dist-info/LICENSE +21 -0
- multipers-1.2.2.dist-info/METADATA +28 -0
- multipers-1.2.2.dist-info/RECORD +78 -0
- multipers-1.2.2.dist-info/WHEEL +5 -0
- multipers-1.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from itertools import product
|
|
3
|
+
from typing import Any, Iterable, Literal
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convolution_signed_measures(
|
|
9
|
+
iterable_of_signed_measures,
|
|
10
|
+
filtrations,
|
|
11
|
+
bandwidth,
|
|
12
|
+
flatten: bool = True,
|
|
13
|
+
n_jobs: int = 1,
|
|
14
|
+
backend="pykeops",
|
|
15
|
+
kernel="gaussian",
|
|
16
|
+
**kwargs,
|
|
17
|
+
):
|
|
18
|
+
"""
|
|
19
|
+
Evaluates the convolution of the signed measures Iterable(pts, weights) with a gaussian measure of bandwidth bandwidth, on a grid given by the filtrations
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
|
|
24
|
+
- iterable_of_signed_measures : (num_signed_measure) x [ (npts) x (num_parameters), (npts)]
|
|
25
|
+
- filtrations : (num_parameter) x (filtration values)
|
|
26
|
+
- flatten : bool
|
|
27
|
+
- n_jobs : int
|
|
28
|
+
|
|
29
|
+
Outputs
|
|
30
|
+
-------
|
|
31
|
+
|
|
32
|
+
The concatenated images, for each signed measure (num_signed_measures) x (len(f) for f in filtration_values)
|
|
33
|
+
"""
|
|
34
|
+
grid_iterator = np.array(list(product(*filtrations)), dtype=float)
|
|
35
|
+
match backend:
|
|
36
|
+
case "sklearn":
|
|
37
|
+
|
|
38
|
+
def convolution_signed_measures_on_grid(
|
|
39
|
+
signed_measures: Iterable[tuple[np.ndarray, np.ndarray]],
|
|
40
|
+
):
|
|
41
|
+
return np.concatenate(
|
|
42
|
+
[
|
|
43
|
+
_pts_convolution_sparse_old(
|
|
44
|
+
pts=pts,
|
|
45
|
+
pts_weights=weights,
|
|
46
|
+
grid_iterator=grid_iterator,
|
|
47
|
+
bandwidth=bandwidth,
|
|
48
|
+
kernel=kernel,
|
|
49
|
+
**kwargs,
|
|
50
|
+
)
|
|
51
|
+
for pts, weights in signed_measures
|
|
52
|
+
],
|
|
53
|
+
axis=0,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
case "pykeops":
|
|
57
|
+
|
|
58
|
+
def convolution_signed_measures_on_grid(
|
|
59
|
+
signed_measures: Iterable[tuple[np.ndarray, np.ndarray]],
|
|
60
|
+
):
|
|
61
|
+
return np.concatenate(
|
|
62
|
+
[
|
|
63
|
+
_pts_convolution_pykeops(
|
|
64
|
+
pts=pts,
|
|
65
|
+
pts_weights=weights,
|
|
66
|
+
grid_iterator=grid_iterator,
|
|
67
|
+
bandwidth=bandwidth,
|
|
68
|
+
kernel=kernel,
|
|
69
|
+
**kwargs,
|
|
70
|
+
)
|
|
71
|
+
for pts, weights in signed_measures
|
|
72
|
+
],
|
|
73
|
+
axis=0,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# compiles first once
|
|
77
|
+
pts, weights = iterable_of_signed_measures[0][0]
|
|
78
|
+
small_pts, small_weights = pts[:2], weights[:2]
|
|
79
|
+
|
|
80
|
+
_pts_convolution_pykeops(
|
|
81
|
+
small_pts,
|
|
82
|
+
small_weights,
|
|
83
|
+
grid_iterator=grid_iterator,
|
|
84
|
+
bandwidth=bandwidth,
|
|
85
|
+
kernel=kernel,
|
|
86
|
+
**kwargs,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if n_jobs > 1 or n_jobs == -1:
|
|
90
|
+
prefer = "processes" if backend == "sklearn" else "threads"
|
|
91
|
+
from joblib import Parallel, delayed
|
|
92
|
+
|
|
93
|
+
convolutions = Parallel(n_jobs=n_jobs, prefer=prefer)(
|
|
94
|
+
delayed(convolution_signed_measures_on_grid)(sms)
|
|
95
|
+
for sms in iterable_of_signed_measures
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
convolutions = [
|
|
99
|
+
convolution_signed_measures_on_grid(sms)
|
|
100
|
+
for sms in iterable_of_signed_measures
|
|
101
|
+
]
|
|
102
|
+
if not flatten:
|
|
103
|
+
out_shape = [-1] + [len(f) for f in filtrations] # Degree
|
|
104
|
+
convolutions = [x.reshape(out_shape) for x in convolutions]
|
|
105
|
+
return np.asarray(convolutions, dtype=float)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# def _test(r=1000, b=0.5, plot=True, kernel=0):
|
|
109
|
+
# import matplotlib.pyplot as plt
|
|
110
|
+
# pts, weigths = np.array([[1.,1.], [1.1,1.1]]), np.array([1,-1])
|
|
111
|
+
# pt_list = np.array(list(product(*[np.linspace(0,2,r)]*2)))
|
|
112
|
+
# img = _pts_convolution_sparse_pts(pts,weigths, pt_list,b,kernel=kernel)
|
|
113
|
+
# if plot:
|
|
114
|
+
# plt.imshow(img.reshape(r,-1).T, origin="lower")
|
|
115
|
+
# plt.show()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _pts_convolution_sparse_old(
|
|
119
|
+
pts: np.ndarray,
|
|
120
|
+
pts_weights: np.ndarray,
|
|
121
|
+
grid_iterator,
|
|
122
|
+
kernel="gaussian",
|
|
123
|
+
bandwidth=0.1,
|
|
124
|
+
**more_kde_args,
|
|
125
|
+
):
|
|
126
|
+
"""
|
|
127
|
+
Old version of `convolution_signed_measures`. Scikitlearn's convolution is slower than the code above.
|
|
128
|
+
"""
|
|
129
|
+
from sklearn.neighbors import KernelDensity
|
|
130
|
+
|
|
131
|
+
if len(pts) == 0:
|
|
132
|
+
# warn("Found a trivial signed measure !")
|
|
133
|
+
return np.zeros(len(grid_iterator))
|
|
134
|
+
kde = KernelDensity(
|
|
135
|
+
kernel=kernel, bandwidth=bandwidth, rtol=1e-4, **more_kde_args
|
|
136
|
+
) # TODO : check rtol
|
|
137
|
+
pos_indices = pts_weights > 0
|
|
138
|
+
neg_indices = pts_weights < 0
|
|
139
|
+
img_pos = (
|
|
140
|
+
np.zeros(len(grid_iterator))
|
|
141
|
+
if pos_indices.sum() == 0
|
|
142
|
+
else kde.fit(
|
|
143
|
+
pts[pos_indices], sample_weight=pts_weights[pos_indices]
|
|
144
|
+
).score_samples(grid_iterator)
|
|
145
|
+
)
|
|
146
|
+
img_neg = (
|
|
147
|
+
np.zeros(len(grid_iterator))
|
|
148
|
+
if neg_indices.sum() == 0
|
|
149
|
+
else kde.fit(
|
|
150
|
+
pts[neg_indices], sample_weight=-pts_weights[neg_indices]
|
|
151
|
+
).score_samples(grid_iterator)
|
|
152
|
+
)
|
|
153
|
+
return np.exp(img_pos) - np.exp(img_neg)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _pts_convolution_pykeops(
|
|
157
|
+
pts: np.ndarray,
|
|
158
|
+
pts_weights: np.ndarray,
|
|
159
|
+
grid_iterator,
|
|
160
|
+
kernel="gaussian",
|
|
161
|
+
bandwidth=0.1,
|
|
162
|
+
**more_kde_args,
|
|
163
|
+
):
|
|
164
|
+
"""
|
|
165
|
+
Pykeops convolution
|
|
166
|
+
"""
|
|
167
|
+
kde = KDE(kernel=kernel, bandwidth=bandwidth, **more_kde_args)
|
|
168
|
+
return kde.fit(
|
|
169
|
+
pts, sample_weights=np.asarray(pts_weights, dtype=pts.dtype)
|
|
170
|
+
).score_samples(np.asarray(grid_iterator, dtype=pts.dtype))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def gaussian_kernel(x_i, y_j, bandwidth):
|
|
174
|
+
exponent = -(((x_i - y_j) / bandwidth) ** 2).sum(dim=-1) / 2
|
|
175
|
+
# float is necessary for some reason (pykeops fails)
|
|
176
|
+
kernel = (exponent).exp() / (bandwidth * float(np.sqrt(2 * np.pi)))
|
|
177
|
+
return kernel
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def multivariate_gaussian_kernel(x_i, y_j, covariance_matrix_inverse):
|
|
181
|
+
# 1 / \sqrt(2 \pi^dim * \Sigma.det()) * exp( -(x-y).T @ \Sigma ^{-1} @ (x-y))
|
|
182
|
+
# CF https://www.kernel-operations.io/keops/_auto_examples/pytorch/plot_anisotropic_kernels.html#sphx-glr-auto-examples-pytorch-plot-anisotropic-kernels-py
|
|
183
|
+
# and https://www.kernel-operations.io/keops/api/math-operations.html
|
|
184
|
+
dim = x_i.shape[-1]
|
|
185
|
+
z = x_i - y_j
|
|
186
|
+
exponent = -(z.weightedsqnorm(covariance_matrix_inverse.flatten()) / 2)
|
|
187
|
+
return (
|
|
188
|
+
float((2 * np.pi) ** (-dim / 2))
|
|
189
|
+
* (covariance_matrix_inverse.det().sqrt())
|
|
190
|
+
* exponent.exp()
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def exponential_kernel(x_i, y_j, bandwidth):
|
|
195
|
+
exponent = -(((((x_i - y_j) ** 2).sum()) ** 1 / 2) / bandwidth).sum(dim=-1)
|
|
196
|
+
kernel = exponent.exp() / bandwidth
|
|
197
|
+
return kernel
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _kernel(
|
|
201
|
+
kernel: (
|
|
202
|
+
Literal["gaussian", "exponential", "multivariate_gaussian"] | Callable
|
|
203
|
+
) = "gaussian",
|
|
204
|
+
):
|
|
205
|
+
match kernel:
|
|
206
|
+
case "gaussian":
|
|
207
|
+
return gaussian_kernel
|
|
208
|
+
case "exponential":
|
|
209
|
+
return exponential_kernel
|
|
210
|
+
case "multivariate_gaussian":
|
|
211
|
+
return multivariate_gaussian_kernel
|
|
212
|
+
case _:
|
|
213
|
+
assert callable(
|
|
214
|
+
kernel
|
|
215
|
+
), f"--------------------------\nUnknown kernel {kernel}.\n--------------------------\n Custom kernel has to be callable, (x:LazyTensor(n,1,D),y:LazyTensor(1,m,D),bandwidth:float) ---> kernel matrix"
|
|
216
|
+
return kernel
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# TODO : multiple bandwidths at once with lazy tensors
|
|
220
|
+
class KDE:
|
|
221
|
+
"""
|
|
222
|
+
Fast, scikit-style, and differentiable kernel density estimation, using PyKeops.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(
|
|
226
|
+
self,
|
|
227
|
+
bandwidth: Any = 1,
|
|
228
|
+
kernel: (
|
|
229
|
+
Literal["m_gaussian", "gaussian", "exponential"] | Callable
|
|
230
|
+
) = "gaussian",
|
|
231
|
+
return_log=False,
|
|
232
|
+
):
|
|
233
|
+
"""
|
|
234
|
+
bandwidth : numeric
|
|
235
|
+
bandwidth for Gaussian kernel
|
|
236
|
+
"""
|
|
237
|
+
self.X = None
|
|
238
|
+
self.bandwidth = bandwidth
|
|
239
|
+
self.kernel = kernel
|
|
240
|
+
self._kernel = None
|
|
241
|
+
self._backend = None
|
|
242
|
+
self._sample_weights = None
|
|
243
|
+
self.return_log = return_log
|
|
244
|
+
|
|
245
|
+
def fit(self, X, sample_weights=None, y=None):
|
|
246
|
+
self.X = X
|
|
247
|
+
self._sample_weights = sample_weights
|
|
248
|
+
if isinstance(X, np.ndarray):
|
|
249
|
+
self._backend = np
|
|
250
|
+
else:
|
|
251
|
+
import torch
|
|
252
|
+
|
|
253
|
+
if isinstance(X, torch.Tensor):
|
|
254
|
+
self._backend = torch
|
|
255
|
+
else:
|
|
256
|
+
raise Exception("Unsupported backend.")
|
|
257
|
+
match self.kernel:
|
|
258
|
+
case "gaussian":
|
|
259
|
+
self._kernel = self.gaussian_kernel
|
|
260
|
+
case "m_gaussian":
|
|
261
|
+
self._kernel = self.multivariate_gaussian_kernel
|
|
262
|
+
case "exponential":
|
|
263
|
+
self._kernel = self.exponential_kernel
|
|
264
|
+
case _:
|
|
265
|
+
assert callable(
|
|
266
|
+
self.kernel
|
|
267
|
+
), f"""
|
|
268
|
+
--------------------------
|
|
269
|
+
Unknown kernel {self.kernel}.
|
|
270
|
+
--------------------------
|
|
271
|
+
Custom kernel has to be callable,
|
|
272
|
+
(x:LazyTensor(n,1,D),y:LazyTensor(1,m,D),bandwidth:float) ---> kernel matrix
|
|
273
|
+
"""
|
|
274
|
+
self._kernel = self.kernel
|
|
275
|
+
return self
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def gaussian_kernel(x_i, y_j, bandwidth):
|
|
279
|
+
exponent = -(((x_i - y_j) / bandwidth) ** 2).sum(dim=2) / 2
|
|
280
|
+
# float is necessary for some reason (pykeops fails)
|
|
281
|
+
kernel = (exponent).exp() / (bandwidth * float(np.sqrt(2 * np.pi)))
|
|
282
|
+
return kernel
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def multivariate_gaussian_kernel(x_i, y_j, covariance_matrix_inverse):
|
|
286
|
+
# 1 / \sqrt(2 \pi^dim * \Sigma.det()) * exp( -(x-y).T @ \Sigma ^{-1} @ (x-y))
|
|
287
|
+
# CF https://www.kernel-operations.io/keops/_auto_examples/pytorch/plot_anisotropic_kernels.html#sphx-glr-auto-examples-pytorch-plot-anisotropic-kernels-py
|
|
288
|
+
# and https://www.kernel-operations.io/keops/api/math-operations.html
|
|
289
|
+
dim = x_i.shape[-1]
|
|
290
|
+
z = x_i - y_j
|
|
291
|
+
exponent = -(z.weightedsqnorm(covariance_matrix_inverse.flatten()) / 2)
|
|
292
|
+
return (
|
|
293
|
+
float((2 * np.pi) ** (-dim / 2))
|
|
294
|
+
* (covariance_matrix_inverse.det().sqrt())
|
|
295
|
+
* exponent.exp()
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def exponential_kernel(x_i, y_j, bandwidth):
|
|
300
|
+
exponent = -(((((x_i - y_j) ** 2).sum()) ** 1 / 2) / bandwidth).sum(dim=2)
|
|
301
|
+
kernel = exponent.exp() / bandwidth
|
|
302
|
+
return kernel
|
|
303
|
+
|
|
304
|
+
@staticmethod
|
|
305
|
+
def to_lazy(X, Y, x_weights):
|
|
306
|
+
if isinstance(X, np.ndarray):
|
|
307
|
+
from pykeops.numpy import LazyTensor
|
|
308
|
+
|
|
309
|
+
lazy_x = LazyTensor(
|
|
310
|
+
X.reshape((X.shape[0], 1, X.shape[1]))
|
|
311
|
+
) # numpts, 1, dim
|
|
312
|
+
lazy_y = LazyTensor(
|
|
313
|
+
Y.reshape((1, Y.shape[0], Y.shape[1]))
|
|
314
|
+
) # 1, numpts, dim
|
|
315
|
+
if x_weights is not None:
|
|
316
|
+
w = LazyTensor(x_weights[:, None], axis=0)
|
|
317
|
+
return lazy_x, lazy_y, w
|
|
318
|
+
return lazy_x, lazy_y, None
|
|
319
|
+
import torch
|
|
320
|
+
|
|
321
|
+
if isinstance(X, torch.Tensor):
|
|
322
|
+
from pykeops.torch import LazyTensor
|
|
323
|
+
|
|
324
|
+
lazy_x = LazyTensor(X.view(X.shape[0], 1, X.shape[1]))
|
|
325
|
+
lazy_y = LazyTensor(Y.view(1, Y.shape[0], Y.shape[1]))
|
|
326
|
+
if x_weights is not None:
|
|
327
|
+
w = LazyTensor(x_weights[:, None], axis=0)
|
|
328
|
+
return lazy_x, lazy_y, w
|
|
329
|
+
return lazy_x, lazy_y, None
|
|
330
|
+
raise Exception("Bad tensor type.")
|
|
331
|
+
|
|
332
|
+
def score_samples(self, Y, X=None, return_kernel=False):
|
|
333
|
+
"""Returns the kernel density estimates of each point in `Y`.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
Y : tensor (m, d)
|
|
338
|
+
`m` points with `d` dimensions for which the probability density will
|
|
339
|
+
be calculated
|
|
340
|
+
X : tensor (n, d), optional
|
|
341
|
+
`n` points with `d` dimensions to which KDE will be fit. Provided to
|
|
342
|
+
allow batch calculations in `log_prob`. By default, `X` is None and
|
|
343
|
+
all points used to initialize KernelDensityEstimator are included.
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
log_probs : tensor (m)
|
|
349
|
+
log probability densities for each of the queried points in `Y`
|
|
350
|
+
"""
|
|
351
|
+
X = self.X if X is None else X
|
|
352
|
+
if X.shape[0] == 0:
|
|
353
|
+
return self._backend.zeros((Y.shape[0]))
|
|
354
|
+
assert Y.shape[1] == X.shape[1] and X.ndim == Y.ndim == 2
|
|
355
|
+
lazy_x, lazy_y, w = self.to_lazy(X, Y, x_weights=self._sample_weights)
|
|
356
|
+
kernel = self._kernel(lazy_x, lazy_y, self.bandwidth)
|
|
357
|
+
if w is not None:
|
|
358
|
+
kernel *= w
|
|
359
|
+
if return_kernel:
|
|
360
|
+
return kernel
|
|
361
|
+
density_estimation = kernel.sum(dim=0).flatten() / kernel.shape[0] # mean
|
|
362
|
+
return (
|
|
363
|
+
self._backend.log(density_estimation)
|
|
364
|
+
if self.return_log
|
|
365
|
+
else density_estimation
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def batch_signed_measure_convolutions(
|
|
370
|
+
signed_measures, # array of shape (num_data,num_pts,D)
|
|
371
|
+
x, # array of shape (num_x, D) or (num_data, num_x, D)
|
|
372
|
+
bandwidth, # either float or matrix if multivariate kernel
|
|
373
|
+
kernel,
|
|
374
|
+
):
|
|
375
|
+
"""
|
|
376
|
+
Input
|
|
377
|
+
-----
|
|
378
|
+
- signed_measures: unragged, of shape (num_data, num_pts, D+1)
|
|
379
|
+
where last coord is weights, (0 for dummy points)
|
|
380
|
+
- x : the points to convolve (num_x,D)
|
|
381
|
+
- bandwidth : the bandwidths or covariance matrix inverse or ... of the kernel
|
|
382
|
+
- kernel : "gaussian", "multivariate_gaussian", "exponential", or Callable (x_i, y_i, bandwidth)->float
|
|
383
|
+
|
|
384
|
+
Output
|
|
385
|
+
------
|
|
386
|
+
Array of shape (num_convolutions, (num_axis), num_data,
|
|
387
|
+
Array of shape (num_convolutions, (num_axis), num_data, max_x_size)
|
|
388
|
+
"""
|
|
389
|
+
if signed_measures.ndim == 2:
|
|
390
|
+
signed_measures = signed_measures[None, :, :]
|
|
391
|
+
sms = signed_measures[..., :-1]
|
|
392
|
+
weights = signed_measures[..., -1]
|
|
393
|
+
if isinstance(signed_measures, np.ndarray):
|
|
394
|
+
from pykeops.numpy import LazyTensor
|
|
395
|
+
else:
|
|
396
|
+
import torch
|
|
397
|
+
|
|
398
|
+
assert isinstance(signed_measures, torch.Tensor)
|
|
399
|
+
from pykeops.torch import LazyTensor
|
|
400
|
+
|
|
401
|
+
_sms = LazyTensor(sms[..., None, :].contiguous())
|
|
402
|
+
_x = x[..., None, :, :].contiguous()
|
|
403
|
+
|
|
404
|
+
sms_kernel = _kernel(kernel)(_sms, _x, bandwidth)
|
|
405
|
+
out = (sms_kernel * weights[..., None, None].contiguous()).sum(
|
|
406
|
+
signed_measures.ndim - 2
|
|
407
|
+
)
|
|
408
|
+
assert out.shape[-1] == 1, "Pykeops bug fixed, TODO : refix this "
|
|
409
|
+
out = out[..., 0] ## pykeops bug + ensures its a tensor
|
|
410
|
+
# assert out.shape == (x.shape[0], x.shape[1]), f"{x.shape=}, {out.shape=}"
|
|
411
|
+
return out
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class DTM:
|
|
415
|
+
"""
|
|
416
|
+
Distance To Measure
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
def __init__(self, masses=[0.1], metric: str = "euclidean", **_kdtree_kwargs):
|
|
420
|
+
"""
|
|
421
|
+
mass : float in [0,1]
|
|
422
|
+
The mass threshold
|
|
423
|
+
metric :
|
|
424
|
+
The distance between points to consider
|
|
425
|
+
"""
|
|
426
|
+
self.masses = masses
|
|
427
|
+
self.metric = metric
|
|
428
|
+
self._kdtree_kwargs = _kdtree_kwargs
|
|
429
|
+
self._ks = None
|
|
430
|
+
self._kdtree = None
|
|
431
|
+
self._X = None
|
|
432
|
+
self._backend = None
|
|
433
|
+
|
|
434
|
+
def fit(self, X, sample_weights=None, y=None):
|
|
435
|
+
if len(self.masses) == 0:
|
|
436
|
+
return self
|
|
437
|
+
assert np.max(self.masses) <= 1, "All masses should be in (0,1]."
|
|
438
|
+
from sklearn.neighbors import KDTree
|
|
439
|
+
|
|
440
|
+
if not isinstance(X, np.ndarray):
|
|
441
|
+
import torch
|
|
442
|
+
|
|
443
|
+
assert isinstance(X, torch.Tensor), "Backend has to be numpy of torch"
|
|
444
|
+
_X = X.detach()
|
|
445
|
+
self._backend = "torch"
|
|
446
|
+
else:
|
|
447
|
+
_X = X
|
|
448
|
+
self._backend = "numpy"
|
|
449
|
+
self._ks = np.array([int(mass * X.shape[0]) + 1 for mass in self.masses])
|
|
450
|
+
self._kdtree = KDTree(_X, metric=self.metric, **self._kdtree_kwargs)
|
|
451
|
+
self._X = X
|
|
452
|
+
return self
|
|
453
|
+
|
|
454
|
+
def score_samples(self, Y, X=None):
|
|
455
|
+
"""Returns the kernel density estimates of each point in `Y`.
|
|
456
|
+
|
|
457
|
+
Parameters
|
|
458
|
+
----------
|
|
459
|
+
Y : tensor (m, d)
|
|
460
|
+
`m` points with `d` dimensions for which the probability density will
|
|
461
|
+
be calculated
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
the DTMs of Y, for each mass in masses.
|
|
467
|
+
"""
|
|
468
|
+
if len(self.masses) == 0:
|
|
469
|
+
return np.empty((0, len(Y)))
|
|
470
|
+
assert Y.ndim == 2
|
|
471
|
+
if self._backend == "torch":
|
|
472
|
+
_Y = Y.detach().numpy()
|
|
473
|
+
else:
|
|
474
|
+
_Y = Y
|
|
475
|
+
NN_Dist, NN = self._kdtree.query(_Y, self._ks.max(), return_distance=True)
|
|
476
|
+
DTMs = np.array([((NN_Dist**2)[:, :k].mean(1)) ** 0.5 for k in self._ks])
|
|
477
|
+
return DTMs
|
|
478
|
+
|
|
479
|
+
def score_samples_diff(self, Y):
|
|
480
|
+
"""Returns the kernel density estimates of each point in `Y`.
|
|
481
|
+
|
|
482
|
+
Parameters
|
|
483
|
+
----------
|
|
484
|
+
Y : tensor (m, d)
|
|
485
|
+
`m` points with `d` dimensions for which the probability density will
|
|
486
|
+
be calculated
|
|
487
|
+
X : tensor (n, d), optional
|
|
488
|
+
`n` points with `d` dimensions to which KDE will be fit. Provided to
|
|
489
|
+
allow batch calculations in `log_prob`. By default, `X` is None and
|
|
490
|
+
all points used to initialize KernelDensityEstimator are included.
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
Returns
|
|
494
|
+
-------
|
|
495
|
+
log_probs : tensor (m)
|
|
496
|
+
log probability densities for each of the queried points in `Y`
|
|
497
|
+
"""
|
|
498
|
+
import torch
|
|
499
|
+
|
|
500
|
+
assert Y.ndim == 2
|
|
501
|
+
assert self._backend == "torch", "Use the non-diff version with numpy."
|
|
502
|
+
if len(self.masses) == 0:
|
|
503
|
+
return torch.empty(0, len(Y))
|
|
504
|
+
NN = self._kdtree.query(Y.detach(), self._ks.max(), return_distance=False)
|
|
505
|
+
DTMs = tuple(
|
|
506
|
+
(((self._X[NN] - Y[:, None, :]) ** 2)[:, :k].sum(dim=(1, 2)) / k) ** 0.5
|
|
507
|
+
for k in self._ks
|
|
508
|
+
) # TODO : kdtree already computes distance, find implementation of kdtree that is pytorch differentiable
|
|
509
|
+
return DTMs
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# def _pts_convolution_sparse(pts:np.ndarray, pts_weights:np.ndarray, filtration_grid:Iterable[np.ndarray], kernel="gaussian", bandwidth=0.1, **more_kde_args):
|
|
513
|
+
# """
|
|
514
|
+
# Old version of `convolution_signed_measures`. Scikitlearn's convolution is slower than the code above.
|
|
515
|
+
# """
|
|
516
|
+
# from sklearn.neighbors import KernelDensity
|
|
517
|
+
# grid_iterator = np.asarray(list(product(*filtration_grid)))
|
|
518
|
+
# grid_shape = [len(f) for f in filtration_grid]
|
|
519
|
+
# if len(pts) == 0:
|
|
520
|
+
# # warn("Found a trivial signed measure !")
|
|
521
|
+
# return np.zeros(shape=grid_shape)
|
|
522
|
+
# kde = KernelDensity(kernel=kernel, bandwidth=bandwidth, rtol = 1e-4, **more_kde_args) # TODO : check rtol
|
|
523
|
+
|
|
524
|
+
# pos_indices = pts_weights>0
|
|
525
|
+
# neg_indices = pts_weights<0
|
|
526
|
+
# img_pos = kde.fit(pts[pos_indices], sample_weight=pts_weights[pos_indices]).score_samples(grid_iterator).reshape(grid_shape)
|
|
527
|
+
# img_neg = kde.fit(pts[neg_indices], sample_weight=-pts_weights[neg_indices]).score_samples(grid_iterator).reshape(grid_shape)
|
|
528
|
+
# return np.exp(img_pos) - np.exp(img_neg)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# Precompiles the convolution
|
|
532
|
+
# _test(r=2,b=.5, plot=False)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import persistable
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# requires installing ripser (pip install ripser) as well as persistable from the higher-homology branch,
|
|
5
|
+
# which can be done as follows:
|
|
6
|
+
# pip install git+https://github.com/LuisScoccola/persistable.git@higher-homology
|
|
7
|
+
# NOTE: only accepts as input a distance matrix
|
|
8
|
+
def hf_degree_rips(
|
|
9
|
+
distance_matrix,
|
|
10
|
+
min_rips_value,
|
|
11
|
+
max_rips_value,
|
|
12
|
+
max_normalized_degree,
|
|
13
|
+
min_normalized_degree,
|
|
14
|
+
grid_granularity,
|
|
15
|
+
max_homological_dimension,
|
|
16
|
+
subsample_size = None,
|
|
17
|
+
):
|
|
18
|
+
if subsample_size == None:
|
|
19
|
+
p = persistable.Persistable(distance_matrix, metric="precomputed")
|
|
20
|
+
else:
|
|
21
|
+
p = persistable.Persistable(distance_matrix, metric="precomputed", subsample=subsample_size)
|
|
22
|
+
|
|
23
|
+
rips_values, normalized_degree_values, hilbert_functions, minimal_hilbert_decompositions = p._hilbert_function(
|
|
24
|
+
min_rips_value,
|
|
25
|
+
max_rips_value,
|
|
26
|
+
max_normalized_degree,
|
|
27
|
+
min_normalized_degree,
|
|
28
|
+
grid_granularity,
|
|
29
|
+
homological_dimension=max_homological_dimension,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return rips_values, normalized_degree_values, hilbert_functions, minimal_hilbert_decompositions
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def hf_h0_degree_rips(
|
|
37
|
+
point_cloud,
|
|
38
|
+
min_rips_value,
|
|
39
|
+
max_rips_value,
|
|
40
|
+
max_normalized_degree,
|
|
41
|
+
min_normalized_degree,
|
|
42
|
+
grid_granularity,
|
|
43
|
+
):
|
|
44
|
+
p = persistable.Persistable(point_cloud, n_neighbors="all")
|
|
45
|
+
|
|
46
|
+
rips_values, normalized_degree_values, hilbert_functions, minimal_hilbert_decompositions = p._hilbert_function(
|
|
47
|
+
min_rips_value,
|
|
48
|
+
max_rips_value,
|
|
49
|
+
max_normalized_degree,
|
|
50
|
+
min_normalized_degree,
|
|
51
|
+
grid_granularity,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return rips_values, normalized_degree_values, hilbert_functions[0], minimal_hilbert_decompositions[0]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ri_h0_degree_rips(
|
|
58
|
+
point_cloud,
|
|
59
|
+
min_rips_value,
|
|
60
|
+
max_rips_value,
|
|
61
|
+
max_normalized_degree,
|
|
62
|
+
min_normalized_degree,
|
|
63
|
+
grid_granularity,
|
|
64
|
+
):
|
|
65
|
+
p = persistable.Persistable(point_cloud, n_neighbors="all")
|
|
66
|
+
|
|
67
|
+
rips_values, normalized_degree_values, rank_invariant, _, _ = p._rank_invariant(
|
|
68
|
+
min_rips_value,
|
|
69
|
+
max_rips_value,
|
|
70
|
+
max_normalized_degree,
|
|
71
|
+
min_normalized_degree,
|
|
72
|
+
grid_granularity,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return rips_values, normalized_degree_values, rank_invariant
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|