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