cluster_based_estimation.hpp
Go to the documentation of this file.
1 // Copyright 2023-2024 Ekumen, Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #ifndef BELUGA_ALGORITHM_CLUSTER_BASED_ESTIMATION_HPP
16 #define BELUGA_ALGORITHM_CLUSTER_BASED_ESTIMATION_HPP
17 
18 // standard library
19 #include <functional>
20 #include <optional>
21 #include <queue>
22 #include <unordered_map>
23 #include <utility>
24 #include <vector>
25 
26 // external
27 #include <range/v3/algorithm/max_element.hpp>
28 #include <range/v3/algorithm/sort.hpp>
29 #include <range/v3/numeric/accumulate.hpp>
30 #include <range/v3/range/conversion.hpp>
31 #include <range/v3/view/cache1.hpp>
32 #include <range/v3/view/filter.hpp>
33 #include <range/v3/view/map.hpp>
34 #include <range/v3/view/zip.hpp>
35 #include <sophus/se2.hpp>
36 #include <sophus/types.hpp>
37 
38 // project
41 
42 #if RANGE_V3_MAJOR == 0 && RANGE_V3_MINOR < 12
43 #include <range/v3/view/group_by.hpp>
44 #else
45 #include <range/v3/view/chunk_by.hpp>
46 #endif
47 
53 namespace beluga {
54 
55 namespace clusterizer_detail {
56 
58 
71 template <class Map, class Proj>
72 [[nodiscard]] auto make_priority_queue(const Map& map, Proj&& proj) {
73  struct KeyWithPriority {
74  double priority; // priority value used to order the queue (higher value first).
75  typename Map::key_type key;
76 
77  bool operator<(const KeyWithPriority& other) const {
78  return priority < other.priority; // sort in decreasing priority order
79  }
80  };
81 
82  const auto make_from_pair = [proj = std::forward<Proj>(proj)](const auto& pair) {
83  return KeyWithPriority{std::invoke(proj, pair.second), pair.first};
84  };
85 
86  const auto range = map | //
87  ranges::views::transform(make_from_pair) | //
88  ranges::views::common;
89 
90  return std::priority_queue<KeyWithPriority>(range.begin(), range.end());
91 }
92 
94 
102 template <class Range>
103 [[nodiscard]] auto calculate_percentile_threshold(Range&& range, double percentile) {
104  auto values = range | ranges::to<std::vector>;
105  const auto n = static_cast<std::ptrdiff_t>(static_cast<double>(values.size()) * percentile);
106  std::nth_element(values.begin(), values.begin() + n, values.end());
107  return values[static_cast<std::size_t>(n)];
108 }
109 
111 template <class State>
112 struct ClusterCell {
114  double weight{0.0};
115  std::size_t num_particles{0};
116  std::optional<std::size_t> cluster_id;
117 };
118 
120 template <class State>
121 using ClusterMap = std::unordered_map<std::size_t, ClusterCell<State>>;
122 
124 
137 template <class States, class Weights, class Hashes>
138 [[nodiscard]] static auto make_cluster_map(States&& states, Weights&& weights, Hashes&& hashes) {
139  using State = ranges::range_value_t<States>;
140  ClusterMap<State> map;
141 
142  // Preallocate memory with a very rough estimation of the number of cells we might end up with.
143  map.reserve(states.size() / 5);
144 
145  // Calculate the accumulated cell weight and save a single representative state for each cell.
146  for (const auto& [state, weight, hash] : ranges::views::zip(states, weights, hashes)) {
147  auto [it, inserted] = map.try_emplace(hash, ClusterCell<State>{});
148  auto& [_, entry] = *it;
149  entry.weight += weight;
150  entry.num_particles++;
151  if (inserted) {
152  entry.representative_state = state;
153  }
154  }
155 
156  return map;
157 }
158 
160 
173 template <class State>
174 static void normalize_and_cap_weights(ClusterMap<State>& map, double percentile) {
175  auto entries = ranges::views::values(map);
176 
177  for (auto& entry : entries) {
178  assert(entry.num_particles > 0);
179  entry.weight /= static_cast<double>(entry.num_particles);
180  }
181 
182  const double max_weight =
183  calculate_percentile_threshold(ranges::views::transform(entries, &ClusterCell<State>::weight), percentile);
184 
185  for (auto& entry : entries) {
186  entry.weight = std::min(entry.weight, max_weight);
187  }
188 }
189 
191 
202 template <class State, class NeighborsFunction>
203 static void assign_clusters(ClusterMap<State>& map, NeighborsFunction&& neighbors_function) {
205  const auto max_priority = queue.top().priority;
206 
207  std::size_t next_cluster_id = 0;
208 
209  // Process cells in order of descending weight.
210  while (!queue.empty()) {
211  const auto hash = queue.top().key;
212  queue.pop();
213 
214  // The hash is guaranteed to exist in the map.
215  auto& cell = map[hash];
216 
217  // If there's no cluster id assigned to the cell, assign a new one.
218  if (!cell.cluster_id.has_value()) {
219  cell.cluster_id = next_cluster_id++;
220  }
221 
222  // Process the neighbors of the cell; if they don't have a cluster id already assigned
223  // then assign them one and add them to the queue with an inflated priority
224  // to ensure they get processed ASAP before moving on to other local peaks.
225  // Notice that with this algorithm each cell will go through the priority queue at most twice.
226 
227  // Define a lambda to check if a neighboring cell is valid for clustering.
228  const auto is_valid_neighbor = [&](const auto& neighbor_hash) {
229  auto it = map.find(neighbor_hash);
230  return (
231  (it != map.end()) && // is within the map
232  (!it->second.cluster_id.has_value()) && // AND not yet mapped to a cluster
233  (it->second.weight <= cell.weight)); // AND has lower weight than the current cell
234  };
235 
236  // Process the neighbors of the current cell.
237  for (const std::size_t neighbor_hash : neighbors_function(cell.representative_state) | //
238  ranges::views::cache1 | //
239  ranges::views::filter(is_valid_neighbor)) {
240  auto& neighbor = map[neighbor_hash];
241  neighbor.cluster_id = cell.cluster_id;
242  queue.push({max_priority + neighbor.weight, neighbor_hash}); // reintroduce with inflated priority
243  }
244  }
245 }
246 
247 } // namespace clusterizer_detail
248 
251  double linear_hash_resolution = 0.20;
252  double angular_hash_resolution = 0.524;
253 
255 
264  double weight_cap_percentile = 0.90;
265 };
266 
269  public:
271  explicit ParticleClusterizer(const ParticleClusterizerParam& parameters) : parameters_{parameters} {}
272 
274 
278  [[nodiscard]] auto neighbors(const Sophus::SE2d& pose) const {
279  return adjacent_grid_cells_ | //
280  ranges::views::transform([&pose](const Sophus::SE2d& neighbor_pose) { return pose * neighbor_pose; }) |
281  ranges::views::transform(spatial_hash_function_);
282  }
283 
285 
292  template <class States, class Weights>
293  [[nodiscard]] auto operator()(States&& states, Weights&& weights) {
294  auto hashes = states | ranges::views::transform(spatial_hash_function_) | ranges::to<std::vector>;
295 
298  clusterizer_detail::assign_clusters(map, [this](const auto& state) { return neighbors(state); });
299 
300  return hashes | //
301  ranges::views::transform([&map](std::size_t hash) { return map[hash].cluster_id.value(); }) |
302  ranges::to<std::vector>;
303  }
304 
305  private:
307 
312  };
313 
314  std::array<Sophus::SE2d, 6> adjacent_grid_cells_ = {
322  };
323 };
324 
326 
335 template <class States, class Weights, class Clusters>
336 [[nodiscard]] auto estimate_clusters(States&& states, Weights&& weights, Clusters&& clusters) {
337  using State = typename ranges::range_value_t<States>;
338  using Weight = typename ranges::range_value_t<Weights>;
339  using Cluster = typename ranges::range_value_t<Clusters>;
340 
341  using EstimateState = std::decay_t<decltype(std::get<0>(beluga::estimate(states, weights)))>;
342  using EstimateCovariance = std::decay_t<decltype(std::get<1>(beluga::estimate(states, weights)))>;
343 
344  static_assert(std::is_same_v<State, EstimateState>);
345 
346  struct Particle {
347  State state;
348  Weight weight;
349  Cluster cluster;
350 
352  static constexpr auto create(const State& s, Weight w, Cluster c) { return Particle{s, w, c}; }
353  };
354 
355  struct Estimate {
356  Weight weight;
357  EstimateState mean;
358  EstimateCovariance covariance;
359  };
360 
361  auto particles = ranges::views::zip_with(&Particle::create, states, weights, clusters) | //
362  ranges::to<std::vector>;
363 
364  ranges::sort(particles, std::less{}, &Particle::cluster);
365 
366  // For each cluster, estimate the mean and covariance of the states that belong to it.
367  return particles |
368 #if RANGE_V3_MAJOR == 0 && RANGE_V3_MINOR < 12
369  // Compatibility support for old Range-v3 versions that don't have a `chunk_by` view.
370  // The difference between the deprecated `group_by` and the standard `chunk_by` is:
371  // - group_by: The predicate is evaluated between the first element in the group and the current one.
372  // - chunk_by: The predicate is evaluated between adjacent elements.
373  //
374  // See also https://github.com/ericniebler/range-v3/issues/1637
375  //
376  // For this specific application, we can use them interchangeably.
377  ranges::views::group_by([](const auto& p1, const auto& p2) { return p1.cluster == p2.cluster; }) | //
378 #else
379  ranges::views::chunk_by([](const auto& p1, const auto& p2) { return p1.cluster == p2.cluster; }) | //
380 #endif
381  ranges::views::cache1 | //
382  ranges::views::filter([](auto subrange) {
383 #if RANGE_V3_MAJOR == 0 && RANGE_V3_MINOR < 11
384  return ranges::distance(subrange) > 1;
385 #else
386  // If there's only one sample in the cluster we can't estimate the covariance.
387  return subrange.size() > 1;
388 #endif
389  }) |
390  ranges::views::transform([](auto subrange) {
391  auto states = subrange | ranges::views::transform(&Particle::state);
392  auto weights = subrange | ranges::views::transform(&Particle::weight);
393  const auto [mean, covariance] = beluga::estimate(states, weights);
394  const auto total_weight = ranges::accumulate(weights, 0.0);
395  return Estimate{total_weight, std::move(mean), std::move(covariance)};
396  }) |
397  ranges::to<std::vector>;
398 }
399 
401 
413 template <class States, class Weights>
414 [[nodiscard]] auto cluster_based_estimate(
415  States&& states, //
416  Weights&& weights, //
417  ParticleClusterizerParam parameters = {}) {
418  const auto clusters = ParticleClusterizer{parameters}(states, weights);
419 
420  auto per_cluster_estimates = estimate_clusters(states, weights, clusters);
421 
422  if (per_cluster_estimates.empty()) {
423  // hmmm... maybe the particles are too fragmented? calculate the overall mean and covariance.
425  }
426 
427  const auto [_, mean, covariance] =
428  *ranges::max_element(per_cluster_estimates, std::less{}, [](const auto& e) { return e.weight; });
429 
430  return std::make_pair(mean, covariance);
431 }
432 
433 } // namespace beluga
434 
435 #endif
beluga::clusterizer_detail::ClusterMap
std::unordered_map< std::size_t, ClusterCell< State > > ClusterMap
A map that holds the sparse data about the particles grouped in cells.
Definition: cluster_based_estimation.hpp:121
beluga::ParticleClusterizerParam::weight_cap_percentile
double weight_cap_percentile
Cluster weight cap threshold.
Definition: cluster_based_estimation.hpp:264
Sophus::SO2
beluga::state
constexpr state_detail::state_fn state
Customization point object for accessing the state of a particle.
Definition: primitives.hpp:163
beluga::clusterizer_detail::assign_clusters
static void assign_clusters(ClusterMap< State > &map, NeighborsFunction &&neighbors_function)
Assign cluster ids to an existing cluster map.
Definition: cluster_based_estimation.hpp:203
beluga::cluster_based_estimate
auto cluster_based_estimate(States &&states, Weights &&weights, ParticleClusterizerParam parameters={})
Computes a cluster-based estimate from a particle set.
Definition: cluster_based_estimation.hpp:414
beluga::ParticleClusterizerParam::angular_hash_resolution
double angular_hash_resolution
clustering algorithm angular resolution
Definition: cluster_based_estimation.hpp:252
beluga::ParticleClusterizer::adjacent_grid_cells_
std::array< Sophus::SE2d, 6 > adjacent_grid_cells_
Definition: cluster_based_estimation.hpp:314
beluga::weight
constexpr weight_detail::weight_fn weight
Customization point object for accessing the weight of a particle.
Definition: primitives.hpp:264
estimation.hpp
Implementation of algorithms that allow calculating the estimated state of a particle filter.
beluga::ParticleClusterizer
Particle clusterizer implementation.
Definition: cluster_based_estimation.hpp:268
beluga::Numeric
Helper for creating strongly typed numeric types.
Definition: strongly_typed_numeric.hpp:38
beluga::ParticleClusterizer::parameters_
ParticleClusterizerParam parameters_
Parameters for the particle clusterizer.
Definition: cluster_based_estimation.hpp:306
beluga::clusterizer_detail::ClusterCell::weight
double weight
average weight of the cell
Definition: cluster_based_estimation.hpp:114
beluga::clusterizer_detail::make_cluster_map
static auto make_cluster_map(States &&states, Weights &&weights, Hashes &&hashes)
Create a cluster map from a range of particles and their corresponding spatial hashes.
Definition: cluster_based_estimation.hpp:138
se2.hpp
beluga::clusterizer_detail::normalize_and_cap_weights
static void normalize_and_cap_weights(ClusterMap< State > &map, double percentile)
Normalize weights and cap them to a given percentile.
Definition: cluster_based_estimation.hpp:174
types.hpp
beluga::ParticleClusterizer::neighbors
auto neighbors(const Sophus::SE2d &pose) const
Computes the neighboring cells for a given pose using the spatial hash function.
Definition: cluster_based_estimation.hpp:278
beluga::ParticleClusterizer::ParticleClusterizer
ParticleClusterizer(const ParticleClusterizerParam &parameters)
Constructor that initializes the ParticleClusterizer with given parameters.
Definition: cluster_based_estimation.hpp:271
Sophus::SE2
beluga::ParticleClusterizer::spatial_hash_function_
beluga::spatial_hash< Sophus::SE2d > spatial_hash_function_
Definition: cluster_based_estimation.hpp:308
Sophus::Vector2d
Vector2< double > Vector2d
beluga::clusterizer_detail::ClusterCell::cluster_id
std::optional< std::size_t > cluster_id
cluster id of the cell
Definition: cluster_based_estimation.hpp:116
beluga::spatial_hash
Callable class, allowing to calculate the hash of a particle state.
Definition: spatial_hash.hpp:100
beluga::clusterizer_detail::ClusterCell
A struct that holds the data of a single cell for the clusterization algorithm.
Definition: cluster_based_estimation.hpp:112
beluga::estimate_clusters
auto estimate_clusters(States &&states, Weights &&weights, Clusters &&clusters)
For each cluster, estimate the mean and covariance of the states that belong to it.
Definition: cluster_based_estimation.hpp:336
beluga::clusterizer_detail::calculate_percentile_threshold
auto calculate_percentile_threshold(Range &&range, double percentile)
Calculates the threshold value at a specified percentile from a range.
Definition: cluster_based_estimation.hpp:103
beluga::estimate
std::pair< Sophus::SE2< Scalar >, Sophus::Matrix3< Scalar > > estimate(Poses &&poses, Weights &&weights)
Returns a pair consisting of the estimated mean pose and its covariance.
Definition: estimation.hpp:129
beluga::clusterizer_detail::make_priority_queue
auto make_priority_queue(const Map &map, Proj &&proj)
Create a priority queue from a map using a specified projection.
Definition: cluster_based_estimation.hpp:72
beluga::views::weights
constexpr auto weights
Definition: particles.hpp:34
spatial_hash.hpp
Implementation of a spatial hash for N dimensional states.
beluga::clusterizer_detail::ClusterCell::representative_state
State representative_state
state of a particle that is within the cell
Definition: cluster_based_estimation.hpp:113
beluga::clusterizer_detail::ClusterCell::num_particles
std::size_t num_particles
number of particles in the cell
Definition: cluster_based_estimation.hpp:115
beluga::ParticleClusterizerParam
Parameters used to construct a ParticleClusterizer instance.
Definition: cluster_based_estimation.hpp:250
beluga::ParticleClusterizerParam::linear_hash_resolution
double linear_hash_resolution
clustering algorithm linear resolution
Definition: cluster_based_estimation.hpp:251
beluga::views::zip
constexpr detail::zip_fn zip
Given N ranges, return a new range where the Mth element is a tuple of the Mth elements of all N rang...
Definition: zip.hpp:74
beluga::ParticleClusterizer::operator()
auto operator()(States &&states, Weights &&weights)
Clusters the given particles based on their states and weights.
Definition: cluster_based_estimation.hpp:293
proj
def proj(v)
beluga::views::states
constexpr auto states
Definition: particles.hpp:30
beluga
The main Beluga namespace.
Definition: 3d_embedding.hpp:21


beluga
Author(s):
autogenerated on Tue Jul 16 2024 02:59:53