multipers 1.0__cp311-cp311-manylinux_2_34_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.

Files changed (56) hide show
  1. multipers/__init__.py +4 -0
  2. multipers/_old_rank_invariant.pyx +328 -0
  3. multipers/_signed_measure_meta.py +72 -0
  4. multipers/data/MOL2.py +350 -0
  5. multipers/data/UCR.py +18 -0
  6. multipers/data/__init__.py +1 -0
  7. multipers/data/graphs.py +272 -0
  8. multipers/data/immuno_regions.py +27 -0
  9. multipers/data/minimal_presentation_to_st_bf.py +0 -0
  10. multipers/data/pytorch2simplextree.py +91 -0
  11. multipers/data/shape3d.py +101 -0
  12. multipers/data/synthetic.py +68 -0
  13. multipers/distances.py +100 -0
  14. multipers/euler_characteristic.cpython-311-x86_64-linux-gnu.so +0 -0
  15. multipers/euler_characteristic.pyx +132 -0
  16. multipers/function_rips.cpython-311-x86_64-linux-gnu.so +0 -0
  17. multipers/function_rips.pyx +101 -0
  18. multipers/hilbert_function.cpython-311-x86_64-linux-gnu.so +0 -0
  19. multipers/hilbert_function.pyi +46 -0
  20. multipers/hilbert_function.pyx +145 -0
  21. multipers/ml/__init__.py +0 -0
  22. multipers/ml/accuracies.py +61 -0
  23. multipers/ml/convolutions.py +384 -0
  24. multipers/ml/invariants_with_persistable.py +79 -0
  25. multipers/ml/kernels.py +128 -0
  26. multipers/ml/mma.py +422 -0
  27. multipers/ml/one.py +472 -0
  28. multipers/ml/point_clouds.py +191 -0
  29. multipers/ml/signed_betti.py +50 -0
  30. multipers/ml/signed_measures.py +1046 -0
  31. multipers/ml/sliced_wasserstein.py +313 -0
  32. multipers/ml/tools.py +99 -0
  33. multipers/multiparameter_edge_collapse.py +29 -0
  34. multipers/multiparameter_module_approximation.cpython-311-x86_64-linux-gnu.so +0 -0
  35. multipers/multiparameter_module_approximation.pxd +147 -0
  36. multipers/multiparameter_module_approximation.pyi +439 -0
  37. multipers/multiparameter_module_approximation.pyx +931 -0
  38. multipers/pickle.py +53 -0
  39. multipers/plots.py +207 -0
  40. multipers/point_measure_integration.cpython-311-x86_64-linux-gnu.so +0 -0
  41. multipers/point_measure_integration.pyx +59 -0
  42. multipers/rank_invariant.cpython-311-x86_64-linux-gnu.so +0 -0
  43. multipers/rank_invariant.pyx +154 -0
  44. multipers/simplex_tree_multi.cpython-311-x86_64-linux-gnu.so +0 -0
  45. multipers/simplex_tree_multi.pxd +121 -0
  46. multipers/simplex_tree_multi.pyi +715 -0
  47. multipers/simplex_tree_multi.pyx +1284 -0
  48. multipers/tensor.pxd +13 -0
  49. multipers/test.pyx +44 -0
  50. multipers-1.0.dist-info/LICENSE +21 -0
  51. multipers-1.0.dist-info/METADATA +9 -0
  52. multipers-1.0.dist-info/RECORD +56 -0
  53. multipers-1.0.dist-info/WHEEL +5 -0
  54. multipers-1.0.dist-info/top_level.txt +1 -0
  55. multipers.libs/libtbb-5d1cde94.so.12.10 +0 -0
  56. multipers.libs/libtbbmalloc-5e0a3d4c.so.2.10 +0 -0
