1 // Copyright(C) 1999-2021 National Technology & Engineering Solutions
2 // of Sandia, LLC (NTESS).  Under the terms of Contract DE-NA0003525 with
3 // NTESS, the U.S. Government retains certain rights in this software.
4 //
5 // See packages/seacas/LICENSE for details
6 
7 #include <Ioss_BoundingBox.h>
8 #include <Ioss_CodeTypes.h>
9 #include <Ioss_CommSet.h>
10 #include <Ioss_DBUsage.h>
11 #include <Ioss_DatabaseIO.h>
12 #include <Ioss_ElementTopology.h>
13 #include <Ioss_EntityBlock.h>
14 #include <Ioss_Field.h>
15 #include <Ioss_FileInfo.h>
16 #include <Ioss_GroupingEntity.h>
17 #include <Ioss_NodeBlock.h>
18 #include <Ioss_ParallelUtils.h>
19 #include <Ioss_Property.h>
20 #include <Ioss_Region.h>
21 #include <Ioss_SerializeIO.h>
22 #include <Ioss_SideBlock.h>
23 #include <Ioss_SideSet.h>
24 #include <Ioss_Sort.h>
25 #include <Ioss_State.h>
26 #include <Ioss_StructuredBlock.h>
27 #include <Ioss_SurfaceSplit.h>
28 #include <Ioss_Utils.h>
29 #include <algorithm>
30 #include <cassert>
31 #include <cfloat>
32 #include <cstddef>
33 #include <cstring>
34 #include <fmt/ostream.h>
35 #include <iomanip>
36 #include <iostream>
37 #include <iterator>
38 #include <set>
39 #include <string>
40 #include <tokenize.h>
41 #include <utility>
42 #include <vector>
43 
44 #if defined SEACAS_HAVE_DATAWARP
45 extern "C" {
46 #include <datawarp.h>
47 }
48 #endif
49 
50 namespace {
51   auto initial_time = std::chrono::steady_clock::now();
52 
53   void log_time(std::chrono::time_point<std::chrono::steady_clock> &start,
54                 std::chrono::time_point<std::chrono::steady_clock> &finish, int current_state,
55                 double state_time, bool is_input, bool single_proc_only,
56                 const Ioss::ParallelUtils &util);
57 
58   void log_field(const char *symbol, const Ioss::GroupingEntity *entity, const Ioss::Field &field,
59                  bool single_proc_only, const Ioss::ParallelUtils &util);
60 
61 #ifndef NDEBUG
internal_parallel_consistent(bool single_proc_only,const Ioss::GroupingEntity * ge,const Ioss::Field & field,int in_out,const Ioss::ParallelUtils & util)62   bool internal_parallel_consistent(bool single_proc_only, const Ioss::GroupingEntity *ge,
63                                     const Ioss::Field &field, int in_out,
64                                     const Ioss::ParallelUtils &util)
65   {
66     if (single_proc_only) {
67       return true;
68     }
69 
70     const std::string &field_name = field.get_name();
71     unsigned int       hash_code  = ge->hash() + Ioss::Utils::hash(field_name);
72     unsigned int       max_hash   = util.global_minmax(hash_code, Ioss::ParallelUtils::DO_MAX);
73     unsigned int       min_hash   = util.global_minmax(hash_code, Ioss::ParallelUtils::DO_MIN);
74     if (max_hash != min_hash) {
75       const std::string &ge_name = ge->name();
76       fmt::print(Ioss::WARNING(),
77                  "[{}] Parallel inconsistency detected for {} field '{}' on entity '{}'. (Hash: {} "
78                  "{} {})\n",
79                  in_out == 0 ? "writing" : "reading", util.parallel_rank(), field_name, ge_name,
80                  hash_code, min_hash, max_hash);
81       return false;
82     }
83     return true;
84   }
85 #endif
my_min(double x1,double x2)86   double my_min(double x1, double x2) { return x1 < x2 ? x1 : x2; }
87 
my_max(double x1,double x2)88   double my_max(double x1, double x2) { return x1 > x2 ? x1 : x2; }
89 
90   template <typename INT>
calc_bounding_box(size_t ndim,size_t node_count,std::vector<double> & coordinates,std::vector<INT> & connectivity,double & xmin,double & ymin,double & zmin,double & xmax,double & ymax,double & zmax)91   void calc_bounding_box(size_t ndim, size_t node_count, std::vector<double> &coordinates,
92                          std::vector<INT> &connectivity, double &xmin, double &ymin, double &zmin,
93                          double &xmax, double &ymax, double &zmax)
94   {
95     std::vector<int> elem_block_nodes(node_count);
96     for (auto &node : connectivity) {
97       elem_block_nodes[node - 1] = 1;
98     }
99 
100     xmin = DBL_MAX;
101     ymin = DBL_MAX;
102     zmin = DBL_MAX;
103 
104     xmax = -DBL_MAX;
105     ymax = -DBL_MAX;
106     zmax = -DBL_MAX;
107 
108     for (size_t i = 0; i < node_count; i++) {
109       if (elem_block_nodes[i] == 1) {
110         xmin = my_min(xmin, coordinates[ndim * i + 0]);
111         xmax = my_max(xmax, coordinates[ndim * i + 0]);
112 
113         if (ndim > 1) {
114           ymin = my_min(ymin, coordinates[ndim * i + 1]);
115           ymax = my_max(ymax, coordinates[ndim * i + 1]);
116         }
117 
118         if (ndim > 2) {
119           zmin = my_min(zmin, coordinates[ndim * i + 2]);
120           zmax = my_max(zmax, coordinates[ndim * i + 2]);
121         }
122       }
123     }
124     if (ndim < 3) {
125       zmin = zmax = 0.0;
126     }
127     if (ndim < 2) {
128       ymin = ymax = 0.0;
129     }
130   }
131 } // namespace
132 
133 namespace Ioss {
DatabaseIO(Region * region,std::string filename,DatabaseUsage db_usage,MPI_Comm communicator,const PropertyManager & props)134   DatabaseIO::DatabaseIO(Region *region, std::string filename, DatabaseUsage db_usage,
135                          MPI_Comm communicator, const PropertyManager &props)
136       : properties(props), DBFilename(std::move(filename)), dbUsage(db_usage),
137         util_(db_usage == WRITE_HISTORY || db_usage == WRITE_HEARTBEAT ? MPI_COMM_SELF
138                                                                        : communicator),
139         region_(region), isInput(is_input_event(db_usage)),
140         singleProcOnly(db_usage == WRITE_HISTORY || db_usage == WRITE_HEARTBEAT ||
141                        SerializeIO::isEnabled())
142   {
143     isParallel  = util_.parallel_size() > 1;
144     myProcessor = util_.parallel_rank();
145 
146     // Some operations modify DBFilename and there is a need to get
147     // back to the original filename...
148     originalDBFilename = DBFilename;
149 
150     // Check environment variable IOSS_PROPERTIES. If it exists, parse
151     // the contents and add to the 'properties' map.
152     util_.add_environment_properties(properties);
153 
154     Utils::check_set_bool_property(properties, "ENABLE_FIELD_RECOGNITION", enableFieldRecognition);
155 
156     if (properties.exists("FIELD_SUFFIX_SEPARATOR")) {
157       std::string tmp = properties.get("FIELD_SUFFIX_SEPARATOR").get_string();
158       fieldSeparator  = tmp[0];
159     }
160 
161     if (properties.exists("INTEGER_SIZE_API")) {
162       int isize = properties.get("INTEGER_SIZE_API").get_int();
163       if (isize == 8) {
164         set_int_byte_size_api(Ioss::USE_INT64_API);
165       }
166     }
167 
168     if (properties.exists("SERIALIZE_IO")) {
169       int isize = properties.get("SERIALIZE_IO").get_int();
170       Ioss::SerializeIO::setGroupFactor(isize);
171       if (isize > 0) {
172         singleProcOnly = true;
173       }
174     }
175 
176     if (properties.exists("CYCLE_COUNT")) {
177       cycleCount = properties.get("CYCLE_COUNT").get_int();
178     }
179 
180     if (properties.exists("OVERLAY_COUNT")) {
181       overlayCount = properties.get("OVERLAY_COUNT").get_int();
182     }
183 
184     Utils::check_set_bool_property(properties, "ENABLE_TRACING", m_enableTracing);
185     Utils::check_set_bool_property(properties, "TIME_STATE_INPUT_OUTPUT", m_timeStateInOut);
186     {
187       bool logging;
188       if (Utils::check_set_bool_property(properties, "LOGGING", logging)) {
189         set_logging(logging);
190       }
191     }
192 
193     Utils::check_set_bool_property(properties, "LOWER_CASE_VARIABLE_NAMES", lowerCaseVariableNames);
194     Utils::check_set_bool_property(properties, "USE_GENERIC_CANONICAL_NAMES",
195                                    useGenericCanonicalName);
196     Utils::check_set_bool_property(properties, "IGNORE_DATABASE_NAMES", ignoreDatabaseNames);
197 
198     {
199       bool consistent;
200       if (Utils::check_set_bool_property(properties, "PARALLEL_CONSISTENCY", consistent)) {
201         set_parallel_consistency(consistent);
202       }
203     }
204 
205     check_setDW();
206 
207     if (!is_input()) {
208       // Create full path to the output file at this point if it doesn't
209       // exist...
210       if (isParallel) {
211         Ioss::FileInfo::create_path(DBFilename, util().communicator());
212       }
213       else {
214         Ioss::FileInfo::create_path(DBFilename);
215       }
216     }
217   }
218 
219   DatabaseIO::~DatabaseIO() = default;
220 
int_byte_size_api()221   int DatabaseIO::int_byte_size_api() const
222   {
223     if (dbIntSizeAPI == USE_INT32_API) {
224       return 4;
225     }
226     return 8;
227   }
228 
229   /** \brief Set the number of bytes used to represent an integer.
230    *
231    *  \param[in] size The number of bytes. This is 4 for INT32 or 8 for INT64.
232    */
set_int_byte_size_api(DataSize size)233   void DatabaseIO::set_int_byte_size_api(DataSize size) const
234   {
235     dbIntSizeAPI = size; // mutable
236   }
237 
238   /** \brief Set the character used to separate a field suffix from the field basename
239    *         when recognizing vector, tensor fields.
240    *
241    *  \param[in] separator The separator character.
242    */
set_field_separator(const char separator)243   void DatabaseIO::set_field_separator(const char separator)
244   {
245     if (properties.exists("FIELD_SUFFIX_SEPARATOR")) {
246       properties.erase("FIELD_SUFFIX_SEPARATOR");
247     }
248     char tmp[2] = {separator, '\0'};
249     properties.add(Property("FIELD_SUFFIX_SEPARATOR", tmp));
250     fieldSeparator = separator;
251   }
252 
253   /**
254    * Check whether user wants to use Cray DataWarp.  It will be enabled if:
255    * the `DW_JOB_STRIPED` or `DW_JOB_PRIVATE` environment variable
256    * is set by the queuing system during runtime and the IOSS property
257    * `ENABLE_DATAWARP` set to `YES`.
258    *
259    * We currently only want output files to be directed to BB.
260    */
check_setDW()261   void DatabaseIO::check_setDW() const
262   {
263     if (!is_input()) {
264       bool set_dw = false;
265       Utils::check_set_bool_property(properties, "ENABLE_DATAWARP", set_dw);
266       if (set_dw) {
267         std::string bb_path;
268         // Selected via `#DW jobdw type=scratch access_mode=striped`
269         util().get_environment("DW_JOB_STRIPED", bb_path, isParallel);
270 
271         if (bb_path.empty()) { // See if using `private` mode...
272           // Selected via `#DW jobdw type=scratch access_mode=private`
273           util().get_environment("DW_JOB_PRIVATE", bb_path, isParallel);
274         }
275         if (!bb_path.empty()) {
276           usingDataWarp = true;
277           dwPath        = bb_path;
278           if (myProcessor == 0) {
279             fmt::print(Ioss::OUTPUT(), "\nDataWarp Burst Buffer Enabled.  Path = `{}`\n\n", dwPath);
280           }
281         }
282         else {
283           if (myProcessor == 0) {
284             fmt::print(Ioss::WARNING(),
285                        "DataWarp enabled via Ioss property `ENABLE_DATAWARP`, but\n"
286                        "         burst buffer path was not specified via `DW_JOB_STRIPED` or "
287                        "`DW_JOB_PRIVATE`\n"
288                        "         environment variables (typically set by queuing system)\n"
289                        "         DataWarp will *NOT* be enabled, but job will still run.\n\n");
290           }
291         }
292       }
293     }
294   }
295 
296   /**
297    * In this wrapper function we check if user intends to use Cray
298    * DataWarp(aka DW), which provides ability to use NVMe based flash
299    * storage available across all compute nodes accessible via high
300    * speed NIC.
301    */
openDW(const std::string & filename)302   void DatabaseIO::openDW(const std::string &filename) const
303   {
304     set_pfsname(filename); // Name on permanent-file-store
305     if (using_dw()) {      // We are about to write to a output database in BB
306       Ioss::FileInfo path{filename};
307       Ioss::FileInfo bb_file{get_dwPath() + path.tailname()};
308       if (bb_file.exists() && !bb_file.is_writable()) {
309         // already existing file which has been closed If we can't
310         // write to the file on the BB, then it is a file which is
311         // being staged by datawarp system over to the permanent
312         // filesystem.  Wait until staging has finished...  stage wait
313         // returns 0 = success, -ENOENT or -errno
314 #if defined SEACAS_HAVE_DATAWARP
315 #if IOSS_DEBUG_OUTPUT
316         if (myProcessor == 0) {
317           fmt::print(Ioss::DEBUG(), "DW: dw_wait_file_stage({});\n", bb_file.filename());
318         }
319 #endif
320         int dwret = dw_wait_file_stage(bb_file.filename().c_str());
321         if (dwret < 0) {
322           std::ostringstream errmsg;
323           fmt::print(errmsg, "ERROR: failed waiting for file stage `{}`: {}\n", bb_file.filename(),
324                      std::strerror(-dwret));
325           IOSS_ERROR(errmsg);
326         }
327 #else
328         // Used to debug DataWarp logic on systems without DataWarp...
329         fmt::print(Ioss::DEBUG(), "DW: (FAKE) dw_wait_file_stage({});\n", bb_file.filename());
330 #endif
331       }
332       set_dwname(bb_file.filename());
333     }
334     else {
335       set_dwname(filename);
336     }
337   }
338 
339   /** \brief This function gets called inside closeDatabase__(), which checks if Cray Datawarp (DW)
340    * is in use, if so, we want to call a stageout before actual close of this file.
341    */
closeDW()342   void DatabaseIO::closeDW() const
343   {
344     if (using_dw()) {
345       if (!using_parallel_io() || (using_parallel_io() && myProcessor == 0)) {
346 #if defined SEACAS_HAVE_DATAWARP
347         int complete = 0, pending = 0, deferred = 0, failed = 0;
348         dw_query_file_stage(get_dwname().c_str(), &complete, &pending, &deferred, &failed);
349 #if IOSS_DEBUG_OUTPUT
350         auto initial = std::chrono::steady_clock::now();
351         fmt::print(Ioss::DEBUG(), "Query: {}, {}, {}, {}\n", complete, pending, deferred, failed);
352 #endif
353         if (pending > 0) {
354           int dwret = dw_wait_file_stage(get_dwname().c_str());
355           if (dwret < 0) {
356             std::ostringstream errmsg;
357             fmt::print(errmsg, "ERROR: failed waiting for file stage `{}`: {}\n", get_dwname(),
358                        std::strerror(-dwret));
359             IOSS_ERROR(errmsg);
360           }
361 #if IOSS_DEBUG_OUTPUT
362           dw_query_file_stage(get_dwname().c_str(), &complete, &pending, &deferred, &failed);
363           fmt::print(Ioss::DEBUG(), "Query: {}, {}, {}, {}\n", complete, pending, deferred, failed);
364 #endif
365         }
366 
367 #if IOSS_DEBUG_OUTPUT
368         fmt::print(Ioss::DEBUG(), "\nDW: BEGIN dw_stage_file_out({}, {}, DW_STAGE_IMMEDIATE);\n",
369                    get_dwname(), get_pfsname());
370 #endif
371         int ret =
372             dw_stage_file_out(get_dwname().c_str(), get_pfsname().c_str(), DW_STAGE_IMMEDIATE);
373 
374 #if IOSS_DEBUG_OUTPUT
375         auto                          time_now = std::chrono::steady_clock::now();
376         std::chrono::duration<double> diff     = time_now - initial;
377         fmt::print(Ioss::DEBUG(), "\nDW: END dw_stage_file_out({})\n", diff.count());
378 #endif
379         if (ret < 0) {
380           std::ostringstream errmsg;
381           fmt::print(errmsg, "ERROR: file staging of `{}` to `{}` failed at close: {}\n",
382                      get_dwname(), get_pfsname(), std::strerror(-ret));
383           IOSS_ERROR(errmsg);
384         }
385 #else
386         fmt::print(Ioss::DEBUG(), "\nDW: (FAKE) dw_stage_file_out({}, {}, DW_STAGE_IMMEDIATE);\n",
387                    get_dwname(), get_pfsname());
388 #endif
389       }
390       if (using_parallel_io()) {
391         util().barrier();
392       }
393     }
394   }
395 
openDatabase__()396   void DatabaseIO::openDatabase__() const { openDW(get_filename()); }
397 
closeDatabase__()398   void DatabaseIO::closeDatabase__() const { closeDW(); }
399 
open_create_behavior()400   IfDatabaseExistsBehavior DatabaseIO::open_create_behavior() const
401   {
402     IfDatabaseExistsBehavior exists = DB_OVERWRITE;
403     if (properties.exists("APPEND_OUTPUT")) {
404       exists = static_cast<IfDatabaseExistsBehavior>(properties.get("APPEND_OUTPUT").get_int());
405     }
406     return exists;
407   }
408 
decoded_filename()409   const std::string &DatabaseIO::decoded_filename() const
410   {
411     if (decodedFilename.empty()) {
412       if (isParallel) {
413         decodedFilename = util().decode_filename(get_filename(), isParallel && !usingParallelIO);
414       }
415       else if (properties.exists("processor_count") && properties.exists("my_processor")) {
416         int proc_count  = properties.get("processor_count").get_int();
417         int my_proc     = properties.get("my_processor").get_int();
418         decodedFilename = Ioss::Utils::decode_filename(get_filename(), my_proc, proc_count);
419       }
420       else {
421         decodedFilename = get_filename();
422       }
423 
424       openDW(decodedFilename);
425       if (using_dw()) {
426         // Note that if using_dw(), then we need to set the decodedFilename to the BB name.
427         decodedFilename = get_dwname();
428       }
429     }
430     return decodedFilename;
431   }
432 
verify_and_log(const GroupingEntity * ge,const Field & field,int in_out)433   void DatabaseIO::verify_and_log(const GroupingEntity *ge, const Field &field, int in_out) const
434   {
435     if (ge != nullptr) {
436       assert(!is_parallel_consistent() ||
437              internal_parallel_consistent(singleProcOnly, ge, field, in_out, util_));
438     }
439     if (get_logging()) {
440       log_field(in_out == 1 ? ">" : "<", ge, field, singleProcOnly, util_);
441     }
442   }
443 
begin_state(int state,double time)444   bool DatabaseIO::begin_state(int state, double time)
445   {
446     IOSS_FUNC_ENTER(m_);
447     if (m_timeStateInOut) {
448       m_stateStart = std::chrono::steady_clock::now();
449     }
450     return begin_state__(state, time);
451   }
end_state(int state,double time)452   bool DatabaseIO::end_state(int state, double time)
453   {
454     IOSS_FUNC_ENTER(m_);
455     bool res = end_state__(state, time);
456     if (m_timeStateInOut) {
457       auto finish = std::chrono::steady_clock::now();
458       log_time(m_stateStart, finish, state, time, is_input(), singleProcOnly, util_);
459     }
460     return res;
461   }
462 
463   // Default versions do nothing...
begin_state__(int,double)464   bool DatabaseIO::begin_state__(int /* state */, double /* time */) { return true; }
465 
end_state__(int,double)466   bool DatabaseIO::end_state__(int /* state */, double /* time */) { return true; }
467 
handle_groups()468   void DatabaseIO::handle_groups()
469   {
470     // Set Grouping requests are specified as properties...
471     // See if the property exists and decode...
472     // There is a property for each "type":
473     // GROUP_SIDESET, GROUP_NODESET, GROUP_EDGESET, GROUP_FACESET,
474     // GROUP_ELEMSET.
475     // Within the property, the "value" consists of multiple groups separated by
476     // ":"
477     // Within the group, the names are "," separated:
478     //
479     // new_surf1,member1,member2,member3:new_surf2,mem1,mem2,mem3,mem4:new_surf3,....
480     //
481     // Currently does not check for duplicate entity membership in a set --
482     // union
483     // with duplicates
484     //
485     create_groups("GROUP_SIDESET", SIDESET, "side", (SideSet *)nullptr);
486     create_groups("GROUP_NODESET", NODESET, "node", (NodeSet *)nullptr);
487     create_groups("GROUP_EDGESET", EDGESET, "edge", (EdgeSet *)nullptr);
488     create_groups("GROUP_FACESET", FACESET, "face", (FaceSet *)nullptr);
489     create_groups("GROUP_ELEMSET", ELEMENTSET, "elem", (ElementSet *)nullptr);
490   }
491 
492   template <typename T>
create_groups(const std::string & property_name,EntityType type,const std::string & type_name,const T * set_type)493   void DatabaseIO::create_groups(const std::string &property_name, EntityType type,
494                                  const std::string &type_name, const T *set_type)
495   {
496     if (!properties.exists(property_name)) {
497       return;
498     }
499 
500     std::string              prop   = properties.get(property_name).get_string();
501     std::vector<std::string> groups = tokenize(prop, ":");
502     for (auto &group : groups) {
503       std::vector<std::string> group_spec = tokenize(group, ",");
504 
505       // group_spec should contain the name of the new group as
506       // the first location and the members of the group as subsequent
507       // locations.  OK to have a single member
508       if (group_spec.size() < 2) {
509         std::ostringstream errmsg;
510         fmt::print(errmsg,
511                    "ERROR: Invalid {} group specification '{}'\n"
512                    "       Correct syntax is 'new_group,member1,...,memberN' and there must "
513                    "       be at least 1 member of the group",
514                    type_name, group);
515         IOSS_ERROR(errmsg);
516       }
517 
518       create_group(type, type_name, group_spec, set_type);
519     }
520   }
521 
522   template <typename T>
create_group(EntityType,const std::string & type_name,const std::vector<std::string> & group_spec,const T *)523   void DatabaseIO::create_group(EntityType /*type*/, const std::string &type_name,
524                                 const std::vector<std::string> &group_spec, const T * /*set_type*/)
525   {
526     fmt::print(Ioss::WARNING(),
527                "Grouping of {0} sets is not yet implemented.\n"
528                "         Skipping the creation of {0} set '{1}'\n\n",
529                type_name, group_spec[0]);
530   }
531 
532   template <>
create_group(EntityType type,const std::string &,const std::vector<std::string> & group_spec,const SideSet *)533   void DatabaseIO::create_group(EntityType type, const std::string & /*type_name*/,
534                                 const std::vector<std::string> &group_spec,
535                                 const SideSet * /*set_type*/)
536   {
537     // Not generalized yet... This only works for T == SideSet
538     if (type != SIDESET) {
539       return;
540     }
541 
542     int64_t entity_count = 0;
543     int64_t df_count     = 0;
544 
545     // Create the new set...
546     auto new_set = new SideSet(this, group_spec[0]);
547 
548     get_region()->add(new_set);
549 
550     // Find the member SideSets...
551     for (size_t i = 1; i < group_spec.size(); i++) {
552       SideSet *set = get_region()->get_sideset(group_spec[i]);
553       if (set != nullptr) {
554         const SideBlockContainer &side_blocks = set->get_side_blocks();
555         for (auto &sbold : side_blocks) {
556           size_t  side_count = sbold->entity_count();
557           auto    sbnew      = new SideBlock(this, sbold->name(), sbold->topology()->name(),
558                                      sbold->parent_element_topology()->name(), side_count);
559           int64_t id         = sbold->get_property("id").get_int();
560           sbnew->property_add(Property("set_offset", entity_count));
561           sbnew->property_add(Property("set_df_offset", df_count));
562           sbnew->property_add(Property("id", id));
563           sbnew->property_add(Property("id", id));
564           sbnew->property_add(Property("guid", util().generate_guid(id)));
565 
566           new_set->add(sbnew);
567 
568           size_t old_df_count = sbold->get_property("distribution_factor_count").get_int();
569           if (old_df_count > 0) {
570             std::string storage = "Real[";
571             storage += std::to_string(sbnew->topology()->number_nodes());
572             storage += "]";
573             sbnew->field_add(
574                 Field("distribution_factors", Field::REAL, storage, Field::MESH, side_count));
575           }
576           entity_count += side_count;
577           df_count += old_df_count;
578         }
579       }
580       else {
581         fmt::print(Ioss::WARNING(),
582                    "While creating the grouped surface '{}', the surface '{}' does not exist. "
583                    "This surface will skipped and not added to the group.\n\n",
584                    group_spec[0], group_spec[i]);
585       }
586     }
587   }
588 
589   // Utility function that may be used by derived classes.  Determines
590   // whether all elements in the model have the same face topology.
591   // This can be used to speed-up certain algorithms since they don't
592   // have to check each face (or group of faces) individually.
set_common_side_topology()593   void DatabaseIO::set_common_side_topology() const
594   {
595     auto *new_this = const_cast<DatabaseIO *>(this);
596 
597     bool                         first          = true;
598     const ElementBlockContainer &element_blocks = get_region()->get_element_blocks();
599     for (auto block : element_blocks) {
600       size_t element_count = block->entity_count();
601 
602       // Check face types.
603       if (element_count > 0) {
604         if (commonSideTopology != nullptr || first) {
605           first                      = false;
606           ElementTopology *side_type = block->topology()->boundary_type();
607           if (commonSideTopology == nullptr) { // First block
608             new_this->commonSideTopology = side_type;
609           }
610           if (commonSideTopology != side_type) { // Face topologies differ in mesh
611             new_this->commonSideTopology = nullptr;
612             return;
613           }
614         }
615       }
616     }
617   }
618 
619   /** \brief Add multiple information records (informative strings) to the database.
620    *
621    *  \param[in] info The strings to add.
622    */
add_information_records(const std::vector<std::string> & info)623   void DatabaseIO::add_information_records(const std::vector<std::string> &info)
624   {
625     informationRecords.reserve(informationRecords.size() + info.size());
626     informationRecords.insert(informationRecords.end(), info.begin(), info.end());
627   }
628 
629   /** \brief Add an information record (an informative string) to the database.
630    *
631    *  \param[in] info The string to add.
632    */
add_information_record(const std::string & info)633   void DatabaseIO::add_information_record(const std::string &info)
634   {
635     informationRecords.push_back(info);
636   }
637 
638   /** \brief Add a QA record, which consists of 4 strings, to the database
639    *
640    *  The 4 function parameters correspond to the 4 QA record strings.
641    *
642    *  \param[in] code A descriptive code name, such as the application that modified the database.
643    *  \param[in] code_qa A descriptive string, such as the version of the application that modified
644    * the database.
645    *  \param[in] date A relevant date, such as the date the database was modified.
646    *  \param[in] time A relevant time, such as the time the database was modified.
647    */
add_qa_record(const std::string & code,const std::string & code_qa,const std::string & date,const std::string & time)648   void DatabaseIO::add_qa_record(const std::string &code, const std::string &code_qa,
649                                  const std::string &date, const std::string &time)
650   {
651     qaRecords.push_back(code);
652     qaRecords.push_back(code_qa);
653     qaRecords.push_back(date);
654     qaRecords.push_back(time);
655   }
656 
set_block_omissions(const std::vector<std::string> & omissions,const std::vector<std::string> & inclusions)657   void DatabaseIO::set_block_omissions(const std::vector<std::string> &omissions,
658                                        const std::vector<std::string> &inclusions)
659   {
660     if (!omissions.empty()) {
661       blockOmissions.assign(omissions.cbegin(), omissions.cend());
662       Ioss::sort(blockOmissions.begin(), blockOmissions.end());
663     }
664     if (!inclusions.empty()) {
665       blockInclusions.assign(inclusions.cbegin(), inclusions.cend());
666       Ioss::sort(blockInclusions.begin(), blockInclusions.end());
667     }
668   }
669 
670   // Check topology of all sides (face/edges) in model...
check_side_topology()671   void DatabaseIO::check_side_topology() const
672   {
673     // The following code creates the sideTopology sets which contain
674     // a list of the side topologies in this model.
675     //
676     // If sideTopology.size() > 1 --> the model has sides with mixed
677     // topology (i.e., quads and tris).
678     //
679     // If sideTopology.size() == 1 --> the model has homogeneous sides
680     // and each side is of the topology type 'sideTopology[0]'
681     //
682     // This is used in other code speed up some tests.
683 
684     // Spheres and Circle have no faces/edges, so handle them special...
685     bool all_sphere = true;
686 
687     if (sideTopology.empty()) {
688       // Set contains (parent_element, boundary_topology) pairs...
689       std::set<std::pair<const ElementTopology *, const ElementTopology *>> side_topo;
690 
691       const ElementBlockContainer &element_blocks = get_region()->get_element_blocks();
692 
693       for (auto &block : element_blocks) {
694         const ElementTopology *elem_type = block->topology();
695         const ElementTopology *side_type = elem_type->boundary_type();
696         if (side_type == nullptr) {
697           // heterogeneous sides.  Iterate through...
698           int size = elem_type->number_boundaries();
699           for (int i = 1; i <= size; i++) {
700             side_type = elem_type->boundary_type(i);
701             side_topo.insert(std::make_pair(elem_type, side_type));
702             all_sphere = false;
703           }
704         }
705         else {
706           // homogeneous sides.
707           side_topo.insert(std::make_pair(elem_type, side_type));
708           all_sphere = false;
709         }
710       }
711       if (all_sphere) {
712         // If we end up here, the model either contains all spheres,
713         // or there are no element blocks in the model...
714         const ElementTopology *ftopo = ElementTopology::factory("unknown");
715         if (element_blocks.empty()) {
716           side_topo.insert(std::make_pair(ftopo, ftopo));
717         }
718         else {
719           const ElementBlock *block = *element_blocks.cbegin();
720           side_topo.insert(std::make_pair(block->topology(), ftopo));
721         }
722       }
723       assert(!side_topo.empty());
724       assert(sideTopology.empty());
725       // Copy into the sideTopology container...
726       auto *new_this = const_cast<DatabaseIO *>(this);
727       std::copy(side_topo.cbegin(), side_topo.cend(), std::back_inserter(new_this->sideTopology));
728     }
729     assert(!sideTopology.empty());
730   }
731 
get_block_adjacencies__(const Ioss::ElementBlock * eb,std::vector<std::string> & block_adjacency)732   void DatabaseIO::get_block_adjacencies__(const Ioss::ElementBlock *eb,
733                                            std::vector<std::string> &block_adjacency) const
734   {
735     if (!blockAdjacenciesCalculated) {
736       compute_block_adjacencies();
737     }
738 
739     const Ioss::ElementBlockContainer &element_blocks = get_region()->get_element_blocks();
740     assert(Ioss::Utils::check_block_order(element_blocks));
741 
742     // Extract the computed block adjacency information for this
743     // element block:
744     int blk_position = 0;
745     if (eb->property_exists("original_block_order")) {
746       blk_position = eb->get_property("original_block_order").get_int();
747     }
748     else {
749       for (const auto &leb : element_blocks) {
750         if (leb == eb) {
751           break;
752         }
753         blk_position++;
754       }
755     }
756 
757     int lblk_position = -1;
758     for (const auto &leb : element_blocks) {
759       if (leb->property_exists("original_block_order")) {
760         lblk_position = leb->get_property("original_block_order").get_int();
761       }
762       else {
763         lblk_position++;
764       }
765 
766       if (blk_position != lblk_position &&
767           static_cast<int>(blockAdjacency[blk_position][lblk_position]) == 1) {
768         block_adjacency.push_back(leb->name());
769       }
770     }
771   }
772 
773   // common
compute_block_adjacencies()774   void DatabaseIO::compute_block_adjacencies() const
775   {
776     // Add a field to each element block specifying which other element
777     // blocks the block is adjacent to (defined as sharing nodes).
778     // This is only calculated on request...
779 
780     blockAdjacenciesCalculated = true;
781 
782     const Ioss::ElementBlockContainer &element_blocks = get_region()->get_element_blocks();
783     assert(Ioss::Utils::check_block_order(element_blocks));
784 
785     if (element_blocks.size() == 1) {
786       blockAdjacency.resize(1);
787       blockAdjacency[0].resize(1);
788       blockAdjacency[0][0] = false;
789       return;
790     }
791 
792     // Each processor first calculates the adjacencies on their own
793     // processor...
794 
795     std::vector<int64_t>          node_used(nodeCount);
796     std::vector<std::vector<int>> inv_con(nodeCount);
797 
798     {
799       Ioss::SerializeIO serializeIO__(this);
800       int               blk_position = -1;
801       for (Ioss::ElementBlock *eb : element_blocks) {
802         if (eb->property_exists("original_block_order")) {
803           blk_position = eb->get_property("original_block_order").get_int();
804         }
805         else {
806           blk_position++;
807         }
808         int64_t my_element_count = eb->entity_count();
809         if (int_byte_size_api() == 8) {
810           std::vector<int64_t> conn;
811           eb->get_field_data("connectivity_raw", conn);
812           for (auto node : conn) {
813             assert(node > 0 && node - 1 < nodeCount);
814             node_used[node - 1] = blk_position + 1;
815           }
816         }
817         else {
818           std::vector<int> conn;
819           eb->get_field_data("connectivity_raw", conn);
820           for (auto node : conn) {
821             assert(node > 0 && node - 1 < nodeCount);
822             node_used[node - 1] = blk_position + 1;
823           }
824         }
825 
826         if (my_element_count > 0) {
827           for (int64_t i = 0; i < nodeCount; i++) {
828             if (node_used[i] == blk_position + 1) {
829               inv_con[i].push_back(blk_position);
830             }
831           }
832         }
833       }
834     }
835 
836 #ifdef SEACAS_HAVE_MPI
837     if (isParallel) {
838       // Get contributions from other processors...
839       // Get the communication map...
840       Ioss::CommSet *css = get_region()->get_commset("commset_node");
841       Ioss::Utils::check_non_null(css, "communication map", "commset_node", __func__);
842       std::vector<std::pair<int, int>> proc_node;
843       {
844         std::vector<int> entity_processor;
845         css->get_field_data("entity_processor", entity_processor);
846         proc_node.reserve(entity_processor.size() / 2);
847         for (size_t i = 0; i < entity_processor.size(); i += 2) {
848           proc_node.emplace_back(entity_processor[i + 1], entity_processor[i]);
849         }
850       }
851 
852       // Now sort by increasing processor number.
853       Ioss::sort(proc_node.begin(), proc_node.end());
854 
855       // Pack the data: global_node_id, bits for each block, ...
856       // Use 'int' as basic type...
857       size_t                id_size   = 1;
858       size_t                word_size = sizeof(int) * 8;
859       size_t                bits_size = (element_blocks.size() + word_size - 1) / word_size;
860       std::vector<unsigned> send(proc_node.size() * (id_size + bits_size));
861       std::vector<unsigned> recv(proc_node.size() * (id_size + bits_size));
862 
863       std::vector<int> procs(util().parallel_size());
864       size_t           offset = 0;
865       for (const auto &pn : proc_node) {
866         int64_t glob_id = pn.second;
867         int     proc    = pn.first;
868         procs[proc]++;
869         send[offset++] = glob_id;
870         int64_t loc_id = nodeMap.global_to_local(glob_id, true) - 1;
871         for (int jblk : inv_con[loc_id]) {
872           size_t wrd_off = jblk / word_size;
873           size_t bit     = jblk % word_size;
874           send[offset + wrd_off] |= (1 << bit);
875         }
876         offset += bits_size;
877       }
878 
879       // Count nonzero entries in 'procs' array -- count of
880       // sends/receives
881       size_t non_zero = util().parallel_size() - std::count(procs.begin(), procs.end(), 0);
882 
883       // Post all receives...
884       MPI_Request              request_null = MPI_REQUEST_NULL;
885       std::vector<MPI_Request> request(non_zero, request_null);
886       std::vector<MPI_Status>  status(non_zero);
887 
888       int    result  = MPI_SUCCESS;
889       size_t req_cnt = 0;
890       offset         = 0;
891       for (int i = 0; result == MPI_SUCCESS && i < util().parallel_size(); i++) {
892         if (procs[i] > 0) {
893           const unsigned size     = procs[i] * (id_size + bits_size);
894           void *const    recv_buf = &recv[offset];
895           result = MPI_Irecv(recv_buf, size, MPI_INT, i, 10101, util().communicator(),
896                              &request[req_cnt]);
897           req_cnt++;
898           offset += size;
899         }
900       }
901       assert(result != MPI_SUCCESS || non_zero == req_cnt);
902 
903       if (result != MPI_SUCCESS) {
904         std::ostringstream errmsg;
905         fmt::print(errmsg, "ERROR: MPI_Irecv error on processor {} in {}", util().parallel_rank(),
906                    __func__);
907         IOSS_ERROR(errmsg);
908       }
909 
910       int local_error  = (MPI_SUCCESS == result) ? 0 : 1;
911       int global_error = util().global_minmax(local_error, Ioss::ParallelUtils::DO_MAX);
912 
913       if (global_error != 0) {
914         std::ostringstream errmsg;
915         fmt::print(errmsg, "ERROR: MPI_Irecv error on some processor in {}", __func__);
916         IOSS_ERROR(errmsg);
917       }
918 
919       result  = MPI_SUCCESS;
920       req_cnt = 0;
921       offset  = 0;
922       for (int i = 0; result == MPI_SUCCESS && i < util().parallel_size(); i++) {
923         if (procs[i] > 0) {
924           const unsigned size     = procs[i] * (id_size + bits_size);
925           void *const    send_buf = &send[offset];
926           result = MPI_Rsend(send_buf, size, MPI_INT, i, 10101, util().communicator());
927           req_cnt++;
928           offset += size;
929         }
930       }
931       assert(result != MPI_SUCCESS || non_zero == req_cnt);
932 
933       if (result != MPI_SUCCESS) {
934         std::ostringstream errmsg;
935         fmt::print(errmsg, "ERROR: MPI_Rsend error on processor {} in {}", util().parallel_rank(),
936                    __func__);
937         IOSS_ERROR(errmsg);
938       }
939 
940       local_error  = (MPI_SUCCESS == result) ? 0 : 1;
941       global_error = util().global_minmax(local_error, Ioss::ParallelUtils::DO_MAX);
942 
943       if (global_error != 0) {
944         std::ostringstream errmsg;
945         fmt::print(errmsg, "ERROR: MPI_Rsend error on some processor in {}", __func__);
946         IOSS_ERROR(errmsg);
947       }
948 
949       result = MPI_Waitall(req_cnt, request.data(), status.data());
950 
951       if (result != MPI_SUCCESS) {
952         std::ostringstream errmsg;
953         fmt::print(errmsg, "ERROR: MPI_Waitall error on processor {} in {}", util().parallel_rank(),
954                    __func__);
955         IOSS_ERROR(errmsg);
956       }
957 
958       // Unpack the data and update the inv_con arrays for boundary
959       // nodes...
960       offset = 0;
961       for (size_t i = 0; i < proc_node.size(); i++) {
962         int64_t glob_id = recv[offset++];
963         int64_t loc_id  = nodeMap.global_to_local(glob_id, true) - 1;
964         for (size_t iblk = 0; iblk < element_blocks.size(); iblk++) {
965           size_t wrd_off = iblk / word_size;
966           size_t bit     = iblk % word_size;
967           if (recv[offset + wrd_off] & (1 << bit)) {
968             inv_con[loc_id].push_back(iblk); // May result in duplicates, but that is OK.
969           }
970         }
971         offset += bits_size;
972       }
973     }
974 #endif
975 
976     // Convert from inv_con arrays to block adjacency...
977     blockAdjacency.resize(element_blocks.size());
978     for (auto &block : blockAdjacency) {
979       block.resize(element_blocks.size());
980     }
981 
982     for (int64_t i = 0; i < nodeCount; i++) {
983       for (size_t j = 0; j < inv_con[i].size(); j++) {
984         int jblk = inv_con[i][j];
985         for (size_t k = j + 1; k < inv_con[i].size(); k++) {
986           int kblk                   = inv_con[i][k];
987           blockAdjacency[jblk][kblk] = true;
988           blockAdjacency[kblk][jblk] = true;
989         }
990       }
991     }
992 
993 #ifdef SEACAS_HAVE_MPI
994     if (isParallel) {
995       // Sync across all processors...
996       size_t word_size = sizeof(int) * 8;
997       size_t bits_size = (element_blocks.size() + word_size - 1) / word_size;
998 
999       std::vector<unsigned> data(element_blocks.size() * bits_size);
1000       int64_t               offset = 0;
1001       for (size_t jblk = 0; jblk < element_blocks.size(); jblk++) {
1002         for (size_t iblk = 0; iblk < element_blocks.size(); iblk++) {
1003           if (blockAdjacency[jblk][iblk] == 1) {
1004             size_t wrd_off = iblk / word_size;
1005             size_t bit     = iblk % word_size;
1006             data[offset + wrd_off] |= (1 << bit);
1007           }
1008         }
1009         offset += bits_size;
1010       }
1011 
1012       std::vector<unsigned> out_data(element_blocks.size() * bits_size);
1013       MPI_Allreduce((void *)data.data(), out_data.data(), static_cast<int>(data.size()),
1014                     MPI_UNSIGNED, MPI_BOR, util().communicator());
1015 
1016       offset = 0;
1017       for (size_t jblk = 0; jblk < element_blocks.size(); jblk++) {
1018         for (size_t iblk = 0; iblk < element_blocks.size(); iblk++) {
1019           if (blockAdjacency[jblk][iblk] == 0) {
1020             size_t wrd_off = iblk / word_size;
1021             size_t bit     = iblk % word_size;
1022             if (out_data[offset + wrd_off] & (1 << bit)) {
1023               blockAdjacency[jblk][iblk] = 1;
1024             }
1025           }
1026         }
1027         offset += bits_size;
1028       }
1029     }
1030 #endif
1031 
1032     // Make it symmetric... (TODO: this probably isn't needed...)
1033     for (size_t iblk = 0; iblk < element_blocks.size(); iblk++) {
1034       for (size_t jblk = iblk; jblk < element_blocks.size(); jblk++) {
1035         blockAdjacency[jblk][iblk] = blockAdjacency[iblk][jblk];
1036       }
1037     }
1038   }
1039 
get_bounding_box(const Ioss::ElementBlock * eb)1040   AxisAlignedBoundingBox DatabaseIO::get_bounding_box(const Ioss::ElementBlock *eb) const
1041   {
1042     if (elementBlockBoundingBoxes.empty()) {
1043       // Calculate the bounding boxes for all element blocks...
1044       std::vector<double> coordinates;
1045       Ioss::NodeBlock *   nb = get_region()->get_node_blocks()[0];
1046       nb->get_field_data("mesh_model_coordinates", coordinates);
1047       ssize_t nnode = nb->entity_count();
1048       ssize_t ndim  = nb->get_property("component_degree").get_int();
1049 
1050       const Ioss::ElementBlockContainer &element_blocks = get_region()->get_element_blocks();
1051       size_t                             nblock         = element_blocks.size();
1052       std::vector<double>                minmax;
1053       minmax.reserve(6 * nblock);
1054 
1055       for (auto &block : element_blocks) {
1056         double xmin, ymin, zmin, xmax, ymax, zmax;
1057         if (block->get_database()->int_byte_size_api() == 8) {
1058           std::vector<int64_t> connectivity;
1059           block->get_field_data("connectivity_raw", connectivity);
1060           calc_bounding_box(ndim, nnode, coordinates, connectivity, xmin, ymin, zmin, xmax, ymax,
1061                             zmax);
1062         }
1063         else {
1064           std::vector<int> connectivity;
1065           block->get_field_data("connectivity_raw", connectivity);
1066           calc_bounding_box(ndim, nnode, coordinates, connectivity, xmin, ymin, zmin, xmax, ymax,
1067                             zmax);
1068         }
1069 
1070         minmax.push_back(xmin);
1071         minmax.push_back(ymin);
1072         minmax.push_back(zmin);
1073         minmax.push_back(-xmax);
1074         minmax.push_back(-ymax);
1075         minmax.push_back(-zmax);
1076       }
1077 
1078       util().global_array_minmax(minmax, Ioss::ParallelUtils::DO_MIN);
1079 
1080       for (size_t i = 0; i < element_blocks.size(); i++) {
1081         Ioss::ElementBlock *   block = element_blocks[i];
1082         const std::string &    name  = block->name();
1083         AxisAlignedBoundingBox bbox(minmax[6 * i + 0], minmax[6 * i + 1], minmax[6 * i + 2],
1084                                     -minmax[6 * i + 3], -minmax[6 * i + 4], -minmax[6 * i + 5]);
1085         elementBlockBoundingBoxes[name] = bbox;
1086       }
1087     }
1088     return elementBlockBoundingBoxes[eb->name()];
1089   }
1090 
get_bounding_box(const Ioss::StructuredBlock * sb)1091   AxisAlignedBoundingBox DatabaseIO::get_bounding_box(const Ioss::StructuredBlock *sb) const
1092   {
1093     ssize_t ndim = sb->get_property("component_degree").get_int();
1094 
1095     std::pair<double, double> xx;
1096     std::pair<double, double> yy;
1097     std::pair<double, double> zz;
1098 
1099     std::vector<double> coordinates;
1100     sb->get_field_data("mesh_model_coordinates_x", coordinates);
1101     auto x = std::minmax_element(coordinates.cbegin(), coordinates.cend());
1102     xx     = std::make_pair(*(x.first), *(x.second));
1103 
1104     if (ndim > 1) {
1105       sb->get_field_data("mesh_model_coordinates_y", coordinates);
1106       auto y = std::minmax_element(coordinates.cbegin(), coordinates.cend());
1107       yy     = std::make_pair(*(y.first), *(y.second));
1108     }
1109 
1110     if (ndim > 2) {
1111       sb->get_field_data("mesh_model_coordinates_z", coordinates);
1112       auto z = std::minmax_element(coordinates.cbegin(), coordinates.cend());
1113       zz     = std::make_pair(*(z.first), *(z.second));
1114     }
1115 
1116     return {xx.first, yy.first, zz.first, xx.second, yy.second, zz.second};
1117   }
1118 } // namespace Ioss
1119 
1120 namespace {
log_time(std::chrono::time_point<std::chrono::steady_clock> & start,std::chrono::time_point<std::chrono::steady_clock> & finish,int current_state,double state_time,bool is_input,bool single_proc_only,const Ioss::ParallelUtils & util)1121   void log_time(std::chrono::time_point<std::chrono::steady_clock> &start,
1122                 std::chrono::time_point<std::chrono::steady_clock> &finish, int current_state,
1123                 double state_time, bool is_input, bool single_proc_only,
1124                 const Ioss::ParallelUtils &util)
1125   {
1126     std::vector<double> all_times;
1127     double duration = std::chrono::duration<double, std::milli>(finish - start).count();
1128     if (single_proc_only) {
1129       all_times.push_back(duration);
1130     }
1131     else {
1132       util.gather(duration, all_times);
1133     }
1134 
1135     if (util.parallel_rank() == 0 || single_proc_only) {
1136       std::ostringstream strm;
1137       fmt::print(strm, "\nIOSS: Time to {} state {}, time {} is ", (is_input ? "read " : "write"),
1138                  current_state, state_time);
1139 
1140       double total = 0.0;
1141       for (auto &p_time : all_times) {
1142         total += p_time;
1143       }
1144 
1145       // Now append each processors time onto the stream...
1146       if (util.parallel_size() == 1) {
1147         fmt::print(strm, "{} (ms)\n", total);
1148       }
1149       else if (util.parallel_size() > 4) {
1150         Ioss::sort(all_times.begin(), all_times.end());
1151         fmt::print(strm, " Min: {}\tMax: {}\tMed: {}", all_times.front(), all_times.back(),
1152                    all_times[all_times.size() / 2]);
1153       }
1154       else {
1155         char sep = (util.parallel_size() > 1) ? ':' : ' ';
1156         for (auto &p_time : all_times) {
1157           fmt::print(strm, "{:8d}{}", p_time, sep);
1158         }
1159       }
1160       if (util.parallel_size() > 1) {
1161         fmt::print(strm, "\tTot: {} (ms)\n", total);
1162       }
1163       fmt::print(Ioss::DEBUG(), "{}", strm.str());
1164     }
1165   }
1166 
log_field(const char * symbol,const Ioss::GroupingEntity * entity,const Ioss::Field & field,bool single_proc_only,const Ioss::ParallelUtils & util)1167   void log_field(const char *symbol, const Ioss::GroupingEntity *entity, const Ioss::Field &field,
1168                  bool single_proc_only, const Ioss::ParallelUtils &util)
1169   {
1170     if (entity != nullptr) {
1171       std::vector<int64_t> all_sizes;
1172       if (single_proc_only) {
1173         all_sizes.push_back(field.get_size());
1174       }
1175       else {
1176         util.gather(static_cast<int64_t>(field.get_size()), all_sizes);
1177       }
1178 
1179       if (util.parallel_rank() == 0 || single_proc_only) {
1180         const std::string &           name = entity->name();
1181         std::ostringstream            strm;
1182         auto                          now  = std::chrono::steady_clock::now();
1183         std::chrono::duration<double> diff = now - initial_time;
1184         fmt::print(strm, "{} [{:.3f}]\t", symbol, diff.count());
1185 
1186         int64_t total = 0;
1187         for (auto &p_size : all_sizes) {
1188           total += p_size;
1189         }
1190         // Now append each processors size onto the stream...
1191         if (util.parallel_size() > 4) {
1192           auto min_max = std::minmax_element(all_sizes.cbegin(), all_sizes.cend());
1193           fmt::print(strm, " m: {:8d} M: {:8d} A: {:8d}", *min_max.first, *min_max.second,
1194                      total / all_sizes.size());
1195         }
1196         else {
1197           for (auto &p_size : all_sizes) {
1198             fmt::print(strm, "{:8d}:", p_size);
1199           }
1200         }
1201         if (util.parallel_size() > 1) {
1202           fmt::print(strm, " T:{:8d}", total);
1203         }
1204         fmt::print(strm, "\t{}/{}\n", name, field.get_name());
1205         fmt::print(Ioss::DEBUG(), "{}", strm.str());
1206       }
1207     }
1208     else {
1209       if (!single_proc_only) {
1210         util.barrier();
1211       }
1212       if (util.parallel_rank() == 0 || single_proc_only) {
1213         auto                          time_now = std::chrono::steady_clock::now();
1214         std::chrono::duration<double> diff     = time_now - initial_time;
1215         fmt::print("{} [{:.3f}]\n", symbol, diff.count());
1216       }
1217     }
1218   }
1219 } // namespace
1220