https://github.com/mglisse/gudhi-devel
Tip revision: e495c4de49a9a226359fbf21966f4ebc4b3fc31b authored by Marc Glisse on 30 March 2022, 17:20:01 UTC
Reintroduce parallel_sort
Reintroduce parallel_sort
Tip revision: e495c4d
/* 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 <gudhi/Debug_utils.h>
#include <boost/container/flat_map.hpp>
#include <boost/container/flat_set.hpp>
#ifdef GUDHI_USE_TBB
#include <tbb/parallel_sort.h>
#endif
#include <utility>
#include <vector>
#include <tuple>
#include <algorithm>
#include <limits>
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<typename Vertex, typename Filtration_value>
struct Flag_complex_edge_collapser {
using Filtered_edge = std::tuple<Vertex, Vertex, Filtration_value>;
typedef std::pair<Vertex,Vertex> Edge;
struct Cmpi { template<class T, class U> bool operator()(T const&a, U const&b)const{return b<a; } };
typedef boost::container::flat_map<Vertex, Filtration_value> Ngb_list;
typedef std::vector<Ngb_list> Neighbors;
Neighbors neighbors; // closed neighborhood
std::size_t num_vertices;
std::vector<std::tuple<Vertex, Vertex, Filtration_value>> 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<Filtration_value> neighbors_data;
void init_neighbors_dense(){
neighbors_data.clear();
neighbors_data.resize(num_vertices*num_vertices, std::numeric_limits<Filtration_value>::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<Filtration_value>::infinity();
neighbors_dense(v,u)=std::numeric_limits<Filtration_value>::infinity();
#endif
}
template<class FilteredEdgeRange>
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<typename Ngb_list::sequence_type> 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<neighbors_seq.size();++i){
neighbors_seq[i].emplace_back(i, -std::numeric_limits<Filtration_value>::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<Filtration_value>::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<Vertex>& e_ngb,
std::vector<std::pair<Filtration_value, Vertex>>& 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<class Ngb>
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<absl::flat_hash_map> 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<class FilteredEdgeRange, class Delay>
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<Vertex> e_ngb;
e_ngb.reserve(num_vertices);
std::vector<std::pair<Filtration_value, Vertex>> 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<Filtered_edge> output() {
return std::move(res);
}
};
template<class R> R to_range(R&& r) { return std::move(r); }
template<class R, class T> R to_range(T&& t) { R r; r.insert(r.end(), t.begin(), t.end()); return r; }
template<class FilteredEdgeRange, class Delay>
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<decltype(std::get<0>(*first_edge_itr))>;
using Filtration_value = std::decay_t<decltype(std::get<2>(*first_edge_itr))>;
using Edge_collapser = Flag_complex_edge_collapser<Vertex, Filtration_value>;
if (first_edge_itr != std::end(edges)) {
auto edges2 = to_range<std::vector<typename Edge_collapser::Filtered_edge>>(std::forward<FilteredEdgeRange>(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>(delay));
return edge_collapser.output();
}
return std::vector<typename Edge_collapser::Filtered_edge>();
}
/** \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<Vertex_handle, Vertex_handle, Filtration_value>`
* where `Vertex_handle` is the type of a vertex index.
*
* \return Remaining edges after collapse as a range of
* `std::tuple<Vertex_handle, Vertex_handle, Filtration_value>`.
*
* \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<class FilteredEdgeRange> 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_