multipers 2.0.0__cp310-cp310-macosx_13_0_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 (78) hide show
  1. multipers/.dylibs/libc++.1.0.dylib +0 -0
  2. multipers/.dylibs/libtbb.12.12.dylib +0 -0
  3. multipers/.dylibs/libtbbmalloc.2.12.dylib +0 -0
  4. multipers/__init__.py +11 -0
  5. multipers/_signed_measure_meta.py +268 -0
  6. multipers/_slicer_meta.py +171 -0
  7. multipers/data/MOL2.py +350 -0
  8. multipers/data/UCR.py +18 -0
  9. multipers/data/__init__.py +1 -0
  10. multipers/data/graphs.py +466 -0
  11. multipers/data/immuno_regions.py +27 -0
  12. multipers/data/minimal_presentation_to_st_bf.py +0 -0
  13. multipers/data/pytorch2simplextree.py +91 -0
  14. multipers/data/shape3d.py +101 -0
  15. multipers/data/synthetic.py +68 -0
  16. multipers/distances.py +198 -0
  17. multipers/euler_characteristic.pyx +132 -0
  18. multipers/filtration_conversions.pxd +229 -0
  19. multipers/filtrations.pxd +225 -0
  20. multipers/function_rips.cpython-310-darwin.so +0 -0
  21. multipers/function_rips.pyx +105 -0
  22. multipers/grids.cpython-310-darwin.so +0 -0
  23. multipers/grids.pyx +281 -0
  24. multipers/hilbert_function.pyi +46 -0
  25. multipers/hilbert_function.pyx +153 -0
  26. multipers/io.cpython-310-darwin.so +0 -0
  27. multipers/io.pyx +571 -0
  28. multipers/ml/__init__.py +0 -0
  29. multipers/ml/accuracies.py +90 -0
  30. multipers/ml/convolutions.py +532 -0
  31. multipers/ml/invariants_with_persistable.py +79 -0
  32. multipers/ml/kernels.py +176 -0
  33. multipers/ml/mma.py +659 -0
  34. multipers/ml/one.py +472 -0
  35. multipers/ml/point_clouds.py +238 -0
  36. multipers/ml/signed_betti.py +50 -0
  37. multipers/ml/signed_measures.py +1542 -0
  38. multipers/ml/sliced_wasserstein.py +461 -0
  39. multipers/ml/tools.py +113 -0
  40. multipers/mma_structures.cpython-310-darwin.so +0 -0
  41. multipers/mma_structures.pxd +127 -0
  42. multipers/mma_structures.pyx +2433 -0
  43. multipers/multiparameter_edge_collapse.py +41 -0
  44. multipers/multiparameter_module_approximation.cpython-310-darwin.so +0 -0
  45. multipers/multiparameter_module_approximation.pyx +211 -0
  46. multipers/pickle.py +53 -0
  47. multipers/plots.py +326 -0
  48. multipers/point_measure_integration.cpython-310-darwin.so +0 -0
  49. multipers/point_measure_integration.pyx +139 -0
  50. multipers/rank_invariant.cpython-310-darwin.so +0 -0
  51. multipers/rank_invariant.pyx +229 -0
  52. multipers/simplex_tree_multi.cpython-310-darwin.so +0 -0
  53. multipers/simplex_tree_multi.pxd +129 -0
  54. multipers/simplex_tree_multi.pyi +715 -0
  55. multipers/simplex_tree_multi.pyx +4655 -0
  56. multipers/slicer.cpython-310-darwin.so +0 -0
  57. multipers/slicer.pxd +781 -0
  58. multipers/slicer.pyx +3393 -0
  59. multipers/tensor.pxd +13 -0
  60. multipers/test.pyx +44 -0
  61. multipers/tests/__init__.py +40 -0
  62. multipers/tests/old_test_rank_invariant.py +91 -0
  63. multipers/tests/test_diff_helper.py +74 -0
  64. multipers/tests/test_hilbert_function.py +82 -0
  65. multipers/tests/test_mma.py +51 -0
  66. multipers/tests/test_point_clouds.py +59 -0
  67. multipers/tests/test_python-cpp_conversion.py +82 -0
  68. multipers/tests/test_signed_betti.py +181 -0
  69. multipers/tests/test_simplextreemulti.py +98 -0
  70. multipers/tests/test_slicer.py +63 -0
  71. multipers/torch/__init__.py +1 -0
  72. multipers/torch/diff_grids.py +217 -0
  73. multipers/torch/rips_density.py +257 -0
  74. multipers-2.0.0.dist-info/LICENSE +21 -0
  75. multipers-2.0.0.dist-info/METADATA +29 -0
  76. multipers-2.0.0.dist-info/RECORD +78 -0
  77. multipers-2.0.0.dist-info/WHEEL +5 -0
  78. multipers-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2433 @@
