Utopia  2
Framework for studying models of complex & adaptive systems.
graph_utils.hh
Go to the documentation of this file.
1 #ifndef UTOPIA_DATAIO_GRAPH_UTILS_HH
2 #define UTOPIA_DATAIO_GRAPH_UTILS_HH
3 
4 #include <tuple>
5 
6 #include <boost/graph/adjacency_list.hpp>
7 #include <boost/graph/adjacency_matrix.hpp>
8 #include <boost/graph/graph_traits.hpp>
9 #include <boost/graph/properties.hpp>
10 #include <boost/hana/all_of.hpp>
11 #include <boost/hana/ext/std/tuple.hpp>
12 #include <boost/hana/for_each.hpp>
13 
14 #include <hdf5.h>
15 
16 #include "../core/logging.hh"
17 #include "../core/type_traits.hh"
18 #include "../core/graph/iterator.hh"
19 #include "hdfdataset.hh"
20 #include "hdfgroup.hh"
21 
22 using namespace std::literals::string_literals;
23 
24 namespace Utopia {
25 namespace DataIO {
31 namespace GraphUtilsHelper{
32 
47 template <typename Graph>
48 std::pair<std::shared_ptr<HDFDataset>,
49  std::shared_ptr<HDFDataset>>
50 setup_graph_containers(const Graph& g, const std::shared_ptr<HDFGroup>& grp)
51 {
52  // Collect some information on the graph
53  const auto num_vertices = boost::num_vertices(g);
54  const auto num_edges = boost::num_edges(g);
55 
56  // Get a logger to use here (Note: needs to have been setup beforehand)
57  spdlog::get("data_io")->info("Saving graph with {} vertices and {} edges "
58  "...", num_vertices, num_edges);
59 
60  // Initialize datasets to store vertices and edges in
61  auto dset_vertices = grp->open_dataset("_vertices", { num_vertices });
62  auto dset_edges = grp->open_dataset("_edges", { 2, num_edges });
63  // NOTE Need shape to be {2, num_edges} because `write` writes line by line.
64 
65  // Set attributes
66  dset_vertices->add_attribute("dim_name__0", "vertex_idx");
67  dset_vertices->add_attribute("coords_mode__vertex_idx", "trivial");
68 
69  dset_edges->add_attribute("dim_name__0", "label");
70  dset_edges->add_attribute("coords_mode__label", "values");
71  dset_edges->add_attribute("coords__label",
72  std::vector<std::string>{"source", "target"});
73  dset_edges->add_attribute("dim_name__1", "edge_idx");
74  dset_edges->add_attribute("coords_mode__edge_idx", "trivial");
75 
76  return std::make_pair(dset_vertices, dset_edges);
77 }
78 
80 template <typename First, typename Second, typename... Tail>
81 constexpr std::tuple<Tail...> tuple_tail(const std::tuple<First, Second,
82  Tail...>& t)
83 {
84  return std::apply([](auto, auto, auto... tail) {
85  return std::make_tuple(tail...);}, t);
86 }
87 
90 template <typename CoordT>
92 {
93  if constexpr (Utils::is_string_v<CoordT>) {
94  return std::vector<std::string>{};
95  }
96  else {
97  return std::vector<CoordT>{};
98  }
99 }
100 
102 enum class EntityKind {
103  vertex,
104  edge
105 };
106 
130 template <EntityKind entity_kind,
131  typename Graph,
132  typename NWGroup,
133  typename ItPair>
134 auto generate_write_function(const Graph& g,
135  NWGroup&& nw_grp,
136  std::string label,
137  ItPair&& it_pair,
138  std::size_t num_entities)
139 {
140  static_assert(entity_kind == EntityKind::vertex or
141  entity_kind == EntityKind::edge,
142  "Error, 'entity_kind' has to be either 'vertex' or 'edge'");
143 
144  return
145  [&g,
146  nw_grp{std::forward<NWGroup>(nw_grp)},
147  label{std::move(label)},
148  it_pair{std::forward<ItPair>(it_pair)},
149  num_entities{std::move(num_entities)}] (auto&& write_spec)
150  // By-reference captures are used where possible to avoid additional
151  // copies. Note that this is not possible for label and num_entities.
152  {
153 
154  std::string prop_prefix;
155 
156  if constexpr (entity_kind == EntityKind::vertex) {
157  prop_prefix = "vertex";
158  }
159  else {
160  prop_prefix = "edge";
161  }
162 
163  const auto grp =
164  nw_grp->open_group(std::get<0>(write_spec));
165 
166  // ... 1D case if second element of write_spec is a callable object
167  // such that the write specifications are of the form
168  // write_spec = (std::string name_adaptor, lambda adaptor)
169  if constexpr (Utils::is_callable_v<std::tuple_element_t<1,
170  std::decay_t<decltype(write_spec)>>>)
171  {
172  const auto dset = grp->open_dataset(label, { num_entities });
173 
174  dset->write(it_pair.first, it_pair.second,
175  [&](auto&& desc) {
176  return std::get<1>(write_spec)(desc, g);
177  }
178  );
179 
180  dset->add_attribute("dim_name__0", prop_prefix + "_idx");
181  dset->add_attribute("coords_mode__"s + prop_prefix + "_idx",
182  "trivial");
183  }
184 
185  // ... 2D case otherwise
186  // write_spec = (adaptor_name, dim0_name,
187  // (coord1, adaptor1),
188  // (coord2, adaptor2), ...)
189  else {
190 
191  static_assert(Utils::is_string_v<std::tuple_element_t<1,
192  std::decay_t<decltype(write_spec)>>>,
193  "Error, the name of dimension 0 has to be s string");
194 
195  // Extract the adaptor pairs (coord, adaptor) from write_spec tuple
196  const auto adaptor_pairs = tuple_tail(write_spec);
197  // FIXME This cannot be calculated at compile time since
198  // hana::for_each uses non-constexpr access.
199 
200  // Get number of adaptors
201  constexpr auto num_adaptors = std::tuple_size_v<std::decay_t<
202  decltype(adaptor_pairs)>>;
203 
204  // Open a 2D dataset
205  const auto dset = grp->open_dataset(label,
206  {num_adaptors, num_entities});
207 
208  // Deduce coordinate container type from adaptor_pairs
209  using WriteSpecT = std::decay_t<decltype(write_spec)>;
210  using FirstTupElementT = std::tuple_element_t<2, WriteSpecT>;
211  using CoordT = std::tuple_element_t<0, FirstTupElementT>;
212 
213  // Create the (empty) coordinates container
214  auto coords = coords_container<CoordT>();
215 
216  // Create the lambda that saves the data to the captured
217  // fire-and-forget dataset and stores the coordinate value
218  auto apply_adaptor = [&](auto&& adaptor_pair){
219  // adaptor_pair = (coordinate, adaptor_function)
220  dset->write(it_pair.first, it_pair.second,
221  [&](auto&& desc) {
222  return std::get<1>(adaptor_pair)(desc, g);
223  }
224  );
225 
226  static_assert(
227  // assert matching coordinate types
228  std::is_same_v<
229  std::tuple_element_t<0,
230  std::decay_t<decltype(adaptor_pair)>>,
231  CoordT>,
232  "Error, coordinate types do not match! Check that all "
233  "coordinates are of the same type");
234 
235  coords.push_back(std::get<0>(adaptor_pair));
236  };
237 
238  boost::hana::for_each(adaptor_pairs, apply_adaptor);
239 
240  // Extract the 0th dimension name from the write spec
241  const std::string dim0_name = std::get<1>(write_spec);
242 
243  // Set attributes
244  dset->add_attribute("dim_name__0", dim0_name);
245  dset->add_attribute("coords_mode__"s + dim0_name, "values");
246  dset->add_attribute("coords__"s + dim0_name, coords);
247 
248  dset->add_attribute("dim_name__1", prop_prefix + "_idx");
249  dset->add_attribute("coords_mode__"s + prop_prefix + "_idx",
250  "trivial");
251  }
252  };
253 }
254 
255 } // namespace GraphUtilsHelper
256 
289 template <typename Graph>
290 std::shared_ptr<HDFGroup>
291 create_graph_group(const Graph& g,
292  const std::shared_ptr<HDFGroup>& parent_grp,
293  const std::string& name)
294 {
295  // Create the group for the graph and store metadata in its attributes
296  auto grp = parent_grp->open_group(name);
297 
298  grp->add_attribute("content", "graph");
299  // Store additional metadata in the group attributes
300  grp->add_attribute("is_directed", boost::is_directed(g));
301  grp->add_attribute("allows_parallel", boost::allows_parallel_edges(g));
302 
303  // Store the information on the edge container shape
304  // NOTE Here, it is assumed that the edge data is written in shape (2,N).
305  // If this is not the case, the attribute must be overwritten.
306  grp->add_attribute("edge_container_is_transposed", true);
307 
308  // Prevent the vertex and edge dimension from being squeezed when selecting
309  // graph data via the GraphGroup.
310  grp->add_attribute(
311  "keep_dim", std::vector<std::string>{"vertex_idx", "edge_idx"}
312  );
313 
314  spdlog::get("data_io")->info("Opened graph group '{}'.", name);
315 
316  return grp;
317 }
318 
320 
330 template <typename Graph>
331 void
332 save_graph(const Graph& g, const std::shared_ptr<HDFGroup>& grp)
333 {
334  // Create and prepare the datasets for vertices and edges
335  auto [dset_vertices,
336  dset_edges] = GraphUtilsHelper::setup_graph_containers(g, grp);
337 
338  // Save vertex list
339  auto [v, v_end] = boost::vertices(g);
340  dset_vertices->write(v, v_end, [&](auto vd) {
341  return boost::get(boost::vertex_index_t(), g, vd);
342  });
343 
344  // Save edges
345  // NOTE Need to call write twice to achieve the desired data shape.
346  auto [e, e_end] = boost::edges(g);
347  dset_edges->write(e, e_end, [&](auto ed) {
348  return boost::get(boost::vertex_index_t(), g, boost::source(ed, g));
349  });
350 
351  dset_edges->write(e, e_end, [&](auto ed) {
352  return boost::get(boost::vertex_index_t(), g, boost::target(ed, g));
353  });
354 
355  spdlog::get("data_io")->debug("Graph saved.");
356 }
357 
359 
372 template <typename Graph, typename PropertyMap>
373 void
374 save_graph(const Graph& g,
375  const std::shared_ptr<HDFGroup>& grp,
376  const PropertyMap vertex_ids)
377 {
378  // Create and prepare the datasets for vertices and edges
379  auto [dset_vertices,
380  dset_edges] = GraphUtilsHelper::setup_graph_containers(g, grp);
381 
382  // Save vertex list
383  auto [v, v_end] = boost::vertices(g);
384  dset_vertices->write(
385  v, v_end, [&](auto vd) { return boost::get(vertex_ids, vd); });
386 
387  // Save edges
388  // NOTE Need to call write twice to achieve the desired data shape.
389  auto [e, e_end] = boost::edges(g);
390  dset_edges->write(e, e_end, [&](auto ed) {
391  return boost::get(vertex_ids, boost::source(ed, g));
392  });
393 
394  dset_edges->write(e, e_end, [&](auto ed) {
395  return boost::get(vertex_ids, boost::target(ed, g));
396  });
397 
398  spdlog::get("data_io")->debug("Graph saved.");
399 }
400 
448 template <IterateOver iterate_over, typename Graph, typename... Adaptors>
449 void
451  const std::shared_ptr<HDFGroup>& nw_grp,
452  const std::string& label,
453  const std::tuple<Adaptors...>& adaptor_tuple)
454 {
455 
456  static_assert(
457  // assert stringlike-ness of first thing
458  std::conjunction_v<Utils::is_string<
459  std::tuple_element_t<0, std::decay_t<Adaptors>>>...>,
460  "Error, the first entry of each entry of 'adaptor_tuple' has to be "
461  "string like, to name the adaptor");
462 
463  static_assert(
464  iterate_over == IterateOver::vertices or
465  iterate_over == IterateOver::edges, "Error, unknown iterator type! "
466  "Has to be either 'IterateOver::vertices' or 'IterateOver::edges'"
467  );
468 
469  // NOTE Consider a check for literal types, which is more capable than
470  // the (now removed) std::is_literal_type ...
471 
472  // Get a logger
473  auto log = spdlog::get("data_io");
474 
477 
478  // ... vertex_iterator ...
479  if constexpr (iterate_over == IterateOver::vertices)
480  {
481  // Collect some information on the graph
482  const auto num_vertices = boost::num_vertices(g);
483 
484  // Make vertex iterator pair
485  auto it_pair = GraphUtils::iterator_pair<IterateOver::vertices>(g);
486 
487  // Get write function that saves data given by the adaptor(s) to new
488  // dataset(s), fire-and-forget
489  auto write_f = generate_write_function<EntityKind::vertex>(
490  g, nw_grp,
491  label,
492  it_pair,
493  num_vertices);
494 
495  boost::hana::for_each(adaptor_tuple, write_f);
496 
497  log->debug("Graph vertex properties saved with label '{}'.", label);
498  }
499 
500  // ... and edge_iterator
501  else {
502  // Collect some information on the graph
503  const auto num_edges = boost::num_edges(g);
504 
505  // Make edge iterator pair
506  auto it_pair = GraphUtils::iterator_pair<IterateOver::edges>(g);
507 
508  // Get write function that saves data given by the adaptor(s) to new
509  // dataset(s), fire-and-forget
510  auto write_f = generate_write_function<EntityKind::edge>(
511  g, nw_grp,
512  label,
513  it_pair,
514  num_edges);
515 
516  boost::hana::for_each(adaptor_tuple, write_f);
517 
518  log->debug("Graph edge properties saved with label '{}'.", label);
519  }
520 }
521 
540 template <typename Graph, typename... Adaptors>
541 void
543  const std::shared_ptr<HDFGroup>& nw_grp,
544  const std::string& label,
545  const std::tuple<Adaptors...>& adaptor_tuple)
546 {
547  save_graph_entity_properties<IterateOver::vertices>(std::forward<Graph>(g),
548  nw_grp,
549  label,
550  adaptor_tuple);
551 }
552 
571 template <typename Graph, typename... Adaptors>
572 void
574  const std::shared_ptr<HDFGroup>& nw_grp,
575  const std::string& label,
576  const std::tuple<Adaptors...>& adaptor_tuple)
577 {
578  save_graph_entity_properties<IterateOver::edges>(std::forward<Graph>(g),
579  nw_grp,
580  label,
581  adaptor_tuple);
582 }
583 
584 // TODO add functions here to open datasets for edge or vertex attributes.
585  // end of group GraphUtilities // end of group DataIO
588 
589 } // namespace DataIO
590 } // namespace Utopia
591 
592 #endif // UTOPIA_DATAIO_GRAPH_UTILS_HH
void for_each(const Utopia::ExecPolicy policy, InputIt first, InputIt last, UnaryFunction f)
Apply a function to a range.
Definition: parallel.hh:346
IterateOver
Over which graph entity to iterate.
Definition: iterator.hh:19
void save_graph(const Graph &g, const std::shared_ptr< HDFGroup > &grp, const PropertyMap vertex_ids)
Write function for a boost::Graph.
Definition: graph_utils.hh:374
void save_edge_properties(Graph &&g, const std::shared_ptr< HDFGroup > &nw_grp, const std::string &label, const std::tuple< Adaptors... > &adaptor_tuple)
Definition: graph_utils.hh:573
std::shared_ptr< HDFGroup > create_graph_group(const Graph &g, const std::shared_ptr< HDFGroup > &parent_grp, const std::string &name)
Definition: graph_utils.hh:291
void save_vertex_properties(Graph &&g, const std::shared_ptr< HDFGroup > &nw_grp, const std::string &label, const std::tuple< Adaptors... > &adaptor_tuple)
Definition: graph_utils.hh:542
void save_graph_entity_properties(const Graph &g, const std::shared_ptr< HDFGroup > &nw_grp, const std::string &label, const std::tuple< Adaptors... > &adaptor_tuple)
Definition: graph_utils.hh:450
This is the central file of the HDF5 dataIO module of Utopia and provides a class for writing to,...
This file provides a class for creating and managing groups in a HDF5 file, which then can create oth...
std::pair< std::shared_ptr< HDFDataset >, std::shared_ptr< HDFDataset > > setup_graph_containers(const Graph &g, const std::shared_ptr< HDFGroup > &grp)
Definition: graph_utils.hh:50
auto generate_write_function(const Graph &g, NWGroup &&nw_grp, std::string label, ItPair &&it_pair, std::size_t num_entities)
Definition: graph_utils.hh:134
EntityKind
Kinds of Entities to get properties from.
Definition: graph_utils.hh:102
auto coords_container()
Definition: graph_utils.hh:91
constexpr std::tuple< Tail... > tuple_tail(const std::tuple< First, Second, Tail... > &t)
Builds new tuple containing all elements but the first two.
Definition: graph_utils.hh:81
constexpr bool is_directed()
Check whether the network type allows for directed edges.
Definition: utils.hh:35
constexpr bool is_callable_v
Shorthand for is_callable<T>::value.
Definition: type_traits.hh:734
constexpr bool is_string_v
Shorthand for 'is_string<T>::value'.
Definition: type_traits.hh:140
Definition: agent.hh:11
Check if a type T is a string-like type, i.e. std::basic_string, const char*, char*,...
Definition: type_traits.hh:105