/* This file is part of the Gudhi Library - https://gudhi.inria.fr/ - which is released under MIT. * See file LICENSE or go to https://gudhi.inria.fr/licensing/ for full license details. * Author(s): Siddharth Pritam, Marc Glisse * * Copyright (C) 2020 Inria * * Modification(s): * - 2020/03 Vincent Rouvreau: integration to the gudhi library * - 2021 Marc Glisse: complete rewrite * - YYYY/MM Author: Description of the modification */ #ifndef FLAG_COMPLEX_EDGE_COLLAPSER_H_ #define FLAG_COMPLEX_EDGE_COLLAPSER_H_ #include #include #include #ifdef GUDHI_USE_TBB #include #endif #include #include #include #include #include namespace Gudhi { namespace collapse { /** \private * * \brief Flag complex sparse matrix data structure. * * \tparam Vertex type must be an integer type. * \tparam Filtration type for the value of the filtration function. */ template struct Flag_complex_edge_collapser { using Filtered_edge = std::tuple; typedef std::pair Edge; struct Cmpi { template bool operator()(T const&a, U const&b)const{return b Ngb_list; typedef std::vector Neighbors; Neighbors neighbors; // closed neighborhood std::size_t num_vertices; std::vector> res; #ifdef GUDHI_COLLAPSE_USE_DENSE_ARRAY // Minimal matrix interface // Using this matrix generally helps performance, but the memory use may be excessive for a very sparse graph // (and in extreme cases the constant initialization of the matrix may start to dominate the runnning time). // Are there cases where the matrix is too big but a hash table would help? std::vector neighbors_data; void init_neighbors_dense(){ neighbors_data.clear(); neighbors_data.resize(num_vertices*num_vertices, std::numeric_limits::infinity()); } Filtration_value& neighbors_dense(Vertex i, Vertex j){return neighbors_data[num_vertices*j+i];} #endif // This does not touch the events list, only the adjacency matrix(es) void delay_neighbor(Vertex u, Vertex v, Filtration_value f) { neighbors[u][v]=f; neighbors[v][u]=f; #ifdef GUDHI_COLLAPSE_USE_DENSE_ARRAY neighbors_dense(u,v)=f; neighbors_dense(v,u)=f; #endif } void remove_neighbor(Vertex u, Vertex v) { neighbors[u].erase(v); neighbors[v].erase(u); #ifdef GUDHI_COLLAPSE_USE_DENSE_ARRAY neighbors_dense(u,v)=std::numeric_limits::infinity(); neighbors_dense(v,u)=std::numeric_limits::infinity(); #endif } template void read_edges(FilteredEdgeRange const&r){ neighbors.resize(num_vertices); #ifdef GUDHI_COLLAPSE_USE_DENSE_ARRAY init_neighbors_dense(); #endif // Use the raw sequence to avoid maintaining the order std::vector neighbors_seq(num_vertices); for(auto&&e : r){ using std::get; Vertex u = get<0>(e); Vertex v = get<1>(e); Filtration_value f = get<2>(e); neighbors_seq[u].emplace_back(v, f); neighbors_seq[v].emplace_back(u, f); #ifdef GUDHI_COLLAPSE_USE_DENSE_ARRAY neighbors_dense(u,v)=f; neighbors_dense(v,u)=f; #endif } for(std::size_t i=0;i::infinity()); neighbors[i].adopt_sequence(std::move(neighbors_seq[i])); // calls sort #ifdef GUDHI_COLLAPSE_USE_DENSE_ARRAY neighbors_dense(i,i)=-std::numeric_limits::infinity(); #endif } } // Open neighborhood // At some point it helped gcc to add __attribute__((noinline)) here, otherwise we had +50% on the running time // on one example. It looks ok now, or I forgot which example that was. void common_neighbors(boost::container::flat_set& e_ngb, std::vector>& e_ngb_later, Vertex u, Vertex v, Filtration_value f_event){ // Using neighbors_dense here seems to hurt, even if we loop on the smaller of nu and nv. Ngb_list const&nu = neighbors[u]; Ngb_list const&nv = neighbors[v]; auto ui = nu.begin(); auto ue = nu.end(); auto vi = nv.begin(); auto ve = nv.end(); assert(ui != ue && vi != ve); while(ui != ue && vi != ve){ Vertex w = ui->first; if(w < vi->first) { ++ui; continue; } if(w > vi->first) { ++vi; continue; } // nu and nv are closed, so we need to exclude e here. if(w != u && w != v) { Filtration_value f = std::max(ui->second, vi->second); if(f > f_event) e_ngb_later.emplace_back(f, w); else e_ngb.insert(e_ngb.end(), w); } ++ui; ++vi; } } // Test if the neighborhood of e is included in the closed neighborhood of c template bool is_dominated_by(Ngb const& e_ngb, Vertex c, Filtration_value f){ // The best strategy probably depends on the distribution, how sparse / dense the adjacency matrix is, // how (un)balanced the sizes of e_ngb and nc are. // Some efficient operations on sets work best with bitsets, although the need for a map complicates things. #ifdef GUDHI_COLLAPSE_USE_DENSE_ARRAY for(auto v : e_ngb) { // if(v==c)continue; if(neighbors_dense(v,c) > f) return false; } return true; #else auto&&nc = neighbors[c]; // if few neighbors, use dichotomy? Seems slower. // I tried storing a copy of neighbors as a vector and using it for nc, but it was // a bit slower here. It did help with neighbors[dominator].find(w) in the main function though, // sometimes enough, sometimes not. auto ci = nc.begin(); auto ce = nc.end(); auto eni = e_ngb.begin(); auto ene = e_ngb.end(); assert(eni != ene); assert(ci != ce); // if(*eni == c && ++eni == ene) return true; for(;;){ Vertex ve = *eni; Vertex vc = ci->first; while(ve > vc) { // try a gallop strategy (exponential search)? Seems slower if(++ci == ce) return false; vc = ci->first; } if(ve < vc) return false; // ve == vc if(ci->second > f) return false; if(++eni == ene)return true; // If we stored an open neighborhood of c (excluding c), we would need to test for c here and before the loop // if(*eni == c && ++eni == ene)return true; if(++ci == ce) return false; } #endif } template void process_edges(FilteredEdgeRange const& edges, Delay&& delay) { { Vertex maxi = 0, maxj = 0; for(auto& fe : edges) { Vertex i = std::get<0>(fe); Vertex j = std::get<1>(fe); if (i > maxi) maxi = i; if (j > maxj) maxj = j; } num_vertices = std::max(maxi, maxj) + 1; } read_edges(edges); boost::container::flat_set e_ngb; e_ngb.reserve(num_vertices); std::vector> e_ngb_later; for(auto&e:edges) { { Vertex u = std::get<0>(e); Vertex v = std::get<1>(e); Filtration_value input_time = std::get<2>(e); auto time = delay(input_time); auto start_time = time; e_ngb.clear(); e_ngb_later.clear(); common_neighbors(e_ngb, e_ngb_later, u, v, time); // If we identify a good candidate (the first common neighbor) for being a dominator of e until infinity, // we could check that a bit more cheaply. It does not seem to help though. auto cmp1=[](auto const&a, auto const&b){return a.first > b.first;}; auto e_ngb_later_begin=e_ngb_later.begin(); auto e_ngb_later_end=e_ngb_later.end(); bool heapified = false; bool dead = false; while(true) { Vertex dominator = -1; // special case for size 1 // if(e_ngb.size()==1){dominator=*e_ngb.begin();}else // It is tempting to test the dominators in increasing order of filtration value, which is likely to reduce // the number of calls to is_dominated_by before finding a dominator, but sorting, even partially / lazily, // is very expensive. for(auto c : e_ngb){ if(is_dominated_by(e_ngb, c, time)){ dominator = c; break; } } if(dominator==-1) break; // Push as long as dominator remains a dominator. // Iterate on times where at least one neighbor appears. for (bool still_dominated = true; still_dominated; ) { if(e_ngb_later_begin == e_ngb_later_end) { dead = true; goto end_move; } if(!heapified) { // Eagerly sorting can be slow std::make_heap(e_ngb_later_begin, e_ngb_later_end, cmp1); heapified=true; } time = e_ngb_later_begin->first; // first place it may become critical // Update the neighborhood for this new time, while checking if any of the new neighbors break domination. while (e_ngb_later_begin != e_ngb_later_end && e_ngb_later_begin->first <= time) { Vertex w = e_ngb_later_begin->second; #ifdef GUDHI_COLLAPSE_USE_DENSE_ARRAY if (neighbors_dense(dominator,w) > e_ngb_later_begin->first) still_dominated = false; #else auto& ngb_dom = neighbors[dominator]; auto wit = ngb_dom.find(w); // neighborhood may be open or closed, it does not matter if (wit == ngb_dom.end() || wit->second > e_ngb_later_begin->first) still_dominated = false; #endif e_ngb.insert(w); std::pop_heap(e_ngb_later_begin, e_ngb_later_end--, cmp1); } } // this doesn't seem to help that much... } end_move: if(dead) { remove_neighbor(u, v); } else if(start_time != time) { delay_neighbor(u, v, time); res.emplace_back(u, v, time); } else { res.emplace_back(u, v, input_time); } } } } std::vector output() { return std::move(res); } }; template R to_range(R&& r) { return std::move(r); } template R to_range(T&& t) { R r; r.insert(r.end(), t.begin(), t.end()); return r; } template auto flag_complex_collapse_edges(FilteredEdgeRange&& edges, Delay&&delay) { // Would it help to label the points according to some spatial sorting? auto first_edge_itr = std::begin(edges); using Vertex = std::decay_t(*first_edge_itr))>; using Filtration_value = std::decay_t(*first_edge_itr))>; using Edge_collapser = Flag_complex_edge_collapser; if (first_edge_itr != std::end(edges)) { auto edges2 = to_range>(std::forward(edges)); #ifdef GUDHI_USE_TBB // I think this sorting is always negligible compared to the collapse, but parallelizing it shouldn't hurt. tbb::parallel_sort(edges2.begin(), edges2.end(), [](auto const&a, auto const&b){return std::get<2>(a)>std::get<2>(b);}); #else std::sort(edges2.begin(), edges2.end(), [](auto const&a, auto const&b){return std::get<2>(a)>std::get<2>(b);}); #endif Edge_collapser edge_collapser; edge_collapser.process_edges(edges2, std::forward(delay)); return edge_collapser.output(); } return std::vector(); } /** \brief Implicitly constructs a flag complex from edges as an input, collapses edges while preserving the persistent * homology and returns the remaining edges as a range. The filtration value of vertices is irrelevant to this function. * * \param[in] edges Range of Filtered edges. There is no need for the range to be sorted, as it will be done internally. * * \tparam FilteredEdgeRange Range of `std::tuple` * where `Vertex_handle` is the type of a vertex index. * * \return Remaining edges after collapse as a range of * `std::tuple`. * * \ingroup edge_collapse * * \note * Advanced: Defining the macro GUDHI_COLLAPSE_USE_DENSE_ARRAY tells gudhi to allocate a square table of size the * maximum vertex index. This usually speeds up the computation for dense graphs. However, for sparse graphs, the memory * use may be problematic and initializing this large table may be slow. */ template auto flag_complex_collapse_edges(const FilteredEdgeRange& edges) { return flag_complex_collapse_edges(edges, [](auto const&d){return d;}); } } // namespace collapse } // namespace Gudhi #endif // FLAG_COMPLEX_EDGE_COLLAPSER_H_