Fishnet 2.0.1
Framework for Graph-Based Analysis of GIS Vector Data
Loading...
Searching...
No Matches
DBSC.hpp
Go to the documentation of this file.
1#include <fishnet/Graph.hpp>
7#include <ranges>
8#include <unordered_map>
9#include <queue>
10#include <cmath>
11#include <cassert>
12#include "ClusterAlgorithm.hpp"
13
14
15namespace fishnet {
16
17template<typename T>
18class DBSC {
19private:
20 struct ClusterNodeStats {
21 double densityIndicator;
22 double meanDistance;
23 double nearestDistance;
24 };
25
26 double T1 = NAN; // attribute difference threshold for spatial reachability, will be computed from the data
27 const double eps;
28 const size_t beta;
29 const size_t minPts;
32 std::unordered_map<size_t, bool> visitedNodes; // node id to visited flag
33 std::unordered_map<size_t, ClusterNodeStats> clusterNodeStats; // node id to cluster node stats
34 mutable std::unordered_map<size_t, double> distanceCache;
35
36 struct ClusterNode {
37 T node;
38 size_t id;
39 double attributeValue;
40
41 bool operator==(const ClusterNode & other) const {
42 return this->id == other.id;
43 }
44 };
45
46 struct ClusterNodeHash {
47 size_t operator()(const ClusterNode & node) const {
48 return node.id;
49 }
50 };
51
52 auto ClusterNodeOrdering() const noexcept {
53 return [this](const ClusterNode & lhs, const ClusterNode & rhs) {
54 const auto &lhsStats = clusterNodeStats.at(lhs.id);
55 const auto &rhsStats = clusterNodeStats.at(rhs.id);
56
57 if (! fishnet::math::areEqual(lhsStats.densityIndicator, rhsStats.densityIndicator)) {
58 return lhsStats.densityIndicator > rhsStats.densityIndicator;
59 }
60 return lhsStats.meanDistance < rhsStats.meanDistance;
61 };
62 }
63
64 auto ClusterNodeOrderingReverse() const noexcept {
65 return [ordering = ClusterNodeOrdering()](const ClusterNode & lhs, const ClusterNode & rhs) {
66 return not ordering(rhs, lhs);
67 };
68 }
69
70 double spatial_distance(const ClusterNode & lhs, const ClusterNode & rhs) const noexcept {
71 size_t hash = fishnet::math::CantorPairing(lhs.id,rhs.id);
72 if (distanceCache.contains(hash)) {
73 return distanceCache.at(hash);
74 }
75 double dist = distanceFunction(lhs.node, rhs.node);
76 distanceCache[hash] = dist;
77 return dist;
78 }
79
80 bool inline spatially_directly_reachable(const ClusterNode & p, const ClusterNode & q)const noexcept {
81 return spatial_distance(p, q) <= eps;
82 }
83
84 double inline attribute_distance(const ClusterNode & p, const ClusterNode & q) const noexcept {
85 return std::fabs(p.attributeValue - q.attributeValue);
86 }
87
88 bool reachable(const ClusterNode & q, fishnet::util::forward_range_of<ClusterNode> auto && temporalCluster) {
89 double avg_clu = fishnet::math::avg(temporalCluster, &ClusterNode::attributeValue).value_or_throw();
90 return std::fabs(q.attributeValue - avg_clu) <= T1;
91 }
92
93
94 constexpr auto getClusterGraphType(fishnet::graph::Graph auto const & graph) const noexcept {
95 if constexpr (std::remove_cvref_t<decltype(graph)>::edge_type::isDirected()) {
97 }
98 else {
100 }
101 }
102
103 auto getClusterGraph(fishnet::graph::Graph auto const & graph) const noexcept {
104 auto clusterGraph = getClusterGraphType(graph);
105 size_t nodeIndex = 0;
106 std::unordered_map<T, ClusterNode> nodeMap;
107 for(const auto & node : graph.getNodes()){
108 nodeMap.try_emplace(node, ClusterNode{
109 .node = node,
110 .id = nodeIndex++,
111 .attributeValue = attributeExtractor(node)
112 });
113 }
114 clusterGraph.addNodes(std::views::values(nodeMap));
115 for(const auto & node: graph.getNodes()){
116 for(const auto & nbr: graph.getNeighbours(node)){
117 clusterGraph.addEdge(nodeMap.at(node), nodeMap.at(nbr));
118 }
119 }
120 return clusterGraph;
121 }
122
123 double prepareClusteringData(fishnet::graph::Graph auto & graph) {
124 /* Update cluster nodes in the trimmed graph with density indicators and mean attribute difference and nearest neighbour attribute difference */
125 for(const ClusterNode & clusterNode : graph.getNodes()){
126 visitedNodes[clusterNode.id] = false;
127 auto neighbors = graph.getNeighbours(clusterNode);
128 double N_size = static_cast<double>(fishnet::util::size(neighbors));
129 if(N_size == 0){
130 clusterNodeStats[clusterNode.id] = ClusterNodeStats{
131 .densityIndicator = 0.0,
132 .meanDistance = 0.0,
133 .nearestDistance = 0.0
134 };
135 continue;
136 }
137 double N_SDR = static_cast<double>(std::ranges::count_if(neighbors, [&](const auto & nbr) {
138 return spatially_directly_reachable(clusterNode, nbr);
139 }));
140 const ClusterNode & nearestNeighbour = *std::ranges::min_element(neighbors, [&](const auto & lhs, const auto & rhs) {
141 return spatial_distance(clusterNode, lhs) < spatial_distance(clusterNode, rhs);
142 });
143 double nearestDistance = attribute_distance(clusterNode, nearestNeighbour);
144 double meanDistance = fishnet::math::mean(neighbors, [&](const auto & nbr) {
145 return attribute_distance(clusterNode, nbr);
146 }).value_or(0.0);
147 double densityIndicator = N_SDR + N_SDR/N_size;
148 clusterNodeStats[clusterNode.id] = ClusterNodeStats{
149 .densityIndicator = densityIndicator,
150 .meanDistance = meanDistance,
151 .nearestDistance = nearestDistance
152 };
153 }
154 double avgAttributeDiff = fishnet::math::mean(std::views::values(clusterNodeStats),&ClusterNodeStats::nearestDistance).value_or_throw("Illegal state, graph has at least one node, so attributeDiffs should not be empty");
155 double stdAttributeDiff = fishnet::math::std(std::views::values(clusterNodeStats), &ClusterNodeStats::nearestDistance).value_or_throw("Illegal state, graph has at least one node, so attributeDiffs should not be empty");
156 const double T1 = fishnet::math::mean(std::views::values(clusterNodeStats) | std::views::filter([avgAttributeDiff, stdAttributeDiff](const ClusterNodeStats & data) {
157 return avgAttributeDiff - 3 * stdAttributeDiff <= data.nearestDistance && data.nearestDistance <= avgAttributeDiff + 3 * stdAttributeDiff;
158 }), &ClusterNodeStats::nearestDistance).value_or_throw("Illegal state, there should be at least one non-outlier attribute difference, so the filtered range should not be empty");
159 return T1;
160 }
161
162 bool is_expanding_core(const ClusterNode & node, fishnet::graph::Graph auto const & graph) const noexcept {
163 bool visited = this->visited(node);
164 size_t counter= std::ranges::count_if(graph.getNeighbours(node), [this,&node](const auto & nbr) {
165 return spatially_directly_reachable(node, nbr);
166 });
167 return !visited && counter > 0;
168 }
169
170 bool visited(const ClusterNode & node) const {
171 return visitedNodes.at(node.id);
172 }
173
174 void markVisited(const ClusterNode & node) {
175 visitedNodes[node.id] = true;
176 }
177
178 std::vector<ClusterNode> expand(const ClusterNode & core, fishnet::graph::Graph auto const& graph) {
179 std::vector<ClusterNode> cluster;
180 cluster.push_back(core);
181 std::priority_queue<ClusterNode,std::vector<ClusterNode>,decltype(ClusterNodeOrderingReverse())> expandingCores(ClusterNodeOrderingReverse());
182 expandingCores.push(core);
183 while (!expandingCores.empty()) {
184 const ClusterNode current = expandingCores.top();
185 expandingCores.pop();
186 for (const ClusterNode & nbr : graph.getNeighbours(current)) {
187 if (!visited(nbr) && spatially_directly_reachable(current,nbr) && reachable(nbr, cluster)) {
188 if (is_expanding_core(nbr, graph)){
189 expandingCores.push(nbr);
190 }
191 markVisited(nbr);
192 cluster.push_back(nbr);
193 }
194 }
195 }
196 return cluster;
197 }
198
199public:
200 DBSC(double eps, size_t beta, size_t minPts, fishnet::util::BiFunction<T,T,double> auto && distanceFunction, fishnet::util::UnaryFunction<T, double> auto && attributeExtractor)
201 : eps(eps), beta(beta), minPts(minPts), distanceFunction(std::forward<fishnet::util::BiFunction_t<T,T,double>>(distanceFunction)), attributeExtractor(std::forward<fishnet::util::UnaryFunction_t<T, double>>(attributeExtractor)) {}
202
203 DBSC(double eps, size_t beta, size_t minPts, fishnet::util::BiFunction<T,T,double> auto && distanceFunction)
204 :eps(eps), beta(beta), minPts(minPts), distanceFunction(std::forward<fishnet::util::BiFunction_t<T,T,double>>(distanceFunction)), attributeExtractor([](){return 0.0;}) {}
205
206 void setCustomT1(double t1) {
207 this->T1 = t1;
208 }
209
211 static_assert(std::derived_from<typename std::decay_t<decltype(input_graph)>::node_type, T>, "Graph node type must be compatible with DBSC point type");
212 auto graph = getClusterGraph(input_graph);
213 auto initalEdges = graph.getEdges();
214 auto edgeDistanceMapper = [this](const auto & edge) {return spatial_distance(edge.getFrom(), edge.getTo());};
215 const double global_mean = fishnet::math::mean(initalEdges, edgeDistanceMapper).value_or(0.0);
216 const double global_variation = fishnet::math::std(initalEdges, edgeDistanceMapper, global_mean).value_or(0.0);
217 auto local_mean = [&](const ClusterNode & node) -> double{
218 return fishnet::math::mean(graph.getNeighbours(node), [this,&node](const auto & nbr) {
219 return spatial_distance(node, nbr);
220 }).value_or(0.0);
221 };
222 auto local_variation = [&](const ClusterNode & node) -> double{
223 return fishnet::math::std(graph.getNeighbours(node), [this,&node](const auto & nbr) {
224 return spatial_distance(node, nbr);
225 }).value_or(0.0);
226 };
227
228 /* Remove global long edges */
229 std::vector<std::pair<ClusterNode, ClusterNode>> edgesToRemove;
230 for(const auto & node : graph.getNodes()){
231 double global_distance_constraint = global_mean + (global_mean/local_mean(node)) * global_variation;
232 for(const auto & nbr : graph.getNeighbours(node)){
233 if(spatial_distance(node, nbr) > global_distance_constraint){
234 edgesToRemove.emplace_back(node, nbr);
235 }
236 }
237 }
238 std::ranges::for_each(edgesToRemove, [&graph](const auto & edge){
239 graph.removeEdge(edge.first, edge.second);
240 });
241
242 edgesToRemove.clear();
243 /* Remove local long edges*/
244 for (const auto & node: graph.getNodes()){
245 auto order2_graph = fishnet::graph::BFS::neighborhood(graph, node, 2);
246 auto order2_mean = fishnet::math::mean(order2_graph.getEdges(), edgeDistanceMapper).value_or(0.0);
247 auto mean_local_variation = fishnet::math::mean(order2_graph.getNodes(), local_variation).value_or(0.0);
248 auto local_distance_constraint = order2_mean + static_cast<double>(beta) * mean_local_variation;
249 for(const auto & edge: order2_graph.getEdges()){
250 if(spatial_distance(edge.getFrom(), edge.getTo()) > local_distance_constraint){
251 edgesToRemove.emplace_back(edge.getFrom(), edge.getTo());
252 }
253 }
254 }
255 std::ranges::for_each(edgesToRemove, [&graph](const auto & edge){
256 graph.removeEdge(edge.first, edge.second);
257 });
258 /* Prepare clustering data */
259 double calculatedT1 =prepareClusteringData(graph);
260 this->T1 = std::isnan(this->T1) ? calculatedT1 : this->T1;
261 std::vector<ClusterNode> clusteringData(graph.getNodes().begin(), graph.getNodes().end());
262 std::ranges::sort(clusteringData, ClusterNodeOrdering());
263 assert(!std::isnan(this->T1) && "T1 should have been set either by user or calculated from the data, but it is still NaN, indicating an illegal state");
264 /* Clustering */
265 ClusterResult<T> clusterResult;
266 for(const ClusterNode & core : clusteringData){
267 if(visited(core))
268 continue;
269 markVisited(core);
270 auto result = expand(core, graph);
271 if (fishnet::util::size(result) >= minPts) {
272 clusterResult.clusters.push_back(fishnet::util::toVector(result | std::views::transform(&ClusterNode::node)));
273 } else {
274 std::ranges::for_each(result, [&clusterResult](auto && clusterNode){
275 clusterResult.noise.push_back(std::move(clusterNode.node));
276 });
277 }
278 }
279 return clusterResult;
280 }
281};
282
283static_assert(ClusterAlgorithm<DBSC<int>, fishnet::graph::UndirectedGraph<int>>);
284
285template<typename T>
287 double eps;
288 size_t beta;
289 size_t minPts = 2;
292 double t1 = NAN;
293
294 auto build() {
296 if(!std::isnan(t1)){
297 dbsc.setCustomT1(t1);
298 }
299 return dbsc;
300 }
301
303 this->eps = eps;
304 return *this;
305 }
306
308 this->beta = beta;
309 return *this;
310 }
311
313 this->minPts = minPts;
314 return *this;
315 }
316
318 this->distanceFunction = distanceFunction;
319 return *this;
320 }
321
326
328 this->t1 = t1;
329 return *this;
330 }
331
332};
333
334} //namespace fishnet
Definition DBSC.hpp:18
DBSC(double eps, size_t beta, size_t minPts, fishnet::util::BiFunction< T, T, double > auto &&distanceFunction, fishnet::util::UnaryFunction< T, double > auto &&attributeExtractor)
Definition DBSC.hpp:200
void setCustomT1(double t1)
Definition DBSC.hpp:206
DBSC(double eps, size_t beta, size_t minPts, fishnet::util::BiFunction< T, T, double > auto &&distanceFunction)
Definition DBSC.hpp:203
ClusterResult< T > operator()(fishnet::graph::Graph auto const &input_graph)
Definition DBSC.hpp:210
Interface for graph types. Every graph type has to inherit from AbstractGraph.
Definition GraphModel.hpp:23
Definition FunctionalConcepts.hpp:15
Definition FunctionalConcepts.hpp:9
Definition CollectionConcepts.hpp:27
G neighborhood(const G &graph, const typename G::node_type &node, size_t order, O &&outputGraph)
Definition BFSAlgorithm.hpp:189
graph::__impl::BaseGraph< DirectedEdge< N, Hash, Equal >, AdjContainer > DirectedGraph
Simple Directed Graph type.
Definition Graph.hpp:31
graph::__impl::BaseGraph< UndirectedEdge< N, Hash, Equal >, AdjContainer > UndirectedGraph
Simple Undirected Graph type.
Definition Graph.hpp:20
constexpr bool areEqual(T lhs, U rhs) noexcept
Definition Constants.hpp:31
static Option< double > avg(std::ranges::input_range auto &&values, fishnet::util::UnaryFunction< std::ranges::range_value_t< decltype(values)>, double > auto const &valueMapper)
Definition Statistics.hpp:28
static Option< double > std(std::ranges::input_range auto &&values, fishnet::util::UnaryFunction< std::ranges::range_value_t< decltype(values)>, double > auto const &valueMapper, double meanValue)
Definition Statistics.hpp:67
static Option< double > mean(std::ranges::input_range auto &&values, fishnet::util::UnaryFunction< std::ranges::range_value_t< decltype(values)>, double > auto const &valueMapper)
Definition Statistics.hpp:12
constexpr size_t CantorPairing(size_t x, size_t y) noexcept
Definition CantorPairing.hpp:5
std::function< R(const T &, const U &)> BiFunction_t
Definition FunctionalConcepts.hpp:18
constexpr size_t size(std::ranges::range auto &&range) noexcept
Definition CollectionConcepts.hpp:50
std::function< R(const T &)> UnaryFunction_t
Definition FunctionalConcepts.hpp:12
constexpr auto toVector(R &&range)
Definition CollectionConcepts.hpp:64
Definition BFSClustering.hpp:5
STL namespace.
Definition ClusterAlgorithm.hpp:9
std::vector< std::vector< R > > clusters
Definition ClusterAlgorithm.hpp:10
std::vector< R > noise
Definition ClusterAlgorithm.hpp:11
Definition DBSC.hpp:286
double eps
Definition DBSC.hpp:287
DBSCBuilder< T > & setBeta(size_t beta)
Definition DBSC.hpp:307
fishnet::util::UnaryFunction_t< T, double > attributeExtractor
Definition DBSC.hpp:291
DBSCBuilder< T > & setT1(double t1)
Definition DBSC.hpp:327
DBSCBuilder< T > & setMinPts(size_t minPts)
Definition DBSC.hpp:312
auto build()
Definition DBSC.hpp:294
size_t minPts
Definition DBSC.hpp:289
double t1
Definition DBSC.hpp:292
fishnet::util::BiFunction_t< T, T, double > distanceFunction
Definition DBSC.hpp:290
DBSCBuilder< T > & setEps(double eps)
Definition DBSC.hpp:302
DBSCBuilder< T > & setAttributeExtractor(fishnet::util::UnaryFunction< T, double > auto &&attributeExtractor)
Definition DBSC.hpp:322
DBSCBuilder< T > & setDistanceFunction(fishnet::util::BiFunction< T, T, double > auto &&distanceFunction)
Definition DBSC.hpp:317
size_t beta
Definition DBSC.hpp:288