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,931 @@
1
+ """!
2
+ @package mma
3
+ @brief Files containing the C++ cythonized functions.
4
+ @author David Loiseaux
5
+ @copyright Copyright (c) 2022 Inria.
6
+ """
7
+
8
+ # distutils: language = c++
9
+
10
+ ###########################################################################
11
+ ## PYTHON LIBRARIES
12
+ import gudhi as gd
13
+ import numpy as np
14
+ from typing import List
15
+ import pickle as pk
16
+ import matplotlib.pyplot as plt
17
+
18
+ ###########################################################################
19
+ ## CPP CLASSES
20
+ from libc.stdint cimport intptr_t
21
+ from libc.stdint cimport uintptr_t
22
+
23
+ ###########################################################################
24
+ ## CYTHON TYPES
25
+ from libcpp.vector cimport vector
26
+ from libcpp.utility cimport pair
27
+ #from libcpp.list cimport list as clist
28
+ from libcpp cimport bool
29
+ from libcpp cimport int
30
+ from typing import Iterable
31
+
32
+ #########################################################################
33
+ ## Multipersistence Module Approximation Classes
34
+ from multipers.multiparameter_module_approximation cimport *
35
+
36
+
37
+
38
+ #########################################################################
39
+ ## Small hack for typing
40
+ from gudhi import SimplexTree
41
+ from multipers.simplex_tree_multi import SimplexTreeMulti
42
+ # cimport numpy as cnp
43
+ # cnp.import_array()
44
+
45
+ ###################################### MMA
46
+ cdef extern from "multiparameter_module_approximation/approximation.h" namespace "Gudhi::multiparameter::mma":
47
+ Module compute_vineyard_barcode_approximation(boundary_matrix, vector[Finitely_critical_multi_filtration] , value_type precision, Box[value_type] &, bool threshold, bool complete, bool multithread, bool verbose) nogil
48
+
49
+
50
+ cdef class PySummand:
51
+ """
52
+ Stores a Summand of a PyModule
53
+ """
54
+ cdef Summand sum
55
+ # def __cinit__(self, vector[corner_type]& births, vector[corner_type]& deaths, int dim):
56
+ # self.sum = Summand(births, deaths, dim)
57
+
58
+ def get_birth_list(self):
59
+ return np.asarray(Finitely_critical_multi_filtration.to_python(self.sum.get_birth_list()))
60
+
61
+ def get_death_list(self):
62
+ return np.asarray(Finitely_critical_multi_filtration.to_python(self.sum.get_death_list()))
63
+ @property
64
+ def degree(self)->int:
65
+ return self.sum.get_dimension()
66
+
67
+ cdef set(self, Summand& summand):
68
+ self.sum = summand
69
+ return self
70
+ def get_bounds(self):
71
+ cdef pair[Finitely_critical_multi_filtration,Finitely_critical_multi_filtration] cbounds
72
+ with nogil:
73
+ cbounds = self.sum.get_bounds().get_pair()
74
+ # return np.array(<value_type[:self.num_parameters]>(&cbounds.first[0])),np.array(<value_type[:self.num_parameters]>(&cbounds.second[0]))
75
+ return np.asarray(cbounds.first._convert_back()), np.asarray(cbounds.second._convert_back())
76
+
77
+ cdef get_summand_filtration_values(Summand summand):
78
+ births = np.asarray(Finitely_critical_multi_filtration.to_python(summand.get_birth_list()))
79
+ deaths = np.asarray(Finitely_critical_multi_filtration.to_python(summand.get_death_list()))
80
+ pts = np.concatenate([births,deaths],axis=0)
81
+ num_parameters = pts.shape[1]
82
+ out = [np.unique(pts[:,parameter]) for parameter in range(num_parameters)]
83
+ out = [f[:-1] if f[-1] == np.inf else f for f in out]
84
+ out = [f[1:] if f[0] == -np.inf else f for f in out]
85
+ return out
86
+
87
+ cdef class PyBox:
88
+ cdef Box[value_type] box
89
+ def __cinit__(self, corner_type bottomCorner, corner_type topCorner):
90
+ self.box = Box[value_type](bottomCorner, topCorner)
91
+ @property
92
+ def num_parameters(self):
93
+ cdef size_t dim = self.box.get_bottom_corner().size()
94
+ if dim == self.box.get_upper_corner().size(): return dim
95
+ else: print("Bad box definition.")
96
+ def contains(self, x):
97
+ return self.box.contains(x)
98
+ cdef set(self, Box[value_type]& b):
99
+ self.box = b
100
+ return self
101
+
102
+ def get(self):
103
+ return [self.box.get_bottom_corner().get_vector(),self.box.get_upper_corner().get_vector()]
104
+ def to_multipers(self):
105
+ #assert (self.get_dimension() == 2) "Multipers only works in dimension 2 !"
106
+ return np.array(self.get()).flatten(order = 'F')
107
+
108
+ cdef class PyMultiDiagramPoint:
109
+ cdef MultiDiagram_point point
110
+ cdef set(self, MultiDiagram_point &pt):
111
+ self.point = pt
112
+ return self
113
+
114
+ def get_degree(self):
115
+ return self.point.get_dimension()
116
+ def get_birth(self):
117
+ return self.point.get_birth()
118
+ def get_death(self):
119
+ return self.point.get_death()
120
+
121
+ cdef class PyMultiDiagram:
122
+ """
123
+ Stores the diagram of a PyModule on a line
124
+ """
125
+ cdef MultiDiagram multiDiagram
126
+ cdef set(self, MultiDiagram m):
127
+ self.multiDiagram = m
128
+ return self
129
+ def get_points(self, degree:int=-1) -> np.ndarray:
130
+ out = self.multiDiagram.get_points(degree)
131
+ if len(out) == 0 and len(self) == 0:
132
+ return np.empty() # TODO Retrieve good number of parameters if there is no points in diagram
133
+ if len(out) == 0:
134
+ return np.empty((0,2,self.multiDiagram.at(0).get_dimension())) # gets the number of parameters
135
+ return np.array(out)
136
+ def to_multipers(self, dimension:int):
137
+ return self.multiDiagram.to_multipers(dimension)
138
+ def __len__(self) -> int:
139
+ return self.multiDiagram.size()
140
+ def __getitem__(self,i:int) -> PyMultiDiagramPoint:
141
+ return PyMultiDiagramPoint().set(self.multiDiagram.at(i % self.multiDiagram.size()))
142
+ cdef class PyMultiDiagrams:
143
+ """
144
+ Stores the barcodes of a PyModule on multiple lines
145
+ """
146
+ cdef MultiDiagrams multiDiagrams
147
+ cdef set(self,MultiDiagrams m):
148
+ self.multiDiagrams = m
149
+ return self
150
+ def to_multipers(self):
151
+ out = self.multiDiagrams.to_multipers()
152
+ # return out
153
+ return [np.array(summand, dtype=np.float64) for summand in out]
154
+ def __getitem__(self,i:int):
155
+ if i >=0 :
156
+ return PyMultiDiagram().set(self.multiDiagrams.at(i))
157
+ else:
158
+ return PyMultiDiagram().set(self.multiDiagrams.at( self.multiDiagrams.size() - i))
159
+ def __len__(self):
160
+ return self.multiDiagrams.size()
161
+ def get_points(self, degree:int=-1):
162
+ return self.multiDiagrams.get_points()
163
+ # return np.array([x.get_points(dimension) for x in self.multiDiagrams], dtype=float)
164
+ # return np.array([PyMultiDiagram().set(x).get_points(dimension) for x in self.multiDiagrams])
165
+ cdef _get_plot_bars(self, dimension:int=-1, min_persistence:float=0):
166
+ return self.multiDiagrams._for_python_plot(dimension, min_persistence);
167
+ def plot(self, degree:int=-1, min_persistence:float=0):
168
+ """
169
+ Plots the barcodes.
170
+
171
+ Parameters
172
+ ----------
173
+ degree:int=-1
174
+ Only plots the bars of specified homology degree. Useful when the multidiagrams contains multiple dimenions
175
+ min_persistence:float=0
176
+ Only plot bars of length greater than this value. Useful to reduce the time to plot.
177
+
178
+ Warning
179
+ -------
180
+ If the barcodes are not thresholded, essential barcodes will not be displayed !
181
+
182
+ Returns
183
+ -------
184
+ Nothing
185
+ """
186
+ from cycler import cycler
187
+ import matplotlib
188
+ import matplotlib.pyplot as plt
189
+ if len(self) == 0: return
190
+ _cmap = matplotlib.colormaps["Spectral"]
191
+ multibarcodes_, colors = self._get_plot_bars(degree, min_persistence)
192
+ n_summands = np.max(colors)+1 if len(colors)>0 else 1
193
+
194
+ plt.rc('axes', prop_cycle = cycler('color', [_cmap(i/n_summands) for i in colors]))
195
+ plt.plot(*multibarcodes_)
196
+ cdef class PyLine:
197
+ cdef Line[value_type] line
198
+
199
+ cdef class PyModule:
200
+ """
201
+ Stores a representation of a n-persistence module.
202
+ """
203
+ cdef Module cmod
204
+
205
+ cdef set(self, Module m):
206
+ self.cmod = m
207
+ def set_box(self, PyBox pybox):
208
+ cdef Box[value_type] cbox = pybox.box
209
+ with nogil:
210
+ self.cmod.set_box(cbox)
211
+ return self
212
+ def get_module_of_degree(self, int degree)->PyModule: # TODO : in c++ ?
213
+ pmodule = PyModule()
214
+ cdef Box[value_type] c_box = self.cmod.get_box()
215
+ pmodule.cmod.set_box(c_box)
216
+ with nogil:
217
+ for summand in self.cmod:
218
+ if summand.get_dimension() == degree:
219
+ pmodule.cmod.add_summand(summand)
220
+ return pmodule
221
+ def get_module_of_degrees(self, degrees:Iterable[int])->PyModule: # TODO : in c++ ?
222
+ pmodule = PyModule()
223
+ cdef Box[value_type] c_box = self.cmod.get_box()
224
+ pmodule.cmod.set_box(c_box)
225
+ cdef vector[int] cdegrees = degrees
226
+ with nogil:
227
+ for summand in self.cmod:
228
+ for d in cdegrees:
229
+ if d == summand.get_dimension():
230
+ pmodule.cmod.add_summand(summand)
231
+ return pmodule
232
+ def _compute_pixels(self,coordinates:np.ndarray,
233
+ degrees=None, box=None, value_type delta=.1,
234
+ value_type p=1., bool normalize=False, int n_jobs=0):
235
+ if degrees is not None: assert np.all(degrees[:-1] <= degrees[1:]), "Degrees have to be sorted"
236
+ cdef vector[int] cdegrees = np.arange(self.max_degree +1) if degrees is None else degrees
237
+ pybox = PyBox(*self.get_box()) if box is None else PyBox(*box)
238
+ cdef Box[value_type] cbox = pybox.box
239
+ cdef vector[vector[value_type]] ccoords = coordinates
240
+ cdef vector[vector[value_type]] out
241
+ with nogil:
242
+ out = self.cmod.compute_pixels(ccoords, cdegrees, cbox, delta, p, normalize, n_jobs)
243
+ return np.asarray(out)
244
+ def __len__(self)->int:
245
+ return self.cmod.size()
246
+ def get_bottom(self)->list:
247
+ return self.cmod.get_box().get_bottom_corner().get_vector()
248
+ def get_top(self)->list:
249
+ return self.cmod.get_box().get_upper_corner().get_vector()
250
+ def get_box(self):
251
+ return np.asarray([self.get_bottom(), self.get_top()])
252
+ @property
253
+ def max_degree(self)->int:
254
+ return self.cmod.get_dimension()
255
+ @property
256
+ def num_parameters(self)->int:
257
+ cdef size_t dim = self.cmod.get_box().get_bottom_corner().size()
258
+ assert dim == self.cmod.get_box().get_upper_corner().size(), "Bad box definition, cannot infer num_parameters."
259
+ return dim
260
+ def dump(self, path:str|None=None):
261
+ """
262
+ Dumps the module into a pickle-able format.
263
+
264
+ Parameters
265
+ ----------
266
+ path:str=None (optional) saves the pickled module in specified path
267
+
268
+ Returns
269
+ -------
270
+ list of list, encoding the module, which can be retrieved with the function `from_dump`.
271
+ """
272
+ ## TODO : optimize, but not really used.
273
+ return dump_cmod(self.cmod)
274
+ def __getstate__(self):
275
+ return self.dump()
276
+ def __setstate__(self,dump):
277
+ cdef Module cmod = cmod_from_dump(dump)
278
+ self.cmod = cmod
279
+ return
280
+ def __getitem__(self, i) -> PySummand:
281
+ if i == slice(None):
282
+ return self
283
+ summand = PySummand()
284
+ summand.set(self.cmod.at(i % self.cmod.size()))
285
+ return summand
286
+ def get_bounds(self):
287
+ cdef pair[Finitely_critical_multi_filtration,Finitely_critical_multi_filtration] cbounds
288
+ with nogil:
289
+ cbounds = self.cmod.get_bounds().get_pair()
290
+ # return np.array(<value_type[:self.num_parameters]>(&cbounds.first[0])),np.array(<value_type[:self.num_parameters]>(&cbounds.second[0]))
291
+ return np.asarray(cbounds.first._convert_back()), np.asarray(cbounds.second._convert_back())
292
+ def rescale(self,rescale_factors, int degree=-1):
293
+ """
294
+ Rescales the fitlration values of the summands by this rescaling vector.
295
+ """
296
+ cdef vector[value_type] crescale_factors = rescale_factors
297
+ with nogil:
298
+ self.cmod.rescale(crescale_factors,degree)
299
+ def translate(self,translation, int degree=-1):
300
+ """
301
+ Translates the module in the filtration space by this vector.
302
+ """
303
+ cdef vector[value_type] ctranslation = translation
304
+ with nogil:
305
+ self.cmod.translate(ctranslation,degree)
306
+
307
+ def get_filtration_values(self, unique=True):
308
+ """
309
+ Retrieves all filtration values of the summands of the module.
310
+
311
+ Output format
312
+ -------------
313
+
314
+ list of filtration values for parameter.
315
+ """
316
+ if len(self) ==0:
317
+ return np.empty((self.num_parameters,0))
318
+ values = [get_summand_filtration_values(summand) for summand in self.cmod]
319
+ values = [np.concatenate([f[parameter] for f in values], axis=0) for parameter in range(self.num_parameters)]
320
+ if unique:
321
+ return [np.unique(f) for f in values]
322
+ return values
323
+
324
+ def plot(self, degree:int=-1,**kwargs)->None:
325
+ """Shows the module on a plot. Each color corresponds to an apprimation summand of the module, and its shape corresponds to its support.
326
+ Only works with 2-parameter modules.
327
+
328
+ Parameters
329
+ ----------
330
+ degree = -1 : integer
331
+ If positive returns only the image of dimension `dimension`.
332
+ box=None : of the form [[b_x,b_y], [d_x,d_y]] where b,d are the bottom and top corner of the rectangle.
333
+ If non-None, will plot the module on this specific rectangle.
334
+ min_persistence =0 : float
335
+ Only plots the summand with a persistence above this threshold.
336
+ separated=False : bool
337
+ If true, plot each summand in a different plot.
338
+ alpha=1 : float
339
+ Transparancy parameter
340
+ save = False : string
341
+ if nontrivial, will save the figure at this path
342
+
343
+
344
+ Returns
345
+ -------
346
+ The figure of the plot.
347
+ """
348
+ from multipers.plots import plot2d_PyModule
349
+ if (kwargs.get('box')):
350
+ box = kwargs.pop('box')
351
+ else:
352
+ box = [self.get_bottom(), self.get_top()]
353
+ if (len(box[0]) != 2):
354
+ print("Filtration size :", len(box[0]), " != 2")
355
+ return
356
+ num = 0
357
+ if(degree < 0):
358
+ ndim = self.cmod.get_dimension()+1
359
+ scale = kwargs.pop("scale", 4)
360
+ fig, axes = plt.subplots(1, ndim, figsize=(ndim*scale,scale))
361
+ for degree in range(ndim):
362
+ plt.sca(axes[degree]) if ndim > 1 else plt.sca(axes)
363
+ self.plot(degree,box=box,**kwargs)
364
+ return
365
+ corners = self.cmod.get_corners_of_dimension(degree)
366
+ plot2d_PyModule(corners, box=box, dimension=degree, **kwargs)
367
+ return
368
+
369
+ def barcode(self, basepoint, degree:int,*, threshold = False): # TODO direction vector interface
370
+ """Computes the barcode of module along a lines.
371
+
372
+ Parameters
373
+ ----------
374
+ basepoint : vector
375
+ basepoint of the lines on which to compute the barcodes, i.e. a point on the line
376
+ degree = -1 : integer
377
+ Homology degree on which to compute the bars. If negative, every dimension is computed
378
+ box (default) :
379
+ box on which to compute the barcodes if basepoints is not specified. Default is a linspace of lines crossing that box.
380
+ threshold = False :
381
+ Thre
382
+
383
+ Warning
384
+ -------
385
+ If the barcodes are not thresholded, essential barcodes will not be plot-able.
386
+
387
+ Returns
388
+ -------
389
+ PyMultiDiagrams
390
+ Structure that holds the barcodes. Barcodes can be retrieved with a .get_points() or a .to_multipers() or a .plot().
391
+ """
392
+ out = PyMultiDiagram()
393
+ out.set(self.cmod.get_barcode(Line[value_type](basepoint), degree, threshold))
394
+ return out
395
+ def barcodes(self, degree:int, basepoints = None, num=100, box = None,threshold = False):
396
+ """Computes barcodes of module along a set of lines.
397
+
398
+ Parameters
399
+ ----------
400
+ basepoints = None : list of vectors
401
+ basepoints of the lines on which to compute the barcodes.
402
+ degree = -1 : integer
403
+ Homology degree on which to compute the bars. If negative, every dimension is computed
404
+ box (default) :
405
+ box on which to compute the barcodes if basepoints is not specified. Default is a linspace of lines crossing that box.
406
+ num:int=100
407
+ if basepoints is not specified, defines the number of lines to consider.
408
+ threshold = False : threshold t
409
+ Resolution of the image(s).
410
+
411
+ Warning
412
+ -------
413
+ If the barcodes are not thresholded, essential barcodes will not be plot-able.
414
+
415
+ Returns
416
+ -------
417
+ PyMultiDiagrams
418
+ Structure that holds the barcodes. Barcodes can be retrieved with a .get_points() or a .to_multipers() or a .plot().
419
+ """
420
+ out = PyMultiDiagrams()
421
+ if box is None:
422
+ box = [self.get_bottom(), self.get_top()]
423
+ if (len(box[0]) != 2) and (basepoints is None):
424
+ raise ValueError("Basepoints has to be specified for filtration dimension >= 3 !")
425
+ elif basepoints is None:
426
+ h = box[1][1] - box[0][1]
427
+ basepoints = np.linspace([box[0][0] - h,box[0][1]], [box[1][0],box[0][1]], num=num)
428
+ else :
429
+ num=len(basepoints)
430
+ cdef vector[cfiltration_type] cbasepoints
431
+ for i in range(num):
432
+ cbasepoints.push_back(Finitely_critical_multi_filtration(basepoints[i]))
433
+
434
+ out.set(self.cmod.get_barcodes(cbasepoints, degree, threshold))
435
+ return out
436
+ def landscape(self, degree:int, k:int=0,box:list|np.ndarray|None=None, resolution:List=[100,100], bool plot=False):
437
+ """Computes the multiparameter landscape from a PyModule. Python interface only bifiltrations.
438
+
439
+ Parameters
440
+ ----------
441
+ degree : integer
442
+ The homology degree of the landscape.
443
+ k = 0 : int
444
+ the k-th landscape
445
+ resolution = [50,50] : pair of integers
446
+ Resolution of the image.
447
+ box = None : in the format [[a,b], [c,d]]
448
+ If nontrivial, compute the landscape of this box. Default is the PyModule box.
449
+ plot = True : Boolean
450
+ If true, plots the images;
451
+ Returns
452
+ -------
453
+ Numpy array
454
+ The landscape of the module.
455
+ """
456
+ if box is None:
457
+ box = self.get_box()
458
+ cdef Box[value_type] c_box = Box[value_type](box)
459
+ out = np.array(self.cmod.get_landscape(degree, k, c_box, resolution))
460
+ if plot:
461
+ plt.figure()
462
+ aspect = (box[1][0]-box[0][0]) / (box[1][1]-box[0][1])
463
+ extent = [box[0][0], box[1][0], box[0][1], box[1][1]]
464
+ plt.imshow(out.T, origin="lower", extent=extent, aspect=aspect)
465
+ return out
466
+ def landscapes(self, degree:int, ks:list|np.ndarray=[0],box=None, resolution:list|np.ndarray=[100,100], bool plot=False):
467
+ """Computes the multiparameter landscape from a PyModule. Python interface only bifiltrations.
468
+
469
+ Parameters
470
+ ----------
471
+ degree : integer
472
+ The homology degree of the landscape.
473
+ ks = 0 : list of int
474
+ the k-th landscape
475
+ resolution = [50,50] : pair of integers
476
+ Resolution of the image.
477
+ box = None : in the format [[a,b], [c,d]]
478
+ If nontrivial, compute the landscape of this box. Default is the PyModule box.
479
+ plot = True : Boolean
480
+ If true, plots the images;
481
+ Returns
482
+ -------
483
+ Numpy array
484
+ The landscapes of the module with parameters ks.
485
+ """
486
+ if box is None:
487
+ box = self.get_box()
488
+ out = np.array(self.cmod.get_landscapes(degree, ks, Box[value_type](box), resolution))
489
+ if plot:
490
+ to_plot = np.sum(out, axis=0)
491
+ plt.figure()
492
+ aspect = (box[1][0]-box[0][0]) / (box[1][1]-box[0][1])
493
+ extent = [box[0][0], box[1][0], box[0][1], box[1][1]]
494
+ plt.imshow(to_plot.T, origin="lower", extent=extent, aspect=aspect)
495
+ return out
496
+
497
+
498
+ def image(self, degrees:Iterable[int]=None, bandwidth:float=0.1, resolution:list|int=50, normalize:bool=False, plot:bool=False, save:bool=False, dpi:int=200,p:float=1., box=None, flatten=False, int n_jobs=0, **kwargs)->np.ndarray:
499
+ """Computes a vectorization from a PyModule. Python interface only bifiltrations.
500
+
501
+ Parameters
502
+ ----------
503
+ degree = -1 : integer
504
+ If positive returns only the image of homology degree `degree`.
505
+ bandwidth = 0.1 : float
506
+ Image parameter. TODO : different weights
507
+ resolution = [100,100] : pair of integers
508
+ Resolution of the image(s).
509
+ normalize = True : Boolean
510
+ Ensures that the image belongs to [0,1].
511
+ plot = False : Boolean
512
+ If true, plots the images;
513
+ flatten=False :
514
+ If False, reshapes the output to the expected shape.
515
+
516
+ Returns
517
+ -------
518
+ List of Numpy arrays or numpy array
519
+ The list of images, or the image of fixed dimension.
520
+ """
521
+ # box = kwargs.get("box",[self.get_bottom(),self.get_top()])
522
+ if box is None:
523
+ box = self.get_box()
524
+ num_parameters = self.num_parameters
525
+ if degrees is None:
526
+ degrees = np.arange(self.max_degree +1)
527
+ num_degrees = len(degrees)
528
+ try:
529
+ int(resolution)
530
+ resolution = [resolution]*num_parameters
531
+ except:
532
+ pass
533
+
534
+ xx = [np.linspace(*np.asarray(box)[:,parameter], num=res) for parameter, res in zip(range(num_parameters), resolution)]
535
+ mesh = np.meshgrid(*xx)
536
+ coordinates = np.concatenate([stuff.flatten()[:,None] for stuff in mesh], axis=1)
537
+
538
+ # if degree < 0:
539
+ # image_vector = np.array(self.cmod.get_vectorization(bandwidth, p, normalize, Box[value_type](box), resolution[0], resolution[1]))
540
+ # else:
541
+ # image_vector = np.array([self.cmod.get_vectorization_in_dimension(degree, bandwidth, p,normalize,Box[value_type](box), resolution[0], resolution[1])])
542
+
543
+ concatenated_images = self._compute_pixels(coordinates, degrees=degrees, box=box, delta=bandwidth, p=p, normalize=normalize,n_jobs=n_jobs)
544
+ if flatten:
545
+ image_vector = concatenated_images.reshape((len(degrees),-1))
546
+ if plot:
547
+ from warnings import warn
548
+ warn("Unflatten to plot.")
549
+ return image_vector
550
+ else:
551
+ image_vector = concatenated_images.reshape((len(degrees),*resolution))
552
+ if plot:
553
+ assert num_parameters == 2
554
+ i=0
555
+ n_plots = len(image_vector)
556
+ scale:float = kwargs.get("size", 4.0)
557
+ fig, axs = plt.subplots(1,n_plots, figsize=(n_plots*scale,scale))
558
+ aspect = (box[1][0]-box[0][0]) / (box[1][1]-box[0][1])
559
+ extent = [box[0][0], box[1][0], box[0][1], box[1][1]]
560
+ for image, degree, i in zip(image_vector, degrees, range(num_degrees)):
561
+ ax = axs if n_plots <= 1 else axs[i]
562
+ temp = ax.imshow(image,origin="lower",extent=extent, aspect=aspect)
563
+ if (kwargs.get('colorbar') or kwargs.get('cb')):
564
+ plt.colorbar(temp, ax = ax)
565
+ if degree < 0 :
566
+ ax.set_title(rf"$H_{i}$ $2$-persistence image")
567
+ if degree >= 0:
568
+ ax.set_title(rf"$H_{degree}$ $2$-persistence image")
569
+ return image_vector
570
+
571
+
572
+ def euler_char(self, points:list|np.ndarray) -> np.ndarray:
573
+ """ Computes the Euler Characteristic of the filtered complex at given (multiparameter) time
574
+
575
+ Parameters
576
+ ----------
577
+ points: list[float] | list[list[float]] | np.ndarray
578
+ List of filtration values on which to compute the euler characteristic.
579
+ WARNING FIXME : the points have to have the same dimension as the simplextree.
580
+
581
+ Returns
582
+ -------
583
+ The list of euler characteristic values
584
+ """
585
+ if len(points) == 0:
586
+ return []
587
+ if type(points[0]) is float:
588
+ points = [points]
589
+ if type(points) is np.ndarray:
590
+ assert len(points.shape) in [1,2]
591
+ if len(points.shape) == 1:
592
+ points = [points]
593
+ cdef vector[Finitely_critical_multi_filtration] c_points = Finitely_critical_multi_filtration.from_python(points)
594
+ cdef Module c_mod = self.cmod
595
+ with nogil:
596
+ c_euler = c_mod.euler_curve(c_points)
597
+ euler = c_euler
598
+ return np.array(euler, dtype=int)
599
+
600
+ def module_approximation(
601
+ st:SimplexTreeMulti|None=None,
602
+ max_error:float|None = None,
603
+ box:list|np.ndarray|None = None,
604
+ threshold:bool = False,
605
+ complete:bool = True,
606
+ multithread:bool = False,
607
+ verbose:bool = False,
608
+ ignore_warning:bool = False,
609
+ nlines:int = 500,
610
+ max_dimension=np.inf,
611
+ boundary = None,
612
+ filtration = None,
613
+ return_timings:bool = False,
614
+ **kwargs
615
+ ):
616
+ """Computes an interval module approximation of a multiparameter filtration.
617
+
618
+ Parameters
619
+ ----------
620
+ st : n-filtered Simplextree, or None if boundary and filtration are provided.
621
+ Defines the n-filtration on which to compute the homology.
622
+ max_error: positive float
623
+ Trade-off between approximation and computational complexity.
624
+ Upper bound of the module approximation, in bottleneck distance,
625
+ for interval-decomposable modules.
626
+ nlines: int
627
+ Alternative to precision.
628
+ box : pair of list of floats
629
+ Defines a rectangle on which to compute the approximation.
630
+ Format : [x,y], where x,y defines the rectangle {z : x ≤ z ≤ y}
631
+ threshold: bool
632
+ When true, intersects the module support with the box.
633
+ verbose: bool
634
+ Prints C++ infos.
635
+ ignore_warning : bool
636
+ Unless set to true, prevents computing on more than 10k lines. Useful to prevent a segmentation fault due to "infinite" recursion.
637
+ return_timings : bool
638
+ If true, will return the time to compute instead (computed in python, using perf_counter_ns).
639
+ Returns
640
+ -------
641
+ PyModule
642
+ An interval decomposable module approximation of the module defined by the
643
+ homology of this multi-filtration.
644
+ """
645
+
646
+ if boundary is None or filtration is None:
647
+ boundary,filtration = simplex_tree2boundary_filtrations(st) # TODO : recomputed each time... maybe store this somewhere ?
648
+ if max_dimension < np.inf: # TODO : make it more efficient
649
+ nsplx = len(boundary)
650
+ for i in range(nsplx-1,-1,-1):
651
+ b = boundary[i]
652
+ dim=len(b) -1
653
+ if dim>max_dimension:
654
+ boundary.pop(i)
655
+ for f in filtration:
656
+ f.pop(i)
657
+ nfiltration = len(filtration)
658
+ if nfiltration <= 0:
659
+ return PyModule()
660
+ if nfiltration == 1 and not(st is None):
661
+ st = st.project_on_line(0)
662
+ return st.persistence()
663
+
664
+ if box is None and not(st is None):
665
+ m,M = st.filtration_bounds()
666
+ elif box is not None:
667
+ m,M = box
668
+ else:
669
+ m, M = np.min(filtration, axis=0), np.max(filtration, axis=0)
670
+ prod = 1
671
+ h = M[-1] - m[-1]
672
+ for i, [a,b] in enumerate(zip(m,M)):
673
+ if i == len(M)-1: continue
674
+ prod *= (b-a + h)
675
+
676
+ if max_error is None:
677
+ max_error:float = (prod/nlines)**(1/(nfiltration-1))
678
+
679
+ if box is None:
680
+ M = [np.max(f)+2*max_error for f in filtration]
681
+ m = [np.min(f)-2*max_error for f in filtration]
682
+ box = [m,M]
683
+
684
+ if ignore_warning and prod >= 20_000:
685
+ from warnings import warn
686
+ warn(f"Warning : the number of lines (around {np.round(prod)}) may be too high. Try to increase the precision parameter, or set `ignore_warning=True` to compute this module. Returning the trivial module.")
687
+ return PyModule()
688
+
689
+ approx_mod = PyModule()
690
+ cdef vector[Finitely_critical_multi_filtration] c_filtration = Finitely_critical_multi_filtration.from_python(filtration)
691
+ cdef boundary_matrix c_boundary = boundary
692
+ cdef value_type c_max_error = max_error
693
+ cdef bool c_threshold = threshold
694
+ cdef bool c_complete = complete
695
+ cdef bool c_multithread = multithread
696
+ cdef bool c_verbose = verbose
697
+ cdef Box[value_type] c_box = Box[value_type](box)
698
+ if return_timings:
699
+ from time import perf_counter_ns
700
+ t = perf_counter_ns()
701
+ with nogil:
702
+ c_mod = compute_vineyard_barcode_approximation(c_boundary,c_filtration,c_max_error, c_box, c_threshold, c_complete, c_multithread,c_verbose)
703
+ if return_timings:
704
+ t = perf_counter_ns() -t
705
+ t /= 10**9
706
+ return t
707
+ approx_mod.set(c_mod)
708
+ return approx_mod
709
+
710
+
711
+
712
+ def dump(self, path:str|None=None):
713
+ """
714
+ Dumps the module into a pickle-able format.
715
+
716
+ Parameters
717
+ ----------
718
+ path:str=None (optional) saves the pickled module in specified path
719
+
720
+ Returns
721
+ -------
722
+ list of list, encoding the module, which can be retrieved with the function `from_dump`.
723
+ """
724
+ ## TODO : optimize, but not really used.
725
+ out = [[] for _ in range(self.cmod.get_dimension()+1)]
726
+ out += [self.get_box()]
727
+
728
+ for summand in self.cmod:
729
+ out[summand.get_dimension()].append((
730
+ Finitely_critical_multi_filtration.to_python(summand.get_birth_list()),
731
+ Finitely_critical_multi_filtration.to_python(summand.get_death_list())
732
+ ))
733
+ if path is None:
734
+ return out
735
+ pk.dump(out, open(path, "wb"))
736
+ return out
737
+
738
+ cdef dump_summand(Summand& summand):
739
+ return (
740
+ np.asarray(Finitely_critical_multi_filtration.to_python(summand.get_birth_list())),
741
+ np.asarray(Finitely_critical_multi_filtration.to_python(summand.get_death_list())),
742
+ summand.get_dimension()
743
+ )
744
+
745
+ cdef Summand summand_from_dump(summand_dump):
746
+ cdef vector[Finitely_critical_multi_filtration] births = Finitely_critical_multi_filtration.from_python(summand_dump[0])
747
+ cdef vector[Finitely_critical_multi_filtration] deaths = Finitely_critical_multi_filtration.from_python(summand_dump[1])
748
+ cdef int dim = summand_dump[2]
749
+ return Summand(births,deaths,dim)
750
+
751
+ cdef dump_cmod(Module& mod):
752
+ cbox = mod.get_box()
753
+ cdef int dim = mod.get_dimension()
754
+ bottom_corner = cbox.get_bottom_corner().get_vector()
755
+ top_corner = cbox.get_upper_corner().get_vector()
756
+ box = np.asarray([bottom_corner,top_corner])
757
+ summands = tuple(dump_summand(summand) for summand in mod)
758
+ return box, summands
759
+
760
+ cdef Module cmod_from_dump(module_dump):
761
+ box = module_dump[0]
762
+ summands = module_dump[1]
763
+ cdef Module out_module = Module()
764
+ out_module.set_box(Box[value_type](box))
765
+ for i in range(len(summands)):
766
+ out_module.add_summand(summand_from_dump(summands[i]))
767
+ return out_module
768
+
769
+
770
+ def from_dump(dump)->PyModule:
771
+ """Retrieves a PyModule from a previous dump.
772
+
773
+ Parameters
774
+ ----------
775
+ dump: either the output of the dump function, or a file containing the output of a dump.
776
+ The dumped module to retrieve
777
+
778
+ Returns
779
+ -------
780
+ PyModule
781
+ The retrieved module.
782
+ """
783
+ # TODO : optimize...
784
+ mod = PyModule()
785
+ if type(dump) is str:
786
+ dump = pk.load(open(dump, "rb"))
787
+ cdef Module cmod = cmod_from_dump(dump)
788
+ mod.cmod = cmod
789
+ return mod
790
+
791
+
792
+
793
+ ################################################## MMA PLOTS
794
+
795
+
796
+
797
+
798
+ #################################################### DATASETS for test
799
+
800
+
801
+
802
+ def test_module(**kwargs):
803
+ """Generates a module from a noisy annulus.
804
+
805
+ Parameters
806
+ ----------
807
+ r1 : float.
808
+ Lower radius of the annulus.
809
+ r2 : float.
810
+ Upper radius of the annulus.
811
+ n1 : int
812
+ Number of points in the annulus.
813
+ n2 : int
814
+ Number of points in the square.
815
+ dim : int
816
+ Dimension of the annulus.
817
+ center: list or array
818
+ center of the annulus.
819
+
820
+ Returns
821
+ -------
822
+ PyModule
823
+ The associated module.
824
+
825
+ """
826
+ from multipers.synthetic_data import noisy_annulus
827
+ points = noisy_annulus(**kwargs)
828
+ points = np.unique(points, axis=0)
829
+ from sklearn.neighbors import KernelDensity
830
+ kde = KernelDensity(bandwidth = 1).fit(points)
831
+ st = gd.RipsComplex(points = points).create_simplex_tree()
832
+ st = gd.SimplexTreeMulti(st, parameters = 2)
833
+ st.collapse_edges(full=True)
834
+ f2 = - kde.score_samples(points)
835
+ st.fill_lowerstar(f2,parameter=1)
836
+ mod = st.persistence(**kwargs)
837
+ return mod
838
+
839
+ def nlines_precision_box(nlines, basepoint, scale, square = False):
840
+ from random import choice, shuffle
841
+ from sympy.ntheory import factorint
842
+ h = scale
843
+ dim = len(basepoint)
844
+ basepoint = np.array(basepoint)
845
+ if square:
846
+ # here we want n^dim-1 lines (n = nlines)
847
+ n=nlines
848
+ basepoint = np.array(basepoint)
849
+ deathpoint = basepoint.copy()
850
+ deathpoint+=n*h + - h/2
851
+ deathpoint[-1] = basepoint[-1]+h/2
852
+ return [basepoint,deathpoint]
853
+ factors = factorint(nlines)
854
+ prime_list=[]
855
+ for prime in factors:
856
+ for i in range(factors[prime]):
857
+ prime_list.append(prime)
858
+ while len(prime_list)<dim-1:
859
+ prime_list.append(1)
860
+ shuffle(prime_list)
861
+ while len(prime_list)>dim-1:
862
+ prime_list[choice(range(dim-1))] *= prime_list.pop()
863
+ deathpoint = basepoint.copy()
864
+ for i in range(dim-1):
865
+ deathpoint[i] = basepoint[i] + prime_list[i] * scale - scale/2
866
+ return [basepoint,deathpoint]
867
+
868
+ ##################################################### MMA CONVERSIONS
869
+
870
+
871
+
872
+
873
+
874
+ cdef extern from "multiparameter_module_approximation/format_python-cpp.h" namespace "Gudhi::multiparameter::mma":
875
+ #list_simplicies_to_sparse_boundary_matrix
876
+ #vector[vector[unsigned int]] build_sparse_boundary_matrix_from_simplex_list(vector[vector[unsigned int]] list_simplices)
877
+ #list_simplices_ls_filtration_to_sparse_boundary_filtration
878
+ #pair[vector[vector[unsigned int]], vector[vector[double]]] build_boundary_matrix_from_simplex_list(vector[vector[unsigned int]] list_simplices, vector[vector[double]] filtrations, vector[unsigned int] filters_to_permute)
879
+ # pair[vector[vector[unsigned int]], vector[double]] simplextree_to_boundary_filtration(vector[boundary_type] &simplexList, filtration_type &filtration)
880
+ pair[boundary_matrix, vector[Finitely_critical_multi_filtration]] simplextree_to_boundary_filtration(uintptr_t)
881
+
882
+ # pair[vector[vector[unsigned int]], vector[double]] __old__simplextree_to_boundary_filtration(vector[boundary_type]&, filtration_type&)
883
+
884
+ # string __old__simplextree2rivet(const uintptr_t, const vector[filtration_type]&)
885
+ # void simplextree2rivet(const string&, const uintptr_t, const vector[filtration_type]&)
886
+
887
+
888
+
889
+ def simplex_tree2boundary_filtrations(simplextree:SimplexTreeMulti | SimplexTree):
890
+ """Computes a (sparse) boundary matrix, with associated filtration. Can be used as an input of approx afterwards.
891
+
892
+ Parameters
893
+ ----------
894
+ simplextree: Gudhi or mma simplextree
895
+ The simplextree defining the filtration to convert to boundary-filtration.
896
+
897
+ Returns
898
+ -------
899
+ B:List of lists of ints
900
+ The boundary matrix.
901
+ F: List of 1D filtration
902
+ The filtrations aligned with B; the i-th simplex of this simplextree has boundary B[i] and filtration(s) F[i].
903
+
904
+ """
905
+ cdef intptr_t cptr
906
+ if isinstance(simplextree, SimplexTreeMulti):
907
+ cptr = simplextree.thisptr
908
+ elif isinstance(simplextree, SimplexTree):
909
+ temp_st = gd.SimplexTreeMulti(simplextree, parameters=1)
910
+ cptr = temp_st.thisptr
911
+ else:
912
+ raise TypeError("Has to be a simplextree")
913
+ cdef pair[boundary_matrix, vector[Finitely_critical_multi_filtration]] cboundary_filtration = simplextree_to_boundary_filtration(cptr)
914
+ boundary = cboundary_filtration.first
915
+ multi_filtrations = np.array(Finitely_critical_multi_filtration.to_python(cboundary_filtration.second))
916
+ return boundary, multi_filtrations
917
+
918
+
919
+ # def simplextree_to_sparse_boundary(st:SimplexTree):
920
+ # return build_sparse_boundary_matrix_from_simplex_list([simplex[0] for simplex in st.get_simplices()])
921
+
922
+
923
+
924
+
925
+
926
+
927
+
928
+ ############################################# MMA - Matching distances
929
+
930
+
931
+