A toolkit for working with phylogenetic data.
v0.19.0
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
utils/math/kmeans.hpp
Go to the documentation of this file.
1 #ifndef GENESIS_UTILS_MATH_KMEANS_H_
2 #define GENESIS_UTILS_MATH_KMEANS_H_
3 
4 /*
5  Genesis - A toolkit for working with phylogenetic data.
6  Copyright (C) 2014-2018 Lucas Czech and HITS gGmbH
7 
8  This program is free software: you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation, either version 3 of the License, or
11  (at your option) any later version.
12 
13  This program is distributed in the hope that it will be useful,
14  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  GNU General Public License for more details.
17 
18  You should have received a copy of the GNU General Public License
19  along with this program. If not, see <http://www.gnu.org/licenses/>.
20 
21  Contact:
22  Lucas Czech <lucas.czech@h-its.org>
23  Exelixis Lab, Heidelberg Institute for Theoretical Studies
24  Schloss-Wolfsbrunnenweg 35, D-69118 Heidelberg, Germany
25 */
26 
37 
38 #include <algorithm>
39 #include <cassert>
40 #include <cstddef>
41 #include <functional>
42 #include <limits>
43 #include <random>
44 #include <stdexcept>
45 #include <string>
46 #include <unordered_set>
47 #include <utility>
48 #include <vector>
49 
50 #ifdef GENESIS_OPENMP
51 # include <omp.h>
52 #endif
53 
54 namespace genesis {
55 namespace utils {
56 
57 // =================================================================================================
58 // Generic K-Means Class
59 // =================================================================================================
60 
64 template< typename Point >
65 class Kmeans
66 {
67 public:
68 
69  // -------------------------------------------------------------------------
70  // Typedefs and Constants
71  // -------------------------------------------------------------------------
72 
73  using value_type = Point;
74 
76  {
80  kNone
81  };
82 
88  {
89  std::vector<double> variances;
90  std::vector<size_t> counts;
91  std::vector<double> distances;
92  };
93 
94  // -------------------------------------------------------------------------
95  // Constructors and Rule of Five
96  // -------------------------------------------------------------------------
97 
98  Kmeans() = default;
99  virtual ~Kmeans() = default;
100 
101  Kmeans( Kmeans const& ) = default;
102  Kmeans( Kmeans&& ) = default;
103 
104  Kmeans& operator= ( Kmeans const& ) = default;
105  Kmeans& operator= ( Kmeans&& ) = default;
106 
107  // -------------------------------------------------------------------------
108  // Member Functions
109  // -------------------------------------------------------------------------
110 
111  size_t run( std::vector<Point> const& data, size_t const k )
112  {
113  // Run basic checks. This throws if necessary.
114  argument_checks_( data, k );
115 
116  // Init assigments and centroids.
117  initialize( data, k );
118 
119  // Call the hook.
120  pre_loop_hook( data, assignments_, centroids_ );
121 
122  // By now, the result vectors should be filled correctly.
123  // This replaces asserts. It is slightly more expensive, but this class offers so much
124  // expansion points and custom behaviour, that we better check thoroughly.
125  runtime_checks_( data, k );
126 
127  size_t iteration = 0;
128  bool changed_assigment;
129 
130  do {
131  // Start a new iteration.
132  LOG_INFO << "Iteration " << iteration;
133  changed_assigment = lloyd_step( data, assignments_, centroids_ );
134 
135  // Check again.
136  runtime_checks_( data, k );
137 
138  // Check if there are empty centroids, and if so, treat them.
139  auto const empty_centroids = get_empty_centroids_();
140  if( ! empty_centroids.empty() ) {
141  LOG_INFO << "Empty centroid occurred: " << empty_centroids.size();
142  changed_assigment |= treat_empty_centroids(
143  data, assignments_, centroids_, empty_centroids
144  );
145  }
146 
147  ++iteration;
148  } while( changed_assigment && iteration < max_iterations_ );
149 
150  // Call the hook.
151  post_loop_hook( data, assignments_, centroids_ );
152 
153  return iteration;
154  }
155 
156  // -------------------------------------------------------------------------
157  // Data Access
158  // -------------------------------------------------------------------------
159 
160  std::vector<size_t> const& assignments() const
161  {
162  return assignments_;
163  }
164 
165  Kmeans& assignments( std::vector<size_t> const& value )
166  {
167  assignments_ = value;
168  return *this;
169  }
170 
171  std::vector<Point> const& centroids() const
172  {
173  return centroids_;
174  }
175 
176  Kmeans& centroids( std::vector<Point> const& value )
177  {
178  centroids_ = value;
179  return *this;
180  }
181 
182  std::vector<size_t> cluster_sizes() const
183  {
184  if( assignments_.empty() || centroids_.empty() ) {
185  throw std::runtime_error( "No assignments or centroids set yet." );
186  }
187  auto result = std::vector<size_t>( centroids_.size(), 0 );
188  for( auto ass : assignments_ ) {
189  assert( ass <= result.size() );
190  ++result[ ass ];
191  }
192  return result;
193  }
194 
195  void clear()
196  {
197  assignments_.clear();
198  centroids_.clear();
199  }
200 
201  // -------------------------------------------------------------------------
202  // Properties
203  // -------------------------------------------------------------------------
204 
205  ClusteringInfo cluster_info(
206  std::vector<Point> const& data
207  ) const {
208  return cluster_info( data, assignments_, centroids_ );
209  }
210 
211  size_t max_iterations() const
212  {
213  return max_iterations_;
214  }
215 
216  Kmeans& max_iterations( size_t value )
217  {
218  if( value == 0 ) {
219  throw std::runtime_error( "Cannot use 0 as max_iterations for Kmeans." );
220  }
221  max_iterations_ = value;
222  return *this;
223  }
224 
226  {
227  return init_strategy_;
228  }
229 
231  {
232  init_strategy_ = value;
233  return *this;
234  }
235 
236  // -------------------------------------------------------------------------
237  // Virtual Functions
238  // -------------------------------------------------------------------------
239 
240 protected:
241 
242  virtual void initialize( std::vector<Point> const& data, size_t const k )
243  {
244  // Select init stragegies.
245  switch( init_strategy_ ) {
247  init_with_random_assignments_( data, k );
248  break;
249  }
251  init_with_random_centroids_( data, k );
252  break;
253  }
255  init_with_kmeans_plus_plus_( data, k );
256  break;
257  }
258  default: {}
259  }
260 
261  // If the strategy did not yield useful values, we still need to init somehow,
262  // so do this now. This also applies if kNone was selected for init, but no centroids were set.
263  if( assignments_.size() == 0 && centroids_.size() == 0 ) {
264  // Nothing given: Sample random centroids from the data.
265  init_with_random_centroids_( data, k );
266 
267  } else if( assignments_.size() == 0 && centroids_.size() > 0 ) {
268  // Centroids given, but no assigments: Nothing to do for now.
269  // We will calculate the proper assigments in the main loop.
270 
271  } else if( assignments_.size() > 0 && centroids_.size() == 0 ) {
272  // Assignments given, but not centroids: Caculate the latter.
273  update_centroids( data, assignments_, centroids_ );
274 
275  } else {
276  // Both given: Nothing to do.
277  assert( assignments_.size() > 0 && centroids_.size() > 0 );
278  }
279 
280  // If we do not have an assigment vector yet, make one. It will be assigned proper values
281  // once we enter the main loop.
282  if( assignments_.size() == 0 ) {
283  assignments_ = std::vector<size_t>( data.size(), 0 );
284  }
285  }
286 
287  virtual bool data_validation( std::vector<Point> const& data ) const
288  {
289  (void) data;
290  return true;
291  }
292 
293  virtual void pre_loop_hook(
294  std::vector<Point> const& data,
295  std::vector<size_t>& assignments,
296  std::vector<Point>& centroids
297  ) {
298  (void) data;
299  (void) assignments;
300  (void) centroids;
301  }
302 
303  virtual bool lloyd_step(
304  std::vector<Point> const& data,
305  std::vector<size_t>& assignments,
306  std::vector<Point>& centroids
307  ) {
308  // Calculate new assignments and check whether they changed.
309  auto changed_assigment = assign_to_centroids( data, centroids, assignments );
310 
311  // Recalculate the centroids.
312  update_centroids( data, assignments, centroids );
313 
314  return changed_assigment;
315  }
316 
317  virtual bool assign_to_centroids(
318  std::vector<Point> const& data,
319  std::vector<Point> const& centroids,
320  std::vector<size_t>& assignments
321  ) {
322  // Store whether anything changed.
323  bool changed_assigment = false;
324 
325  // Assign each Point to its nearest centroid.
326  #pragma omp parallel for
327  for( size_t i = 0; i < data.size(); ++i ) {
328  auto const new_idx = find_nearest_cluster( centroids, data[i] ).first;
329 
330  if( new_idx != assignments[i] ) {
331  // Update the assignment. No need for locking, as each thread works on its own i.
332  assignments[i] = new_idx;
333 
334  // If we have a new assigment for this datum, we need to do another loop iteration.
335  // Do this atomically, as all threads use this variable.
336  #pragma omp atomic write
337  changed_assigment = true;
338  }
339  }
340 
341  return changed_assigment;
342  }
343 
344  virtual std::pair<size_t, double> find_nearest_cluster(
345  std::vector<Point> const& centroids,
346  Point const& datum
347  ) const {
348  size_t min_i = std::numeric_limits<size_t>::max();
349  double min_d = std::numeric_limits<double>::max();
350 
351  assert( centroids.size() > 0 );
352  for( size_t i = 0; i < centroids.size(); ++i ) {
353  auto const dist = distance( datum, centroids[i] );
354  if( dist < min_d ) {
355  min_i = i;
356  min_d = dist;
357  }
358  }
359 
360  return { min_i, min_d };
361  }
362 
363  virtual ClusteringInfo cluster_info(
364  std::vector<Point> const& data,
365  std::vector<size_t> const& assignments,
366  std::vector<Point> const& centroids
367  ) const {
368  auto const k = centroids.size();
369 
370  auto result = ClusteringInfo();
371  result.variances = std::vector<double>( k, 0.0 );
372  result.counts = std::vector<size_t>( k, 0 );
373  result.distances = std::vector<double>( data.size(), 0.0 );
374 
375  // Work through the data and assigments and accumulate.
376  #pragma omp parallel for
377  for( size_t i = 0; i < data.size(); ++i ) {
378 
379  // Shorthands.
380  auto const a = assignments[ i ];
381  assert( a < k );
382  auto const& centroid = centroids[ a ];
383 
384  // Get dist from datum to centroid.
385  auto const dist = distance( centroid, data[ i ] );
386  result.distances[ i ] = dist;
387 
388  // Update centroid accumulators.
389  #pragma omp atomic update
390  result.variances[ a ] += dist * dist;
391  #pragma omp atomic update
392  ++result.counts[ a ];
393  }
394 
395  // Build the mean dist to get the variance for each centroid.
396  for( size_t i = 0; i < k; ++i ) {
397  if( result.counts[ i ] > 0 ) {
398  result.variances[ i ] /= result.counts[ i ];
399  }
400  }
401 
402  return result;
403  }
404 
405  virtual bool treat_empty_centroids(
406  std::vector<Point> const& data,
407  std::vector<size_t>& assignments,
408  std::vector<Point>& centroids,
409  std::unordered_set<size_t> const& empty_centroids
410  ) {
411  // If there are not empty centroids, we have nothing to do, and did not change anything,
412  // so return false.
413  if( empty_centroids.empty() ) {
414  return false;
415  }
416 
417  // Process all empty centroid indices.
418  for( auto const& ec_idx : empty_centroids ) {
419  // Get variances and counts of clusters and distances from data to them.
420  auto clus_info = cluster_info( data, assignments, centroids );
421  assert( clus_info.variances.size() == centroids.size() );
422  assert( clus_info.distances.size() == data.size() );
423  assert( data.size() == assignments.size() );
424 
425  // Get index of centroid with max variance.
426  auto const max_var_idx = static_cast<size_t>( std::distance(
427  clus_info.variances.begin(),
428  std::max_element( clus_info.variances.begin(), clus_info.variances.end() )
429  ));
430 
431  // If the max variance is 0, we cannot do anything. All points are the same.
432  if( clus_info.variances[ max_var_idx ] == 0.0 ) {
433  return false;
434  }
435 
436  // The empty centroid cannot be the same as the one we want to take a point from,
437  // because empty clusters have a variance of 0. If this assertion fails, we probably
438  // ran an analysis with k >> data.size() or so.
439  assert( ec_idx != max_var_idx );
440 
441  // The current empty cluster should actually be empty.
442  assert( clus_info.counts[ ec_idx ] == 0 );
443  assert( clus_info.variances[ ec_idx ] == 0.0 );
444 
445  // Find the point in the max var cluster that is furthest away from the centroid.
446  size_t furth_idx = std::numeric_limits<size_t>::max();
447  double furth_dist = std::numeric_limits<double>::lowest();
448  for( size_t i = 0; i < data.size(); ++i ) {
449  if( assignments[i] != max_var_idx ) {
450  continue;
451  }
452  if( clus_info.distances[ i ] > furth_dist ) {
453  furth_idx = i;
454  furth_dist = clus_info.distances[ i ];
455  }
456  }
457 
458  // The point needs to be part of the max var cluster, otherwise something went wrong.
459  assert( assignments[ furth_idx ] == max_var_idx );
460 
461  // Add the point to the empty cluster.
462  assignments[ furth_idx ] = ec_idx;
463 
464  // The following is some test code that could be used to avoid calculating the cluster
465  // info for each empty cluster. However, as we probably almost never run into this
466  // function anyway, we better keep it simple and calculate the info fresh every time.
467 
468  // // Adjust variance and count of the max var cluster.
469  // auto const dsqrt = clus_info.distances[ furth_idx ] * clus_info.distances[ furth_idx ];
470  // clus_info.variances[ max_var_idx ] *= clus_info.counts[ max_var_idx ];
471  // clus_info.variances[ max_var_idx ] -= dsqrt;
472  // --clus_info.counts[ max_var_idx ];
473  // clus_info.variances[ max_var_idx ] /= clus_info.counts[ max_var_idx ];
474  //
475  // // Adjust count of the (now not any longer) empty cluster.
476  // ++clus_info.counts[ ec_idx ];
477 
478  // Finally, we need to update the centroids in order to reflect the changes.
479  update_centroids( data, assignments, centroids );
480  }
481 
482  // Now we return true, because we changed some assignments.
483  return true;
484  }
485 
486  virtual double distance(
487  Point const& lhs,
488  Point const& rhs
489  ) const = 0;
490 
491  virtual void update_centroids(
492  std::vector<Point> const& data,
493  std::vector<size_t> const& assignments,
494  std::vector<Point>& centroids
495  ) = 0;
496 
497  virtual void post_loop_hook(
498  std::vector<Point> const& data,
499  std::vector<size_t>& assignments,
500  std::vector<Point>& centroids
501  ) {
502  (void) data;
503  (void) assignments;
504  (void) centroids;
505  }
506 
507  // -------------------------------------------------------------------------
508  // Internal Functions
509  // -------------------------------------------------------------------------
510 
511 private:
512 
513  void argument_checks_(
514  std::vector<Point> const& data,
515  size_t const k
516  ) const {
517  // Basic checks.
518  if( k > data.size() ) {
519  LOG_WARN << "Cannot run Kmeans with more clusters (k == " << k << ") than data points ("
520  << data.size() << ")";
521  }
522  if( k == 0 ) {
523  throw std::runtime_error(
524  "Cannot run Kmeans with zero clusters (k == 0)."
525  );
526  }
527 
528  // Validate the data. The function might also throw on its own, in order
529  // to provide a more helpful message about what is actually invalid about the data.
530  if( ! data_validation( data ) ) {
531  throw std::runtime_error( "Invalid data." );
532  }
533  }
534 
535  void runtime_checks_(
536  std::vector<Point> const& data,
537  size_t const k
538  ) const {
539  if( assignments_.size() != data.size() ) {
540  throw std::runtime_error(
541  "Assignments has size " + std::to_string( assignments_.size() ) +
542  " but data has size " + std::to_string( data.size() ) + "."
543  );
544  }
545  for( auto const& assign : assignments_ ) {
546  if( assign >= k ) {
547  throw std::runtime_error(
548  "Invalid assignment " + std::to_string( assign ) +
549  " >= k = " + std::to_string( k ) + "."
550  );
551  }
552  }
553  if( centroids_.size() != k ) {
554  throw std::runtime_error(
555  "Centroids has size " + std::to_string( centroids_.size() ) +
556  " but k is " + std::to_string( k ) + "."
557  );
558  }
559  }
560 
561  void init_with_random_assignments_(
562  std::vector<Point> const& data,
563  size_t const k
564  ) {
565  // Prepare a vector of the desired size.
566  assignments_ = std::vector<size_t>( data.size(), 0 );
567 
568  // Prepare a random distribution in range [0,k).
569  auto& engine = Options::get().random_engine();
570  std::uniform_int_distribution<size_t> distribution( 0, k - 1 );
571 
572  // Assign random cluster indices for each data point.
573  for( size_t i = 0; i < data.size(); ++i ) {
574  assignments_[ i ] = distribution( engine );
575  }
576  }
577 
578  void init_with_random_centroids_(
579  std::vector<Point> const& data,
580  size_t const k
581  ) {
582  // Prepare centroids vector. Empty, because we don't want to assume any default
583  // constructor for the Points.
584  centroids_ = std::vector<Point>();
585 
586  // Select k unique numbers out of the interval [ 0, data.size() ),
587  // and copy those data points to the centroids.
588  auto idxs = select_without_replacement( k, data.size() );
589  for( auto const idx : idxs ) {
590  centroids_.push_back( data[idx] );
591  }
592 
593  assert( centroids_.size() == k );
594  }
595 
596  void init_with_kmeans_plus_plus_(
597  std::vector<Point> const& data,
598  size_t const k
599  ) {
600  // Prepare centroids vector. Empty, because we don't want to assume any default
601  // constructor for the Points.
602  centroids_ = std::vector<Point>();
603 
604  // Shorthand.
605  auto& engine = Options::get().random_engine();
606 
607  // Use a random point as first centroid.
608  std::uniform_int_distribution<size_t> first_dist( 0, data.size() - 1 );
609  centroids_.push_back( data[ first_dist(engine) ]);
610 
611  // Prepare a vector of probabilities to select each data point as a centroid.
612  auto data_probs = std::vector<double>( data.size(), 0.0 );
613 
614  // Add more centroids.
615  for( size_t i = 1; i < k; ++i ) {
616 
617  // For each data point...
618  #pragma omp parallel for
619  for( size_t di = 0; di < data.size(); ++di ) {
620 
621  // ...find the closest centroid (of the ones that are produced so far), ...
622  double const min_d = find_nearest_cluster( centroids_, data[ di ] ).second;
623 
624  // ...and use its square as probability to select this point.
625  // (No need for OpenMP locking here, as di is unique to each thread).
626  data_probs[ di ] = min_d * min_d;
627  }
628 
629  // Now select a new centroid from the data, according to the given probabilities.
630  std::discrete_distribution<size_t> distribution(
631  data_probs.begin(), data_probs.end()
632  );
633  auto idx = distribution( engine );
634  assert( idx < data.size() );
635  centroids_.push_back( data[ idx ] );
636  }
637 
638  assert( centroids_.size() == k );
639  }
640 
641  std::unordered_set<size_t> get_empty_centroids_() {
642  auto const k = centroids_.size();
643 
644  // Fill a list with all numbers up to k...
645  auto empties = std::unordered_set<size_t>();
646  for( size_t i = 0; i < k; ++i ) {
647  empties.insert( i );
648  }
649 
650  // ... then remove all assigned ones again.
651  for( size_t i = 0; i < assignments_.size(); ++i ) {
652  assert( assignments_[i] < k );
653  empties.erase( assignments_[i] );
654 
655  // Prematurely exit if there is nothing else to remove.
656  // We don't want to go to all assignments if not necessary.
657  if( empties.empty() ) {
658  return empties;
659  }
660  }
661 
662  // If we are here, there are empty centroids, otherwise we'd have prematurely exited.
663  assert( ! empties.empty() );
664  return empties;
665  }
666 
667  // -------------------------------------------------------------------------
668  // Data Members
669  // -------------------------------------------------------------------------
670 
671 private:
672 
673  std::vector<size_t> assignments_;
674  std::vector<Point> centroids_;
675 
676  size_t max_iterations_ = 100;
678 
679 };
680 
681 // =================================================================================================
682 // Euclidean K-Means Specialization
683 // =================================================================================================
684 
686  : public Kmeans< std::vector<double> >
687 {
688 public:
689 
690  // -------------------------------------------------------------------------
691  // Typedefs and Constants
692  // -------------------------------------------------------------------------
693 
694  using Point = std::vector<double>;
695 
696  // -------------------------------------------------------------------------
697  // Constructors and Rule of Five
698  // -------------------------------------------------------------------------
699 
700  EuclideanKmeans( size_t dimensions );
701  virtual ~EuclideanKmeans() = default;
702 
703  EuclideanKmeans( EuclideanKmeans const& ) = default;
704  EuclideanKmeans( EuclideanKmeans&& ) = default;
705 
706  EuclideanKmeans& operator= ( EuclideanKmeans const& ) = default;
708 
709  // -------------------------------------------------------------------------
710  // Default K-Means Functions
711  // -------------------------------------------------------------------------
712 
713 private:
714 
715  virtual bool data_validation( std::vector<Point> const& data ) const override;
716 
717  virtual void update_centroids(
718  std::vector<Point> const& data,
719  std::vector<size_t> const& assignments,
720  std::vector<Point>& centroids
721  ) override;
722 
723  virtual double distance( Point const& lhs, Point const& rhs ) const override;
724 
725  // -------------------------------------------------------------------------
726  // Data Members
727  // -------------------------------------------------------------------------
728 
729 private:
730 
731  size_t dimensions_;
732 
733 };
734 
735 } // namespace utils
736 } // namespace genesis
737 
738 #endif // include guard
InitializationStrategy initialization_strategy() const
virtual bool treat_empty_centroids(std::vector< Point > const &data, std::vector< size_t > &assignments, std::vector< Point > &centroids, std::unordered_set< size_t > const &empty_centroids)
Kmeans & centroids(std::vector< Point > const &value)
virtual ~EuclideanKmeans()=default
#define LOG_WARN
Log a warning. See genesis::utils::LoggingLevel.
Definition: logging.hpp:95
size_t run(std::vector< Point > const &data, size_t const k)
std::vector< Point > const & centroids() const
std::string to_string(T const &v)
Return a string representation of a given value.
Definition: string.hpp:373
EuclideanKmeans & operator=(EuclideanKmeans const &)=default
virtual void initialize(std::vector< Point > const &data, size_t const k)
virtual std::pair< size_t, double > find_nearest_cluster(std::vector< Point > const &centroids, Point const &datum) const
virtual void pre_loop_hook(std::vector< Point > const &data, std::vector< size_t > &assignments, std::vector< Point > &centroids)
virtual bool lloyd_step(std::vector< Point > const &data, std::vector< size_t > &assignments, std::vector< Point > &centroids)
virtual ~Kmeans()=default
size_t max_iterations() const
Kmeans & max_iterations(size_t value)
virtual bool assign_to_centroids(std::vector< Point > const &data, std::vector< Point > const &centroids, std::vector< size_t > &assignments)
Helper POD that stores the variances and number of data points of each centroid, as well as the dista...
Provides easy and fast logging functionality.
virtual ClusteringInfo cluster_info(std::vector< Point > const &data, std::vector< size_t > const &assignments, std::vector< Point > const &centroids) const
virtual bool data_validation(std::vector< Point > const &data) const
std::default_random_engine & random_engine()
Returns the default engine for random number generation.
Definition: options.hpp:154
Kmeans & operator=(Kmeans const &)=default
std::vector< size_t > select_without_replacement(size_t const k, size_t const n)
Select k many unique numbers out of the range [ 0, n ).
Definition: random.cpp:46
Kmeans & assignments(std::vector< size_t > const &value)
std::vector< size_t > cluster_sizes() const
virtual void post_loop_hook(std::vector< Point > const &data, std::vector< size_t > &assignments, std::vector< Point > &centroids)
static Options & get()
Returns a single instance of this class.
Definition: options.hpp:59
virtual void update_centroids(std::vector< Point > const &data, std::vector< size_t > const &assignments, std::vector< Point > &centroids)=0
virtual double distance(Point const &lhs, Point const &rhs) const =0
std::vector< size_t > const & assignments() const
Kmeans & initialization_strategy(InitializationStrategy value)
#define LOG_INFO
Log an info message. See genesis::utils::LoggingLevel.
Definition: logging.hpp:98
ClusteringInfo cluster_info(std::vector< Point > const &data) const