diff --git a/docs/guide/code/case.rst b/docs/guide/code/case.rst index bb4e102532..223d9d0c23 100644 --- a/docs/guide/code/case.rst +++ b/docs/guide/code/case.rst @@ -10,6 +10,8 @@ The goal is to have PSC cases to be self-contained, ie., everything related to p * One cannot just change a parameter in the case and run it -- the code needs to be recompiled first. +Alternatively, use an InputParams object to load parameters from an input file at runtime. See ``src/include/input_params.hxx``. + Changing / adding a case ======================== diff --git a/src/include/input_params.hxx b/src/include/input_params.hxx new file mode 100644 index 0000000000..a804996203 --- /dev/null +++ b/src/include/input_params.hxx @@ -0,0 +1,190 @@ +#pragma once + +#include +#include +#include +#include +#include + +// TODO in c++20, use std::format +// TODO use LOG_WARNING and LOG_ERROR, but ideally only on 1 proc + +/// @brief A parser that reads a dict-like map of parameter names to +/// parameter values from a single input file. The input file syntax is +/// extremely simple: +/// ```txt +/// param1 val1 +/// param2 val2 thiswordisignored andsoisthis +/// ``` +/// The first (space-separated) word in each line is interpreted as a parameter +/// name, and the second word is interpreted as a value. Words after the first +/// two are silently ignored, allowing for comments. If a parameter is +/// duplicated, all values but the last are silently ignored. +/// +/// This class has no knowledge about what parameters should or shouldn't be +/// present, nor does it know what types its values should have. A value isn't +/// parsed to a specific type (e.g. `double`) until it is actually accessed as +/// that type by a user. +class InputParams +{ +private: + std::unordered_map params; + +public: + InputParams(const std::string file_path) + { + // iterate over each line + std::ifstream ifs(file_path); + + if (ifs.is_open()) { + for (std::string line; std::getline(ifs, line);) { + + // parse first two words within line + std::istringstream iss(line); + std::string paramName, paramVal; + if (iss >> paramName >> paramVal) + params[paramName] = paramVal; + } + + ifs.close(); + } else { + std::cout << "Failed to open params file: " << file_path << "\n"; + exit(EXIT_FAILURE); + } + } + + /// @brief Check if the parameter is present. + /// @param paramName name of parameter + /// @return whether or not the parameter is present + bool has(const std::string paramName) { return params.count(paramName) == 1; } + + /// @brief Get a parameter, parsing it to the given type. + /// @tparam T type of parameter + /// @param paramName name of parameter + /// @return the parameter + template + T get(const std::string paramName) + { + try { + return _getParsed(paramName); + } catch (const std::invalid_argument& e) { + std::string unparsed = _getUnparsed(paramName); + // TODO ensure human-readable type name + std::cerr << "ERROR Unable to parse parameter '" << paramName + << "', which has value '" << unparsed << "', to type " + << typeid(T).name() << "\n"; + abort(); + } + } + + /// @brief Get a parameter if it's there, otherwise + /// return the given default value. + /// @tparam T type of parameter + /// @param paramName name of parameter + /// @param deflt default value of parameter + /// @return the parameter + template + T getOrDefault(const std::string paramName, T deflt) + { + if (has(paramName)) { + return get(paramName); + } + + std::cout << "Warning: using default value for parameter '" << paramName + << "': " << deflt << "\n"; + return deflt; + } + + /// @brief Get a parameter and display a warning if it's there, otherwise + /// return the given default value. + /// @tparam T type of parameter + /// @param paramName name of parameter + /// @param deflt default value of parameter + /// @return the parameter + template + T getAndWarnOrDefault(const std::string paramName, T deflt) + { + if (!has(paramName)) { + return deflt; + } + + T val = get(paramName); + std::cout << "Warning: using non-default value for parameter '" << paramName + << "': " << val << "\nDefault value of '" << deflt + << "' is recommended.\n"; + return val; + } + + /// @brief Display a warning if a parameter is present. + /// @param paramName name of parameter + /// @param advice user-friendly instructions on what to do instead + /// @return whether or not the parameter was present + bool warnIfPresent(const std::string paramName, const std::string advice) + { + if (!has(paramName)) { + return false; + } + + std::cout << "Warning: parameter " << paramName << " is deprecated.\n" + << advice << "\n"; + return true; + } + +private: + /// Retrieves an unparsed value, throwing a helpful error if the parameter + /// is missing. + std::string _getUnparsed(const std::string paramName) + { + if (has(paramName)) { + return params.at(paramName); + } + + throw std::out_of_range("missing required input parameter: " + paramName); + } + + /// Retrieve and parse a value, possibly throwing std::invalid_argument + template + T _getParsed(const std::string paramName); +}; + +// get implementations + +template <> +bool InputParams::_getParsed(const std::string paramName) +{ + auto lowercase = _getUnparsed(paramName); + std::transform(lowercase.begin(), lowercase.end(), lowercase.begin(), + [](unsigned char c) { return std::tolower(c); }); + + if (lowercase == "true") { + return true; + } else if (lowercase == "false") { + return false; + } else { + throw std::invalid_argument(_getUnparsed(paramName)); + } +} + +template <> +double InputParams::_getParsed(const std::string paramName) +{ + return std::stod(_getUnparsed(paramName)); +} + +template <> +int InputParams::_getParsed(const std::string paramName) +{ + return std::stoi(_getUnparsed(paramName)); +} + +template <> +float InputParams::_getParsed(const std::string paramName) +{ + return std::stof(_getUnparsed(paramName)); +} + +template <> +std::string InputParams::_getParsed(const std::string paramName) +{ + return _getUnparsed(paramName); +} diff --git a/src/psc_bgk.cxx b/src/psc_bgk.cxx index b58190eb65..0141ec7c24 100644 --- a/src/psc_bgk.cxx +++ b/src/psc_bgk.cxx @@ -6,10 +6,10 @@ #include "DiagnosticsDefault.h" #include "OutputFieldsDefault.h" #include "psc_config.hxx" +#include "input_params.hxx" #include "psc_bgk_util/bgk_params.hxx" #include "psc_bgk_util/table.hxx" -#include "psc_bgk_util/params_parser.hxx" // ====================================================================== // PSC configuration @@ -66,7 +66,7 @@ void setupParameters(int argc, char** argv) exit(1); } std::string path_to_params(argv[1]); - ParsedParams parsedParams(path_to_params); + InputParams parsedParams(path_to_params); ic_table = new Table(parsedParams.get("path_to_data")); g.loadParams(parsedParams, *ic_table); diff --git a/src/psc_bgk_util/bgk_params.hxx b/src/psc_bgk_util/bgk_params.hxx index 71ab27ac8a..ee5f6d3f25 100644 --- a/src/psc_bgk_util/bgk_params.hxx +++ b/src/psc_bgk_util/bgk_params.hxx @@ -1,7 +1,9 @@ #pragma once #include -#include "params_parser.hxx" + +#include "../include/input_params.hxx" + #include "table.hxx" // ====================================================================== @@ -106,7 +108,7 @@ struct PscBgkParams double rel_box_size_3; // length of 3rd dimension in calculated units int n_patches_3; // number of patches in 3rd dimension - void loadParams(ParsedParams parsedParams, Table& ic_table) + void loadParams(InputParams parsedParams, Table& ic_table) { box_size = parsedParams.getAndWarnOrDefault("box_size", -1); rel_box_size = parsedParams.getOrDefault("rel_box_size", 1); diff --git a/src/psc_bgk_util/params_parser.hxx b/src/psc_bgk_util/params_parser.hxx deleted file mode 100644 index 767ddcb26d..0000000000 --- a/src/psc_bgk_util/params_parser.hxx +++ /dev/null @@ -1,139 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -// ====================================================================== -// ParsedParams -// A simple parser intended for reading run parameters from a file, rather than -// hard-coding them. See psc_bgk_util/sample_bgk_params.txt for an example. - -class ParsedParams -{ -private: - std::unordered_map params; - -public: - ParsedParams(const std::string file_path) - { - // iterate over each line - std::ifstream ifs(file_path); - - if (ifs.is_open()) { - for (std::string line; std::getline(ifs, line);) { - - // parse first two words within line - std::istringstream iss(line); - std::string paramName, paramVal; - if (iss >> paramName >> paramVal) - params[paramName] = paramVal; - } - - ifs.close(); - } else { - std::cout << "Failed to open params file: " << file_path << "\n"; - exit(EXIT_FAILURE); - } - } - - template - T get(const std::string paramName); - - template - T getOrDefault(const std::string paramName, T deflt); - - template - T getAndWarnOrDefault(const std::string paramName, T deflt); - - // return true and display warning iff paramName is present - bool warnIfPresent(const std::string paramName, const std::string advice); - -private: - /// @brief Wrapper for retrieving an unparsed value with an enhanced error - /// message. - /// @param paramName the name of the parameter to fetch - /// @throws std::out_of_range (with an error message that specifies - /// `paramName`) if the parameter is not present - /// @return the unparsed parameter value (i.e., as a string) - std::string _get_inner(const std::string paramName); -}; - -// implementations - -template -T ParsedParams::getOrDefault(const std::string paramName, T deflt) -{ - if (params.count(paramName) == 1) - return get(paramName); - std::cout << "Warning: using default value for parameter '" << paramName - << "': " << deflt << "\n"; - return deflt; -} - -template -T ParsedParams::getAndWarnOrDefault(const std::string paramName, T deflt) -{ - if (params.count(paramName) == 1) { - T val = get(paramName); - std::cout << "Warning: using non-default value for parameter '" << paramName - << "': " << val << "\nDefault value of '" << deflt - << "' is recommended.\n"; - return val; - } - return deflt; -} - -bool ParsedParams::warnIfPresent(const std::string paramName, - const std::string advice) -{ - if (params.count(paramName) == 1) { - - std::cout << "Warning: parameter " << paramName << " is deprecated.\n" - << advice << "\n"; - return true; - } - return false; -} - -template <> -bool ParsedParams::get(const std::string paramName) -{ - bool b; - std::istringstream(_get_inner(paramName)) >> std::boolalpha >> b; - return b; -} - -template <> -double ParsedParams::get(const std::string paramName) -{ - return std::stod(_get_inner(paramName)); -} - -template <> -int ParsedParams::get(const std::string paramName) -{ - return std::stoi(_get_inner(paramName)); -} - -template <> -float ParsedParams::get(const std::string paramName) -{ - return std::stof(_get_inner(paramName)); -} - -template <> -std::string ParsedParams::get(const std::string paramName) -{ - return _get_inner(paramName); -} - -std::string ParsedParams::_get_inner(const std::string paramName) -{ - if (params.count(paramName) == 0) { - throw std::out_of_range("missing required input parameter: " + paramName); - } - return params.at(paramName); -} \ No newline at end of file diff --git a/src/psc_shock.cxx b/src/psc_shock.cxx index 85874b9e77..f89761745f 100644 --- a/src/psc_shock.cxx +++ b/src/psc_shock.cxx @@ -5,8 +5,7 @@ #include "DiagnosticsDefault.h" #include "OutputFieldsDefault.h" #include "psc_config.hxx" - -#include "psc_bgk_util/params_parser.hxx" +#include "input_params.hxx" // ====================================================================== // PSC configuration @@ -87,7 +86,7 @@ void setupParameters(int argc, char** argv) exit(1); } std::string path_to_params(argv[1]); - ParsedParams parsedParams(path_to_params); + InputParams parsedParams(path_to_params); psc_params.stats_every = 1000; psc_params.cfl = parsedParams.getOrDefault("cfl", .75);