1
+ # WARNING: Do not edit this file directly.
2
+ # It is automatically generated from 'multipers/mma_structures.pyx.tp'.
3
+ # Changes must be made there.
4
+
5
+
6
+
7
+
8
+
9
+
10
+ """!
11
+ @package mma
12
+ @brief Files containing the C++ cythonized functions.
13
+ @author David Loiseaux
14
+ @copyright Copyright (c) 2022 Inria.
15
+ """
16
+
17
+ # distutils: language = c++
18
+
19
+ ###########################################################################
20
+ ## PYTHON LIBRARIES
21
+ import gudhi as gd
22
+ import numpy as np
23
+ from typing import List, Union
24
+ import pickle as pk
25
+
26
+ ###########################################################################
27
+ ## CPP CLASSES
28
+ from libc.stdint cimport intptr_t
29
+ from libc.stdint cimport uintptr_t
30
+
31
+ ###########################################################################
32
+ ## CYTHON TYPES
33
+ from libcpp.vector cimport vector
34
+ from libcpp.utility cimport pair
35
+ #from libcpp.list cimport list as clist
36
+ from libcpp cimport bool
37
+ from libcpp cimport int
38
+ from typing import Iterable
39
+ from cython.operator cimport dereference
40
+ from libcpp.utility cimport move
41
+ #########################################################################
42
+ ## Multipersistence Module Approximation Classes
43
+ from multipers.mma_structures cimport *
44
+ from multipers.filtration_conversions cimport *
45
+ cimport numpy as cnp
46
+
47
+
48
+ #########################################################################
49
+ ## Small hack for typing
50
+ from gudhi import SimplexTree
51
+ from multipers.simplex_tree_multi import SimplexTreeMulti
52
+ from joblib import Parallel, delayed
53
+
54
+
55
+ cdef class PySummand_i32:
56
+ """
57
+ Stores a Summand of a PyModule
58
+ """
59
+ cdef Summand[int32_t] sum
60
+
61
+ def get_birth_list(self):
62
+ cdef vector[Finitely_critical_multi_filtration[int32_t]] v = self.sum.get_birth_list()
63
+ return _vff21cview_i32(v, copy = True)
64
+
65
+ def get_death_list(self):
66
+ cdef vector[Finitely_critical_multi_filtration[int32_t]] v = self.sum.get_death_list()
67
+ return _vff21cview_i32(v, copy = True)
68
+ @property
69
+ def degree(self)->int:
70
+ return self.sum.get_dimension()
71
+
72
+ cdef set(self, Summand[int32_t]& summand):
73
+ self.sum = summand
74
+ return self
75
+ def get_bounds(self):
76
+ cdef pair[Finitely_critical_multi_filtration[int32_t],Finitely_critical_multi_filtration[int32_t]] cbounds
77
+ with nogil:
78
+ cbounds = self.sum.get_bounds().get_pair()
79
+ return _ff21cview_i32(&cbounds.first).copy(), _ff21cview_i32(&cbounds.second).copy()
80
+ @property
81
+ def dtype(self):
82
+ return np.int32
83
+
84
+ cdef inline get_summand_filtration_values_i32(Summand[int32_t] summand):
85
+ """
86
+ Returns a list (over parameter) of the filtrations values of this parameter.
87
+ """
88
+ cdef vector[Finitely_critical_multi_filtration[int32_t]] v = summand.get_birth_list()
89
+ births = _vff21cview_i32(v, copy=True)
90
+ v = summand.get_death_list()
91
+ deaths = _vff21cview_i32(v, copy=True)
92
+ pts = np.concatenate([births,deaths],axis=0)
93
+ num_parameters = pts.shape[1]
94
+ out = [np.unique(pts[:,parameter]) for parameter in range(num_parameters)]
95
+ out = [f[:-1] if f[-1] == np.inf else f for f in out]
96
+ out = [f[1:] if f[0] == -np.inf else f for f in out]
97
+ return out
98
+
99
+ cdef class PyBox_i32:
100
+ cdef Box[int32_t] box
101
+ def __cinit__(self, vector[int32_t]& bottomCorner, vector[int32_t]& topCorner):
102
+ self.box = Box[int32_t](bottomCorner, topCorner)
103
+ @property
104
+ def num_parameters(self):
105
+ cdef size_t dim = self.box.get_bottom_corner().num_parameters()
106
+ if dim == self.box.get_upper_corner().num_parameters(): return dim
107
+ else: print("Bad box definition.")
108
+ def contains(self, x):
109
+ return self.box.contains(x)
110
+ cdef set(self, Box[int32_t]& b):
111
+ self.box = b
112
+ return self
113
+
114
+ def get(self):
115
+ return [<vector[int32_t]>self.box.get_bottom_corner(), <vector[int32_t]>self.box.get_upper_corner()]
116
+ def to_multipers(self):
117
+ #assert (self.get_dimension() == 2) "Multipers only works in dimension 2 !"
118
+ return np.array(self.get()).flatten(order = 'F')
119
+ @property
120
+ def dtype(self):
121
+ return np.int32
122
+
123
+
124
+
125
+ cdef class PyModule_i32:
126
+ """
127
+ Stores a representation of a n-persistence module.
128
+ """
129
+ cdef Module[int32_t] cmod
130
+
131
+ @property
132
+ def dtype(self):
133
+ return np.int32
134
+
135
+ cdef set(self, Module[int32_t] m):
136
+ self.cmod = m
137
+ def merge(self, PyModule_i32 other):
138
+ """
139
+ Merges two modules into one
140
+ """
141
+ cdef Module[int32_t] c_other = other.cmod
142
+ with nogil:
143
+ for summand in c_other:
144
+ self.cmod.add_summand(summand)
145
+ return self
146
+
147
+ def _set_from_ptr(self, intptr_t module_ptr):
148
+ """
149
+ Copy module from a memory pointer. Unsafe.
150
+ """
151
+ self.cmod = move(dereference(<Module[int32_t]*>(module_ptr)))
152
+ def set_box(self, PyBox_i32 pybox):
153
+ cdef Box[int32_t] cbox = pybox.box
154
+ with nogil:
155
+ self.cmod.set_box(cbox)
156
+ return self
157
+ def get_module_of_degree(self, int degree)->PyModule_i32: # TODO : in c++ ?
158
+ """
159
+ Returns a copy of a module of fixed degree.
160
+ """
161
+ pmodule = PyModule_i32()
162
+ cdef Box[int32_t] c_box = self.cmod.get_box()
163
+ pmodule.cmod.set_box(c_box)
164
+ with nogil:
165
+ for summand in self.cmod:
166
+ if summand.get_dimension() == degree:
167
+ pmodule.cmod.add_summand(summand)
168
+ return pmodule
169
+ def get_module_of_degrees(self, degrees:Iterable[int])->PyModule_i32: # TODO : in c++ ?
170
+ """
171
+ Returns a copy of the summands of degrees in `degrees`
172
+ """
173
+ pmodule = PyModule_i32()
174
+ cdef Box[int32_t] c_box = self.cmod.get_box()
175
+ pmodule.cmod.set_box(c_box)
176
+ cdef vector[int] cdegrees = degrees
177
+ with nogil:
178
+ for summand in self.cmod:
179
+ for d in cdegrees:
180
+ if d == summand.get_dimension():
181
+ pmodule.cmod.add_summand(summand)
182
+ return pmodule
183
+ def __len__(self)->int:
184
+ return self.cmod.size()
185
+ def get_bottom(self)->np.ndarray:
186
+ """
187
+ Bottom of the box of the module
188
+ """
189
+ return np.asarray(<vector[int32_t]>(self.cmod.get_box().get_bottom_corner()))
190
+ def get_top(self)->np.ndarray:
191
+ """
192
+ Top of the box of the module
193
+ """
194
+ return np.asarray(<vector[int32_t]>(self.cmod.get_box().get_upper_corner()))
195
+ def get_box(self)->np.ndarray:
196
+ """
197
+ Returns the current bounding box of the module.
198
+ """
199
+ return np.asarray([self.get_bottom(), self.get_top()])
200
+ @property
201
+ def max_degree(self)->int:
202
+ """
203
+ Returns the maximum degree of the module.
204
+ """
205
+ return self.cmod.get_dimension()
206
+ @property
207
+ def num_parameters(self)->int:
208
+ cdef size_t dim = self.cmod.get_box().get_bottom_corner().num_parameters()
209
+ assert dim == self.cmod.get_box().get_upper_corner().num_parameters(), "Bad box definition, cannot infer num_parameters."
210
+ return dim
211
+ def dump(self, path:str|None=None):
212
+ """
213
+ Dumps the module into a pickle-able format.
214
+
215
+ Parameters
216
+ ----------
217
+
218
+ path:str=None (optional) saves the pickled module in specified path
219
+
220
+ Returns
221
+ -------
222
+
223
+ list of list, encoding the module, which can be retrieved with the function `from_dump`.
224
+ """
225
+ ## TODO : optimize, but not really used.
226
+ return dump_cmod_i32(self.cmod)
227
+ def __getstate__(self):
228
+ return self.dump()
229
+ def __setstate__(self,dump):
230
+ cdef Module[int32_t] cmod = cmod_from_dump_i32(dump)
231
+ self.cmod = cmod
232
+ return
233
+ def __getitem__(self, int i) -> PySummand_i32:
234
+ if i == slice(None):
235
+ return self
236
+ summand = PySummand_i32()
237
+ summand.set(self.cmod.at(i % self.cmod.size()))
238
+ return summand
239
+ def __iter__(self):
240
+ cdef int num_summands = self.cmod.size()
241
+ for i in range(num_summands):
242
+ summand = PySummand_i32()
243
+ summand.set(self.cmod.at(i)) ## hmm copy. maybe do summand from ptr
244
+ yield summand
245
+
246
+ def get_bounds(self):
247
+ """
248
+ Computes bounds from the summands' bounds.
249
+ Useful to change this' box.
250
+ """
251
+ cdef pair[Finitely_critical_multi_filtration[int32_t],Finitely_critical_multi_filtration[int32_t]] cbounds
252
+ with nogil:
253
+ cbounds = self.cmod.get_bounds().get_pair()
254
+ return _ff21cview_i32(&cbounds.first).copy(), _ff21cview_i32(&cbounds.second).copy()
255
+ def rescale(self,rescale_factors, int degree=-1):
256
+ """
257
+ Rescales the fitlration values of the summands by this rescaling vector.
258
+ """
259
+ cdef vector[int32_t] crescale_factors = rescale_factors
260
+ with nogil:
261
+ self.cmod.rescale(crescale_factors,degree)
262
+ def translate(self,translation, int degree=-1):
263
+ """
264
+ Translates the module in the filtration space by this vector.
265
+ """
266
+ cdef vector[int32_t] ctranslation = translation
267
+ with nogil:
268
+ self.cmod.translate(ctranslation,degree)
269
+
270
+ def get_filtration_values(self, bool unique=True):
271
+ """
272
+ Retrieves all filtration values of the summands of the module.
273
+
274
+ Output format
275
+ -------------
276
+
277
+ list of filtration values for parameter.
278
+ """
279
+ if len(self) ==0:
280
+ return np.empty((self.num_parameters,0))
281
+ values = tuple(tuple(stuff) if len(stuff:=get_summand_filtration_values_i32(summand)) == self.num_parameters else list(stuff) + [[]]*(self.num_parameters - len(stuff)) for summand in self.cmod)
282
+ try:
283
+ values = tuple(np.concatenate([
284
+ f[parameter]
285
+ for f in values
286
+ ], axis=0) for parameter in range(self.num_parameters)
287
+ )
288
+ except:
289
+ return values
290
+ if unique:
291
+ return [np.unique(f) for f in values]
292
+ return values
293
+
294
+ def plot(self, int degree=-1,**kwargs)->None:
295
+ """Shows the module on a plot. Each color corresponds to an apprimation summand of the module, and its shape corresponds to its support.
296
+ Only works with 2-parameter modules.
297
+
298
+ Parameters
299
+ ----------
300
+ degree = -1 : integer
301
+ If positive returns only the image of dimension `dimension`.
302
+ 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.
303
+ If non-None, will plot the module on this specific rectangle.
304
+ min_persistence =0 : float
305
+ Only plots the summand with a persistence above this threshold.
306
+ separated=False : bool
307
+ If true, plot each summand in a different plot.
308
+ alpha=1 : float
309
+ Transparancy parameter
310
+ save = False : string
311
+ if nontrivial, will save the figure at this path
312
+
313
+
314
+ Returns
315
+ -------
316
+ The figure of the plot.
317
+ """
318
+ from multipers.plots import plot2d_PyModule
319
+ import matplotlib.pyplot as plt
320
+ if (kwargs.get('box')):
321
+ box = kwargs.pop('box')
322
+ else:
323
+ box = [self.get_bottom(), self.get_top()]
324
+ if (len(box[0]) != 2):
325
+ print("Filtration size :", len(box[0]), " != 2")
326
+ return
327
+ num = 0
328
+ if(degree < 0):
329
+ ndim = self.cmod.get_dimension()+1
330
+ scale = kwargs.pop("scale", 4)
331
+ fig, axes = plt.subplots(1, ndim, figsize=(ndim*scale,scale))
332
+ for degree in range(ndim):
333
+ plt.sca(axes[degree]) if ndim > 1 else plt.sca(axes)
334
+ self.plot(degree,box=box,**kwargs)
335
+ return
336
+ corners = self.cmod.get_corners_of_dimension(degree)
337
+ plot2d_PyModule(corners, box=box, dimension=degree, **kwargs)
338
+ return
339
+ def degree_splits(self):
340
+ return np.asarray(self.cmod.get_degree_splits(), dtype=np.int64)
341
+
342
+
343
+ cdef dump_summand_i32(Summand[int32_t]& summand):
344
+ cdef vector[Finitely_critical_multi_filtration[int32_t]] births = summand.get_birth_list()
345
+ cdef vector[Finitely_critical_multi_filtration[int32_t]] deaths = summand.get_death_list()
346
+ return (
347
+ _vff21cview_i32(births, copy=True), ## copy as local variables
348
+ _vff21cview_i32(deaths, copy=True),
349
+ summand.get_dimension(),
350
+ )
351
+
352
+ cdef Summand[int32_t] summand_from_dump_i32(summand_dump):
353
+ cdef vector[Finitely_critical_multi_filtration[int32_t]] births = _py2v1c_i32(summand_dump[0])
354
+ cdef vector[Finitely_critical_multi_filtration[int32_t]] deaths = _py2v1c_i32(summand_dump[1])
355
+ cdef int dim = summand_dump[2]
356
+ return Summand[int32_t](births,deaths,dim)
357
+
358
+ cdef dump_cmod_i32(Module[int32_t]& mod):
359
+ cdef Box[int32_t] cbox = mod.get_box()
360
+ cdef int dim = mod.get_dimension()
361
+ cdef cnp.ndarray[int32_t, ndim=1] bottom_corner = _ff21cview_i32(&cbox.get_bottom_corner())
362
+ cdef cnp.ndarray[int32_t, ndim=1] top_corner = _ff21cview_i32(&cbox.get_upper_corner())
363
+ box = np.asarray([bottom_corner, top_corner])
364
+ summands = tuple(dump_summand_i32(summand) for summand in mod)
365
+ return box, summands
366
+
367
+ cdef Module[int32_t] cmod_from_dump_i32(module_dump):
368
+ box = module_dump[0]
369
+ summands = module_dump[1]
370
+ cdef Module[int32_t] out_module = Module[int32_t]()
371
+ out_module.set_box(Box[int32_t](box))
372
+ for i in range(len(summands)):
373
+ out_module.add_summand(summand_from_dump_i32(summands[i]))
374
+ return out_module
375
+
376
+
377
+ def from_dump_i32(dump)->PyModule_i32:
378
+ """Retrieves a PyModule from a previous dump.
379
+
380
+ Parameters
381
+ ----------
382
+
383
+ dump: either the output of the dump function, or a file containing the output of a dump.
384
+ The dumped module to retrieve
385
+
386
+ Returns
387
+ -------
388
+
389
+ PyModule
390
+ The retrieved module.
391
+ """
392
+ # TODO : optimize...
393
+ mod = PyModule_i32()
394
+ if type(dump) is str:
395
+ dump = pk.load(open(dump, "rb"))
396
+ cdef Module[int32_t] cmod = cmod_from_dump_i32(dump)
397
+ mod.cmod = cmod
398
+ return mod
399
+
400
+ cdef class PySummand_i64:
401
+ """
402
+ Stores a Summand of a PyModule
403
+ """
404
+ cdef Summand[int64_t] sum
405
+
406
+ def get_birth_list(self):
407
+ cdef vector[Finitely_critical_multi_filtration[int64_t]] v = self.sum.get_birth_list()
408
+ return _vff21cview_i64(v, copy = True)
409
+
410
+ def get_death_list(self):
411
+ cdef vector[Finitely_critical_multi_filtration[int64_t]] v = self.sum.get_death_list()
412
+ return _vff21cview_i64(v, copy = True)
413
+ @property
414
+ def degree(self)->int:
415
+ return self.sum.get_dimension()
416
+
417
+ cdef set(self, Summand[int64_t]& summand):
418
+ self.sum = summand
419
+ return self
420
+ def get_bounds(self):
421
+ cdef pair[Finitely_critical_multi_filtration[int64_t],Finitely_critical_multi_filtration[int64_t]] cbounds
422
+ with nogil:
423
+ cbounds = self.sum.get_bounds().get_pair()
424
+ return _ff21cview_i64(&cbounds.first).copy(), _ff21cview_i64(&cbounds.second).copy()
425
+ @property
426
+ def dtype(self):
427
+ return np.int64
428
+
429
+ cdef inline get_summand_filtration_values_i64(Summand[int64_t] summand):
430
+ """
431
+ Returns a list (over parameter) of the filtrations values of this parameter.
432
+ """
433
+ cdef vector[Finitely_critical_multi_filtration[int64_t]] v = summand.get_birth_list()
434
+ births = _vff21cview_i64(v, copy=True)
435
+ v = summand.get_death_list()
436
+ deaths = _vff21cview_i64(v, copy=True)
437
+ pts = np.concatenate([births,deaths],axis=0)
438
+ num_parameters = pts.shape[1]
439
+ out = [np.unique(pts[:,parameter]) for parameter in range(num_parameters)]
440
+ out = [f[:-1] if f[-1] == np.inf else f for f in out]
441
+ out = [f[1:] if f[0] == -np.inf else f for f in out]
442
+ return out
443
+
444
+ cdef class PyBox_i64:
445
+ cdef Box[int64_t] box
446
+ def __cinit__(self, vector[int64_t]& bottomCorner, vector[int64_t]& topCorner):
447
+ self.box = Box[int64_t](bottomCorner, topCorner)
448
+ @property
449
+ def num_parameters(self):
450
+ cdef size_t dim = self.box.get_bottom_corner().num_parameters()
451
+ if dim == self.box.get_upper_corner().num_parameters(): return dim
452
+ else: print("Bad box definition.")
453
+ def contains(self, x):
454
+ return self.box.contains(x)
455
+ cdef set(self, Box[int64_t]& b):
456
+ self.box = b
457
+ return self
458
+
459
+ def get(self):
460
+ return [<vector[int64_t]>self.box.get_bottom_corner(), <vector[int64_t]>self.box.get_upper_corner()]
461
+ def to_multipers(self):
462
+ #assert (self.get_dimension() == 2) "Multipers only works in dimension 2 !"
463
+ return np.array(self.get()).flatten(order = 'F')
464
+ @property
465
+ def dtype(self):
466
+ return np.int64
467
+
468
+
469
+
470
+ cdef class PyModule_i64:
471
+ """
472
+ Stores a representation of a n-persistence module.
473
+ """
474
+ cdef Module[int64_t] cmod
475
+
476
+ @property
477
+ def dtype(self):
478
+ return np.int64
479
+
480
+ cdef set(self, Module[int64_t] m):
481
+ self.cmod = m
482
+ def merge(self, PyModule_i64 other):
483
+ """
484
+ Merges two modules into one
485
+ """
486
+ cdef Module[int64_t] c_other = other.cmod
487
+ with nogil:
488
+ for summand in c_other:
489
+ self.cmod.add_summand(summand)
490
+ return self
491
+
492
+ def _set_from_ptr(self, intptr_t module_ptr):
493
+ """
494
+ Copy module from a memory pointer. Unsafe.
495
+ """
496
+ self.cmod = move(dereference(<Module[int64_t]*>(module_ptr)))
497
+ def set_box(self, PyBox_i64 pybox):
498
+ cdef Box[int64_t] cbox = pybox.box
499
+ with nogil:
500
+ self.cmod.set_box(cbox)
501
+ return self
502
+ def get_module_of_degree(self, int degree)->PyModule_i64: # TODO : in c++ ?
503
+ """
504
+ Returns a copy of a module of fixed degree.
505
+ """
506
+ pmodule = PyModule_i64()
507
+ cdef Box[int64_t] c_box = self.cmod.get_box()
508
+ pmodule.cmod.set_box(c_box)
509
+ with nogil:
510
+ for summand in self.cmod:
511
+ if summand.get_dimension() == degree:
512
+ pmodule.cmod.add_summand(summand)
513
+ return pmodule
514
+ def get_module_of_degrees(self, degrees:Iterable[int])->PyModule_i64: # TODO : in c++ ?
515
+ """
516
+ Returns a copy of the summands of degrees in `degrees`
517
+ """
518
+ pmodule = PyModule_i64()
519
+ cdef Box[int64_t] c_box = self.cmod.get_box()
520
+ pmodule.cmod.set_box(c_box)
521
+ cdef vector[int] cdegrees = degrees
522
+ with nogil:
523
+ for summand in self.cmod:
524
+ for d in cdegrees:
525
+ if d == summand.get_dimension():
526
+ pmodule.cmod.add_summand(summand)
527
+ return pmodule
528
+ def __len__(self)->int:
529
+ return self.cmod.size()
530
+ def get_bottom(self)->np.ndarray:
531
+ """
532
+ Bottom of the box of the module
533
+ """
534
+ return np.asarray(<vector[int64_t]>(self.cmod.get_box().get_bottom_corner()))
535
+ def get_top(self)->np.ndarray:
536
+ """
537
+ Top of the box of the module
538
+ """
539
+ return np.asarray(<vector[int64_t]>(self.cmod.get_box().get_upper_corner()))
540
+ def get_box(self)->np.ndarray:
541
+ """
542
+ Returns the current bounding box of the module.
543
+ """
544
+ return np.asarray([self.get_bottom(), self.get_top()])
545
+ @property
546
+ def max_degree(self)->int:
547
+ """
548
+ Returns the maximum degree of the module.
549
+ """
550
+ return self.cmod.get_dimension()
551
+ @property
552
+ def num_parameters(self)->int:
553
+ cdef size_t dim = self.cmod.get_box().get_bottom_corner().num_parameters()
554
+ assert dim == self.cmod.get_box().get_upper_corner().num_parameters(), "Bad box definition, cannot infer num_parameters."
555
+ return dim
556
+ def dump(self, path:str|None=None):
557
+ """
558
+ Dumps the module into a pickle-able format.
559
+
560
+ Parameters
561
+ ----------
562
+
563
+ path:str=None (optional) saves the pickled module in specified path
564
+
565
+ Returns
566
+ -------
567
+
568
+ list of list, encoding the module, which can be retrieved with the function `from_dump`.
569
+ """
570
+ ## TODO : optimize, but not really used.
571
+ return dump_cmod_i64(self.cmod)
572
+ def __getstate__(self):
573
+ return self.dump()
574
+ def __setstate__(self,dump):
575
+ cdef Module[int64_t] cmod = cmod_from_dump_i64(dump)
576
+ self.cmod = cmod
577
+ return
578
+ def __getitem__(self, int i) -> PySummand_i64:
579
+ if i == slice(None):
580
+ return self
581
+ summand = PySummand_i64()
582
+ summand.set(self.cmod.at(i % self.cmod.size()))
583
+ return summand
584
+ def __iter__(self):
585
+ cdef int num_summands = self.cmod.size()
586
+ for i in range(num_summands):
587
+ summand = PySummand_i64()
588
+ summand.set(self.cmod.at(i)) ## hmm copy. maybe do summand from ptr
589
+ yield summand
590
+
591
+ def get_bounds(self):
592
+ """
593
+ Computes bounds from the summands' bounds.
594
+ Useful to change this' box.
595
+ """
596
+ cdef pair[Finitely_critical_multi_filtration[int64_t],Finitely_critical_multi_filtration[int64_t]] cbounds
597
+ with nogil:
598
+ cbounds = self.cmod.get_bounds().get_pair()
599
+ return _ff21cview_i64(&cbounds.first).copy(), _ff21cview_i64(&cbounds.second).copy()
600
+ def rescale(self,rescale_factors, int degree=-1):
601
+ """
602
+ Rescales the fitlration values of the summands by this rescaling vector.
603
+ """
604
+ cdef vector[int64_t] crescale_factors = rescale_factors
605
+ with nogil:
606
+ self.cmod.rescale(crescale_factors,degree)
607
+ def translate(self,translation, int degree=-1):
608
+ """
609
+ Translates the module in the filtration space by this vector.
610
+ """
611
+ cdef vector[int64_t] ctranslation = translation
612
+ with nogil:
613
+ self.cmod.translate(ctranslation,degree)
614
+
615
+ def get_filtration_values(self, bool unique=True):
616
+ """
617
+ Retrieves all filtration values of the summands of the module.
618
+
619
+ Output format
620
+ -------------
621
+
622
+ list of filtration values for parameter.
623
+ """
624
+ if len(self) ==0:
625
+ return np.empty((self.num_parameters,0))
626
+ values = tuple(tuple(stuff) if len(stuff:=get_summand_filtration_values_i64(summand)) == self.num_parameters else list(stuff) + [[]]*(self.num_parameters - len(stuff)) for summand in self.cmod)
627
+ try:
628
+ values = tuple(np.concatenate([
629
+ f[parameter]
630
+ for f in values
631
+ ], axis=0) for parameter in range(self.num_parameters)
632
+ )
633
+ except:
634
+ return values
635
+ if unique:
636
+ return [np.unique(f) for f in values]
637
+ return values
638
+
639
+ def plot(self, int degree=-1,**kwargs)->None:
640
+ """Shows the module on a plot. Each color corresponds to an apprimation summand of the module, and its shape corresponds to its support.
641
+ Only works with 2-parameter modules.
642
+
643
+ Parameters
644
+ ----------
645
+ degree = -1 : integer
646
+ If positive returns only the image of dimension `dimension`.
647
+ 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.
648
+ If non-None, will plot the module on this specific rectangle.
649
+ min_persistence =0 : float
650
+ Only plots the summand with a persistence above this threshold.
651
+ separated=False : bool
652
+ If true, plot each summand in a different plot.
653
+ alpha=1 : float
654
+ Transparancy parameter
655
+ save = False : string
656
+ if nontrivial, will save the figure at this path
657
+
658
+
659
+ Returns
660
+ -------
661
+ The figure of the plot.
662
+ """
663
+ from multipers.plots import plot2d_PyModule
664
+ import matplotlib.pyplot as plt
665
+ if (kwargs.get('box')):
666
+ box = kwargs.pop('box')
667
+ else:
668
+ box = [self.get_bottom(), self.get_top()]
669
+ if (len(box[0]) != 2):
670
+ print("Filtration size :", len(box[0]), " != 2")
671
+ return
672
+ num = 0
673
+ if(degree < 0):
674
+ ndim = self.cmod.get_dimension()+1
675
+ scale = kwargs.pop("scale", 4)
676
+ fig, axes = plt.subplots(1, ndim, figsize=(ndim*scale,scale))
677
+ for degree in range(ndim):
678
+ plt.sca(axes[degree]) if ndim > 1 else plt.sca(axes)
679
+ self.plot(degree,box=box,**kwargs)
680
+ return
681
+ corners = self.cmod.get_corners_of_dimension(degree)
682
+ plot2d_PyModule(corners, box=box, dimension=degree, **kwargs)
683
+ return
684
+ def degree_splits(self):
685
+ return np.asarray(self.cmod.get_degree_splits(), dtype=np.int64)
686
+
687
+
688
+ cdef dump_summand_i64(Summand[int64_t]& summand):
689
+ cdef vector[Finitely_critical_multi_filtration[int64_t]] births = summand.get_birth_list()
690
+ cdef vector[Finitely_critical_multi_filtration[int64_t]] deaths = summand.get_death_list()
691
+ return (
692
+ _vff21cview_i64(births, copy=True), ## copy as local variables
693
+ _vff21cview_i64(deaths, copy=True),
694
+ summand.get_dimension(),
695
+ )
696
+
697
+ cdef Summand[int64_t] summand_from_dump_i64(summand_dump):
698
+ cdef vector[Finitely_critical_multi_filtration[int64_t]] births = _py2v1c_i64(summand_dump[0])
699
+ cdef vector[Finitely_critical_multi_filtration[int64_t]] deaths = _py2v1c_i64(summand_dump[1])
700
+ cdef int dim = summand_dump[2]
701
+ return Summand[int64_t](births,deaths,dim)
702
+
703
+ cdef dump_cmod_i64(Module[int64_t]& mod):
704
+ cdef Box[int64_t] cbox = mod.get_box()
705
+ cdef int dim = mod.get_dimension()
706
+ cdef cnp.ndarray[int64_t, ndim=1] bottom_corner = _ff21cview_i64(&cbox.get_bottom_corner())
707
+ cdef cnp.ndarray[int64_t, ndim=1] top_corner = _ff21cview_i64(&cbox.get_upper_corner())
708
+ box = np.asarray([bottom_corner, top_corner])
709
+ summands = tuple(dump_summand_i64(summand) for summand in mod)
710
+ return box, summands
711
+
712
+ cdef Module[int64_t] cmod_from_dump_i64(module_dump):
713
+ box = module_dump[0]
714
+ summands = module_dump[1]
715
+ cdef Module[int64_t] out_module = Module[int64_t]()
716
+ out_module.set_box(Box[int64_t](box))
717
+ for i in range(len(summands)):
718
+ out_module.add_summand(summand_from_dump_i64(summands[i]))
719
+ return out_module
720
+
721
+
722
+ def from_dump_i64(dump)->PyModule_i64:
723
+ """Retrieves a PyModule from a previous dump.
724
+
725
+ Parameters
726
+ ----------
727
+
728
+ dump: either the output of the dump function, or a file containing the output of a dump.
729
+ The dumped module to retrieve
730
+
731
+ Returns
732
+ -------
733
+
734
+ PyModule
735
+ The retrieved module.
736
+ """
737
+ # TODO : optimize...
738
+ mod = PyModule_i64()
739
+ if type(dump) is str:
740
+ dump = pk.load(open(dump, "rb"))
741
+ cdef Module[int64_t] cmod = cmod_from_dump_i64(dump)
742
+ mod.cmod = cmod
743
+ return mod
744
+
745
+ cdef class PySummand_f32:
746
+ """
747
+ Stores a Summand of a PyModule
748
+ """
749
+ cdef Summand[float] sum
750
+
751
+ def get_birth_list(self):
752
+ cdef vector[Finitely_critical_multi_filtration[float]] v = self.sum.get_birth_list()
753
+ return _vff21cview_f32(v, copy = True)
754
+
755
+ def get_death_list(self):
756
+ cdef vector[Finitely_critical_multi_filtration[float]] v = self.sum.get_death_list()
757
+ return _vff21cview_f32(v, copy = True)
758
+ @property
759
+ def degree(self)->int:
760
+ return self.sum.get_dimension()
761
+
762
+ cdef set(self, Summand[float]& summand):
763
+ self.sum = summand
764
+ return self
765
+ def get_bounds(self):
766
+ cdef pair[Finitely_critical_multi_filtration[float],Finitely_critical_multi_filtration[float]] cbounds
767
+ with nogil:
768
+ cbounds = self.sum.get_bounds().get_pair()
769
+ return _ff21cview_f32(&cbounds.first).copy(), _ff21cview_f32(&cbounds.second).copy()
770
+ @property
771
+ def dtype(self):
772
+ return np.float32
773
+
774
+ cdef inline get_summand_filtration_values_f32(Summand[float] summand):
775
+ """
776
+ Returns a list (over parameter) of the filtrations values of this parameter.
777
+ """
778
+ cdef vector[Finitely_critical_multi_filtration[float]] v = summand.get_birth_list()
779
+ births = _vff21cview_f32(v, copy=True)
780
+ v = summand.get_death_list()
781
+ deaths = _vff21cview_f32(v, copy=True)
782
+ pts = np.concatenate([births,deaths],axis=0)
783
+ num_parameters = pts.shape[1]
784
+ out = [np.unique(pts[:,parameter]) for parameter in range(num_parameters)]
785
+ out = [f[:-1] if f[-1] == np.inf else f for f in out]
786
+ out = [f[1:] if f[0] == -np.inf else f for f in out]
787
+ return out
788
+
789
+ cdef class PyBox_f32:
790
+ cdef Box[float] box
791
+ def __cinit__(self, vector[float]& bottomCorner, vector[float]& topCorner):
792
+ self.box = Box[float](bottomCorner, topCorner)
793
+ @property
794
+ def num_parameters(self):
795
+ cdef size_t dim = self.box.get_bottom_corner().num_parameters()
796
+ if dim == self.box.get_upper_corner().num_parameters(): return dim
797
+ else: print("Bad box definition.")
798
+ def contains(self, x):
799
+ return self.box.contains(x)
800
+ cdef set(self, Box[float]& b):
801
+ self.box = b
802
+ return self
803
+
804
+ def get(self):
805
+ return [<vector[float]>self.box.get_bottom_corner(), <vector[float]>self.box.get_upper_corner()]
806
+ def to_multipers(self):
807
+ #assert (self.get_dimension() == 2) "Multipers only works in dimension 2 !"
808
+ return np.array(self.get()).flatten(order = 'F')
809
+ @property
810
+ def dtype(self):
811
+ return np.float32
812
+
813
+
814
+
815
+ cdef class PyModule_f32:
816
+ """
817
+ Stores a representation of a n-persistence module.
818
+ """
819
+ cdef Module[float] cmod
820
+
821
+ @property
822
+ def dtype(self):
823
+ return np.float32
824
+
825
+ cdef set(self, Module[float] m):
826
+ self.cmod = m
827
+ def merge(self, PyModule_f32 other):
828
+ """
829
+ Merges two modules into one
830
+ """
831
+ cdef Module[float] c_other = other.cmod
832
+ with nogil:
833
+ for summand in c_other:
834
+ self.cmod.add_summand(summand)
835
+ return self
836
+
837
+ def _set_from_ptr(self, intptr_t module_ptr):
838
+ """
839
+ Copy module from a memory pointer. Unsafe.
840
+ """
841
+ self.cmod = move(dereference(<Module[float]*>(module_ptr)))
842
+ def set_box(self, PyBox_f32 pybox):
843
+ cdef Box[float] cbox = pybox.box
844
+ with nogil:
845
+ self.cmod.set_box(cbox)
846
+ return self
847
+ def get_module_of_degree(self, int degree)->PyModule_f32: # TODO : in c++ ?
848
+ """
849
+ Returns a copy of a module of fixed degree.
850
+ """
851
+ pmodule = PyModule_f32()
852
+ cdef Box[float] c_box = self.cmod.get_box()
853
+ pmodule.cmod.set_box(c_box)
854
+ with nogil:
855
+ for summand in self.cmod:
856
+ if summand.get_dimension() == degree:
857
+ pmodule.cmod.add_summand(summand)
858
+ return pmodule
859
+ def get_module_of_degrees(self, degrees:Iterable[int])->PyModule_f32: # TODO : in c++ ?
860
+ """
861
+ Returns a copy of the summands of degrees in `degrees`
862
+ """
863
+ pmodule = PyModule_f32()
864
+ cdef Box[float] c_box = self.cmod.get_box()
865
+ pmodule.cmod.set_box(c_box)
866
+ cdef vector[int] cdegrees = degrees
867
+ with nogil:
868
+ for summand in self.cmod:
869
+ for d in cdegrees:
870
+ if d == summand.get_dimension():
871
+ pmodule.cmod.add_summand(summand)
872
+ return pmodule
873
+ def __len__(self)->int:
874
+ return self.cmod.size()
875
+ def get_bottom(self)->np.ndarray:
876
+ """
877
+ Bottom of the box of the module
878
+ """
879
+ return np.asarray(<vector[float]>(self.cmod.get_box().get_bottom_corner()))
880
+ def get_top(self)->np.ndarray:
881
+ """
882
+ Top of the box of the module
883
+ """
884
+ return np.asarray(<vector[float]>(self.cmod.get_box().get_upper_corner()))
885
+ def get_box(self)->np.ndarray:
886
+ """
887
+ Returns the current bounding box of the module.
888
+ """
889
+ return np.asarray([self.get_bottom(), self.get_top()])
890
+ @property
891
+ def max_degree(self)->int:
892
+ """
893
+ Returns the maximum degree of the module.
894
+ """
895
+ return self.cmod.get_dimension()
896
+ @property
897
+ def num_parameters(self)->int:
898
+ cdef size_t dim = self.cmod.get_box().get_bottom_corner().num_parameters()
899
+ assert dim == self.cmod.get_box().get_upper_corner().num_parameters(), "Bad box definition, cannot infer num_parameters."
900
+ return dim
901
+ def dump(self, path:str|None=None):
902
+ """
903
+ Dumps the module into a pickle-able format.
904
+
905
+ Parameters
906
+ ----------
907
+
908
+ path:str=None (optional) saves the pickled module in specified path
909
+
910
+ Returns
911
+ -------
912
+
913
+ list of list, encoding the module, which can be retrieved with the function `from_dump`.
914
+ """
915
+ ## TODO : optimize, but not really used.
916
+ return dump_cmod_f32(self.cmod)
917
+ def __getstate__(self):
918
+ return self.dump()
919
+ def __setstate__(self,dump):
920
+ cdef Module[float] cmod = cmod_from_dump_f32(dump)
921
+ self.cmod = cmod
922
+ return
923
+ def __getitem__(self, int i) -> PySummand_f32:
924
+ if i == slice(None):
925
+ return self
926
+ summand = PySummand_f32()
927
+ summand.set(self.cmod.at(i % self.cmod.size()))
928
+ return summand
929
+ def __iter__(self):
930
+ cdef int num_summands = self.cmod.size()
931
+ for i in range(num_summands):
932
+ summand = PySummand_f32()
933
+ summand.set(self.cmod.at(i)) ## hmm copy. maybe do summand from ptr
934
+ yield summand
935
+
936
+ def get_bounds(self):
937
+ """
938
+ Computes bounds from the summands' bounds.
939
+ Useful to change this' box.
940
+ """
941
+ cdef pair[Finitely_critical_multi_filtration[float],Finitely_critical_multi_filtration[float]] cbounds
942
+ with nogil:
943
+ cbounds = self.cmod.get_bounds().get_pair()
944
+ return _ff21cview_f32(&cbounds.first).copy(), _ff21cview_f32(&cbounds.second).copy()
945
+ def rescale(self,rescale_factors, int degree=-1):
946
+ """
947
+ Rescales the fitlration values of the summands by this rescaling vector.
948
+ """
949
+ cdef vector[float] crescale_factors = rescale_factors
950
+ with nogil:
951
+ self.cmod.rescale(crescale_factors,degree)
952
+ def translate(self,translation, int degree=-1):
953
+ """
954
+ Translates the module in the filtration space by this vector.
955
+ """
956
+ cdef vector[float] ctranslation = translation
957
+ with nogil:
958
+ self.cmod.translate(ctranslation,degree)
959
+
960
+ def get_filtration_values(self, bool unique=True):
961
+ """
962
+ Retrieves all filtration values of the summands of the module.
963
+
964
+ Output format
965
+ -------------
966
+
967
+ list of filtration values for parameter.
968
+ """
969
+ if len(self) ==0:
970
+ return np.empty((self.num_parameters,0))
971
+ values = tuple(tuple(stuff) if len(stuff:=get_summand_filtration_values_f32(summand)) == self.num_parameters else list(stuff) + [[]]*(self.num_parameters - len(stuff)) for summand in self.cmod)
972
+ try:
973
+ values = tuple(np.concatenate([
974
+ f[parameter]
975
+ for f in values
976
+ ], axis=0) for parameter in range(self.num_parameters)
977
+ )
978
+ except:
979
+ return values
980
+ if unique:
981
+ return [np.unique(f) for f in values]
982
+ return values
983
+
984
+ def plot(self, int degree=-1,**kwargs)->None:
985
+ """Shows the module on a plot. Each color corresponds to an apprimation summand of the module, and its shape corresponds to its support.
986
+ Only works with 2-parameter modules.
987
+
988
+ Parameters
989
+ ----------
990
+ degree = -1 : integer
991
+ If positive returns only the image of dimension `dimension`.
992
+ 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.
993
+ If non-None, will plot the module on this specific rectangle.
994
+ min_persistence =0 : float
995
+ Only plots the summand with a persistence above this threshold.
996
+ separated=False : bool
997
+ If true, plot each summand in a different plot.
998
+ alpha=1 : float
999
+ Transparancy parameter
1000
+ save = False : string
1001
+ if nontrivial, will save the figure at this path
1002
+
1003
+
1004
+ Returns
1005
+ -------
1006
+ The figure of the plot.
1007
+ """
1008
+ from multipers.plots import plot2d_PyModule
1009
+ import matplotlib.pyplot as plt
1010
+ if (kwargs.get('box')):
1011
+ box = kwargs.pop('box')
1012
+ else:
1013
+ box = [self.get_bottom(), self.get_top()]
1014
+ if (len(box[0]) != 2):
1015
+ print("Filtration size :", len(box[0]), " != 2")
1016
+ return
1017
+ num = 0
1018
+ if(degree < 0):
1019
+ ndim = self.cmod.get_dimension()+1
1020
+ scale = kwargs.pop("scale", 4)
1021
+ fig, axes = plt.subplots(1, ndim, figsize=(ndim*scale,scale))
1022
+ for degree in range(ndim):
1023
+ plt.sca(axes[degree]) if ndim > 1 else plt.sca(axes)
1024
+ self.plot(degree,box=box,**kwargs)
1025
+ return
1026
+ corners = self.cmod.get_corners_of_dimension(degree)
1027
+ plot2d_PyModule(corners, box=box, dimension=degree, **kwargs)
1028
+ return
1029
+ def degree_splits(self):
1030
+ return np.asarray(self.cmod.get_degree_splits(), dtype=np.int64)
1031
+ def _compute_pixels(self,coordinates:np.ndarray,
1032
+ degrees=None, box=None, float delta=.1,
1033
+ float p=1., bool normalize=False, int n_jobs=0):
1034
+ """
1035
+ Computes the image of the module at the given coordinates
1036
+ """
1037
+ if degrees is not None: assert np.all(degrees[:-1] <= degrees[1:]), "Degrees have to be sorted"
1038
+ cdef vector[int] cdegrees = np.arange(self.max_degree +1) if degrees is None else degrees
1039
+ pybox = PyBox_f32(*self.get_box()) if box is None else PyBox_f32(*box)
1040
+ cdef Box[float] cbox = pybox.box
1041
+ cdef vector[vector[float]] ccoords = coordinates
1042
+ cdef vector[vector[float]] out
1043
+ with nogil:
1044
+ out = self.cmod.compute_pixels(ccoords, cdegrees, cbox, delta, p, normalize, n_jobs)
1045
+ return np.asarray(out)
1046
+ def barcode(self, basepoint, int degree = -1,*, bool threshold = False): # TODO direction vector interface
1047
+ """Computes the barcode of module along a lines.
1048
+
1049
+ Parameters
1050
+ ----------
1051
+
1052
+ basepoint : vector
1053
+ basepoint of the lines on which to compute the barcodes, i.e. a point on the line
1054
+ degree = -1 : integer
1055
+ Homology degree on which to compute the bars. If negative, every dimension is computed
1056
+ box (default) :
1057
+ box on which to compute the barcodes if basepoints is not specified. Default is a linspace of lines crossing that box.
1058
+ threshold = False :
1059
+ Thre
1060
+
1061
+ Warning
1062
+ -------
1063
+
1064
+ If the barcodes are not thresholded, essential barcodes will not be plot-able.
1065
+
1066
+ Returns
1067
+ -------
1068
+
1069
+ PyMultiDiagrams
1070
+ Structure that holds the barcodes. Barcodes can be retrieved with a .get_points() or a .to_multipers() or a .plot().
1071
+ """
1072
+ out = PyMultiDiagram_f32()
1073
+ out.set(self.cmod.get_barcode(Line[float](_py21c_f32(basepoint)), degree, threshold))
1074
+ return out
1075
+ def barcode2(self, basepoint, int degree = -1,*, bool threshold = False): # TODO direction vector interface
1076
+ """
1077
+ Compute the 1d-barcode a diagonal line based on basepoint, with direction (1,1).
1078
+ """
1079
+ return tuple(np.asarray(x) for x in self.cmod.get_barcode2(Line[float](_py21c_f32(basepoint)), degree))
1080
+
1081
+ def barcodes(self, int degree, basepoints = None, num=100, box = None,threshold = False):
1082
+ """Computes barcodes of module along a set of lines.
1083
+
1084
+ Parameters
1085
+ ----------
1086
+
1087
+ basepoints = None : list of vectors
1088
+ basepoints of the lines on which to compute the barcodes.
1089
+ degree = -1 : integer
1090
+ Homology degree on which to compute the bars. If negative, every dimension is computed
1091
+ box (default) :
1092
+ box on which to compute the barcodes if basepoints is not specified. Default is a linspace of lines crossing that box.
1093
+ num:int=100
1094
+ if basepoints is not specified, defines the number of lines to consider.
1095
+ threshold = False : threshold t
1096
+ Resolution of the image(s).
1097
+
1098
+ Warning
1099
+ -------
1100
+
1101
+ If the barcodes are not thresholded, essential barcodes will not be plot-able.
1102
+
1103
+ Returns
1104
+ -------
1105
+
1106
+ PyMultiDiagrams
1107
+ Structure that holds the barcodes. Barcodes can be retrieved with a .get_points() or a .to_multipers() or a .plot().
1108
+ """
1109
+ out = PyMultiDiagrams_f32()
1110
+ if box is None:
1111
+ box = [self.get_bottom(), self.get_top()]
1112
+ if (len(box[0]) != 2) and (basepoints is None):
1113
+ raise ValueError("Basepoints has to be specified for filtration dimension >= 3 !")
1114
+ elif basepoints is None:
1115
+ h = box[1][1] - box[0][1]
1116
+ basepoints = np.linspace([box[0][0] - h,box[0][1]], [box[1][0],box[0][1]], num=num)
1117
+ else :
1118
+ num=len(basepoints)
1119
+ cdef vector[Finitely_critical_multi_filtration[float]] cbasepoints = _py2v1c_f32(basepoints)
1120
+
1121
+ out.set(self.cmod.get_barcodes(cbasepoints, degree, threshold))
1122
+ return out
1123
+
1124
+ def barcodes2(self, int degree = -1, basepoints = None, int num=100, box = None,threshold = False):
1125
+ """Computes barcodes of module along a set of lines.
1126
+
1127
+ Parameters
1128
+ ----------
1129
+
1130
+ basepoints = None : list of vectors
1131
+ basepoints of the lines on which to compute the barcodes.
1132
+ degree = -1 : integer
1133
+ Homology degree on which to compute the bars. If negative, every dimension is computed
1134
+ box (default) :
1135
+ box on which to compute the barcodes if basepoints is not specified. Default is a linspace of lines crossing that box.
1136
+ num:int=100
1137
+ if basepoints is not specified, defines the number of lines to consider.
1138
+ threshold = False : threshold t
1139
+ Resolution of the image(s).
1140
+
1141
+ Warning
1142
+ -------
1143
+
1144
+ If the barcodes are not thresholded, essential barcodes will not be plot-able.
1145
+
1146
+ Returns
1147
+ -------
1148
+
1149
+ tuple of 1d barcodes, based on basepoint, with direction (1,1)
1150
+ """
1151
+ if box is None:
1152
+ box = [self.get_bottom(), self.get_top()]
1153
+ if (len(box[0]) != 2) and (basepoints is None):
1154
+ raise ValueError("Basepoints has to be specified for filtration dimension >= 3 !")
1155
+ elif basepoints is None:
1156
+ h = box[1][1] - box[0][1]
1157
+ basepoints = np.linspace([box[0][0] - h,box[0][1]], [box[1][0],box[0][1]], num=num)
1158
+ else :
1159
+ num=len(basepoints)
1160
+ cdef vector[Line[float]] cbasepoints
1161
+ for i in range(num):
1162
+ cbasepoints.push_back(Line[float](_py21c_f32(basepoints[i])))
1163
+
1164
+ return tuple(np.asarray(bc) for bc in self.cmod.get_barcodes2(cbasepoints, degree)) ###
1165
+
1166
+ def landscape(self, degree:int, k:int=0,box:list|np.ndarray|None=None, resolution:List=[100,100], bool plot=False):
1167
+ """Computes the multiparameter landscape from a PyModule. Python interface only bifiltrations.
1168
+
1169
+ Parameters
1170
+ ----------
1171
+
1172
+ degree : integer
1173
+ The homology degree of the landscape.
1174
+ k = 0 : int
1175
+ the k-th landscape
1176
+ resolution = [50,50] : pair of integers
1177
+ Resolution of the image.
1178
+ box = None : in the format [[a,b], [c,d]]
1179
+ If nontrivial, compute the landscape of this box. Default is the PyModule box.
1180
+ plot = True : Boolean
1181
+ If true, plots the images;
1182
+ Returns
1183
+ -------
1184
+
1185
+ The landscape of the module.
1186
+
1187
+ """
1188
+ import matplotlib.pyplot as plt
1189
+ if box is None:
1190
+ box = self.get_box()
1191
+ cdef Box[float] c_box = Box[float](box)
1192
+ out = np.array(self.cmod.get_landscape(degree, k, c_box, resolution))
1193
+ if plot:
1194
+ plt.figure()
1195
+ aspect = (box[1][0]-box[0][0]) / (box[1][1]-box[0][1])
1196
+ extent = [box[0][0], box[1][0], box[0][1], box[1][1]]
1197
+ plt.imshow(out.T, origin="lower", extent=extent, aspect=aspect)
1198
+ return out
1199
+
1200
+ def landscapes(self, degree:int, ks:list|np.ndarray=[0],box=None, resolution:list|np.ndarray=[100,100], bool plot=False):
1201
+ """Computes the multiparameter landscape from a PyModule. Python interface only bifiltrations.
1202
+
1203
+ Parameters
1204
+ ----------
1205
+
1206
+ - degree : integer
1207
+ The homology degree of the landscape.
1208
+ - ks = 0 : list of int
1209
+ the k-th landscape
1210
+ - resolution = [50,50] : pair of integers
1211
+ Resolution of the image.
1212
+ - box = None : in the format [[a,b], [c,d]]
1213
+ If nontrivial, compute the landscape of this box. Default is the PyModule box.
1214
+ - plot = True : bool
1215
+ If true, plots the images;
1216
+ Returns
1217
+ -------
1218
+
1219
+ The landscapes of the module with parameters ks.
1220
+
1221
+ """
1222
+ import matplotlib.pyplot as plt
1223
+ if box is None:
1224
+ box = self.get_box()
1225
+ out = np.array(self.cmod.get_landscapes(degree, ks, Box[float](box), resolution))
1226
+ if plot:
1227
+ to_plot = np.sum(out, axis=0)
1228
+ plt.figure()
1229
+ aspect = (box[1][0]-box[0][0]) / (box[1][1]-box[0][1])
1230
+ extent = [box[0][0], box[1][0], box[0][1], box[1][1]]
1231
+ plt.imshow(to_plot.T, origin="lower", extent=extent, aspect=aspect)
1232
+ return out
1233
+
1234
+
1235
+ def representation(self, degrees=None, double bandwidth=0.1,
1236
+ resolution:List[int]|int=50,
1237
+ str kernel = "gaussian",
1238
+ bool signed=False,
1239
+ bool normalize=False, bool plot=False,
1240
+ bool save=False, int dpi=200,double p=2., box=None,
1241
+ bool flatten=False, int n_jobs=0,
1242
+ coordinates = None)->np.ndarray:
1243
+
1244
+ """Computes a representation of the module, using
1245
+
1246
+ [A Framework for Fast and Stable Representations of Multiparameter Persistent Homology Decompositions, Neurips2023]
1247
+
1248
+ Parameters
1249
+ ----------
1250
+
1251
+ - degrees = None : integer list
1252
+ If given returns only the image(s) of homology degrees `degrees`.
1253
+ - bandwidth = 0.1 : float
1254
+ Image parameter.
1255
+ - resolution = [100,100] : pair of integers
1256
+ Resolution of the image(s).
1257
+ - normalize = True : Boolean
1258
+ Ensures that the image belongs to [0,1].
1259
+ - plot = False : Boolean
1260
+ If true, plots the images;
1261
+ - flatten=False :
1262
+ If True, reshapes the output to a flattened shape.
1263
+ - kernel: Either linear, gaussian, or callable
1264
+ The kernel to apply to the matrix $(d(x,I), x \in \mathrm{pixels}, I\in \mathrm{summands})$.
1265
+ signature should be : (distance matrix, summand_weights, bandwidth, p) -> representation
1266
+
1267
+ Returns
1268
+ -------
1269
+
1270
+ The list of images, or the image of fixed dimension.
1271
+ """
1272
+ import matplotlib.pyplot as plt
1273
+ # box = kwargs.get("box",[self.get_bottom(),self.get_top()])
1274
+ if box is None:
1275
+ box = self.get_box()
1276
+ num_parameters = self.num_parameters
1277
+ if degrees is None:
1278
+ degrees = np.arange(self.max_degree +1)
1279
+ num_degrees = len(degrees)
1280
+ try:
1281
+ int(resolution)
1282
+ resolution = [resolution]*num_parameters
1283
+ except:
1284
+ pass
1285
+
1286
+ if coordinates is None:
1287
+ xx = [np.linspace(*np.asarray(box)[:,parameter], num=res) for parameter, res in zip(range(num_parameters), resolution)]
1288
+ mesh = np.meshgrid(*xx)
1289
+ coordinates = np.concatenate([stuff.flatten()[:,None] for stuff in mesh], axis=1)
1290
+
1291
+
1292
+ if kernel == "linear":
1293
+ concatenated_images = self._compute_pixels(coordinates, degrees=degrees, box=box, delta=bandwidth, p=p, normalize=normalize,n_jobs=n_jobs)
1294
+ elif kernel == "gaussian":
1295
+ def todo(PyModule_f32 mod_degree):
1296
+ return (
1297
+ (np.exp(
1298
+ -((b:=mod_degree).distance_to(coordinates, signed=signed)/bandwidth)**2)
1299
+ *b.get_interleavings()[None]**p)
1300
+ .sum(1)
1301
+ )
1302
+ concatenated_images = np.stack(
1303
+ Parallel(n_jobs = -1, backend= "threading")(
1304
+ delayed(todo)(self.get_module_of_degree(degree))
1305
+ for degree in degrees
1306
+ )
1307
+ )
1308
+
1309
+ else:
1310
+ concatenated_images = np.stack([
1311
+ kernel((b:=self.get_module_of_degree(degree).distance_to(coordinates, signed=signed)), b.get_interleavings(), bandwidth, p)
1312
+ for degree in degrees
1313
+ ])
1314
+ if flatten:
1315
+ image_vector = concatenated_images.reshape((len(degrees),-1))
1316
+ if plot:
1317
+ raise ValueError("Unflatten to plot.")
1318
+ return image_vector
1319
+ else:
1320
+ image_vector = concatenated_images.reshape((len(degrees),*resolution))
1321
+ if plot:
1322
+ assert num_parameters == 2, "Plot only available for 2-parameter modules"
1323
+ import multipers.plots
1324
+ i=0
1325
+ n_plots = len(image_vector)
1326
+ scale = 4
1327
+ if n_plots >1:
1328
+ fig, axs = plt.subplots(1,n_plots, figsize=(n_plots*scale,scale))
1329
+ else:
1330
+ fig = plt.gcf()
1331
+ axs = [plt.gca()]
1332
+ for image, degree, i in zip(image_vector, degrees, range(num_degrees)):
1333
+ ax = axs[i]
1334
+ temp = multipers.plots.plot_surface(xx, image.T, ax=ax)
1335
+ plt.colorbar(temp, ax = ax)
1336
+ if degree < 0 :
1337
+ ax.set_title(rf"$H_{i}$ $2$-persistence image")
1338
+ if degree >= 0:
1339
+ ax.set_title(rf"$H_{degree}$ $2$-persistence image")
1340
+ return image_vector
1341
+
1342
+ def euler_char(self, points:list|np.ndarray) -> np.ndarray:
1343
+ """ Computes the Euler Characteristic of the filtered complex at given (multiparameter) time
1344
+
1345
+ Parameters
1346
+ ----------
1347
+
1348
+ points: list[float] | list[list[float]] | np.ndarray
1349
+ List of filtration values on which to compute the euler characteristic.
1350
+ WARNING FIXME : the points have to have the same dimension as the simplextree.
1351
+
1352
+ Returns
1353
+ -------
1354
+
1355
+ The list of euler characteristic values
1356
+ """
1357
+ if len(points) == 0:
1358
+ return []
1359
+ if type(points[0]) is float:
1360
+ points = [points]
1361
+ if type(points) is np.ndarray:
1362
+ assert len(points.shape) in [1,2]
1363
+ if len(points.shape) == 1:
1364
+ points = [points]
1365
+ cdef vector[Finitely_critical_multi_filtration[float]] c_points = _py2v1c_f32(points)
1366
+ # cdef Finitely_critical_multi_filtration temp
1367
+ # for point in points:
1368
+ # temp.clear()
1369
+ # for truc in point:
1370
+ # temp.push_back(<float>(truc))
1371
+ # c_points.push_back(temp)
1372
+ cdef Module[float] c_mod = self.cmod
1373
+ with nogil:
1374
+ c_euler = c_mod.euler_curve(c_points)
1375
+ euler = c_euler
1376
+ return np.asarray(euler, dtype=int)
1377
+ def to_idx(self,grid):
1378
+ cdef vector[vector[float]] cgrid = grid
1379
+ cdef vector[vector[pair[vector[vector[int]],vector[vector[int]]]]] out
1380
+ with nogil:
1381
+ out = self.cmod.to_idx(cgrid)
1382
+ return tuple(tuple((np.asarray(I.first,dtype=np.int64), np.asarray(I.second, dtype=np.int64)) for I in Is_of_degree) for Is_of_degree in out)
1383
+ def to_flat_idx(self,grid):
1384
+ cdef vector[vector[float]] cgrid = grid
1385
+ cdef vector[vector[vector[int]]] out
1386
+ with nogil:
1387
+ out = self.cmod.to_flat_idx(cgrid)
1388
+ return np.asarray(out[0], dtype=np.int32), np.asarray(out[1], dtype=np.int32), np.asarray(out[2], dtype=np.int32)
1389
+
1390
+ def distances_idx_to(self, pts, bool full=False, int n_jobs=1):
1391
+ pts = np.asarray(pts)
1392
+ if pts.ndim == 1:
1393
+ pts = pts[None]
1394
+ cdef vector[vector[float]] cpts = pts
1395
+ cdef vector[vector[vector[int]]] out
1396
+ with nogil:
1397
+ out = self.cmod.compute_distances_idx_to(cpts,full, n_jobs)
1398
+ return np.asarray(out, dtype=np.int32)
1399
+
1400
+ def distance_to(self, pts, bool signed=False, int n_jobs = 1)->np.ndarray:
1401
+ """
1402
+ Distance from a point to each summand's support.
1403
+ Signed distance is the distance to the boundary,
1404
+ with negative values inside the summands.
1405
+
1406
+ pts of shape (num_pts, num_parameters)
1407
+
1408
+ output shape : (num_pts,num_summands)
1409
+ """
1410
+ pts = np.asarray(pts)
1411
+ if pts.ndim == 1:
1412
+ pts = pts[None]
1413
+ assert pts.shape[-1] == self.num_parameters
1414
+ cdef vector[vector[float]] cpts = pts
1415
+ cdef vector[vector[float]] out
1416
+ with nogil:
1417
+ out = self.cmod.compute_distances_to(cpts,signed, n_jobs)
1418
+ return np.asarray(out)
1419
+
1420
+ def get_interleavings(self,box=None):
1421
+ if box is None:
1422
+ box = self.get_box()
1423
+ cdef Box[float] cbox = Box[float](box)
1424
+ return np.asarray(self.cmod.get_interleavings(cbox))
1425
+
1426
+ cdef class PyMultiDiagramPoint_f32:
1427
+ cdef MultiDiagram_point[Finitely_critical_multi_filtration[float]] point
1428
+ cdef set(self, MultiDiagram_point[Finitely_critical_multi_filtration[float]] pt):
1429
+ self.point = pt
1430
+ return self
1431
+
1432
+ def get_degree(self):
1433
+ return self.point.get_dimension()
1434
+ def get_birth(self):
1435
+ cdef Finitely_critical_multi_filtration[float] v = self.point.get_birth()
1436
+ return _ff21cview_f32(&v, copy=True)
1437
+ def get_death(self):
1438
+ cdef Finitely_critical_multi_filtration[float] v = self.point.get_death()
1439
+ return _ff21cview_f32(&v, copy=True)
1440
+
1441
+
1442
+ cdef class PyMultiDiagram_f32:
1443
+ """
1444
+ Stores the diagram of a PyModule on a line
1445
+ """
1446
+ cdef MultiDiagram[Finitely_critical_multi_filtration[float], float] multiDiagram
1447
+ cdef set(self, MultiDiagram[Finitely_critical_multi_filtration[float], float] m):
1448
+ self.multiDiagram = m
1449
+ return self
1450
+ def get_points(self, degree:int=-1) -> np.ndarray:
1451
+ cdef vector[pair[vector[float],vector[float]]] out = self.multiDiagram.get_points(degree)
1452
+ if len(out) == 0 and len(self) == 0:
1453
+ return np.empty() # TODO Retrieve good number of parameters if there is no points in diagram
1454
+ if len(out) == 0:
1455
+ return np.empty((0,2,self.multiDiagram.at(0).get_dimension())) # gets the number of parameters
1456
+ return np.array(out)
1457
+ def to_multipers(self, dimension:int):
1458
+ return self.multiDiagram.to_multipers(dimension)
1459
+ def __len__(self) -> int:
1460
+ return self.multiDiagram.size()
1461
+ def __getitem__(self,i:int) -> PyMultiDiagramPoint_f32:
1462
+ return PyMultiDiagramPoint_f32().set(self.multiDiagram.at(i % self.multiDiagram.size()))
1463
+ cdef class PyMultiDiagrams_f32:
1464
+ """
1465
+ Stores the barcodes of a PyModule on multiple lines
1466
+ """
1467
+ cdef MultiDiagrams[Finitely_critical_multi_filtration[float], float] multiDiagrams
1468
+ cdef set(self,MultiDiagrams[Finitely_critical_multi_filtration[float], float] m):
1469
+ self.multiDiagrams = m
1470
+ return self
1471
+ def to_multipers(self):
1472
+ out = self.multiDiagrams.to_multipers()
1473
+ return [np.asarray(summand) for summand in out]
1474
+ def __getitem__(self,i:int):
1475
+ if i >=0 :
1476
+ return PyMultiDiagram_f32().set(self.multiDiagrams.at(i))
1477
+ else:
1478
+ return PyMultiDiagram_f32().set(self.multiDiagrams.at( self.multiDiagrams.size() - i))
1479
+ def __len__(self):
1480
+ return self.multiDiagrams.size()
1481
+ def get_points(self, degree:int=-1):
1482
+ return self.multiDiagrams.get_points()
1483
+ cdef _get_plot_bars(self, dimension:int=-1, min_persistence:float=0):
1484
+ return self.multiDiagrams._for_python_plot(dimension, min_persistence);
1485
+ def plot(self, degree:int=-1, min_persistence:float=0):
1486
+ """
1487
+ Plots the barcodes.
1488
+
1489
+ Parameters
1490
+ ----------
1491
+
1492
+ - degree:int=-1
1493
+ Only plots the bars of specified homology degree. Useful when the multidiagrams contains multiple dimenions
1494
+ - min_persistence:float=0
1495
+ Only plot bars of length greater than this value. Useful to reduce the time to plot.
1496
+
1497
+ Warning
1498
+ -------
1499
+
1500
+ If the barcodes are not thresholded, essential barcodes will not be displayed !
1501
+
1502
+ """
1503
+ from cycler import cycler
1504
+ import matplotlib
1505
+ import matplotlib.pyplot as plt
1506
+ if len(self) == 0: return
1507
+ _cmap = matplotlib.colormaps["Spectral"]
1508
+ multibarcodes_, colors = self._get_plot_bars(degree, min_persistence)
1509
+ n_summands = np.max(colors)+1 if len(colors)>0 else 1
1510
+
1511
+ plt.rc('axes', prop_cycle = cycler('color', [_cmap(i/n_summands) for i in colors]))
1512
+ return plt.plot(*multibarcodes_)
1513
+
1514
+
1515
+ cdef dump_summand_f32(Summand[float]& summand):
1516
+ cdef vector[Finitely_critical_multi_filtration[float]] births = summand.get_birth_list()
1517
+ cdef vector[Finitely_critical_multi_filtration[float]] deaths = summand.get_death_list()
1518
+ return (
1519
+ _vff21cview_f32(births, copy=True), ## copy as local variables
1520
+ _vff21cview_f32(deaths, copy=True),
1521
+ summand.get_dimension(),
1522
+ )
1523
+
1524
+ cdef Summand[float] summand_from_dump_f32(summand_dump):
1525
+ cdef vector[Finitely_critical_multi_filtration[float]] births = _py2v1c_f32(summand_dump[0])
1526
+ cdef vector[Finitely_critical_multi_filtration[float]] deaths = _py2v1c_f32(summand_dump[1])
1527
+ cdef int dim = summand_dump[2]
1528
+ return Summand[float](births,deaths,dim)
1529
+
1530
+ cdef dump_cmod_f32(Module[float]& mod):
1531
+ cdef Box[float] cbox = mod.get_box()
1532
+ cdef int dim = mod.get_dimension()
1533
+ cdef cnp.ndarray[float, ndim=1] bottom_corner = _ff21cview_f32(&cbox.get_bottom_corner())
1534
+ cdef cnp.ndarray[float, ndim=1] top_corner = _ff21cview_f32(&cbox.get_upper_corner())
1535
+ box = np.asarray([bottom_corner, top_corner])
1536
+ summands = tuple(dump_summand_f32(summand) for summand in mod)
1537
+ return box, summands
1538
+
1539
+ cdef Module[float] cmod_from_dump_f32(module_dump):
1540
+ box = module_dump[0]
1541
+ summands = module_dump[1]
1542
+ cdef Module[float] out_module = Module[float]()
1543
+ out_module.set_box(Box[float](box))
1544
+ for i in range(len(summands)):
1545
+ out_module.add_summand(summand_from_dump_f32(summands[i]))
1546
+ return out_module
1547
+
1548
+
1549
+ def from_dump_f32(dump)->PyModule_f32:
1550
+ """Retrieves a PyModule from a previous dump.
1551
+
1552
+ Parameters
1553
+ ----------
1554
+
1555
+ dump: either the output of the dump function, or a file containing the output of a dump.
1556
+ The dumped module to retrieve
1557
+
1558
+ Returns
1559
+ -------
1560
+
1561
+ PyModule
1562
+ The retrieved module.
1563
+ """
1564
+ # TODO : optimize...
1565
+ mod = PyModule_f32()
1566
+ if type(dump) is str:
1567
+ dump = pk.load(open(dump, "rb"))
1568
+ cdef Module[float] cmod = cmod_from_dump_f32(dump)
1569
+ mod.cmod = cmod
1570
+ return mod
1571
+
1572
+ cdef class PySummand_f64:
1573
+ """
1574
+ Stores a Summand of a PyModule
1575
+ """
1576
+ cdef Summand[double] sum
1577
+
1578
+ def get_birth_list(self):
1579
+ cdef vector[Finitely_critical_multi_filtration[double]] v = self.sum.get_birth_list()
1580
+ return _vff21cview_f64(v, copy = True)
1581
+
1582
+ def get_death_list(self):
1583
+ cdef vector[Finitely_critical_multi_filtration[double]] v = self.sum.get_death_list()
1584
+ return _vff21cview_f64(v, copy = True)
1585
+ @property
1586
+ def degree(self)->int:
1587
+ return self.sum.get_dimension()
1588
+
1589
+ cdef set(self, Summand[double]& summand):
1590
+ self.sum = summand
1591
+ return self
1592
+ def get_bounds(self):
1593
+ cdef pair[Finitely_critical_multi_filtration[double],Finitely_critical_multi_filtration[double]] cbounds
1594
+ with nogil:
1595
+ cbounds = self.sum.get_bounds().get_pair()
1596
+ return _ff21cview_f64(&cbounds.first).copy(), _ff21cview_f64(&cbounds.second).copy()
1597
+ @property
1598
+ def dtype(self):
1599
+ return np.float64
1600
+
1601
+ cdef inline get_summand_filtration_values_f64(Summand[double] summand):
1602
+ """
1603
+ Returns a list (over parameter) of the filtrations values of this parameter.
1604
+ """
1605
+ cdef vector[Finitely_critical_multi_filtration[double]] v = summand.get_birth_list()
1606
+ births = _vff21cview_f64(v, copy=True)
1607
+ v = summand.get_death_list()
1608
+ deaths = _vff21cview_f64(v, copy=True)
1609
+ pts = np.concatenate([births,deaths],axis=0)
1610
+ num_parameters = pts.shape[1]
1611
+ out = [np.unique(pts[:,parameter]) for parameter in range(num_parameters)]
1612
+ out = [f[:-1] if f[-1] == np.inf else f for f in out]
1613
+ out = [f[1:] if f[0] == -np.inf else f for f in out]
1614
+ return out
1615
+
1616
+ cdef class PyBox_f64:
1617
+ cdef Box[double] box
1618
+ def __cinit__(self, vector[double]& bottomCorner, vector[double]& topCorner):
1619
+ self.box = Box[double](bottomCorner, topCorner)
1620
+ @property
1621
+ def num_parameters(self):
1622
+ cdef size_t dim = self.box.get_bottom_corner().num_parameters()
1623
+ if dim == self.box.get_upper_corner().num_parameters(): return dim
1624
+ else: print("Bad box definition.")
1625
+ def contains(self, x):
1626
+ return self.box.contains(x)
1627
+ cdef set(self, Box[double]& b):
1628
+ self.box = b
1629
+ return self
1630
+
1631
+ def get(self):
1632
+ return [<vector[double]>self.box.get_bottom_corner(), <vector[double]>self.box.get_upper_corner()]
1633
+ def to_multipers(self):
1634
+ #assert (self.get_dimension() == 2) "Multipers only works in dimension 2 !"
1635
+ return np.array(self.get()).flatten(order = 'F')
1636
+ @property
1637
+ def dtype(self):
1638
+ return np.float64
1639
+
1640
+
1641
+
1642
+ cdef class PyModule_f64:
1643
+ """
1644
+ Stores a representation of a n-persistence module.
1645
+ """
1646
+ cdef Module[double] cmod
1647
+
1648
+ @property
1649
+ def dtype(self):
1650
+ return np.float64
1651
+
1652
+ cdef set(self, Module[double] m):
1653
+ self.cmod = m
1654
+ def merge(self, PyModule_f64 other):
1655
+ """
1656
+ Merges two modules into one
1657
+ """
1658
+ cdef Module[double] c_other = other.cmod
1659
+ with nogil:
1660
+ for summand in c_other:
1661
+ self.cmod.add_summand(summand)
1662
+ return self
1663
+
1664
+ def _set_from_ptr(self, intptr_t module_ptr):
1665
+ """
1666
+ Copy module from a memory pointer. Unsafe.
1667
+ """
1668
+ self.cmod = move(dereference(<Module[double]*>(module_ptr)))
1669
+ def set_box(self, PyBox_f64 pybox):
1670
+ cdef Box[double] cbox = pybox.box
1671
+ with nogil:
1672
+ self.cmod.set_box(cbox)
1673
+ return self
1674
+ def get_module_of_degree(self, int degree)->PyModule_f64: # TODO : in c++ ?
1675
+ """
1676
+ Returns a copy of a module of fixed degree.
1677
+ """
1678
+ pmodule = PyModule_f64()
1679
+ cdef Box[double] c_box = self.cmod.get_box()
1680
+ pmodule.cmod.set_box(c_box)
1681
+ with nogil:
1682
+ for summand in self.cmod:
1683
+ if summand.get_dimension() == degree:
1684
+ pmodule.cmod.add_summand(summand)
1685
+ return pmodule
1686
+ def get_module_of_degrees(self, degrees:Iterable[int])->PyModule_f64: # TODO : in c++ ?
1687
+ """
1688
+ Returns a copy of the summands of degrees in `degrees`
1689
+ """
1690
+ pmodule = PyModule_f64()
1691
+ cdef Box[double] c_box = self.cmod.get_box()
1692
+ pmodule.cmod.set_box(c_box)
1693
+ cdef vector[int] cdegrees = degrees
1694
+ with nogil:
1695
+ for summand in self.cmod:
1696
+ for d in cdegrees:
1697
+ if d == summand.get_dimension():
1698
+ pmodule.cmod.add_summand(summand)
1699
+ return pmodule
1700
+ def __len__(self)->int:
1701
+ return self.cmod.size()
1702
+ def get_bottom(self)->np.ndarray:
1703
+ """
1704
+ Bottom of the box of the module
1705
+ """
1706
+ return np.asarray(<vector[double]>(self.cmod.get_box().get_bottom_corner()))
1707
+ def get_top(self)->np.ndarray:
1708
+ """
1709
+ Top of the box of the module
1710
+ """
1711
+ return np.asarray(<vector[double]>(self.cmod.get_box().get_upper_corner()))
1712
+ def get_box(self)->np.ndarray:
1713
+ """
1714
+ Returns the current bounding box of the module.
1715
+ """
1716
+ return np.asarray([self.get_bottom(), self.get_top()])
1717
+ @property
1718
+ def max_degree(self)->int:
1719
+ """
1720
+ Returns the maximum degree of the module.
1721
+ """
1722
+ return self.cmod.get_dimension()
1723
+ @property
1724
+ def num_parameters(self)->int:
1725
+ cdef size_t dim = self.cmod.get_box().get_bottom_corner().num_parameters()
1726
+ assert dim == self.cmod.get_box().get_upper_corner().num_parameters(), "Bad box definition, cannot infer num_parameters."
1727
+ return dim
1728
+ def dump(self, path:str|None=None):
1729
+ """
1730
+ Dumps the module into a pickle-able format.
1731
+
1732
+ Parameters
1733
+ ----------
1734
+
1735
+ path:str=None (optional) saves the pickled module in specified path
1736
+
1737
+ Returns
1738
+ -------
1739
+
1740
+ list of list, encoding the module, which can be retrieved with the function `from_dump`.
1741
+ """
1742
+ ## TODO : optimize, but not really used.
1743
+ return dump_cmod_f64(self.cmod)
1744
+ def __getstate__(self):
1745
+ return self.dump()
1746
+ def __setstate__(self,dump):
1747
+ cdef Module[double] cmod = cmod_from_dump_f64(dump)
1748
+ self.cmod = cmod
1749
+ return
1750
+ def __getitem__(self, int i) -> PySummand_f64:
1751
+ if i == slice(None):
1752
+ return self
1753
+ summand = PySummand_f64()
1754
+ summand.set(self.cmod.at(i % self.cmod.size()))
1755
+ return summand
1756
+ def __iter__(self):
1757
+ cdef int num_summands = self.cmod.size()
1758
+ for i in range(num_summands):
1759
+ summand = PySummand_f64()
1760
+ summand.set(self.cmod.at(i)) ## hmm copy. maybe do summand from ptr
1761
+ yield summand
1762
+
1763
+ def get_bounds(self):
1764
+ """
1765
+ Computes bounds from the summands' bounds.
1766
+ Useful to change this' box.
1767
+ """
1768
+ cdef pair[Finitely_critical_multi_filtration[double],Finitely_critical_multi_filtration[double]] cbounds
1769
+ with nogil:
1770
+ cbounds = self.cmod.get_bounds().get_pair()
1771
+ return _ff21cview_f64(&cbounds.first).copy(), _ff21cview_f64(&cbounds.second).copy()
1772
+ def rescale(self,rescale_factors, int degree=-1):
1773
+ """
1774
+ Rescales the fitlration values of the summands by this rescaling vector.
1775
+ """
1776
+ cdef vector[double] crescale_factors = rescale_factors
1777
+ with nogil:
1778
+ self.cmod.rescale(crescale_factors,degree)
1779
+ def translate(self,translation, int degree=-1):
1780
+ """
1781
+ Translates the module in the filtration space by this vector.
1782
+ """
1783
+ cdef vector[double] ctranslation = translation
1784
+ with nogil:
1785
+ self.cmod.translate(ctranslation,degree)
1786
+
1787
+ def get_filtration_values(self, bool unique=True):
1788
+ """
1789
+ Retrieves all filtration values of the summands of the module.
1790
+
1791
+ Output format
1792
+ -------------
1793
+
1794
+ list of filtration values for parameter.
1795
+ """
1796
+ if len(self) ==0:
1797
+ return np.empty((self.num_parameters,0))
1798
+ values = tuple(tuple(stuff) if len(stuff:=get_summand_filtration_values_f64(summand)) == self.num_parameters else list(stuff) + [[]]*(self.num_parameters - len(stuff)) for summand in self.cmod)
1799
+ try:
1800
+ values = tuple(np.concatenate([
1801
+ f[parameter]
1802
+ for f in values
1803
+ ], axis=0) for parameter in range(self.num_parameters)
1804
+ )
1805
+ except:
1806
+ return values
1807
+ if unique:
1808
+ return [np.unique(f) for f in values]
1809
+ return values
1810
+
1811
+ def plot(self, int degree=-1,**kwargs)->None:
1812
+ """Shows the module on a plot. Each color corresponds to an apprimation summand of the module, and its shape corresponds to its support.
1813
+ Only works with 2-parameter modules.
1814
+
1815
+ Parameters
1816
+ ----------
1817
+ degree = -1 : integer
1818
+ If positive returns only the image of dimension `dimension`.
1819
+ 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.
1820
+ If non-None, will plot the module on this specific rectangle.
1821
+ min_persistence =0 : float
1822
+ Only plots the summand with a persistence above this threshold.
1823
+ separated=False : bool
1824
+ If true, plot each summand in a different plot.
1825
+ alpha=1 : float
1826
+ Transparancy parameter
1827
+ save = False : string
1828
+ if nontrivial, will save the figure at this path
1829
+
1830
+
1831
+ Returns
1832
+ -------
1833
+ The figure of the plot.
1834
+ """
1835
+ from multipers.plots import plot2d_PyModule
1836
+ import matplotlib.pyplot as plt
1837
+ if (kwargs.get('box')):
1838
+ box = kwargs.pop('box')
1839
+ else:
1840
+ box = [self.get_bottom(), self.get_top()]
1841
+ if (len(box[0]) != 2):
1842
+ print("Filtration size :", len(box[0]), " != 2")
1843
+ return
1844
+ num = 0
1845
+ if(degree < 0):
1846
+ ndim = self.cmod.get_dimension()+1
1847
+ scale = kwargs.pop("scale", 4)
1848
+ fig, axes = plt.subplots(1, ndim, figsize=(ndim*scale,scale))
1849
+ for degree in range(ndim):
1850
+ plt.sca(axes[degree]) if ndim > 1 else plt.sca(axes)
1851
+ self.plot(degree,box=box,**kwargs)
1852
+ return
1853
+ corners = self.cmod.get_corners_of_dimension(degree)
1854
+ plot2d_PyModule(corners, box=box, dimension=degree, **kwargs)
1855
+ return
1856
+ def degree_splits(self):
1857
+ return np.asarray(self.cmod.get_degree_splits(), dtype=np.int64)
1858
+ def _compute_pixels(self,coordinates:np.ndarray,
1859
+ degrees=None, box=None, double delta=.1,
1860
+ double p=1., bool normalize=False, int n_jobs=0):
1861
+ """
1862
+ Computes the image of the module at the given coordinates
1863
+ """
1864
+ if degrees is not None: assert np.all(degrees[:-1] <= degrees[1:]), "Degrees have to be sorted"
1865
+ cdef vector[int] cdegrees = np.arange(self.max_degree +1) if degrees is None else degrees
1866
+ pybox = PyBox_f64(*self.get_box()) if box is None else PyBox_f64(*box)
1867
+ cdef Box[double] cbox = pybox.box
1868
+ cdef vector[vector[double]] ccoords = coordinates
1869
+ cdef vector[vector[double]] out
1870
+ with nogil:
1871
+ out = self.cmod.compute_pixels(ccoords, cdegrees, cbox, delta, p, normalize, n_jobs)
1872
+ return np.asarray(out)
1873
+ def barcode(self, basepoint, int degree = -1,*, bool threshold = False): # TODO direction vector interface
1874
+ """Computes the barcode of module along a lines.
1875
+
1876
+ Parameters
1877
+ ----------
1878
+
1879
+ basepoint : vector
1880
+ basepoint of the lines on which to compute the barcodes, i.e. a point on the line
1881
+ degree = -1 : integer
1882
+ Homology degree on which to compute the bars. If negative, every dimension is computed
1883
+ box (default) :
1884
+ box on which to compute the barcodes if basepoints is not specified. Default is a linspace of lines crossing that box.
1885
+ threshold = False :
1886
+ Thre
1887
+
1888
+ Warning
1889
+ -------
1890
+
1891
+ If the barcodes are not thresholded, essential barcodes will not be plot-able.
1892
+
1893
+ Returns
1894
+ -------
1895
+
1896
+ PyMultiDiagrams
1897
+ Structure that holds the barcodes. Barcodes can be retrieved with a .get_points() or a .to_multipers() or a .plot().
1898
+ """
1899
+ out = PyMultiDiagram_f64()
1900
+ out.set(self.cmod.get_barcode(Line[double](_py21c_f64(basepoint)), degree, threshold))
1901
+ return out
1902
+ def barcode2(self, basepoint, int degree = -1,*, bool threshold = False): # TODO direction vector interface
1903
+ """
1904
+ Compute the 1d-barcode a diagonal line based on basepoint, with direction (1,1).
1905
+ """
1906
+ return tuple(np.asarray(x) for x in self.cmod.get_barcode2(Line[double](_py21c_f64(basepoint)), degree))
1907
+
1908
+ def barcodes(self, int degree, basepoints = None, num=100, box = None,threshold = False):
1909
+ """Computes barcodes of module along a set of lines.
1910
+
1911
+ Parameters
1912
+ ----------
1913
+
1914
+ basepoints = None : list of vectors
1915
+ basepoints of the lines on which to compute the barcodes.
1916
+ degree = -1 : integer
1917
+ Homology degree on which to compute the bars. If negative, every dimension is computed
1918
+ box (default) :
1919
+ box on which to compute the barcodes if basepoints is not specified. Default is a linspace of lines crossing that box.
1920
+ num:int=100
1921
+ if basepoints is not specified, defines the number of lines to consider.
1922
+ threshold = False : threshold t
1923
+ Resolution of the image(s).
1924
+
1925
+ Warning
1926
+ -------
1927
+
1928
+ If the barcodes are not thresholded, essential barcodes will not be plot-able.
1929
+
1930
+ Returns
1931
+ -------
1932
+
1933
+ PyMultiDiagrams
1934
+ Structure that holds the barcodes. Barcodes can be retrieved with a .get_points() or a .to_multipers() or a .plot().
1935
+ """
1936
+ out = PyMultiDiagrams_f64()
1937
+ if box is None:
1938
+ box = [self.get_bottom(), self.get_top()]
1939
+ if (len(box[0]) != 2) and (basepoints is None):
1940
+ raise ValueError("Basepoints has to be specified for filtration dimension >= 3 !")
1941
+ elif basepoints is None:
1942
+ h = box[1][1] - box[0][1]
1943
+ basepoints = np.linspace([box[0][0] - h,box[0][1]], [box[1][0],box[0][1]], num=num)
1944
+ else :
1945
+ num=len(basepoints)
1946
+ cdef vector[Finitely_critical_multi_filtration[double]] cbasepoints = _py2v1c_f64(basepoints)
1947
+
1948
+ out.set(self.cmod.get_barcodes(cbasepoints, degree, threshold))
1949
+ return out
1950
+
1951
+ def barcodes2(self, int degree = -1, basepoints = None, int num=100, box = None,threshold = False):
1952
+ """Computes barcodes of module along a set of lines.
1953
+
1954
+ Parameters
1955
+ ----------
1956
+
1957
+ basepoints = None : list of vectors
1958
+ basepoints of the lines on which to compute the barcodes.
1959
+ degree = -1 : integer
1960
+ Homology degree on which to compute the bars. If negative, every dimension is computed
1961
+ box (default) :
1962
+ box on which to compute the barcodes if basepoints is not specified. Default is a linspace of lines crossing that box.
1963
+ num:int=100
1964
+ if basepoints is not specified, defines the number of lines to consider.
1965
+ threshold = False : threshold t
1966
+ Resolution of the image(s).
1967
+
1968
+ Warning
1969
+ -------
1970
+
1971
+ If the barcodes are not thresholded, essential barcodes will not be plot-able.
1972
+
1973
+ Returns
1974
+ -------
1975
+
1976
+ tuple of 1d barcodes, based on basepoint, with direction (1,1)
1977
+ """
1978
+ if box is None:
1979
+ box = [self.get_bottom(), self.get_top()]
1980
+ if (len(box[0]) != 2) and (basepoints is None):
1981
+ raise ValueError("Basepoints has to be specified for filtration dimension >= 3 !")
1982
+ elif basepoints is None:
1983
+ h = box[1][1] - box[0][1]
1984
+ basepoints = np.linspace([box[0][0] - h,box[0][1]], [box[1][0],box[0][1]], num=num)
1985
+ else :
1986
+ num=len(basepoints)
1987
+ cdef vector[Line[double]] cbasepoints
1988
+ for i in range(num):
1989
+ cbasepoints.push_back(Line[double](_py21c_f64(basepoints[i])))
1990
+
1991
+ return tuple(np.asarray(bc) for bc in self.cmod.get_barcodes2(cbasepoints, degree)) ###
1992
+
1993
+ def landscape(self, degree:int, k:int=0,box:list|np.ndarray|None=None, resolution:List=[100,100], bool plot=False):
1994
+ """Computes the multiparameter landscape from a PyModule. Python interface only bifiltrations.
1995
+
1996
+ Parameters
1997
+ ----------
1998
+
1999
+ degree : integer
2000
+ The homology degree of the landscape.
2001
+ k = 0 : int
2002
+ the k-th landscape
2003
+ resolution = [50,50] : pair of integers
2004
+ Resolution of the image.
2005
+ box = None : in the format [[a,b], [c,d]]
2006
+ If nontrivial, compute the landscape of this box. Default is the PyModule box.
2007
+ plot = True : Boolean
2008
+ If true, plots the images;
2009
+ Returns
2010
+ -------
2011
+
2012
+ The landscape of the module.
2013
+
2014
+ """
2015
+ import matplotlib.pyplot as plt
2016
+ if box is None:
2017
+ box = self.get_box()
2018
+ cdef Box[double] c_box = Box[double](box)
2019
+ out = np.array(self.cmod.get_landscape(degree, k, c_box, resolution))
2020
+ if plot:
2021
+ plt.figure()
2022
+ aspect = (box[1][0]-box[0][0]) / (box[1][1]-box[0][1])
2023
+ extent = [box[0][0], box[1][0], box[0][1], box[1][1]]
2024
+ plt.imshow(out.T, origin="lower", extent=extent, aspect=aspect)
2025
+ return out
2026
+
2027
+ def landscapes(self, degree:int, ks:list|np.ndarray=[0],box=None, resolution:list|np.ndarray=[100,100], bool plot=False):
2028
+ """Computes the multiparameter landscape from a PyModule. Python interface only bifiltrations.
2029
+
2030
+ Parameters
2031
+ ----------
2032
+
2033
+ - degree : integer
2034
+ The homology degree of the landscape.
2035
+ - ks = 0 : list of int
2036
+ the k-th landscape
2037
+ - resolution = [50,50] : pair of integers
2038
+ Resolution of the image.
2039
+ - box = None : in the format [[a,b], [c,d]]
2040
+ If nontrivial, compute the landscape of this box. Default is the PyModule box.
2041
+ - plot = True : bool
2042
+ If true, plots the images;
2043
+ Returns
2044
+ -------
2045
+
2046
+ The landscapes of the module with parameters ks.
2047
+
2048
+ """
2049
+ import matplotlib.pyplot as plt
2050
+ if box is None:
2051
+ box = self.get_box()
2052
+ out = np.array(self.cmod.get_landscapes(degree, ks, Box[double](box), resolution))
2053
+ if plot:
2054
+ to_plot = np.sum(out, axis=0)
2055
+ plt.figure()
2056
+ aspect = (box[1][0]-box[0][0]) / (box[1][1]-box[0][1])
2057
+ extent = [box[0][0], box[1][0], box[0][1], box[1][1]]
2058
+ plt.imshow(to_plot.T, origin="lower", extent=extent, aspect=aspect)
2059
+ return out
2060
+
2061
+
2062
+ def representation(self, degrees=None, double bandwidth=0.1,
2063
+ resolution:List[int]|int=50,
2064
+ str kernel = "gaussian",
2065
+ bool signed=False,
2066
+ bool normalize=False, bool plot=False,
2067
+ bool save=False, int dpi=200,double p=2., box=None,
2068
+ bool flatten=False, int n_jobs=0,
2069
+ coordinates = None)->np.ndarray:
2070
+
2071
+ """Computes a representation of the module, using
2072
+
2073
+ [A Framework for Fast and Stable Representations of Multiparameter Persistent Homology Decompositions, Neurips2023]
2074
+
2075
+ Parameters
2076
+ ----------
2077
+
2078
+ - degrees = None : integer list
2079
+ If given returns only the image(s) of homology degrees `degrees`.
2080
+ - bandwidth = 0.1 : float
2081
+ Image parameter.
2082
+ - resolution = [100,100] : pair of integers
2083
+ Resolution of the image(s).
2084
+ - normalize = True : Boolean
2085
+ Ensures that the image belongs to [0,1].
2086
+ - plot = False : Boolean
2087
+ If true, plots the images;
2088
+ - flatten=False :
2089
+ If True, reshapes the output to a flattened shape.
2090
+ - kernel: Either linear, gaussian, or callable
2091
+ The kernel to apply to the matrix $(d(x,I), x \in \mathrm{pixels}, I\in \mathrm{summands})$.
2092
+ signature should be : (distance matrix, summand_weights, bandwidth, p) -> representation
2093
+
2094
+ Returns
2095
+ -------
2096
+
2097
+ The list of images, or the image of fixed dimension.
2098
+ """
2099
+ import matplotlib.pyplot as plt
2100
+ # box = kwargs.get("box",[self.get_bottom(),self.get_top()])
2101
+ if box is None:
2102
+ box = self.get_box()
2103
+ num_parameters = self.num_parameters
2104
+ if degrees is None:
2105
+ degrees = np.arange(self.max_degree +1)
2106
+ num_degrees = len(degrees)
2107
+ try:
2108
+ int(resolution)
2109
+ resolution = [resolution]*num_parameters
2110
+ except:
2111
+ pass
2112
+
2113
+ if coordinates is None:
2114
+ xx = [np.linspace(*np.asarray(box)[:,parameter], num=res) for parameter, res in zip(range(num_parameters), resolution)]
2115
+ mesh = np.meshgrid(*xx)
2116
+ coordinates = np.concatenate([stuff.flatten()[:,None] for stuff in mesh], axis=1)
2117
+
2118
+
2119
+ if kernel == "linear":
2120
+ concatenated_images = self._compute_pixels(coordinates, degrees=degrees, box=box, delta=bandwidth, p=p, normalize=normalize,n_jobs=n_jobs)
2121
+ elif kernel == "gaussian":
2122
+ def todo(PyModule_f64 mod_degree):
2123
+ return (
2124
+ (np.exp(
2125
+ -((b:=mod_degree).distance_to(coordinates, signed=signed)/bandwidth)**2)
2126
+ *b.get_interleavings()[None]**p)
2127
+ .sum(1)
2128
+ )
2129
+ concatenated_images = np.stack(
2130
+ Parallel(n_jobs = -1, backend= "threading")(
2131
+ delayed(todo)(self.get_module_of_degree(degree))
2132
+ for degree in degrees
2133
+ )
2134
+ )
2135
+
2136
+ else:
2137
+ concatenated_images = np.stack([
2138
+ kernel((b:=self.get_module_of_degree(degree).distance_to(coordinates, signed=signed)), b.get_interleavings(), bandwidth, p)
2139
+ for degree in degrees
2140
+ ])
2141
+ if flatten:
2142
+ image_vector = concatenated_images.reshape((len(degrees),-1))
2143
+ if plot:
2144
+ raise ValueError("Unflatten to plot.")
2145
+ return image_vector
2146
+ else:
2147
+ image_vector = concatenated_images.reshape((len(degrees),*resolution))
2148
+ if plot:
2149
+ assert num_parameters == 2, "Plot only available for 2-parameter modules"
2150
+ import multipers.plots
2151
+ i=0
2152
+ n_plots = len(image_vector)
2153
+ scale = 4
2154
+ if n_plots >1:
2155
+ fig, axs = plt.subplots(1,n_plots, figsize=(n_plots*scale,scale))
2156
+ else:
2157
+ fig = plt.gcf()
2158
+ axs = [plt.gca()]
2159
+ for image, degree, i in zip(image_vector, degrees, range(num_degrees)):
2160
+ ax = axs[i]
2161
+ temp = multipers.plots.plot_surface(xx, image.T, ax=ax)
2162
+ plt.colorbar(temp, ax = ax)
2163
+ if degree < 0 :
2164
+ ax.set_title(rf"$H_{i}$ $2$-persistence image")
2165
+ if degree >= 0:
2166
+ ax.set_title(rf"$H_{degree}$ $2$-persistence image")
2167
+ return image_vector
2168
+
2169
+ def euler_char(self, points:list|np.ndarray) -> np.ndarray:
2170
+ """ Computes the Euler Characteristic of the filtered complex at given (multiparameter) time
2171
+
2172
+ Parameters
2173
+ ----------
2174
+
2175
+ points: list[float] | list[list[float]] | np.ndarray
2176
+ List of filtration values on which to compute the euler characteristic.
2177
+ WARNING FIXME : the points have to have the same dimension as the simplextree.
2178
+
2179
+ Returns
2180
+ -------
2181
+
2182
+ The list of euler characteristic values
2183
+ """
2184
+ if len(points) == 0:
2185
+ return []
2186
+ if type(points[0]) is float:
2187
+ points = [points]
2188
+ if type(points) is np.ndarray:
2189
+ assert len(points.shape) in [1,2]
2190
+ if len(points.shape) == 1:
2191
+ points = [points]
2192
+ cdef vector[Finitely_critical_multi_filtration[double]] c_points = _py2v1c_f64(points)
2193
+ # cdef Finitely_critical_multi_filtration temp
2194
+ # for point in points:
2195
+ # temp.clear()
2196
+ # for truc in point:
2197
+ # temp.push_back(<double>(truc))
2198
+ # c_points.push_back(temp)
2199
+ cdef Module[double] c_mod = self.cmod
2200
+ with nogil:
2201
+ c_euler = c_mod.euler_curve(c_points)
2202
+ euler = c_euler
2203
+ return np.asarray(euler, dtype=int)
2204
+ def to_idx(self,grid):
2205
+ cdef vector[vector[double]] cgrid = grid
2206
+ cdef vector[vector[pair[vector[vector[int]],vector[vector[int]]]]] out
2207
+ with nogil:
2208
+ out = self.cmod.to_idx(cgrid)
2209
+ return tuple(tuple((np.asarray(I.first,dtype=np.int64), np.asarray(I.second, dtype=np.int64)) for I in Is_of_degree) for Is_of_degree in out)
2210
+ def to_flat_idx(self,grid):
2211
+ cdef vector[vector[double]] cgrid = grid
2212
+ cdef vector[vector[vector[int]]] out
2213
+ with nogil:
2214
+ out = self.cmod.to_flat_idx(cgrid)
2215
+ return np.asarray(out[0], dtype=np.int32), np.asarray(out[1], dtype=np.int32), np.asarray(out[2], dtype=np.int32)
2216
+
2217
+ def distances_idx_to(self, pts, bool full=False, int n_jobs=1):
2218
+ pts = np.asarray(pts)
2219
+ if pts.ndim == 1:
2220
+ pts = pts[None]
2221
+ cdef vector[vector[double]] cpts = pts
2222
+ cdef vector[vector[vector[int]]] out
2223
+ with nogil:
2224
+ out = self.cmod.compute_distances_idx_to(cpts,full, n_jobs)
2225
+ return np.asarray(out, dtype=np.int32)
2226
+
2227
+ def distance_to(self, pts, bool signed=False, int n_jobs = 1)->np.ndarray:
2228
+ """
2229
+ Distance from a point to each summand's support.
2230
+ Signed distance is the distance to the boundary,
2231
+ with negative values inside the summands.
2232
+
2233
+ pts of shape (num_pts, num_parameters)
2234
+
2235
+ output shape : (num_pts,num_summands)
2236
+ """
2237
+ pts = np.asarray(pts)
2238
+ if pts.ndim == 1:
2239
+ pts = pts[None]
2240
+ assert pts.shape[-1] == self.num_parameters
2241
+ cdef vector[vector[double]] cpts = pts
2242
+ cdef vector[vector[double]] out
2243
+ with nogil:
2244
+ out = self.cmod.compute_distances_to(cpts,signed, n_jobs)
2245
+ return np.asarray(out)
2246
+
2247
+ def get_interleavings(self,box=None):
2248
+ if box is None:
2249
+ box = self.get_box()
2250
+ cdef Box[double] cbox = Box[double](box)
2251
+ return np.asarray(self.cmod.get_interleavings(cbox))
2252
+
2253
+ cdef class PyMultiDiagramPoint_f64:
2254
+ cdef MultiDiagram_point[Finitely_critical_multi_filtration[double]] point
2255
+ cdef set(self, MultiDiagram_point[Finitely_critical_multi_filtration[double]] pt):
2256
+ self.point = pt
2257
+ return self
2258
+
2259
+ def get_degree(self):
2260
+ return self.point.get_dimension()
2261
+ def get_birth(self):
2262
+ cdef Finitely_critical_multi_filtration[double] v = self.point.get_birth()
2263
+ return _ff21cview_f64(&v, copy=True)
2264
+ def get_death(self):
2265
+ cdef Finitely_critical_multi_filtration[double] v = self.point.get_death()
2266
+ return _ff21cview_f64(&v, copy=True)
2267
+
2268
+
2269
+ cdef class PyMultiDiagram_f64:
2270
+ """
2271
+ Stores the diagram of a PyModule on a line
2272
+ """
2273
+ cdef MultiDiagram[Finitely_critical_multi_filtration[double], double] multiDiagram
2274
+ cdef set(self, MultiDiagram[Finitely_critical_multi_filtration[double], double] m):
2275
+ self.multiDiagram = m
2276
+ return self
2277
+ def get_points(self, degree:int=-1) -> np.ndarray:
2278
+ cdef vector[pair[vector[double],vector[double]]] out = self.multiDiagram.get_points(degree)
2279
+ if len(out) == 0 and len(self) == 0:
2280
+ return np.empty() # TODO Retrieve good number of parameters if there is no points in diagram
2281
+ if len(out) == 0:
2282
+ return np.empty((0,2,self.multiDiagram.at(0).get_dimension())) # gets the number of parameters
2283
+ return np.array(out)
2284
+ def to_multipers(self, dimension:int):
2285
+ return self.multiDiagram.to_multipers(dimension)
2286
+ def __len__(self) -> int:
2287
+ return self.multiDiagram.size()
2288
+ def __getitem__(self,i:int) -> PyMultiDiagramPoint_f64:
2289
+ return PyMultiDiagramPoint_f64().set(self.multiDiagram.at(i % self.multiDiagram.size()))
2290
+ cdef class PyMultiDiagrams_f64:
2291
+ """
2292
+ Stores the barcodes of a PyModule on multiple lines
2293
+ """
2294
+ cdef MultiDiagrams[Finitely_critical_multi_filtration[double], double] multiDiagrams
2295
+ cdef set(self,MultiDiagrams[Finitely_critical_multi_filtration[double], double] m):
2296
+ self.multiDiagrams = m
2297
+ return self
2298
+ def to_multipers(self):
2299
+ out = self.multiDiagrams.to_multipers()
2300
+ return [np.asarray(summand) for summand in out]
2301
+ def __getitem__(self,i:int):
2302
+ if i >=0 :
2303
+ return PyMultiDiagram_f64().set(self.multiDiagrams.at(i))
2304
+ else:
2305
+ return PyMultiDiagram_f64().set(self.multiDiagrams.at( self.multiDiagrams.size() - i))
2306
+ def __len__(self):
2307
+ return self.multiDiagrams.size()
2308
+ def get_points(self, degree:int=-1):
2309
+ return self.multiDiagrams.get_points()
2310
+ cdef _get_plot_bars(self, dimension:int=-1, min_persistence:float=0):
2311
+ return self.multiDiagrams._for_python_plot(dimension, min_persistence);
2312
+ def plot(self, degree:int=-1, min_persistence:float=0):
2313
+ """
2314
+ Plots the barcodes.
2315
+
2316
+ Parameters
2317
+ ----------
2318
+
2319
+ - degree:int=-1
2320
+ Only plots the bars of specified homology degree. Useful when the multidiagrams contains multiple dimenions
2321
+ - min_persistence:float=0
2322
+ Only plot bars of length greater than this value. Useful to reduce the time to plot.
2323
+
2324
+ Warning
2325
+ -------
2326
+
2327
+ If the barcodes are not thresholded, essential barcodes will not be displayed !
2328
+
2329
+ """
2330
+ from cycler import cycler
2331
+ import matplotlib
2332
+ import matplotlib.pyplot as plt
2333
+ if len(self) == 0: return
2334
+ _cmap = matplotlib.colormaps["Spectral"]
2335
+ multibarcodes_, colors = self._get_plot_bars(degree, min_persistence)
2336
+ n_summands = np.max(colors)+1 if len(colors)>0 else 1
2337
+
2338
+ plt.rc('axes', prop_cycle = cycler('color', [_cmap(i/n_summands) for i in colors]))
2339
+ return plt.plot(*multibarcodes_)
2340
+
2341
+
2342
+ cdef dump_summand_f64(Summand[double]& summand):
2343
+ cdef vector[Finitely_critical_multi_filtration[double]] births = summand.get_birth_list()
2344
+ cdef vector[Finitely_critical_multi_filtration[double]] deaths = summand.get_death_list()
2345
+ return (
2346
+ _vff21cview_f64(births, copy=True), ## copy as local variables
2347
+ _vff21cview_f64(deaths, copy=True),
2348
+ summand.get_dimension(),
2349
+ )
2350
+
2351
+ cdef Summand[double] summand_from_dump_f64(summand_dump):
2352
+ cdef vector[Finitely_critical_multi_filtration[double]] births = _py2v1c_f64(summand_dump[0])
2353
+ cdef vector[Finitely_critical_multi_filtration[double]] deaths = _py2v1c_f64(summand_dump[1])
2354
+ cdef int dim = summand_dump[2]
2355
+ return Summand[double](births,deaths,dim)
2356
+
2357
+ cdef dump_cmod_f64(Module[double]& mod):
2358
+ cdef Box[double] cbox = mod.get_box()
2359
+ cdef int dim = mod.get_dimension()
2360
+ cdef cnp.ndarray[double, ndim=1] bottom_corner = _ff21cview_f64(&cbox.get_bottom_corner())
2361
+ cdef cnp.ndarray[double, ndim=1] top_corner = _ff21cview_f64(&cbox.get_upper_corner())
2362
+ box = np.asarray([bottom_corner, top_corner])
2363
+ summands = tuple(dump_summand_f64(summand) for summand in mod)
2364
+ return box, summands
2365
+
2366
+ cdef Module[double] cmod_from_dump_f64(module_dump):
2367
+ box = module_dump[0]
2368
+ summands = module_dump[1]
2369
+ cdef Module[double] out_module = Module[double]()
2370
+ out_module.set_box(Box[double](box))
2371
+ for i in range(len(summands)):
2372
+ out_module.add_summand(summand_from_dump_f64(summands[i]))
2373
+ return out_module
2374
+
2375
+
2376
+ def from_dump_f64(dump)->PyModule_f64:
2377
+ """Retrieves a PyModule from a previous dump.
2378
+
2379
+ Parameters
2380
+ ----------
2381
+
2382
+ dump: either the output of the dump function, or a file containing the output of a dump.
2383
+ The dumped module to retrieve
2384
+
2385
+ Returns
2386
+ -------
2387
+
2388
+ PyModule
2389
+ The retrieved module.
2390
+ """
2391
+ # TODO : optimize...
2392
+ mod = PyModule_f64()
2393
+ if type(dump) is str:
2394
+ dump = pk.load(open(dump, "rb"))
2395
+ cdef Module[double] cmod = cmod_from_dump_f64(dump)
2396
+ mod.cmod = cmod
2397
+ return mod
2398
+
2399
+
2400
+
2401
+ global PyModule_type, PySummand_type
2402
+ PyModule_type = Union[
2403
+ PyModule_i32,
2404
+ PyModule_i64,
2405
+ PyModule_f32,
2406
+ PyModule_f64,
2407
+ ]
2408
+ PySummand_type = Union[
2409
+ PySummand_i32,
2410
+ PySummand_i64,
2411
+ PySummand_f32,
2412
+ PySummand_f64,
2413
+ ]
2414
+
2415
+ PyBox_type = Union[
2416
+ PyBox_i32,
2417
+ PyBox_i64,
2418
+ PyBox_f32,
2419
+ PyBox_f64,
2420
+ ]
2421
+
2422
+
2423
+ PyMultiDiagram_type = Union[
2424
+ PyMultiDiagram_f32,
2425
+ PyMultiDiagram_f64,
2426
+ ]
2427
+
2428
+
2429
+ PyMultiDiagrams_type = Union[
2430
+ PyMultiDiagrams_f32,
2431
+ PyMultiDiagrams_f64,
2432
+ ]
2433
+