dyng
DynamicGraphLayout
foresighted_layout.h
1 /*
2  Copyright 2020 František Bráblík
3 
4  Licensed under the Apache License, Version 2.0 (the "License");
5  you may not use this file except in compliance with the License.
6  You may obtain a copy of the License at
7 
8  http://www.apache.org/licenses/LICENSE-2.0
9 
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15 */
16 #pragma once
17 
18 #include "graph.h"
19 #include "mapped_graph.h"
20 #include "dynamic_graph.h"
21 #include "cooling.h"
22 
23 #include <vector>
24 #include <unordered_map>
25 #include <unordered_set>
26 #include <cmath>
27 #include <utility> // std::move
28 #include <algorithm> // std::max_element
29 
30 namespace dyng {
31 
47 template <typename StaticLayout>
49 
50 using node_live_sets = std::unordered_map<node_id, detail::live_set>;
51 using edge_live_sets = std::unordered_map<edge_id, detail::live_set>;
52 
53 public:
60  float tolerance
61  , float canvas_width
62  , float canvas_height
63  , coords center = coords())
64  : m_tolerance(tolerance)
65  , m_canvas_width(canvas_width)
66  , m_canvas_height(canvas_height)
67  , m_center(center) {}
68 
69  explicit foresighted_layout(float tolerance)
70  : foresighted_layout(tolerance, 1, 1, { 0, 0 }) {}
71 
73  : foresighted_layout(0, 1, 1, { 0, 0 }) {}
74 
80  void set_canvas(float w, float h, coords center = coords()) {
81  m_canvas_width = w;
82  m_canvas_height = h;
83  m_center = center;
84  }
85 
86  void set_tolerance(float tolerance) { m_tolerance = tolerance; }
87 
89 
92  void use_relative_distance(bool relative) { m_relative_distance = relative; }
93 
95  const StaticLayout& static_layout() const { return m_static_layout; }
96 
98  StaticLayout& static_layout() { return m_static_layout; }
99 
101  void set_cooling(cooling c) { m_cooling = std::move(c); }
102 
104  void operator()(dynamic_graph& dgraph) {
105  if (dgraph.states().empty()) {
106  return;
107  }
108  // scale calculation canvas size to ratio
109  float calculation_h = CalculationHeight;
110  float calculation_w = calculation_h * m_canvas_width / m_canvas_height;
111 
112  // calculate using basic Foresighted Layout
113  basic_layout(dgraph.states(), calculation_w, calculation_h);
114 
115  // improve resulting layouts within tolerance
116  if (m_tolerance != 0) {
117  tolerance(dgraph.states(), calculation_w, calculation_h, m_tolerance);
118  }
119 
120  // rescale to required dimensions
121  for (auto& state : dgraph.states()) {
122  rescale(state, calculation_w, calculation_h, m_canvas_width, m_canvas_height);
123  move(state, 0.0, 0.0, m_center.x, m_center.y);
124  }
125  }
126 
127 protected:
128  static constexpr float CalculationHeight = 1;
129 
130  float m_tolerance;
131  float m_canvas_width;
132  float m_canvas_height;
133  coords m_center;
134 
135  cooling m_cooling{ 250, 0.4, [](float t){ return t * 0.977; } };
136  StaticLayout m_static_layout;
137  bool m_relative_distance = true;
138 
139  // increases layout quality within tolerance
140  virtual void tolerance(
141  std::vector<graph_state>& states
142  , float width
143  , float height
144  , float tolerance_value) {
145  float temp = m_cooling.start_temperature;
146  // when absolute mental distance should be used, make it relative to the largest graph state
147  // (this doesn't defeat the purpose of it being absolute)
148  // otherwise every differently sized graph would accept completely different values
149  if (!m_relative_distance) {
150  tolerance_value *= m_static_layout.relative_unit(width, height) * max_nodes(states);
151  }
152  for (unsigned i = 0; i < m_cooling.iterations; ++i) {
153  for (unsigned s = 0; s < states.size(); ++s) {
154  graph_state copy = states[s];
155  m_static_layout.iteration(copy, width, height, temp);
156  if ((s == 0 || distance(copy, states[s - 1]) < tolerance_value)
157  && (s >= states.size() - 1
158  || distance(copy, states[s + 1]) < tolerance_value)) {
159  states[s] = std::move(copy);
160  }
161  }
162  temp = m_cooling.anneal(temp);
163  }
164  }
165 
166  void basic_layout(std::vector<graph_state>& states, float width, float height) {
167  node_live_sets nodes_live = node_live_times(states);
168  edge_live_sets edges_live = edge_live_times(states);
169 
170  // main steps
171  auto supergraph = calculate_supergraph(states);
172  auto gap = calculate_gap(std::move(supergraph), nodes_live, edges_live);
173  auto rgap = calculate_rgap(std::move(gap));
174 
175  m_static_layout(rgap.graph(), width, height);
176 
177  // use results
178  for (auto& state : states) {
179  for (auto& node : state.nodes()) {
180  const auto& target_node = rgap.node_at(node.id());
181  node.pos() = target_node.pos();
182  }
183  }
184  }
185 
186  void rescale(graph_state& graph
187  , float src_width
188  , float src_height
189  , float dst_width
190  , float dst_height) const {
191  float w_coeff = dst_width / src_width;
192  float h_coeff = dst_height / src_height;
193  auto perform = [&w_coeff, &h_coeff](coords& c){
194  c.x *= w_coeff;
195  c.y *= h_coeff;
196  };
197  for (auto& node : graph.nodes()) {
198  perform(node.pos());
199  }
200  }
201 
202  void move(graph_state& graph
203  , float src_x
204  , float src_y
205  , float dst_x
206  , float dst_y) const {
207  float x_diff = dst_x - src_x;
208  float y_diff = dst_y - src_y;
209  auto perform = [&x_diff, &y_diff](coords& c){
210  c.x += x_diff;
211  c.y += y_diff;
212  };
213  for (auto& node : graph.nodes()) {
214  perform(node.pos());
215  }
216  }
217 
218  node_live_sets node_live_times(const std::vector<graph_state>& states) const {
219  std::unordered_map<node_id, detail::live_set> result;
220  for (unsigned t = 0; t < states.size(); ++t) {
221  for (const auto& node : states[t].nodes()) {
222  result[node.id()].add(t);
223  }
224  }
225  return result;
226  }
227 
228  edge_live_sets edge_live_times(const std::vector<graph_state>& states) const {
229  std::unordered_map<edge_id, detail::live_set> result;
230  for (unsigned t = 0; t < states.size(); ++t) {
231  for (const auto& edge : states[t].edges()) {
232  result[edge.id()].add(t);
233  }
234  }
235  return result;
236  }
237 
238  // calculates a supergraph (a union of all edges and nodes) from the sequence
239  graph_state calculate_supergraph(const std::vector<graph_state>& animation) const {
240  graph_state supergraph;
241  for (auto& state : animation) {
242  for (const auto& node : state.nodes()) {
243  supergraph.emplace_node(node.id());
244  }
245  for (const auto& edge : state.edges()) {
246  supergraph.emplace_edge(edge.id(), edge.one_id(), edge.two_id());
247  }
248  }
249  return supergraph;
250  }
251 
252  // foresighted layout algorithm: GAP calculation
253  // GAP - graph animation partitioning
254  detail::mapped_graph calculate_gap(const graph_state& supergraph,
255  const node_live_sets& nodes_live,
256  const edge_live_sets& edges_live) const {
257  detail::mapped_graph gap;
258  for (auto& node : supergraph.nodes()) {
259  bool exists = false;
260  for (auto& partition : gap.graph().nodes()) {
261  node_id partition_id = partition.id();
262  if (partition.live_time().intersection(nodes_live.at(node.id())).empty()) {
263  partition.add_live_time(nodes_live.at(node.id()));
264  gap.map_node(node.id(), partition_id);
265  exists = true;
266  break;
267  }
268  }
269  if (!exists) {
270  auto& added = gap.graph().push_node(node.id());
271  added.add_live_time(nodes_live.at(node.id()));
272  }
273  }
274  // add all partition edges
275  for (auto& edge : supergraph.edges()) {
276  node_id one = gap.node_at(edge.one_id()).id();
277  node_id two = gap.node_at(edge.two_id()).id();
278  auto& added = gap.graph().emplace_edge(edge.id(), one, two);
279  added.add_live_time(edges_live.at(edge.id()));
280  }
281  return gap;
282  }
283 
284  // foresighted layout algorithm: heuristic for calculating an RGAP
285  // RGAP - reduced graph animation partitioning
286  detail::mapped_graph calculate_rgap(detail::mapped_graph gap) const {
287  detail::mapped_graph rgap = gap;
288  rgap.clear_edges();
289  std::unordered_set<edge_id> removed; // used to mark removed edges
290  auto same_edge = [](const auto& a, const auto& b){
291  // return if 'a' and 'b' are between the same nodes
292  return (a.one_id() == b.one_id() && a.two_id() == b.two_id())
293  || (a.one_id() == b.two_id() && a.two_id() == b.one_id());
294  };
295  for (unsigned i = 0; i < gap.graph().edges().size(); ++i) {
296  auto& edge_i = gap.graph().edges()[i];
297  if (!removed.count(edge_i.id())) {
298  detail::edge_partition& current_partition
299  = rgap.graph().emplace_edge(edge_i.id(), edge_i.one_id(), edge_i.two_id());
300  current_partition.add_live_time(edge_i.live_time());
301  for (unsigned k = i + 1; k < gap.graph().edges().size(); ++k) {
302  auto& edge_k = gap.graph().edges()[k];
303  if (!removed.count(edge_k.id())
304  && same_edge(edge_i, edge_k)
305  && current_partition.live_time()
306  .intersection(edge_k.live_time()).empty()) {
307  rgap.map_edge(edge_k.id(), current_partition.id());
308  current_partition.add_live_time(edge_k.live_time());
309  removed.insert(edge_k.id());
310  }
311  }
312  }
313  }
314  // no need to remove inactive edges
315  return rgap;
316  }
317 
318  unsigned max_nodes(std::vector<graph_state>& states) const {
319  const auto& max = *std::max_element(states.begin(), states.end(),
320  [](const graph_state& a, const graph_state& b) {
321  return a.nodes().size() < b.nodes().size();
322  });
323  return max.nodes().size();
324  }
325 
326  // calculates euclidean mental distance between two layouts
327  float distance(const graph_state& one, const graph_state& two) const {
328  float result = 0;
329  unsigned count = 0;
330  for (const auto& node : one.nodes()) {
331  if (two.node_exists(node.id())) {
332  const auto& other = two.node_at(node.id());
333  float diff_x = node.pos().x - other.pos().x;
334  float diff_y = node.pos().y - other.pos().y;
335  result += std::sqrt(diff_x * diff_x + diff_y * diff_y);
336  ++count;
337  }
338  }
339  if (m_relative_distance) {
340  return result / static_cast<float>(count);
341  }
342  return result;
343  }
344 };
345 
346 } // namespace dyng
dyng::foresighted_layout::static_layout
const StaticLayout & static_layout() const
Returns the Layout object used for static layout.
Definition: foresighted_layout.h:95
dyng::dynamic_graph::states
std::vector< graph_state > & states()
Returns reference to the vector of graph states.
Definition: dynamic_graph.h:157
dyng::foresighted_layout::set_canvas
void set_canvas(float w, float h, coords center=coords())
Definition: foresighted_layout.h:80
dyng::foresighted_layout
Definition: foresighted_layout.h:48
dyng::coords
Class representing coordinates in 2D space.
Definition: coords.h:21
dyng::foresighted_layout::foresighted_layout
foresighted_layout(float tolerance, float canvas_width, float canvas_height, coords center=coords())
Definition: foresighted_layout.h:59
dyng::dynamic_graph
Represents the sequence of states of a dynamic graph.
Definition: dynamic_graph.h:42
dyng::foresighted_layout::operator()
void operator()(dynamic_graph &dgraph)
Performs the algorithm on a dynamic graph.
Definition: foresighted_layout.h:104
dyng::foresighted_layout::static_layout
StaticLayout & static_layout()
Returns the Layout object used for static layout.
Definition: foresighted_layout.h:98
dyng::foresighted_layout::use_relative_distance
void use_relative_distance(bool relative)
Sets whether to use relative or absolute mental distance calculations.
Definition: foresighted_layout.h:92
dyng::cooling
Structure representing a cooling strategy.
Definition: cooling.h:27
dyng::foresighted_layout::set_cooling
void set_cooling(cooling c)
Sets a different cooling strategy.
Definition: foresighted_layout.h:101