diff --git a/packages/seacas/libraries/ioss/src/Ioss_DatabaseIO.C b/packages/seacas/libraries/ioss/src/Ioss_DatabaseIO.C index bee5f80d1d..870140a1d2 100644 --- a/packages/seacas/libraries/ioss/src/Ioss_DatabaseIO.C +++ b/packages/seacas/libraries/ioss/src/Ioss_DatabaseIO.C @@ -379,6 +379,11 @@ namespace Ioss { DatabaseIO::~DatabaseIO() = default; + Ioss::DataSize DatabaseIO::int_byte_size_data_size() const + { + return dbIntSizeAPI; + } + int DatabaseIO::int_byte_size_api() const { if (dbIntSizeAPI == USE_INT32_API) { diff --git a/packages/seacas/libraries/ioss/src/Ioss_DatabaseIO.h b/packages/seacas/libraries/ioss/src/Ioss_DatabaseIO.h index f5edcdb3b4..5d193ed64f 100644 --- a/packages/seacas/libraries/ioss/src/Ioss_DatabaseIO.h +++ b/packages/seacas/libraries/ioss/src/Ioss_DatabaseIO.h @@ -279,6 +279,18 @@ namespace Ioss { return create_subgroup_nl(group_name); } + bool open_root_group() + { + IOSS_FUNC_ENTER(m_); + return open_root_group_nl(); + } + + Ioss::NameList groups_describe(bool return_full_names = false) + { + IOSS_FUNC_ENTER(m_); + return groups_describe_nl(return_full_names); + } + /** \brief Set the database to the given State. * * All transitions must begin from the 'STATE_CLOSED' state or be to @@ -502,6 +514,7 @@ namespace Ioss { IOSS_NODISCARD virtual int int_byte_size_db() const = 0; //! Returns 4 or 8 IOSS_NODISCARD int int_byte_size_api() const; //! Returns 4 or 8 virtual void set_int_byte_size_api(Ioss::DataSize size) const; + IOSS_NODISCARD Ioss::DataSize int_byte_size_data_size() const; /*! * The owning region of this database. @@ -737,6 +750,14 @@ namespace Ioss { virtual void closeDatabase_nl() const; virtual void flush_database_nl() const {} + virtual void release_memory_nl() + { + nodeMap.release_memory(); + edgeMap.release_memory(); + faceMap.release_memory(); + elemMap.release_memory(); + } + private: virtual bool ok_nl(bool /* write_message */, std::string * /* error_message */, int *bad_count) const @@ -757,16 +778,10 @@ namespace Ioss { return elemMap.global_to_local(global); } - virtual void release_memory_nl() - { - nodeMap.release_memory(); - edgeMap.release_memory(); - faceMap.release_memory(); - elemMap.release_memory(); - } - + virtual bool open_root_group_nl() { return false; } virtual bool open_group_nl(const std::string & /* group_name */) { return false; } virtual bool create_subgroup_nl(const std::string & /* group_name */) { return false; } + virtual Ioss::NameList groups_describe_nl(bool /* return_full_names */) { return Ioss::NameList(); } virtual bool begin_nl(Ioss::State state) = 0; virtual bool end_nl(Ioss::State state) = 0; diff --git a/packages/seacas/libraries/ioss/src/Ioss_DynamicTopology.C b/packages/seacas/libraries/ioss/src/Ioss_DynamicTopology.C new file mode 100644 index 0000000000..ac5af4849e --- /dev/null +++ b/packages/seacas/libraries/ioss/src/Ioss_DynamicTopology.C @@ -0,0 +1,554 @@ +// Copyright(C) 2024 National Technology & Engineering Solutions +// of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with +// NTESS, the U.S. Government retains certain rights in this software. +// +// See packages/seacas/LICENSE for details + +#include "Ioss_Assembly.h" +#include "Ioss_Blob.h" +#include "Ioss_CodeTypes.h" +#include "Ioss_CommSet.h" +#include "Ioss_DBUsage.h" +#include "Ioss_DatabaseIO.h" +#include "Ioss_DynamicTopology.h" +#include "Ioss_EdgeBlock.h" +#include "Ioss_EdgeSet.h" +#include "Ioss_ElementBlock.h" +#include "Ioss_ElementSet.h" +#include "Ioss_EntityBlock.h" +#include "Ioss_EntityType.h" +#include "Ioss_FaceBlock.h" +#include "Ioss_FaceSet.h" +#include "Ioss_Field.h" +#include "Ioss_FileInfo.h" +#include "Ioss_GroupingEntity.h" +#include "Ioss_IOFactory.h" +#include "Ioss_NodeBlock.h" +#include "Ioss_NodeSet.h" +#include "Ioss_Region.h" +#include "Ioss_SideBlock.h" +#include "Ioss_SideSet.h" +#include "Ioss_StructuredBlock.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "Ioss_ParallelUtils.h" + +namespace Ioss { + +void DynamicTopologyObserver::check_region() const +{ + if(nullptr == m_region) { + std::ostringstream errmsg; + fmt::print(errmsg, + "ERROR: A region has not been registered with the " + "Dynamic Topology Observer.\n\n"); + IOSS_ERROR(errmsg); + } +} + +void DynamicTopologyObserver::register_region(Region *region) +{ + if(nullptr != m_region && region != m_region) { + std::ostringstream errmsg; + fmt::print(errmsg, + "ERROR: Attempt to re-register different region on " + "Dynamic Topology Observer.\n\n"); + IOSS_ERROR(errmsg); + } + + m_region = region; +} + +void DynamicTopologyObserver::set_cumulative_topology_modification(unsigned int type) +{ m_cumulativeTopologyModification = type; } + +unsigned int DynamicTopologyObserver::get_cumulative_topology_modification() const +{ return m_cumulativeTopologyModification; } + +unsigned int DynamicTopologyObserver::get_topology_modification() const +{ return m_topologyModification; } + +void DynamicTopologyObserver::set_topology_modification(unsigned int type) +{ + m_topologyModification |= type; + m_cumulativeTopologyModification |= type; +} + +void DynamicTopologyObserver::reset_topology_modification() +{ + m_topologyModification = TOPOLOGY_SAME; +} + +bool DynamicTopologyObserver::is_topology_modified() const +{ return m_topologyModification != 0; } + +const ParallelUtils &DynamicTopologyObserver::util() const +{ + check_region(); + return m_region->get_database()->util(); +} + +void DynamicTopologyObserver::synchronize_topology_modified_flags() +{ + check_region(); + int num_processors = m_region->get_database()->parallel_size(); + // Synchronize the topology flags between all processors in case + // it has not been set consistently. + if (num_processors > 1) { + static unsigned int buffer[2]; + buffer[0] = m_cumulativeTopologyModification; + buffer[1] = m_topologyModification; + + util().attribute_reduction(2*sizeof(unsigned int), reinterpret_cast(buffer)); + + m_cumulativeTopologyModification = buffer[0]; + m_topologyModification = buffer[1]; + } +} + +int DynamicTopologyObserver::get_cumulative_topology_modification_field() +{ + check_region(); + const std::string variable_name = topology_modification_change_name(); + + int ivalue = 0; + + if (m_region->field_exists(variable_name)) { + Field topo_field = m_region->get_field(variable_name); + if (topo_field.get_type() == Field::INTEGER) { + m_region->get_field_data(variable_name, &ivalue, sizeof(int)); + } else { + double value; + m_region->get_field_data(variable_name, &value, sizeof(double)); + ivalue = (int)value; + } + } + + int num_processors = m_region->get_database()->parallel_size(); + // Synchronize the value between all processors in case + // it has not been set consistently. + if (num_processors > 1) { + unsigned int buffer[1]; + buffer[0] = ivalue; + + util().attribute_reduction(sizeof(unsigned int), reinterpret_cast(buffer)); + + ivalue = (int)buffer[0]; + } + + m_cumulativeTopologyModification = ivalue; + + return ivalue; +} + +void DynamicTopologyObserver::define_model(IOSS_MAYBE_UNUSED Region& region) +{ + +} + +void DynamicTopologyObserver::write_model(IOSS_MAYBE_UNUSED Region& region) +{ + +} + +void DynamicTopologyObserver::define_transient(IOSS_MAYBE_UNUSED Region& region) +{ + +} + +DynamicTopologyFileControl::DynamicTopologyFileControl(Region *region, unsigned int fileCyclicCount, + IfDatabaseExistsBehavior &ifDatabaseExists, + unsigned int &dbChangeCount) + : m_region(region) + , m_fileCyclicCount(fileCyclicCount) + , m_ifDatabaseExists(ifDatabaseExists) + , m_dbChangeCount(dbChangeCount) +{ + if(nullptr == region) { + std::ostringstream errmsg; + fmt::print(errmsg, "ERROR: null region passed in as argument to DynamicTopologyFileControl"); + IOSS_ERROR(errmsg); + } + + m_ioDB = region->get_property("base_filename").get_string(); + m_dbType = region->get_property("database_type").get_string(); +} + +const ParallelUtils &DynamicTopologyFileControl::util() const +{ + return m_region->get_database()->util(); +} + +bool DynamicTopologyFileControl::file_exists(const std::string &filename, + const std::string &db_type, + Ioss::DatabaseUsage db_usage) +{ + bool exists = false; + int par_size = m_region->get_database()->parallel_size(); + int par_rank = m_region->get_database()->parallel_rank(); + bool is_parallel = par_size > 1; + std::string full_filename = filename; + if (is_parallel && db_type == "exodusII" && db_usage != Ioss::WRITE_HISTORY) { + full_filename = Ioss::Utils::decode_filename(filename, par_rank, par_size); + } + + if (!is_parallel || par_rank == 0) { + // Now, see if this file exists... + // Don't want to do a system call on all processors since it can take minutes + // on some of the larger machines, filesystems, and processor counts... + Ioss::FileInfo file = Ioss::FileInfo(full_filename); + exists = file.exists(); + } + + if (is_parallel) { + int iexists = exists ? 1 : 0; + util().broadcast(iexists, 0); + exists = iexists == 1; + } + return exists; +} + +std::string DynamicTopologyFileControl::get_unique_filename(Ioss::DatabaseUsage db_usage) +{ + std::string filename = m_ioDB; + + do { + // Run this loop at least once for all files. If this is an automatic + // restart, then make sure that the generated file does not already exist, + // so keep running the loop until we generate a filename that doesn't exist... + std::ostringstream tmp_filename; + tmp_filename << m_ioDB; + + // Don't append the "-s000X" the first time in case the base filename doesn't + // exist -- we want write to the name specified by the user if at all possible and + // once that exists, then start adding on the suffix... + if (m_dbChangeCount > 1) { + tmp_filename << "-s" << std::setw(4) << std::setfill('0') << m_dbChangeCount; + } + filename = tmp_filename.str(); + ++m_dbChangeCount; + } while(file_exists(filename, m_dbType, db_usage)); + --m_dbChangeCount; + return filename; +} + +std::string DynamicTopologyFileControl::construct_database_filename(int& step, Ioss::DatabaseUsage db_usage) +{ + // Filename will be of the form -- ioDB-sxxxx where xxxx is step + // number. Assume maximum of 9999 steps (will do more, but won't have + // good lineup of step numbers. + // Check database for validity (filename and a type) + if(m_ioDB.empty() || m_dbType.empty()) + { + std::string error_message; + if(m_dbType.empty()) + error_message += "The database TYPE has not been defined\n"; + + if(m_ioDB.empty()) + { + error_message += "The database FILENAME has not been defined\n"; + } + std::ostringstream errmsg; + fmt::print(errmsg, error_message); + IOSS_ERROR(errmsg); + } + assert(!m_ioDB.empty()); + assert(!m_dbType.empty()); + std::string filename = m_ioDB; + if(m_fileCyclicCount > 0) + { + // In this mode, we close the old file and open a new file + // every time this is called. The file suffix cycles through + // the first fileCyclicCount'th entries in A,B,C,D,E,F,... + if(step == 0) + step++; + + static std::string suffix = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + std::string tmp = "-" + suffix.substr((step - 1) % m_fileCyclicCount, 1); + filename += tmp; + m_properties.add(Ioss::Property("APPEND_OUTPUT", Ioss::DB_OVERWRITE)); + } + else + { + if(m_region->model_is_written()) + { + // After the initial open, we want to add suffix if the topology changes + // during the run + m_ifDatabaseExists = Ioss::DB_ADD_SUFFIX_OVERWRITE; + } + + // Handle complications of DB_APPEND mode... + // If in DB_APPEND mode, then we don't output metadata + // information, so some knowledge is needed at this level if + // we are appending. If user specified APPEND, but the file + // doesn't yet exist OR it does exist and we are not + // restarting, change the mode to OVERWRITE. + // 0. Must be restarting; either manual or automatic. + std::shared_ptr observer = m_region->get_mesh_modification_observer(); + + if(m_ifDatabaseExists == Ioss::DB_APPEND) + { + if(!observer->is_restart_requested()) + { + // Not restarting + m_ifDatabaseExists = Ioss::DB_OVERWRITE; + } + else if(!file_exists(m_ioDB, m_dbType, db_usage)) + { + m_ifDatabaseExists = Ioss::DB_OVERWRITE; + } + } + if(step > 1 || (m_dbChangeCount > 1)) + { + // Use the !is_input_event test since restart input files already have the + // -s000x extension... + if(m_ifDatabaseExists == Ioss::DB_APPEND) + { + std::ostringstream tmp_filename; + tmp_filename << m_ioDB; + filename = m_ioDB; + if(m_dbChangeCount > 1) + { + tmp_filename << "-s" << std::setw(4) << std::setfill('0') << m_dbChangeCount; + } + size_t inc = 0; + while(file_exists(tmp_filename.str(), m_dbType, db_usage)) + { + filename = tmp_filename.str(); + tmp_filename.clear(); + tmp_filename.str(""); + tmp_filename << m_ioDB << "-s" << std::setw(4) << std::setfill('0') << m_dbChangeCount + (++inc); + } + if(inc > 0) + { + m_dbChangeCount += (inc - 1); + } + else + { + m_ifDatabaseExists = Ioss::DB_OVERWRITE; + } + } + else if(m_ifDatabaseExists == Ioss::DB_ADD_SUFFIX) + { + filename = get_unique_filename(db_usage); + } + else if(m_ifDatabaseExists == Ioss::DB_ADD_SUFFIX_OVERWRITE) + { + if(m_dbChangeCount > 0) + { + std::ostringstream tmp_filename; + tmp_filename << m_ioDB << "-s" << std::setw(4) << std::setfill('0') << ++m_dbChangeCount; + filename = tmp_filename.str(); + } + else + { + filename = m_ioDB; + } + } + else + { + filename = m_ioDB; + } + } + else if(m_ifDatabaseExists == Ioss::DB_ADD_SUFFIX) + { + filename = get_unique_filename(db_usage); + } + else + { + filename = m_ioDB; + } + + m_properties.add(Ioss::Property("APPEND_OUTPUT", m_ifDatabaseExists)); + // A little complicated on deciding whether we are actually + // overwriting the database. The 'validate' routine for Results and + // History will call create_database once the parser block is + // ended. This routine will then create the database and the + // ioRegion_. However, the database will not really be opened or + // written to at this time. If the code is auto-restarting, then it will + // detect that the database exists and create a database with the + // -s000x extension. + // At this point, we need to skip the 'abort_if_exists' test if we + // are in this routine from the 'validate' and we are restarting + // since we won't really write to the file. So, the cases where we + // *don't* check are: + // -- is_input_event(db_usage) + // -- ifExists_ == DB_OVERWRITE || DB_ADD_SUFFIX_OVERWRITE || DB_APPEND + // -- is_automatic_restart() && step == 0 (coming from validate) + if(m_ifDatabaseExists != DB_OVERWRITE && + m_ifDatabaseExists != DB_APPEND && + m_ifDatabaseExists != DB_ADD_SUFFIX_OVERWRITE && + !(step == 0 && observer->is_automatic_restart())) + { + abort_if_exists(filename, m_dbType, db_usage); + } + } + return filename; +} + +bool DynamicTopologyFileControl::abort_if_exists(const std::string &filename, + const std::string &db_type, + Ioss::DatabaseUsage db_usage) +{ + // Check whether file with same name as database already exists. If so, + // print error message and stop... + // At the current time, only check on processor 0 and assume if it doesn't exist + // there, then it doesn't exist on other processors. Or, if it doesn't exist on + // processor 0, then it doesn't matter if it doesn't exist on other processors + // since we don't have all pieces... + + bool exists = file_exists(filename, db_type, db_usage); + if (exists) { + std::ostringstream errmsg; + fmt::print(errmsg, + "ERROR: The database file named '{} exists" + "and would be overwritten if the code continued.\n\n" + "Input options specified that this file *not* be overwritten,\n" + "\tso you must rename or remove this file and restart the code.\n", + filename); + IOSS_ERROR(errmsg); + } + return exists; +} + +Ioss::DatabaseIO * DynamicTopologyFileControl::clone_output_database(int steps) +{ + auto current_db = m_region->get_database(); + + if (current_db->is_input()) + return nullptr; + + const Ioss::PropertyManager& current_properties = current_db->get_property_manager(); + Ioss::NameList names; + current_properties.describe(&names); + + // Iterate through properties and transfer to new output database... + Ioss::NameList::const_iterator I; + for (I = names.begin(); I != names.end(); ++I) { + if (!current_properties.exists(*I)) + m_properties.add(current_properties.get(*I)); + } + + auto db_usage = current_db->usage(); + + std::string filename = construct_database_filename(steps, db_usage); + + Ioss::DatabaseIO *db = Ioss::IOFactory::create(m_dbType, filename, db_usage, + current_db->util().communicator(), + m_properties); + + if (nullptr == db) { + std::ostringstream errmsg; + fmt::print(errmsg, + "ERROR: unable to create output database named '{}'" + " of type '{}'", filename, m_dbType); + IOSS_ERROR(errmsg); + } + + assert(db != nullptr); + if(!db->ok(true)) { + std::ostringstream errmsg; + fmt::print(errmsg, + "ERROR: unable to validate output database named '{}'" + " of type '{}'", filename, m_dbType); + IOSS_ERROR(errmsg); + } + + db->set_field_separator(current_db->get_field_separator()); + db->set_surface_split_type(current_db->get_surface_split_type()); + db->set_maximum_symbol_length(current_db->maximum_symbol_length()); + db->set_int_byte_size_api(current_db->int_byte_size_data_size()); + + return db; +} + +template +void update_database_for_grouping_entities(const T& container, Ioss::DatabaseIO *db) +{ + for(auto * entity : container) { + Ioss::GroupingEntity* ge = dynamic_cast(entity); + assert(ge != nullptr); + + if(ge->type() == Ioss::SIDESET) { + Ioss::SideSet *sset = dynamic_cast(ge); + assert(sset != nullptr); + + sset->reset_database(db); + const auto &sblocks = sset->get_side_blocks(); + for (const auto &sblock : sblocks) { + sblock->reset_database(db); + } + } else { + ge->reset_database(db); + } + } +} + +bool DynamicTopologyFileControl::replace_output_database(Ioss::DatabaseIO *db) +{ + auto current_db = m_region->get_database(); + + if (current_db->is_input()) + return false; + + current_db->finalize_database(); + current_db->closeDatabase(); + delete current_db; + + m_region->reset_database(db); + db->set_region(m_region); + + update_database_for_grouping_entities(m_region->get_node_blocks(), db); + update_database_for_grouping_entities(m_region->get_edge_blocks(), db); + update_database_for_grouping_entities(m_region->get_face_blocks(), db); + update_database_for_grouping_entities(m_region->get_element_blocks(), db); + update_database_for_grouping_entities(m_region->get_sidesets(), db); + update_database_for_grouping_entities(m_region->get_nodesets(), db); + update_database_for_grouping_entities(m_region->get_edgesets(), db); + update_database_for_grouping_entities(m_region->get_facesets(), db); + update_database_for_grouping_entities(m_region->get_elementsets(), db); + update_database_for_grouping_entities(m_region->get_commsets(), db); + update_database_for_grouping_entities(m_region->get_structured_blocks(), db); + update_database_for_grouping_entities(m_region->get_assemblies(), db); + update_database_for_grouping_entities(m_region->get_blobs(), db); + + return true; +} + +void DynamicTopologyFileControl::clone_and_replace_output_database(int steps) +{ + auto db = clone_output_database(steps); + + if(nullptr != db) + replace_output_database(db); +} + +void DynamicTopologyFileControl::add_output_database_group(int steps) +{ + auto current_db = m_region->get_database(); + + std::ostringstream oss; + oss << "STEP-"; + oss << steps; + + current_db->release_memory(); + current_db->open_root_group(); + current_db->create_subgroup(oss.str()); +} + +} + + + + diff --git a/packages/seacas/libraries/ioss/src/Ioss_DynamicTopology.h b/packages/seacas/libraries/ioss/src/Ioss_DynamicTopology.h new file mode 100644 index 0000000000..be25390e3c --- /dev/null +++ b/packages/seacas/libraries/ioss/src/Ioss_DynamicTopology.h @@ -0,0 +1,144 @@ +// Copyright(C) 2024 National Technology & Engineering Solutions +// of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with +// NTESS, the U.S. Government retains certain rights in this software. +// +// See packages/seacas/LICENSE for details + +#pragma once + +#include "Ioss_DatabaseIO.h" // for DatabaseIO +#include "Ioss_DBUsage.h" +#include "Ioss_ParallelUtils.h" // for ParallelUtils +#include "Ioss_PropertyManager.h" // for PropertyManager +#include +#include // for size_t, nullptr +#include // for int64_t + +#include "Ioss_CodeTypes.h" +#include "Ioss_Utils.h" +#include "ioss_export.h" + +#include // for ostream +#include +#include // for string, operator< + +namespace Ioss { + class Region; + + /*! The TopologyModified enumeration is used as an argument to the + * topology_modified() functions in io to + * specify the type of topology modification that has ocurred. The + * calls to topology_modified() are cumulative between + * output steps, so a call with TOPOLOGY_REORDER followed by a call + * with TOPOLOGY_SHUFFLE will do the right thing. Typical examples + * of when these would be used are: + * - TOPOLOGY_SAME: No change, but easier to call function than not. + * - TOPOLOGY_REORDER: Element Death which reorders the Registrars + * - TOPOLOGY_SHUFFLE: Load Balancing + * - TOPOLOGY_HADAPT: H-Adaptivity + * - TOPOLOGY_GHOST: Ghost nodes/edges/faces/elements created/destroyed + * - TOPOLOGY_GEOMETRY: Model data is modified, overlap removal. + * - TOPOLOGY_CREATEDELETE: Surface erosion, particle creation + * - TOPOLOGY_UNKNOWN: Something else, catchall option. + */ + enum TopologyModified { + TOPOLOGY_SAME = ( 0), //!< No change, also used for initialization + TOPOLOGY_REORDER = (1U << 0), //!< Data structures reordered on processor, no change between procs. + TOPOLOGY_SHUFFLE = (1U << 1), //!< Globally the same, data moved among processors. + TOPOLOGY_HADAPT = (1U << 2), //!< Elements split/combined; not moved cross-proc + TOPOLOGY_GHOST = (1U << 3), //!< Ghost entities created/destroyed + TOPOLOGY_GEOMETRY = (1U << 4), //!< Geometry (mesh coordinates) modified. Restart needs to know this. + TOPOLOGY_CREATEFACE = (1U << 5), //!< Face/Edge are created/deleted. + TOPOLOGY_CREATEELEM = (1U << 6), //!< Elements are created/deleted. + TOPOLOGY_CREATENODE = (1U << 7), //!< Nodes are created/deleted. + TOPOLOGY_UNKNOWN = (1U << 8), //!< Unknown change, recreate from scratch. + TOPOLOGY_AUXILIARY = (1U << 9), //!< An AUXILIARY relation was created/modified. + TOPOLOGY_CONSTRAINT = (1U << 10) //!< Contact constraints + }; + + enum class FileControlOption { + CONTROL_NONE, + CONTROL_AUTO_MULTI_FILE, + CONTROL_AUTO_SINGLE_FILE + }; + + class IOSS_EXPORT DynamicTopologyObserver + { + public: + DynamicTopologyObserver(Region *region) + : m_region(region) {} + DynamicTopologyObserver() {} + + virtual ~DynamicTopologyObserver() {} + + virtual void reset_topology_modification(); + virtual void set_topology_modification(unsigned int type); + virtual unsigned int get_topology_modification() const; + + virtual unsigned int get_cumulative_topology_modification() const; + virtual void set_cumulative_topology_modification(unsigned int type); + + int get_cumulative_topology_modification_field(); + + virtual bool is_topology_modified() const; + virtual bool is_automatic_restart() const { return false; } + virtual bool is_restart_requested() const { return false; } + + static const std::string topology_modification_change_name() {return std::string("CUMULATIVE_TOPOLOGY_MODIFICATION");} + + void register_region(Region *region); + Region* get_region() const { return m_region; } + + virtual void define_model(Region& region); + virtual void write_model(Region& region); + virtual void define_transient(Region& region); + + virtual FileControlOption get_control_option() const { return FileControlOption::CONTROL_AUTO_MULTI_FILE; } + + protected: + Region *m_region{nullptr}; + unsigned int m_topologyModification{TOPOLOGY_SAME}; + unsigned int m_cumulativeTopologyModification{TOPOLOGY_SAME}; + + bool m_automaticRestart{false}; + bool m_restartRequested{false}; + + void check_region() const; + IOSS_NODISCARD const ParallelUtils &util() const; + void synchronize_topology_modified_flags(); + }; + + class IOSS_EXPORT DynamicTopologyFileControl + { + public: + DynamicTopologyFileControl(Region *region, unsigned int fileCyclicCount, + IfDatabaseExistsBehavior &ifDatabaseExists, + unsigned int &dbChangeCount); + + void clone_and_replace_output_database(int steps=0); + void add_output_database_group(int steps=0); + + private: + Region *m_region{nullptr}; + std::string m_ioDB; + std::string m_dbType; + + PropertyManager m_properties; + + unsigned int m_fileCyclicCount; + IfDatabaseExistsBehavior &m_ifDatabaseExists; + unsigned int &m_dbChangeCount; + + IOSS_NODISCARD const ParallelUtils &util() const; + + std::string get_unique_filename(DatabaseUsage db_usage); + std::string construct_database_filename(int& step, DatabaseUsage db_usage); + bool file_exists(const std::string &filename, const std::string &db_type, DatabaseUsage db_usage); + bool abort_if_exists(const std::string &filename, const std::string &db_type, + DatabaseUsage db_usage); + + DatabaseIO * clone_output_database(int steps); + bool replace_output_database(DatabaseIO *db); + }; + +} // namespace Ioss diff --git a/packages/seacas/libraries/ioss/src/Ioss_IOFactory.C b/packages/seacas/libraries/ioss/src/Ioss_IOFactory.C index e020d8dd37..463040ed0f 100644 --- a/packages/seacas/libraries/ioss/src/Ioss_IOFactory.C +++ b/packages/seacas/libraries/ioss/src/Ioss_IOFactory.C @@ -94,6 +94,8 @@ Ioss::DatabaseIO *Ioss::IOFactory::create(const std::string &type, const std::st } else { auto my_props(properties); + my_props.add(Property("database_type", type)); + Ioss::ParallelUtils pu(communicator); pu.add_environment_properties(my_props); if (my_props.exists("SHOW_CONFIG")) { diff --git a/packages/seacas/libraries/ioss/src/Ioss_Region.C b/packages/seacas/libraries/ioss/src/Ioss_Region.C index a3660c18f6..82850ffaa1 100644 --- a/packages/seacas/libraries/ioss/src/Ioss_Region.C +++ b/packages/seacas/libraries/ioss/src/Ioss_Region.C @@ -20,7 +20,9 @@ #include "Ioss_FaceBlock.h" #include "Ioss_FaceSet.h" #include "Ioss_Field.h" +#include "Ioss_FileInfo.h" #include "Ioss_GroupingEntity.h" +#include "Ioss_IOFactory.h" #include "Ioss_NodeBlock.h" #include "Ioss_NodeSet.h" #include "Ioss_Property.h" @@ -43,6 +45,11 @@ #include #include +#include +#include +#include +#include + #include "Ioss_MeshType.h" #include "Ioss_ParallelUtils.h" @@ -95,20 +102,30 @@ namespace { return count; } + void update_database(Ioss::DatabaseIO *db, Ioss::GroupingEntity *entity) + { + entity->reset_database(db); + } + void update_database(const Ioss::Region *region, Ioss::GroupingEntity *entity) { - entity->reset_database(region->get_database()); + update_database(region->get_database(), entity); } - void update_database(const Ioss::Region *region, Ioss::SideSet *sset) + void update_database(Ioss::DatabaseIO *db, Ioss::SideSet *sset) { - sset->reset_database(region->get_database()); + sset->reset_database(db); const auto &blocks = sset->get_side_blocks(); for (const auto &block : blocks) { - block->reset_database(region->get_database()); + block->reset_database(db); } } + void update_database(const Ioss::Region *region, Ioss::SideSet *sset) + { + update_database(region->get_database(), sset); + } + constexpr unsigned numberOfBits(unsigned x) { return x < 2 ? x : 1 + numberOfBits(x >> 1); } size_t compute_hash(Ioss::GroupingEntity *entity, size_t which) @@ -348,6 +365,9 @@ namespace Ioss { properties.add(Property(this, "state_count", Property::INTEGER)); properties.add(Property(this, "current_state", Property::INTEGER)); properties.add(Property(this, "database_name", Property::STRING)); + + property_add(Property("base_filename", iodatabase->get_filename())); + property_add(Property("database_type", iodatabase->get_property_manager().get_optional("database_type", ""))); } Region::~Region() @@ -358,63 +378,86 @@ namespace Ioss { // Region owns all sub-grouping entities it contains... try { IOSS_FUNC_ENTER(m_); - for (const auto &nb : nodeBlocks) { - delete (nb); - } + reset_region(); - for (const auto &eb : edgeBlocks) { - delete (eb); - } + // Region owns the database pointer even though other entities use it. + GroupingEntity::really_delete_database(); + } + catch (...) { + } + } - for (const auto &fb : faceBlocks) { - delete (fb); - } + void Region::reset_region() + { + for (const auto &nb : nodeBlocks) { + delete (nb); + } + nodeBlocks.clear(); - for (const auto &eb : elementBlocks) { - delete (eb); - } + for (const auto &eb : edgeBlocks) { + delete (eb); + } + edgeBlocks.clear(); - for (const auto &sb : structuredBlocks) { - delete (sb); - } + for (const auto &fb : faceBlocks) { + delete (fb); + } + faceBlocks.clear(); - for (const auto &ss : sideSets) { - delete (ss); - } + for (const auto &eb : elementBlocks) { + delete (eb); + } + elementBlocks.clear(); - for (const auto &ns : nodeSets) { - delete (ns); - } + for (const auto &sb : structuredBlocks) { + delete (sb); + } + structuredBlocks.clear(); - for (const auto &es : edgeSets) { - delete (es); - } + for (const auto &ss : sideSets) { + delete (ss); + } + sideSets.clear(); - for (const auto &fs : faceSets) { - delete (fs); - } + for (const auto &ns : nodeSets) { + delete (ns); + } + nodeSets.clear(); - for (const auto &es : elementSets) { - delete (es); - } + for (const auto &es : edgeSets) { + delete (es); + } + edgeSets.clear(); - for (const auto &cs : commSets) { - delete (cs); - } + for (const auto &fs : faceSets) { + delete (fs); + } + faceSets.clear(); - for (const auto &as : assemblies) { - delete (as); - } + for (const auto &es : elementSets) { + delete (es); + } + elementSets.clear(); - for (const auto &bl : blobs) { - delete (bl); - } + for (const auto &cs : commSets) { + delete (cs); + } + commSets.clear(); - // Region owns the database pointer even though other entities use it. - GroupingEntity::really_delete_database(); + for (const auto &as : assemblies) { + delete (as); } - catch (...) { + assemblies.clear(); + + for (const auto &bl : blobs) { + delete (bl); } + blobs.clear(); + + stateTimes.clear(); + + currentState = -1; + stateCount = 0; } void Region::delete_database() { GroupingEntity::really_delete_database(); } @@ -652,6 +695,27 @@ namespace Ioss { success = set_state(new_state); } else { + bool has_output_observer = topologyObserver && !get_database()->is_input(); + + if(new_state == STATE_DEFINE_MODEL) { + if(has_output_observer && (topologyObserver->get_control_option() == FileControlOption::CONTROL_AUTO_SINGLE_FILE)) { + if(!modelWritten) { + int steps = get_property("state_count").get_int(); + bool force_addition = true; + add_output_database_group(steps, force_addition); + } + } + } else if(new_state == STATE_TRANSIENT) { + if(has_output_observer && topologyObserver->is_topology_modified()) { + int steps = get_property("state_count").get_int(); + start_new_output_database_entry(steps); + + topologyObserver->define_model(*this); + topologyObserver->write_model(*this); + topologyObserver->define_transient(*this); + topologyObserver->reset_topology_modification(); + } + } switch (get_state()) { case STATE_CLOSED: // Make sure we can go to the specified state. @@ -785,6 +849,12 @@ namespace Ioss { else if (current_state == STATE_DEFINE_TRANSIENT) { transientDefined = true; } + else if (current_state == STATE_MODEL) { + modelWritten = true; + } + else if (current_state == STATE_TRANSIENT) { + transientWritten = true; + } return success; } @@ -2741,4 +2811,165 @@ namespace Ioss { } } + void Region::register_mesh_modification_observer(std::shared_ptr observer) + { + if(observer && observer->get_control_option() == FileControlOption::CONTROL_AUTO_SINGLE_FILE) { + const Ioss::PropertyManager &properties = get_database()->get_property_manager(); + if(!properties.exists("ENABLE_FILE_GROUPS")) { + std::ostringstream errmsg; + fmt::print(errmsg, + "ERROR: File groups are not enabled in the database file '{}'.\n", + get_database()->get_filename()); + IOSS_ERROR(errmsg); + } + } + + topologyObserver = observer; + topologyObserver->register_region(this); + } + + void Region::start_new_output_database_entry(int steps) + { + if (get_database()->is_input()) + return; + + if(!topologyObserver) + return; + + switch(topologyObserver->get_control_option()) { + case FileControlOption::CONTROL_AUTO_MULTI_FILE: + clone_and_replace_output_database(steps); + break; + case FileControlOption::CONTROL_AUTO_SINGLE_FILE: + add_output_database_group(steps); + break; + case FileControlOption::CONTROL_NONE: + default: + return; + break; + } + } + + void Region::add_output_database_group(int steps, bool force_addition) + { + if (get_database()->is_input()) + return; + + const Ioss::PropertyManager &properties = get_database()->get_property_manager(); + if(!properties.exists("ENABLE_FILE_GROUPS")) { + std::ostringstream errmsg; + fmt::print(errmsg, + "ERROR: File groups are not enabled in the database file '{}'.\n", + get_database()->get_filename()); + IOSS_ERROR(errmsg); + } + + if(topologyObserver && (topologyObserver->get_control_option() == FileControlOption::CONTROL_AUTO_MULTI_FILE)) { + std::ostringstream errmsg; + fmt::print(errmsg, + "ERROR: TopologyObserver for database file '{}' does not support file groups.\n", + get_database()->get_filename()); + IOSS_ERROR(errmsg); + } + + int state = steps; + if ((topologyObserver && topologyObserver->is_topology_modified()) || force_addition) { + // Determine how many steps have been written already... + state = get_property("state_count").get_int(); + + if (state == 0) + state = steps; + + // See if this is a continuation database... + if (property_exists("state_offset")) + state += get_property("state_offset").get_int(); + + state++; // For the state we are going to write. + + reset_region(); + DynamicTopologyFileControl fileControl(this, fileCyclicCount, ifDatabaseExists, dbChangeCount); + fileControl.add_output_database_group(state); + } + } + + void Region::clone_and_replace_output_database(int steps) + { + if (get_database()->is_input()) + return; + + if(!topologyObserver) + return; + + int state = steps; + if (topologyObserver->is_topology_modified() || fileCyclicCount > 0) { + // Determine how many steps have been written already... + state = get_property("state_count").get_int(); + + // Needed for automatic restart... The current database has not + // been written to, but we want to open a new one instead of + // (possibly) overwriting the current one... + // If this is not an automatic restart, then we don't need a new database... + if (state == 0 && topologyObserver->is_automatic_restart()) + return; + + if (topologyObserver->is_automatic_restart() && ifDatabaseExists == Ioss::DB_APPEND) + return; + + if (state == 0) + state = steps; + + // See if this is a continutation database... + if (property_exists("state_offset")) + state += get_property("state_offset").get_int(); + + state++; // For the state we are going to write. + + reset_region(); + DynamicTopologyFileControl fileControl(this, fileCyclicCount, ifDatabaseExists, dbChangeCount); + fileControl.clone_and_replace_output_database(state); + } + } + + bool Region::load_group_mesh(const std::string &group_name) + { + // Check name for '/' which is not allowed since it is the + // separator character in a full group path + if (group_name.find('/') != std::string::npos) { + std::ostringstream errmsg; + fmt::print(errmsg, "ERROR: Invalid group name '{}' contains a '/' which is not allowed.\n", + group_name); + IOSS_ERROR(errmsg); + } + + DatabaseIO *iodatabase = get_database(); + + if (!iodatabase->is_input()) + return false; + + if(!iodatabase->open_root_group()) + return false; + + if(!iodatabase->open_group(group_name)) + return false; + + reset_region(); + iodatabase->release_memory(); + + Region::set_state(STATE_CLOSED); + modelDefined = false; + transientDefined = false; + + Region::begin_mode(STATE_DEFINE_MODEL); + iodatabase->read_meta_data(); + Region::end_mode(STATE_DEFINE_MODEL); + if (iodatabase->open_create_behavior() != Ioss::DB_APPEND && + iodatabase->open_create_behavior() != Ioss::DB_MODIFY) { + modelDefined = true; + transientDefined = true; + Region::begin_mode(STATE_READONLY); + } + + return true; + } + } // namespace Ioss diff --git a/packages/seacas/libraries/ioss/src/Ioss_Region.h b/packages/seacas/libraries/ioss/src/Ioss_Region.h index 730693ad61..9a7f27754b 100644 --- a/packages/seacas/libraries/ioss/src/Ioss_Region.h +++ b/packages/seacas/libraries/ioss/src/Ioss_Region.h @@ -8,6 +8,8 @@ #include "Ioss_CoordinateFrame.h" // for CoordinateFrame #include "Ioss_DatabaseIO.h" // for DatabaseIO +#include "Ioss_DBUsage.h" +#include "Ioss_DynamicTopology.h" #include "Ioss_EntityType.h" // for EntityType, etc #include "Ioss_Field.h" #include "Ioss_GroupingEntity.h" // for GroupingEntity @@ -28,6 +30,7 @@ #include // for less #include // for ostream #include // for map, map<>::value_compare +#include #include #include // for string, operator< #include // for pair @@ -55,6 +58,7 @@ namespace Ioss { namespace Ioss { class CoordinateFrame; + enum class MeshType; using AssemblyContainer = std::vector; @@ -280,7 +284,28 @@ namespace Ioss { const std::vector &entity_container, std::vector &field_data) const; + void register_mesh_modification_observer(std::shared_ptr observer); + std::shared_ptr get_mesh_modification_observer() const { return topologyObserver; } + + void start_new_output_database_entry(int steps=0); + + void set_topology_change_count(unsigned int new_count) {dbChangeCount = new_count;} + unsigned int get_topology_change_count() {return dbChangeCount;} + + void set_file_cyclic_count(unsigned int new_count) {fileCyclicCount = new_count;} + unsigned int get_file_cyclic_count() {return fileCyclicCount;} + + void set_if_database_exists_behavior(IfDatabaseExistsBehavior if_exists) {ifDatabaseExists = if_exists;} + + bool model_is_written() const { return modelWritten; } + bool transient_is_written() const { return transientWritten; } + + bool load_group_mesh(const std::string &group_name); + protected: + void clone_and_replace_output_database(int steps=0); + void add_output_database_group(int steps=0, bool force_addition=false); + int64_t internal_get_field_data(const Field &field, void *data, size_t data_size = 0) const override; @@ -306,6 +331,7 @@ namespace Ioss { bool end_mode_nl(State current_state); void delete_database() override; + void reset_region(); mutable std::map aliases_; ///< Stores alias mappings @@ -332,6 +358,15 @@ namespace Ioss { mutable int stateCount{0}; bool modelDefined{false}; bool transientDefined{false}; + + std::shared_ptr topologyObserver; + + unsigned int dbChangeCount{1}; //!< Used to track number of topology changes. + unsigned int fileCyclicCount{0}; //!< For cycling file-A, file-B, file-C, ..., File-A, typically restart only. + IfDatabaseExistsBehavior ifDatabaseExists{DB_OVERWRITE}; + + bool modelWritten{false}; + bool transientWritten{false}; }; } // namespace Ioss diff --git a/packages/seacas/libraries/ioss/src/exodus/Ioex_BaseDatabaseIO.C b/packages/seacas/libraries/ioss/src/exodus/Ioex_BaseDatabaseIO.C index d56ed2194c..5733bdf694 100644 --- a/packages/seacas/libraries/ioss/src/exodus/Ioex_BaseDatabaseIO.C +++ b/packages/seacas/libraries/ioss/src/exodus/Ioex_BaseDatabaseIO.C @@ -18,6 +18,7 @@ #include #include #include +#include #include "Ioex_Utils.h" #include "Ioss_Assembly.h" @@ -87,6 +88,8 @@ namespace { template void write_attribute_names(int exoid, ex_entity_type type, const std::vector &entities); + void query_groups(int exoid, Ioss::NameList& names, bool return_full_names); + class AssemblyTreeFilter { public: @@ -499,6 +502,40 @@ namespace Ioex { ex_set_max_name_length(m_exodusFilePtr, maximumNameLength); } + bool BaseDatabaseIO::open_root_group_nl() + { + // Get existing file pointer... + bool success = false; + + int exoid = get_file_pointer(); + + int group_name_length = ex_inquire_int(exoid, EX_INQ_GROUP_NAME_LEN); + std::vector group_name(group_name_length+1, '\0'); + + // Get name of this group... + int idum; + float rdum; + int ierr = ex_inquire(exoid, EX_INQ_GROUP_NAME, &idum, &rdum, group_name.data()); + if (ierr < 0) { + std::ostringstream errmsg; + fmt::print(errmsg, "ERROR: Could not open root group of group named '{}' in file '{}'.\n", m_groupName, + get_filename()); + IOSS_ERROR(errmsg); + } + + m_groupName = std::string(group_name.data()); + m_exodusFilePtr = ex_inquire_int(exoid, EX_INQ_GROUP_ROOT); + + if (m_exodusFilePtr < 0) { + std::ostringstream errmsg; + fmt::print(errmsg, "ERROR: Could not open group named '{}' in file '{}'.\n", m_groupName, + get_filename()); + IOSS_ERROR(errmsg); + } + success = true; + return success; + } + bool BaseDatabaseIO::open_group_nl(const std::string &group_name) { // Get existing file pointer... @@ -3180,6 +3217,41 @@ namespace Ioex { // Write coordinate frame data... write_coordinate_frames(get_file_pointer(), get_region()->get_coordinate_frames()); } + + + Ioss::NameList BaseDatabaseIO::groups_describe_nl(bool return_full_names) + { + Ioss::SerializeIO serializeIO_(this); + + Ioss::NameList names; + int group_root = ex_inquire_int(get_file_pointer(), EX_INQ_GROUP_ROOT); + query_groups(group_root, names, return_full_names); + + return names; + } + + void BaseDatabaseIO::release_memory_nl() + { + Ioss::DatabaseIO::release_memory_nl(); + + ids_.clear(); + m_groupCount.clear(); + + nodeCmapIds.clear(); + nodeCmapNodeCnts.clear(); + elemCmapIds.clear(); + elemCmapElemCnts.clear(); + + m_truthTable.clear(); + m_variables.clear(); + m_reductionVariables.clear(); + + m_reductionValues.clear(); + + nodeConnectivityStatus.clear(); + + activeNodeSetNodesIndex.clear(); + } } // namespace Ioex namespace { @@ -3481,4 +3553,41 @@ namespace { } #endif } + + void query_groups(int exoid, Ioss::NameList& names, bool return_full_names) + { + int idum; + float rdum; + + int group_name_length = ex_inquire_int(exoid, EX_INQ_GROUP_NAME_LEN); + std::vector group_name(group_name_length+1, '\0'); + + // Get name of this group... + int ierr = ex_inquire(exoid, EX_INQ_GROUP_NAME, &idum, &rdum, group_name.data()); + if (ierr < 0) { + Ioex::exodus_error(exoid, __LINE__, __func__, __FILE__); + } + + if(return_full_names) { + std::fill(group_name.begin(), group_name.end(), '\0'); + ierr = ex_inquire(exoid, EX_INQ_FULL_GROUP_NAME, &idum, &rdum, group_name.data()); + if (ierr < 0) { + Ioex::exodus_error(exoid, __LINE__, __func__, __FILE__); + } + names.push_back(std::string(group_name.data())); + } else { + names.push_back(std::string(group_name.data())); + } + + int num_children = ex_inquire_int(exoid, EX_INQ_NUM_CHILD_GROUPS); + std::vector children(num_children); + ierr = ex_get_group_ids(exoid, nullptr, Data(children)); + if (ierr < 0) { + Ioex::exodus_error(exoid, __LINE__, __func__, __FILE__); + } + + for (int i = 0; i < num_children; i++) { + query_groups(children[i], names, return_full_names); + } + } } // namespace diff --git a/packages/seacas/libraries/ioss/src/exodus/Ioex_BaseDatabaseIO.h b/packages/seacas/libraries/ioss/src/exodus/Ioex_BaseDatabaseIO.h index 9521e11cd6..11174e6052 100644 --- a/packages/seacas/libraries/ioss/src/exodus/Ioex_BaseDatabaseIO.h +++ b/packages/seacas/libraries/ioss/src/exodus/Ioex_BaseDatabaseIO.h @@ -99,8 +99,12 @@ namespace Ioex { IOSS_NODISCARD bool ok_nl(bool write_message = false, std::string *error_message = nullptr, int *bad_count = nullptr) const override; + void release_memory_nl() override; + + bool open_root_group_nl() override; bool open_group_nl(const std::string &group_name) override; bool create_subgroup_nl(const std::string &group_name) override; + Ioss::NameList groups_describe_nl(bool return_full_names) override; bool begin_nl(Ioss::State state) override; bool end_nl(Ioss::State state) override; diff --git a/packages/seacas/libraries/ioss/src/exodus/Ioex_Utils.C b/packages/seacas/libraries/ioss/src/exodus/Ioex_Utils.C index d922e6fafd..12ac92d9e6 100644 --- a/packages/seacas/libraries/ioss/src/exodus/Ioex_Utils.C +++ b/packages/seacas/libraries/ioss/src/exodus/Ioex_Utils.C @@ -17,6 +17,7 @@ #include #include #include +#include #include "Ioss_BasisVariableType.h" #include "Ioss_CoordinateFrame.h" @@ -616,7 +617,7 @@ namespace Ioex { // Try to decode an id from the name. std::string name_string = entity->get_property(prop_name).get_string(); std::string type_name = entity->short_type_string(); - if (std::strncmp(type_name.c_str(), name_string.c_str(), type_name.size()) == 0) { + if (strncasecmp(type_name.c_str(), name_string.c_str(), type_name.size()) == 0) { id = extract_id(name_string); if (id <= 0) { id = 1; diff --git a/packages/seacas/libraries/ioss/src/unit_tests/CMakeLists.txt b/packages/seacas/libraries/ioss/src/unit_tests/CMakeLists.txt index 17a7bbb440..db5ff09522 100644 --- a/packages/seacas/libraries/ioss/src/unit_tests/CMakeLists.txt +++ b/packages/seacas/libraries/ioss/src/unit_tests/CMakeLists.txt @@ -14,6 +14,17 @@ TRIBITS_ADD_EXECUTABLE( SOURCES unitMain.C UnitTestElementBlockBatchRead.C ) +TRIBITS_ADD_EXECUTABLE( + Utst_dynamictopology + SOURCES unitMain.C UnitTestDynamicTopology.C +) + +TRIBITS_ADD_TEST( + Utst_dynamictopology + NAME Utst_dynamictopology + NUM_MPI_PROCS 1 +) + TRIBITS_ADD_TEST( Utst_blockbatchread NAME Utst_blockbatchread diff --git a/packages/seacas/libraries/ioss/src/unit_tests/UnitTestDynamicTopology.C b/packages/seacas/libraries/ioss/src/unit_tests/UnitTestDynamicTopology.C new file mode 100644 index 0000000000..150ff08038 --- /dev/null +++ b/packages/seacas/libraries/ioss/src/unit_tests/UnitTestDynamicTopology.C @@ -0,0 +1,679 @@ +// Copyright(C) 1999-2020, 2022 National Technology & Engineering Solutions +// of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 with +// NTESS, the U.S. Government retains certain rights in this software. +// +// See packages/seacas/LICENSE for details + +#include +#include + +#ifdef SEACAS_HAVE_MPI +#include "mpi.h" +#endif +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +#include // for unlink + +#include "Ionit_Initializer.h" +#include "Ioss_DBUsage.h" +#include "Ioss_DatabaseIO.h" // for DatabaseIO +#include "Ioss_Field.h" // for Field, etc +#include "Ioss_IOFactory.h" +#include "Ioss_ParallelUtils.h" +#include "Ioss_Property.h" +#include "Ioss_FileInfo.h" +#include "Ioss_ElementBlock.h" +#include "Ioss_NodeBlock.h" +#include "Ioss_ParallelUtils.h" +#include "Ioss_Region.h" + +#include "Ioss_Utils.h" + +#include "exodus/Ioex_DatabaseIO.h" + +namespace { +std::string get_many_block_mesh_desc(unsigned numBlocks) +{ + std::ostringstream oss; + std::vector elementIds(numBlocks); + std::iota(elementIds.begin(), elementIds.end(), 1); + + unsigned proc = 0; + for (unsigned i = 0; i < numBlocks; ++i) { + unsigned elemId = elementIds[i]; + unsigned firstNodeId = i * 4 + 1; + oss << proc << "," << elemId << ",HEX_8,"; + for (unsigned node = firstNodeId; node < firstNodeId + 8; ++node) { + oss << node << ","; + } + unsigned blockId = i + 1; + oss << "block_" << blockId; + + if (i < numBlocks - 1) { + oss << "\n"; + } + + proc++; + } + + oss << "|coordinates:"; + + std::vector planeCoords = {0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0}; + + for (double coord : planeCoords) { + oss << coord << ","; + } + + for (unsigned i = 1; i <= numBlocks; ++i) { + for (unsigned point = 0; point < 4; ++point) { + planeCoords[3 * point + 2] += 1; + } + + for (double coord : planeCoords) { + oss << coord << ","; + } + } + + return oss.str(); +} + +void define_model(const Ioss::Region &i_region, Ioss::Region &o_region) +{ + Ioss::DatabaseIO *o_database = o_region.get_database(); + + o_region.begin_mode(Ioss::STATE_DEFINE_MODEL); + + auto& nodeblocks = o_region.get_node_blocks(); + + Ioss::NodeBlock *i_nb = i_region.get_node_blocks()[0]; + int64_t spatial_dim = 3; + int64_t num_nodes = i_nb->entity_count(); + Ioss::NodeBlock *o_nb = new Ioss::NodeBlock(o_database, "nodeblock_1", num_nodes, spatial_dim); + o_region.add(o_nb); + + for (Ioss::ElementBlock *i_eb : i_region.get_element_blocks()) { + Ioss::ElementBlock *o_eb = new Ioss::ElementBlock( + o_database, i_eb->name(), i_eb->topology()->name(), i_eb->entity_count()); + o_eb->property_add(i_eb->get_property("id")); + o_region.add(o_eb); + } + + o_region.end_mode(Ioss::STATE_DEFINE_MODEL); +} + +void write_model(const Ioss::Region &i_region, Ioss::Region &o_region) +{ + Ioss::NodeBlock *i_nb = i_region.get_node_blocks()[0]; + Ioss::NodeBlock *o_nb = o_region.get_node_blocks()[0]; + + o_region.begin_mode(Ioss::STATE_MODEL); + std::vector coordinates; + std::vector node_ids; + i_nb->get_field_data("ids", node_ids); + i_nb->get_field_data("mesh_model_coordinates", coordinates); + + o_nb->put_field_data("ids", node_ids); + o_nb->put_field_data("mesh_model_coordinates", coordinates); + + for (Ioss::ElementBlock *i_eb : i_region.get_element_blocks()) { + Ioss::ElementBlock *o_eb = o_region.get_element_block(i_eb->name()); + std::vector elem_ids; + std::vector connectivity; + + i_eb->get_field_data("ids", elem_ids); + i_eb->get_field_data("connectivity", connectivity); + + o_eb->put_field_data("ids", elem_ids); + o_eb->put_field_data("connectivity", connectivity); + } + + o_region.end_mode(Ioss::STATE_MODEL); +} + +void define_transient(const Ioss::Region &i_region, Ioss::Region &o_region, + const std::string &elemFieldName) +{ + o_region.begin_mode(Ioss::STATE_DEFINE_TRANSIENT); + + for (Ioss::ElementBlock *o_eb : o_region.get_element_blocks()) { + size_t num_elem = o_eb->get_property("entity_count").get_int(); + std::string storage = "scalar"; + + Ioss::Field field(elemFieldName, Ioss::Field::REAL, storage, 1, Ioss::Field::Field::TRANSIENT, + num_elem); + o_eb->field_add(field); + } + o_region.end_mode(Ioss::STATE_DEFINE_TRANSIENT); +} + +int write_transient(Ioss::Region &o_region, const std::string &elemFieldName, const double time) +{ + o_region.begin_mode(Ioss::STATE_TRANSIENT); + int step = o_region.add_state(time); + o_region.begin_state(step); + + for (Ioss::ElementBlock *o_eb : o_region.get_element_blocks()) { + size_t num_elem = o_eb->get_property("entity_count").get_int(); + + std::vector field_data(num_elem); + std::vector elem_ids; + + o_eb->get_field_data("ids", elem_ids); + for (size_t i = 0; i < elem_ids.size(); i++) { + field_data[i] = (double)elem_ids[i] + 100*time; + } + + o_eb->put_field_data(elemFieldName, field_data); + } + + o_region.end_state(step); + o_region.end_mode(Ioss::STATE_TRANSIENT); + + return step; +} + +class Observer : public Ioss::DynamicTopologyObserver +{ +public: + Observer(Ioss::Region& inputRegion_, + const std::string &elemFieldName_, + const Ioss::FileControlOption fileControlOption_) + : inputRegion(inputRegion_) + , elemFieldName(elemFieldName_) + , fileControlOption(fileControlOption_) + {} + + virtual ~Observer() {} + + void define_model(Ioss::Region& region) override + { + ::define_model(inputRegion, region); + } + + void write_model(Ioss::Region& region) override + { + ::write_model(inputRegion, region); + } + + void define_transient(Ioss::Region& region) override + { + ::define_transient(inputRegion, region, elemFieldName); + } + + Ioss::FileControlOption get_control_option() const + { + return fileControlOption; + } + +private: + Observer(); + + Ioss::Region& inputRegion; + const std::string elemFieldName; + Ioss::FileControlOption fileControlOption; +}; + +void run_simple_topology_change(const Ioss::Region& i_region, + Ioss::Region &o_region, + std::shared_ptr observer, + const std::string &elemFieldName) +{ + define_model(i_region, o_region); + write_model(i_region, o_region); + + define_transient(i_region, o_region, elemFieldName); + + double time = 0.0; + write_transient(o_region, elemFieldName, time); + + auto min_result1 = o_region.get_min_time(); + EXPECT_EQ(1, min_result1.first); + EXPECT_NEAR(0.0, min_result1.second, 1.0e-6); + + auto max_result1 = o_region.get_max_time(); + EXPECT_EQ(1, max_result1.first); + EXPECT_NEAR(0.0, max_result1.second, 1.0e-6); + + observer->set_topology_modification(Ioss::TOPOLOGY_UNKNOWN); + + time = 1.0; + write_transient(o_region, elemFieldName, time); + + auto min_result2 = o_region.get_min_time(); + EXPECT_EQ(1, min_result2.first); + EXPECT_NEAR(1.0, min_result2.second, 1.0e-6); + + auto max_result2 = o_region.get_max_time(); + EXPECT_EQ(1, max_result2.first); + EXPECT_NEAR(1.0, max_result2.second, 1.0e-6); + + observer->set_topology_modification(Ioss::TOPOLOGY_SAME); + + time = 2.0; + write_transient(o_region, elemFieldName, time); + + auto min_result3 = o_region.get_min_time(); + EXPECT_EQ(1, min_result3.first); + EXPECT_NEAR(1.0, min_result3.second, 1.0e-6); + + auto max_result3 = o_region.get_max_time(); + EXPECT_EQ(2, max_result3.first); + EXPECT_NEAR(2.0, max_result3.second, 1.0e-6); +} + +void cleanup_multi_files(const std::string &outFile) +{ + Ioss::ParallelUtils util(Ioss::ParallelUtils::comm_world()); + + std::string file1 = Ioss::Utils::decode_filename(outFile, util.parallel_rank(), util.parallel_size()); + unlink(file1.c_str()); + + std::string file2 = Ioss::Utils::decode_filename(outFile + "-s0002", util.parallel_rank(), util.parallel_size()); + unlink(file2.c_str()); +} + +void run_multi_file_simple_topology_change(const std::string &elemFieldName, const std::string &outFile) +{ + Ioss::Init::Initializer io; + Ioss::ParallelUtils util(Ioss::ParallelUtils::comm_world()); + + int numBlocks = util.parallel_size(); + + std::string meshDesc = get_many_block_mesh_desc(numBlocks); + + Ioss::PropertyManager propertyManager; + + Ioss::DatabaseIO *i_database = Ioss::IOFactory::create("textmesh", meshDesc, Ioss::READ_MODEL, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region i_region(i_database, "input_model"); + EXPECT_TRUE(i_database != nullptr); + EXPECT_TRUE(i_database->ok(true)); + + Ioss::DatabaseIO *o_database = Ioss::IOFactory::create("exodus", outFile, Ioss::WRITE_RESULTS, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region o_region(o_database, "output_model"); + EXPECT_TRUE(o_database != nullptr); + EXPECT_TRUE(o_database->ok(true)); + + auto fileControlOption = Ioss::FileControlOption::CONTROL_AUTO_MULTI_FILE; + auto observer = std::make_shared(i_region, elemFieldName, fileControlOption); + o_region.register_mesh_modification_observer(observer); + + run_simple_topology_change(i_region, o_region, observer, elemFieldName); +} + +TEST(TestDynamicWrite, multi_file_simple_topology_modification) +{ + std::string outFile("multiFileManyBlocks.g"); + std::string elemFieldName = "elem_field"; + + cleanup_multi_files(outFile); + run_multi_file_simple_topology_change(elemFieldName, outFile); + cleanup_multi_files(outFile); +} + +void cleanup_single_file(const std::string &outFile) +{ + Ioss::ParallelUtils util(Ioss::ParallelUtils::comm_world()); + + std::string file1 = Ioss::Utils::decode_filename(outFile, util.parallel_rank(), util.parallel_size()); + unlink(file1.c_str()); +} + +void run_single_file_simple_topology_change(const std::string &elemFieldName, const std::string &outFile) +{ + Ioss::Init::Initializer io; + Ioss::ParallelUtils util(Ioss::ParallelUtils::comm_world()); + + int numBlocks = util.parallel_size(); + + std::string meshDesc = get_many_block_mesh_desc(numBlocks); + + Ioss::PropertyManager propertyManager; + + Ioss::DatabaseIO *i_database = Ioss::IOFactory::create("textmesh", meshDesc, Ioss::READ_MODEL, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region i_region(i_database, "input_model"); + EXPECT_TRUE(i_database != nullptr); + EXPECT_TRUE(i_database->ok(true)); + + propertyManager.add(Ioss::Property("ENABLE_FILE_GROUPS", 1)); + propertyManager.add(Ioss::Property("APPEND_OUTPUT", Ioss::DB_APPEND_GROUP)); + Ioss::DatabaseIO *o_database = Ioss::IOFactory::create("exodus", outFile, Ioss::WRITE_RESULTS, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region o_region(o_database, "output_model"); + EXPECT_TRUE(o_database != nullptr); + EXPECT_TRUE(o_database->ok(true)); + + auto fileControlOption = Ioss::FileControlOption::CONTROL_AUTO_SINGLE_FILE; + auto observer = std::make_shared(i_region, elemFieldName, fileControlOption); + o_region.register_mesh_modification_observer(observer); + + run_simple_topology_change(i_region, o_region, observer, elemFieldName); + + Ioss::NameList names = o_database->groups_describe(false); + Ioss::NameList full_names = o_database->groups_describe(true); + + std::vector gold_names{"/", "STEP-1", "STEP-2"}; + std::vector gold_full_names{"/", "/STEP-1", "/STEP-2"}; + + EXPECT_EQ(gold_names, names); + EXPECT_EQ(gold_full_names, full_names); +} + +TEST(TestDynamicWrite, single_file_simple_topology_modification) +{ + std::string outFile("singleFileManyBlocks.g"); + std::string elemFieldName = "elem_field"; + + cleanup_single_file(outFile); + run_single_file_simple_topology_change(elemFieldName, outFile); + cleanup_single_file(outFile); +} + +TEST(TestDynamicWrite, single_file_groups_not_enabled) +{ + Ioss::Init::Initializer io; + Ioss::ParallelUtils util(Ioss::ParallelUtils::comm_world()); + + int numBlocks = util.parallel_size(); + if(numBlocks > 1) GTEST_SKIP(); + + std::string meshDesc = "0,1,HEX_8,1,2,3,4,5,6,7,8,block_1" + "|coordinates:0,0,0,1,0,0,1,1,0,0,1,0,0,0,1,1,0,1,1,1,1,0,1,1"; + + Ioss::PropertyManager propertyManager; + + Ioss::DatabaseIO *i_database = Ioss::IOFactory::create("textmesh", meshDesc, Ioss::READ_MODEL, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region i_region(i_database, "input_model"); + EXPECT_TRUE(i_database != nullptr); + EXPECT_TRUE(i_database->ok(true)); + + std::string outFile("singleFileGroupsNotEnabled.g"); + std::string elemFieldName = "elem_field"; + cleanup_single_file(outFile); + + // Need the line below to allow this to pass + // propertyManager.add(Ioss::Property("ENABLE_FILE_GROUPS", 1)); + propertyManager.add(Ioss::Property("APPEND_OUTPUT", Ioss::DB_APPEND_GROUP)); + Ioss::DatabaseIO *o_database = Ioss::IOFactory::create("exodus", outFile, Ioss::WRITE_RESULTS, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region o_region(o_database, "output_model"); + EXPECT_TRUE(o_database != nullptr); + EXPECT_TRUE(o_database->ok(true)); + + auto fileControlOption = Ioss::FileControlOption::CONTROL_AUTO_SINGLE_FILE; + auto observer = std::make_shared(i_region, elemFieldName, fileControlOption); + EXPECT_THROW(o_region.register_mesh_modification_observer(observer), std::runtime_error); + cleanup_single_file(outFile); +} + +TEST(TestDynamicWrite, create_subgroup_with_file_reopen) +{ + std::string outFile("subgroupManyBlocks.g"); + std::string elemFieldName = "elem_field"; + + Ioss::Init::Initializer io; + Ioss::ParallelUtils util(Ioss::ParallelUtils::comm_world()); + + std::string file1 = Ioss::Utils::decode_filename(outFile, util.parallel_rank(), util.parallel_size()); + unlink(file1.c_str()); + + int numBlocks = util.parallel_size(); + if(numBlocks > 1) GTEST_SKIP(); + + std::string meshDesc = "0,1,HEX_8,1,2,3,4,5,6,7,8,block_1" + "|coordinates:0,0,0,1,0,0,1,1,0,0,1,0,0,0,1,1,0,1,1,1,1,0,1,1"; + + Ioss::PropertyManager propertyManager; + + Ioss::DatabaseIO *i_database = Ioss::IOFactory::create("textmesh", meshDesc, Ioss::READ_MODEL, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region i_region(i_database, "input_model"); + EXPECT_TRUE(i_database != nullptr); + EXPECT_TRUE(i_database->ok(true)); + + { + propertyManager.add(Ioss::Property("ENABLE_FILE_GROUPS", 1)); + Ioss::DatabaseIO *o_database = Ioss::IOFactory::create("exodus", outFile, Ioss::WRITE_RESULTS, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region o_region(o_database, "output_model"); + EXPECT_TRUE(o_database != nullptr); + EXPECT_TRUE(o_database->ok(true)); + o_database->create_subgroup("GROUP_1"); + } + + { + propertyManager.add(Ioss::Property("APPEND_OUTPUT", Ioss::DB_APPEND_GROUP)); + Ioss::DatabaseIO *o_database = Ioss::IOFactory::create("exodus", outFile, Ioss::WRITE_RESULTS, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region o_region(o_database, "output_model"); + EXPECT_TRUE(o_database != nullptr); + EXPECT_TRUE(o_database->ok(true)); + + // Group pointer is still at root level + o_database->create_subgroup("GROUP_2"); + + Ioss::NameList names = o_database->groups_describe(false); + Ioss::NameList full_names = o_database->groups_describe(true); + + std::vector gold_names{"/", "GROUP_1", "GROUP_2"}; + std::vector gold_full_names{"/", "/GROUP_1", "/GROUP_2"}; + + EXPECT_EQ(gold_names, names); + EXPECT_EQ(gold_full_names, full_names); + } + + unlink(file1.c_str()); +} + +TEST(TestDynamicWrite, create_subgroup_with_file_persistence_and_child_group) +{ + std::string outFile("subgroupManyBlocks.g"); + std::string elemFieldName = "elem_field"; + + Ioss::Init::Initializer io; + Ioss::ParallelUtils util(Ioss::ParallelUtils::comm_world()); + + std::string file1 = Ioss::Utils::decode_filename(outFile, util.parallel_rank(), util.parallel_size()); + unlink(file1.c_str()); + + int numBlocks = util.parallel_size(); + if(numBlocks > 1) GTEST_SKIP(); + + std::string meshDesc = "0,1,HEX_8,1,2,3,4,5,6,7,8,block_1" + "|coordinates:0,0,0,1,0,0,1,1,0,0,1,0,0,0,1,1,0,1,1,1,1,0,1,1"; + + Ioss::PropertyManager propertyManager; + + Ioss::DatabaseIO *i_database = Ioss::IOFactory::create("textmesh", meshDesc, Ioss::READ_MODEL, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region i_region(i_database, "input_model"); + EXPECT_TRUE(i_database != nullptr); + EXPECT_TRUE(i_database->ok(true)); + + { + propertyManager.add(Ioss::Property("ENABLE_FILE_GROUPS", 1)); + propertyManager.add(Ioss::Property("APPEND_OUTPUT", Ioss::DB_APPEND_GROUP)); + Ioss::DatabaseIO *o_database = Ioss::IOFactory::create("exodus", outFile, Ioss::WRITE_RESULTS, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region o_region(o_database, "output_model"); + EXPECT_TRUE(o_database != nullptr); + EXPECT_TRUE(o_database->ok(true)); + + o_database->create_subgroup("GROUP_1"); + + // Group pointer is at "GROUP_1" ... "GROUP_2" is a child + o_database->create_subgroup("GROUP_2"); + + Ioss::NameList names = o_database->groups_describe(false); + Ioss::NameList full_names = o_database->groups_describe(true); + + std::vector gold_names{"/", "GROUP_1", "GROUP_2"}; + std::vector gold_full_names{"/", "/GROUP_1", "/GROUP_1/GROUP_2"}; + + EXPECT_EQ(gold_names, names); + EXPECT_EQ(gold_full_names, full_names); + } + + unlink(file1.c_str()); +} + +TEST(TestDynamicWrite, create_subgroup_with_file_persistence_and_no_child_group) +{ + std::string outFile("subgroupManyBlocks.g"); + std::string elemFieldName = "elem_field"; + + Ioss::Init::Initializer io; + Ioss::ParallelUtils util(Ioss::ParallelUtils::comm_world()); + + std::string file1 = Ioss::Utils::decode_filename(outFile, util.parallel_rank(), util.parallel_size()); + unlink(file1.c_str()); + + int numBlocks = util.parallel_size(); + if(numBlocks > 1) GTEST_SKIP(); + + std::string meshDesc = "0,1,HEX_8,1,2,3,4,5,6,7,8,block_1" + "|coordinates:0,0,0,1,0,0,1,1,0,0,1,0,0,0,1,1,0,1,1,1,1,0,1,1"; + + Ioss::PropertyManager propertyManager; + + Ioss::DatabaseIO *i_database = Ioss::IOFactory::create("textmesh", meshDesc, Ioss::READ_MODEL, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region i_region(i_database, "input_model"); + EXPECT_TRUE(i_database != nullptr); + EXPECT_TRUE(i_database->ok(true)); + + { + propertyManager.add(Ioss::Property("ENABLE_FILE_GROUPS", 1)); + propertyManager.add(Ioss::Property("APPEND_OUTPUT", Ioss::DB_APPEND_GROUP)); + Ioss::DatabaseIO *o_database = Ioss::IOFactory::create("exodus", outFile, Ioss::WRITE_RESULTS, + Ioss::ParallelUtils::comm_world(), + propertyManager); + Ioss::Region o_region(o_database, "output_model"); + EXPECT_TRUE(o_database != nullptr); + EXPECT_TRUE(o_database->ok(true)); + + o_database->create_subgroup("GROUP_1"); + + // Group pointer is reset to root group + EXPECT_TRUE(o_database->open_root_group()); + o_database->create_subgroup("GROUP_2"); + + Ioss::NameList names = o_database->groups_describe(false); + Ioss::NameList full_names = o_database->groups_describe(true); + + std::vector gold_names{"/", "GROUP_1", "GROUP_2"}; + std::vector gold_full_names{"/", "/GROUP_1", "/GROUP_2"}; + + EXPECT_EQ(gold_names, names); + EXPECT_EQ(gold_full_names, full_names); + } + + unlink(file1.c_str()); +} + +void test_single_file_simple_topology_change_data(Ioss::Region& i_region, const std::string& elemFieldName, + int gold_step, double gold_time) +{ + i_region.begin_state(gold_step); + for (Ioss::ElementBlock *i_eb : i_region.get_element_blocks()) { + size_t num_elem = i_eb->get_property("entity_count").get_int(); + + std::vector field_data(num_elem); + std::vector elem_ids; + + i_eb->get_field_data(elemFieldName, field_data); + i_eb->get_field_data("ids", elem_ids); + + for (size_t i = 0; i < elem_ids.size(); i++) { + double gold_value = (double)elem_ids[i] + 100*gold_time; + EXPECT_NEAR(gold_value, field_data[i], 1.0e-6); + } + } +} + +void read_and_test_single_file_simple_topology_change(const std::string& elemFieldName, const std::string& outFile) +{ + Ioss::PropertyManager propertyManager; + + Ioss::DatabaseIO *i_database = Ioss::IOFactory::create("exodus", outFile, Ioss::READ_RESTART, + Ioss::ParallelUtils::comm_world(), + propertyManager); + + Ioss::NameList names = i_database->groups_describe(false); + Ioss::NameList full_names = i_database->groups_describe(true); + + std::vector gold_names{"/", "STEP-1", "STEP-2"}; + std::vector gold_full_names{"/", "/STEP-1", "/STEP-2"}; + + EXPECT_EQ(gold_names, names); + EXPECT_EQ(gold_full_names, full_names); + + EXPECT_TRUE(i_database->open_group("STEP-1")); + + Ioss::Region i_region(i_database, "input_model"); + EXPECT_TRUE(i_database != nullptr); + EXPECT_TRUE(i_database->ok(true)); + + double gold_time = 0.0; + int gold_step = 1; + auto min_result1 = i_region.get_min_time(); + EXPECT_EQ(gold_step, min_result1.first); + EXPECT_NEAR(gold_time, min_result1.second, 1.0e-6); + + auto max_result1 = i_region.get_max_time(); + EXPECT_EQ(gold_step, max_result1.first); + EXPECT_NEAR(gold_time, max_result1.second, 1.0e-6); + test_single_file_simple_topology_change_data(i_region, elemFieldName, gold_step, gold_time); + + EXPECT_TRUE(i_region.load_group_mesh("STEP-2")); + + double gold_min_time = 1.0; + int gold_min_step = 1; + auto min_result2 = i_region.get_min_time(); + EXPECT_EQ(gold_min_step, min_result2.first); + EXPECT_NEAR(gold_min_time, min_result2.second, 1.0e-6); + test_single_file_simple_topology_change_data(i_region, elemFieldName, gold_min_step, gold_min_time); + + auto max_result2 = i_region.get_max_time(); + double gold_max_time = 2.0; + int gold_max_step = 2; + EXPECT_EQ(gold_max_step, max_result2.first); + EXPECT_NEAR(gold_max_time, max_result2.second, 1.0e-6); + test_single_file_simple_topology_change_data(i_region, elemFieldName, gold_max_step, gold_max_time); +} + +TEST(TestDynamicRead, single_file_simple_topology_modification) +{ + std::string outFile("singleFileManyBlocks.g"); + std::string elemFieldName = "elem_field"; + + cleanup_single_file(outFile); + run_single_file_simple_topology_change(elemFieldName, outFile); + read_and_test_single_file_simple_topology_change(elemFieldName, outFile); + cleanup_single_file(outFile); +} + +} + +