multipers 2.3.3b6__cp313-cp313-macosx_10_13_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of multipers might be problematic. Click here for more details.

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