/* * Copyright(C) 1999-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 "EP_SystemInterface.h" #include "EP_Version.h" // for qainfo #include "FileInfo.h" #include "GetLongOpt.h" // for GetLongOption, etc #include "SL_tokenize.h" // for tokenize #include // for sort, transform #include // for tolower #include #include // for size_t #include // for strtol, abs, exit, strtoul, etc #include // for strchr, strlen #include #include #include #include #include // for string, char_traits, etc #include #include #include // for pair, make_pair #include // for vector #if defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) || \ defined(__MINGW32__) || defined(_WIN64) || defined(__MINGW64__) #define __SUP_WINDOWS__ 1 #include #endif #if !defined(__SUP_WINDOWS__) #include #endif namespace { bool str_equal(const std::string &s1, const std::string &s2) { return (s1.size() == s2.size()) && std::equal(s1.begin(), s1.end(), s2.begin(), [](char a, char b) { return std::tolower(a) == std::tolower(b); }); } void parse_variable_names(const char *tokens, Excn::StringIdVector *variable_list); std::string find_matching_file(const std::string &path, const std::string &basename); } // namespace Excn::SystemInterface::SystemInterface(int rank) : myRank_(rank) { enroll_options(); } Excn::SystemInterface::~SystemInterface() = default; void Excn::SystemInterface::enroll_options() { options_.usage("[options] basename"); options_.enroll("help", GetLongOption::NoValue, "Print this summary and exit", nullptr); options_.enroll("version", GetLongOption::NoValue, "Print version and exit", nullptr, nullptr, true); options_.enroll("auto", GetLongOption::NoValue, "Automatically set Root, Proc, Ext from filename 'Root/basename.ext.#p.00'.", nullptr); options_.enroll("extension", GetLongOption::MandatoryValue, "Exodus database extension for the input files", "e"); options_.enroll("output_extension", GetLongOption::MandatoryValue, "Exodus database extension for the output file", nullptr); options_.enroll("processor_count", GetLongOption::MandatoryValue, "Number of processors", "1"); options_.enroll("current_directory", GetLongOption::MandatoryValue, "Current Directory", "."); options_.enroll("Root_directory", GetLongOption::MandatoryValue, "Root directory", nullptr); options_.enroll("Subdirectory", GetLongOption::MandatoryValue, "subdirectory containing input exodusII files", nullptr, nullptr, true); options_.enroll("subcycle", GetLongOption::OptionalValue, "Subcycle. Create $val subparts if $val is specified.\n" "\t\tOtherwise, create multiple parts each of size 'Part_count'.\n" "\t\tThe subparts can then be joined by a subsequent run of epu.\n" "\t\tUseful if the maximum number of open files is less\n" "\t\tthan the processor count.", nullptr, "0"); options_.enroll("join_subcycles", GetLongOption::NoValue, "If -subcycle is specified, then after the subcycle files are processed,\n" "\t\trun epu one more time and join the subcycle files into a single file.", nullptr); options_.enroll("Part_count", GetLongOption::MandatoryValue, "How many pieces (files) of the model should be joined.", "0"); options_.enroll("start_part", GetLongOption::MandatoryValue, "Start with piece {L} (file)", "0"); options_.enroll("cycle", GetLongOption::MandatoryValue, "Cycle number. If subcycle # is specified, then only execute\n" "\t\tcycle $val ($val < #). The cycle number is 0-based.", "-1"); options_.enroll("keep_temporary", GetLongOption::NoValue, "If -join_subcycles is specified, then after joining the subcycle files,\n" "\t\tthey are automatically deleted unless -keep_temporary is specified.", nullptr); options_.enroll( "verify_valid_file", GetLongOption::NoValue, "Reopen the output file right after closing it to verify that the file is valid.\n" "\t\tThis tries to detect file corruption immediately instead of later. Mainly useful in " "large subcycle runs.", nullptr, nullptr, true); options_.enroll("map", GetLongOption::NoValue, "Map element ids to original order if possible [default]", nullptr); options_.enroll("nomap", GetLongOption::NoValue, "Do not map element ids to original order", nullptr, nullptr, true); options_.enroll("netcdf4", GetLongOption::NoValue, "Output database uses HDF5-based netcdf which allows for up to 2.1 billion " "nodes/elements", nullptr); options_.enroll("netcdf5", GetLongOption::NoValue, "Output database uses PnetCDF netcdf 5 format which allows for up to 2.1 billion " "nodes/elements", nullptr); options_.enroll("64", GetLongOption::NoValue, "The output database will be written in the 64-bit integer mode which allows\n" "\t\tfor more than 2.1 billion nodes/elements", nullptr, nullptr, true); options_.enroll( "zlib", GetLongOption::NoValue, "Use the Zlib / libz compression method if compression is enabled (default) [exodus only].", nullptr); options_.enroll("szip", GetLongOption::NoValue, "Use SZip compression. [exodus only, enables netcdf-4]", nullptr); options_.enroll("compress_data", GetLongOption::MandatoryValue, "The output database will be written using compression (netcdf-4 mode only).\n" "\t\tValue ranges from 0..9 for zlib/gzip or even values 4..32 for szip.", nullptr, nullptr, true); options_.enroll("append", GetLongOption::NoValue, "Append to database instead of opening a new database.\n" "\t\tTimestep transfer will start after last timestep on database", nullptr); options_.enroll("steps", GetLongOption::MandatoryValue, "Specify subset of timesteps to transfer to output file.\n" "\t\tFormat is beg:end:step. 1:10:2 --> 1,3,5,7,9\n" "\t\tEnter LAST for last step", "1:", nullptr, true); options_.enroll( "add_nodal_communication_map", GetLongOption::NoValue, "In subcycle mode, add the `nodal communication map` data to the output files.\n" "\t\tThe resulting files can then be used as input to a subsequent analysis (N to M)", nullptr); options_.enroll("add_processor_id", GetLongOption::NoValue, "Add element variable named 'processor_id' to the output file which shows the\n" "\t\tprocessor that an element was on in the decomposed mesh.\n" "\t\tCan be used by SLICE or auto-decomp to reproduce decomposition.", nullptr); options_.enroll("add_map_processor_id", GetLongOption::NoValue, "Add element map named 'processor_id' to the output file which shows the\n" "\t\tprocessor that an element was on in the decomposed mesh.\n" "\t\tCan be used by SLICE or auto-decomp to reproduce decomposition.", nullptr, nullptr, true); options_.enroll("gvar", GetLongOption::MandatoryValue, "Comma-separated list of global variables to be joined or ALL or NONE.", nullptr); options_.enroll("evar", GetLongOption::MandatoryValue, "Comma-separated list of element variables to be joined or ALL or NONE.\n" "\t\tVariables can be limited to certain blocks by appending a\n" "\t\tcolon followed by the block id. E.g. -evar sigxx:10:20", nullptr); options_.enroll("nvar", GetLongOption::MandatoryValue, "Comma-separated list of nodal variables to be joined or ALL or NONE.", nullptr); options_.enroll("nsetvar", GetLongOption::MandatoryValue, "Comma-separated list of nodeset variables to be joined or ALL or NONE.", nullptr); options_.enroll("ssetvar", GetLongOption::MandatoryValue, "Comma-separated list of sideset variables to be joined or ALL or NONE.", nullptr); options_.enroll("edblkvar", GetLongOption::MandatoryValue, "Comma-separated list of edgeblock variables to be joined or ALL or NONE.", nullptr); options_.enroll("fablkvar", GetLongOption::MandatoryValue, "Comma-separated list of faceblock variables to be joined or ALL or NONE.", nullptr, nullptr, true); options_.enroll("omit_nodesets", GetLongOption::NoValue, "Don't transfer nodesets to output file.", nullptr); options_.enroll("omit_sidesets", GetLongOption::NoValue, "Don't transfer sidesets to output file.", nullptr); options_.enroll("omit_edgeblocks", GetLongOption::NoValue, "Don't transfer edgeblocks to output file.", nullptr); options_.enroll("omit_faceblocks", GetLongOption::NoValue, "Don't transfer faceblocks to output file.", nullptr, nullptr, true); options_.enroll("debug", GetLongOption::MandatoryValue, "debug level (values are or'd)\n" "\t\t 1 = timing information.\n" "\t\t 2 = Check consistent nodal field values between processors.\n" "\t\t 4 = Verbose Element block information.\n" "\t\t 8 = Check consistent nodal coordinates between processors.\n" "\t\t 16 = Verbose Sideset information.\n" "\t\t 32 = Verbose Nodeset information.\n" "\t\t 64 = Verbose Edge block information.\n" "\t\t128 = Verbose Face block information.\n" "\t\t256 = put exodus library into verbose mode.\n" "\t\t512 = Check consistent global field values between processors.", "0"); options_.enroll("sum_shared_nodes", GetLongOption::NoValue, "[Rare, special case] The nodal results data on all shared nodes (nodes on " "processor boundaries)\n" "\t\twill be the sum of the individual nodal results data on each shared node.\n" "\t\tThe default behavior assumes that the values are equal.", nullptr); options_.enroll( "output_shared_nodes", GetLongOption::NoValue, "[Debugging] Output list of shared nodes and the processors they are shared with.", nullptr); options_.enroll("width", GetLongOption::MandatoryValue, "Width of output screen, default = 80", nullptr); options_.enroll("max_open_files", GetLongOption::MandatoryValue, "For testing auto subcycle only. Sets file limit that triggers auto subcycling.", "0"); options_.enroll("large_model", GetLongOption::NoValue, "[deprecated; use netcdf4 instead]", nullptr); options_.enroll("copyright", GetLongOption::NoValue, "Show copyright and license data.", nullptr); } bool Excn::SystemInterface::parse_options(int argc, char **argv) { // Get options from environment variable also... char *options = getenv("EPU_OPTIONS"); if (options != nullptr) { if (myRank_ == 0) { fmt::print( "\nThe following options were specified via the EPU_OPTIONS environment variable:\n" "\t{}\n\n", options); } options_.parse(options, options_.basename(*argv)); } int option_index = options_.parse(argc, argv); if (option_index < 1) { throw std::runtime_error("ERROR: (EPU) No arguments specified."); } if (options_.retrieve("help") != nullptr) { if (myRank_ == 0) { options_.usage(); fmt::print("\n\tCan also set options via EPU_OPTIONS environment variable.\n\n" "\tWrites: current_directory/basename.output_suf\n" "\tReads: root/sub/basename.suf.#p.0 to\n" "\t\troot/sub/basename.suf.#p.#p-1\n" "\n\t->->-> Send email to gdsjaar@sandia.gov for epu support.<-<-<-\n"); } return false; } if (options_.retrieve("version") != nullptr) { // Version is printed up front, just exit... return false; } inExtension_ = options_.get_option_value("extension", inExtension_); outExtension_ = options_.get_option_value("output_extension", outExtension_); processorCount_ = options_.get_option_value("processor_count", processorCount_); partCount_ = options_.get_option_value("Part_count", partCount_); startPart_ = options_.get_option_value("start_part", startPart_); maxOpenFiles_ = options_.get_option_value("max_open_files", maxOpenFiles_); cwd_ = options_.get_option_value("current_directory", cwd_); rootDirectory_ = options_.get_option_value("Root_directory", rootDirectory_); subDirectory_ = options_.get_option_value("Subdirectory", subDirectory_); debugLevel_ = options_.get_option_value("debug", debugLevel_); screenWidth_ = options_.get_option_value("width", term_width()); { const char *temp = options_.retrieve("steps"); if (temp != nullptr) { parse_step_option(temp); } } { const char *temp = options_.retrieve("gvar"); parse_variable_names(temp, &globalVarNames_); } { const char *temp = options_.retrieve("nvar"); parse_variable_names(temp, &nodeVarNames_); } { const char *temp = options_.retrieve("evar"); parse_variable_names(temp, &elemVarNames_); } { const char *temp = options_.retrieve("nsetvar"); parse_variable_names(temp, &nsetVarNames_); } { const char *temp = options_.retrieve("ssetvar"); parse_variable_names(temp, &ssetVarNames_); } { const char *temp = options_.retrieve("edblkvar"); parse_variable_names(temp, &edblkVarNames_); } { const char *temp = options_.retrieve("fablkvar"); parse_variable_names(temp, &fablkVarNames_); } addProcessorIdField_ = options_.retrieve("add_processor_id") != nullptr; addProcessorIdMap_ = options_.retrieve("add_map_processor_id") != nullptr; addNodalCommunicationMap_ = options_.retrieve("add_nodal_communication_map") != nullptr; if (options_.retrieve("large_model") != nullptr) { useNetcdf4_ = true; fmt::print(stderr, "\nWARNING: the -large_model option is deprecated; please use -netcdf4 instead.\n"); } useNetcdf4_ = options_.retrieve("netcdf4") != nullptr; useNetcdf5_ = options_.retrieve("netcdf5") != nullptr; append_ = options_.retrieve("append") != nullptr; intIs64Bit_ = options_.retrieve("64") != nullptr; if (options_.retrieve("szip") != nullptr) { szip_ = true; zlib_ = false; } zlib_ = (options_.retrieve("zlib") != nullptr); if (szip_ && zlib_) { fmt::print(stderr, "ERROR: Only one of 'szip' or 'zlib' can be specified.\n"); } compressData_ = options_.get_option_value("compress_data", compressData_); sumSharedNodes_ = options_.retrieve("sum_shared_nodes") != nullptr; append_ = options_.retrieve("append") != nullptr; subcycle_ = options_.get_option_value("subcycle", subcycle_); cycle_ = options_.get_option_value("cycle", cycle_); subcycleJoin_ = options_.retrieve("join_subcycles") != nullptr; keepTemporary_ = options_.retrieve("keep_temporary") != nullptr; verifyValidFile_ = options_.retrieve("verify_valid_file") != nullptr; if (options_.retrieve("map") != nullptr) { mapIds_ = true; } if (options_.retrieve("nomap") != nullptr) { mapIds_ = false; } omitNodesets_ = options_.retrieve("omit_nodesets") != nullptr; omitSidesets_ = options_.retrieve("omit_sidesets") != nullptr; omitEdgeBlocks_ = options_.retrieve("omit_edgeblocks") != nullptr; omitFaceBlocks_ = options_.retrieve("omit_faceblocks") != nullptr; outputSharedNodes_ = options_.retrieve("output_shared_nodes") != nullptr; if (options_.retrieve("copyright") != nullptr) { if (myRank_ == 0) { fmt::print("{}", copyright("2010-2022")); } return false; } // Parse remaining options as directory paths. if (option_index < argc) { basename_ = argv[option_index]; if (options_.retrieve("auto") != nullptr) { // Determine Root, Proc, Extension, and Basename automatically // by parsing the basename_ entered by the user. Assumed to be // in the form: "/directory/sub/basename.ext.#proc.34" FileInfo file(basename_); auto path = file.pathname(); if (path.empty()) { path = "."; } #if defined(__SUP_WINDOWS__) rootDirectory_ = _fullpath(nullptr, path.c_str(), _MAX_PATH); #else char *tmp = ::realpath(path.c_str(), nullptr); if (tmp != nullptr) { rootDirectory_ = std::string(tmp); free(tmp); } #endif basename_ = file.tailname(); if (basename_.empty()) { std::ostringstream errmsg; fmt::print( errmsg, "\nERROR: (EPU) If the '-auto' option is specified, the basename must specify an " "existing filename or portion of a base of a filename (no rank/proc count).\n" " The entered basename ('{}') does not contain a filename.\n", basename_); throw std::runtime_error(errmsg.str()); } bool success = decompose_filename(basename_); if (!success) { // See if we can find files that match the basename and take the first match as the "new" // basename... std::string candidate = find_matching_file(rootDirectory_, basename_); if (!candidate.empty()) { basename_ = candidate; success = decompose_filename(basename_); if (!success) { std::ostringstream errmsg; fmt::print( errmsg, "\nERROR: (EPU) If the '-auto' option is specified, the basename must specify an " "existing filename or a basename (no rank/proc count).\n" " The entered basename ('{}') does not contain an extension or processor " "count.\n", basename_); throw std::runtime_error(errmsg.str()); } } } auto_ = true; if (myRank_ == 0) { fmt::print("\nThe following options were determined automatically:\n" "\t basename = '{}'\n" "\t-processor_count {}\n" "\t-extension {}\n" "\t-Root_directory {}\n\n", basename_, processorCount_, inExtension_, rootDirectory_); } } } else { throw std::runtime_error("\nERROR: (EPU) basename not specified\n"); } // Check that subcycle count does not match processor count -- // in that case the existing files will be overwritten. if (processorCount_ <= subcycle_) { if (myRank_ == 0) { fmt::print(stderr, "\nERROR: (EPU) Invalid subcycle count specified: '{}'." "\n Must be less than processor count '{}'.\n\n", subcycle_, processorCount_); } return false; } // If subcycle is specified, but not part_count, then calculate partCount_ if (partCount_ <= 0 && subcycle_ > 0) { partCount_ = processorCount_ / subcycle_; } return true; } void Excn::SystemInterface::dump(std::ostream & /*unused*/) const {} std::string Excn::SystemInterface::output_suffix() const { if (outExtension_ == "") { return inExtension_; } return outExtension_; } void Excn::SystemInterface::show_version(int rank) { if (rank == 0) { fmt::print("{}\n" "\t(Out of Many One -- see http://www.greatseal.com/mottoes/unum.html)\n" "\tExodusII Parallel Unification Program\n" "\t(Version: {}) Modified: {}\n", qainfo[0], qainfo[1], qainfo[2]); } } void Excn::SystemInterface::parse_step_option(const char *tokens) { //: The defined formats for the count attribute are:
//:
    //:
  • -- default -- 1 <= count <= oo (all steps)
  • //:
  • "X" -- X <= count <= X (just step X) LAST for last step.
  • //:
  • "X:Y" -- X to Y by 1
  • //:
  • "X:" -- X to oo by 1
  • //:
  • ":Y" -- 1 to Y by 1
  • //:
  • "::Z" -- 1 to oo by Z
  • //:
//: The count and step must always be >= 0 // Break into tokens separated by ":" // Default is given in constructor above... if (tokens != nullptr) { if (strchr(tokens, ':') != nullptr) { // The string contains a separator int vals[3]; vals[0] = stepMin_; vals[1] = stepMax_; vals[2] = stepInterval_; int j = 0; for (auto &val : vals) { // Parse 'i'th field char tmp_str[128]; ; int k = 0; while (tokens[j] != '\0' && tokens[j] != ':') { tmp_str[k++] = tokens[j++]; } tmp_str[k] = '\0'; if (strlen(tmp_str) > 0) { val = strtoul(tmp_str, nullptr, 0); } if (tokens[j++] == '\0') { break; // Reached end of string } } stepMin_ = abs(vals[0]); stepMax_ = abs(vals[1]); stepInterval_ = abs(vals[2]); } else if (str_equal("LAST", tokens)) { stepMin_ = stepMax_ = -1; } else { // Does not contain a separator, min == max stepMin_ = stepMax_ = strtol(tokens, nullptr, 0); } } } bool Excn::SystemInterface::decompose_filename(const std::string &cs) { std::string s(cs); // Decompose the input string 's' into processor count, extension, basename, and root. // Input string should be of the form: // "root/basename.ext.proc.nn" // 'root/' is optional and is all characters preceding the last (if any) '/'; // 'basename' can contain multiple '.' // NOTE: This used to use the tokenize function, but that didn't work since we need // to handle leading and embedded '..' which tokenize threw away... // Get rid of the 'nn' which is not used at this time... size_t ind = s.find_last_of('.', std::string::npos); // last '.' if (ind == std::string::npos) { return false; } s.erase(ind); // Now find the processor count... ind = s.find_last_of('.', std::string::npos); if (ind == std::string::npos) { return false; } std::string tmp = s.substr(ind + 1); // Skip the '.' processorCount_ = std::stoi(tmp); if (processorCount_ <= 0) { fmt::print( stderr, "\nERROR: (EPU) Invalid processor count specified: '{}'. Must be greater than zero.\n", processorCount_); return false; } s.erase(ind); // Should now be an extension... ind = s.find_last_of('.', std::string::npos); if (ind == std::string::npos) { inExtension_ = ""; } else { inExtension_ = s.substr(ind + 1); s.erase(ind); } // The directory path was stripped prior to entering this function, // so remainder of 's' is just the new basename_ basename_ = s; return true; } namespace { std::string LowerCase(const std::string &name) { std::string s = name; std::transform(s.begin(), s.end(), // source s.begin(), // destination ::tolower); // operation return s; } using StringVector = std::vector; bool string_id_sort(const std::pair &t1, const std::pair &t2) { return t1.first < t2.first || (!(t2.first < t1.first) && t1.second < t2.second); } void parse_variable_names(const char *tokens, Excn::StringIdVector *variable_list) { // Break into tokens separated by "," // Value of num_vars includes optional add_processor_id if (tokens != nullptr) { std::string token_string(tokens); StringVector var_list = SLIB::tokenize(token_string, ","); // At this point, var_list is either a single string, or a string // separated from 1 or more block ids with ":" delimiter. // For example, sigxx:1:10:100 would indicate that the variable // "sigxx" should be written only for blocks with id 1, 10, and // 100. "sigxx" would indicate that the variable should be // written for all blocks. auto I = var_list.begin(); while (I != var_list.end()) { StringVector name_id = SLIB::tokenize(*I, ":"); std::string var_name = LowerCase(name_id[0]); if (name_id.size() == 1) { (*variable_list).push_back(std::make_pair(var_name, 0)); } else { for (size_t i = 1; i < name_id.size(); i++) { // Convert string to integer... int id = std::stoi(name_id[i]); (*variable_list).push_back(std::make_pair(var_name, id)); } } ++I; } // Sort the list... std::sort(variable_list->begin(), variable_list->end(), string_id_sort); } } std::string find_matching_file(const std::string &path, const std::string &basename) { glob::glob g(basename + ".*.*"); #if !defined(__SUP_WINDOWS__) struct dirent *entry = nullptr; DIR *dp = nullptr; dp = opendir(path.c_str()); if (dp != nullptr) { while ((entry = readdir(dp))) { std::string filename = entry->d_name; if (glob::glob_match(filename, g)) { closedir(dp); return filename; } } } closedir(dp); #endif return ""; } } // namespace