LArSoft  v09_90_00
Liquid Argon Software toolkit - https://larsoft.org/
prune_configuration.cc
Go to the documentation of this file.
5 #include "boost/algorithm/string.hpp"
7 #include "cetlib/container_algorithms.h"
9 #include "range/v3/view.hpp"
10 
11 #include <initializer_list>
12 #include <iostream>
13 #include <regex>
14 #include <set>
15 
16 using namespace fhicl;
17 using namespace std::string_literals;
18 using namespace art::detail;
19 
22 using modules_t = std::map<std::string, std::string>;
23 
24 namespace {
25 
26  std::string const at_nil{"@nil"};
27  std::string const trigger_paths_str{"trigger_paths"};
28  std::string const end_paths_str{"end_paths"};
29  bool
30  matches(std::string const& path_spec_str, std::string const& path_name)
31  {
32  std::regex const path_spec_re{R"((\d+:)?)" + path_name};
33  return std::regex_match(path_spec_str, path_spec_re);
34  }
35 
36  auto module_tables = {"physics.producers",
37  "physics.filters",
38  "physics.analyzers",
39  "outputs"};
40  auto modifier_tables = {"physics.producers", "physics.filters"};
41  auto observer_tables = {"physics.analyzers", "outputs"};
42 
43  auto allowed_physics_tables = {"producers", "filters", "analyzers"};
44 
45  enum class ModuleCategory { modifier, observer, unset };
46 
47  auto
48  config_exception(std::string const& context)
49  {
50  return art::Exception{art::errors::Configuration, context + '\n'};
51  }
52 
53  auto
54  path_exception(std::string const& selection_override,
55  std::size_t const i,
56  std::string const& suffix)
57  {
58  std::string msg{"The following error occurred while processing " +
59  selection_override + "[" + std::to_string(i) + "]"};
60  msg += " (i.e. '" + suffix + "'):";
61  return config_exception(msg);
62  }
63 
64  std::ostream&
65  operator<<(std::ostream& os, ModuleCategory const cat)
66  {
67  switch (cat) {
68  case ModuleCategory::modifier:
69  os << "modifier";
70  break;
71  case ModuleCategory::observer:
72  os << "observer";
73  break;
74  case ModuleCategory::unset:
75  os << "unset";
76  }
77  return os;
78  }
79 
81  opposite(ModuleCategory const cat)
82  {
83  auto result = ModuleCategory::unset;
84  switch (cat) {
85  case ModuleCategory::modifier:
86  result = ModuleCategory::observer;
87  break;
88  case ModuleCategory::observer:
89  result = ModuleCategory::modifier;
90  break;
91  case ModuleCategory::unset: {
93  << "The " << cat << " category has no opposite.\n";
94  }
95  }
96  return result;
97  }
98 
99  auto
100  module_category(std::string const& full_module_key)
101  {
102  // For a full module key (e.g. physics.analyzers.a), we strip off
103  // the module name ('a') to determine its module category.
104  auto const table =
105  full_module_key.substr(0, full_module_key.find_last_of('.'));
106  if (cet::search_all(modifier_tables, table)) {
107  return ModuleCategory::modifier;
108  }
109  if (cet::search_all(observer_tables, table)) {
110  return ModuleCategory::observer;
111  }
112  return ModuleCategory::unset;
113  }
114 
115  bool
116  is_path_selection_override(std::string const& path_name)
117  {
118  return path_name == trigger_paths_str or path_name == end_paths_str;
119  }
120 
121  std::string
122  path_selection_override(ModuleCategory const category)
123  {
124  switch (category) {
125  case ModuleCategory::modifier:
126  return trigger_paths_str;
127  case ModuleCategory::observer:
128  return end_paths_str;
129  default:
130  assert(false); // Unreachable
131  return {};
132  }
133  }
134 
135  // module name => full module key
136  modules_t
137  declared_modules(intermediate_table const& config,
138  std::initializer_list<char const*> tables)
139  {
140  modules_t result;
141  for (auto const tbl : tables) {
142  if (!exists_outside_prolog(config, tbl))
143  continue;
144  table_t const& table = config.find(tbl);
145  for (auto const& [modname, value] : table) {
146  // Record only tables, which are the allowed FHiCL values for
147  // module configurations.
148  if (!value.is_a(TABLE)) {
149  continue;
150  }
151 
152  auto const key = fhicl_key(tbl, modname);
153  auto const [it, success] = result.try_emplace(modname, fhicl_key(key));
154  if (!success && it->second != key) {
155  auto const& cached_key = it->second;
156  auto const parent =
157  cached_key.substr(0, cached_key.rfind(modname) - 1);
158  throw config_exception("An error occurred while processing "
159  "module configurations.")
160  << "Module label '" << modname << "' has been used in '" << tbl
161  << "' and '" << parent << "'.\n"
162  << "Module labels must be unique across an art process.\n";
163  }
164  }
165  }
166  return result;
167  }
168 
169 }
170 
171 namespace art::detail {
172  std::vector<ModuleSpec>
173  sequence_to_entries(sequence_t const& seq, bool const allow_nil_entries)
174  {
175  std::vector<ModuleSpec> result;
176  for (auto const& ev : seq) {
177  if (allow_nil_entries and ev.is_a(NIL)) {
178  result.push_back({at_nil, FilterAction::Normal});
179  continue;
180  }
181  if (!ev.is_a(STRING)) {
182  continue;
183  }
184  auto mod_spec = ev.to_string();
185  if (empty(mod_spec)) {
186  continue;
187  }
188  boost::replace_all(mod_spec, "\"", "");
189  auto action = FilterAction::Normal;
190  if (mod_spec[0] == '!') {
191  action = FilterAction::Veto;
192  mod_spec = mod_spec.substr(1);
193  } else if (mod_spec[0] == '-') {
194  action = FilterAction::Ignore;
195  mod_spec = mod_spec.substr(1);
196  }
197 
198  // Handle remaining '!' or '-' characters
199  if (mod_spec.find_first_of("!-") != std::string::npos) {
200  throw config_exception("There was an error parsing the entry "s +
201  ev.to_string() + "in a FHiCL sequence.")
202  << "The '!' or '-' character may appear as only the first character "
203  "in the path entry.\n";
204  }
205  result.push_back({mod_spec, action});
206  }
207 
208  if (result.size() != seq.size()) {
209  throw config_exception("There was an error parsing the specified entries "
210  "in a FHiCL sequence.")
211  << "One of the presented elements is either an empty string or not a "
212  "string at all.\n";
213  }
214  return result;
215  }
216 
217  std::vector<art::PathSpec>
218  path_specs(std::vector<ModuleSpec> const& selection_override_entries,
219  std::string const& path_selection_override)
220  {
221  auto guidance = [](std::string const& name,
222  std::string const& path_selection_override,
223  std::string const& id_str) {
224  std::ostringstream oss;
225  oss << "If you would like to repeat the path specification, all "
226  "path specifications\n"
227  "with the name '"
228  << name << "' must be prepended with the same path ID (e.g.):\n\n"
229  << " " << path_selection_override << ": ['" << id_str << ':' << name
230  << "', '" << id_str << ':' << name << "', ...]\n\n";
231  return oss.str();
232  };
233 
234  std::map<PathID, std::string> id_to_name;
235  std::map<std::string, PathID> name_to_id;
236  std::vector<art::PathSpec> result;
237 
238  size_t i = 0;
239  for (auto it = cbegin(selection_override_entries),
240  e = cend(selection_override_entries);
241  it != e;
242  ++it, ++i) {
243  auto const& path = *it;
244  auto spec = art::path_spec(path.name);
245  if (spec.name == at_nil) {
246  continue;
247  }
248 
249  // Path names with unspecified IDs cannot be reused
250  auto const emplacement_result =
251  name_to_id.try_emplace(spec.name, spec.path_id);
252  bool const name_already_present = not emplacement_result.second;
253  auto const emplaced_path_id = emplacement_result.first->second;
254 
255  if (name_already_present) {
256  if (spec.path_id == art::PathID::invalid()) {
257  throw path_exception(path_selection_override, i, path.name)
258  << "The path name '" << spec.name
259  << "' has already been specified in the " << path_selection_override
260  << " sequence.\n"
261  << guidance(spec.name,
262  path_selection_override,
263  to_string(emplaced_path_id));
264  }
265  if (spec.path_id != emplaced_path_id) {
266  throw path_exception(path_selection_override, i, path.name)
267  << "The path name '" << spec.name
268  << "' has already been specified (perhaps implicitly) with a\n"
269  "path ID of "
270  << to_string(emplaced_path_id) << " (not "
271  << to_string(spec.path_id) << ") in the " << path_selection_override
272  << " sequence.\n\n"
273  << guidance(spec.name,
274  path_selection_override,
275  to_string(emplaced_path_id));
276  }
277  // Name is already present and the PathID has been explicitly
278  // listed and matches what has already been seen.
279  continue;
280  }
281 
282  if (spec.path_id == art::PathID::invalid()) {
283  spec.path_id =
284  art::PathID{i}; // Use calculated bit number if not specified
285  emplacement_result.first->second = spec.path_id;
286  }
287 
288  // Each ID must have only one name
289  if (auto const [it, inserted] =
290  id_to_name.try_emplace(spec.path_id, spec.name);
291  not inserted) {
292  throw path_exception(path_selection_override, i, path.name)
293  << "Path ID " << to_string(spec.path_id)
294  << " cannot be assigned to path name '" << spec.name
295  << "' as it has already been assigned to path name '" << it->second
296  << "'.\n";
297  }
298 
299  result.push_back(std::move(spec));
300  }
301  return result;
302  }
303 }
304 
305 namespace {
306  void
307  verify_supported_names(table_t const& physics_table)
308  {
309  std::string bad_names{};
310  for (auto const& [name, value] : physics_table) {
311  if (value.is_a(SEQUENCE)) {
312  continue;
313  }
314 
315  bool const is_table = value.is_a(TABLE);
316  if (is_table and cet::search_all(allowed_physics_tables, name)) {
317  continue;
318  }
319  std::string const type = is_table ? "table" : "atom";
320  bad_names += " \"physics." + name + "\" (" + type + ")\n";
321  }
322 
323  if (empty(bad_names)) {
324  return;
325  }
326 
327  throw config_exception(
328  "\nYou have specified the following unsupported parameters in the\n"
329  "\"physics\" block of your configuration:\n")
330  << bad_names
331  << "\nSupported parameters include the following tables:\n"
332  " \"physics.producers\"\n"
333  " \"physics.filters\"\n"
334  " \"physics.analyzers\"\n"
335  "and sequences. Atomic configuration parameters are not "
336  "allowed.\n\n";
337  }
338 
339  void
340  replace_empty_paths(intermediate_table& config,
341  std::set<std::string> const& empty_paths,
342  std::string const& path_selection_override)
343  {
344  if (not exists_outside_prolog(config, path_selection_override)) {
345  return;
346  }
347  sequence_t result = config.find(path_selection_override);
348  for (auto const& name : empty_paths) {
349  std::replace_if(
350  begin(result),
351  end(result),
352  [&name](auto const& ex_val) {
353  if (not ex_val.is_a(STRING)) {
354  return false;
355  }
356  std::string path_spec_str;
357  fhicl::detail::decode(ex_val.value, path_spec_str);
358  return matches(path_spec_str, name);
359  },
360  fhicl::extended_value{false, NIL, at_nil});
361  }
362  config.get<sequence_t&>(path_selection_override) = result;
363  }
364 
366  all_paths(intermediate_table& config)
367  {
368  std::string const physics{"physics"};
369  if (!exists_outside_prolog(config, physics))
370  return {};
371 
372  std::set<std::string> empty_paths;
373  std::map<std::string, std::vector<ModuleSpec>> paths;
374  table_t const& table = config.find(physics);
375  verify_supported_names(table);
376  for (auto const& [path_name, module_names] : table) {
377  if (!module_names.is_a(SEQUENCE)) {
378  continue;
379  }
380  sequence_t const entries = module_names;
381  if (empty(entries)) {
382  empty_paths.insert(path_name);
383  }
384  paths[path_name] = sequence_to_entries(
385  module_names, is_path_selection_override(path_name));
386  }
387 
388  // Replace empty paths from trigger_paths and end_paths with @nil
389  replace_empty_paths(
390  config, empty_paths, fhicl_key(physics, trigger_paths_str));
391  replace_empty_paths(config, empty_paths, fhicl_key(physics, end_paths_str));
392 
393  return paths;
394  }
395 
396  // The return type of 'paths_for_category' is a vector of pairs -
397  // i.e. hence the "ordered" component of the return type. By
398  // choosing this return type, we are able to preserve the
399  // user-specified order in 'trigger_paths'. In this case, art
400  // specified the ordering for the user, but we use the return type
401  // that matches that of the 'explicitly_declared_paths' function.
402 
404  paths_for_category(module_entries_for_path_t const& all_paths,
405  modules_t const& modules,
406  ModuleCategory const category)
407  {
409  module_entries_for_path_t sorted_result;
410  for (auto const& [path_name, entries] : all_paths) {
411  // Skip over special path names, which are handled later.
412  if (is_path_selection_override(path_name)) {
413  continue;
414  }
415  std::vector<ModuleSpec> right_modules;
416  std::vector<std::string> wrong_modules;
417  for (auto const& mod_spec : entries) {
418  auto const& name = mod_spec.name;
419  auto full_module_key_it = modules.find(name);
420  if (full_module_key_it == cend(modules)) {
421  throw config_exception("The following error occurred while "
422  "processing a path configuration:")
423  << "Entry with name " << name << " in path " << path_name
424  << " does not have a module configuration.\n";
425  }
426  auto const& full_module_key = full_module_key_it->second;
427  auto const module_cat = module_category(full_module_key);
428  assert(module_cat != ModuleCategory::unset);
429  if (module_cat == category) {
430  right_modules.push_back(mod_spec);
431  } else {
432  wrong_modules.push_back(name);
433  }
434  }
435 
436  if (right_modules.empty()) {
437  // None of the modules in the path was of the correct
438  // category. This means that all modules were of the opposite
439  // category--we can safely skip it.
440  continue;
441  }
442 
443  if (right_modules.size() == entries.size()) {
444  sorted_result.try_emplace(path_name, std::move(right_modules));
445  } else {
446  // This is the case where a path contains a mixture of
447  // modifiers and observers.
448  auto e = config_exception("An error occurred while "
449  "processing a path configuration.");
450  e << "The following modules specified in path " << path_name << " are "
451  << opposite(category)
452  << "s when all\n"
453  "other modules are "
454  << category << "s:\n";
455  for (auto const& modname : wrong_modules) {
456  e << " '" << modname << "'\n";
457  }
458  throw e;
459  }
460  }
461 
462  // Convert to correct type
464  size_t i = 0;
465  for (auto const& [path_name, modules] : sorted_result) {
466  result.emplace_back(art::PathSpec{path_name, art::PathID{i++}}, modules);
467  }
468  return result;
469  }
470 
472  explicitly_declared_paths(module_entries_for_path_t const& modules_for_path,
473  std::vector<ModuleSpec> const& override_entries,
474  modules_t const& modules,
475  std::string const& path_selection_override)
476  {
477  auto specs =
478  art::detail::path_specs(override_entries, path_selection_override);
479 
481 
482  std::ostringstream os;
483  for (auto& spec : specs) {
484  auto res = modules_for_path.find(spec.name);
485  if (res == cend(modules_for_path)) {
486  os << "Unknown path " << spec.name << " has been specified in '"
487  << path_selection_override << "'.\n";
488  continue;
489  }
490 
491  // Skip empty paths
492  if (empty(res->second)) {
493  continue;
494  }
495 
496  // Check that module names in paths are supported
497  for (auto const& entry : res->second) {
498  auto const& name = entry.name;
499  auto full_module_key_it = modules.find(name);
500  if (full_module_key_it == cend(modules)) {
501  throw config_exception("The following error occurred while "
502  "processing a path configuration:")
503  << "Entry with name " << name << " in path " << spec.name
504  << " does not have a module configuration.\n";
505  }
506  }
507  result.emplace_back(std::move(spec), res->second);
508  }
509 
510  auto const err = os.str();
511  if (!err.empty()) {
512  throw config_exception(
513  "The following error occurred while processing path configurations:")
514  << err;
515  }
516 
517  return result;
518  }
519 
521  get_enabled_modules(modules_t const& modules,
522  module_entries_for_ordered_path_t const& enabled_paths,
523  ModuleCategory const category)
524  {
525  keytype_for_name_t result;
526  for (auto const& [path_spec, entries] : enabled_paths) {
527  for (auto const& [module_name, action] : entries) {
528  auto const& module_key = modules.at(module_name);
529  auto const actual_category = module_category(module_key);
530  auto const type = module_type(module_key);
531  if (actual_category != category) {
532  throw config_exception("The following error occurred while "
533  "processing a path configuration:")
534  << "The '" << path_selection_override(category)
535  << "' override parameter contains the path " << path_spec.name
536  << ", which has"
537  << (actual_category == ModuleCategory::observer ? " an\n" : " a\n")
538  << to_string(type) << " with the name " << module_name << ".\n\n"
539  << "Path " << path_spec.name
540  << " should instead be included as part of the '"
541  << path_selection_override(opposite(category)) << "' parameter.\n"
542  << "Contact artists@fnal.gov for guidance.\n";
543  }
544  if (action != art::detail::FilterAction::Normal &&
546  throw config_exception("The following error occurred while "
547  "processing a path configuration:")
548  << "Entry with name " << module_name << " in path "
549  << path_spec.name << " is"
550  << (category == ModuleCategory::observer ? " an " : " a ")
551  << to_string(type) << " and cannot have a '!' or '-' prefix.\n";
552  }
553  result.try_emplace(module_name, ModuleKeyAndType{module_key, type});
554  }
555  }
556  return result;
557  }
558 
559  std::pair<module_entries_for_ordered_path_t, bool>
560  enabled_paths(module_entries_for_path_t const& paths,
561  modules_t const& modules,
562  ModuleCategory const category)
563  {
564  auto const selection_override = path_selection_override(category);
565  auto const it = paths.find(selection_override);
566  if (it == cend(paths)) {
567  return {paths_for_category(paths, modules, category), false};
568  }
569 
570  return {
571  explicitly_declared_paths(paths, it->second, modules, selection_override),
572  true};
573  }
574 }
575 
577 art::detail::prune_config_if_enabled(bool const prune_config,
578  bool const report_enabled,
579  intermediate_table& config)
580 {
581  auto const modules = declared_modules(config, module_tables);
582 
583  auto paths = all_paths(config);
584 
585  auto [trigger_paths, trigger_paths_override] =
586  enabled_paths(paths, modules, ModuleCategory::modifier);
587  auto [end_paths, end_paths_override] =
588  enabled_paths(paths, modules, ModuleCategory::observer);
589 
590  auto enabled_modules =
591  get_enabled_modules(modules, trigger_paths, ModuleCategory::modifier);
592  // C++17 provides the std::map::merge member function, but Clang 7
593  // and older does not support it. Will do it by hand for now, until
594  // we have time to handle this properly.
595  auto end_path_enabled_modules =
596  get_enabled_modules(modules, end_paths, ModuleCategory::observer);
597  enabled_modules.insert(begin(end_path_enabled_modules),
598  end(end_path_enabled_modules));
599 
600  modules_t unused_modules;
601  for (auto const& pr : modules) {
602  if (enabled_modules.find(pr.first) == cend(enabled_modules)) {
603  unused_modules.insert(pr);
604  }
605  }
606 
607  // Find unused paths
608  using namespace ::ranges;
609  paths.erase("trigger_paths");
610  paths.erase("end_paths");
611  for (auto const& spec : trigger_paths | views::keys) {
612  paths.erase(spec.name);
613  }
614  for (auto const& spec : end_paths | views::keys) {
615  paths.erase(spec.name);
616  }
617 
618  // The only paths left are those that are not enabled for execution.
619  if (report_enabled && !empty(paths)) {
620  std::cerr << "The following paths have not been enabled for execution and "
621  "will be ignored:\n";
622  for (auto const& path_name : paths | views::keys) {
623  std::cerr << " " << path_name << '\n';
624  }
625  }
626 
627  if (report_enabled && !empty(unused_modules)) {
628  std::ostringstream os;
629  os << "The following module label"
630  << ((unused_modules.size() == 1) ? " is" : "s are")
631  << " either not assigned to any path,\n"
632  << "or " << ((unused_modules.size() == 1ull) ? "it has" : "they have")
633  << " been assigned to ignored path(s):\n";
634  for (auto const& label : unused_modules | views::keys) {
635  os << " " << label << '\n';
636  }
637  std::cerr << os.str();
638  }
639 
640  if (prune_config) {
641  auto to_full_path_name = [](auto const& path_name) {
642  return fhicl_key("physics", path_name);
643  };
644  for (auto const& key :
645  paths | views::keys | views::transform(to_full_path_name)) {
646  config.erase(key);
647  }
648  for (auto const& label : unused_modules | views::values) {
649  config.erase(label);
650  }
651 
652  // Check if module tables can be removed
653  auto if_outside_prolog = [&config](auto const& table_name) {
654  return exists_outside_prolog(config, table_name);
655  };
656  for (auto const& table_name :
657  module_tables | views::filter(if_outside_prolog)) {
658  if (table_t const value = config.find(table_name); empty(value)) {
659  config.erase(table_name);
660  }
661  }
662 
663  // Check if top-level physics table can be removed
664  if (exists_outside_prolog(config, "physics")) {
665  if (table_t const value = config.find("physics"); empty(value)) {
666  config.erase("physics");
667  }
668  }
669  }
670 
671  // Ensure that trigger_paths/end_paths is present in the configuration
672  if (not empty(end_paths) and not end_paths_override) {
673  // We do not return the Path ID for end paths as they are not meaningful.
674  auto end_paths_entries =
675  end_paths | views::keys |
676  views::transform([](auto const& path_spec) { return path_spec.name; }) |
677  to<std::vector>();
678 
679  config.put("physics.end_paths", std::move(end_paths_entries));
680  }
681 
682  // Place 'trigger_paths' as top-level configuration table
683  if (not empty(trigger_paths)) {
684  auto trigger_paths_entries = trigger_paths | views::keys |
685  views::transform([](auto const& path_spec) {
686  return to_string(path_spec);
687  }) |
688  to<std::vector>();
689  if (not trigger_paths_override) {
690  config.put("physics.trigger_paths", trigger_paths_entries);
691  }
692  config.put("trigger_paths.trigger_paths", std::move(trigger_paths_entries));
693  }
694 
695  return EnabledModules{std::move(enabled_modules),
696  std::move(trigger_paths),
697  std::move(end_paths),
698  trigger_paths_override,
699  end_paths_override};
700 }
ModuleCategory
decltype(auto) constexpr cend(T &&obj)
ADL-aware version of std::cend.
Definition: StdUtils.h:93
bool exists_outside_prolog(fhicl::intermediate_table const &config, std::string const &key)
std::vector< ModuleSpec > sequence_to_entries(sequence_t const &seq, bool const allow_nil_entries)
std::map< std::string, ModuleKeyAndType > keytype_for_name_t
ModuleType module_type(std::string const &full_key)
std::map< std::string, std::vector< ModuleSpec >> module_entries_for_path_t
std::string to_string(Protection p)
Definition: Protection.cc:4
void decode(std::any const &, std::string &)
std::map< std::string, std::string > modules_t
decltype(auto) constexpr end(T &&obj)
ADL-aware version of std::end.
Definition: StdUtils.h:77
EnabledModules prune_config_if_enabled(bool prune_config, bool report_enabled, fhicl::intermediate_table &config)
decltype(auto) constexpr to_string(T &&obj)
ADL-aware version of std::to_string.
decltype(auto) values(Coll &&coll)
Range-for loop helper iterating across the values of the specified collection.
parameter set interface
std::enable_if_t< std::is_convertible_v< T, std::string >, std::string > fhicl_key(T const &name)
Definition: fhicl_key.h:12
std::vector< extended_value > sequence_t
shims::map< std::string, extended_value > table_t
std::string name
Definition: PathSpec.h:48
bool put(std::string const &name, std::string const &value, bool in_prolog=false)
double value
Definition: spectrum.C:18
fhicl::extended_value::sequence_t sequence_t
cet::coded_exception< errors::ErrorCodes, ExceptionDetail::translate > Exception
Definition: Exception.h:66
void erase(std::string const &key, bool in_prolog=false)
extended_value const & find(std::string const &key) const
constexpr static auto invalid() noexcept
Definition: PathSpec.h:20
T get(std::string const &name)
decltype(auto) constexpr cbegin(T &&obj)
ADL-aware version of std::cbegin.
Definition: StdUtils.h:85
bool is_table(par_type const pt)
PathSpec path_spec(std::string const &path_spec)
Definition: PathSpec.cc:22
std::vector< art::PathSpec > path_specs(std::vector< ModuleSpec > const &selection_override_entries, std::string const &path_selection_override)
decltype(auto) constexpr begin(T &&obj)
ADL-aware version of std::begin.
Definition: StdUtils.h:69
std::ostream & operator<<(std::ostream &, ParameterSetID const &)
Float_t e
Definition: plot.C:35
decltype(auto) constexpr empty(T &&obj)
ADL-aware version of std::empty.
Definition: StdUtils.h:109
std::vector< std::pair< PathSpec, std::vector< ModuleSpec >>> module_entries_for_ordered_path_t