Persistence_on_rectangle.h
1/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT.
2 * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details.
3 * Author(s): Marc Glisse
4 *
5 * Copyright (C) 2023 Inria
6 *
7 * Modification(s):
8 * - YYYY/MM Author: Description of the modification
9 */
10
11// ds_find_set_ is inspired from code in Boost.Graph that is
12//
13// (C) Copyright Jeremy Siek 2004
14// Distributed under the Boost Software License, Version 1.0. (See
15// accompanying file LICENSE_1_0.txt or copy at
16// http://www.boost.org/LICENSE_1_0.txt)
17
18
19#ifndef PERSISTENCE_ON_RECTANGLE_H
20#define PERSISTENCE_ON_RECTANGLE_H
21
22#include <gudhi/Debug_utils.h>
23#ifdef GUDHI_DETAILED_TIMES
24 #include <gudhi/Clock.h>
25#endif
26
27#include <boost/range/adaptor/reversed.hpp>
28
29#ifdef GUDHI_USE_TBB
30 #include <tbb/parallel_sort.h>
31#endif
32
33#ifdef DEBUG_TRACES
34 #include <iostream>
35#endif
36#include <vector>
37#include <memory>
38#include <algorithm>
39#include <stdexcept>
40#include <cstddef>
41
42namespace Gudhi::cubical_complex {
43
44// When building a cubical complex from top-dimensional cells, there are
45// normally more vertices than input top cells ((x+1)*(y+1) instead of x*y).
46// However, the top cells along the boundary turn out to be collapsible, and we
47// only need to work with (x-1)*(y-1) vertices.
48template <class Filtration_value, class Index = std::size_t, bool output_index = false>
49struct Persistence_on_rectangle {
50 // If we want to save space, we don't have to store the redundant 'first'
51 // field in T_with_index. However, it would slow down the primal pass.
52 struct T_with_index {
53 Filtration_value first; Index second;
54 T_with_index() = default;
55 T_with_index(Filtration_value f, Index i) : first(f), second(i) {}
56 bool operator<(T_with_index const& other) const {
57 return std::tie(first, second) < std::tie(other.first, other.second);
58 }
59 Index out() const { return second; }
60 };
61 // Don't store the index if we don't want to output it.
62 struct T_no_index {
63 Filtration_value first;
64 T_no_index() = default;
65 T_no_index(Filtration_value f, Index) : first(f) {}
66 bool operator<(T_no_index const& other) const { return first < other.first; }
67 Filtration_value out() const { return first; }
68 };
69 typedef std::conditional_t<output_index, T_with_index, T_no_index> T;
70
71 Filtration_value const* input_p;
72 Filtration_value input(Index i) const { return input_p[i]; }
73
74 // size_* counts the number of vertices in each direction.
75 Index size_x, size_y, input_size;
76 // The square i + dy is right above i.
77 Index dy;
78
79 // Squares keep their index from the input.
80 // Vertices have the same index as the square at their bottom left (smaller x and y)
81 // Store the filtration value of vertices that could be critical. We could store them
82 // in some map, or recompute them on demand, but this strongly affects performance.
83 std::unique_ptr<T[]> data_v_;
84 T& data_vertex(Index i){ return data_v_[i]; }
85 T data_vertex(Index i) const { return data_v_[i]; }
86
87 std::conditional_t<output_index, Index, Filtration_value> global_min;
88
89 // Information on a cluster
90 // We do not use the rank/size heuristics, they do not go well with the pre-pairing and end up slowing things down.
91 // We thus use the same representative for disjoint-sets and persistence (the minimum).
92 std::unique_ptr<Index[]> ds_parent_v_;
93 std::vector<Index> ds_parent_s_;
94 Index& ds_parent_vertex(Index n) { return ds_parent_v_[n]; }
95 Index& ds_parent_square(Index n) { return ds_parent_s_[n]; }
96
97 template<class Parent>
98 Index ds_find_set_(Index v, Parent&&ds_parent) {
99 // Experimentally, path halving is currently the fastest. Note that with a
100 // different algorithm, full compression was faster, so make sure to check
101 // again if the algorithm changes.
102 // (the setting is unusual because we start from a forest with broken ranks)
103#if 0
104 // Full compression
105 Index old = v;
106 Index ancestor = ds_parent(v);
107 while (ancestor != v)
108 {
109 v = ancestor;
110 ancestor = ds_parent(v);
111 }
112 v = ds_parent(old);
113 while (ancestor != v)
114 {
115 ds_parent(old) = ancestor;
116 old = v;
117 v = ds_parent(old);
118 }
119 return ancestor;
120#elif 1
121 // Path halving
122 Index parent = ds_parent(v);
123 Index grandparent = ds_parent(parent);
124 while (parent != grandparent)
125 {
126 ds_parent(v) = grandparent;
127 v = grandparent;
128 parent = ds_parent(v);
129 grandparent = ds_parent(parent);
130 }
131 return parent;
132#elif 1
133 // Path splitting
134 Index parent = ds_parent(v);
135 Index grandparent = ds_parent(parent);
136 while (parent != grandparent)
137 {
138 ds_parent(v) = grandparent;
139 v = parent;
140 parent = grandparent;
141 grandparent = ds_parent(parent);
142 }
143 return parent;
144#elif 1
145 // No compression (just for reference)
146 Index parent;
147 while (v != (parent = ds_parent(v)))
148 v = parent;
149 return v;
150#endif
151 }
152 Index ds_find_set_vertex(Index v) {
153 return ds_find_set_(v, [this](Index i) -> Index& { return ds_parent_vertex(i); });
154 }
155 Index ds_find_set_square(Index v) {
156 return ds_find_set_(v, [this](Index i) -> Index& { return ds_parent_square(i); });
157 }
158
159 struct Edge {
160 T f;
161 Index v1, v2; // v1 < v2
162 Edge() = default;
163 Edge(T f, Index v1, Index v2) : f(f), v1(v1), v2(v2) {}
164 Filtration_value filt() const { return f.first; }
165 bool operator<(Edge const& other) const { return filt() < other.filt(); }
166 };
167 void dualize_edge(Edge& e) const {
168 Index new_v2 = e.v1 + (dy + 1);
169 e.v1 = e.v2;
170 e.v2 = new_v2;
171 };
172 std::vector<Edge> edges;
173
174 void init(const Filtration_value* input_, Index n_rows, Index n_cols) {
175 input_size = n_rows * n_cols;
176 input_p = input_;
177#ifdef DEBUG_TRACES
178 std::clog << "Input\n";
179 for(Index i = 0; i < input_size; ++i) {
180 std::clog << i << '\t' << input(i) << '\n';
181 }
182#endif
183 dy = n_cols;
184 size_x = dy - 1;
185 size_y = n_rows - 1;
186 // The unique_ptr could be std::vector, but the initialization is useless.
187 data_v_.reset(new T[input_size - dy - 1]); // 1 row/column less for vertices than squares
188 ds_parent_v_.reset(new Index[input_size - dy - 1]);
189 // Initializing the boundary squares to 0 is important, it represents the infinite exterior cell.
190 ds_parent_s_.resize(input_size);
191 // What is a good estimate here? For a random 1000x1000 input, we get ~311k edges. For a checkerboard, ~498k.
192 edges.reserve(input_size / 2);
193 }
194
195 bool has_larger_input(Index a, Index b, Filtration_value fb) const {
196 // Is passing fb useful, or would the compiler notice that it already has it available?
197 GUDHI_CHECK(a != b, std::logic_error("Bug in Gudhi: comparing a cell to itself"));
198 Filtration_value fa = input(a);
199 if (fb < fa) return true;
200 if (fa < fb) return false;
201 return a > b; // Arbitrary, but has to be consistent
202 }
203 void set_parent_vertex(Index child, Index parent) {
204 GUDHI_CHECK(child != parent, std::logic_error("Bug in Gudhi: use mark_*_critical instead of set_parent"));
205 ds_parent_vertex(child) = parent;
206 }
207 void set_parent_square(Index child, Index parent) {
208 GUDHI_CHECK(child != parent, std::logic_error("Bug in Gudhi: use mark_*_critical instead of set_parent"));
209 ds_parent_square(child) = parent;
210 }
211
212 // Locally pair simplices around each square.
213 // Work implicitly from input, only store the filtration value of critical vertices (squares are already in input).
214 // Store critical edges for later processing.
215 void fill_and_pair() {
216 Index i; // Index of the current square
217 Filtration_value f; // input(i)
218 auto mark_vertex_critical = [&](Index c) {
219 ds_parent_vertex(c) = c;
220 data_vertex(c) = T(f, i);
221 };
222 auto mark_square_critical = [&]() {
223 ds_parent_square(i) = i;
224 };
225 auto mark_edge_critical = [&](Index v1, Index v2) {
226 edges.emplace_back(T(f, i), v1, v2);
227 };
228 auto v_up_left = [&](){ return i - 1; };
229 auto v_up_right = [&](){ return i; };
230 auto v_down_left = [&](){ return i - dy - 1; };
231 auto v_down_right = [&](){ return i - dy; };
232 auto pair_square_up = [&](){ set_parent_square(i, i + dy); };
233 auto pair_square_down = [&](){ set_parent_square(i, i - dy); };
234 auto pair_square_left = [&](){ set_parent_square(i, i - 1); };
235 auto pair_square_right = [&](){ set_parent_square(i, i + 1); };
236
237 // Mark the corners as critical, it will be overwritten if not
238 i = 0; f = input(i);
239 mark_vertex_critical(v_up_right());
240 i = size_x; f = input(i);
241 mark_vertex_critical(v_up_left());
242 i = dy * size_y; f = input(i);
243 mark_vertex_critical(v_down_right());
244 i = size_x + dy * size_y; f = input(i);
245 mark_vertex_critical(v_down_left());
246
247 // Boundary nodes, 1st row
248 for(Index x = 1; x < size_x; ++x) {
249 i = x;
250 f = input(x);
251 if (has_larger_input(i + dy, i, f)) {
252 auto up_left = [&](){ return has_larger_input(i - 1, i, f) && has_larger_input(i + dy - 1, i, f); };
253 auto up_right = [&](){ return has_larger_input(i + 1, i, f) && has_larger_input(i + dy + 1, i, f); };
254 if (up_left()) {
255 set_parent_vertex(v_up_left(), v_up_right());
256 if (up_right()) mark_vertex_critical(v_up_right());
257 } else if (up_right()) {
258 set_parent_vertex(v_up_right(), v_up_left());
259 } else {
260 mark_edge_critical(v_up_left(), v_up_right());
261 }
262 }
263 }
264 // Internal rows
265 for(Index y = 1; y < size_y; ++y) {
266 // First column
267 {
268 i = y * dy;
269 f = input(i);
270 if (has_larger_input(i + 1, i, f)) {
271 auto down_right = [&](){ return has_larger_input(i - dy, i, f) && has_larger_input(i + 1 - dy, i, f); };
272 auto up_right = [&](){ return has_larger_input(i + dy, i, f) && has_larger_input(i + 1 + dy, i, f); };
273 if (down_right()) {
274 set_parent_vertex(v_down_right(), v_up_right());
275 if (up_right()) mark_vertex_critical(v_up_right());
276 } else if (up_right()) {
277 set_parent_vertex(v_up_right(), v_down_right());
278 } else {
279 mark_edge_critical(v_down_right(), v_up_right());
280 }
281 }
282 }
283 // Internal squares
284 for(Index x = 1; x < size_x; ++x) {
285 i = x + dy * y;
286 f = input(i);
287 // See what part of the boundary shares f
288 auto left = [&]() { return has_larger_input(i - 1, i, f); };
289 auto right = [&]() { return has_larger_input(i + 1, i, f); };
290 auto down = [&]() { return has_larger_input(i - dy, i, f); };
291 auto up = [&]() { return has_larger_input(i + dy, i, f); };
292 auto down_left = [&]() { return has_larger_input(i - dy - 1, i, f); };
293 auto up_left = [&]() { return has_larger_input(i + dy - 1, i, f); };
294 auto down_right = [&]() { return has_larger_input(i - dy + 1, i, f); };
295 auto up_right = [&]() { return has_larger_input(i + dy + 1, i, f); };
296 if (up()) { // u
297 if (left()) { // u l
298 if (up_left()) { // u l ul
299 set_parent_vertex(v_up_left(), v_up_right());
300 if (down()) { // U l UL d
301 if (down_left()) { // U l UL d dl
302 set_parent_vertex(v_down_left(), v_up_left());
303 if (right()) { // U L UL d DL r
304 if (down_right()) { // U L UL d DL r dr
305 set_parent_vertex(v_down_right(), v_down_left());
306 pair_square_right();
307 if (up_right()) { // U L UL D DL R DR ur - cr
308 mark_vertex_critical(v_up_right());
309 }
310 } else { // U L UL d DL r !dr
311 pair_square_down();
312 if (up_right()) { // U L UL D DL r !dr ur - cd
313 set_parent_vertex(v_up_right(), v_down_right());
314 } else { // U L UL D DL r !dr !ur - cd
315 mark_edge_critical(v_down_right(), v_up_right());
316 }
317 }
318 } else { // U L UL d DL !r
319 pair_square_down();
320 }
321 } else { // U l UL d !dl
322 pair_square_left();
323 if (right()) { // U L UL d !dl r - cl
324 if (down_right()) { // U L UL d !dl r dr - cl
325 set_parent_vertex(v_down_right(), v_down_left());
326 } else { // U L UL d !dl r !dr - cl
327 mark_edge_critical(v_down_left(), v_down_right());
328 }
329 if (up_right()) { // U L UL D !dl r ur - cl
330 set_parent_vertex(v_up_right(), v_down_right());
331 } else { // U L UL D !dl r !ur - cl
332 mark_edge_critical(v_down_right(), v_up_right());
333 }
334 } else { // U L UL d !dl !r - cl
335 mark_edge_critical(v_down_left(), v_down_right());
336 }
337 }
338 } else { // U l UL !d
339 pair_square_left();
340 if (right()) { // U L UL !d r - cl
341 if (up_right()) { // U L UL !d r ur - cl
342 set_parent_vertex(v_up_right(), v_down_right());
343 } else { // U L UL !d r !ur - cl
344 mark_edge_critical(v_down_right(), v_up_right());
345 }
346 } else {} // U L UL !d !r - cl
347 }
348 } else { // u l !ul
349 pair_square_up();
350 if (down()) { // U l !ul d - cu
351 if (down_left()) { // U l !ul d dl - cu
352 set_parent_vertex(v_down_left(), v_up_left());
353 } else { // U l !ul d !dl - cu
354 mark_edge_critical(v_down_left(), v_up_left());
355 }
356 if (right()) { // U L !ul d r - cu
357 if (down_right()) { // U L !ul d r dr - cu
358 set_parent_vertex(v_down_right(), v_down_left());
359 } else { // U L !ul d r !dr - cu
360 mark_edge_critical(v_down_left(), v_down_right());
361 }
362 if (up_right()) { // U L !ul D r ur - cu
363 set_parent_vertex(v_up_right(), v_down_right());
364 } else { // U L !ul D r !ur - cu
365 mark_edge_critical(v_down_right(), v_up_right());
366 }
367 } else { // U L !ul d !r - cu
368 mark_edge_critical(v_down_left(), v_down_right());
369 }
370 } else { // U l !ul !d - cu
371 mark_edge_critical(v_down_left(), v_up_left());
372 if (right()) { // U L !ul !d r - cu
373 if (up_right()) { // U L !ul !d r ur - cu
374 set_parent_vertex(v_up_right(), v_down_right());
375 } else { // U L !ul !d r !ur - cu
376 mark_edge_critical(v_down_right(), v_up_right());
377 }
378 } else {} // U L !ul !d !r - cu
379 }
380 }
381 } else { // u !l
382 pair_square_up();
383 if (down()) { // U !l d - cu
384 if (right()) { // U !l d r - cu
385 if (down_right()) { // U !l d r dr - cu
386 set_parent_vertex(v_down_right(), v_down_left());
387 } else { // U !l d r !dr - cu
388 mark_edge_critical(v_down_left(), v_down_right());
389 }
390 if (up_right()) { // U !l D r ur - cu
391 set_parent_vertex(v_up_right(), v_down_right());
392 } else { // U !l D r !ur - cu
393 mark_edge_critical(v_down_right(), v_up_right());
394 }
395 } else { // U !l d !r - cu
396 mark_edge_critical(v_down_left(), v_down_right());
397 }
398 } else { // U !l !d - cu
399 if (right()) { // U !l !d r - cu
400 if (up_right()) { // U !l !d r ur - cu
401 set_parent_vertex(v_up_right(), v_down_right());
402 } else { // U !l !d r !ur - cu
403 mark_edge_critical(v_down_right(), v_up_right());
404 }
405 } else {} // U !l !d !r - cu
406 }
407 }
408 } else { // !u
409 if (left()) { // !u l
410 if (down()) { // !u l d
411 if (down_left()) { // !u l d dl
412 set_parent_vertex(v_down_left(), v_up_left());
413 if (right()) { // !u L d DL r
414 if (down_right()) { // !u L d DL r dr
415 set_parent_vertex(v_down_right(), v_down_left());
416 } else { // !u L d DL r !dr
417 mark_edge_critical(v_down_left(), v_down_right());
418 }
419 pair_square_right();
420 } else { // !u L d DL !r
421 pair_square_down();
422 }
423 } else { // !u l d !dl
424 pair_square_left();
425 if (right()) { // !u L d !dl r - cl
426 if (down_right()) { // !u L d !dl r dr - cl
427 set_parent_vertex(v_down_right(), v_down_left());
428 } else { // !u L d !dl r !dr - cl
429 mark_edge_critical(v_down_left(), v_down_right());
430 }
431 mark_edge_critical(v_down_right(), v_up_right());
432 } else { // !u L d !dl !r - cl
433 mark_edge_critical(v_down_left(), v_down_right());
434 }
435 }
436 } else { // !u l !d
437 pair_square_left();
438 if (right()) { // !u L !d r - cl
439 mark_edge_critical(v_down_right(), v_up_right());
440 } else {} // !u L !d !r - cl
441 }
442 } else { // !u !l
443 if (down()) { // !u !l d
444 pair_square_down();
445 if (right()) { // !u !l D r - cd
446 if (down_right()) { // !u !l D r dr - cd
447 set_parent_vertex(v_down_right(), v_up_right());
448 } else { // !u !l D r !dr - cd
449 mark_edge_critical(v_down_right(), v_up_right());
450 }
451 } else {} // !u !l D !r - cd
452 } else { // !u !l !d
453 if (right()) { // !u !l !d r
454 pair_square_right();
455 } else { // !u !l !d !r
456 mark_square_critical();
457 }
458 }
459 }
460 }
461 }
462 // Last column
463 {
464 i = size_x + dy * y;
465 f = input(i);
466 if (has_larger_input(i - 1, i, f)) {
467 auto down_left = [&](){ return has_larger_input(i - dy, i, f) && has_larger_input(i - 1 - dy, i, f); };
468 auto up_left = [&](){ return has_larger_input(i + dy, i, f) && has_larger_input(i - 1 + dy, i, f); };
469 if (down_left()) {
470 set_parent_vertex(v_down_left(), v_up_left());
471 if (up_left()) mark_vertex_critical(v_up_left());
472 } else if (up_left()) {
473 set_parent_vertex(v_up_left(), v_down_left());
474 } else {
475 mark_edge_critical(v_down_left(), v_up_left());
476 }
477 }
478 }
479 }
480 // Boundary nodes, last row
481 for(Index x = 1; x < size_x; ++x) {
482 i = size_y * dy + x;
483 f = input(i);
484 if (has_larger_input(i - dy, i, f)) {
485 auto down_left = [&](){ return has_larger_input(i - 1, i, f) && has_larger_input(i - dy - 1, i, f); };
486 auto down_right = [&](){ return has_larger_input(i + 1, i, f) && has_larger_input(i - dy + 1, i, f); };
487 if (down_left()) {
488 set_parent_vertex(v_down_left(), v_down_right());
489 if (down_right()) mark_vertex_critical(v_down_right());
490 } else if (down_right()) {
491 set_parent_vertex(v_down_right(), v_down_left());
492 } else {
493 mark_edge_critical(v_down_left(), v_down_right());
494 }
495 }
496 }
497 }
498
499 void sort_edges(){
500#ifdef GUDHI_USE_TBB
501 // Parallelizing just this part is a joke. It would be possible to
502 // parallelize the pairing (one edge list per thread) and run the dual in
503 // parallel with the primal if we were motivated...
504 tbb::parallel_sort(edges.begin(), edges.end());
505#else
506 std::sort(edges.begin(), edges.end());
507#endif
508#ifdef DEBUG_TRACES
509 std::clog << "edges\n";
510 for(auto&e : edges){ std::clog << e.v1 << '\t' << e.v2 << '\t' << e.filt() << '\n'; }
511#endif
512 }
513
514 template<class Out>
515 void primal(Out&&out){
516 auto it = std::remove_if(edges.begin(), edges.end(), [&](Edge& e) {
517 assert(e.v1 < e.v2);
518 Index a = ds_find_set_vertex(e.v1);
519 Index b = ds_find_set_vertex(e.v2);
520 if (a == b) return false;
521 if (data_vertex(b) < data_vertex(a)) std::swap(a, b);
522 ds_parent_vertex(b) = a;
523 out(data_vertex(b).out(), e.f.out());
524 return true;
525 });
526 edges.erase(it, edges.end());
527 global_min = data_vertex(ds_find_set_vertex(0)).out();
528 }
529
530 // In the dual, squares behave like vertices, and edges are rotated 90° around their middle.
531 // To handle boundaries correctly, we imagine a single exterior cell with filtration +inf.
532 template<class Out>
533 void dual(Out&&out){
534 for (auto e : boost::adaptors::reverse(edges)) {
535 dualize_edge(e);
536 Index a = ds_find_set_square(e.v1);
537 Index b = ds_find_set_square(e.v2);
538 GUDHI_CHECK(a != b, std::logic_error("Bug in Gudhi"));
539 // This is more robust in case the input contains inf? I used to set the filtration of 0 to inf.
540 if (b == 0 || (a != 0 && input(a) < input(b))) std::swap(a, b);
541 ds_parent_square(b) = a;
542 if constexpr (output_index)
543 out(e.f.out(), b);
544 else
545 out(e.f.out(), input(b));
546 }
547 }
548};
549// Ideas for improvement:
550// * for large hard (many intervals) inputs, primal/dual dominate the running time because of the random reads in
551// find_set and input(a/b). The input load would be cheaper if we stored it with parents, but then find_set would
552// be slower.
553// * to increase memory locality, maybe pairing, which is currently arbitrary, could use some heuristic to favor
554// some pairs over others.
555// * try to loosen tight dependency chains, load values several instructions before they are needed. Performing 2
556// find_set in lock step surprisingly doesn't help.
557// * To handle very big instances, we could remove data_v_ and recompute it on demand as the min of the inputs of i,
558// i+1, i+dy and i+dy+1. On hard instances, it wouldn't save that much memory (and it is a bit slower). On easy
559// instances, if we also remove the calls to reserve(), the saving is less negligible, but we still have ds_parent_*_
560// that take about as much space as the input. We could, on a subarray, fill a dense ds_parent, then reduce it and
561// export only the critical vertices and boundary to some sparse datastructure, but it doesn't seem worth the trouble
562// for now.
563
584template <bool output_index = false, typename Filtration_value, typename Index, typename Out0, typename Out1>
585auto persistence_on_rectangle_from_top_cells(Filtration_value const* input, Index n_rows, Index n_cols,
586 Out0&&out0, Out1&&out1){
587#ifdef GUDHI_DETAILED_TIMES
588 Gudhi::Clock clock;
589#endif
590 GUDHI_CHECK(n_rows >= 2 && n_cols >= 2,
591 std::domain_error("The complex must truly be 2d, i.e. at least 2 rows and 2 columns"));
592 Persistence_on_rectangle<Filtration_value, Index, output_index> X;
593 X.init(input, n_rows, n_cols);
594#ifdef GUDHI_DETAILED_TIMES
595 std::clog << "init: " << clock; clock.begin();
596#endif
597 X.fill_and_pair();
598#ifdef GUDHI_DETAILED_TIMES
599 std::clog << "fill and pair: " << clock; clock.begin();
600#endif
601 X.sort_edges();
602#ifdef GUDHI_DETAILED_TIMES
603 std::clog << "sort: " << clock; clock.begin();
604#endif
605 X.primal(out0);
606#ifdef GUDHI_DETAILED_TIMES
607 std::clog << "primal pass: " << clock; clock.begin();
608#endif
609 X.dual(out1);
610#ifdef GUDHI_DETAILED_TIMES
611 std::clog << "dual pass: " << clock;
612#endif
613 return X.global_min;
614}
615} // namespace Gudhi::cubical_complex
616
617#endif // PERSISTENCE_ON_RECTANGLE_H
Value type for a filtration function on a cell complex.
Definition: FiltrationValue.h:20