diff --git a/include/realizations/catchment/Formulation_Constructors.hpp b/include/realizations/catchment/Formulation_Constructors.hpp index a5cf348f04..640c3abbbb 100644 --- a/include/realizations/catchment/Formulation_Constructors.hpp +++ b/include/realizations/catchment/Formulation_Constructors.hpp @@ -45,6 +45,14 @@ namespace realization { #endif // ACTIVATE_PYTHON }; + static std::string valid_formulation_keys(){ + std::string keys = ""; + for(const auto& kv : formulations){ + keys.append(kv.first+" "); + } + return keys; + } + static bool formulation_exists(std::string formulation_type) { return formulations.count(formulation_type) > 0; } diff --git a/include/realizations/catchment/Formulation_Manager.hpp b/include/realizations/catchment/Formulation_Manager.hpp index 9866aa58fe..8b01f933b8 100644 --- a/include/realizations/catchment/Formulation_Manager.hpp +++ b/include/realizations/catchment/Formulation_Manager.hpp @@ -19,6 +19,7 @@ #include "LayerData.hpp" #include "realizations/config/time.hpp" #include "realizations/config/routing.hpp" +#include "realizations/config/config.hpp" namespace realization { @@ -58,31 +59,9 @@ namespace realization { auto possible_global_config = tree.get_child_optional("global"); if (possible_global_config) { - this->global_formulation_tree = *possible_global_config; - - //get forcing info - for (auto &forcing_parameter : (*possible_global_config).get_child("forcing")) { - this->global_forcing.emplace( - forcing_parameter.first, - geojson::JSONProperty(forcing_parameter.first, forcing_parameter.second) - ); - } - - //get first empty key under formulations (corresponds to first json array element) - auto formulation = (*possible_global_config).get_child("formulations.."); - - for (std::pair global_setting : formulation.get_child("params")) { - this->global_formulation_parameters.emplace( - global_setting.first, - geojson::JSONProperty(global_setting.first, global_setting.second) - ); - } + global_config = realization::config::Config(*possible_global_config); } - /** - * Read simulation time from configuration file - * /// \todo TODO: Separate input_interval from output_interval - */ auto possible_simulation_time = tree.get_child_optional("time"); if (!possible_simulation_time) { @@ -186,46 +165,24 @@ namespace realization { #endif continue; } + realization::config::Config catchment_formulation(catchment_config.second); - decltype(auto) formulations = catchment_config.second.get_child_optional("formulations"); - if( !formulations ) { + if(!catchment_formulation.has_formulation()){ throw std::runtime_error("ERROR: No formulations defined for "+catchment_config.first+"."); } - // Parse catchment-specific model_params auto catchment_feature = fabric->get_feature(catchment_index); - - for (auto &formulation: *formulations) { - // Handle single-bmi - decltype(auto) model_params = formulation.second.get_child_optional("params.model_params"); - if (model_params) { - parse_external_model_params(*model_params, catchment_feature); - } - - // Handle multi-bmi - // FIXME: this will not handle doubly nested multi-BMI configs, - // might need a recursive helper here? - decltype(auto) nested_modules = formulation.second.get_child_optional("params.modules"); - if (nested_modules) { - for (decltype(auto) nested_formulation : *nested_modules) { - decltype(auto) nested_model_params = nested_formulation.second.get_child_optional("params.model_params"); - if (nested_model_params) { - parse_external_model_params(*nested_model_params, catchment_feature); - } - } - } - - this->add_formulation( - this->construct_formulation_from_tree( - simulation_time_config, - catchment_config.first, - catchment_config.second, - formulation.second, - output_stream - ) - ); - break; //only construct one for now FIXME - } //end for formulaitons + catchment_formulation.formulation.link_external(catchment_feature); + this->add_formulation( + this->construct_formulation_from_config( + simulation_time_config, + catchment_config.first, + catchment_formulation, + output_stream + ) + ); + // break; //only construct one for now FIXME + // } //end for formulaitons }//end for catchments @@ -328,35 +285,24 @@ namespace realization { protected: - std::shared_ptr construct_formulation_from_tree( + std::shared_ptr construct_formulation_from_config( simulation_time_params &simulation_time_config, std::string identifier, - boost::property_tree::ptree &tree, - const boost::property_tree::ptree &formulation, + const realization::config::Config& catchment_formulation, utils::StreamHandler output_stream ) { - auto params = formulation.get_child("params"); - std::string formulation_type_key; - try { - formulation_type_key = get_formulation_key(formulation); - } - catch(std::exception& e) { - throw std::runtime_error("Catchment " + identifier + " failed initialization: " + e.what()); + if(!formulation_exists(catchment_formulation.formulation.type)){ + throw std::runtime_error("Catchment " + identifier + " failed initialization: " + + catchment_formulation.formulation.type + "is not a valid formulation. Options are: "+valid_formulation_keys()); } - boost::property_tree::ptree formulation_config = formulation.get_child("params"); - - auto possible_forcing = tree.get_child_optional("forcing"); - - if (!possible_forcing) { + if(catchment_formulation.forcing.parameters.empty()){ throw std::runtime_error("No forcing definition was found for " + identifier); } - geojson::JSONProperty forcing_parameters("forcing", *possible_forcing); - std::vector missing_parameters; - if (!forcing_parameters.has_key("path")) { + if (!catchment_formulation.forcing.has_key("path")) { missing_parameters.push_back("path"); } @@ -374,71 +320,37 @@ namespace realization { throw std::runtime_error(message); } - geojson::PropertyMap local_forcing; - for (auto &forcing_parameter : *possible_forcing) { - local_forcing.emplace( - forcing_parameter.first, - geojson::JSONProperty(forcing_parameter.first, forcing_parameter.second) - ); - } - - forcing_params forcing_config = this->get_forcing_params(local_forcing, identifier, simulation_time_config); - - std::shared_ptr constructed_formulation = construct_formulation(formulation_type_key, identifier, forcing_config, output_stream); + forcing_params forcing_config = this->get_forcing_params(catchment_formulation.forcing.parameters, identifier, simulation_time_config); + std::shared_ptr constructed_formulation = construct_formulation(catchment_formulation.formulation.type, identifier, forcing_config, output_stream); //, geometry); - constructed_formulation->create_formulation(formulation_config, &global_formulation_parameters); + + constructed_formulation->create_formulation(catchment_formulation.formulation.parameters); return constructed_formulation; } std::shared_ptr construct_missing_formulation(geojson::Feature& feature, utils::StreamHandler output_stream, simulation_time_params &simulation_time_config){ const std::string identifier = feature->get_id(); - - std::string formulation_type_key = get_formulation_key(global_formulation_tree.get_child("formulations..")); - - forcing_params forcing_config = this->get_forcing_params(this->global_forcing, identifier, simulation_time_config); - - std::shared_ptr missing_formulation = construct_formulation(formulation_type_key, identifier, forcing_config, output_stream); + + forcing_params forcing_config = this->get_forcing_params(global_config.forcing.parameters, identifier, simulation_time_config); + std::shared_ptr missing_formulation = construct_formulation(global_config.formulation.type, identifier, forcing_config, output_stream); // Need to work with a copy, since it is altered in-place - geojson::PropertyMap global_properties_copy = global_formulation_parameters; - Catchment_Formulation::config_pattern_substitution(global_properties_copy, + realization::config::Config global_copy = global_config; + Catchment_Formulation::config_pattern_substitution(global_copy.formulation.parameters, BMI_REALIZATION_CFG_PARAM_REQ__INIT_CONFIG, "{{id}}", identifier); - - // parse any external model parameters in this config - - // handle single-bmi - if (global_properties_copy.count("model_params") > 0) { - decltype(auto) model_params = global_properties_copy.at("model_params"); - geojson::PropertyMap model_params_copy = model_params.get_values(); - parse_external_model_params(model_params_copy, feature); - global_properties_copy.at("model_params") = geojson::JSONProperty("model_params", model_params_copy); - } + //Some helpful debugging prints, commented out, but left for later + //because they will eventually be used by someone, someday, looking at configurations + //being turned into concrecte formulations... + // geojson::JSONProperty::print_property(global_config.formulation.parameters.at("modules")); + global_config.formulation.link_external(feature); + // geojson::JSONProperty::print_property(global_config.formulation.parameters.at("modules")); + missing_formulation->create_formulation(global_config.formulation.parameters); - // handle multi-bmi - // FIXME: this seems inefficient -- is there a better way? - if (global_properties_copy.count("modules") > 0) { - decltype(auto) nested_modules = global_properties_copy.at("modules").as_list(); - for (auto& bmi_module : nested_modules) { - geojson::PropertyMap module_def = bmi_module.get_values(); - geojson::PropertyMap module_params = module_def.at("params").get_values(); - if (module_params.count("model_params") > 0) { - decltype(auto) model_params = module_params.at("model_params"); - geojson::PropertyMap model_params_copy = model_params.get_values(); - parse_external_model_params(model_params_copy, feature); - module_params.at("model_params") = geojson::JSONProperty("model_params", model_params_copy); - } - module_def.at("params") = geojson::JSONProperty("params", module_params); - bmi_module = geojson::JSONProperty("", module_def); - } - global_properties_copy.at("modules") = geojson::JSONProperty("", nested_modules); - } - - missing_formulation->create_formulation(global_properties_copy); return missing_formulation; } - forcing_params get_forcing_params(geojson::PropertyMap &forcing_prop_map, std::string identifier, simulation_time_params &simulation_time_config) { - std::string path; + forcing_params get_forcing_params(const geojson::PropertyMap &forcing_prop_map, std::string identifier, simulation_time_params &simulation_time_config) { + std::string path = ""; if(forcing_prop_map.count("path") != 0){ path = forcing_prop_map.at("path").as_string(); } @@ -703,11 +615,7 @@ namespace realization { boost::property_tree::ptree tree; - boost::property_tree::ptree global_formulation_tree; - - geojson::PropertyMap global_formulation_parameters; - - geojson::PropertyMap global_forcing; + realization::config::Config global_config; std::map> formulations; diff --git a/include/realizations/config/config.hpp b/include/realizations/config/config.hpp new file mode 100644 index 0000000000..6b47aba5c2 --- /dev/null +++ b/include/realizations/config/config.hpp @@ -0,0 +1,60 @@ +#ifndef NGEN_REALIZATION_CONFIG_H +#define NGEN_REALIZATION_CONFIG_H + +#include + +#include "formulation.hpp" +#include "forcing.hpp" + +namespace realization{ + namespace config{ + + /** + * @brief Structure representing the configuration for a general Formulation. + * + */ + struct Config{ + + /** + * @brief Construct a new Config object + * + */ + Config() = default; + + /** + * @brief Construct a new Config object from a property tree + * + * @param tree + */ + Config(const boost::property_tree::ptree& tree){ + + auto possible_forcing = tree.get_child_optional("forcing"); + + if (possible_forcing) { + forcing = Forcing(*possible_forcing); + } + //get first empty key under formulations (corresponds to first json array element) + auto possible_formulation_tree = tree.get_child_optional("formulations.."); + if(possible_formulation_tree){ + formulation = Formulation(*possible_formulation_tree); + } + } + + /** + * @brief Determine if the config has a formulation + * + * @return true if the formulation name/type is set or if model parameters are present + * @return false if either the type or parameters are empty + */ + bool has_formulation(){ + return !(formulation.type.empty() || formulation.parameters.empty()); + } + + Formulation formulation; + Forcing forcing; + }; + + + };//end namespace config +}//end namespace realization +#endif //NGEN_REALIZATION_CONFIG_H diff --git a/include/realizations/config/forcing.hpp b/include/realizations/config/forcing.hpp new file mode 100644 index 0000000000..9d0f24fe76 --- /dev/null +++ b/include/realizations/config/forcing.hpp @@ -0,0 +1,58 @@ +#ifndef NGEN_REALIZATION_CONFIG_FORCING_H +#define NGEN_REALIZATION_CONFIG_FORCING_H + +#include + +#include "JSONProperty.hpp" + +namespace realization{ + namespace config{ + + /** + * @brief Structure for holding forcing configuration information + * + */ + struct Forcing{ + /** + * @brief key -> Property mapping for forcing parameters + * + */ + geojson::PropertyMap parameters; + + /** + * @brief Construct a new, empty Forcing object + * + */ + Forcing():parameters(geojson::PropertyMap()){}; + + /** + * @brief Construct a new Forcing object from a property_tree + * + * @param tree + */ + Forcing(const boost::property_tree::ptree& tree){ + //get forcing info + for (auto &forcing_parameter : tree) { + this->parameters.emplace( + forcing_parameter.first, + geojson::JSONProperty(forcing_parameter.first, forcing_parameter.second) + ); + } + } + + /** + * @brief Test if a particualr forcing parameter exists + * + * @param key parameter name to test + * @return true if the forcing properties contain key + * @return false if the key is not in the forcing properties + */ + bool has_key(const std::string& key) const{ + return parameters.count(key) > 0; + } + }; + + + };//end namespace config +}//end namespace realization +#endif //NGEN_REALIZATION_CONFIG_FORCING_H diff --git a/include/realizations/config/formulation.hpp b/include/realizations/config/formulation.hpp new file mode 100644 index 0000000000..4b80e19303 --- /dev/null +++ b/include/realizations/config/formulation.hpp @@ -0,0 +1,135 @@ +#ifndef NGEN_REALIZATION_CONFIG_FORMULATION_H +#define NGEN_REALIZATION_CONFIG_FORMULATION_H + +#include +#include + +#include "JSONProperty.hpp" + +namespace realization{ + namespace config{ + + struct Formulation{ + std::string type; + //Formulation parameters, object as a PropertyMap + geojson::PropertyMap parameters; + //List of nested formulations (used for multi bmi representations) + std::vector nested; + + /** + * @brief Construct a new default Formulation object + * + * Default objects have an "" type and and empty property map. + */ + Formulation() = default; + + /** + * @brief Construct a new Formulation object + * + * @param type formulation type represented + * @param params formulation parameter mapping + */ + Formulation(std::string type, geojson::PropertyMap params):type(std::move(type)), parameters(params){} + + /** + * @brief Construct a new Formulation object from a boost property tree + * + * The tree should have a "name" key corresponding to the formulation type + * as well as a "params" key to build the property map from + * + * @param tree property tree to build Formulation from + */ + Formulation(const boost::property_tree::ptree& tree){ + type = tree.get("name"); + for (std::pair setting : tree.get_child("params")) { + //Construct the geoJSON PropertyMap from each key, value pair in "params" + parameters.emplace( + setting.first, + geojson::JSONProperty(setting.first, setting.second) + ); + } + if(type=="bmi_multi"){ + for(auto& module : tree.get_child("params.modules")){ + //Create the nested formulations in order of definition + nested.push_back(Formulation(module.second)); + } + geojson::JSONProperty::print_property(parameters.at("modules")); + } + + } + + /** + * @brief Link formulation parameters to hydrofabric data held in feature + * + * @param feature Hydrofabric feature with properties to assign to formulation + * model params + */ + void link_external(geojson::Feature feature){ + + if(type == "bmi_multi"){ + std::vector tmp; + for(auto& n : nested){ + //Iterate and link any nested modules with this feature + if(n.parameters.count("model_params")){ + n.link_external(feature); + } + //Need a temporary map to hold the updated formulation properties in + geojson::PropertyMap map = {}; + map.emplace("name", geojson::JSONProperty("name", n.type)); + map.emplace("params", geojson::JSONProperty("", n.parameters)); + tmp.push_back(geojson::JSONProperty("", map)); + } + //Reset the bmi_multi modules with the now linked module definitions + parameters.at("modules") = geojson::JSONProperty("modules", tmp); + return; + } + //Short circut + if(parameters.count("model_params") < 1 ) return; + //Have some model params, check to see if any should be linked to the hyrdofabric feature + geojson::PropertyMap attr = parameters.at("model_params").get_values(); + for (decltype(auto) param : attr) { + // Check for type to short-circuit. If param.second is not an object, `.has_key()` will throw + if (param.second.get_type() != geojson::PropertyType::Object || !param.second.has_key("source")) { + attr.emplace(param.first, param.second); + continue; + } + + decltype(auto) param_source = param.second.at("source"); + decltype(auto) param_source_name = param_source.as_string(); + if (param_source_name != "hydrofabric") { + // TODO: temporary until the logic for alternative sources is designed + throw std::logic_error("ERROR: 'model_params' source `" + param_source_name + "` not currently supported. Only `hydrofabric` is supported."); + } + + // Property name in the feature properties is either + // the value of key "from", or has the same name as + // the expected model parameter key + decltype(auto) param_name = param.second.has_key("from") + ? param.second.at("from").as_string() + : param.first; + + if (feature->has_property(param_name)) { + auto catchment_attribute = feature->get_property(param_name); + + // Use param.first in the `.emplace` calls instead of param_name since + // the expected name is given by the key of the model_params values. + switch (catchment_attribute.get_type()) { + case geojson::PropertyType::List: + case geojson::PropertyType::Object: + // TODO: Should list/object values be passed to model parameters? + // Typically, feature properties *should* be scalars. + std::cerr << "WARNING: property type " << static_cast(catchment_attribute.get_type()) << " not allowed as model parameter. " + << "Must be one of: Natural (int), Real (double), Boolean, or String" << '\n'; + break; + default: + attr.at(param.first) = geojson::JSONProperty(param.first, catchment_attribute); + } + } + } + parameters.at("model_params") = geojson::JSONProperty("model_params", attr); + } + }; + + };//end namespace config +}//end namespace realization +#endif //NGEN_REALIZATION_CONFIG_FORMULATION_H diff --git a/test/realizations/Formulation_Manager_Test.cpp b/test/realizations/Formulation_Manager_Test.cpp index c062c4717c..46952aac3e 100644 --- a/test/realizations/Formulation_Manager_Test.cpp +++ b/test/realizations/Formulation_Manager_Test.cpp @@ -602,6 +602,7 @@ const std::string EXAMPLE_5_a = " \"init_config\": \"\"," " \"allow_exceed_end_time\": true," " \"main_output_variable\": \"OUTPUT_VAR_4\"," +" \"uses_forcing_file\": false," " \"modules\": [" " {" " \"name\": \"bmi_c++\","