@@ -0,0 +1,128 @@
1
+ from sklearn.base import BaseEstimator, TransformerMixin, clone
2
+ import numpy as np
3
+ from typing import Iterable
4
+
5
+ ## To do k folds with a distance matrix, we need to slice it into list of distances.
6
+ # k-fold usually shuffles the lists, so we need to add an identifier to each entry,
7
+ #
8
+ class DistanceMatrix2DistanceList(BaseEstimator, TransformerMixin):
9
+ def __init__(self) -> None:
10
+ super().__init__()
11
+ def fit(self, X, y=None):
12
+ return self
13
+ def transform(self,X):
14
+ X = np.asarray(X)
15
+ assert X.ndim == 2 ## Its a matrix
16
+ return np.asarray([[i, *distance_to_pt] for i,distance_to_pt in enumerate(X)])
17
+
18
+
19
+ class DistanceList2DistanceMatrix(BaseEstimator, TransformerMixin):
20
+ def __init__(self) -> None:
21
+ super().__init__()
22
+ def fit(self, X, y=None):
23
+ return self
24
+ def transform(self,X):
25
+ index_list = np.asarray(X[:,0], dtype=int) + 1 # shift of 1, because the first index is for indexing the pts
26
+ return X[:, index_list] ## The distance matrix of the index_list
27
+
28
+
29
+ class DistanceMatrices2DistancesList(BaseEstimator, TransformerMixin):
30
+ """
31
+ Input (degree) x (distance matrix) or (axis) x (degree) x (distance matrix D)
32
+ Output _ (D1) x opt (axis) x (degree) x (D2, , with indices first)
33
+ """
34
+ def __init__(self) -> None:
35
+ super().__init__()
36
+ self._axes=None
37
+ def fit(self, X, y=None):
38
+ X = np.asarray(X)
39
+ self._axes = X.ndim ==4
40
+ assert self._axes or X.ndim == 3, " Bad input shape. Input is either (degree) x (distance matrix) or (axis) x (degree) x (distance matrix) "
41
+
42
+ return self
43
+ def transform(self, X):
44
+ X = np.asarray(X)
45
+ assert (X.ndim == 3 and not self._axes) or (X.ndim == 4 and self._axes), f"X shape ({X.shape}) is not valid"
46
+ if self._axes:
47
+ out = np.asarray([[DistanceMatrix2DistanceList().fit_transform(M) for M in matrices_in_axes] for matrices_in_axes in X])
48
+ return np.moveaxis(out, [2,0,1,3], [0,1,2,3])
49
+ else:
50
+ out = np.array([DistanceMatrix2DistanceList().fit_transform(M) for M in X]) ## indices are at [:,0,Any_coord]
51
+ # return np.moveaxis(out, 0, -1) ## indices are at [:,0,any_coord], degree axis is the last
52
+ return np.moveaxis(out, [1,0,2], [0,1,2])
53
+
54
+
55
+ def predict(self,X):
56
+ return self.transform(X)
57
+
58
+ class DistancesLists2DistanceMatrices(BaseEstimator, TransformerMixin):
59
+ """
60
+ Input (D1) x opt (axis) x (degree) x (D2 with indices first)
61
+ Output opt (axis) x (degree) x (distance matrix (D1,D2))
62
+ """
63
+ def __init__(self) -> None:
64
+ super().__init__()
65
+ self.train_indices = None
66
+ self._axes = None
67
+ def fit(self, X:np.ndarray, y=None):
68
+ X = np.asarray(X)
69
+ assert X.ndim in [3,4]
70
+ self._axes = X.ndim == 4
71
+ if self._axes:
72
+ self.train_indices = np.asarray(X[:,0,0,0], dtype=int)
73
+ else:
74
+ self.train_indices = np.asarray(X[:,0,0], dtype=int)
75
+ return self
76
+ def transform(self,X):
77
+ X = np.asarray(X)
78
+ assert X.ndim in [3,4]
79
+ # test_indices = np.asarray(X[:,0,0], dtype=int)
80
+ # print(X.shape, self.train_indices, test_indices, flush=1)
81
+ # First coord of X is test indices by design, train indices have to be selected in the second coord, last one is the degree
82
+ if self._axes:
83
+ Y=X[:,:,:,self.train_indices+1]
84
+ return np.moveaxis(Y, [0,1,2,3], [2,0,1,3])
85
+ else:
86
+ Y = X[:,:,self.train_indices+1] ## we only keep the good indices # shift of 1, because the first index is for indexing the pts
87
+ return np.moveaxis(Y, [0,1,2], [1,0,2]) ## we put back the degree axis first
88
+
89
+ # # out = np.moveaxis(Y,-1,0) ## we put back the degree axis first
90
+ # return out
91
+
92
+
93
+
94
+ class DistanceMatrix2Kernel(BaseEstimator, TransformerMixin):
95
+ """
96
+ Input : (degree) x (distance matrix) or (axis) x (degree) x (distance matrix) in the second case, axis HAS to be specified (meant for cross validation)
97
+ Output : kernel of the same shape of distance matrix
98
+ """
99
+ def __init__(self, sigma:float|Iterable[float]=1, axis:int|None=None, weights:Iterable[float]|float=1) -> None:
100
+ super().__init__()
101
+ self.sigma = sigma
102
+ self.axis=axis
103
+ self.weights = weights
104
+ # self._num_axes=None
105
+ self._num_degrees = None
106
+ def fit(self, X, y=None):
107
+ if len(X) == 0: return self
108
+ assert X.ndim in [3,4], "Bad input."
109
+ if self.axis is None:
110
+ assert X.ndim ==3 or X.shape[0] == 1, "Set an axis for data with axis !"
111
+ if X.shape[0] == 1 and X.ndim == 4:
112
+ self.axis=0
113
+ self._num_degrees = len(X[0])
114
+ else:
115
+ self._num_degrees = len(X)
116
+ else:
117
+ assert X.ndim ==4, "Cannot choose axis from data with no axis !"
118
+ self._num_degrees = len(X[self.axis])
119
+ if isinstance(self.weights,float) or isinstance(self.weights,int): self.weights = [self.weights]*self._num_degrees
120
+ assert len(self.weights) == self._num_degrees, f"Number of weights ({len(self.weights)}) has to be the same as the number of degrees ({self._num_degrees})"
121
+ return self
122
+ def transform(self,X)->np.ndarray:
123
+ if self.axis is not None:
124
+ X=X[self.axis]
125
+ kernels = np.asarray([np.exp(-distance_matrix / (2*self.sigma**2))*weight for distance_matrix, weight in zip(X, self.weights)])
126
+ out = np.mean(kernels, axis=0)
127
+
128
+ return out
multipers/ml/mma.py ADDED
@@ -0,0 +1,422 @@
1
+
2
+ from typing import Callable, Iterable,List, Optional
3
+ import multipers as mp
4
+ from multipers.ml.tools import filtration_grid_to_coordinates
5
+ import numpy as np
6
+ from joblib import Parallel, delayed
7
+ from sklearn.base import BaseEstimator, TransformerMixin
8
+ from multipers.multiparameter_module_approximation import PyModule
9
+ from tqdm import tqdm
10
+
11
+ from multipers.simplex_tree_multi import SimplexTreeMulti
12
+ reduce_grid = mp.simplex_tree_multi.SimplexTreeMulti._reduce_grid
13
+
14
+
15
+
16
+ class SimplexTree2MMA(BaseEstimator, TransformerMixin):
17
+ """
18
+ Turns a list of simplextrees to MMA approximations
19
+ """
20
+ def __init__(self,n_jobs=-1, expand_dim:Optional[int]=None, prune_degrees_above:Optional[int]=None, progress=False, **persistence_kwargs) -> None:
21
+ super().__init__()
22
+ self.persistence_args = persistence_kwargs
23
+ self.n_jobs=n_jobs
24
+ self._has_axis=None
25
+ self._num_axis=None
26
+ self.prune_degrees_above=prune_degrees_above
27
+ self.progress=progress
28
+ self.expand_dim=expand_dim
29
+ self._boxes=None
30
+ return
31
+ def fit(self, X, y=None):
32
+ if len(X) == 0:
33
+ return self
34
+ self._has_axis = not isinstance(X[0], mp.SimplexTreeMulti)
35
+ if self._has_axis:
36
+ try:
37
+ X[0][0]
38
+ except IndexError:
39
+ print(f"IndexError, {X[0]=}")
40
+ if len(X[0]) == 0:
41
+ print("No simplextree found, maybe you forgot to give a filtration parameter to the previous pipeline")
42
+ raise IndexError
43
+ assert isinstance(X[0][0], mp.SimplexTreeMulti), f"X[0] is not a simplextre, {X[0]=}, and X[0][0] neither."
44
+ self._num_axis = len(X[0])
45
+ filtration_values = np.asarray([[x[axis].filtration_bounds() for x in X] for axis in range(self._num_axis)])
46
+ num_parameters = filtration_values.shape[-1]
47
+ ## Output : axis, data, min/max, num_parameters
48
+ # print("TEST : NUM PARAMETERS ", num_parameters)
49
+ m = np.asarray([[filtration_values[axis,:,0,parameter].min() for parameter in range(num_parameters)] for axis in range(self._num_axis)])
50
+ M = np.asarray([[filtration_values[axis,:,1,parameter].max() for parameter in range(num_parameters)] for axis in range(self._num_axis)])
51
+ ## shape of m/M axis,num_parameters
52
+ self._boxes = [[m_of_axis,M_of_axis] for m_of_axis,M_of_axis in zip(m,M)]
53
+ else:
54
+ filtration_values = np.asarray([x.filtration_bounds() for x in X])
55
+ num_parameters = filtration_values.shape[-1]
56
+ # print("TEST : NUM PARAMETERS ", num_parameters)
57
+ m = np.asarray([filtration_values[:,0,parameter].min() for parameter in range(num_parameters)])
58
+ M = np.asarray([filtration_values[:,1,parameter].max() for parameter in range(num_parameters)])
59
+ self._boxes = [m,M]
60
+ return self
61
+ def transform(self,X):
62
+ if self.prune_degrees_above is not None:
63
+ for x in X:
64
+ if self._has_axis:
65
+ for x_ in x:
66
+ x_.prune_above_dimension(self.prune_degrees_above) # we only do for H0 for computational ease
67
+ else:
68
+ x.prune_above_dimension(self.prune_degrees_above) # we only do for H0 for computational ease
69
+
70
+ def todo1(x:mp.SimplexTreeMulti,box):
71
+ # print(x.get_filtration_grid(resolution=3, grid_strategy="regular"))
72
+ # print("TEST BOX",box)
73
+ if self.expand_dim is not None:
74
+ x.expansion(self.expand_dim)
75
+ return x.persistence_approximation(box=box,verbose=False,**self.persistence_args)
76
+ # if self._has_axis:
77
+ # def todo(sts:List[SimplexTreeMulti]):
78
+ # return [todo1(st,box) for st,box in zip(sts,self._boxes)]
79
+ # else:
80
+ # def todo(x:SimplexTreeMulti):
81
+ # return todo1(x,self._boxes)
82
+ def todo(sts:List[SimplexTreeMulti]|SimplexTreeMulti):
83
+ if self._has_axis:
84
+ assert not isinstance(sts,SimplexTreeMulti)
85
+ return [todo1(st,box) for st,box in zip(sts,self._boxes)]
86
+ assert isinstance(sts,SimplexTreeMulti)
87
+ return todo1(sts,self._boxes)
88
+ return Parallel(n_jobs=self.n_jobs, backend="threading")(delayed(todo)(x) for x in tqdm(X, desc="Computing modules", disable = not self.progress))
89
+
90
+
91
+ class MMAFormatter(BaseEstimator, TransformerMixin):
92
+
93
+ def __init__(self, degrees:list=[0,1], axis=None, verbose:bool=False, normalize:bool=False,weights=None, quantiles=None, dump=False,from_dump=False):
94
+ self._module_bounds=None
95
+ self.verbose=verbose
96
+ self.axis=axis
97
+ self._axis=[]
98
+ self._has_axis=None
99
+ self._num_axis=0
100
+ self.degrees=degrees
101
+ self.normalize = normalize
102
+ self._num_parameters = None
103
+ self.weights = weights
104
+ self.quantiles=quantiles
105
+ self.dump=dump
106
+ self.from_dump=from_dump
107
+
108
+ @staticmethod
109
+ def _maybe_from_dump(X_in):
110
+ if len(X_in) == 0:
111
+ return X_in
112
+ import pickle
113
+ if isinstance(X_in[0], bytes):
114
+ X = [pickle.loads(mods) for mods in X_in]
115
+ else:
116
+ X = X_in
117
+ return X
118
+ # return [[mp.multiparameter_module_approximation.from_dump(mod) for mod in mods] for mods in dumped_modules]
119
+
120
+ @staticmethod
121
+ def _get_module_bound(x,degree):
122
+ """
123
+ Output format : (2,num_parameters)
124
+ """
125
+ # l,L = x.get_box()
126
+ filtration_values = x.get_module_of_degree(degree).get_filtration_values(unique=True)
127
+ out = np.array([[f[0],f[-1]] for f in filtration_values if len(f)>0 ]).T
128
+ if len(out) != 2:
129
+ print(f"Missing degree {degree} here !")
130
+ m = M = [np.nan for _ in range(x.num_parameters)]
131
+ else:
132
+ m,M = out
133
+ # m = np.where(m<np.inf, m, l)
134
+ # M = np.where(M>-np.inf, M,L)
135
+ return m,M
136
+
137
+ @staticmethod
138
+ def _infer_axis(X):
139
+ has_axis = not isinstance(X[0], PyModule)
140
+ assert not has_axis or isinstance(X[0][0], PyModule)
141
+ return has_axis
142
+
143
+ @staticmethod
144
+ def _infer_num_parameters(X,ax=slice(None)):
145
+ return X[0][ax].num_parameters
146
+
147
+ @staticmethod
148
+ def _infer_bounds(X, degrees=None, axis=[slice(None)], quantiles=None):
149
+ """
150
+ Compute bounds of filtration values of a list of modules.
151
+
152
+ Output Format
153
+ -------------
154
+ m,M of shape : (num_axis,num_degrees,2,num_parameters)
155
+ """
156
+ if degrees is None:
157
+ degrees = np.arange(X[0][axis[0]].max_degree+1)
158
+ bounds = np.array([[[MMAFormatter._get_module_bound(x[ax],degree) for degree in degrees] for ax in axis] for x in X])
159
+ if quantiles is not None:
160
+ qm,qM = quantiles
161
+ # TODO per axis, degree !!
162
+ # m = np.quantile(bounds[:,:,:,0,:], q=qm,axis=0)
163
+ # M = np.quantile(bounds[:,:,:,1,:], q=1-qM,axis=0)
164
+ num_pts, num_axis,num_degrees,_,num_parameters = bounds.shape
165
+ m = [[[np.nanquantile(bounds[:,ax,degree,0,parameter], axis=0, q=qm) for parameter in range(num_parameters)] for degree in range(num_degrees)] for ax in range(num_axis)]
166
+ m = np.asarray(m)
167
+ M = [[[np.nanquantile(bounds[:,ax,degree,1,parameter], axis=0, q=1-qM) for parameter in range(num_parameters)] for degree in range(num_degrees)] for ax in range(num_axis)]
168
+ M = np.asarray(M)
169
+ else:
170
+ num_pts, num_axis,num_degrees,_,num_parameters = bounds.shape
171
+ m = [[[np.nanmin(bounds[:,ax,degree,0,parameter], axis=0) for parameter in range(num_parameters)] for degree in range(num_degrees)] for ax in range(num_axis)]
172
+ m = np.asarray(m)
173
+ M = [[[np.nanmax(bounds[:,ax,degree,1,parameter], axis=0) for parameter in range(num_parameters)] for degree in range(num_degrees)] for ax in range(num_axis)]
174
+ M = np.asarray(M)
175
+ # m = bounds[:,:,:,0,:].min(axis=0)
176
+ # M = bounds[:,:,:,1,:].max(axis=0)
177
+ return (m,M)
178
+
179
+ @staticmethod
180
+ def _infer_grid(X:List[PyModule], strategy:str,resolution:int, degrees=None):
181
+ """
182
+ Given a list of PyModules, computes a multiparameter discrete grid,
183
+ with a given strategy,
184
+ from the filtration values of the summands of the modules.
185
+ """
186
+ num_parameters = X[0].num_parameters
187
+ if degrees is None:
188
+ ## Format here : ((filtration values of parameter) for parameter)
189
+ filtration_values = tuple(mod.get_filtration_values(unique=True) for mod in X)
190
+ else:
191
+ filtration_values = tuple(mod.get_module_of_degrees(degrees).get_filtration_values(unique=True) for mod in X)
192
+
193
+ if "_mean" in strategy:
194
+ substrategy = strategy.split("_")[0]
195
+ processed_filtration_values = [reduce_grid(f, resolution, substrategy, unique=False) for f in filtration_values]
196
+ reduced_grid = np.mean(processed_filtration_values, axis=0)
197
+ # elif "_quantile" in strategy:
198
+ # substrategy = strategy.split("_")[0]
199
+ # processed_filtration_values = [reduce_grid(f, resolution, substrategy, unique=False) for f in filtration_values]
200
+ # reduced_grid = np.qu(processed_filtration_values, axis=0)
201
+ else:
202
+ filtration_values = [np.unique(np.concatenate([f[parameter] for f in filtration_values], axis=0)) for parameter in range(num_parameters)]
203
+ reduced_grid = reduce_grid(filtration_values, resolution, strategy,unique=True)
204
+
205
+ coordinates, new_resolution = filtration_grid_to_coordinates(reduced_grid, return_resolution=True)
206
+ return coordinates,new_resolution
207
+
208
+ def fit(self, X_in, y=None):
209
+ X = self._maybe_from_dump(X_in)
210
+ if len(X) == 0:
211
+ return self
212
+ self._has_axis = self._infer_axis(X)
213
+ # assert not self._has_axis or isinstance(X[0][0], mp.PyModule)
214
+ if self.axis is None and self._has_axis:
215
+ self.axis = -1
216
+ if self.axis is not None and not (self._has_axis):
217
+ raise Exception(f"SMF didn't find an axis, but requested axis {self.axis}")
218
+ if self._has_axis:
219
+ self._num_axis = len(X[0])
220
+ if self.verbose:
221
+ print('-----------MMAFormatter-----------')
222
+ print('---- Infered stats')
223
+ print(f'Found axis : {self._has_axis}, num : {self._num_axis}')
224
+ print(f'Number of parameters : {self._num_parameters}')
225
+ self._axis = [slice(None)] if self.axis is None else range(self._num_axis) if self.axis == -1 else [self.axis]
226
+
227
+ self._num_parameters = self._infer_num_parameters(X, ax=self._axis[0])
228
+ if self.normalize:
229
+ # print(self._axis)
230
+ self._module_bounds = self._infer_bounds(X,self.degrees, self._axis, self.quantiles)
231
+ else:
232
+ m = np.zeros((self._num_axis,len(self.degrees),self._num_parameters))
233
+ M = m+1
234
+ self._module_bounds = (m,M)
235
+ assert self._num_parameters == self._module_bounds[0].shape[-1]
236
+ if self.verbose:
237
+ print('---- Bounds (only computed if normalize):')
238
+ if self._has_axis and self._num_axis>1:
239
+ print('(axis) x (degree) x (parameter)')
240
+ else:
241
+ print('(degree) x (parameter)')
242
+ m,M = self._module_bounds
243
+ print('-- Lower bound : ', m.shape)
244
+ print(m)
245
+ print('-- Upper bound :', M.shape)
246
+ print(M)
247
+ w = 1 if self.weights is None else np.asarray(self.weights)
248
+ m,M = self._module_bounds
249
+ normalizer = M-m
250
+ zero_normalizer = normalizer==0
251
+ if np.any(zero_normalizer):
252
+ from warnings import warn
253
+ warn(f"Encountered empty bounds. Please fix me. \n M-m = {normalizer}")
254
+ normalizer[zero_normalizer] = 1
255
+ self._normalization_factors = w/normalizer
256
+ if self.verbose:
257
+ print('-- Normalization factors:', self._normalization_factors.shape)
258
+ print(self._normalization_factors)
259
+
260
+ if self.verbose:
261
+ print('---- Module size :')
262
+ for ax in self._axis:
263
+ print(f'- Axis {ax}')
264
+ for degree in self.degrees:
265
+ sizes = [len(x[ax].get_module_of_degree(degree)) for x in X]
266
+ print(f' - Degree {degree} size {np.mean(sizes).round(decimals=2)}±{np.std(sizes).round(decimals=2)}')
267
+ print('----------------------------------')
268
+ return self
269
+
270
+ @staticmethod
271
+ def copy_transform(mod, degrees, translation, rescale_factors, new_box):
272
+ copy = mod.get_module_of_degrees(degrees) # and only returns the specific degrees
273
+ for j,degree in enumerate(degrees):
274
+ copy.translate(translation[j], degree=degree)
275
+ copy.rescale(rescale_factors[j], degree=degree)
276
+ copy.set_box(new_box)
277
+ return copy
278
+
279
+ def transform(self, X_in):
280
+ X = self._maybe_from_dump(X_in)
281
+ if np.any(self._normalization_factors != 1):
282
+ if self.verbose: print("Normalizing...", end="")
283
+ w = [1]*self._num_parameters if self.weights is None else np.asarray(self.weights)
284
+ standard_box = mp.multiparameter_module_approximation.PyBox([0]*self._num_parameters, w)
285
+
286
+ X_copy = [[self.copy_transform(
287
+ mod=x[ax],
288
+ degrees=self.degrees,
289
+ translation=-self._module_bounds[0][i],
290
+ rescale_factors = self._normalization_factors[i],
291
+ new_box=standard_box)
292
+ for i,ax in enumerate(self._axis)]
293
+ for x in X]
294
+ if self.verbose:
295
+ print("Done.")
296
+ return X_copy
297
+ if self.axis != -1:
298
+ X = [x[self.axis] for x in X]
299
+ if self.dump:
300
+ import pickle
301
+ X = [pickle.dumps(mods) for mods in X]
302
+ return X
303
+ # return [todo(x) for x in X]
304
+
305
+ class MMA2IMG(BaseEstimator, TransformerMixin):
306
+ def __init__(self,
307
+ degrees:list,
308
+ bandwidth:float=0.1,
309
+ power:float=1,
310
+ normalize:bool=False,
311
+ resolution:list|int=50,
312
+ plot:bool=False,
313
+ box = None,
314
+ n_jobs=1,
315
+ flatten=False,
316
+ progress=False,
317
+ grid_strategy="regular",
318
+ ):
319
+ self.bandwidth=bandwidth
320
+ self.degrees = degrees
321
+ self.resolution=resolution
322
+ self.box=box
323
+ self.plot = plot
324
+ self._box=None
325
+ self.normalize = normalize
326
+ self.power = power
327
+ self._has_axis=None
328
+ self._num_parameters=None
329
+ self.n_jobs=n_jobs
330
+ self.flatten=flatten
331
+ self.progress=progress
332
+ self.grid_strategy=grid_strategy
333
+ self._num_axis=None
334
+ self._coords_to_compute=None
335
+ self._new_resolutions=None
336
+ def fit(self, X, y=None):
337
+ # TODO infer box
338
+ # TODO rescale module
339
+ self._has_axis = MMAFormatter._infer_axis(X)
340
+ if self._has_axis:
341
+ self._num_axis = len(X[0])
342
+ if self.box is None:
343
+ self._box = [[0],[1,1]]
344
+ else:
345
+ self._box = self.box
346
+ if self._has_axis:
347
+ its = (tuple(x[axis] for x in X) for axis in range(self._num_axis))
348
+ crs = tuple(MMAFormatter._infer_grid(X_axis, self.grid_strategy,self.resolution, degrees=self.degrees) for X_axis in its)
349
+ self._coords_to_compute = [c for c,_ in crs] ## not the same resolutions, so cannot be put in an array
350
+ self._new_resolutions = np.asarray([r for _, r in crs])
351
+ else:
352
+ coords, new_resolution = MMAFormatter._infer_grid(X, self.grid_strategy,self.resolution, degrees=self.degrees)
353
+ self._coords_to_compute = coords
354
+ self._new_resolutions = new_resolution
355
+ return self
356
+
357
+ def transform(self, X):
358
+ img_args = {
359
+ "delta":self.bandwidth,
360
+ "p":self.power,
361
+ "normalize" : self.normalize,
362
+ # "plot":self.plot,
363
+ # "cb":1, # colorbar
364
+ # "resolution" : self.resolution, # info in coordinates
365
+ "box" : self.box,
366
+ "degrees" : self.degrees,
367
+ "n_jobs":self.n_jobs, # num_jobs is better for parallel over modules.
368
+ }
369
+ if self._has_axis:
370
+ todo1 = lambda x, c : x._compute_pixels(c, **img_args)
371
+ else:
372
+ todo1 = lambda x : x._compute_pixels(self._coords_to_compute, **img_args)[None,:] # shape same as has_axis
373
+
374
+ if self._has_axis:
375
+ todo2 = lambda mods : [todo1(mod,c) for mod,c in zip(mods, self._coords_to_compute)]
376
+ else:
377
+ todo2 = todo1
378
+
379
+ if self.flatten:
380
+ todo = lambda mods : np.concatenate(todo2(mods),axis=1).flatten()
381
+ else:
382
+ todo = lambda mods : [img.reshape(len(img_args["degrees"]),*r) for img,r in zip(todo2(mods), self._new_resolutions)]
383
+
384
+ return Parallel(n_jobs=self.n_jobs, backend="threading")(delayed(todo)(x) for x in tqdm(X, desc="Computing images", disable = not self.progress)) ## res depends on ax (infer_grid)
385
+
386
+
387
+
388
+
389
+
390
+
391
+ class MMA2Landscape(BaseEstimator, TransformerMixin):
392
+ """
393
+ Turns a list of MMA approximations into Landscapes vectorisations
394
+ """
395
+ def __init__(self, resolution=[100,100], degrees:list[int]|None = [0,1], ks:Iterable[int]=range(5), phi:Callable = np.sum, box=None, plot:bool=False, n_jobs=-1, filtration_quantile:float=0.01) -> None:
396
+ super().__init__()
397
+ self.resolution:list[int]=resolution
398
+ self.degrees = degrees
399
+ self.ks=ks
400
+ self.phi=phi # Has to have a axis=0 !
401
+ self.box = box
402
+ self.plot = plot
403
+ self.n_jobs=n_jobs
404
+ self.filtration_quantile = filtration_quantile
405
+ return
406
+ def fit(self, X, y=None):
407
+ if len(X) <= 0: return
408
+ assert X[0].num_parameters == 2, f"Number of parameters {X[0].num_parameters} has to be 2."
409
+ if self.box is None:
410
+ _bottom = lambda mod : mod.get_bottom()
411
+ _top = lambda mod : mod.get_top()
412
+ m = np.quantile(Parallel(n_jobs=self.n_jobs, backend="threading")(delayed(_bottom)(mod) for mod in X), q=self.filtration_quantile, axis=0)
413
+ M = np.quantile(Parallel(n_jobs=self.n_jobs, backend="threading")(delayed(_top)(mod) for mod in X), q=1-self.filtration_quantile, axis=0)
414
+ self.box=[m,M]
415
+ return self
416
+ def transform(self,X)->list[np.ndarray]:
417
+ if len(X) <= 0: return
418
+ todo = lambda mod : np.concatenate([
419
+ self.phi(mod.landscapes(ks=self.ks, resolution = self.resolution, degree=degree, plot=self.plot), axis=0).flatten()
420
+ for degree in self.degrees
421
+ ]).flatten()
422
+ return Parallel(n_jobs=self.n_jobs, backend="threading")(delayed(todo)(x) for x in X)