diff --git a/src/HealthGPS.Console/CMakeLists.txt b/src/HealthGPS.Console/CMakeLists.txt index 453f882cb..40555bab9 100644 --- a/src/HealthGPS.Console/CMakeLists.txt +++ b/src/HealthGPS.Console/CMakeLists.txt @@ -5,16 +5,15 @@ find_package(TBB CONFIG REQUIRED) find_path(RAPIDCSV_INCLUDE_DIRS "rapidcsv.h") find_package(Threads REQUIRED) -add_executable(HealthGPS.Console "") -target_compile_features(HealthGPS.Console PUBLIC cxx_std_${CMAKE_CXX_STANDARD}) - -configure_file("version.h.in" "version.h" ESCAPE_QUOTES) -include_directories(${CMAKE_CURRENT_BINARY_DIR}) - -target_sources(HealthGPS.Console - PRIVATE +add_executable(HealthGPS.Console "program.cpp") +add_library(HealthGPS.LibConsole STATIC + "command_options.cpp" + "command_options.h" "configuration.cpp" "configuration.h" + "configuration_parsing.cpp" + "configuration_parsing.h" + "configuration_parsing_helpers.h" "csvparser.cpp" "csvparser.h" "event_monitor.cpp" @@ -27,7 +26,6 @@ target_sources(HealthGPS.Console "jsonparser.cpp" "jsonparser.h" "model_info.h" - "options.h" "resource.h" "result_writer.h" "riskmodel.h" @@ -40,7 +38,21 @@ if(WIN32) target_sources(HealthGPS.Console PRIVATE versioninfo.rc) endif(WIN32) +target_include_directories(HealthGPS.LibConsole PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) + target_link_libraries(HealthGPS.Console +PRIVATE + HealthGPS.Core + HealthGPS.Datastore + HealthGPS.LibConsole + HealthGPS + fmt::fmt + Threads::Threads + cxxopts::cxxopts + nlohmann_json::nlohmann_json + TBB::tbb) + +target_link_libraries(HealthGPS.LibConsole PRIVATE HealthGPS.Core HealthGPS.Datastore @@ -56,4 +68,4 @@ if(WIN32) install(IMPORTED_RUNTIME_ARTIFACTS fmt::fmt) endif() -set(ROOT_NAMESPACE hgps) \ No newline at end of file +set(ROOT_NAMESPACE hgps) diff --git a/src/HealthGPS.Console/command_options.cpp b/src/HealthGPS.Console/command_options.cpp new file mode 100644 index 000000000..c891c0b85 --- /dev/null +++ b/src/HealthGPS.Console/command_options.cpp @@ -0,0 +1,96 @@ +#include "command_options.h" +#include "version.h" + +#include + +#include + +namespace host { + +cxxopts::Options create_options() { + cxxopts::Options options("HealthGPS.Console", "Health-GPS microsimulation for policy options."); + options.add_options()("f,file", "Configuration file full name.", cxxopts::value())( + "s,storage", "Path to root folder of the data storage.", cxxopts::value())( + "j,jobid", "The batch execution job identifier.", + cxxopts::value())("verbose", "Print more information about progress", + cxxopts::value()->default_value("false"))( + "help", "Help about this application.")("version", "Print the application version number."); + + return options; +} + +CommandOptions parse_arguments(cxxopts::Options &options, int &argc, char *argv[]) { + namespace fs = std::filesystem; + + CommandOptions cmd; + try { + cmd.success = true; + cmd.exit_code = EXIT_SUCCESS; + cmd.verbose = false; + auto result = options.parse(argc, argv); + if (result.count("help")) { + std::cout << options.help() << std::endl; + cmd.success = false; + return cmd; + } + + if (result.count("version")) { + fmt::print("Version {}\n\n", PROJECT_VERSION); + cmd.success = false; + return cmd; + } + + cmd.verbose = result["verbose"].as(); + if (cmd.verbose) { + fmt::print(fg(fmt::color::dark_salmon), "Verbose output enabled\n"); + } + + if (result.count("file")) { + cmd.config_file = result["file"].as(); + if (cmd.config_file.is_relative()) { + cmd.config_file = std::filesystem::absolute(cmd.config_file); + fmt::print("Configuration file..: {}\n", cmd.config_file.string()); + } + } + + if (!fs::exists(cmd.config_file)) { + fmt::print(fg(fmt::color::red), "\nConfiguration file: {} not found.\n", + cmd.config_file.string()); + cmd.exit_code = EXIT_FAILURE; + } + + if (result.count("storage")) { + cmd.storage_folder = result["storage"].as(); + if (cmd.storage_folder.is_relative()) { + cmd.storage_folder = std::filesystem::absolute(cmd.storage_folder); + fmt::print("File storage folder.: {}\n", cmd.storage_folder.string()); + } + } + + if (!fs::exists(cmd.storage_folder)) { + fmt::print(fg(fmt::color::red), "\nFile storage folder: {} not found.\n", + cmd.storage_folder.string()); + cmd.exit_code = EXIT_FAILURE; + } + + if (result.count("jobid")) { + cmd.job_id = result["jobid"].as(); + if (cmd.job_id < 1) { + fmt::print(fg(fmt::color::red), + "\nJob identifier value outside range: (0 < x) given: {}.\n", + std::to_string(cmd.job_id)); + cmd.exit_code = EXIT_FAILURE; + } + } + + cmd.success = cmd.exit_code == EXIT_SUCCESS; + } catch (const cxxopts::exceptions::exception &ex) { + fmt::print(fg(fmt::color::red), "\nInvalid command line argument: {}.\n", ex.what()); + fmt::print("\n{}\n", options.help()); + cmd.success = false; + cmd.exit_code = EXIT_FAILURE; + } + + return cmd; +} +} // namespace host \ No newline at end of file diff --git a/src/HealthGPS.Console/command_options.h b/src/HealthGPS.Console/command_options.h new file mode 100644 index 000000000..9016b4114 --- /dev/null +++ b/src/HealthGPS.Console/command_options.h @@ -0,0 +1,43 @@ +/** + * @file + * @brief Functionality for parsing console application's command-line arguments + */ +#pragma once +#include + +#include + +namespace host { +/// @brief Defines the Command Line Interface (CLI) arguments options +struct CommandOptions { + /// @brief Indicates whether the argument parsing succeed + bool success{}; + + /// @brief The exit code to return, in case of CLI arguments parsing failure + int exit_code{}; + + /// @brief The configuration file argument value + std::filesystem::path config_file{}; + + /// @brief The back-end storage full path argument value + std::filesystem::path storage_folder{}; + + /// @brief Indicates whether the application logging is verbose + bool verbose{}; + + /// @brief The batch job identifier value, optional. + int job_id{}; +}; + +/// @brief Creates the command-line interface (CLI) options +/// @return Health-GPS CLI options +cxxopts::Options create_options(); + +/// @brief Parses the command-line interface (CLI) arguments +/// @param options The valid CLI options +/// @param argc Number of input arguments +/// @param argv List of input arguments +/// @return User command-line options +CommandOptions parse_arguments(cxxopts::Options &options, int &argc, char *argv[]); + +} // namespace host \ No newline at end of file diff --git a/src/HealthGPS.Console/configuration.cpp b/src/HealthGPS.Console/configuration.cpp index 0f004e92c..716077a50 100644 --- a/src/HealthGPS.Console/configuration.cpp +++ b/src/HealthGPS.Console/configuration.cpp @@ -1,4 +1,5 @@ #include "configuration.h" +#include "configuration_parsing.h" #include "jsonparser.h" #include "version.h" @@ -11,6 +12,7 @@ #include "HealthGPS/physical_activity_scenario.h" #include "HealthGPS/simple_policy_scenario.h" +#include "HealthGPS.Core/poco.h" #include "HealthGPS.Core/scoped_timer.h" #include @@ -30,264 +32,87 @@ namespace host { using namespace hgps; +using json = nlohmann::json; -std::string get_time_now_str() { - auto tp = std::chrono::system_clock::now(); - return fmt::format("{0:%F %H:%M:}{1:%S} {0:%Z}", tp, tp.time_since_epoch()); -} - -cxxopts::Options create_options() { - cxxopts::Options options("HealthGPS.Console", "Health-GPS microsimulation for policy options."); - options.add_options()("f,file", "Configuration file full name.", cxxopts::value())( - "s,storage", "Path to root folder of the data storage.", cxxopts::value())( - "j,jobid", "The batch execution job identifier.", - cxxopts::value())("verbose", "Print more information about progress", - cxxopts::value()->default_value("false"))( - "help", "Help about this application.")("version", "Print the application version number."); - - return options; -} - -void print_app_title() { - fmt::print(fg(fmt::color::yellow) | fmt::emphasis::bold, - "\n# Health-GPS Microsimulation for Policy Options #\n\n"); +ConfigurationError::ConfigurationError(const std::string &msg) : std::runtime_error{msg} {} - fmt::print("Today: {}\n\n", get_time_now_str()); -} - -CommandOptions parse_arguments(cxxopts::Options &options, int &argc, char *argv[]) { +Configuration get_configuration(CommandOptions &options) { MEASURE_FUNCTION(); namespace fs = std::filesystem; + using namespace host::poco; - CommandOptions cmd; - try { - cmd.success = true; - cmd.exit_code = EXIT_SUCCESS; - cmd.verbose = false; - auto result = options.parse(argc, argv); - if (result.count("help")) { - std::cout << options.help() << std::endl; - cmd.success = false; - return cmd; - } - - if (result.count("version")) { - fmt::print("Version {}\n\n", PROJECT_VERSION); - cmd.success = false; - return cmd; - } - - cmd.verbose = result["verbose"].as(); - if (cmd.verbose) { - fmt::print(fg(fmt::color::dark_salmon), "Verbose output enabled\n"); - } - - if (result.count("file")) { - cmd.config_file = result["file"].as(); - if (cmd.config_file.is_relative()) { - cmd.config_file = std::filesystem::absolute(cmd.config_file); - fmt::print("Configuration file..: {}\n", cmd.config_file.string()); - } - } - - if (!fs::exists(cmd.config_file)) { - fmt::print(fg(fmt::color::red), "\nConfiguration file: {} not found.\n", - cmd.config_file.string()); - cmd.exit_code = EXIT_FAILURE; - } - - if (result.count("storage")) { - cmd.storage_folder = result["storage"].as(); - if (cmd.storage_folder.is_relative()) { - cmd.storage_folder = std::filesystem::absolute(cmd.storage_folder); - fmt::print("File storage folder.: {}\n", cmd.storage_folder.string()); - } - } - - if (!fs::exists(cmd.storage_folder)) { - fmt::print(fg(fmt::color::red), "\nFile storage folder: {} not found.\n", - cmd.storage_folder.string()); - cmd.exit_code = EXIT_FAILURE; - } + bool success = true; - if (result.count("jobid")) { - cmd.job_id = result["jobid"].as(); - if (cmd.job_id < 1) { - fmt::print(fg(fmt::color::red), - "\nJob identifier value outside range: (0 < x) given: {}.\n", - std::to_string(cmd.job_id)); - cmd.exit_code = EXIT_FAILURE; - } - } + Configuration config; + config.job_id = options.job_id; - cmd.success = cmd.exit_code == EXIT_SUCCESS; - } catch (const cxxopts::exceptions::exception &ex) { - fmt::print(fg(fmt::color::red), "\nInvalid command line argument: {}.\n", ex.what()); - fmt::print("\n{}\n", options.help()); - cmd.success = false; - cmd.exit_code = EXIT_FAILURE; + // verbosity + config.verbosity = core::VerboseMode::none; + if (options.verbose) { + config.verbosity = core::VerboseMode::verbose; } - return cmd; -} - -Configuration load_configuration(CommandOptions &options) { - MEASURE_FUNCTION(); - namespace fs = std::filesystem; - using namespace host::poco; - - Configuration config; - fs::path file_path; std::ifstream ifs(options.config_file, std::ifstream::in); + if (!ifs) { + throw ConfigurationError( + fmt::format("File {} doesn't exist.", options.config_file.string())); + } - if (ifs) { - auto opt = json::parse(ifs); - if (!opt.contains("version")) { - throw std::runtime_error("Invalid definition, file must have a schema version"); - } - - auto version = opt["version"].get(); - if (version != 1 && version != 2) { - throw std::runtime_error(fmt::format( - "configuration schema version: {} mismatch, supported: 1 and 2", version)); - } - - // application version - config.app_name = PROJECT_NAME; - config.app_version = PROJECT_VERSION; - - // Configuration root path - config.root_path = options.config_file.parent_path(); - - // input dataset file - auto dataset_key = "dataset"; - if (version == 1) { - dataset_key = "file"; - } - - config.file = opt["inputs"][dataset_key].get(); - file_path = config.file.name; - if (file_path.is_relative()) { - file_path = config.root_path / file_path; - config.file.name = file_path.string(); - } - - fmt::print("Input dataset file: {}\n", config.file.name); - if (!fs::exists(file_path)) { - fmt::print(fg(fmt::color::red), "\nInput data file: {} not found.\n", - file_path.string()); - } - - // Settings and SES mapping - config.settings = opt["inputs"]["settings"].get(); - config.ses = opt["modelling"]["ses_model"].get(); - - // Modelling information - config.modelling = opt["modelling"].get(); - for (auto &model : config.modelling.risk_factor_models) { - file_path = model.second; - if (file_path.is_relative()) { - file_path = config.root_path / file_path; - model.second = file_path.string(); - } - - fmt::print("Risk factor model: {}, file: {}\n", model.first, model.second); - if (!fs::exists(file_path)) { - fmt::print(fg(fmt::color::red), "Risk factor model: {}, file: {} not found.\n", - model.first, file_path.string()); - } - } - - for (auto &item : config.modelling.baseline_adjustment.file_names) { - file_path = item.second; - if (file_path.is_relative()) { - file_path = config.root_path / file_path; - item.second = file_path.string(); - } - - fmt::print("Baseline factor adjustment type: {}, file: {}\n", item.first, - file_path.string()); - if (!fs::exists(file_path)) { - fmt::print(fg(fmt::color::red), - "Baseline factor adjustment type: {}, file: {} not found.\n", item.first, - file_path.string()); - } + const auto opt = [&ifs]() { + try { + return json::parse(ifs); + } catch (const std::exception &e) { + throw ConfigurationError(fmt::format("Could not parse JSON: {}", e.what())); } + }(); - // Run-time - opt["running"]["start_time"].get_to(config.start_time); - opt["running"]["stop_time"].get_to(config.stop_time); - opt["running"]["trial_runs"].get_to(config.trial_runs); - opt["running"]["sync_timeout_ms"].get_to(config.sync_timeout_ms); - auto seed = opt["running"]["seed"].get>(); - if (seed.size() > 0) { - config.custom_seed = seed[0]; - } + // Check the file format version + try { + check_version(opt); + } catch (const ConfigurationError &) { + success = false; + } - opt["running"]["diseases"].get_to(config.diseases); - - // Intervention Policy - auto &interventions = opt["running"]["interventions"]; - if (!interventions["active_type_id"].is_null()) { - auto active_type = interventions["active_type_id"].get(); - auto &policy_types = interventions["types"]; - for (auto it = policy_types.begin(); it != policy_types.end(); ++it) { - if (core::case_insensitive::equals(it.key(), active_type)) { - config.intervention = it.value().get(); - config.intervention.identifier = core::to_lower(it.key()); - config.has_active_intervention = true; - break; - } - } + // Base dir for relative paths + config.root_path = options.config_file.parent_path(); - if (!core::case_insensitive::equals(config.intervention.identifier, active_type)) { - throw std::runtime_error( - fmt::format("Unknown active intervention type identifier: {}", active_type)); - } - } - - config.job_id = options.job_id; - config.output = opt["output"].get(); - config.output.folder = expand_environment_variables(config.output.folder); - if (!fs::exists(config.output.folder)) { - fmt::print(fg(fmt::color::dark_salmon), "\nCreating output folder: {} ...\n", - config.output.folder); - if (!create_output_folder(config.output.folder)) { - throw std::runtime_error( - fmt::format("Failed to create output folder: {}", config.output.folder)); - } - } + // input dataset file + try { + load_input_info(opt, config); + fmt::print("Input dataset file: {}\n", config.file.name.string()); + } catch (const std::exception &e) { + success = false; + fmt::print(fg(fmt::color::red), "Could not load dataset file: {}\n", e.what()); + } - // verbosity - config.verbosity = core::VerboseMode::none; - if (options.verbose) { - config.verbosity = core::VerboseMode::verbose; - } - } else { - std::cout << fmt::format("File {} doesn't exist.", options.config_file.string()) - << std::endl; + // Modelling information + try { + load_modelling_info(opt, config); + } catch (const std::exception &e) { + success = false; + fmt::print(fg(fmt::color::red), "Could not load modelling info: {}\n", e.what()); } - ifs.close(); - return config; -} + // Run-time info + try { + load_running_info(opt, config); + } catch (const std::exception &e) { + success = false; + fmt::print(fg(fmt::color::red), "Could not load running info: {}\n", e.what()); + } -bool create_output_folder(std::filesystem::path folder_path, unsigned int num_retries) { - using namespace std::chrono_literals; - for (unsigned int i = 1; i <= num_retries; i++) { - try { - if (std::filesystem::create_directories(folder_path)) { - return true; - } - } catch (const std::exception &ex) { - fmt::print(fg(fmt::color::red), "Failed to create output folder, attempt #{} - {}.\n", - i, ex.what()); - } + try { + load_output_info(opt, config); + } catch (const ConfigurationError &) { + success = false; + fmt::print(fg(fmt::color::red), "Could not load output info"); + } - std::this_thread::sleep_for(1000ms); + if (!success) { + throw ConfigurationError{"Error loading config file"}; } - return false; + return config; } std::vector get_diseases_info(core::Datastore &data_api, Configuration &config) { @@ -306,9 +131,6 @@ std::vector get_diseases_info(core::Datastore &data_api, Conf ModelInput create_model_input(core::DataTable &input_table, core::Country country, Configuration &config, std::vector diseases) { // Create simulation configuration - auto age_range = - core::IntegerInterval(config.settings.age_range.front(), config.settings.age_range.back()); - auto comorbidities = config.output.comorbidities; auto diseases_number = static_cast(diseases.size()); if (comorbidities > diseases_number) { @@ -317,7 +139,7 @@ ModelInput create_model_input(core::DataTable &input_table, core::Country countr config.output.comorbidities, comorbidities); } - auto settings = Settings(country, config.settings.size_fraction, age_range); + auto settings = Settings(country, config.settings.size_fraction, config.settings.age_range); auto job_custom_seed = create_job_seed(config.job_id, config.custom_seed); auto run_info = RunInfo{ .start_time = config.start_time, @@ -332,13 +154,8 @@ ModelInput create_model_input(core::DataTable &input_table, core::Country countr SESDefinition{.fuction_name = config.ses.function, .parameters = config.ses.parameters}; auto mapping = std::vector(); - for (auto &item : config.modelling.risk_factors) { - if (item.range.empty()) { - mapping.emplace_back(item.name, item.level); - } else { - auto boundary = hgps::OptionalRange{{item.range[0], item.range[1]}}; - mapping.emplace_back(item.name, item.level, boundary); - } + for (const auto &item : config.modelling.risk_factors) { + mapping.emplace_back(item.name, item.level, item.range); } return ModelInput(input_table, settings, run_info, ses_mapping, @@ -389,12 +206,14 @@ std::string create_output_file_name(const poco::OutputInfo &info, int job_id) { ResultFileWriter create_results_file_logger(const Configuration &config, const hgps::ModelInput &input) { - return ResultFileWriter{create_output_file_name(config.output, config.job_id), - ExperimentInfo{.model = config.app_name, - .version = config.app_version, - .intervention = config.intervention.identifier, - .job_id = config.job_id, - .seed = input.seed().value_or(0u)}}; + return ResultFileWriter{ + create_output_file_name(config.output, config.job_id), + ExperimentInfo{.model = config.app_name, + .version = config.app_version, + .intervention = + config.active_intervention ? config.active_intervention->identifier : "", + .job_id = config.job_id, + .seed = input.seed().value_or(0u)}}; } std::unique_ptr create_baseline_scenario(hgps::SyncChannel &channel) { diff --git a/src/HealthGPS.Console/configuration.h b/src/HealthGPS.Console/configuration.h index a44d6dd66..d86facdfe 100644 --- a/src/HealthGPS.Console/configuration.h +++ b/src/HealthGPS.Console/configuration.h @@ -1,5 +1,15 @@ +/** + * @file + * @brief Main header file for functionality related to loading config files + * + * This file contains definitions for the main functions required to load JSON-formatted + * configuration files from disk. + */ #pragma once -#include + +#include "command_options.h" +#include "poco.h" +#include "version.h" #include "HealthGPS/healthgps.h" #include "HealthGPS/intervention_scenario.h" @@ -8,38 +18,77 @@ #include "HealthGPS.Core/api.h" -#include "options.h" #include "result_file_writer.h" +#include +#include + namespace host { -/// @brief Get a string representation of current system time -/// @return The system time as string -std::string get_time_now_str(); -/// @brief Creates the command-line interface (CLI) options -/// @return Health-GPS CLI options -cxxopts::Options create_options(); +/// @brief Defines the application configuration data structure +struct Configuration { + /// @brief The root path for configuration files + std::filesystem::path root_path; + + /// @brief The input data file details + poco::FileInfo file; + + /// @brief Experiment population settings + poco::SettingsInfo settings; + + /// @brief Socio-economic status (SES) model inputs + poco::SESInfo ses; + + /// @brief User defined model and parameters information + poco::ModellingInfo modelling; + + /// @brief List of diseases to include in experiment + std::vector diseases; + + /// @brief Simulation initialisation custom seed value, optional + std::optional custom_seed; + + /// @brief The experiment start time (simulation clock) + unsigned int start_time{}; -/// @brief Prints application start-up messages -void print_app_title(); + /// @brief The experiment stop time (simulation clock) + unsigned int stop_time{}; -/// @brief Parses the command-line interface (CLI) arguments -/// @param options The valid CLI options -/// @param argc Number of input arguments -/// @param argv List of input arguments -/// @return User command-line options -CommandOptions parse_arguments(cxxopts::Options &options, int &argc, char *argv[]); + /// @brief The number of simulation runs (replications) to execute + unsigned int trial_runs{}; + + /// @brief Baseline to intervention data synchronisation time out (milliseconds) + unsigned int sync_timeout_ms{}; + + /// @brief The active intervention policy definition + std::optional active_intervention; + + /// @brief Experiment output folder and file information + poco::OutputInfo output; + + /// @brief Application logging verbosity mode + hgps::core::VerboseMode verbosity{}; + + /// @brief Experiment batch job identifier + int job_id{}; + + /// @brief Experiment model name + const char *app_name = PROJECT_NAME; + + /// @brief Experiment model version + const char *app_version = PROJECT_VERSION; +}; + +/// @brief Represents an error that occurred with the format of a config file +class ConfigurationError : public std::runtime_error { + public: + ConfigurationError(const std::string &msg); +}; /// @brief Loads the input configuration file, *.json, information /// @param options User command-line options /// @return The configuration file information -Configuration load_configuration(CommandOptions &options); - -/// @brief Creates the configuration output folder for result files -/// @param folder_path Full path to output folder -/// @param num_retries Number of attempts before giving up -/// @return true for successful creation, otherwise false -bool create_output_folder(std::filesystem::path folder_path, unsigned int num_retries = 3); +Configuration get_configuration(CommandOptions &options); /// @brief Gets the collection of diseases that matches the selected input list /// @param data_api The back-end data store instance to be used. diff --git a/src/HealthGPS.Console/configuration_parsing.cpp b/src/HealthGPS.Console/configuration_parsing.cpp new file mode 100644 index 000000000..a0b098951 --- /dev/null +++ b/src/HealthGPS.Console/configuration_parsing.cpp @@ -0,0 +1,267 @@ +#include "configuration_parsing.h" +#include "configuration_parsing_helpers.h" +#include "jsonparser.h" + +#include +#include +#include + +namespace host { +using json = nlohmann::json; + +nlohmann::json get(const json &j, const std::string &key) { + try { + return j.at(key); + } catch (const std::exception &) { + fmt::print(fg(fmt::color::red), "Missing key \"{}\"\n", key); + throw ConfigurationError{fmt::format("Missing key \"{}\"", key)}; + } +} + +void rebase_valid_path(std::filesystem::path &path, const std::filesystem::path &base_dir) try { + if (path.is_relative()) { + path = std::filesystem::absolute(base_dir / path); + } + + if (!std::filesystem::exists(path)) { + throw ConfigurationError{fmt::format("Path does not exist: {}", path.string())}; + } +} catch (const std::filesystem::filesystem_error &) { + throw ConfigurationError{fmt::format("OS error while reading path {}", path.string())}; +} + +// NOLINTNEXTLINE(bugprone-exception-escape) +bool rebase_valid_path_to(const json &j, const std::string &key, std::filesystem::path &out, + const std::filesystem::path &base_dir) noexcept { + if (!get_to(j, key, out)) { + return false; + } + + try { + rebase_valid_path(out, base_dir); + } catch (const ConfigurationError &) { + fmt::print(fg(fmt::color::red), "Could not find file {}\n", out.string()); + return false; + } + + return true; +} + +// NOLINTNEXTLINE(bugprone-exception-escape) +void rebase_valid_path_to(const json &j, const std::string &key, std::filesystem::path &out, + const std::filesystem::path &base_dir, bool &success) noexcept { + if (!rebase_valid_path_to(j, key, out, base_dir)) { + success = false; + } +} + +poco::FileInfo get_file_info(const json &node, const std::filesystem::path &base_dir) { + bool success = true; + poco::FileInfo info; + rebase_valid_path_to(node, "name", info.name, base_dir, success); + get_to(node, "format", info.format, success); + get_to(node, "delimiter", info.delimiter, success); + get_to(node, "columns", info.columns, success); + if (!success) { + throw ConfigurationError{"Could not load input file info"}; + } + + return info; +} + +poco::SettingsInfo get_settings(const json &j) { + poco::SettingsInfo info; + if (!get_to(j, "settings", info)) { + throw ConfigurationError{"Could not load settings info"}; + } + + return info; +} + +poco::BaselineInfo get_baseline_info(const json &j, const std::filesystem::path &base_dir) { + const auto &adj = get(j, "baseline_adjustments"); + + bool success = true; + poco::BaselineInfo info; + get_to(adj, "format", info.format, success); + get_to(adj, "delimiter", info.delimiter, success); + get_to(adj, "encoding", info.encoding, success); + if (get_to(adj, "file_names", info.file_names, success)) { + // Rebase paths and check for errors + for (auto &[name, path] : info.file_names) { + try { + rebase_valid_path(path, base_dir); + fmt::print("{:<14}, file: {}\n", name, path.string()); + } catch (const ConfigurationError &) { + fmt::print(fg(fmt::color::red), "Could not find file: {}\n", path.string()); + success = false; + } + } + } + + if (!success) { + throw ConfigurationError{"Could not get baseline adjustments"}; + } + + return info; +} + +void load_interventions(const json &running, Configuration &config) { + const auto interventions = get(running, "interventions"); + + bool success = true; + + std::optional active_type_id; // might be null + if (!get_to(interventions, "active_type_id", active_type_id, success)) { + fmt::print(fmt::fg(fmt::color::red), "active_type_id is invalid\n"); + } + + /* + * NB: This loads all of the policy scenario info from the JSON file, which is + * strictly speaking unnecessary, but it does mean that we can verify the data + * format is correct. + */ + std::unordered_map policy_types; + if (!get_to(interventions, "types", policy_types, success)) { + fmt::print(fmt::fg(fmt::color::red), + "Could not load policy types from interventions section\n"); + } + + if (active_type_id) { + try { + config.active_intervention = policy_types.at(active_type_id.value()); + config.active_intervention->identifier = active_type_id->to_string(); + } catch (const std::out_of_range &) { + success = false; + fmt::print(fmt::fg(fmt::color::red), + "Unknown active intervention type identifier: {}\n", + active_type_id->to_string()); + } + } + + if (!success) { + throw ConfigurationError{"Failed to load policy interventions"}; + } +} + +void check_version(const json &j) { + int version; + if (!get_to(j, "version", version)) { + throw ConfigurationError{"File must have a schema version"}; + } + + if (version != 2) { + throw ConfigurationError{ + fmt::format("Configuration schema version: {} mismatch, supported: 2", version)}; + } +} + +void load_input_info(const json &j, Configuration &config) { + const auto inputs = get(j, "inputs"); + bool success = true; + + // Input dataset file + try { + config.file = get_file_info(get(inputs, "dataset"), config.root_path); + fmt::print("Input dataset file: {}\n", config.file.name.string()); + } catch (const std::exception &e) { + success = false; + fmt::print(fg(fmt::color::red), "Could not load dataset file: {}\n", e.what()); + } + + // Settings + try { + config.settings = get_settings(inputs); + } catch (const std::exception &) { + success = false; + fmt::print(fg(fmt::color::red), "Could not load settings info"); + } + + if (!success) { + throw ConfigurationError{"Could not load settings info"}; + } +} + +void load_modelling_info(const json &j, Configuration &config) { + bool success = true; + const auto modelling = get(j, "modelling"); + + auto &info = config.modelling; + get_to(modelling, "risk_factors", info.risk_factors, success); + + // Rebase paths and check for errors + if (get_to(modelling, "risk_factor_models", info.risk_factor_models, success)) { + for (auto &[type, path] : info.risk_factor_models) { + try { + rebase_valid_path(path, config.root_path); + fmt::print("{:<14}, file: {}\n", type, path.string()); + } catch (const ConfigurationError &) { + success = false; + fmt::print(fg(fmt::color::red), "Adjustment type: {}, file: {} not found.\n", type, + path.string()); + } + } + } + + try { + info.baseline_adjustment = get_baseline_info(modelling, config.root_path); + } catch (const std::exception &e) { + success = false; + fmt::print(fmt::fg(fmt::color::red), "Could not load baseline adjustment: {}\n", e.what()); + } + + try { + // SES mapping + // TODO: Maybe this needs its own helper function + config.ses = get(modelling, "ses_model").get(); + } catch (const std::exception &) { + success = false; + fmt::print(fmt::fg(fmt::color::red), "Could not load SES mappings"); + } + + if (!success) { + throw ConfigurationError("Could not load modelling info"); + } +} + +void load_running_info(const json &j, Configuration &config) { + const auto running = get(j, "running"); + + bool success = true; + get_to(running, "start_time", config.start_time, success); + get_to(running, "stop_time", config.stop_time, success); + get_to(running, "trial_runs", config.trial_runs, success); + get_to(running, "sync_timeout_ms", config.sync_timeout_ms, success); + get_to(running, "diseases", config.diseases, success); + + { + // I copied this logic from the old code, but it seems strange to me. Why do we + // store multiple seeds but only use the first? -- Alex + std::vector seeds; + if (get_to(running, "seed", seeds, success) && !seeds.empty()) { + config.custom_seed = seeds[0]; + } + } + + // Intervention Policy + try { + load_interventions(running, config); + } catch (const ConfigurationError &e) { + success = false; + fmt::print(fmt::fg(fmt::color::red), "Could not load interventions: {}", e.what()); + } + + if (!success) { + throw ConfigurationError{"Could not load running info"}; + } +} + +void load_output_info(const json &j, Configuration &config) { + if (!get_to(j, "output", config.output)) { + throw ConfigurationError{"Could not load output info"}; + } + + config.output.folder = expand_environment_variables(config.output.folder); +} + +} // namespace host diff --git a/src/HealthGPS.Console/configuration_parsing.h b/src/HealthGPS.Console/configuration_parsing.h new file mode 100644 index 000000000..033054701 --- /dev/null +++ b/src/HealthGPS.Console/configuration_parsing.h @@ -0,0 +1,37 @@ +/** + * @file + * @brief This file contains functions for loading subsections of the main JSON file + */ +#pragma once +#include "configuration.h" + +namespace host { +/// @brief Check the schema version and throw if invalid +/// @param j The root JSON object +/// @throw ConfigurationError: If version attribute is not present or invalid +void check_version(const nlohmann::json &j); + +/// @brief Load input dataset +/// @param j The root JSON object +/// @param config The config object to update +/// @throw ConfigurationError: Could not load input dataset +void load_input_info(const nlohmann::json &j, Configuration &config); + +/// @brief Load ModellingInfo from JSON +/// @param j The root JSON object +/// @param config The config object to update +/// @throw ConfigurationError: Could not load modelling info +void load_modelling_info(const nlohmann::json &j, Configuration &config); + +/// @brief Load running section of JSON object +/// @param j The root JSON object +/// @param config The config object to update +/// @throw ConfigurationError: Could not load running section +void load_running_info(const nlohmann::json &j, Configuration &config); + +/// @brief Load output section of JSON object +/// @param j The root JSON object +/// @param config The config object to update +/// @throw ConfigurationError: Could not load output info +void load_output_info(const nlohmann::json &j, Configuration &config); +} // namespace host \ No newline at end of file diff --git a/src/HealthGPS.Console/configuration_parsing_helpers.h b/src/HealthGPS.Console/configuration_parsing_helpers.h new file mode 100644 index 000000000..2ef3750bc --- /dev/null +++ b/src/HealthGPS.Console/configuration_parsing_helpers.h @@ -0,0 +1,114 @@ +/** + * @file configuration_parsing_helpers.h + * @brief This file contains helper functions used by the configuration parsing code + * + * The reason for placing these definitions in a separate header is so as to keep the + * main configuration_parsing.h file tidy, whilst also making them available to testing + * code. + */ +#pragma once +#include "configuration.h" +#include "poco.h" + +#include +#include + +#include +#include + +namespace host { +/// @brief Load value from JSON, printing an error message if it fails +/// @param j JSON object +/// @param key Key to value +/// @throw ConfigurationError: Key not found +/// @return Key value +nlohmann::json get(const nlohmann::json &j, const std::string &key); + +/// @brief Get value from JSON object and store in out +/// @tparam T Type of output object +/// @param j JSON object +/// @param key Key to value +/// @param out Output object +/// @return True if value was retrieved successfully, false otherwise +template bool get_to(const nlohmann::json &j, const std::string &key, T &out) noexcept { + try { + out = j.at(key).get(); + return true; + } catch (const nlohmann::json::out_of_range &) { + fmt::print(fg(fmt::color::red), "Missing key \"{}\"\n", key); + return false; + } catch (const nlohmann::json::type_error &) { + fmt::print(fg(fmt::color::red), "Key \"{}\" is of wrong type\n", key); + return false; + } +} + +/// @brief Get value from JSON object and store in out, setting success flag +/// @tparam T Type of output object +/// @param j JSON object +/// @param key Key to value +/// @param out Output object +/// @param success Success flag, set to false in case of failure +/// @return True if value was retrieved successfully, false otherwise +template +bool get_to(const nlohmann::json &j, const std::string &key, T &out, bool &success) noexcept { + const bool ret = get_to(j, key, out); + if (!ret) { + success = false; + } + return ret; +} + +/// @brief Rebase path on base_dir +/// @param path Initial path (relative or absolute) +/// @param base_dir New base directory for relative path +/// @throw ConfigurationError: If path does not exist +void rebase_valid_path(std::filesystem::path &path, const std::filesystem::path &base_dir); + +/// @brief Get a valid path from a JSON object +/// @param j JSON object +/// @param key Key to value +/// @param base_dir Base directory for relative path +/// @param out Output variable +/// @return True if value was retrieved successfully and is valid path, false otherwise +bool rebase_valid_path_to(const nlohmann::json &j, const std::string &key, + std::filesystem::path &out, + const std::filesystem::path &base_dir) noexcept; + +/// @brief Get a valid path from a JSON object +/// @param j JSON object +/// @param key Key to value +/// @param base_dir Base directory for relative path +/// @param out Output variable +/// @param success Success flag, set to false in case of failure +void rebase_valid_path_to(const nlohmann::json &j, const std::string &key, + std::filesystem::path &out, const std::filesystem::path &base_dir, + bool &success) noexcept; + +/// @brief Load FileInfo from JSON +/// @param node The node of the JSON object containing the FileInfo +/// @param base_dir Base folder +/// @return FileInfo +/// @throw ConfigurationError: Invalid config file format +poco::FileInfo get_file_info(const nlohmann::json &node, const std::filesystem::path &base_dir); + +/// @brief Load settings section of JSON +/// @param j Input JSON +/// @return SettingsInfo +/// @throw ConfigurationError: Could not load settings +poco::SettingsInfo get_settings(const nlohmann::json &j); + +/// @brief Load BaselineInfo from JSON +/// @param j Input JSON +/// @param base_dir Base folder +/// @return BaselineInfo +/// @throw ConfigurationError: One or more files could not be found +poco::BaselineInfo get_baseline_info(const nlohmann::json &j, + const std::filesystem::path &base_dir); + +/// @brief Load interventions from running section +/// @param running Running section of JSON object +/// @param config Config object to update +/// @throw ConfigurationError: Could not load interventions +void load_interventions(const nlohmann::json &running, Configuration &config); +} // namespace host diff --git a/src/HealthGPS.Console/csvparser.cpp b/src/HealthGPS.Console/csvparser.cpp index 5fcbd5288..72782f04f 100644 --- a/src/HealthGPS.Console/csvparser.cpp +++ b/src/HealthGPS.Console/csvparser.cpp @@ -90,7 +90,8 @@ hgps::core::DataTable load_datatable_from_csv(const poco::FileInfo &file_info) { using namespace rapidcsv; bool success = true; - Document doc{file_info.name, LabelParams{}, SeparatorParams{file_info.delimiter.front()}}; + Document doc{file_info.name.string(), LabelParams{}, + SeparatorParams{file_info.delimiter.front()}}; // Validate columns and create file columns map auto headers = doc.GetColumnNames(); diff --git a/src/HealthGPS.Console/jsonparser.cpp b/src/HealthGPS.Console/jsonparser.cpp index 771e6d7a9..425fe2482 100644 --- a/src/HealthGPS.Console/jsonparser.cpp +++ b/src/HealthGPS.Console/jsonparser.cpp @@ -4,6 +4,11 @@ namespace host::poco { //-------------------------------------------------------- // Risk Model JSON serialisation / de-serialisation //-------------------------------------------------------- +// Data file information +void to_json(json &j, const FileInfo &p) { + j = json{ + {"name", p.name}, {"format", p.format}, {"delimiter", p.delimiter}, {"columns", p.columns}}; +} // Linear models void to_json(json &j, const CoefficientInfo &p) { @@ -62,19 +67,6 @@ void from_json(const json &j, HierarchicalLevelInfo &p) { // Options JSON serialisation / de-serialisation //-------------------------------------------------------- -// Data file information -void to_json(json &j, const FileInfo &p) { - j = json{ - {"name", p.name}, {"format", p.format}, {"delimiter", p.delimiter}, {"columns", p.columns}}; -} - -void from_json(const json &j, FileInfo &p) { - j.at("name").get_to(p.name); - j.at("format").get_to(p.format); - j.at("delimiter").get_to(p.delimiter); - j.at("columns").get_to(p.columns); -} - // Settings Information void to_json(json &j, const SettingsInfo &p) { j = json{{"country_code", p.country}, @@ -88,32 +80,7 @@ void from_json(const json &j, SettingsInfo &p) { j.at("age_range").get_to(p.age_range); } -// SES Model Information -void to_json(json &j, const SESInfo &p) { - j = json{{"function_name", p.function}, {"function_parameters", p.parameters}}; -} - -void from_json(const json &j, SESInfo &p) { - j.at("function_name").get_to(p.function); - j.at("function_parameters").get_to(p.parameters); -} - -// Baseline scenario adjustments -void to_json(json &j, const BaselineInfo &p) { - j = json{{"format", p.format}, - {"delimiter", p.delimiter}, - {"encoding", p.encoding}, - {"file_names", p.file_names}}; -} - -void from_json(const json &j, BaselineInfo &p) { - j.at("format").get_to(p.format); - j.at("delimiter").get_to(p.delimiter); - j.at("encoding").get_to(p.encoding); - j.at("file_names").get_to(p.file_names); -} - -// Risk Factor Modelling +// Risk factor modelling void to_json(json &j, const RiskFactorInfo &p) { j = json{{"name", p.name}, {"level", p.level}, {"range", p.range}}; } @@ -124,16 +91,14 @@ void from_json(const json &j, RiskFactorInfo &p) { j.at("range").get_to(p.range); } -void to_json(json &j, const ModellingInfo &p) { - j = json{{"risk_factors", p.risk_factors}, - {"risk_factor_models", p.risk_factor_models}, - {"baseline_adjustments", p.baseline_adjustment}}; +// SES Model Information +void to_json(json &j, const SESInfo &p) { + j = json{{"function_name", p.function}, {"function_parameters", p.parameters}}; } -void from_json(const json &j, ModellingInfo &p) { - j.at("risk_factors").get_to(p.risk_factors); - j.at("risk_factor_models").get_to(p.risk_factor_models); - j.at("baseline_adjustments").get_to(p.baseline_adjustment); +void from_json(const json &j, SESInfo &p) { + j.at("function_name").get_to(p.function); + j.at("function_parameters").get_to(p.parameters); } void to_json(json &j, const VariableInfo &p) { @@ -158,6 +123,14 @@ void from_json(const json &j, FactorDynamicEquationInfo &p) { j.at("ResidualsStandardDeviation").get_to(p.residuals_standard_deviation); } +// Baseline scenario adjustments +void to_json(json &j, const BaselineInfo &p) { + j = json{{"format", p.format}, + {"delimiter", p.delimiter}, + {"encoding", p.encoding}, + {"file_names", p.file_names}}; +} + // Policy Scenario void to_json(json &j, const PolicyPeriodInfo &p) { j = json{{"start_time", p.start_time}, {"finish_time", p.to_finish_time_str()}}; diff --git a/src/HealthGPS.Console/jsonparser.h b/src/HealthGPS.Console/jsonparser.h index ece133fc6..386e23a4b 100644 --- a/src/HealthGPS.Console/jsonparser.h +++ b/src/HealthGPS.Console/jsonparser.h @@ -1,5 +1,15 @@ +/** + * @file + * @brief Helper functions used by the JSON library to (de)serialise data structures + * + * Configuration file serialisation / de-serialisation mapping specific to the `JSON for + * Modern C++` library adopted by the project. + * + * @sa https://github.com/nlohmann/json#arbitrary-types-conversions for details about + * the contents and code structure in this file. + */ #pragma once -#include "options.h" +#include "poco.h" #include "riskmodel.h" #include @@ -7,12 +17,6 @@ namespace host::poco { /// @brief JSON parser namespace alias. -/// -/// Configuration file serialisation / de-serialisation mapping specific -/// to the `JSON for Modern C++` library adopted by the project. -/// -/// @sa https://github.com/nlohmann/json#arbitrary-types-conversions -/// for details about the contents and code structure in this file. using json = nlohmann::json; //-------------------------------------------------------- @@ -39,7 +43,6 @@ void from_json(const json &j, HierarchicalLevelInfo &p); // Data file information void to_json(json &j, const FileInfo &p); -void from_json(const json &j, FileInfo &p); // Settings Information void to_json(json &j, const SettingsInfo &p); @@ -49,9 +52,8 @@ void from_json(const json &j, SettingsInfo &p); void to_json(json &j, const SESInfo &p); void from_json(const json &j, SESInfo &p); -// Baseline scenario adjustments +// Baseline model information void to_json(json &j, const BaselineInfo &p); -void from_json(const json &j, BaselineInfo &p); // Lite risk factors models (Energy Balance Model) void to_json(json &j, const RiskFactorInfo &p); @@ -60,9 +62,6 @@ void from_json(const json &j, RiskFactorInfo &p); void to_json(json &j, const VariableInfo &p); void from_json(const json &j, VariableInfo &p); -void to_json(json &j, const ModellingInfo &p); -void from_json(const json &j, ModellingInfo &p); - void to_json(json &j, const FactorDynamicEquationInfo &p); void from_json(const json &j, FactorDynamicEquationInfo &p); @@ -85,6 +84,23 @@ void from_json(const json &j, OutputInfo &p); } // namespace host::poco +namespace hgps::core { +using json = nlohmann::json; + +template void to_json(json &j, const Interval &interval) { + j = json::array({interval.lower(), interval.upper()}); +} + +template void from_json(const json &j, Interval &interval) { + const auto vec = j.get>(); + if (vec.size() != 2) { + throw json::type_error::create(302, "Interval arrays must have only two elements", nullptr); + } + + interval = Interval{vec[0], vec[1]}; +} +} // namespace hgps::core + namespace std { // Optional parameters diff --git a/src/HealthGPS.Console/model_parser.cpp b/src/HealthGPS.Console/model_parser.cpp index 650621de5..ba9811223 100644 --- a/src/HealthGPS.Console/model_parser.cpp +++ b/src/HealthGPS.Console/model_parser.cpp @@ -1,4 +1,5 @@ #include "model_parser.h" +#include "configuration_parsing_helpers.h" #include "csvparser.h" #include "jsonparser.h" @@ -21,11 +22,10 @@ namespace host { hgps::BaselineAdjustment load_baseline_adjustments(const poco::BaselineInfo &info) { MEASURE_FUNCTION(); - auto &male_filename = info.file_names.at("factorsmean_male"); - auto &female_filename = info.file_names.at("factorsmean_female"); + const auto male_filename = info.file_names.at("factorsmean_male").string(); + const auto female_filename = info.file_names.at("factorsmean_female").string(); try { - if (hgps::core::case_insensitive::equals(info.format, "CSV")) { auto data = std::map>>{}; @@ -240,20 +240,11 @@ load_newebm_risk_model_definition(const poco::json &opt, const host::Configurati } // Foods nutrition data table. - auto foods_file_info = opt["FoodsDataFile"].get(); - std::filesystem::path file_path = foods_file_info.name; - if (file_path.is_relative()) { - file_path = config.root_path / file_path; - foods_file_info.name = file_path.string(); - } - if (!std::filesystem::exists(file_path)) { - throw std::runtime_error( - fmt::format("Foods nutrition dataset file: {} not found.\n", file_path.string())); - } - auto foods_data_table = load_datatable_from_csv(foods_file_info); + const auto foods_file_info = host::get_file_info(opt["FoodsDataFile"], config.root_path); + const auto foods_data_table = load_datatable_from_csv(foods_file_info); // Load M/F average heights for age. - unsigned int max_age = config.settings.age_range.back(); + const auto max_age = static_cast(config.settings.age_range.upper()); auto male_height = opt["AgeMeanHeight"]["Male"].get>(); auto female_height = opt["AgeMeanHeight"]["Female"].get>(); if (male_height.size() <= max_age) { @@ -289,10 +280,10 @@ load_risk_model_definition(const std::string &model_type, const poco::json &opt, throw std::invalid_argument(fmt::format("Unknown model type: {}", model_type)); } -poco::json load_json(const std::string &model_filename) { - std::ifstream ifs(model_filename, std::ifstream::in); +poco::json load_json(const std::filesystem::path &model_path) { + std::ifstream ifs(model_path, std::ifstream::in); if (!ifs.good()) { - throw std::invalid_argument(fmt::format("Model file: {} not found", model_filename)); + throw std::invalid_argument(fmt::format("Model file: {} not found", model_path.string())); } return poco::json::parse(ifs); @@ -314,15 +305,13 @@ void register_risk_factor_model_definitions(hgps::CachedRepository &repository, } auto adjustment = load_baseline_adjustments(config.modelling.baseline_adjustment); - auto age_range = hgps::core::IntegerInterval(config.settings.age_range.front(), - config.settings.age_range.back()); - auto max_age = static_cast(age_range.upper()); + auto max_age = static_cast(config.settings.age_range.upper()); for (const auto &table : adjustment.values) { for (const auto &item : table.second) { if (item.second.size() <= max_age) { fmt::print(fg(fmt::color::red), "Baseline adjustment files data must cover age range: [{}].\n", - age_range.to_string()); + config.settings.age_range.to_string()); throw std::invalid_argument( "Baseline adjustment file must cover the required age range."); } diff --git a/src/HealthGPS.Console/model_parser.h b/src/HealthGPS.Console/model_parser.h index eb5003584..ec8dc714c 100644 --- a/src/HealthGPS.Console/model_parser.h +++ b/src/HealthGPS.Console/model_parser.h @@ -2,8 +2,8 @@ #include "HealthGPS/repository.h" #include "HealthGPS/riskfactor_adjustment_types.h" +#include "configuration.h" #include "jsonparser.h" -#include "options.h" #include diff --git a/src/HealthGPS.Console/options.h b/src/HealthGPS.Console/options.h deleted file mode 100644 index 14d1124cf..000000000 --- a/src/HealthGPS.Console/options.h +++ /dev/null @@ -1,84 +0,0 @@ -#pragma once -#include "HealthGPS.Core/forward_type.h" -#include "poco.h" -#include - -namespace host { -/// @brief Defines the Command Line Interface (CLI) arguments options -struct CommandOptions { - /// @brief Indicates whether the argument parsing succeed - bool success{}; - - /// @brief The exit code to return, in case of CLI arguments parsing failure - int exit_code{}; - - /// @brief The configuration file argument value - std::filesystem::path config_file{}; - - /// @brief The back-end storage full path argument value - std::filesystem::path storage_folder{}; - - /// @brief Indicates whether the application logging is verbose - bool verbose{}; - - /// @brief The batch job identifier value, optional. - int job_id{}; -}; - -/// @brief Defines the application configuration data structure -struct Configuration { - /// @brief The root path for configuration files - std::filesystem::path root_path; - - /// @brief The input data file details - poco::FileInfo file; - - /// @brief Experiment population settings - poco::SettingsInfo settings; - - /// @brief Socio-economic status (SES) model inputs - poco::SESInfo ses; - - /// @brief User defined model and parameters information - poco::ModellingInfo modelling; - - /// @brief List of diseases to include in experiment - std::vector diseases; - - /// @brief Simulation initialisation custom seed value, optional - std::optional custom_seed; - - /// @brief The experiment start time (simulation clock) - unsigned int start_time{}; - - /// @brief The experiment stop time (simulation clock) - unsigned int stop_time{}; - - /// @brief The number of simulation runs (replications) to execute - unsigned int trial_runs{}; - - /// @brief Baseline to intervention data synchronisation time out (milliseconds) - unsigned int sync_timeout_ms{}; - - /// @brief Indicates whether an alternative intervention policy is active - bool has_active_intervention{false}; - - /// @brief The active intervention policy definition - poco::PolicyScenarioInfo intervention; - - /// @brief Experiment output folder and file information - poco::OutputInfo output; - - /// @brief Application logging verbosity mode - hgps::core::VerboseMode verbosity{}; - - /// @brief Experiment batch job identifier - int job_id{}; - - /// @brief Experiment model name - std::string app_name; - - /// @brief Experiment model version - std::string app_version; -}; -} // namespace host \ No newline at end of file diff --git a/src/HealthGPS.Console/poco.h b/src/HealthGPS.Console/poco.h index 75db4d288..605591b84 100644 --- a/src/HealthGPS.Console/poco.h +++ b/src/HealthGPS.Console/poco.h @@ -1,55 +1,83 @@ #pragma once +#include "HealthGPS.Core/interval.h" + +#include #include #include #include #include #include +/** + * @brief Data structures containing model parameters and configuration options + * + * POCO stands for "plain old class object". These structs represent data structures + * which are contained in JSON-formatted configuration files. + */ namespace host::poco { +//! Information about a data file to be loaded struct FileInfo { - std::string name; + std::filesystem::path name; std::string format; std::string delimiter; - std::string encoding; std::map columns; + + auto operator<=>(const FileInfo &rhs) const = default; }; +//! Experiment's population settings struct SettingsInfo { - std::string country{}; + std::string country; + hgps::core::IntegerInterval age_range; float size_fraction{}; - std::vector age_range; + + auto operator<=>(const SettingsInfo &rhs) const = default; }; +//! Socio-economic status (SES) model inputs struct SESInfo { std::string function; std::vector parameters; + + auto operator<=>(const SESInfo &) const = default; }; +//! Baseline adjustment information struct BaselineInfo { std::string format; std::string delimiter; std::string encoding; - std::map file_names; + std::map file_names; + + auto operator<=>(const BaselineInfo &rhs) const = default; }; +//! Information about a health risk factor struct RiskFactorInfo { std::string name; int level{}; - std::vector range; + std::optional range; + + auto operator<=>(const RiskFactorInfo &rhs) const = default; }; +//! User-defined model and parameter information struct ModellingInfo { std::vector risk_factors; - std::unordered_map risk_factor_models; + std::unordered_map risk_factor_models; BaselineInfo baseline_adjustment; }; +//! Experiment output folder and file information struct OutputInfo { unsigned int comorbidities{}; std::string folder{}; std::string file_name{}; + + auto operator<=>(const OutputInfo &rhs) const = default; }; +//! Information about the period over which a policy is applied struct PolicyPeriodInfo { int start_time{}; std::optional finish_time; @@ -61,8 +89,11 @@ struct PolicyPeriodInfo { return "null"; } + + auto operator<=>(const PolicyPeriodInfo &rhs) const = default; }; +//! Information about policy impacts struct PolicyImpactInfo { std::string risk_factor{}; double impact_value{}; @@ -75,13 +106,19 @@ struct PolicyImpactInfo { return "null"; } + + auto operator<=>(const PolicyImpactInfo &rhs) const = default; }; +//! Extra adjustments made to a policy struct PolicyAdjustmentInfo { std::string risk_factor{}; double value{}; + + auto operator<=>(const PolicyAdjustmentInfo &rhs) const = default; }; +//! Information about an active policy intervention struct PolicyScenarioInfo { std::string identifier{}; PolicyPeriodInfo active_period; @@ -109,5 +146,7 @@ struct PolicyScenarioInfo { return "null"; } + + auto operator<=>(const PolicyScenarioInfo &rhs) const = default; }; } // namespace host::poco \ No newline at end of file diff --git a/src/HealthGPS.Console/program.cpp b/src/HealthGPS.Console/program.cpp index 808901862..506b326ee 100644 --- a/src/HealthGPS.Console/program.cpp +++ b/src/HealthGPS.Console/program.cpp @@ -8,12 +8,35 @@ #include "HealthGPS/event_bus.h" #include "event_monitor.h" +#include #include +#include + +/// @brief Get a string representation of current system time +/// @return The system time as string +std::string get_time_now_str() { + auto tp = std::chrono::system_clock::now(); + return fmt::format("{0:%F %H:%M:}{1:%S} {0:%Z}", tp, tp.time_since_epoch()); +} + +/// @brief Prints application start-up messages +void print_app_title() { + fmt::print(fg(fmt::color::yellow) | fmt::emphasis::bold, + "\n# Health-GPS Microsimulation for Policy Options #\n\n"); + + fmt::print("Today: {}\n\n", get_time_now_str()); +} + /// @brief Prints application exit message /// @param exit_code The application exit code /// @return The respective exit code -int exit_application(int exit_code); +int exit_application(int exit_code) { + fmt::print("\n\n"); + fmt::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Goodbye."); + fmt::print(" {}.\n\n", get_time_now_str()); + return exit_code; +} /// @brief Health-GPS host application entry point /// @param argc The number of command arguments @@ -40,12 +63,23 @@ int main(int argc, char *argv[]) { // NOLINT(bugprone-exception-escape) // Parse inputs configuration file, *.json. Configuration config; try { - config = load_configuration(cmd_args); + config = get_configuration(cmd_args); } catch (const std::exception &ex) { fmt::print(fg(fmt::color::red), "\n\nInvalid configuration - {}.\n", ex.what()); return exit_application(EXIT_FAILURE); } + // Create output folder + if (!std::filesystem::exists(config.output.folder)) { + fmt::print(fg(fmt::color::dark_salmon), "\nCreating output folder: {} ...\n", + config.output.folder); + if (!std::filesystem::create_directories(config.output.folder)) { + fmt::print(fg(fmt::color::red), "Failed to create output folder: {}\n", + config.output.folder); + return exit_application(EXIT_FAILURE); + } + } + // Load input data file into a datatable asynchronous auto table_future = core::run_async(load_datatable_from_csv, config.file); @@ -104,12 +138,12 @@ int main(int argc, char *argv[]) { // NOLINT(bugprone-exception-escape) fmt::print(fg(fmt::color::cyan), "\nStarting baseline simulation with {} trials ...\n\n", config.trial_runs); auto baseline_sim = create_baseline_simulation(channel, factory, event_bus, model_input); - if (config.has_active_intervention) { + if (config.active_intervention.has_value()) { fmt::print(fg(fmt::color::cyan), "\nStarting intervention simulation with {} trials ...\n", config.trial_runs); - auto policy_sim = create_intervention_simulation(channel, factory, event_bus, - model_input, config.intervention); + auto policy_sim = create_intervention_simulation( + channel, factory, event_bus, model_input, config.active_intervention.value()); // Run simulations side by side on a background thread auto worker = @@ -157,18 +191,8 @@ int main(int argc, char *argv[]) { // NOLINT(bugprone-exception-escape) return exit_application(EXIT_SUCCESS); } -int exit_application(int exit_code) { - fmt::print("\n\n"); - fmt::print(fg(fmt::color::yellow) | fmt::emphasis::bold, "Goodbye."); - fmt::print(" {}.\n\n", host::get_time_now_str()); - return exit_code; -} - /// @brief Top-level namespace for Health-GPS Console host application namespace host { /// @brief Internal details namespace for private data types and functions namespace detail {} - -/// @brief Plain old class object (POCO) types for loading configuration file -namespace poco {} } // namespace host \ No newline at end of file diff --git a/src/HealthGPS.Core/CMakeLists.txt b/src/HealthGPS.Core/CMakeLists.txt index 9c78f4c8b..f0dbc0ffa 100644 --- a/src/HealthGPS.Core/CMakeLists.txt +++ b/src/HealthGPS.Core/CMakeLists.txt @@ -15,8 +15,6 @@ target_sources(HealthGPS.Core "univariate_summary.h" "math_util.cpp" "math_util.h" - "version.cpp" - "version.h" "identifier.cpp" "identifier.h" diff --git a/src/HealthGPS.Core/api.h b/src/HealthGPS.Core/api.h index e0178fe9a..5d5ff4942 100644 --- a/src/HealthGPS.Core/api.h +++ b/src/HealthGPS.Core/api.h @@ -10,7 +10,6 @@ #include "interval.h" #include "poco.h" #include "univariate_summary.h" -#include "version.h" #include "visitor.h" namespace hgps { diff --git a/src/HealthGPS.Core/identifier.cpp b/src/HealthGPS.Core/identifier.cpp index 587050d46..668b06c31 100644 --- a/src/HealthGPS.Core/identifier.cpp +++ b/src/HealthGPS.Core/identifier.cpp @@ -12,7 +12,7 @@ Identifier Identifier::empty() { Identifier::Identifier(std::string value) : value_{to_lower(value)} { if (!value_.empty()) { - validate_identifeir(); + validate_identifier(); } hash_code_ = std::hash{}(value_); @@ -40,7 +40,7 @@ bool Identifier::equal(const Identifier &other) const noexcept { return hash_code_ == other.hash_code_; } -void Identifier::validate_identifeir() const { +void Identifier::validate_identifier() const { if (std::isdigit(value_.at(0))) { throw std::invalid_argument("Identifier must not start with a numeric value"); } @@ -58,10 +58,4 @@ std::ostream &operator<<(std::ostream &stream, const Identifier &identifier) { void from_json(const nlohmann::json &j, Identifier &id) { id = Identifier{j.get()}; } -void from_json(const nlohmann::json &j, std::map &map) { - for (auto &[key, value] : j.items()) { - map.emplace(key, value.get()); - } -} - } // namespace hgps::core diff --git a/src/HealthGPS.Core/identifier.h b/src/HealthGPS.Core/identifier.h index 63b84b4e3..2d970270c 100644 --- a/src/HealthGPS.Core/identifier.h +++ b/src/HealthGPS.Core/identifier.h @@ -1,7 +1,9 @@ #pragma once #include +#include #include #include +#include #include "nlohmann/json.hpp" @@ -80,16 +82,33 @@ struct Identifier final { std::string value_{}; std::size_t hash_code_{std::hash{}("")}; - void validate_identifeir() const; + void validate_identifier() const; }; void from_json(const nlohmann::json &j, Identifier &id); -void from_json(const nlohmann::json &j, std::map &map); +namespace detail { +template