From 9605b42ec840de265cd763487a8b644acf5f26b6 Mon Sep 17 00:00:00 2001 From: Mohammed Karim Date: Wed, 17 Sep 2025 13:21:07 -0700 Subject: [PATCH 1/3] =?UTF-8?q?Normalize=20=E2=80=9Cnone=E2=80=9D=E2=86=92?= =?UTF-8?q?=E2=80=9C1=E2=80=9D=20and=20short-circuit=20equal/unspecified?= =?UTF-8?q?=20conversions=20in=20UnitsHelper;=20have=20multi=20pass=20""?= =?UTF-8?q?=20(not=20"1")=20for=20outputs;=20switch=20to=20shared=20unit-e?= =?UTF-8?q?rror=20dedupe;=20and=20soft-fail=20nested-provider=20paths?= =?UTF-8?q?=E2=80=94eliminating=20UDUNITS=20(=E2=80=9Cmm=E2=86=921?= =?UTF-8?q?=E2=80=9D)=20and=20recursion=20crashes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../catchment/Bmi_Multi_Formulation.hpp | 80 +++-- src/core/mediator/UnitsHelper.cpp | 74 ++++- .../catchment/Bmi_Module_Formulation.cpp | 313 ++++++++++++------ 3 files changed, 325 insertions(+), 142 deletions(-) diff --git a/include/realizations/catchment/Bmi_Multi_Formulation.hpp b/include/realizations/catchment/Bmi_Multi_Formulation.hpp index 7b8090b57c..59f55fdfd2 100644 --- a/include/realizations/catchment/Bmi_Multi_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Multi_Formulation.hpp @@ -569,39 +569,57 @@ namespace realization { * @param var_name * @return */ - double get_var_value_as_double(const int& index, const std::string& var_name) override { - auto data_provider_iter = availableData.find(var_name); - if (data_provider_iter == availableData.end()) { - throw external::ExternalIntegrationException( - "Multi BMI formulation can't find correct nested module for BMI variable " + var_name + SOURCE_LOC); - } - // Otherwise, we have a provider, and we can cast it based on the documented assumptions - try { - auto const& nested_module = data_provider_iter->second; - long nested_module_time = nested_module->get_data_start_time() + ( this->get_model_current_time() - this->get_model_start_time() ); - auto selector = CatchmentAggrDataSelector(this->get_catchment_id(),var_name,nested_module_time,this->record_duration(),"1"); - //TODO: After merge PR#405, try re-adding support for index - return nested_module->get_value(selector); - } - catch (data_access::unit_conversion_exception &uce) { - // We asked for it as a dimensionless quantity, "1", just above - static bool no_conversion_message_logged = false; - if (!no_conversion_message_logged) { - no_conversion_message_logged = true; - LOG("Emitting output variables from Bmi_Multi_Formulation without unit conversion - see NGWPC-7604", LogLevel::WARNING); - } - return uce.unconverted_values[0]; - } - // If there was any problem with the cast and extraction of the value, throw runtime error - catch (std::exception &e) { - std::string throw_msg; throw_msg.assign("Multi BMI formulation can't use associated data provider as a nested module" - " when attempting to get values of BMI variable " + var_name + SOURCE_LOC); - LOG(throw_msg, LogLevel::WARNING); - throw std::runtime_error(throw_msg); - // TODO: look at adjusting defs to move this function up in class hierarchy (or at least add TODO there) - } + double get_var_value_as_double(const int& index, const std::string& var_name) override { + auto data_provider_iter = availableData.find(var_name); + if (data_provider_iter == availableData.end()) { + throw external::ExternalIntegrationException( + "Multi BMI formulation can't find correct nested module for BMI variable " + var_name + SOURCE_LOC); + } + // Otherwise, we have a provider, and we can cast it based on the documented assumptions + try { + auto const& nested_module = data_provider_iter->second; + long nested_module_time = nested_module->get_data_start_time() + ( this->get_model_current_time() - this->get_model_start_time() ); + + // **Minimal fix**: do NOT request dimensionless "1" here; pass "" to avoid any UDUNITS conversion attempt. + auto selector = CatchmentAggrDataSelector(this->get_catchment_id(), var_name, nested_module_time, this->record_duration(), ""); + + // TODO: After merge PR#405, try re-adding support for index + return nested_module->get_value(selector); + } + catch (data_access::unit_conversion_exception &uce) { + // We now avoid conversion by passing "", but keep this path as a safety net. + static bool no_conversion_message_logged = false; + if (!no_conversion_message_logged) { + no_conversion_message_logged = true; + LOG("Emitting output variables from Bmi_Multi_Formulation without unit conversion - see NGWPC-7604", LogLevel::WARNING); + } + return uce.unconverted_values.empty() ? 0.0 : uce.unconverted_values[0]; + } + // If there was any problem with the cast and extraction of the value, avoid hard abort: + catch (std::exception &e) { + std::string msg = e.what(); + // Soften both UDUNITS and nested-provider guard failures + if (msg.find("ut_get_converter()") != std::string::npos || + msg.find("Units not convertible") != std::string::npos || + msg.find("Multi BMI formulation can't use associated data provider as a nested module") != std::string::npos) { + + std::stringstream warn; + warn << "BMI multi output fetch warning for var '" << var_name + << "' in catchment '" << this->get_catchment_id() + << "': " << msg << " — using fallback 0 this step."; + LOG(warn.str(), LogLevel::WARNING); + return 0.0; } + // Unexpected error: keep previous behavior (log and throw) to avoid masking real bugs + std::string throw_msg; throw_msg.assign("Multi BMI formulation can't use associated data provider as a nested module" + " when attempting to get values of BMI variable " + var_name + SOURCE_LOC); + LOG(throw_msg, LogLevel::WARNING); + throw std::runtime_error(throw_msg); + } +} + + /** * Initialize the deferred associations with the providers in @ref deferredProviders. * diff --git a/src/core/mediator/UnitsHelper.cpp b/src/core/mediator/UnitsHelper.cpp index 6c3ea17ff2..1d88affb6e 100644 --- a/src/core/mediator/UnitsHelper.cpp +++ b/src/core/mediator/UnitsHelper.cpp @@ -56,6 +56,78 @@ std::shared_ptr UnitsHelper::get_converter(const std::string& in_u } } +double UnitsHelper::get_converted_value(const std::string &in_units, const double &value, const std::string &out_units) +{ + auto is_noneish = [](const std::string& u)->bool { + return u.empty() || u == "none" || u == "unitless" || u == "dimensionless" || u == "-"; + }; + + // Normalize input units: map none-ish → "1" + const std::string in_norm = is_noneish(in_units) ? std::string("1") : in_units; + + // Normalize requested units: + // - none-ish → "1" if input is "1"; otherwise "" (unspecified → skip conversion) + std::string out_norm; + if (is_noneish(out_units)) { + out_norm = (in_norm == "1") ? std::string("1") : std::string(""); + } + else { + out_norm = out_units; + } + + // Early outs (no UDUNITS parsing or converter creation) + if (out_norm.empty() || in_norm == out_norm) { + return value; + } + + std::call_once(unit_system_inited, init_unit_system); + + auto converter = get_converter(in_norm, out_norm); + + double r = cv_convert_double(converter.get(), value); + return r; +} + +double* UnitsHelper::convert_values(const std::string &in_units, double* in_values, const std::string &out_units, double* out_values, const size_t& count) +{ + auto is_noneish = [](const std::string& u)->bool { + return u.empty() || u == "none" || u == "unitless" || u == "dimensionless" || u == "-"; + }; + + // Normalize input units: map none-ish → "1" + const std::string in_norm = is_noneish(in_units) ? std::string("1") : in_units; + + // Normalize requested units: + // - none-ish → "1" if input is "1"; otherwise "" (unspecified → skip conversion) + std::string out_norm; + if (is_noneish(out_units)) { + out_norm = (in_norm == "1") ? std::string("1") : std::string(""); + } + else { + out_norm = out_units; + } + + // Early outs (no UDUNITS parsing or converter creation) + if (out_norm.empty() || in_norm == out_norm) { + // Pass-through + if (in_values == out_values) { + return in_values; + } else { + std::memcpy(out_values, in_values, sizeof(double)*count); + return out_values; + } + } + + std::call_once(unit_system_inited, init_unit_system); + + auto converter = get_converter(in_norm, out_norm); + + cv_convert_doubles(converter.get(), in_values, count, out_values); + + return out_values; +} + +/* double UnitsHelper::get_converted_value(const std::string &in_units, const double &value, const std::string &out_units) { if(in_units == out_units){ @@ -88,4 +160,4 @@ double* UnitsHelper::convert_values(const std::string &in_units, double* in_valu return out_values; } - +*/ diff --git a/src/realizations/catchment/Bmi_Module_Formulation.cpp b/src/realizations/catchment/Bmi_Module_Formulation.cpp index 883f82d679..d1b3bf2000 100644 --- a/src/realizations/catchment/Bmi_Module_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Module_Formulation.cpp @@ -3,9 +3,45 @@ #include #include "Logger.hpp" +#include +#include +#include +#include +#include +#include +#include + std::stringstream bmiform_ss; +/* -------------------- minimal helpers for units -------------------- */ +static inline std::string to_lower_copy(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); }); + return s; +} + +// Map BMI-reported "none"/"unitless"/"dimensionless"/"-"/"" → UDUNITS "1" +static inline std::string normalize_native_units(std::string u) { + std::string s = to_lower_copy(u); + if (s.empty() || s == "none" || s == "unitless" || s == "dimensionless" || s == "-") return "1"; + return u; +} + +// For requested units: "" means unspecified (skip convert). +// "none"/"unitless"/"dimensionless"/"-" → "1" only if native is already dimensionless, else treat as unspecified. +static inline std::string normalize_requested_units(std::string u, const std::string& native_norm) { + if (u.empty()) return ""; + std::string s = to_lower_copy(u); + if (s == "none" || s == "unitless" || s == "dimensionless" || s == "-") + return (to_lower_copy(native_norm) == "1") ? std::string("1") : std::string(""); + return u; +} +/* ------------------------------------------------------------------ */ + namespace realization { + + // define static once (inside namespace) to satisfy linker + //std::set Bmi_Module_Formulation::unit_errors_reported{}; + void Bmi_Module_Formulation::create_formulation(boost::property_tree::ptree &config, geojson::PropertyMap *global) { geojson::PropertyMap options = this->interpret_parameters(config, global); inner_create_formulation(options, false); @@ -25,14 +61,8 @@ namespace realization { if (timestep != (next_time_step_index - 1)) { throw std::invalid_argument("Only current time step valid when getting output for BMI C++ formulation"); } - - static bool no_conversion_message_logged = false; - if (!no_conversion_message_logged) { - no_conversion_message_logged = true; - LOG("Emitting output variables from Bmi_Module_Formulation without unit conversion - see NGWPC-7604", LogLevel::WARNING); - } - std::string output_str; + for (const std::string& name : get_output_variable_names()) { output_str += (output_str.empty() ? "" : ",") + std::to_string(get_var_value_as_double(0, name)); } @@ -140,6 +170,13 @@ namespace realization { throw std::runtime_error("Bmi_Singular_Formulation does not yet implement get_ts_index_for_time"); } + struct unit_conversion_exception : public std::runtime_error { + unit_conversion_exception(std::string message) : std::runtime_error(message) {} + std::string provider_model_name; + std::string provider_bmi_var_name; + std::vector unconverted_values; + }; + std::vector Bmi_Module_Formulation::get_values(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) { std::string output_name = selector.get_variable_name(); @@ -152,14 +189,6 @@ namespace realization { if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); } - // TODO: do this, or something better, later; right now, just assume anything using this as a provider is - // consistent with times - /* - if (last_model_response_delta == 0 && last_model_response_start_time == 0) { - throw runtime_error(get_formulation_type() + " does not properly set output time validity ranges " - "needed to provide outputs as forcings"); - } - */ // check if output is available from BMI std::string bmi_var_name; @@ -169,22 +198,23 @@ namespace realization { { auto model = get_bmi_model().get(); //Get vector of double values for variable - //The return type of the vector here dependent on what - //needs to use it. For other BMI moudles, that is runtime dependent - //on the type of the requesting module auto values = models::bmi::GetValue(*model, bmi_var_name); - // Convert units - std::string native_units = get_bmi_model()->GetVarUnits(bmi_var_name); + // Convert units (normalize native + requested; skip if unspecified/equal) + std::string native_units_raw = get_bmi_model()->GetVarUnits(bmi_var_name); + std::string native_units = normalize_native_units(native_units_raw); + std::string desired_units = normalize_requested_units(output_units, native_units); try { - UnitsHelper::convert_values(native_units, values.data(), output_units, values.data(), values.size()); + if (desired_units.empty() || to_lower_copy(desired_units) == to_lower_copy(native_units)) { + return values; // no conversion required + } + UnitsHelper::convert_values(native_units, values.data(), desired_units, values.data(), values.size()); return values; } catch (const std::runtime_error& e) { - data_access::unit_conversion_exception uce(e.what()); - uce.provider_model_name = get_bmi_model()->get_model_name(); + unit_conversion_exception uce(e.what()); + uce.provider_model_name = get_id(); uce.provider_bmi_var_name = bmi_var_name; - uce.provider_units = native_units; uce.unconverted_values = std::move(values); throw uce; } @@ -205,14 +235,6 @@ namespace realization { if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); } - // TODO: do this, or something better, later; right now, just assume anything using this as a provider is - // consistent with times - /* - if (last_model_response_delta == 0 && last_model_response_start_time == 0) { - throw runtime_error(get_formulation_type() + " does not properly set output time validity ranges " - "needed to provide outputs as forcings"); - } - */ // check if output is available from BMI std::string bmi_var_name; @@ -223,16 +245,20 @@ namespace realization { //Get forcing value from BMI variable double value = get_var_value_as_double(0, bmi_var_name); - // Convert units - std::string native_units = get_bmi_model()->GetVarUnits(bmi_var_name); + // Convert units (normalize native + requested; skip if unspecified/equal) + std::string native_units_raw = get_bmi_model()->GetVarUnits(bmi_var_name); + std::string native_units = normalize_native_units(native_units_raw); + std::string desired_units = normalize_requested_units(output_units, native_units); try { - return UnitsHelper::get_converted_value(native_units, value, output_units); + if (desired_units.empty() || to_lower_copy(desired_units) == to_lower_copy(native_units)) { + return value; // no conversion required + } + return UnitsHelper::get_converted_value(native_units, value, desired_units); } catch (const std::runtime_error& e){ - data_access::unit_conversion_exception uce(e.what()); - uce.provider_model_name = get_bmi_model()->get_model_name(); + unit_conversion_exception uce(e.what()); + uce.provider_model_name = get_id(); uce.provider_bmi_var_name = bmi_var_name; - uce.provider_units = native_units; uce.unconverted_values.push_back(value); throw uce; } @@ -663,86 +689,152 @@ namespace realization { "': no logic for converting value to variable's type."); } - void Bmi_Module_Formulation::set_model_inputs_prior_to_update(const double &model_init_time, time_step_t t_delta) { - std::vector in_var_names = get_bmi_model()->GetInputVarNames(); - time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); - - for (std::string & var_name : in_var_names) { - data_access::GenericDataProvider *provider; - std::string var_map_alias = get_config_mapped_variable_name(var_name); - if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_map_alias].get(); +void Bmi_Module_Formulation::set_model_inputs_prior_to_update(const double &model_init_time, time_step_t t_delta) { + std::vector in_var_names = get_bmi_model()->GetInputVarNames(); + time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); + + // tiny helpers local to this function (no changes elsewhere) + auto to_lower = [](std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); }); + return s; + }; + auto normalize_consumer_units = [&](const std::string& u)->std::string { + std::string s = to_lower(u); + if (s.empty() || s == "none" || s == "unitless" || s == "dimensionless" || s == "-") + return "1"; // internal canonical for dimensionless + return u; + }; + auto is_nested_provider_guard = [](const std::string& msg)->bool { + return msg.find("Multi BMI formulation can't use associated data provider as a nested module") != std::string::npos; + }; + + for (std::string & var_name : in_var_names) { + data_access::GenericDataProvider *provider; + std::string var_map_alias = get_config_mapped_variable_name(var_name); + if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_map_alias].get(); + } + else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_name].get(); + } + else { + provider = forcing.get(); + } + + // array sizing + int nbytes = get_bmi_model()->GetVarNbytes(var_name); + int varItemSize = get_bmi_model()->GetVarItemsize(var_name); + int numItems = nbytes / varItemSize; + assert(nbytes % varItemSize == 0); + + std::shared_ptr value_ptr; + + // resolve actual C++ type the BMI adapter expects + std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), varItemSize); + + // normalize consumer units once; if truly "dimensionless" (1), ask provider for "" to avoid mm->1 conversions + const std::string consumer_units_raw = get_bmi_model()->GetVarUnits(var_name); + const std::string consumer_units_norm = normalize_consumer_units(consumer_units_raw); + const std::string units_for_selector = (consumer_units_norm == "1") ? std::string("") : consumer_units_norm; + + if (numItems != 1) { + // --- array input path --- + try { + auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(), + var_map_alias, model_epoch_time, t_delta, + units_for_selector)); + if(values.size() == 1){ + #ifndef NGEN_QUIET + std::stringstream ss; + ss << "WARN: broadcasting variable '" << var_name << "' from scalar to expected array\n"; + LOG(ss.str(), LogLevel::SEVERE); ss.str(""); + #endif + values.resize(numItems, values[0]); } - else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_name].get(); + else if (values.size() != numItems) { + throw std::runtime_error("Mismatch in item count for variable '" + var_name + "': model expects " + + std::to_string(numItems) + ", provider returned " + std::to_string(values.size()) + " items\n"); } - else { - provider = forcing.get(); + value_ptr = get_values_as_type(type, values.begin(), values.end()); + } + catch (data_access::unit_conversion_exception &uce) { + // log once per unique producer/consumer pair + data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; + if (data_access::unit_errors_reported.insert(key).second) { + std::stringstream ss; + ss << "Unit conversion failure:" + << " requester '" << get_id() << "' catchment '" << get_catchment_id() << "' variable '" << var_map_alias << "'" + << " provider '" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" + << " raw values count " << uce.unconverted_values.size() + << " message \"" << uce.what() << "\""; + LOG(ss.str(), LogLevel::WARNING); } - - // TODO: probably need to actually allow this by default and warn, but have config option to activate - // this type of behavior - // TODO: account for arrays later - int nbytes = get_bmi_model()->GetVarNbytes(var_name); - int varItemSize = get_bmi_model()->GetVarItemsize(var_name); - int numItems = nbytes / varItemSize; - assert(nbytes % varItemSize == 0); - - std::shared_ptr value_ptr; - // Finally, use the value obtained to set the model input - std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), - varItemSize); - if (numItems != 1) { - //more than a single value needed for var_name - auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, - get_bmi_model()->GetVarUnits(var_name))); - //need to marshal data types to the receiver as well - //this could be done a little more elegantly if the provider interface were - //"type aware", but for now, this will do (but requires yet another copy) - if(values.size() == 1){ - //FIXME this isn't generic broadcasting, but works for scalar implementations - #ifndef NGEN_QUIET - std::stringstream ss; - ss << "WARN: broadcasting variable '" << var_name << "' from scalar to expected array\n";; - LOG(ss.str(), LogLevel::SEVERE); ss.str(""); - #endif - values.resize(numItems, values[0]); - } else if (values.size() != numItems) { - throw std::runtime_error("Mismatch in item count for variable '" + var_name + "': model expects " + - std::to_string(numItems) + ", provider returned " + std::to_string(values.size()) + - " items\n"); - - } - value_ptr = get_values_as_type( type, values.begin(), values.end() ); - - } else { - try { - //scalar value - double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, - get_bmi_model()->GetVarUnits(var_name))); - value_ptr = get_value_as_type(type, value); - } catch (data_access::unit_conversion_exception &uce) { - data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; - auto ret = data_access::unit_errors_reported.insert(key); - bool new_error = ret.second; - if (new_error) { - std::stringstream ss; - ss << "Unit conversion failure:" - << " requester {'" << get_bmi_model()->get_model_name() << "' catchment '" << get_catchment_id() - << "' variable '" << var_name << "'" << " (alias '" << var_map_alias << "')" - << " units '" << get_bmi_model()->GetVarUnits(var_name) << "'}" - << " provider {'" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" - << " raw value " << uce.unconverted_values[0] << "}" - << " message \"" << uce.what() << "\""; - LOG(ss.str(), LogLevel::WARNING); ss.str(""); - } - value_ptr = get_value_as_type(type, uce.unconverted_values[0]); + // fall back: use unconverted values if present, else zeros + std::vector fallback(numItems, 0.0); + if (!uce.unconverted_values.empty()) { + if (uce.unconverted_values.size() == 1) std::fill(fallback.begin(), fallback.end(), uce.unconverted_values[0]); + else if (uce.unconverted_values.size() == (size_t)numItems) fallback = uce.unconverted_values; + else { + // size mismatch: repeat or truncate + for (int i=0; iSetValue(var_name, value_ptr.get()); + value_ptr = get_values_as_type(type, fallback.begin(), fallback.end()); + } + catch (const std::exception &ex) { + if (is_nested_provider_guard(ex.what())) { + std::stringstream ss; + ss << "BMI coupling warning: nested provider disallowed for input array '" << var_name + << "' (alias '" << var_map_alias << "') in catchment '" << get_catchment_id() + << "'. Using fallback zeros this step. Configure an explicit provider mapping for this input."; + LOG(ss.str(), LogLevel::WARNING); + // zero array + std::vector zeros(numItems, 0.0); + value_ptr = get_values_as_type(type, zeros.begin(), zeros.end()); + } + else throw; + } + } + else { + // --- scalar input path --- + try { + double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(), + var_map_alias, model_epoch_time, t_delta, + units_for_selector)); + value_ptr = get_value_as_type(type, value); + } + catch (data_access::unit_conversion_exception &uce) { + data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; + if (data_access::unit_errors_reported.insert(key).second) { + std::stringstream ss; + ss << "Unit conversion failure:" + << " requester '" << get_id() << "' catchment '" << get_catchment_id() << "' variable '" << var_map_alias << "'" + << " provider '" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" + << " raw value " << (uce.unconverted_values.empty() ? std::numeric_limits::quiet_NaN() : uce.unconverted_values[0]) + << " message \"" << uce.what() << "\""; + LOG(ss.str(), LogLevel::WARNING); + } + // fallback to producer value if present, else 0.0 + const double fallback = uce.unconverted_values.empty() ? 0.0 : uce.unconverted_values[0]; + value_ptr = get_value_as_type(type, fallback); + } + catch (const std::exception &ex) { + if (is_nested_provider_guard(ex.what())) { + std::stringstream ss; + ss << "BMI coupling warning: nested provider disallowed for input '" << var_name + << "' (alias '" << var_map_alias << "') in catchment '" << get_catchment_id() + << "'. Using fallback 0 this step. Configure an explicit provider mapping for this input."; + LOG(ss.str(), LogLevel::WARNING); + value_ptr = get_value_as_type(type, 0.0); + } + else throw; } } + get_bmi_model()->SetValue(var_name, value_ptr.get()); + } +} void Bmi_Module_Formulation::append_model_inputs_to_stream(const double &model_init_time, time_step_t t_delta, std::stringstream &inputs) { std::vector in_var_names = get_bmi_model()->GetInputVarNames(); @@ -898,3 +990,4 @@ namespace realization { bmi->SetValue("serialization_free", _); } } + From c6c047a9294aed663c11cd567a1a2ab9b98f2205 Mon Sep 17 00:00:00 2001 From: Mohammed Karim Date: Wed, 17 Sep 2025 16:44:14 -0700 Subject: [PATCH 2/3] fixing indentation --- .../catchment/Bmi_Multi_Formulation.hpp | 807 ++++----- .../catchment/Bmi_Module_Formulation.cpp | 1581 +++++++++-------- 2 files changed, 1195 insertions(+), 1193 deletions(-) diff --git a/include/realizations/catchment/Bmi_Multi_Formulation.hpp b/include/realizations/catchment/Bmi_Multi_Formulation.hpp index 59f55fdfd2..964bf5a03c 100644 --- a/include/realizations/catchment/Bmi_Multi_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Multi_Formulation.hpp @@ -25,59 +25,59 @@ class Bmi_Cpp_Multi_Array_Test; namespace realization { /** - * Abstraction of a formulation with multiple backing model object that implements the BMI. - */ + * Abstraction of a formulation with multiple backing model object that implements the BMI. + */ class Bmi_Multi_Formulation : public Bmi_Formulation { - public: + public: typedef Bmi_Formulation nested_module_type; typedef std::shared_ptr nested_module_ptr; /** - * Minimal constructor for objects initialize using the Formulation_Manager and subsequent calls to - * ``create_formulation``. - * - * @param id - * @param forcing_config - * @param output_stream - */ + * Minimal constructor for objects initialize using the Formulation_Manager and subsequent calls to + * ``create_formulation``. + * + * @param id + * @param forcing_config + * @param output_stream + */ Bmi_Multi_Formulation(std::string id, std::shared_ptr forcing_provider, utils::StreamHandler output_stream) - : Bmi_Formulation(std::move(id), forcing_provider, output_stream) { }; + : Bmi_Formulation(std::move(id), forcing_provider, output_stream) { }; virtual ~Bmi_Multi_Formulation() {}; /** - * Convert a time value from the model to an epoch time in seconds. - * - * Model time values are typically (though not always) 0-based totals count upward as time progresses. The - * units are not necessarily seconds. This performs the necessary lookup and conversion for such units, and - * then shifts the value appropriately for epoch time representation. - * - * For this type, this function will behave in the same manner as the analogous function of the current - * "primary" nested formulation, which is found in the instance's ordered collection of nested module - * formulations at the index returned by @ref get_index_for_primary_module. - * - * @param model_time The time value in a model's representation that is to be converted. - * @return The equivalent epoch time. - */ + * Convert a time value from the model to an epoch time in seconds. + * + * Model time values are typically (though not always) 0-based totals count upward as time progresses. The + * units are not necessarily seconds. This performs the necessary lookup and conversion for such units, and + * then shifts the value appropriately for epoch time representation. + * + * For this type, this function will behave in the same manner as the analogous function of the current + * "primary" nested formulation, which is found in the instance's ordered collection of nested module + * formulations at the index returned by @ref get_index_for_primary_module. + * + * @param model_time The time value in a model's representation that is to be converted. + * @return The equivalent epoch time. + */ time_t convert_model_time(const double &model_time) const override { return convert_model_time(model_time, get_index_for_primary_module()); } /** - * Convert a time value from the model to an epoch time in seconds. - * - * Model time values are typically (though not always) 0-based totals count upward as time progresses. The - * units are not necessarily seconds. This performs the necessary lookup and conversion for such units, and - * then shifts the value appropriately for epoch time representation. - * - * For this type, this function will behave in the same manner as the analogous function of nested formulation - * found at the provided index within the instance's ordered collection of nested module formulations. - * - * @param model_time The time value in a model's representation that is to be converted. - * @return The equivalent epoch time. - */ + * Convert a time value from the model to an epoch time in seconds. + * + * Model time values are typically (though not always) 0-based totals count upward as time progresses. The + * units are not necessarily seconds. This performs the necessary lookup and conversion for such units, and + * then shifts the value appropriately for epoch time representation. + * + * For this type, this function will behave in the same manner as the analogous function of nested formulation + * found at the provided index within the instance's ordered collection of nested module formulations. + * + * @param model_time The time value in a model's representation that is to be converted. + * @return The equivalent epoch time. + */ inline time_t convert_model_time(const double &model_time, int module_index) const { return modules[module_index]->convert_model_time(model_time); } @@ -92,36 +92,36 @@ namespace realization { } /** - * Get whether a model may perform updates beyond its ``end_time``. - * - * Get whether model ``Update`` calls are allowed and handled in some way by the backing model for time steps - * after the model's ``end_time``. Implementations of this type should use this function to safeguard against - * entering either an invalid or otherwise undesired state as a result of attempting to process a model beyond - * its available data. - * - * As mentioned, even for models that are capable of validly handling processing beyond end time, it may be - * desired that they do not for some reason (e.g., the way they account for the lack of input data leads to - * valid but incorrect results for a specific application). Because of this, whether models are allowed to - * process beyond their end time is configuration-based. - * - * @return Whether a model may perform updates beyond its ``end_time``. - */ + * Get whether a model may perform updates beyond its ``end_time``. + * + * Get whether model ``Update`` calls are allowed and handled in some way by the backing model for time steps + * after the model's ``end_time``. Implementations of this type should use this function to safeguard against + * entering either an invalid or otherwise undesired state as a result of attempting to process a model beyond + * its available data. + * + * As mentioned, even for models that are capable of validly handling processing beyond end time, it may be + * desired that they do not for some reason (e.g., the way they account for the lack of input data leads to + * valid but incorrect results for a specific application). Because of this, whether models are allowed to + * process beyond their end time is configuration-based. + * + * @return Whether a model may perform updates beyond its ``end_time``. + */ const bool &get_allow_model_exceed_end_time() const override; /** - * Get the collection of forcing output property names this instance can provide. - * - * For this type, this is the collection of the names/aliases of the BMI output variables for nested modules; - * i.e., the config-mapped alias for the variable when set in the realization config, or just the name when no - * alias was included in the configuration. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The collection of forcing output property names this instance can provide. - * @see ForcingProvider - */ + * Get the collection of forcing output property names this instance can provide. + * + * For this type, this is the collection of the names/aliases of the BMI output variables for nested modules; + * i.e., the config-mapped alias for the variable when set in the realization config, or just the name when no + * alias was included in the configuration. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The collection of forcing output property names this instance can provide. + * @see ForcingProvider + */ boost::span get_available_variable_names() const override; /** @@ -137,91 +137,91 @@ namespace realization { const time_t &get_bmi_model_start_time_forcing_offset_s() const override; /** - * Get the output variables of the last nested BMI model. - * - * @return The output variables of the last nested BMI model. - */ + * Get the output variables of the last nested BMI model. + * + * @return The output variables of the last nested BMI model. + */ const std::vector get_bmi_output_variables() const override { return modules.back()->get_bmi_output_variables(); } /** - * When possible, translate a variable name for a BMI model to an internally recognized name. - * - * Because of the implementation of this type, this function can only translate variable names for input or - * output variables of either the first or last nested BMI module. In cases when this is not possible, it will - * return the original parameter. - * - * The function will only check input variable names for the first module and output variable names for the - * last module. Further, it will always check the first module first, returning if it finds a translation. - * Only then will it check the last module. This can be controlled by using the overloaded function - * @ref get_config_mapped_variable_name(string, bool, bool). - * - * To perform a similar translation between modules, see the overloaded function - * @ref get_config_mapped_variable_name(string, shared_ptr, shared_ptr). - * - * @param model_var_name The BMI variable name to translate so its purpose is recognized internally. - * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. - * @see get_config_mapped_variable_name(string, bool, bool) - * @see get_config_mapped_variable_name(string, shared_ptr, shared_ptr) - */ + * When possible, translate a variable name for a BMI model to an internally recognized name. + * + * Because of the implementation of this type, this function can only translate variable names for input or + * output variables of either the first or last nested BMI module. In cases when this is not possible, it will + * return the original parameter. + * + * The function will only check input variable names for the first module and output variable names for the + * last module. Further, it will always check the first module first, returning if it finds a translation. + * Only then will it check the last module. This can be controlled by using the overloaded function + * @ref get_config_mapped_variable_name(string, bool, bool). + * + * To perform a similar translation between modules, see the overloaded function + * @ref get_config_mapped_variable_name(string, shared_ptr, shared_ptr). + * + * @param model_var_name The BMI variable name to translate so its purpose is recognized internally. + * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. + * @see get_config_mapped_variable_name(string, bool, bool) + * @see get_config_mapped_variable_name(string, shared_ptr, shared_ptr) + */ const std::string &get_config_mapped_variable_name(const std::string &model_var_name) const override; /** - * When possible, translate a variable name for the first or last BMI model to an internally recognized name. - * - * Because of the implementation of this type, this function can only translate variable names for input or - * output variables of either the first or last nested BMI module. In cases when this is not possible, it will - * return the original parameter. - * - * The function can only check input variable names for the first module and output variable names for the - * last module. Parameters control whether each is actually checked. When both are ``true``, it will always - * check the first module first, returning if it finds a translation without checking the last. - * - * @param model_var_name The BMI variable name to translate so its purpose is recognized internally. - * @param check_first Whether an input variable mapping for the first module should sought. - * @param check_last Whether an output variable mapping for the last module should sought. - * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. - */ + * When possible, translate a variable name for the first or last BMI model to an internally recognized name. + * + * Because of the implementation of this type, this function can only translate variable names for input or + * output variables of either the first or last nested BMI module. In cases when this is not possible, it will + * return the original parameter. + * + * The function can only check input variable names for the first module and output variable names for the + * last module. Parameters control whether each is actually checked. When both are ``true``, it will always + * check the first module first, returning if it finds a translation without checking the last. + * + * @param model_var_name The BMI variable name to translate so its purpose is recognized internally. + * @param check_first Whether an input variable mapping for the first module should sought. + * @param check_last Whether an output variable mapping for the last module should sought. + * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. + */ const std::string &get_config_mapped_variable_name(const std::string &model_var_name, bool check_first, bool check_last) const; /** - * When possible, translate the name of an output variable for one BMI model to an input variable for another. - * - * This function behaves similarly to @ref get_config_mapped_variable_name(string), except that is performs the - * translation between modules (rather than between a module and the framework). As such, it is designed for - * translation between two sequential models, although this is not a requirement for valid execution. - * - * The function will first request the mapping for the parameter name from the outputting module, which will either - * return a mapped name or the original param. It will check if the returned value is one of the advertised BMI input - * variable names of the inputting module; if so, it returns that name. Otherwise, it proceeds. - * - * The function then iterates through all the BMI input variable names for the inputting module. If it finds any that - * maps to either the original parameter or the mapped name from the outputting module, it returns it. - * - * If neither of those find a mapping, then the original parameter is returned. - * - * Note that if this is not an output variable name of the outputting module, the function treats this as a no-mapping - * condition and returns the parameter. - * - * @param output_var_name The output variable to be translated. - * @param out_module The module having the output variable. - * @param in_module The module needing a translation of ``output_var_name`` to one of its input variable names. - * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. - */ + * When possible, translate the name of an output variable for one BMI model to an input variable for another. + * + * This function behaves similarly to @ref get_config_mapped_variable_name(string), except that is performs the + * translation between modules (rather than between a module and the framework). As such, it is designed for + * translation between two sequential models, although this is not a requirement for valid execution. + * + * The function will first request the mapping for the parameter name from the outputting module, which will either + * return a mapped name or the original param. It will check if the returned value is one of the advertised BMI input + * variable names of the inputting module; if so, it returns that name. Otherwise, it proceeds. + * + * The function then iterates through all the BMI input variable names for the inputting module. If it finds any that + * maps to either the original parameter or the mapped name from the outputting module, it returns it. + * + * If neither of those find a mapping, then the original parameter is returned. + * + * Note that if this is not an output variable name of the outputting module, the function treats this as a no-mapping + * condition and returns the parameter. + * + * @param output_var_name The output variable to be translated. + * @param out_module The module having the output variable. + * @param in_module The module needing a translation of ``output_var_name`` to one of its input variable names. + * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. + */ const std::string &get_config_mapped_variable_name(const std::string &output_var_name, - const std::shared_ptr& out_module, - const std::shared_ptr& in_module) const; + const std::shared_ptr& out_module, + const std::shared_ptr& in_module) const; /** - * Get the inclusive beginning of the period of time over which this instance can provide data for this forcing. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The inclusive beginning of the period of time over which this instance can provide this data. - */ + * Get the inclusive beginning of the period of time over which this instance can provide data for this forcing. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The inclusive beginning of the period of time over which this instance can provide this data. + */ long get_data_start_time() const override @@ -230,14 +230,14 @@ namespace realization { } /** - * Get the inclusive beginning of the period of time over which this instance can provide data for this forcing. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The inclusive beginning of the period of time over which this instance can provide this data. - */ + * Get the inclusive beginning of the period of time over which this instance can provide data for this forcing. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The inclusive beginning of the period of time over which this instance can provide this data. + */ time_t get_variable_time_begin(const std::string &variable_name) const { std::string var_name = variable_name; @@ -256,14 +256,14 @@ namespace realization { } /** - * Get the exclusive ending of the period of time over which this instance can provide data for this forcing. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The exclusive ending of the period of time over which this instance can provide this data. - */ + * Get the exclusive ending of the period of time over which this instance can provide data for this forcing. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The exclusive ending of the period of time over which this instance can provide this data. + */ long get_data_stop_time() const override { @@ -271,14 +271,14 @@ namespace realization { } /** - * Get the exclusive ending of the period of time over which this instance can provide data for this forcing. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The exclusive ending of the period of time over which this instance can provide this data. - */ + * Get the exclusive ending of the period of time over which this instance can provide data for this forcing. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The exclusive ending of the period of time over which this instance can provide this data. + */ //time_t get_forcing_output_time_end(const std::string &forcing_name) { time_t get_variable_time_end(const std::string &variable_name) const { // when unspecified, assume all data is available for the same range. @@ -324,28 +324,28 @@ namespace realization { } /** - * Get the current time for the primary nested BMI model in its native format and units. - * - * @return The current time for the primary nested BMI model in its native format and units. - */ + * Get the current time for the primary nested BMI model in its native format and units. + * + * @return The current time for the primary nested BMI model in its native format and units. + */ const double get_model_current_time() const override { return modules[get_index_for_primary_module()]->get_model_current_time(); } /** - * Get the end time for the primary nested BMI model in its native format and units. - * - * @return The end time for the primary nested BMI model in its native format and units. - */ + * Get the end time for the primary nested BMI model in its native format and units. + * + * @return The end time for the primary nested BMI model in its native format and units. + */ const double get_model_end_time() const override { return modules[get_index_for_primary_module()]->get_model_end_time(); } /** - * Get the end time for the primary nested BMI model in its native format and units. - * - * @return The end time for the primary nested BMI model in its native format and units. - */ + * Get the end time for the primary nested BMI model in its native format and units. + * + * @return The end time for the primary nested BMI model in its native format and units. + */ const double get_model_start_time() { return modules[get_index_for_primary_module()]->get_data_start_time(); } @@ -355,18 +355,18 @@ namespace realization { double get_response(time_step_t t_index, time_step_t t_delta) override; /** - * Get the index of the forcing time step that contains the given point in time. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * An @ref std::out_of_range exception should be thrown if the time is not in any time step. - * - * @param epoch_time The point in time, as a seconds-based epoch time. - * @return The index of the forcing time step that contains the given point in time. - * @throws std::out_of_range If the given point is not in any time step. - */ + * Get the index of the forcing time step that contains the given point in time. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * An @ref std::out_of_range exception should be thrown if the time is not in any time step. + * + * @param epoch_time The point in time, as a seconds-based epoch time. + * @return The index of the forcing time step that contains the given point in time. + * @throws std::out_of_range If the given point is not in any time step. + */ size_t get_ts_index_for_time(const time_t &epoch_time) const override { // TODO: come back and implement if actually necessary for this type; for now don't use std::string throw_msg; throw_msg.assign("Bmi_Multi_Formulation does not yet implement get_ts_index_for_time"); @@ -375,26 +375,26 @@ namespace realization { } /** - * Get the value of a forcing property for an arbitrary time period, converting units if needed. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * For this type, the @ref availableData map contains available properties and either the external forcing - * provider or the internal nested module that provides that output property. That member is set during - * creation, within the @ref create_multi_formulation function. This function implementation simply defers to - * the function of the same name in the appropriate nested forcing provider. - * - * An @ref std::out_of_range exception should be thrown if the data for the time period is not available. - * - * @param output_name The name of the forcing property of interest. - * @param init_time_epoch The epoch time (in seconds) of the start of the time period. - * @param duration_seconds The length of the time period, in seconds. - * @param output_units The expected units of the desired output value. - * @return The value of the forcing property for the described time period, with units converted if needed. - * @throws std::out_of_range If data for the time period is not available. - */ + * Get the value of a forcing property for an arbitrary time period, converting units if needed. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * For this type, the @ref availableData map contains available properties and either the external forcing + * provider or the internal nested module that provides that output property. That member is set during + * creation, within the @ref create_multi_formulation function. This function implementation simply defers to + * the function of the same name in the appropriate nested forcing provider. + * + * An @ref std::out_of_range exception should be thrown if the data for the time period is not available. + * + * @param output_name The name of the forcing property of interest. + * @param init_time_epoch The epoch time (in seconds) of the start of the time period. + * @param duration_seconds The length of the time period, in seconds. + * @param output_units The expected units of the desired output value. + * @return The value of the forcing property for the described time period, with units converted if needed. + * @throws std::out_of_range If data for the time period is not available. + */ double get_value(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) override { std::string output_name = selector.get_variable_name(); @@ -429,43 +429,43 @@ namespace realization { bool is_bmi_input_variable(const std::string &var_name) const override; /** - * Test whether all backing models have fixed time step size. - * - * @return Whether all backing models has fixed time step size. - */ + * Test whether all backing models have fixed time step size. + * + * @return Whether all backing models has fixed time step size. + */ bool is_bmi_model_time_step_fixed() const override; bool is_bmi_output_variable(const std::string &var_name) const override; /** - * Test whether all backing models have been initialize using the BMI standard ``Initialize`` function. - * - * @return Whether all backing models have been initialize using the BMI standard ``Initialize`` function. - */ + * Test whether all backing models have been initialize using the BMI standard ``Initialize`` function. + * + * @return Whether all backing models have been initialize using the BMI standard ``Initialize`` function. + */ bool is_model_initialized() const override; /** - * Get whether a property's per-time-step values are each an aggregate sum over the entire time step. - * - * Certain properties, like rain fall, are aggregated sums over an entire time step. Others, such as pressure, - * are not such sums and instead something else like an instantaneous reading or an average value. - * - * It may be the case that forcing data is needed for some discretization different than the forcing time step. - * This aspect must be known in such cases to perform the appropriate value interpolation. - * - * For instances of this type, all output forcings fall under this category. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @param name The name of the forcing property for which the current value is desired. - * @return Whether the property's value is an aggregate sum. - */ + * Get whether a property's per-time-step values are each an aggregate sum over the entire time step. + * + * Certain properties, like rain fall, are aggregated sums over an entire time step. Others, such as pressure, + * are not such sums and instead something else like an instantaneous reading or an average value. + * + * It may be the case that forcing data is needed for some discretization different than the forcing time step. + * This aspect must be known in such cases to perform the appropriate value interpolation. + * + * For instances of this type, all output forcings fall under this category. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @param name The name of the forcing property for which the current value is desired. + * @return Whether the property's value is an aggregate sum. + */ bool is_property_sum_over_time_step(const std::string &name) const override { if (availableData.empty() || availableData.find(name) == availableData.end()) { std::string throw_msg; throw_msg.assign( - get_formulation_type() + " cannot get whether unknown property " + name + " is summation"); + get_formulation_type() + " cannot get whether unknown property " + name + " is summation"); LOG(throw_msg, LogLevel::WARNING); throw std::runtime_error(throw_msg); } @@ -473,30 +473,30 @@ namespace realization { } /** - * Get whether this time step goes beyond this formulations (i.e., any of it's modules') end time. - * - * @param t_index The time step index in question. - * @return Whether this time step goes beyond this formulations (i.e., any of it's modules') end time. - */ + * Get whether this time step goes beyond this formulations (i.e., any of it's modules') end time. + * + * @param t_index The time step index in question. + * @return Whether this time step goes beyond this formulations (i.e., any of it's modules') end time. + */ bool is_time_step_beyond_end_time(time_step_t t_index); /** - * Get the index of the primary module. - * - * @return The index of the primary module. - */ + * Get the index of the primary module. + * + * @return The index of the primary module. + */ inline int get_index_for_primary_module() const { return primary_module_index; } /** - * Set the index of the primary module. - * - * Note that this function does not alter the state of the class, or produce an error, if the index is out of - * range. - * - * @param index The index for the module. - */ + * Set the index of the primary module. + * + * Note that this function does not alter the state of the class, or produce an error, if the index is out of + * range. + * + * @param index The index for the module. + */ inline void set_index_for_primary_module(int index) { if (index < modules.size()) { primary_module_index = index; @@ -504,8 +504,8 @@ namespace realization { } /** - * Check that the output variable names in the global bmi_multi are valid names - */ + * Check that the output variable names in the global bmi_multi are valid names + */ void check_output_var_names() { // variable already checked if (is_out_vars_from_last_mod) { @@ -535,106 +535,106 @@ namespace realization { } } - protected: + protected: /** - * Creating a multi-BMI-module formulation from NGen config. - * - * @param properties - * @param needs_param_validation - */ + * Creating a multi-BMI-module formulation from NGen config. + * + * @param properties + * @param needs_param_validation + */ void create_multi_formulation(geojson::PropertyMap properties, bool needs_param_validation); /** - * Get value for some BMI model variable at a specific index. - * - * Function gets the value for a provided variable, retrieving the variable array from the backing model of the - * appropriate nested formulation. The function then returns the specific value at the desired index, cast as a - * double type. - * - * The function makes several assumptions: - * - * 1. `index` is within array bounds - * 2. `var_name` corresponds to a BMI variable for some nested module. - * 3. `var_name` is sufficient to identify what value needs to be retrieved - * 4. the type for output variable allows the value to be cast to a `double` appropriately - * - * Item 3. here can be inferred from 2. for non-multi formulations. For multi formulations, this means the - * provided ``var_name`` must either be a unique BMI variable name among all nested module, or a unique mapped - * alias to a specific variable in a specific module. - * - * It falls to users of this function (i.e., other functions) to ensure these assumptions hold before invoking. - * - * @param index - * @param var_name - * @return - */ - double get_var_value_as_double(const int& index, const std::string& var_name) override { - auto data_provider_iter = availableData.find(var_name); - if (data_provider_iter == availableData.end()) { - throw external::ExternalIntegrationException( + * Get value for some BMI model variable at a specific index. + * + * Function gets the value for a provided variable, retrieving the variable array from the backing model of the + * appropriate nested formulation. The function then returns the specific value at the desired index, cast as a + * double type. + * + * The function makes several assumptions: + * + * 1. `index` is within array bounds + * 2. `var_name` corresponds to a BMI variable for some nested module. + * 3. `var_name` is sufficient to identify what value needs to be retrieved + * 4. the type for output variable allows the value to be cast to a `double` appropriately + * + * Item 3. here can be inferred from 2. for non-multi formulations. For multi formulations, this means the + * provided ``var_name`` must either be a unique BMI variable name among all nested module, or a unique mapped + * alias to a specific variable in a specific module. + * + * It falls to users of this function (i.e., other functions) to ensure these assumptions hold before invoking. + * + * @param index + * @param var_name + * @return + */ + double get_var_value_as_double(const int& index, const std::string& var_name) override { + auto data_provider_iter = availableData.find(var_name); + if (data_provider_iter == availableData.end()) { + throw external::ExternalIntegrationException( "Multi BMI formulation can't find correct nested module for BMI variable " + var_name + SOURCE_LOC); - } - // Otherwise, we have a provider, and we can cast it based on the documented assumptions - try { - auto const& nested_module = data_provider_iter->second; - long nested_module_time = nested_module->get_data_start_time() + ( this->get_model_current_time() - this->get_model_start_time() ); - - // **Minimal fix**: do NOT request dimensionless "1" here; pass "" to avoid any UDUNITS conversion attempt. - auto selector = CatchmentAggrDataSelector(this->get_catchment_id(), var_name, nested_module_time, this->record_duration(), ""); - - // TODO: After merge PR#405, try re-adding support for index - return nested_module->get_value(selector); - } - catch (data_access::unit_conversion_exception &uce) { - // We now avoid conversion by passing "", but keep this path as a safety net. - static bool no_conversion_message_logged = false; - if (!no_conversion_message_logged) { - no_conversion_message_logged = true; - LOG("Emitting output variables from Bmi_Multi_Formulation without unit conversion - see NGWPC-7604", LogLevel::WARNING); - } - return uce.unconverted_values.empty() ? 0.0 : uce.unconverted_values[0]; - } - // If there was any problem with the cast and extraction of the value, avoid hard abort: - catch (std::exception &e) { - std::string msg = e.what(); - // Soften both UDUNITS and nested-provider guard failures - if (msg.find("ut_get_converter()") != std::string::npos || - msg.find("Units not convertible") != std::string::npos || - msg.find("Multi BMI formulation can't use associated data provider as a nested module") != std::string::npos) { - - std::stringstream warn; - warn << "BMI multi output fetch warning for var '" << var_name - << "' in catchment '" << this->get_catchment_id() - << "': " << msg << " — using fallback 0 this step."; - LOG(warn.str(), LogLevel::WARNING); - return 0.0; - } + } + // Otherwise, we have a provider, and we can cast it based on the documented assumptions + try { + auto const& nested_module = data_provider_iter->second; + long nested_module_time = nested_module->get_data_start_time() + ( this->get_model_current_time() - this->get_model_start_time() ); - // Unexpected error: keep previous behavior (log and throw) to avoid masking real bugs - std::string throw_msg; throw_msg.assign("Multi BMI formulation can't use associated data provider as a nested module" - " when attempting to get values of BMI variable " + var_name + SOURCE_LOC); - LOG(throw_msg, LogLevel::WARNING); - throw std::runtime_error(throw_msg); - } -} + // **Minimal fix**: do NOT request dimensionless "1" here; pass "" to avoid any UDUNITS conversion attempt. + auto selector = CatchmentAggrDataSelector(this->get_catchment_id(), var_name, nested_module_time, this->record_duration(), ""); + + // TODO: After merge PR#405, try re-adding support for index + return nested_module->get_value(selector); + } + catch (data_access::unit_conversion_exception &uce) { + // We now avoid conversion by passing "", but keep this path as a safety net. + static bool no_conversion_message_logged = false; + if (!no_conversion_message_logged) { + no_conversion_message_logged = true; + LOG("Emitting output variables from Bmi_Multi_Formulation without unit conversion - see NGWPC-7604", LogLevel::WARNING); + } + return uce.unconverted_values.empty() ? 0.0 : uce.unconverted_values[0]; + } + // If there was any problem with the cast and extraction of the value, avoid hard abort: + catch (std::exception &e) { + std::string msg = e.what(); + // Soften both UDUNITS and nested-provider guard failures + if (msg.find("ut_get_converter()") != std::string::npos || + msg.find("Units not convertible") != std::string::npos || + msg.find("Multi BMI formulation can't use associated data provider as a nested module") != std::string::npos) { + + std::stringstream warn; + warn << "BMI multi output fetch warning for var '" << var_name + << "' in catchment '" << this->get_catchment_id() + << "': " << msg << " — using fallback 0 this step."; + LOG(warn.str(), LogLevel::WARNING); + return 0.0; + } + + // Unexpected error: keep previous behavior (log and throw) to avoid masking real bugs + std::string throw_msg; throw_msg.assign("Multi BMI formulation can't use associated data provider as a nested module" + " when attempting to get values of BMI variable " + var_name + SOURCE_LOC); + LOG(throw_msg, LogLevel::WARNING); + throw std::runtime_error(throw_msg); + } + } /** - * Initialize the deferred associations with the providers in @ref deferredProviders. - * - * During nested formulation creation, when a nested formulation requires as input some output expected from - * soon-to-be-created (i.e., later in execution order) formulation (e.g., in a look-back scenario to an earlier - * time step), then a deferred provider gets registered with the nested module and has a reference added to - * the @ref deferredProviders member. This function goes through all such the deferred providers, ensures there - * is something available that can serve as the backing wrapped provider, and associates them. - */ + * Initialize the deferred associations with the providers in @ref deferredProviders. + * + * During nested formulation creation, when a nested formulation requires as input some output expected from + * soon-to-be-created (i.e., later in execution order) formulation (e.g., in a look-back scenario to an earlier + * time step), then a deferred provider gets registered with the nested module and has a reference added to + * the @ref deferredProviders member. This function goes through all such the deferred providers, ensures there + * is something available that can serve as the backing wrapped provider, and associates them. + */ inline void init_deferred_associations() { for (int d = 0; d < deferredProviders.size(); ++d) { std::shared_ptr &deferredProvider = deferredProviders[d]; // Skip doing anything for any deferred provider that already has its backing provider set if (deferredProvider->isWrappedProviderSet()) - continue; + continue; // TODO: improve this later; since defaults can be used, it is technically possible to grab something // valid when something more appropriate would later be available @@ -651,34 +651,34 @@ namespace realization { if (!deferredProvider->isWrappedProviderSet()) { // TODO: this probably needs to be some kind of custom configuration exception std::string msg = "Multi BMI formulation cannot be created from config: cannot find available data " - "provider to satisfy set of deferred provisions for nested module at index " - + std::to_string(deferredProviderModuleIndices[d]) + ": {"; - // There must always be at least 1; get manually to help with formatting - msg += deferredProvider->get_available_variable_names()[0]; - // And here make sure to start at 1 instead of 0 - for (int i = 1; i < deferredProvider->get_available_variable_names().size(); ++i) + "provider to satisfy set of deferred provisions for nested module at index " + + std::to_string(deferredProviderModuleIndices[d]) + ": {"; + // There must always be at least 1; get manually to help with formatting + msg += deferredProvider->get_available_variable_names()[0]; + // And here make sure to start at 1 instead of 0 + for (int i = 1; i < deferredProvider->get_available_variable_names().size(); ++i) msg += ", " + deferredProvider->get_available_variable_names()[i]; - msg += "}"; + msg += "}"; throw realization::ConfigurationException(msg); } } } /** - * Initialize a nested formulation from the given properties and update multi formulation metadata. - * - * This function creates a new formulation, processes the mapping of BMI variables, and adds outputs to the outer - * module's provideable data items. - * - * Note that it is VERY IMPORTANT that ``properties`` argument`` is provided by value, as this copy is - * potentially updated to perform per-feature pattern substitution for certain property element values. - * - * @tparam T The particular type for the nested formulation object. - * @param mod_index The index for the new formulation in this instance's collection of nested formulations. - * @param identifier The id of for the represented feature. - * @param properties A COPY of the nested module config properties for the nested formulation of interest. - * @return - */ + * Initialize a nested formulation from the given properties and update multi formulation metadata. + * + * This function creates a new formulation, processes the mapping of BMI variables, and adds outputs to the outer + * module's provideable data items. + * + * Note that it is VERY IMPORTANT that ``properties`` argument`` is provided by value, as this copy is + * potentially updated to perform per-feature pattern substitution for certain property element values. + * + * @tparam T The particular type for the nested formulation object. + * @param mod_index The index for the new formulation in this instance's collection of nested formulations. + * @param identifier The id of for the represented feature. + * @param properties A COPY of the nested module config properties for the nested formulation of interest. + * @return + */ template std::shared_ptr init_nested_module(int mod_index, std::string identifier, geojson::PropertyMap properties) { std::shared_ptr wfp = std::make_shared(this); @@ -686,7 +686,7 @@ namespace realization { // Since this is a nested formulation, support usage of the '{{id}}' syntax for init config file paths. Catchment_Formulation::config_pattern_substitution(properties, BMI_REALIZATION_CFG_PARAM_REQ__INIT_CONFIG, - "{{id}}", id); + "{{id}}", id); // Call create_formulation to perform the rest of the typical initialization steps for the formulation. mod->create_formulation(properties); @@ -713,10 +713,10 @@ namespace realization { (*var_aliases)[framework_alias] = var_name; if (availableData.count(framework_alias) > 0) { std::string throw_msg; throw_msg.assign( - "Multi BMI cannot be created with module " + mod->get_model_type_name() + - " with output variable " + framework_alias + - (var_name == framework_alias ? "" : " (an alias of BMI variable " + var_name + ")") + - " because a previous module is using this output variable name/alias."); + "Multi BMI cannot be created with module " + mod->get_model_type_name() + + " with output variable " + framework_alias + + (var_name == framework_alias ? "" : " (an alias of BMI variable " + var_name + ")") + + " because a previous module is using this output variable name/alias."); LOG(throw_msg, LogLevel::WARNING); throw std::runtime_error(throw_msg); } @@ -727,37 +727,37 @@ namespace realization { } /** - * A mapping of data properties to their providers. - * - * Keys for the data properties are unique, name-like identifiers. These could be a BMI output variable name - * for a module, or a configuration-mapped alias of such a variable. The intent is for any module needing any - * input data to also have either an input variable name or input variable name mapping identical to one of - * these keys (and though ordering is important at a higher level, it is not handled directly by this member). - */ + * A mapping of data properties to their providers. + * + * Keys for the data properties are unique, name-like identifiers. These could be a BMI output variable name + * for a module, or a configuration-mapped alias of such a variable. The intent is for any module needing any + * input data to also have either an input variable name or input variable name mapping identical to one of + * these keys (and though ordering is important at a higher level, it is not handled directly by this member). + */ std::map> availableData; - private: - - /** - * Setup a deferred provider for a nested module, tracking the class as needed. - * - * Create an optional wrapped provider for use with a nested module and some required variable it needs - * provided. Track this and the index of the nested modules in the member collections necessary for later - * initializing associations to backing providers that were originally deferred. Then, assign the created - * deferred wrapper provider as the provider for the variable in the nested module. - * - * @tparam T - * @param bmi_input_var_name The name of the required input variable a nested module needs provided. - * @param framework_output_name The framework alias of the required output variable that will be provided to the - * aforementioned input variable (which may be the same as ``bmi_input_var_name``). - * @param mod The nested module requiring a deferred wrapped provider for a variable. - * @param mod_index The index of the given nested module in the ordering of all this instance's nested modules. - */ + private: + + /** + * Setup a deferred provider for a nested module, tracking the class as needed. + * + * Create an optional wrapped provider for use with a nested module and some required variable it needs + * provided. Track this and the index of the nested modules in the member collections necessary for later + * initializing associations to backing providers that were originally deferred. Then, assign the created + * deferred wrapper provider as the provider for the variable in the nested module. + * + * @tparam T + * @param bmi_input_var_name The name of the required input variable a nested module needs provided. + * @param framework_output_name The framework alias of the required output variable that will be provided to the + * aforementioned input variable (which may be the same as ``bmi_input_var_name``). + * @param mod The nested module requiring a deferred wrapped provider for a variable. + * @param mod_index The index of the given nested module in the ordering of all this instance's nested modules. + */ template void setup_nested_deferred_provider(const std::string &bmi_input_var_name, - const std::string &framework_output_name, - std::shared_ptr mod, - int mod_index) { + const std::string &framework_output_name, + std::shared_ptr mod, + int mod_index) { // TODO: probably don't actually need bmi_input_var_name, and just can deal with framework_output_name // Create deferred, optional provider for providing this // Only include BMI variable name, as that's what'll be visible when associating to backing provider @@ -785,37 +785,37 @@ namespace realization { /** The set of available "forcings" (output variables, plus their mapped aliases) this instance can provide. */ std::vector available_forcings; /** - * Any configured default values for outputs, keyed by framework alias (or var name if this is globally unique). - */ + * Any configured default values for outputs, keyed by framework alias (or var name if this is globally unique). + */ std::map default_output_values; /** - * A collection of wrappers to nested formulations providing some output to an earlier nested formulation. - * - * During formulation creation, when a nested formulation requires as input some output from a later formulation - * (e.g., in a look-back scenario to an earlier time step), then an "optimistic" wrapper gets put into place. - * It assumes that the necessary provider will be available and associated once all nested formulations have - * been created. This member tracks these so that this deferred association can be done. - */ + * A collection of wrappers to nested formulations providing some output to an earlier nested formulation. + * + * During formulation creation, when a nested formulation requires as input some output from a later formulation + * (e.g., in a look-back scenario to an earlier time step), then an "optimistic" wrapper gets put into place. + * It assumes that the necessary provider will be available and associated once all nested formulations have + * been created. This member tracks these so that this deferred association can be done. + */ std::vector> deferredProviders; /** - * The module indices for the modules associated with each item in @ref deferredProviders. - * - * E.g., the value in this vector at index ``0`` is the index of a module within @ref modules. That module is - * what required the deferred provider in the @ref deferredProviders collection at its index ``0``. - */ + * The module indices for the modules associated with each item in @ref deferredProviders. + * + * E.g., the value in this vector at index ``0`` is the index of a module within @ref modules. That module is + * what required the deferred provider in the @ref deferredProviders collection at its index ``0``. + */ std::vector deferredProviderModuleIndices; /** - * Whether the @ref Bmi_Formulation::output_variable_names value is just the analogous value from this - * instance's final nested module. - */ + * Whether the @ref Bmi_Formulation::output_variable_names value is just the analogous value from this + * instance's final nested module. + */ bool is_out_vars_from_last_mod = false; /** The nested BMI modules composing this multi-module formulation, in their order of execution. */ std::vector modules; std::vector module_types; /** - * Per-module maps (ordered as in @ref modules) of configuration-mapped names to BMI variable names. - */ + * Per-module maps (ordered as in @ref modules) of configuration-mapped names to BMI variable names. + */ // TODO: confirm that we actually need this for something std::vector>> module_variable_maps; /** Index value (0-based) of the time step that will be processed by the next update of the model. */ @@ -830,3 +830,4 @@ namespace realization { } #endif //NGEN_BMI_MULTI_FORMULATION_HPP + diff --git a/src/realizations/catchment/Bmi_Module_Formulation.cpp b/src/realizations/catchment/Bmi_Module_Formulation.cpp index d1b3bf2000..559fa7525c 100644 --- a/src/realizations/catchment/Bmi_Module_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Module_Formulation.cpp @@ -32,962 +32,963 @@ static inline std::string normalize_requested_units(std::string u, const std::st if (u.empty()) return ""; std::string s = to_lower_copy(u); if (s == "none" || s == "unitless" || s == "dimensionless" || s == "-") - return (to_lower_copy(native_norm) == "1") ? std::string("1") : std::string(""); + return (to_lower_copy(native_norm) == "1") ? std::string("1") : std::string(""); return u; } /* ------------------------------------------------------------------ */ namespace realization { - // define static once (inside namespace) to satisfy linker - //std::set Bmi_Module_Formulation::unit_errors_reported{}; + // define static once (inside namespace) to satisfy linker + //std::set Bmi_Module_Formulation::unit_errors_reported{}; - void Bmi_Module_Formulation::create_formulation(boost::property_tree::ptree &config, geojson::PropertyMap *global) { - geojson::PropertyMap options = this->interpret_parameters(config, global); - inner_create_formulation(options, false); - } - - void Bmi_Module_Formulation::create_formulation(geojson::PropertyMap properties) { - inner_create_formulation(properties, true); - } + void Bmi_Module_Formulation::create_formulation(boost::property_tree::ptree &config, geojson::PropertyMap *global) { + geojson::PropertyMap options = this->interpret_parameters(config, global); + inner_create_formulation(options, false); + } - boost::span Bmi_Module_Formulation::get_available_variable_names() const { - return available_forcings; - } + void Bmi_Module_Formulation::create_formulation(geojson::PropertyMap properties) { + inner_create_formulation(properties, true); + } - std::string Bmi_Module_Formulation::get_output_line_for_timestep(int timestep, std::string delimiter) { - // TODO: something must be added to store values if more than the current time step is wanted - // TODO: if such a thing is added, it should probably be configurable to turn it off - if (timestep != (next_time_step_index - 1)) { - throw std::invalid_argument("Only current time step valid when getting output for BMI C++ formulation"); - } - std::string output_str; + boost::span Bmi_Module_Formulation::get_available_variable_names() const { + return available_forcings; + } - for (const std::string& name : get_output_variable_names()) { - output_str += (output_str.empty() ? "" : ",") + std::to_string(get_var_value_as_double(0, name)); - } - return output_str; + std::string Bmi_Module_Formulation::get_output_line_for_timestep(int timestep, std::string delimiter) { + // TODO: something must be added to store values if more than the current time step is wanted + // TODO: if such a thing is added, it should probably be configurable to turn it off + if (timestep != (next_time_step_index - 1)) { + throw std::invalid_argument("Only current time step valid when getting output for BMI C++ formulation"); } + std::string output_str; - double Bmi_Module_Formulation::get_response(time_step_t t_index, time_step_t t_delta) { - if (get_bmi_model() == nullptr) { - throw std::runtime_error("Trying to process response of improperly created BMI formulation of type '" + get_formulation_type() + "'."); - } - if (t_index < 0) { - throw std::invalid_argument("Getting response of negative time step in BMI formulation of type '" + get_formulation_type() + "' is not allowed."); - } - // Use (next_time_step_index - 1) so that second call with current time step index still works - if (t_index < (next_time_step_index - 1)) { - // TODO: consider whether we should (optionally) store and return historic values - throw std::invalid_argument("Getting response of previous time step in BMI formulation of type '" + get_formulation_type() + "' is not allowed."); - } + for (const std::string& name : get_output_variable_names()) { + output_str += (output_str.empty() ? "" : ",") + std::to_string(get_var_value_as_double(0, name)); + } + return output_str; + } - // The time step delta size, expressed in the units internally used by the model - double t_delta_model_units; - if (next_time_step_index <= t_index) { - t_delta_model_units = get_bmi_model()->convert_seconds_to_model_time((double)t_delta); - double model_time = get_bmi_model()->GetCurrentTime(); - // Also, before running, make sure this doesn't cause a problem with model end_time - if (!get_allow_model_exceed_end_time()) { - int total_time_steps_to_process = abs((int)t_index - next_time_step_index) + 1; - if (get_bmi_model()->GetEndTime() < (model_time + (t_delta_model_units * total_time_steps_to_process))) { - throw std::invalid_argument("Cannot process BMI formulation of type '" + get_formulation_type() + "' to get response of future time step " - "that exceeds model end time."); - } + double Bmi_Module_Formulation::get_response(time_step_t t_index, time_step_t t_delta) { + if (get_bmi_model() == nullptr) { + throw std::runtime_error("Trying to process response of improperly created BMI formulation of type '" + get_formulation_type() + "'."); + } + if (t_index < 0) { + throw std::invalid_argument("Getting response of negative time step in BMI formulation of type '" + get_formulation_type() + "' is not allowed."); + } + // Use (next_time_step_index - 1) so that second call with current time step index still works + if (t_index < (next_time_step_index - 1)) { + // TODO: consider whether we should (optionally) store and return historic values + throw std::invalid_argument("Getting response of previous time step in BMI formulation of type '" + get_formulation_type() + "' is not allowed."); + } + + // The time step delta size, expressed in the units internally used by the model + double t_delta_model_units; + if (next_time_step_index <= t_index) { + t_delta_model_units = get_bmi_model()->convert_seconds_to_model_time((double)t_delta); + double model_time = get_bmi_model()->GetCurrentTime(); + // Also, before running, make sure this doesn't cause a problem with model end_time + if (!get_allow_model_exceed_end_time()) { + int total_time_steps_to_process = abs((int)t_index - next_time_step_index) + 1; + if (get_bmi_model()->GetEndTime() < (model_time + (t_delta_model_units * total_time_steps_to_process))) { + throw std::invalid_argument("Cannot process BMI formulation of type '" + get_formulation_type() + "' to get response of future time step " + "that exceeds model end time."); } } + } - int update_method; - while (next_time_step_index <= t_index) { - double model_initial_time = get_bmi_model()->GetCurrentTime(); - set_model_inputs_prior_to_update(model_initial_time, t_delta); - try { - if (t_delta_model_units == get_bmi_model()->GetTimeStep()) { - update_method = 0; - get_bmi_model()->Update(); - } - else { - update_method = 1; - get_bmi_model()->UpdateUntil(model_initial_time + t_delta_model_units); - } - } catch (const std::exception &e) { - std::stringstream error_message; - error_message << "Model " << (update_method == 0 ? "Update" : "UpdateUntil") - << " failed on catchment " << this->get_catchment_id() - << ". t_index=" << t_index - << ", next_step_index=" << next_time_step_index << "\n"; - append_model_inputs_to_stream(model_initial_time, t_delta, error_message); - Logger::Log(LogLevel::FATAL, error_message.str()); - throw; + int update_method; + while (next_time_step_index <= t_index) { + double model_initial_time = get_bmi_model()->GetCurrentTime(); + set_model_inputs_prior_to_update(model_initial_time, t_delta); + try { + if (t_delta_model_units == get_bmi_model()->GetTimeStep()) { + update_method = 0; + get_bmi_model()->Update(); } - // TODO: again, consider whether we should store any historic response, ts_delta, or other var values - next_time_step_index++; - } - return get_var_value_as_double(0, get_bmi_main_output_var()); - } + else { + update_method = 1; + get_bmi_model()->UpdateUntil(model_initial_time + t_delta_model_units); + } + } catch (const std::exception &e) { + std::stringstream error_message; + error_message << "Model " << (update_method == 0 ? "Update" : "UpdateUntil") + << " failed on catchment " << this->get_catchment_id() + << ". t_index=" << t_index + << ", next_step_index=" << next_time_step_index << "\n"; + append_model_inputs_to_stream(model_initial_time, t_delta, error_message); + Logger::Log(LogLevel::FATAL, error_message.str()); + throw; + } + // TODO: again, consider whether we should store any historic response, ts_delta, or other var values + next_time_step_index++; + } + return get_var_value_as_double(0, get_bmi_main_output_var()); + } - time_t Bmi_Module_Formulation::get_variable_time_begin(const std::string &variable_name) { - // TODO: come back and implement if actually necessary for this type; for now don't use - throw std::runtime_error("Bmi_Modular_Formulation does not yet implement get_variable_time_begin"); - } + time_t Bmi_Module_Formulation::get_variable_time_begin(const std::string &variable_name) { + // TODO: come back and implement if actually necessary for this type; for now don't use + throw std::runtime_error("Bmi_Modular_Formulation does not yet implement get_variable_time_begin"); + } - long Bmi_Module_Formulation::get_data_start_time() const - { - return this->get_bmi_model()->GetStartTime(); - } + long Bmi_Module_Formulation::get_data_start_time() const + { + return this->get_bmi_model()->GetStartTime(); + } - long Bmi_Module_Formulation::get_data_stop_time() const { - // TODO: come back and implement if actually necessary for this type; for now don't use - throw std::runtime_error("Bmi_Module_Formulation does not yet implement get_data_stop_time"); - } + long Bmi_Module_Formulation::get_data_stop_time() const { + // TODO: come back and implement if actually necessary for this type; for now don't use + throw std::runtime_error("Bmi_Module_Formulation does not yet implement get_data_stop_time"); + } - long Bmi_Module_Formulation::record_duration() const { - throw std::runtime_error("Bmi_Module_Formulation does not yet implement record_duration"); - } + long Bmi_Module_Formulation::record_duration() const { + throw std::runtime_error("Bmi_Module_Formulation does not yet implement record_duration"); + } - const double Bmi_Module_Formulation::get_model_current_time() const { - return get_bmi_model()->GetCurrentTime(); - } + const double Bmi_Module_Formulation::get_model_current_time() const { + return get_bmi_model()->GetCurrentTime(); + } - const double Bmi_Module_Formulation::get_model_end_time() const { - return get_bmi_model()->GetEndTime(); - } + const double Bmi_Module_Formulation::get_model_end_time() const { + return get_bmi_model()->GetEndTime(); + } - const std::vector& Bmi_Module_Formulation::get_required_parameters() const { - return REQUIRED_PARAMETERS; - } + const std::vector& Bmi_Module_Formulation::get_required_parameters() const { + return REQUIRED_PARAMETERS; + } - const std::string& Bmi_Module_Formulation::get_config_mapped_variable_name(const std::string &model_var_name) const { - // TODO: need to introduce validation elsewhere that all mapped names are valid AORC field constants. - if (bmi_var_names_map.find(model_var_name) != bmi_var_names_map.end()) - return bmi_var_names_map.at(model_var_name); - else - return model_var_name; - } + const std::string& Bmi_Module_Formulation::get_config_mapped_variable_name(const std::string &model_var_name) const { + // TODO: need to introduce validation elsewhere that all mapped names are valid AORC field constants. + if (bmi_var_names_map.find(model_var_name) != bmi_var_names_map.end()) + return bmi_var_names_map.at(model_var_name); + else + return model_var_name; + } - size_t Bmi_Module_Formulation::get_ts_index_for_time(const time_t &epoch_time) const { - // TODO: come back and implement if actually necessary for this type; for now don't use - throw std::runtime_error("Bmi_Singular_Formulation does not yet implement get_ts_index_for_time"); - } + size_t Bmi_Module_Formulation::get_ts_index_for_time(const time_t &epoch_time) const { + // TODO: come back and implement if actually necessary for this type; for now don't use + throw std::runtime_error("Bmi_Singular_Formulation does not yet implement get_ts_index_for_time"); + } - struct unit_conversion_exception : public std::runtime_error { - unit_conversion_exception(std::string message) : std::runtime_error(message) {} - std::string provider_model_name; - std::string provider_bmi_var_name; - std::vector unconverted_values; - }; + struct unit_conversion_exception : public std::runtime_error { + unit_conversion_exception(std::string message) : std::runtime_error(message) {} + std::string provider_model_name; + std::string provider_bmi_var_name; + std::vector unconverted_values; + }; - std::vector Bmi_Module_Formulation::get_values(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) - { - std::string output_name = selector.get_variable_name(); - time_t init_time = selector.get_init_time(); - long duration_s = selector.get_duration_secs(); - std::string output_units = selector.get_output_units(); - - // First make sure this is an available output - auto forcing_outputs = get_available_variable_names(); - if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { - throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); - } + std::vector Bmi_Module_Formulation::get_values(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) + { + std::string output_name = selector.get_variable_name(); + time_t init_time = selector.get_init_time(); + long duration_s = selector.get_duration_secs(); + std::string output_units = selector.get_output_units(); - // check if output is available from BMI - std::string bmi_var_name; - get_bmi_output_var_name(output_name, bmi_var_name); + // First make sure this is an available output + auto forcing_outputs = get_available_variable_names(); + if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { + throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); + } - if( !bmi_var_name.empty() ) - { - auto model = get_bmi_model().get(); - //Get vector of double values for variable - auto values = models::bmi::GetValue(*model, bmi_var_name); + // check if output is available from BMI + std::string bmi_var_name; + get_bmi_output_var_name(output_name, bmi_var_name); - // Convert units (normalize native + requested; skip if unspecified/equal) - std::string native_units_raw = get_bmi_model()->GetVarUnits(bmi_var_name); - std::string native_units = normalize_native_units(native_units_raw); - std::string desired_units = normalize_requested_units(output_units, native_units); - try { - if (desired_units.empty() || to_lower_copy(desired_units) == to_lower_copy(native_units)) { - return values; // no conversion required - } - UnitsHelper::convert_values(native_units, values.data(), desired_units, values.data(), values.size()); - return values; - } - catch (const std::runtime_error& e) { - unit_conversion_exception uce(e.what()); - uce.provider_model_name = get_id(); - uce.provider_bmi_var_name = bmi_var_name; - uce.unconverted_values = std::move(values); - throw uce; + if( !bmi_var_name.empty() ) + { + auto model = get_bmi_model().get(); + //Get vector of double values for variable + auto values = models::bmi::GetValue(*model, bmi_var_name); + + // Convert units (normalize native + requested; skip if unspecified/equal) + std::string native_units_raw = get_bmi_model()->GetVarUnits(bmi_var_name); + std::string native_units = normalize_native_units(native_units_raw); + std::string desired_units = normalize_requested_units(output_units, native_units); + try { + if (desired_units.empty() || to_lower_copy(desired_units) == to_lower_copy(native_units)) { + return values; // no conversion required } + UnitsHelper::convert_values(native_units, values.data(), desired_units, values.data(), values.size()); + return values; + } + catch (const std::runtime_error& e) { + unit_conversion_exception uce(e.what()); + uce.provider_model_name = get_id(); + uce.provider_bmi_var_name = bmi_var_name; + uce.unconverted_values = std::move(values); + throw uce; } - //This is unlikely (impossible?) to throw since a pre-check on available names is done above. Assert instead? - throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); } + //This is unlikely (impossible?) to throw since a pre-check on available names is done above. Assert instead? + throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); + } - double Bmi_Module_Formulation::get_value(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) - { - std::string output_name = selector.get_variable_name(); - time_t init_time = selector.get_init_time(); - long duration_s = selector.get_duration_secs(); - std::string output_units = selector.get_output_units(); - - // First make sure this is an available output - auto forcing_outputs = get_available_variable_names(); - if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { - throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); - } + double Bmi_Module_Formulation::get_value(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) + { + std::string output_name = selector.get_variable_name(); + time_t init_time = selector.get_init_time(); + long duration_s = selector.get_duration_secs(); + std::string output_units = selector.get_output_units(); - // check if output is available from BMI - std::string bmi_var_name; - get_bmi_output_var_name(output_name, bmi_var_name); + // First make sure this is an available output + auto forcing_outputs = get_available_variable_names(); + if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { + throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); + } - if( !bmi_var_name.empty() ) - { - //Get forcing value from BMI variable - double value = get_var_value_as_double(0, bmi_var_name); + // check if output is available from BMI + std::string bmi_var_name; + get_bmi_output_var_name(output_name, bmi_var_name); - // Convert units (normalize native + requested; skip if unspecified/equal) - std::string native_units_raw = get_bmi_model()->GetVarUnits(bmi_var_name); - std::string native_units = normalize_native_units(native_units_raw); - std::string desired_units = normalize_requested_units(output_units, native_units); - try { - if (desired_units.empty() || to_lower_copy(desired_units) == to_lower_copy(native_units)) { - return value; // no conversion required - } - return UnitsHelper::get_converted_value(native_units, value, desired_units); - } - catch (const std::runtime_error& e){ - unit_conversion_exception uce(e.what()); - uce.provider_model_name = get_id(); - uce.provider_bmi_var_name = bmi_var_name; - uce.unconverted_values.push_back(value); - throw uce; + if( !bmi_var_name.empty() ) + { + //Get forcing value from BMI variable + double value = get_var_value_as_double(0, bmi_var_name); + + // Convert units (normalize native + requested; skip if unspecified/equal) + std::string native_units_raw = get_bmi_model()->GetVarUnits(bmi_var_name); + std::string native_units = normalize_native_units(native_units_raw); + std::string desired_units = normalize_requested_units(output_units, native_units); + try { + if (desired_units.empty() || to_lower_copy(desired_units) == to_lower_copy(native_units)) { + return value; // no conversion required } + return UnitsHelper::get_converted_value(native_units, value, desired_units); + } + catch (const std::runtime_error& e){ + unit_conversion_exception uce(e.what()); + uce.provider_model_name = get_id(); + uce.provider_bmi_var_name = bmi_var_name; + uce.unconverted_values.push_back(value); + throw uce; } - - //This is unlikely (impossible?) to throw since a pre-check on available names is done above. Assert instead? - throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); } + //This is unlikely (impossible?) to throw since a pre-check on available names is done above. Assert instead? + throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); + } - static bool is_var_name_in_collection(const std::vector &all_names, const std::string &var_name) { - return std::count(all_names.begin(), all_names.end(), var_name) > 0; - } - bool Bmi_Module_Formulation::is_bmi_input_variable(const std::string &var_name) const { - return is_var_name_in_collection(get_bmi_input_variables(), var_name); - } + static bool is_var_name_in_collection(const std::vector &all_names, const std::string &var_name) { + return std::count(all_names.begin(), all_names.end(), var_name) > 0; + } - bool Bmi_Module_Formulation::is_bmi_output_variable(const std::string &var_name) const { - return is_var_name_in_collection(get_bmi_output_variables(), var_name); - } + bool Bmi_Module_Formulation::is_bmi_input_variable(const std::string &var_name) const { + return is_var_name_in_collection(get_bmi_input_variables(), var_name); + } - bool Bmi_Module_Formulation::is_property_sum_over_time_step(const std::string& name) const { - // TODO: verify with some kind of proof that "always true" is appropriate - return true; - } + bool Bmi_Module_Formulation::is_bmi_output_variable(const std::string &var_name) const { + return is_var_name_in_collection(get_bmi_output_variables(), var_name); + } - const std::vector Bmi_Module_Formulation::get_bmi_input_variables() const { - return get_bmi_model()->GetInputVarNames(); - } + bool Bmi_Module_Formulation::is_property_sum_over_time_step(const std::string& name) const { + // TODO: verify with some kind of proof that "always true" is appropriate + return true; + } - const std::vector Bmi_Module_Formulation::get_bmi_output_variables() const { - return get_bmi_model()->GetOutputVarNames(); - } + const std::vector Bmi_Module_Formulation::get_bmi_input_variables() const { + return get_bmi_model()->GetInputVarNames(); + } + + const std::vector Bmi_Module_Formulation::get_bmi_output_variables() const { + return get_bmi_model()->GetOutputVarNames(); + } - void Bmi_Module_Formulation::get_bmi_output_var_name(const std::string &name, std::string &bmi_var_name) + void Bmi_Module_Formulation::get_bmi_output_var_name(const std::string &name, std::string &bmi_var_name) + { + //check standard output names first + std::vector output_names = get_bmi_model()->GetOutputVarNames(); + if (std::find(output_names.begin(), output_names.end(), name) != output_names.end()) { + bmi_var_name = name; + } + else { - //check standard output names first - std::vector output_names = get_bmi_model()->GetOutputVarNames(); - if (std::find(output_names.begin(), output_names.end(), name) != output_names.end()) { - bmi_var_name = name; - } - else - { - //check mapped names - std::string mapped_name; - for (auto & iter : bmi_var_names_map) { - if (iter.second == name) { - mapped_name = iter.first; - break; - } - } - //ensure mapped name maps to an output variable, see GH #393 =) - if (std::find(output_names.begin(), output_names.end(), mapped_name) != output_names.end()){ - bmi_var_name = mapped_name; + //check mapped names + std::string mapped_name; + for (auto & iter : bmi_var_names_map) { + if (iter.second == name) { + mapped_name = iter.first; + break; } - //else not an output variable } + //ensure mapped name maps to an output variable, see GH #393 =) + if (std::find(output_names.begin(), output_names.end(), mapped_name) != output_names.end()){ + bmi_var_name = mapped_name; + } + //else not an output variable } + } - void Bmi_Module_Formulation::determine_model_time_offset() { - set_bmi_model_start_time_forcing_offset_s( - // TODO: Look at making this epoch start configurable instead of from forcing - forcing->get_data_start_time() - convert_model_time(get_bmi_model()->GetStartTime())); - } + void Bmi_Module_Formulation::determine_model_time_offset() { + set_bmi_model_start_time_forcing_offset_s( + // TODO: Look at making this epoch start configurable instead of from forcing + forcing->get_data_start_time() - convert_model_time(get_bmi_model()->GetStartTime())); + } - const bool& Bmi_Module_Formulation::get_allow_model_exceed_end_time() const { - return allow_model_exceed_end_time; - } + const bool& Bmi_Module_Formulation::get_allow_model_exceed_end_time() const { + return allow_model_exceed_end_time; + } - const std::string& Bmi_Module_Formulation::get_bmi_init_config() const { - return bmi_init_config; - } + const std::string& Bmi_Module_Formulation::get_bmi_init_config() const { + return bmi_init_config; + } + + std::shared_ptr Bmi_Module_Formulation::get_bmi_model() const { + return bmi_model; + } - std::shared_ptr Bmi_Module_Formulation::get_bmi_model() const { - return bmi_model; + const time_t& Bmi_Module_Formulation::get_bmi_model_start_time_forcing_offset_s() const { + return bmi_model_start_time_forcing_offset_s; + } + + void Bmi_Module_Formulation::inner_create_formulation(geojson::PropertyMap properties, bool needs_param_validation) { + if (needs_param_validation) { + validate_parameters(properties); } + // Required parameters first + set_bmi_init_config(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__INIT_CONFIG).as_string()); + set_bmi_main_output_var(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__MAIN_OUT_VAR).as_string()); + set_model_type_name(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__MODEL_TYPE).as_string()); - const time_t& Bmi_Module_Formulation::get_bmi_model_start_time_forcing_offset_s() const { - return bmi_model_start_time_forcing_offset_s; + // Then optional ... + + auto uses_forcings_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__USES_FORCINGS); + if (uses_forcings_it != properties.end() && uses_forcings_it->second.as_boolean()) { + throw std::runtime_error("The '" BMI_REALIZATION_CFG_PARAM_OPT__USES_FORCINGS "' parameter was removed and cannot be set"); } - void Bmi_Module_Formulation::inner_create_formulation(geojson::PropertyMap properties, bool needs_param_validation) { - if (needs_param_validation) { - validate_parameters(properties); - } - // Required parameters first - set_bmi_init_config(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__INIT_CONFIG).as_string()); - set_bmi_main_output_var(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__MAIN_OUT_VAR).as_string()); - set_model_type_name(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__MODEL_TYPE).as_string()); + auto forcing_file_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__FORCING_FILE); + if (forcing_file_it != properties.end() && forcing_file_it->second.as_string() != "") { + throw std::runtime_error("The '" BMI_REALIZATION_CFG_PARAM_OPT__FORCING_FILE "' parameter was removed and cannot be set " + forcing_file_it->second.as_string()); + } - // Then optional ... + if (properties.find(BMI_REALIZATION_CFG_PARAM_OPT__ALLOW_EXCEED_END) != properties.end()) { + set_allow_model_exceed_end_time( + properties.at(BMI_REALIZATION_CFG_PARAM_OPT__ALLOW_EXCEED_END).as_boolean()); + } + if (properties.find(BMI_REALIZATION_CFG_PARAM_OPT__FIXED_TIME_STEP) != properties.end()) { + set_bmi_model_time_step_fixed( + properties.at(BMI_REALIZATION_CFG_PARAM_OPT__FIXED_TIME_STEP).as_boolean()); + } - auto uses_forcings_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__USES_FORCINGS); - if (uses_forcings_it != properties.end() && uses_forcings_it->second.as_boolean()) { - throw std::runtime_error("The '" BMI_REALIZATION_CFG_PARAM_OPT__USES_FORCINGS "' parameter was removed and cannot be set"); + auto std_names_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__VAR_STD_NAMES); + if (std_names_it != properties.end()) { + geojson::PropertyMap names_map = std_names_it->second.get_values(); + for (auto& names_it : names_map) { + bmi_var_names_map.insert( + std::pair(names_it.first, names_it.second.as_string())); } + } - auto forcing_file_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__FORCING_FILE); - if (forcing_file_it != properties.end() && forcing_file_it->second.as_string() != "") { - throw std::runtime_error("The '" BMI_REALIZATION_CFG_PARAM_OPT__FORCING_FILE "' parameter was removed and cannot be set " + forcing_file_it->second.as_string()); - } + // Do this next, since after checking whether other input variables are present in the properties, we can + // now construct the adapter and init the model + set_bmi_model(construct_model(properties)); - if (properties.find(BMI_REALIZATION_CFG_PARAM_OPT__ALLOW_EXCEED_END) != properties.end()) { - set_allow_model_exceed_end_time( - properties.at(BMI_REALIZATION_CFG_PARAM_OPT__ALLOW_EXCEED_END).as_boolean()); - } - if (properties.find(BMI_REALIZATION_CFG_PARAM_OPT__FIXED_TIME_STEP) != properties.end()) { - set_bmi_model_time_step_fixed( - properties.at(BMI_REALIZATION_CFG_PARAM_OPT__FIXED_TIME_STEP).as_boolean()); - } + //Check if any parameter values need to be set on the BMI model, + //and set them before it is run + set_initial_bmi_parameters(properties); - auto std_names_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__VAR_STD_NAMES); - if (std_names_it != properties.end()) { - geojson::PropertyMap names_map = std_names_it->second.get_values(); - for (auto& names_it : names_map) { - bmi_var_names_map.insert( - std::pair(names_it.first, names_it.second.as_string())); - } - } + // Make sure that this is able to interpret model time and convert to real time, since BMI model time is + // usually starting at 0 and just counting up + determine_model_time_offset(); - // Do this next, since after checking whether other input variables are present in the properties, we can - // now construct the adapter and init the model - set_bmi_model(construct_model(properties)); - - //Check if any parameter values need to be set on the BMI model, - //and set them before it is run - set_initial_bmi_parameters(properties); - - // Make sure that this is able to interpret model time and convert to real time, since BMI model time is - // usually starting at 0 and just counting up - determine_model_time_offset(); - - // Output variable subset and order, if present - auto out_var_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUT_VARS); - if (out_var_it != properties.end()) { - std::vector out_vars_json_list = out_var_it->second.as_list(); - std::vector out_vars(out_vars_json_list.size()); - for (int i = 0; i < out_vars_json_list.size(); ++i) { - out_vars[i] = out_vars_json_list[i].as_string(); - } - set_output_variable_names(out_vars); - } - // Otherwise, just take what literally is provided by the model - else { - set_output_variable_names(get_bmi_model()->GetOutputVarNames()); + // Output variable subset and order, if present + auto out_var_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUT_VARS); + if (out_var_it != properties.end()) { + std::vector out_vars_json_list = out_var_it->second.as_list(); + std::vector out_vars(out_vars_json_list.size()); + for (int i = 0; i < out_vars_json_list.size(); ++i) { + out_vars[i] = out_vars_json_list[i].as_string(); } + set_output_variable_names(out_vars); + } + // Otherwise, just take what literally is provided by the model + else { + set_output_variable_names(get_bmi_model()->GetOutputVarNames()); + } - // Output header fields, if present - auto out_headers_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUT_HEADER_FIELDS); - if (out_headers_it != properties.end()) { - std::vector out_headers_json_list = out_var_it->second.as_list(); - std::vector out_headers(out_headers_json_list.size()); - for (int i = 0; i < out_headers_json_list.size(); ++i) { - out_headers[i] = out_headers_json_list[i].as_string(); - } - set_output_header_fields(out_headers); - } - else { - set_output_header_fields(get_output_variable_names()); + // Output header fields, if present + auto out_headers_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUT_HEADER_FIELDS); + if (out_headers_it != properties.end()) { + std::vector out_headers_json_list = out_var_it->second.as_list(); + std::vector out_headers(out_headers_json_list.size()); + for (int i = 0; i < out_headers_json_list.size(); ++i) { + out_headers[i] = out_headers_json_list[i].as_string(); } + set_output_header_fields(out_headers); + } + else { + set_output_header_fields(get_output_variable_names()); + } - // Output precision, if present - auto out_precision_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUTPUT_PRECISION); - if (out_precision_it != properties.end()) { - set_output_precision(properties.at(BMI_REALIZATION_CFG_PARAM_OPT__OUTPUT_PRECISION).as_natural_number()); - } + // Output precision, if present + auto out_precision_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUTPUT_PRECISION); + if (out_precision_it != properties.end()) { + set_output_precision(properties.at(BMI_REALIZATION_CFG_PARAM_OPT__OUTPUT_PRECISION).as_natural_number()); + } - // Finally, make sure this is set - model_initialized = get_bmi_model()->is_model_initialized(); + // Finally, make sure this is set + model_initialized = get_bmi_model()->is_model_initialized(); - // Get output variable names - if (model_initialized) { - for (const std::string &output_var_name : get_bmi_model()->GetOutputVarNames()) { - available_forcings.push_back(output_var_name); - if (bmi_var_names_map.find(output_var_name) != bmi_var_names_map.end()) - available_forcings.push_back(bmi_var_names_map[output_var_name]); - } + // Get output variable names + if (model_initialized) { + for (const std::string &output_var_name : get_bmi_model()->GetOutputVarNames()) { + available_forcings.push_back(output_var_name); + if (bmi_var_names_map.find(output_var_name) != bmi_var_names_map.end()) + available_forcings.push_back(bmi_var_names_map[output_var_name]); } } - /** - * @brief Template function for copying iterator range into contiguous array. - * - * This function will iterate the range and cast the iterator value to type T - * and copy that value into a C-like array of contiguous, dynamically allocated memory. - * This array is stored in a smart pointer with a custom array deleter. - * - * @tparam T - * @tparam Iterator - * @param begin - * @param end - * @return std::shared_ptr - */ - template - std::shared_ptr as_c_array(Iterator begin, Iterator end){ - //Make a shared pointer large enough to hold all elements - //This is a CONTIGUOUS array of type T - //Must provide a custom deleter to delete the array - std::shared_ptr ptr( new T[std::distance(begin, end)], [](T *p) { delete[] p; } ); - Iterator it = begin; - int i = 0; - while(it != end){ - //Be safe and cast the input to the desired type - ptr.get()[i] = static_cast(*it); - ++it; - ++i; - } - return ptr; - } - - /** - * @brief Gets values in iterator range, casted based on @p type then returned as typeless (void) pointer. - * - * @tparam Iterator - * @param type - * @param begin - * @param end - * @return std::shared_ptr - */ - template - std::shared_ptr get_values_as_type(std::string type, Iterator begin, Iterator end) - { - //Use std::vector range constructor to ensure contiguous storage of values - //Return the pointer to the contiguous storage - if (type == "double" || type == "double precision") - return as_c_array(begin, end); + } + /** + * @brief Template function for copying iterator range into contiguous array. + * + * This function will iterate the range and cast the iterator value to type T + * and copy that value into a C-like array of contiguous, dynamically allocated memory. + * This array is stored in a smart pointer with a custom array deleter. + * + * @tparam T + * @tparam Iterator + * @param begin + * @param end + * @return std::shared_ptr + */ + template + std::shared_ptr as_c_array(Iterator begin, Iterator end){ + //Make a shared pointer large enough to hold all elements + //This is a CONTIGUOUS array of type T + //Must provide a custom deleter to delete the array + std::shared_ptr ptr( new T[std::distance(begin, end)], [](T *p) { delete[] p; } ); + Iterator it = begin; + int i = 0; + while(it != end){ + //Be safe and cast the input to the desired type + ptr.get()[i] = static_cast(*it); + ++it; + ++i; + } + return ptr; + } - if (type == "float" || type == "real") - return as_c_array(begin, end); + /** + * @brief Gets values in iterator range, casted based on @p type then returned as typeless (void) pointer. + * + * @tparam Iterator + * @param type + * @param begin + * @param end + * @return std::shared_ptr + */ + template + std::shared_ptr get_values_as_type(std::string type, Iterator begin, Iterator end) + { + //Use std::vector range constructor to ensure contiguous storage of values + //Return the pointer to the contiguous storage + if (type == "double" || type == "double precision") + return as_c_array(begin, end); - if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") - return as_c_array(begin, end); + if (type == "float" || type == "real") + return as_c_array(begin, end); - if (type == "unsigned short" || type == "unsigned short int") - return as_c_array(begin, end); + if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") + return as_c_array(begin, end); - if (type == "int" || type == "signed" || type == "signed int" || type == "integer") - return as_c_array(begin, end); + if (type == "unsigned short" || type == "unsigned short int") + return as_c_array(begin, end); - if (type == "unsigned" || type == "unsigned int") - return as_c_array(begin, end); + if (type == "int" || type == "signed" || type == "signed int" || type == "integer") + return as_c_array(begin, end); - if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") - return as_c_array(begin, end); + if (type == "unsigned" || type == "unsigned int") + return as_c_array(begin, end); - if (type == "unsigned long" || type == "unsigned long int") - return as_c_array(begin, end); + if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") + return as_c_array(begin, end); - if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") - return as_c_array(begin, end); + if (type == "unsigned long" || type == "unsigned long int") + return as_c_array(begin, end); - if (type == "unsigned long long" || type == "unsigned long long int") - return as_c_array(begin, end); + if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") + return as_c_array(begin, end); - throw std::runtime_error("Unable to get values of iterable as type" + type + - " : no logic for converting values to variable's type."); - } + if (type == "unsigned long long" || type == "unsigned long long int") + return as_c_array(begin, end); + throw std::runtime_error("Unable to get values of iterable as type" + type + + " : no logic for converting values to variable's type."); + } - void Bmi_Module_Formulation::set_initial_bmi_parameters(geojson::PropertyMap properties) { - auto model = get_bmi_model(); - if( model == nullptr ) return; - //Now that the model is ready, we can set some intial parameters passed in the config - auto model_params = properties.find("model_params"); - - if (model_params != properties.end() ){ - - geojson::PropertyMap params = model_params->second.get_values(); - //Declare/init the possible vectors here - //reuse them for each loop iteration, make sure to clear them - std::vector long_vec; - std::vector double_vec; - //not_supported - //std::vector str_vec; - //std::vector bool_vec; - std::shared_ptr value_ptr; - for (auto& param : params) { - //Get some basic BMI info for this param - int varItemSize = get_bmi_model()->GetVarItemsize(param.first); - int totalBytes = get_bmi_model()->GetVarNbytes(param.first); - - //Figure out the c++ type to convert data to - std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(param.first), - varItemSize); - //TODO might consider refactoring as_vector and get_values_as_type - //(and by extension, as_c_array) into the JSONProperty class - //then instead of the PropertyVariant visitor filling vectors - //it could fill the c-like array and avoid another copy. - switch( param.second.get_type() ){ - case geojson::PropertyType::Natural: - param.second.as_vector(long_vec); - value_ptr = get_values_as_type(type, long_vec.begin(), long_vec.end()); - break; - case geojson::PropertyType::Real: - param.second.as_vector(double_vec); - value_ptr = get_values_as_type(type, double_vec.begin(), double_vec.end()); - break; - /* Not currently supporting string parameter values - case geojson::PropertyType::String: - param.second.as_vector(str_vec); - value_ptr = get_values_as_type(type, long_vec.begin(), long_vec.end()); - */ - /* Not currently supporting native bool (true/false) parameter values (use int 0/1) - case geojson::PropertyType::Boolean: - param.second.as_vector(bool_vec); - //data_ptr = bool_vec.data(); - */ - case geojson::PropertyType::List: - //In this case, only supporting numeric lists - //will retrieve as double (longs will get casted) - //TODO consider some additional introspection/optimization for this? - param.second.as_vector(double_vec); - if(double_vec.size() == 0){ - //logging::warning(("Cannot pass non-numeric lists as a BMI parameter, skipping "+param.first+"\n").c_str()); - bmiform_ss << "Cannot pass non-numeric lists as a BMI parameter, skipping " << param.first << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); - continue; - } - value_ptr = get_values_as_type(type, double_vec.begin(), double_vec.end()); - break; - default: - //logging::warning(("Cannot pass parameter of type "+geojson::get_propertytype_name(param.second.get_type())+" as a BMI parameter, skipping "+param.first+"\n").c_str()); - bmiform_ss << "Cannot pass parameter of type " << geojson::get_propertytype_name(param.second.get_type()) << " as a BMI parameter, skipping " << param.first << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); - continue; - } - try{ - // Finally, use the value obtained to set the model param - get_bmi_model()->SetValue(param.first, value_ptr.get()); - } - catch (const std::exception &e) - { -// logging::warning((std::string("Exception setting parameter value: ")+e.what()).c_str()); -// logging::warning(("Skipping parameter: "+param.first+"\n").c_str()); - bmiform_ss << "Exception setting parameter value: " << e.what() << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); - bmiform_ss << "Skipping parameter: " << param.first << std::endl; + void Bmi_Module_Formulation::set_initial_bmi_parameters(geojson::PropertyMap properties) { + auto model = get_bmi_model(); + if( model == nullptr ) return; + //Now that the model is ready, we can set some intial parameters passed in the config + auto model_params = properties.find("model_params"); + + if (model_params != properties.end() ){ + + geojson::PropertyMap params = model_params->second.get_values(); + //Declare/init the possible vectors here + //reuse them for each loop iteration, make sure to clear them + std::vector long_vec; + std::vector double_vec; + //not_supported + //std::vector str_vec; + //std::vector bool_vec; + std::shared_ptr value_ptr; + for (auto& param : params) { + //Get some basic BMI info for this param + int varItemSize = get_bmi_model()->GetVarItemsize(param.first); + int totalBytes = get_bmi_model()->GetVarNbytes(param.first); + + //Figure out the c++ type to convert data to + std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(param.first), + varItemSize); + //TODO might consider refactoring as_vector and get_values_as_type + //(and by extension, as_c_array) into the JSONProperty class + //then instead of the PropertyVariant visitor filling vectors + //it could fill the c-like array and avoid another copy. + switch( param.second.get_type() ){ + case geojson::PropertyType::Natural: + param.second.as_vector(long_vec); + value_ptr = get_values_as_type(type, long_vec.begin(), long_vec.end()); + break; + case geojson::PropertyType::Real: + param.second.as_vector(double_vec); + value_ptr = get_values_as_type(type, double_vec.begin(), double_vec.end()); + break; + /* Not currently supporting string parameter values + case geojson::PropertyType::String: + param.second.as_vector(str_vec); + value_ptr = get_values_as_type(type, long_vec.begin(), long_vec.end()); + */ + /* Not currently supporting native bool (true/false) parameter values (use int 0/1) + case geojson::PropertyType::Boolean: + param.second.as_vector(bool_vec); + //data_ptr = bool_vec.data(); + */ + case geojson::PropertyType::List: + //In this case, only supporting numeric lists + //will retrieve as double (longs will get casted) + //TODO consider some additional introspection/optimization for this? + param.second.as_vector(double_vec); + if(double_vec.size() == 0){ + //logging::warning(("Cannot pass non-numeric lists as a BMI parameter, skipping "+param.first+"\n").c_str()); + bmiform_ss << "Cannot pass non-numeric lists as a BMI parameter, skipping " << param.first << std::endl; LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + continue; } - catch (...) - { -// logging::warning((std::string("Unknown Exception setting parameter value: \n")).c_str()); -// logging::warning(("Skipping parameter: "+param.first+"\n").c_str()); - bmiform_ss << "Unknown Exception setting parameter value" << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); - bmiform_ss << "Skipping parameter: " << param.first << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); - } - long_vec.clear(); - double_vec.clear(); - //Not supported - //str_vec.clear(); - //bool_vec.clear(); + value_ptr = get_values_as_type(type, double_vec.begin(), double_vec.end()); + break; + default: + //logging::warning(("Cannot pass parameter of type "+geojson::get_propertytype_name(param.second.get_type())+" as a BMI parameter, skipping "+param.first+"\n").c_str()); + bmiform_ss << "Cannot pass parameter of type " << geojson::get_propertytype_name(param.second.get_type()) << " as a BMI parameter, skipping " << param.first << std::endl; + LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + continue; } + try{ + // Finally, use the value obtained to set the model param + get_bmi_model()->SetValue(param.first, value_ptr.get()); + } + catch (const std::exception &e) + { + // logging::warning((std::string("Exception setting parameter value: ")+e.what()).c_str()); + // logging::warning(("Skipping parameter: "+param.first+"\n").c_str()); + bmiform_ss << "Exception setting parameter value: " << e.what() << std::endl; + LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + bmiform_ss << "Skipping parameter: " << param.first << std::endl; + LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + } + catch (...) + { + // logging::warning((std::string("Unknown Exception setting parameter value: \n")).c_str()); + // logging::warning(("Skipping parameter: "+param.first+"\n").c_str()); + bmiform_ss << "Unknown Exception setting parameter value" << std::endl; + LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + bmiform_ss << "Skipping parameter: " << param.first << std::endl; + LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + } + long_vec.clear(); + double_vec.clear(); + //Not supported + //str_vec.clear(); + //bool_vec.clear(); } - //TODO use SetValue(name, vector) overloads??? - //implment the overloads in each adapter - //ensure proper type is prepared before setting value - } - bool Bmi_Module_Formulation::is_bmi_model_time_step_fixed() const { - return bmi_model_time_step_fixed; - } - bool Bmi_Module_Formulation::is_model_initialized() const { - return model_initialized; } + //TODO use SetValue(name, vector) overloads??? + //implment the overloads in each adapter + //ensure proper type is prepared before setting value + } + bool Bmi_Module_Formulation::is_bmi_model_time_step_fixed() const { + return bmi_model_time_step_fixed; + } - void Bmi_Module_Formulation::set_allow_model_exceed_end_time(bool allow_exceed_end) { - allow_model_exceed_end_time = allow_exceed_end; - } + bool Bmi_Module_Formulation::is_model_initialized() const { + return model_initialized; + } - void Bmi_Module_Formulation::set_bmi_init_config(const std::string &init_config) { - bmi_init_config = init_config; - } - void Bmi_Module_Formulation::set_bmi_model(std::shared_ptr model) { - bmi_model = model; - } + void Bmi_Module_Formulation::set_allow_model_exceed_end_time(bool allow_exceed_end) { + allow_model_exceed_end_time = allow_exceed_end; + } - void Bmi_Module_Formulation::set_bmi_model_start_time_forcing_offset_s(const time_t &offset_s) { - bmi_model_start_time_forcing_offset_s = offset_s; - } + void Bmi_Module_Formulation::set_bmi_init_config(const std::string &init_config) { + bmi_init_config = init_config; + } + void Bmi_Module_Formulation::set_bmi_model(std::shared_ptr model) { + bmi_model = model; + } - void Bmi_Module_Formulation::set_bmi_model_time_step_fixed(bool is_fix_time_step) { - bmi_model_time_step_fixed = is_fix_time_step; - } + void Bmi_Module_Formulation::set_bmi_model_start_time_forcing_offset_s(const time_t &offset_s) { + bmi_model_start_time_forcing_offset_s = offset_s; + } - void Bmi_Module_Formulation::set_model_initialized(bool is_initialized) { - model_initialized = is_initialized; - } + void Bmi_Module_Formulation::set_bmi_model_time_step_fixed(bool is_fix_time_step) { + bmi_model_time_step_fixed = is_fix_time_step; + } - // TODO: need to modify this to support arrays properly, since in general that's what BMI modules deal with - template - std::shared_ptr get_value_as_type(std::string type, T value) - { - if (type == "double" || type == "double precision") - return std::make_shared( static_cast(value) ); + void Bmi_Module_Formulation::set_model_initialized(bool is_initialized) { + model_initialized = is_initialized; + } - if (type == "float" || type == "real") - return std::make_shared( static_cast(value) ); + // TODO: need to modify this to support arrays properly, since in general that's what BMI modules deal with + template + std::shared_ptr get_value_as_type(std::string type, T value) + { + if (type == "double" || type == "double precision") + return std::make_shared( static_cast(value) ); - if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") - return std::make_shared( static_cast(value) ); + if (type == "float" || type == "real") + return std::make_shared( static_cast(value) ); - if (type == "unsigned short" || type == "unsigned short int") - return std::make_shared( static_cast(value) ); + if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") + return std::make_shared( static_cast(value) ); - if (type == "int" || type == "signed" || type == "signed int" || type == "integer") - return std::make_shared( static_cast(value) ); + if (type == "unsigned short" || type == "unsigned short int") + return std::make_shared( static_cast(value) ); - if (type == "unsigned" || type == "unsigned int") - return std::make_shared( static_cast(value) ); + if (type == "int" || type == "signed" || type == "signed int" || type == "integer") + return std::make_shared( static_cast(value) ); - if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") - return std::make_shared( static_cast(value) ); + if (type == "unsigned" || type == "unsigned int") + return std::make_shared( static_cast(value) ); - if (type == "unsigned long" || type == "unsigned long int") - return std::make_shared( static_cast(value) ); + if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") + return std::make_shared( static_cast(value) ); - if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") - return std::make_shared( static_cast(value) ); + if (type == "unsigned long" || type == "unsigned long int") + return std::make_shared( static_cast(value) ); - if (type == "unsigned long long" || type == "unsigned long long int") - return std::make_shared( static_cast(value) ); + if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") + return std::make_shared( static_cast(value) ); - throw std::runtime_error("Unable to get value of variable as type '" + type + - "': no logic for converting value to variable's type."); - } + if (type == "unsigned long long" || type == "unsigned long long int") + return std::make_shared( static_cast(value) ); -void Bmi_Module_Formulation::set_model_inputs_prior_to_update(const double &model_init_time, time_step_t t_delta) { - std::vector in_var_names = get_bmi_model()->GetInputVarNames(); - time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); + throw std::runtime_error("Unable to get value of variable as type '" + type + + "': no logic for converting value to variable's type."); + } - // tiny helpers local to this function (no changes elsewhere) - auto to_lower = [](std::string s) { - std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); }); - return s; - }; - auto normalize_consumer_units = [&](const std::string& u)->std::string { - std::string s = to_lower(u); - if (s.empty() || s == "none" || s == "unitless" || s == "dimensionless" || s == "-") + void Bmi_Module_Formulation::set_model_inputs_prior_to_update(const double &model_init_time, time_step_t t_delta) { + std::vector in_var_names = get_bmi_model()->GetInputVarNames(); + time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); + + // tiny helpers local to this function (no changes elsewhere) + auto to_lower = [](std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); }); + return s; + }; + auto normalize_consumer_units = [&](const std::string& u)->std::string { + std::string s = to_lower(u); + if (s.empty() || s == "none" || s == "unitless" || s == "dimensionless" || s == "-") return "1"; // internal canonical for dimensionless - return u; - }; - auto is_nested_provider_guard = [](const std::string& msg)->bool { - return msg.find("Multi BMI formulation can't use associated data provider as a nested module") != std::string::npos; - }; + return u; + }; + auto is_nested_provider_guard = [](const std::string& msg)->bool { + return msg.find("Multi BMI formulation can't use associated data provider as a nested module") != std::string::npos; + }; - for (std::string & var_name : in_var_names) { - data_access::GenericDataProvider *provider; - std::string var_map_alias = get_config_mapped_variable_name(var_name); - if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_map_alias].get(); - } - else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_name].get(); - } - else { - provider = forcing.get(); - } + for (std::string & var_name : in_var_names) { + data_access::GenericDataProvider *provider; + std::string var_map_alias = get_config_mapped_variable_name(var_name); + if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_map_alias].get(); + } + else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_name].get(); + } + else { + provider = forcing.get(); + } - // array sizing - int nbytes = get_bmi_model()->GetVarNbytes(var_name); - int varItemSize = get_bmi_model()->GetVarItemsize(var_name); - int numItems = nbytes / varItemSize; - assert(nbytes % varItemSize == 0); + // array sizing + int nbytes = get_bmi_model()->GetVarNbytes(var_name); + int varItemSize = get_bmi_model()->GetVarItemsize(var_name); + int numItems = nbytes / varItemSize; + assert(nbytes % varItemSize == 0); - std::shared_ptr value_ptr; + std::shared_ptr value_ptr; - // resolve actual C++ type the BMI adapter expects - std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), varItemSize); + // resolve actual C++ type the BMI adapter expects + std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), varItemSize); - // normalize consumer units once; if truly "dimensionless" (1), ask provider for "" to avoid mm->1 conversions - const std::string consumer_units_raw = get_bmi_model()->GetVarUnits(var_name); - const std::string consumer_units_norm = normalize_consumer_units(consumer_units_raw); - const std::string units_for_selector = (consumer_units_norm == "1") ? std::string("") : consumer_units_norm; + // normalize consumer units once; if truly "dimensionless" (1), ask provider for "" to avoid mm->1 conversions + const std::string consumer_units_raw = get_bmi_model()->GetVarUnits(var_name); + const std::string consumer_units_norm = normalize_consumer_units(consumer_units_raw); + const std::string units_for_selector = (consumer_units_norm == "1") ? std::string("") : consumer_units_norm; - if (numItems != 1) { - // --- array input path --- - try { - auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(), - var_map_alias, model_epoch_time, t_delta, - units_for_selector)); - if(values.size() == 1){ - #ifndef NGEN_QUIET - std::stringstream ss; - ss << "WARN: broadcasting variable '" << var_name << "' from scalar to expected array\n"; - LOG(ss.str(), LogLevel::SEVERE); ss.str(""); - #endif - values.resize(numItems, values[0]); - } - else if (values.size() != numItems) { - throw std::runtime_error("Mismatch in item count for variable '" + var_name + "': model expects " + - std::to_string(numItems) + ", provider returned " + std::to_string(values.size()) + " items\n"); - } - value_ptr = get_values_as_type(type, values.begin(), values.end()); - } - catch (data_access::unit_conversion_exception &uce) { - // log once per unique producer/consumer pair - data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; - if (data_access::unit_errors_reported.insert(key).second) { - std::stringstream ss; - ss << "Unit conversion failure:" - << " requester '" << get_id() << "' catchment '" << get_catchment_id() << "' variable '" << var_map_alias << "'" - << " provider '" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" - << " raw values count " << uce.unconverted_values.size() - << " message \"" << uce.what() << "\""; - LOG(ss.str(), LogLevel::WARNING); + if (numItems != 1) { + // --- array input path --- + try { + auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(), + var_map_alias, model_epoch_time, t_delta, + units_for_selector)); + if(values.size() == 1){ +#ifndef NGEN_QUIET + std::stringstream ss; + ss << "WARN: broadcasting variable '" << var_name << "' from scalar to expected array\n"; + LOG(ss.str(), LogLevel::SEVERE); ss.str(""); +#endif + values.resize(numItems, values[0]); + } + else if (values.size() != numItems) { + throw std::runtime_error("Mismatch in item count for variable '" + var_name + "': model expects " + + std::to_string(numItems) + ", provider returned " + std::to_string(values.size()) + " items\n"); + } + value_ptr = get_values_as_type(type, values.begin(), values.end()); } - // fall back: use unconverted values if present, else zeros - std::vector fallback(numItems, 0.0); - if (!uce.unconverted_values.empty()) { - if (uce.unconverted_values.size() == 1) std::fill(fallback.begin(), fallback.end(), uce.unconverted_values[0]); - else if (uce.unconverted_values.size() == (size_t)numItems) fallback = uce.unconverted_values; - else { - // size mismatch: repeat or truncate - for (int i=0; i fallback(numItems, 0.0); + if (!uce.unconverted_values.empty()) { + if (uce.unconverted_values.size() == 1) std::fill(fallback.begin(), fallback.end(), uce.unconverted_values[0]); + else if (uce.unconverted_values.size() == (size_t)numItems) fallback = uce.unconverted_values; + else { + // size mismatch: repeat or truncate + for (int i=0; i zeros(numItems, 0.0); - value_ptr = get_values_as_type(type, zeros.begin(), zeros.end()); + catch (const std::exception &ex) { + if (is_nested_provider_guard(ex.what())) { + std::stringstream ss; + ss << "BMI coupling warning: nested provider disallowed for input array '" << var_name + << "' (alias '" << var_map_alias << "') in catchment '" << get_catchment_id() + << "'. Using fallback zeros this step. Configure an explicit provider mapping for this input."; + LOG(ss.str(), LogLevel::WARNING); + // zero array + std::vector zeros(numItems, 0.0); + value_ptr = get_values_as_type(type, zeros.begin(), zeros.end()); + } + else throw; } - else throw; - } - } - else { - // --- scalar input path --- - try { - double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(), - var_map_alias, model_epoch_time, t_delta, - units_for_selector)); - value_ptr = get_value_as_type(type, value); } - catch (data_access::unit_conversion_exception &uce) { - data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; - if (data_access::unit_errors_reported.insert(key).second) { - std::stringstream ss; - ss << "Unit conversion failure:" - << " requester '" << get_id() << "' catchment '" << get_catchment_id() << "' variable '" << var_map_alias << "'" - << " provider '" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" - << " raw value " << (uce.unconverted_values.empty() ? std::numeric_limits::quiet_NaN() : uce.unconverted_values[0]) - << " message \"" << uce.what() << "\""; - LOG(ss.str(), LogLevel::WARNING); + else { + // --- scalar input path --- + try { + double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(), + var_map_alias, model_epoch_time, t_delta, + units_for_selector)); + value_ptr = get_value_as_type(type, value); } - // fallback to producer value if present, else 0.0 - const double fallback = uce.unconverted_values.empty() ? 0.0 : uce.unconverted_values[0]; - value_ptr = get_value_as_type(type, fallback); - } - catch (const std::exception &ex) { - if (is_nested_provider_guard(ex.what())) { - std::stringstream ss; - ss << "BMI coupling warning: nested provider disallowed for input '" << var_name - << "' (alias '" << var_map_alias << "') in catchment '" << get_catchment_id() - << "'. Using fallback 0 this step. Configure an explicit provider mapping for this input."; - LOG(ss.str(), LogLevel::WARNING); - value_ptr = get_value_as_type(type, 0.0); + catch (data_access::unit_conversion_exception &uce) { + data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; + if (data_access::unit_errors_reported.insert(key).second) { + std::stringstream ss; + ss << "Unit conversion failure:" + << " requester '" << get_id() << "' catchment '" << get_catchment_id() << "' variable '" << var_map_alias << "'" + << " provider '" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" + << " raw value " << (uce.unconverted_values.empty() ? std::numeric_limits::quiet_NaN() : uce.unconverted_values[0]) + << " message \"" << uce.what() << "\""; + LOG(ss.str(), LogLevel::WARNING); + } + // fallback to producer value if present, else 0.0 + const double fallback = uce.unconverted_values.empty() ? 0.0 : uce.unconverted_values[0]; + value_ptr = get_value_as_type(type, fallback); + } + catch (const std::exception &ex) { + if (is_nested_provider_guard(ex.what())) { + std::stringstream ss; + ss << "BMI coupling warning: nested provider disallowed for input '" << var_name + << "' (alias '" << var_map_alias << "') in catchment '" << get_catchment_id() + << "'. Using fallback 0 this step. Configure an explicit provider mapping for this input."; + LOG(ss.str(), LogLevel::WARNING); + value_ptr = get_value_as_type(type, 0.0); + } + else throw; } - else throw; } - } - get_bmi_model()->SetValue(var_name, value_ptr.get()); + get_bmi_model()->SetValue(var_name, value_ptr.get()); + } } -} - void Bmi_Module_Formulation::append_model_inputs_to_stream(const double &model_init_time, time_step_t t_delta, std::stringstream &inputs) { - std::vector in_var_names = get_bmi_model()->GetInputVarNames(); - time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); - inputs << "Input variables were as follows:"; + void Bmi_Module_Formulation::append_model_inputs_to_stream(const double &model_init_time, time_step_t t_delta, std::stringstream &inputs) { + std::vector in_var_names = get_bmi_model()->GetInputVarNames(); + time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); + inputs << "Input variables were as follows:"; - for (std::string & var_name : in_var_names) { - data_access::GenericDataProvider *provider; - std::string var_map_alias = get_config_mapped_variable_name(var_name); - if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_map_alias].get(); - } - else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_name].get(); - } - else { - provider = forcing.get(); - } + for (std::string & var_name : in_var_names) { + data_access::GenericDataProvider *provider; + std::string var_map_alias = get_config_mapped_variable_name(var_name); + if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_map_alias].get(); + } + else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_name].get(); + } + else { + provider = forcing.get(); + } + + // TODO: probably need to actually allow this by default and warn, but have config option to activate + // this type of behavior + // TODO: account for arrays later + int nbytes = get_bmi_model()->GetVarNbytes(var_name); + int varItemSize = get_bmi_model()->GetVarItemsize(var_name); + int numItems = nbytes / varItemSize; - // TODO: probably need to actually allow this by default and warn, but have config option to activate - // this type of behavior - // TODO: account for arrays later - int nbytes = get_bmi_model()->GetVarNbytes(var_name); - int varItemSize = get_bmi_model()->GetVarItemsize(var_name); - int numItems = nbytes / varItemSize; - - std::shared_ptr value_ptr; - // Finally, use the value obtained to set the model input - std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), - varItemSize); + std::shared_ptr value_ptr; + // Finally, use the value obtained to set the model input + std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), + varItemSize); - inputs << "\n" << var_map_alias << " = "; - if (numItems != 1) { - //more than a single value needed for var_name - auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, - get_bmi_model()->GetVarUnits(var_name))); - value_ptr = get_values_as_type( type, values.begin(), values.end() ); - // array like input: precipitation_mm_per_h = [0.2, 0.8, 1.8] - this->append_inputs(type, value_ptr, numItems, inputs); - - } else { - //scalar value - double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, - get_bmi_model()->GetVarUnits(var_name))); - this->append_input(type, value, inputs); - } + inputs << "\n" << var_map_alias << " = "; + if (numItems != 1) { + //more than a single value needed for var_name + auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, + get_bmi_model()->GetVarUnits(var_name))); + value_ptr = get_values_as_type( type, values.begin(), values.end() ); + // array like input: precipitation_mm_per_h = [0.2, 0.8, 1.8] + this->append_inputs(type, value_ptr, numItems, inputs); + + } else { + //scalar value + double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, + get_bmi_model()->GetVarUnits(var_name))); + this->append_input(type, value, inputs); } } + } - template - void Bmi_Module_Formulation::append_inputs(std::shared_ptr values, int num_items, std::stringstream &inputs) { - T *array = (T*)values.get(); - inputs << "["; - for (int i = 0; i < num_items; ++i) { - if (i != 0) - inputs << ", "; - inputs << array[i]; - } - inputs << "]"; + template + void Bmi_Module_Formulation::append_inputs(std::shared_ptr values, int num_items, std::stringstream &inputs) { + T *array = (T*)values.get(); + inputs << "["; + for (int i = 0; i < num_items; ++i) { + if (i != 0) + inputs << ", "; + inputs << array[i]; } + inputs << "]"; + } - void Bmi_Module_Formulation::append_inputs(std::string type, std::shared_ptr values, int num_items, std::stringstream &inputs) { + void Bmi_Module_Formulation::append_inputs(std::string type, std::shared_ptr values, int num_items, std::stringstream &inputs) { - if (type == "double" || type == "double precision") - this->append_inputs(values, num_items, inputs); + if (type == "double" || type == "double precision") + this->append_inputs(values, num_items, inputs); - else if (type == "float" || type == "real") - this->append_inputs(values, num_items, inputs); + else if (type == "float" || type == "real") + this->append_inputs(values, num_items, inputs); - else if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") - this->append_inputs(values, num_items, inputs); + else if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") + this->append_inputs(values, num_items, inputs); - else if (type == "unsigned short" || type == "unsigned short int") - this->append_inputs(values, num_items, inputs); + else if (type == "unsigned short" || type == "unsigned short int") + this->append_inputs(values, num_items, inputs); - else if (type == "int" || type == "signed" || type == "signed int" || type == "integer") - this->append_inputs(values, num_items, inputs); + else if (type == "int" || type == "signed" || type == "signed int" || type == "integer") + this->append_inputs(values, num_items, inputs); - else if (type == "unsigned" || type == "unsigned int") - this->append_inputs(values, num_items, inputs); + else if (type == "unsigned" || type == "unsigned int") + this->append_inputs(values, num_items, inputs); - else if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") - this->append_inputs(values, num_items, inputs); + else if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") + this->append_inputs(values, num_items, inputs); - else if (type == "unsigned long" || type == "unsigned long int") - this->append_inputs(values, num_items, inputs); + else if (type == "unsigned long" || type == "unsigned long int") + this->append_inputs(values, num_items, inputs); - else if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") - this->append_inputs(values, num_items, inputs); + else if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") + this->append_inputs(values, num_items, inputs); - else if (type == "unsigned long long" || type == "unsigned long long int") - this->append_inputs(values, num_items, inputs); + else if (type == "unsigned long long" || type == "unsigned long long int") + this->append_inputs(values, num_items, inputs); - } + } - template - void Bmi_Module_Formulation::append_input(std::string type, T value, std::stringstream &inputs) { + template + void Bmi_Module_Formulation::append_input(std::string type, T value, std::stringstream &inputs) { - if (type == "double" || type == "double precision") - inputs << static_cast(value); + if (type == "double" || type == "double precision") + inputs << static_cast(value); - else if (type == "float" || type == "real") - inputs << static_cast(value); + else if (type == "float" || type == "real") + inputs << static_cast(value); - else if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") - inputs << static_cast(value); + else if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") + inputs << static_cast(value); - else if (type == "unsigned short" || type == "unsigned short int") - inputs << static_cast(value); + else if (type == "unsigned short" || type == "unsigned short int") + inputs << static_cast(value); - else if (type == "int" || type == "signed" || type == "signed int" || type == "integer") - inputs << static_cast(value); + else if (type == "int" || type == "signed" || type == "signed int" || type == "integer") + inputs << static_cast(value); - else if (type == "unsigned" || type == "unsigned int") - inputs << static_cast(value); + else if (type == "unsigned" || type == "unsigned int") + inputs << static_cast(value); - else if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") - inputs << static_cast(value); + else if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") + inputs << static_cast(value); - else if (type == "unsigned long" || type == "unsigned long int") - inputs << static_cast(value); + else if (type == "unsigned long" || type == "unsigned long int") + inputs << static_cast(value); - else if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") - inputs << static_cast(value); + else if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") + inputs << static_cast(value); - else if (type == "unsigned long long" || type == "unsigned long long int") - inputs << static_cast(value); + else if (type == "unsigned long long" || type == "unsigned long long int") + inputs << static_cast(value); - } + } - const boost::span Bmi_Module_Formulation::get_serialization_state() const { - auto bmi = this->bmi_model; - // create a new serialized state, getting the amount of data that was saved - uint64_t* size = (uint64_t*)bmi->GetValuePtr("serialization_create"); - // get the pointer of the new state - char* serialized = (char*)bmi->GetValuePtr("serialization_state"); - const boost::span span(serialized, *size); - return span; - } + const boost::span Bmi_Module_Formulation::get_serialization_state() const { + auto bmi = this->bmi_model; + // create a new serialized state, getting the amount of data that was saved + uint64_t* size = (uint64_t*)bmi->GetValuePtr("serialization_create"); + // get the pointer of the new state + char* serialized = (char*)bmi->GetValuePtr("serialization_state"); + const boost::span span(serialized, *size); + return span; + } - void Bmi_Module_Formulation::load_serialization_state(const boost::span state) const { - auto bmi = this->bmi_model; - // grab the pointer to the underlying state data - void* data = (void*)state.data(); - // load the state through SetValue - bmi->SetValue("serialization_state", data); - } + void Bmi_Module_Formulation::load_serialization_state(const boost::span state) const { + auto bmi = this->bmi_model; + // grab the pointer to the underlying state data + void* data = (void*)state.data(); + // load the state through SetValue + bmi->SetValue("serialization_state", data); + } - void Bmi_Module_Formulation::free_serialization_state() const { - auto bmi = this->bmi_model; - // send message to clear memory associated with serialized data - void* _; // this pointer will be unused by SetValue - bmi->SetValue("serialization_free", _); - } + void Bmi_Module_Formulation::free_serialization_state() const { + auto bmi = this->bmi_model; + // send message to clear memory associated with serialized data + void* _; // this pointer will be unused by SetValue + bmi->SetValue("serialization_free", _); + } } + From 18d48673404356c208d40b8d61f6862c73119a73 Mon Sep 17 00:00:00 2001 From: Mohammed Karim Date: Thu, 18 Sep 2025 06:59:30 -0700 Subject: [PATCH 3/3] unit conversion fix --- .../catchment/Bmi_Multi_Formulation.hpp | 747 ++++--- src/core/mediator/UnitsHelper.cpp | 35 - .../catchment/Bmi_Module_Formulation.cpp | 1730 ++++++++--------- 3 files changed, 1217 insertions(+), 1295 deletions(-) diff --git a/include/realizations/catchment/Bmi_Multi_Formulation.hpp b/include/realizations/catchment/Bmi_Multi_Formulation.hpp index 964bf5a03c..7f9effee02 100644 --- a/include/realizations/catchment/Bmi_Multi_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Multi_Formulation.hpp @@ -25,59 +25,59 @@ class Bmi_Cpp_Multi_Array_Test; namespace realization { /** - * Abstraction of a formulation with multiple backing model object that implements the BMI. - */ + * Abstraction of a formulation with multiple backing model object that implements the BMI. + */ class Bmi_Multi_Formulation : public Bmi_Formulation { - public: + public: typedef Bmi_Formulation nested_module_type; typedef std::shared_ptr nested_module_ptr; /** - * Minimal constructor for objects initialize using the Formulation_Manager and subsequent calls to - * ``create_formulation``. - * - * @param id - * @param forcing_config - * @param output_stream - */ + * Minimal constructor for objects initialize using the Formulation_Manager and subsequent calls to + * ``create_formulation``. + * + * @param id + * @param forcing_config + * @param output_stream + */ Bmi_Multi_Formulation(std::string id, std::shared_ptr forcing_provider, utils::StreamHandler output_stream) - : Bmi_Formulation(std::move(id), forcing_provider, output_stream) { }; + : Bmi_Formulation(std::move(id), forcing_provider, output_stream) { }; virtual ~Bmi_Multi_Formulation() {}; /** - * Convert a time value from the model to an epoch time in seconds. - * - * Model time values are typically (though not always) 0-based totals count upward as time progresses. The - * units are not necessarily seconds. This performs the necessary lookup and conversion for such units, and - * then shifts the value appropriately for epoch time representation. - * - * For this type, this function will behave in the same manner as the analogous function of the current - * "primary" nested formulation, which is found in the instance's ordered collection of nested module - * formulations at the index returned by @ref get_index_for_primary_module. - * - * @param model_time The time value in a model's representation that is to be converted. - * @return The equivalent epoch time. - */ + * Convert a time value from the model to an epoch time in seconds. + * + * Model time values are typically (though not always) 0-based totals count upward as time progresses. The + * units are not necessarily seconds. This performs the necessary lookup and conversion for such units, and + * then shifts the value appropriately for epoch time representation. + * + * For this type, this function will behave in the same manner as the analogous function of the current + * "primary" nested formulation, which is found in the instance's ordered collection of nested module + * formulations at the index returned by @ref get_index_for_primary_module. + * + * @param model_time The time value in a model's representation that is to be converted. + * @return The equivalent epoch time. + */ time_t convert_model_time(const double &model_time) const override { return convert_model_time(model_time, get_index_for_primary_module()); } /** - * Convert a time value from the model to an epoch time in seconds. - * - * Model time values are typically (though not always) 0-based totals count upward as time progresses. The - * units are not necessarily seconds. This performs the necessary lookup and conversion for such units, and - * then shifts the value appropriately for epoch time representation. - * - * For this type, this function will behave in the same manner as the analogous function of nested formulation - * found at the provided index within the instance's ordered collection of nested module formulations. - * - * @param model_time The time value in a model's representation that is to be converted. - * @return The equivalent epoch time. - */ + * Convert a time value from the model to an epoch time in seconds. + * + * Model time values are typically (though not always) 0-based totals count upward as time progresses. The + * units are not necessarily seconds. This performs the necessary lookup and conversion for such units, and + * then shifts the value appropriately for epoch time representation. + * + * For this type, this function will behave in the same manner as the analogous function of nested formulation + * found at the provided index within the instance's ordered collection of nested module formulations. + * + * @param model_time The time value in a model's representation that is to be converted. + * @return The equivalent epoch time. + */ inline time_t convert_model_time(const double &model_time, int module_index) const { return modules[module_index]->convert_model_time(model_time); } @@ -92,36 +92,36 @@ namespace realization { } /** - * Get whether a model may perform updates beyond its ``end_time``. - * - * Get whether model ``Update`` calls are allowed and handled in some way by the backing model for time steps - * after the model's ``end_time``. Implementations of this type should use this function to safeguard against - * entering either an invalid or otherwise undesired state as a result of attempting to process a model beyond - * its available data. - * - * As mentioned, even for models that are capable of validly handling processing beyond end time, it may be - * desired that they do not for some reason (e.g., the way they account for the lack of input data leads to - * valid but incorrect results for a specific application). Because of this, whether models are allowed to - * process beyond their end time is configuration-based. - * - * @return Whether a model may perform updates beyond its ``end_time``. - */ + * Get whether a model may perform updates beyond its ``end_time``. + * + * Get whether model ``Update`` calls are allowed and handled in some way by the backing model for time steps + * after the model's ``end_time``. Implementations of this type should use this function to safeguard against + * entering either an invalid or otherwise undesired state as a result of attempting to process a model beyond + * its available data. + * + * As mentioned, even for models that are capable of validly handling processing beyond end time, it may be + * desired that they do not for some reason (e.g., the way they account for the lack of input data leads to + * valid but incorrect results for a specific application). Because of this, whether models are allowed to + * process beyond their end time is configuration-based. + * + * @return Whether a model may perform updates beyond its ``end_time``. + */ const bool &get_allow_model_exceed_end_time() const override; /** - * Get the collection of forcing output property names this instance can provide. - * - * For this type, this is the collection of the names/aliases of the BMI output variables for nested modules; - * i.e., the config-mapped alias for the variable when set in the realization config, or just the name when no - * alias was included in the configuration. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The collection of forcing output property names this instance can provide. - * @see ForcingProvider - */ + * Get the collection of forcing output property names this instance can provide. + * + * For this type, this is the collection of the names/aliases of the BMI output variables for nested modules; + * i.e., the config-mapped alias for the variable when set in the realization config, or just the name when no + * alias was included in the configuration. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The collection of forcing output property names this instance can provide. + * @see ForcingProvider + */ boost::span get_available_variable_names() const override; /** @@ -137,91 +137,91 @@ namespace realization { const time_t &get_bmi_model_start_time_forcing_offset_s() const override; /** - * Get the output variables of the last nested BMI model. - * - * @return The output variables of the last nested BMI model. - */ + * Get the output variables of the last nested BMI model. + * + * @return The output variables of the last nested BMI model. + */ const std::vector get_bmi_output_variables() const override { return modules.back()->get_bmi_output_variables(); } /** - * When possible, translate a variable name for a BMI model to an internally recognized name. - * - * Because of the implementation of this type, this function can only translate variable names for input or - * output variables of either the first or last nested BMI module. In cases when this is not possible, it will - * return the original parameter. - * - * The function will only check input variable names for the first module and output variable names for the - * last module. Further, it will always check the first module first, returning if it finds a translation. - * Only then will it check the last module. This can be controlled by using the overloaded function - * @ref get_config_mapped_variable_name(string, bool, bool). - * - * To perform a similar translation between modules, see the overloaded function - * @ref get_config_mapped_variable_name(string, shared_ptr, shared_ptr). - * - * @param model_var_name The BMI variable name to translate so its purpose is recognized internally. - * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. - * @see get_config_mapped_variable_name(string, bool, bool) - * @see get_config_mapped_variable_name(string, shared_ptr, shared_ptr) - */ + * When possible, translate a variable name for a BMI model to an internally recognized name. + * + * Because of the implementation of this type, this function can only translate variable names for input or + * output variables of either the first or last nested BMI module. In cases when this is not possible, it will + * return the original parameter. + * + * The function will only check input variable names for the first module and output variable names for the + * last module. Further, it will always check the first module first, returning if it finds a translation. + * Only then will it check the last module. This can be controlled by using the overloaded function + * @ref get_config_mapped_variable_name(string, bool, bool). + * + * To perform a similar translation between modules, see the overloaded function + * @ref get_config_mapped_variable_name(string, shared_ptr, shared_ptr). + * + * @param model_var_name The BMI variable name to translate so its purpose is recognized internally. + * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. + * @see get_config_mapped_variable_name(string, bool, bool) + * @see get_config_mapped_variable_name(string, shared_ptr, shared_ptr) + */ const std::string &get_config_mapped_variable_name(const std::string &model_var_name) const override; /** - * When possible, translate a variable name for the first or last BMI model to an internally recognized name. - * - * Because of the implementation of this type, this function can only translate variable names for input or - * output variables of either the first or last nested BMI module. In cases when this is not possible, it will - * return the original parameter. - * - * The function can only check input variable names for the first module and output variable names for the - * last module. Parameters control whether each is actually checked. When both are ``true``, it will always - * check the first module first, returning if it finds a translation without checking the last. - * - * @param model_var_name The BMI variable name to translate so its purpose is recognized internally. - * @param check_first Whether an input variable mapping for the first module should sought. - * @param check_last Whether an output variable mapping for the last module should sought. - * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. - */ + * When possible, translate a variable name for the first or last BMI model to an internally recognized name. + * + * Because of the implementation of this type, this function can only translate variable names for input or + * output variables of either the first or last nested BMI module. In cases when this is not possible, it will + * return the original parameter. + * + * The function can only check input variable names for the first module and output variable names for the + * last module. Parameters control whether each is actually checked. When both are ``true``, it will always + * check the first module first, returning if it finds a translation without checking the last. + * + * @param model_var_name The BMI variable name to translate so its purpose is recognized internally. + * @param check_first Whether an input variable mapping for the first module should sought. + * @param check_last Whether an output variable mapping for the last module should sought. + * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. + */ const std::string &get_config_mapped_variable_name(const std::string &model_var_name, bool check_first, bool check_last) const; /** - * When possible, translate the name of an output variable for one BMI model to an input variable for another. - * - * This function behaves similarly to @ref get_config_mapped_variable_name(string), except that is performs the - * translation between modules (rather than between a module and the framework). As such, it is designed for - * translation between two sequential models, although this is not a requirement for valid execution. - * - * The function will first request the mapping for the parameter name from the outputting module, which will either - * return a mapped name or the original param. It will check if the returned value is one of the advertised BMI input - * variable names of the inputting module; if so, it returns that name. Otherwise, it proceeds. - * - * The function then iterates through all the BMI input variable names for the inputting module. If it finds any that - * maps to either the original parameter or the mapped name from the outputting module, it returns it. - * - * If neither of those find a mapping, then the original parameter is returned. - * - * Note that if this is not an output variable name of the outputting module, the function treats this as a no-mapping - * condition and returns the parameter. - * - * @param output_var_name The output variable to be translated. - * @param out_module The module having the output variable. - * @param in_module The module needing a translation of ``output_var_name`` to one of its input variable names. - * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. - */ + * When possible, translate the name of an output variable for one BMI model to an input variable for another. + * + * This function behaves similarly to @ref get_config_mapped_variable_name(string), except that is performs the + * translation between modules (rather than between a module and the framework). As such, it is designed for + * translation between two sequential models, although this is not a requirement for valid execution. + * + * The function will first request the mapping for the parameter name from the outputting module, which will either + * return a mapped name or the original param. It will check if the returned value is one of the advertised BMI input + * variable names of the inputting module; if so, it returns that name. Otherwise, it proceeds. + * + * The function then iterates through all the BMI input variable names for the inputting module. If it finds any that + * maps to either the original parameter or the mapped name from the outputting module, it returns it. + * + * If neither of those find a mapping, then the original parameter is returned. + * + * Note that if this is not an output variable name of the outputting module, the function treats this as a no-mapping + * condition and returns the parameter. + * + * @param output_var_name The output variable to be translated. + * @param out_module The module having the output variable. + * @param in_module The module needing a translation of ``output_var_name`` to one of its input variable names. + * @return Either the translated equivalent variable name, or the provided name if there is not a mapping entry. + */ const std::string &get_config_mapped_variable_name(const std::string &output_var_name, - const std::shared_ptr& out_module, - const std::shared_ptr& in_module) const; + const std::shared_ptr& out_module, + const std::shared_ptr& in_module) const; /** - * Get the inclusive beginning of the period of time over which this instance can provide data for this forcing. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The inclusive beginning of the period of time over which this instance can provide this data. - */ + * Get the inclusive beginning of the period of time over which this instance can provide data for this forcing. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The inclusive beginning of the period of time over which this instance can provide this data. + */ long get_data_start_time() const override @@ -230,14 +230,14 @@ namespace realization { } /** - * Get the inclusive beginning of the period of time over which this instance can provide data for this forcing. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The inclusive beginning of the period of time over which this instance can provide this data. - */ + * Get the inclusive beginning of the period of time over which this instance can provide data for this forcing. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The inclusive beginning of the period of time over which this instance can provide this data. + */ time_t get_variable_time_begin(const std::string &variable_name) const { std::string var_name = variable_name; @@ -256,14 +256,14 @@ namespace realization { } /** - * Get the exclusive ending of the period of time over which this instance can provide data for this forcing. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The exclusive ending of the period of time over which this instance can provide this data. - */ + * Get the exclusive ending of the period of time over which this instance can provide data for this forcing. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The exclusive ending of the period of time over which this instance can provide this data. + */ long get_data_stop_time() const override { @@ -271,14 +271,14 @@ namespace realization { } /** - * Get the exclusive ending of the period of time over which this instance can provide data for this forcing. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @return The exclusive ending of the period of time over which this instance can provide this data. - */ + * Get the exclusive ending of the period of time over which this instance can provide data for this forcing. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @return The exclusive ending of the period of time over which this instance can provide this data. + */ //time_t get_forcing_output_time_end(const std::string &forcing_name) { time_t get_variable_time_end(const std::string &variable_name) const { // when unspecified, assume all data is available for the same range. @@ -324,28 +324,28 @@ namespace realization { } /** - * Get the current time for the primary nested BMI model in its native format and units. - * - * @return The current time for the primary nested BMI model in its native format and units. - */ + * Get the current time for the primary nested BMI model in its native format and units. + * + * @return The current time for the primary nested BMI model in its native format and units. + */ const double get_model_current_time() const override { return modules[get_index_for_primary_module()]->get_model_current_time(); } /** - * Get the end time for the primary nested BMI model in its native format and units. - * - * @return The end time for the primary nested BMI model in its native format and units. - */ + * Get the end time for the primary nested BMI model in its native format and units. + * + * @return The end time for the primary nested BMI model in its native format and units. + */ const double get_model_end_time() const override { return modules[get_index_for_primary_module()]->get_model_end_time(); } /** - * Get the end time for the primary nested BMI model in its native format and units. - * - * @return The end time for the primary nested BMI model in its native format and units. - */ + * Get the end time for the primary nested BMI model in its native format and units. + * + * @return The end time for the primary nested BMI model in its native format and units. + */ const double get_model_start_time() { return modules[get_index_for_primary_module()]->get_data_start_time(); } @@ -355,18 +355,18 @@ namespace realization { double get_response(time_step_t t_index, time_step_t t_delta) override; /** - * Get the index of the forcing time step that contains the given point in time. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * An @ref std::out_of_range exception should be thrown if the time is not in any time step. - * - * @param epoch_time The point in time, as a seconds-based epoch time. - * @return The index of the forcing time step that contains the given point in time. - * @throws std::out_of_range If the given point is not in any time step. - */ + * Get the index of the forcing time step that contains the given point in time. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * An @ref std::out_of_range exception should be thrown if the time is not in any time step. + * + * @param epoch_time The point in time, as a seconds-based epoch time. + * @return The index of the forcing time step that contains the given point in time. + * @throws std::out_of_range If the given point is not in any time step. + */ size_t get_ts_index_for_time(const time_t &epoch_time) const override { // TODO: come back and implement if actually necessary for this type; for now don't use std::string throw_msg; throw_msg.assign("Bmi_Multi_Formulation does not yet implement get_ts_index_for_time"); @@ -375,26 +375,26 @@ namespace realization { } /** - * Get the value of a forcing property for an arbitrary time period, converting units if needed. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * For this type, the @ref availableData map contains available properties and either the external forcing - * provider or the internal nested module that provides that output property. That member is set during - * creation, within the @ref create_multi_formulation function. This function implementation simply defers to - * the function of the same name in the appropriate nested forcing provider. - * - * An @ref std::out_of_range exception should be thrown if the data for the time period is not available. - * - * @param output_name The name of the forcing property of interest. - * @param init_time_epoch The epoch time (in seconds) of the start of the time period. - * @param duration_seconds The length of the time period, in seconds. - * @param output_units The expected units of the desired output value. - * @return The value of the forcing property for the described time period, with units converted if needed. - * @throws std::out_of_range If data for the time period is not available. - */ + * Get the value of a forcing property for an arbitrary time period, converting units if needed. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * For this type, the @ref availableData map contains available properties and either the external forcing + * provider or the internal nested module that provides that output property. That member is set during + * creation, within the @ref create_multi_formulation function. This function implementation simply defers to + * the function of the same name in the appropriate nested forcing provider. + * + * An @ref std::out_of_range exception should be thrown if the data for the time period is not available. + * + * @param output_name The name of the forcing property of interest. + * @param init_time_epoch The epoch time (in seconds) of the start of the time period. + * @param duration_seconds The length of the time period, in seconds. + * @param output_units The expected units of the desired output value. + * @return The value of the forcing property for the described time period, with units converted if needed. + * @throws std::out_of_range If data for the time period is not available. + */ double get_value(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) override { std::string output_name = selector.get_variable_name(); @@ -429,43 +429,43 @@ namespace realization { bool is_bmi_input_variable(const std::string &var_name) const override; /** - * Test whether all backing models have fixed time step size. - * - * @return Whether all backing models has fixed time step size. - */ + * Test whether all backing models have fixed time step size. + * + * @return Whether all backing models has fixed time step size. + */ bool is_bmi_model_time_step_fixed() const override; bool is_bmi_output_variable(const std::string &var_name) const override; /** - * Test whether all backing models have been initialize using the BMI standard ``Initialize`` function. - * - * @return Whether all backing models have been initialize using the BMI standard ``Initialize`` function. - */ + * Test whether all backing models have been initialize using the BMI standard ``Initialize`` function. + * + * @return Whether all backing models have been initialize using the BMI standard ``Initialize`` function. + */ bool is_model_initialized() const override; /** - * Get whether a property's per-time-step values are each an aggregate sum over the entire time step. - * - * Certain properties, like rain fall, are aggregated sums over an entire time step. Others, such as pressure, - * are not such sums and instead something else like an instantaneous reading or an average value. - * - * It may be the case that forcing data is needed for some discretization different than the forcing time step. - * This aspect must be known in such cases to perform the appropriate value interpolation. - * - * For instances of this type, all output forcings fall under this category. - * - * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this - * type to be usable as "forcing" providers for situations when some other object needs to receive as an input - * (i.e., one of its forcings) a data property output from this object. - * - * @param name The name of the forcing property for which the current value is desired. - * @return Whether the property's value is an aggregate sum. - */ + * Get whether a property's per-time-step values are each an aggregate sum over the entire time step. + * + * Certain properties, like rain fall, are aggregated sums over an entire time step. Others, such as pressure, + * are not such sums and instead something else like an instantaneous reading or an average value. + * + * It may be the case that forcing data is needed for some discretization different than the forcing time step. + * This aspect must be known in such cases to perform the appropriate value interpolation. + * + * For instances of this type, all output forcings fall under this category. + * + * This is part of the @ref ForcingProvider interface. This interface must be implemented for items of this + * type to be usable as "forcing" providers for situations when some other object needs to receive as an input + * (i.e., one of its forcings) a data property output from this object. + * + * @param name The name of the forcing property for which the current value is desired. + * @return Whether the property's value is an aggregate sum. + */ bool is_property_sum_over_time_step(const std::string &name) const override { if (availableData.empty() || availableData.find(name) == availableData.end()) { std::string throw_msg; throw_msg.assign( - get_formulation_type() + " cannot get whether unknown property " + name + " is summation"); + get_formulation_type() + " cannot get whether unknown property " + name + " is summation"); LOG(throw_msg, LogLevel::WARNING); throw std::runtime_error(throw_msg); } @@ -473,30 +473,30 @@ namespace realization { } /** - * Get whether this time step goes beyond this formulations (i.e., any of it's modules') end time. - * - * @param t_index The time step index in question. - * @return Whether this time step goes beyond this formulations (i.e., any of it's modules') end time. - */ + * Get whether this time step goes beyond this formulations (i.e., any of it's modules') end time. + * + * @param t_index The time step index in question. + * @return Whether this time step goes beyond this formulations (i.e., any of it's modules') end time. + */ bool is_time_step_beyond_end_time(time_step_t t_index); /** - * Get the index of the primary module. - * - * @return The index of the primary module. - */ + * Get the index of the primary module. + * + * @return The index of the primary module. + */ inline int get_index_for_primary_module() const { return primary_module_index; } /** - * Set the index of the primary module. - * - * Note that this function does not alter the state of the class, or produce an error, if the index is out of - * range. - * - * @param index The index for the module. - */ + * Set the index of the primary module. + * + * Note that this function does not alter the state of the class, or produce an error, if the index is out of + * range. + * + * @param index The index for the module. + */ inline void set_index_for_primary_module(int index) { if (index < modules.size()) { primary_module_index = index; @@ -504,8 +504,8 @@ namespace realization { } /** - * Check that the output variable names in the global bmi_multi are valid names - */ + * Check that the output variable names in the global bmi_multi are valid names + */ void check_output_var_names() { // variable already checked if (is_out_vars_from_last_mod) { @@ -535,106 +535,88 @@ namespace realization { } } - protected: + protected: /** - * Creating a multi-BMI-module formulation from NGen config. - * - * @param properties - * @param needs_param_validation - */ + * Creating a multi-BMI-module formulation from NGen config. + * + * @param properties + * @param needs_param_validation + */ void create_multi_formulation(geojson::PropertyMap properties, bool needs_param_validation); /** - * Get value for some BMI model variable at a specific index. - * - * Function gets the value for a provided variable, retrieving the variable array from the backing model of the - * appropriate nested formulation. The function then returns the specific value at the desired index, cast as a - * double type. - * - * The function makes several assumptions: - * - * 1. `index` is within array bounds - * 2. `var_name` corresponds to a BMI variable for some nested module. - * 3. `var_name` is sufficient to identify what value needs to be retrieved - * 4. the type for output variable allows the value to be cast to a `double` appropriately - * - * Item 3. here can be inferred from 2. for non-multi formulations. For multi formulations, this means the - * provided ``var_name`` must either be a unique BMI variable name among all nested module, or a unique mapped - * alias to a specific variable in a specific module. - * - * It falls to users of this function (i.e., other functions) to ensure these assumptions hold before invoking. - * - * @param index - * @param var_name - * @return - */ + * Get value for some BMI model variable at a specific index. + * + * Function gets the value for a provided variable, retrieving the variable array from the backing model of the + * appropriate nested formulation. The function then returns the specific value at the desired index, cast as a + * double type. + * + * The function makes several assumptions: + * + * 1. `index` is within array bounds + * 2. `var_name` corresponds to a BMI variable for some nested module. + * 3. `var_name` is sufficient to identify what value needs to be retrieved + * 4. the type for output variable allows the value to be cast to a `double` appropriately + * + * Item 3. here can be inferred from 2. for non-multi formulations. For multi formulations, this means the + * provided ``var_name`` must either be a unique BMI variable name among all nested module, or a unique mapped + * alias to a specific variable in a specific module. + * + * It falls to users of this function (i.e., other functions) to ensure these assumptions hold before invoking. + * + * @param index + * @param var_name + * @return + */ double get_var_value_as_double(const int& index, const std::string& var_name) override { auto data_provider_iter = availableData.find(var_name); if (data_provider_iter == availableData.end()) { throw external::ExternalIntegrationException( - "Multi BMI formulation can't find correct nested module for BMI variable " + var_name + SOURCE_LOC); + "Multi BMI formulation can't find correct nested module for BMI variable " + var_name + SOURCE_LOC); } // Otherwise, we have a provider, and we can cast it based on the documented assumptions try { auto const& nested_module = data_provider_iter->second; long nested_module_time = nested_module->get_data_start_time() + ( this->get_model_current_time() - this->get_model_start_time() ); - - // **Minimal fix**: do NOT request dimensionless "1" here; pass "" to avoid any UDUNITS conversion attempt. - auto selector = CatchmentAggrDataSelector(this->get_catchment_id(), var_name, nested_module_time, this->record_duration(), ""); - - // TODO: After merge PR#405, try re-adding support for index + auto selector = CatchmentAggrDataSelector(this->get_catchment_id(),var_name,nested_module_time,this->record_duration(),""); + //TODO: After merge PR#405, try re-adding support for index return nested_module->get_value(selector); } catch (data_access::unit_conversion_exception &uce) { - // We now avoid conversion by passing "", but keep this path as a safety net. + // We asked for it as a dimensionless quantity, "1", just above static bool no_conversion_message_logged = false; if (!no_conversion_message_logged) { no_conversion_message_logged = true; LOG("Emitting output variables from Bmi_Multi_Formulation without unit conversion - see NGWPC-7604", LogLevel::WARNING); } - return uce.unconverted_values.empty() ? 0.0 : uce.unconverted_values[0]; + return uce.unconverted_values[0]; } - // If there was any problem with the cast and extraction of the value, avoid hard abort: + // If there was any problem with the cast and extraction of the value, throw runtime error catch (std::exception &e) { - std::string msg = e.what(); - // Soften both UDUNITS and nested-provider guard failures - if (msg.find("ut_get_converter()") != std::string::npos || - msg.find("Units not convertible") != std::string::npos || - msg.find("Multi BMI formulation can't use associated data provider as a nested module") != std::string::npos) { - - std::stringstream warn; - warn << "BMI multi output fetch warning for var '" << var_name - << "' in catchment '" << this->get_catchment_id() - << "': " << msg << " — using fallback 0 this step."; - LOG(warn.str(), LogLevel::WARNING); - return 0.0; - } - - // Unexpected error: keep previous behavior (log and throw) to avoid masking real bugs std::string throw_msg; throw_msg.assign("Multi BMI formulation can't use associated data provider as a nested module" - " when attempting to get values of BMI variable " + var_name + SOURCE_LOC); + " when attempting to get values of BMI variable " + var_name + SOURCE_LOC); LOG(throw_msg, LogLevel::WARNING); throw std::runtime_error(throw_msg); + // TODO: look at adjusting defs to move this function up in class hierarchy (or at least add TODO there) } } - /** - * Initialize the deferred associations with the providers in @ref deferredProviders. - * - * During nested formulation creation, when a nested formulation requires as input some output expected from - * soon-to-be-created (i.e., later in execution order) formulation (e.g., in a look-back scenario to an earlier - * time step), then a deferred provider gets registered with the nested module and has a reference added to - * the @ref deferredProviders member. This function goes through all such the deferred providers, ensures there - * is something available that can serve as the backing wrapped provider, and associates them. - */ + * Initialize the deferred associations with the providers in @ref deferredProviders. + * + * During nested formulation creation, when a nested formulation requires as input some output expected from + * soon-to-be-created (i.e., later in execution order) formulation (e.g., in a look-back scenario to an earlier + * time step), then a deferred provider gets registered with the nested module and has a reference added to + * the @ref deferredProviders member. This function goes through all such the deferred providers, ensures there + * is something available that can serve as the backing wrapped provider, and associates them. + */ inline void init_deferred_associations() { for (int d = 0; d < deferredProviders.size(); ++d) { std::shared_ptr &deferredProvider = deferredProviders[d]; // Skip doing anything for any deferred provider that already has its backing provider set if (deferredProvider->isWrappedProviderSet()) - continue; + continue; // TODO: improve this later; since defaults can be used, it is technically possible to grab something // valid when something more appropriate would later be available @@ -651,34 +633,34 @@ namespace realization { if (!deferredProvider->isWrappedProviderSet()) { // TODO: this probably needs to be some kind of custom configuration exception std::string msg = "Multi BMI formulation cannot be created from config: cannot find available data " - "provider to satisfy set of deferred provisions for nested module at index " - + std::to_string(deferredProviderModuleIndices[d]) + ": {"; - // There must always be at least 1; get manually to help with formatting - msg += deferredProvider->get_available_variable_names()[0]; - // And here make sure to start at 1 instead of 0 - for (int i = 1; i < deferredProvider->get_available_variable_names().size(); ++i) + "provider to satisfy set of deferred provisions for nested module at index " + + std::to_string(deferredProviderModuleIndices[d]) + ": {"; + // There must always be at least 1; get manually to help with formatting + msg += deferredProvider->get_available_variable_names()[0]; + // And here make sure to start at 1 instead of 0 + for (int i = 1; i < deferredProvider->get_available_variable_names().size(); ++i) msg += ", " + deferredProvider->get_available_variable_names()[i]; - msg += "}"; + msg += "}"; throw realization::ConfigurationException(msg); } } } /** - * Initialize a nested formulation from the given properties and update multi formulation metadata. - * - * This function creates a new formulation, processes the mapping of BMI variables, and adds outputs to the outer - * module's provideable data items. - * - * Note that it is VERY IMPORTANT that ``properties`` argument`` is provided by value, as this copy is - * potentially updated to perform per-feature pattern substitution for certain property element values. - * - * @tparam T The particular type for the nested formulation object. - * @param mod_index The index for the new formulation in this instance's collection of nested formulations. - * @param identifier The id of for the represented feature. - * @param properties A COPY of the nested module config properties for the nested formulation of interest. - * @return - */ + * Initialize a nested formulation from the given properties and update multi formulation metadata. + * + * This function creates a new formulation, processes the mapping of BMI variables, and adds outputs to the outer + * module's provideable data items. + * + * Note that it is VERY IMPORTANT that ``properties`` argument`` is provided by value, as this copy is + * potentially updated to perform per-feature pattern substitution for certain property element values. + * + * @tparam T The particular type for the nested formulation object. + * @param mod_index The index for the new formulation in this instance's collection of nested formulations. + * @param identifier The id of for the represented feature. + * @param properties A COPY of the nested module config properties for the nested formulation of interest. + * @return + */ template std::shared_ptr init_nested_module(int mod_index, std::string identifier, geojson::PropertyMap properties) { std::shared_ptr wfp = std::make_shared(this); @@ -686,7 +668,7 @@ namespace realization { // Since this is a nested formulation, support usage of the '{{id}}' syntax for init config file paths. Catchment_Formulation::config_pattern_substitution(properties, BMI_REALIZATION_CFG_PARAM_REQ__INIT_CONFIG, - "{{id}}", id); + "{{id}}", id); // Call create_formulation to perform the rest of the typical initialization steps for the formulation. mod->create_formulation(properties); @@ -713,10 +695,10 @@ namespace realization { (*var_aliases)[framework_alias] = var_name; if (availableData.count(framework_alias) > 0) { std::string throw_msg; throw_msg.assign( - "Multi BMI cannot be created with module " + mod->get_model_type_name() + - " with output variable " + framework_alias + - (var_name == framework_alias ? "" : " (an alias of BMI variable " + var_name + ")") + - " because a previous module is using this output variable name/alias."); + "Multi BMI cannot be created with module " + mod->get_model_type_name() + + " with output variable " + framework_alias + + (var_name == framework_alias ? "" : " (an alias of BMI variable " + var_name + ")") + + " because a previous module is using this output variable name/alias."); LOG(throw_msg, LogLevel::WARNING); throw std::runtime_error(throw_msg); } @@ -727,37 +709,37 @@ namespace realization { } /** - * A mapping of data properties to their providers. - * - * Keys for the data properties are unique, name-like identifiers. These could be a BMI output variable name - * for a module, or a configuration-mapped alias of such a variable. The intent is for any module needing any - * input data to also have either an input variable name or input variable name mapping identical to one of - * these keys (and though ordering is important at a higher level, it is not handled directly by this member). - */ + * A mapping of data properties to their providers. + * + * Keys for the data properties are unique, name-like identifiers. These could be a BMI output variable name + * for a module, or a configuration-mapped alias of such a variable. The intent is for any module needing any + * input data to also have either an input variable name or input variable name mapping identical to one of + * these keys (and though ordering is important at a higher level, it is not handled directly by this member). + */ std::map> availableData; - private: - - /** - * Setup a deferred provider for a nested module, tracking the class as needed. - * - * Create an optional wrapped provider for use with a nested module and some required variable it needs - * provided. Track this and the index of the nested modules in the member collections necessary for later - * initializing associations to backing providers that were originally deferred. Then, assign the created - * deferred wrapper provider as the provider for the variable in the nested module. - * - * @tparam T - * @param bmi_input_var_name The name of the required input variable a nested module needs provided. - * @param framework_output_name The framework alias of the required output variable that will be provided to the - * aforementioned input variable (which may be the same as ``bmi_input_var_name``). - * @param mod The nested module requiring a deferred wrapped provider for a variable. - * @param mod_index The index of the given nested module in the ordering of all this instance's nested modules. - */ + private: + + /** + * Setup a deferred provider for a nested module, tracking the class as needed. + * + * Create an optional wrapped provider for use with a nested module and some required variable it needs + * provided. Track this and the index of the nested modules in the member collections necessary for later + * initializing associations to backing providers that were originally deferred. Then, assign the created + * deferred wrapper provider as the provider for the variable in the nested module. + * + * @tparam T + * @param bmi_input_var_name The name of the required input variable a nested module needs provided. + * @param framework_output_name The framework alias of the required output variable that will be provided to the + * aforementioned input variable (which may be the same as ``bmi_input_var_name``). + * @param mod The nested module requiring a deferred wrapped provider for a variable. + * @param mod_index The index of the given nested module in the ordering of all this instance's nested modules. + */ template void setup_nested_deferred_provider(const std::string &bmi_input_var_name, - const std::string &framework_output_name, - std::shared_ptr mod, - int mod_index) { + const std::string &framework_output_name, + std::shared_ptr mod, + int mod_index) { // TODO: probably don't actually need bmi_input_var_name, and just can deal with framework_output_name // Create deferred, optional provider for providing this // Only include BMI variable name, as that's what'll be visible when associating to backing provider @@ -785,37 +767,37 @@ namespace realization { /** The set of available "forcings" (output variables, plus their mapped aliases) this instance can provide. */ std::vector available_forcings; /** - * Any configured default values for outputs, keyed by framework alias (or var name if this is globally unique). - */ + * Any configured default values for outputs, keyed by framework alias (or var name if this is globally unique). + */ std::map default_output_values; /** - * A collection of wrappers to nested formulations providing some output to an earlier nested formulation. - * - * During formulation creation, when a nested formulation requires as input some output from a later formulation - * (e.g., in a look-back scenario to an earlier time step), then an "optimistic" wrapper gets put into place. - * It assumes that the necessary provider will be available and associated once all nested formulations have - * been created. This member tracks these so that this deferred association can be done. - */ + * A collection of wrappers to nested formulations providing some output to an earlier nested formulation. + * + * During formulation creation, when a nested formulation requires as input some output from a later formulation + * (e.g., in a look-back scenario to an earlier time step), then an "optimistic" wrapper gets put into place. + * It assumes that the necessary provider will be available and associated once all nested formulations have + * been created. This member tracks these so that this deferred association can be done. + */ std::vector> deferredProviders; /** - * The module indices for the modules associated with each item in @ref deferredProviders. - * - * E.g., the value in this vector at index ``0`` is the index of a module within @ref modules. That module is - * what required the deferred provider in the @ref deferredProviders collection at its index ``0``. - */ + * The module indices for the modules associated with each item in @ref deferredProviders. + * + * E.g., the value in this vector at index ``0`` is the index of a module within @ref modules. That module is + * what required the deferred provider in the @ref deferredProviders collection at its index ``0``. + */ std::vector deferredProviderModuleIndices; /** - * Whether the @ref Bmi_Formulation::output_variable_names value is just the analogous value from this - * instance's final nested module. - */ + * Whether the @ref Bmi_Formulation::output_variable_names value is just the analogous value from this + * instance's final nested module. + */ bool is_out_vars_from_last_mod = false; /** The nested BMI modules composing this multi-module formulation, in their order of execution. */ std::vector modules; std::vector module_types; /** - * Per-module maps (ordered as in @ref modules) of configuration-mapped names to BMI variable names. - */ + * Per-module maps (ordered as in @ref modules) of configuration-mapped names to BMI variable names. + */ // TODO: confirm that we actually need this for something std::vector>> module_variable_maps; /** Index value (0-based) of the time step that will be processed by the next update of the model. */ @@ -830,4 +812,3 @@ namespace realization { } #endif //NGEN_BMI_MULTI_FORMULATION_HPP - diff --git a/src/core/mediator/UnitsHelper.cpp b/src/core/mediator/UnitsHelper.cpp index 1d88affb6e..f15e90f269 100644 --- a/src/core/mediator/UnitsHelper.cpp +++ b/src/core/mediator/UnitsHelper.cpp @@ -126,38 +126,3 @@ double* UnitsHelper::convert_values(const std::string &in_units, double* in_valu return out_values; } - -/* -double UnitsHelper::get_converted_value(const std::string &in_units, const double &value, const std::string &out_units) -{ - if(in_units == out_units){ - return value; // Early-out optimization - } - std::call_once(unit_system_inited, init_unit_system); - - auto converter = get_converter(in_units, out_units); - - double r = cv_convert_double(converter.get(), value); - return r; -} - -double* UnitsHelper::convert_values(const std::string &in_units, double* in_values, const std::string &out_units, double* out_values, const size_t& count) -{ - if(in_units == out_units){ - // Early-out optimization - if(in_values == out_values){ - return in_values; - } else { - memcpy(out_values, in_values, sizeof(double)*count); - return out_values; - } - } - std::call_once(unit_system_inited, init_unit_system); - - auto converter = get_converter(in_units, out_units); - - cv_convert_doubles(converter.get(), in_values, count, out_values); - - return out_values; -} -*/ diff --git a/src/realizations/catchment/Bmi_Module_Formulation.cpp b/src/realizations/catchment/Bmi_Module_Formulation.cpp index 559fa7525c..4e9d279bab 100644 --- a/src/realizations/catchment/Bmi_Module_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Module_Formulation.cpp @@ -3,992 +3,968 @@ #include #include "Logger.hpp" -#include -#include -#include -#include -#include -#include -#include - std::stringstream bmiform_ss; -/* -------------------- minimal helpers for units -------------------- */ -static inline std::string to_lower_copy(std::string s) { - std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); }); - return s; -} +namespace realization { + void Bmi_Module_Formulation::create_formulation(boost::property_tree::ptree &config, geojson::PropertyMap *global) { + geojson::PropertyMap options = this->interpret_parameters(config, global); + inner_create_formulation(options, false); + } -// Map BMI-reported "none"/"unitless"/"dimensionless"/"-"/"" → UDUNITS "1" -static inline std::string normalize_native_units(std::string u) { - std::string s = to_lower_copy(u); - if (s.empty() || s == "none" || s == "unitless" || s == "dimensionless" || s == "-") return "1"; - return u; -} + void Bmi_Module_Formulation::create_formulation(geojson::PropertyMap properties) { + inner_create_formulation(properties, true); + } -// For requested units: "" means unspecified (skip convert). -// "none"/"unitless"/"dimensionless"/"-" → "1" only if native is already dimensionless, else treat as unspecified. -static inline std::string normalize_requested_units(std::string u, const std::string& native_norm) { - if (u.empty()) return ""; - std::string s = to_lower_copy(u); - if (s == "none" || s == "unitless" || s == "dimensionless" || s == "-") - return (to_lower_copy(native_norm) == "1") ? std::string("1") : std::string(""); - return u; -} -/* ------------------------------------------------------------------ */ + boost::span Bmi_Module_Formulation::get_available_variable_names() const { + return available_forcings; + } -namespace realization { + std::string Bmi_Module_Formulation::get_output_line_for_timestep(int timestep, std::string delimiter) { + // TODO: something must be added to store values if more than the current time step is wanted + // TODO: if such a thing is added, it should probably be configurable to turn it off + if (timestep != (next_time_step_index - 1)) { + throw std::invalid_argument("Only current time step valid when getting output for BMI C++ formulation"); + } - // define static once (inside namespace) to satisfy linker - //std::set Bmi_Module_Formulation::unit_errors_reported{}; + static bool no_conversion_message_logged = false; + if (!no_conversion_message_logged) { + no_conversion_message_logged = true; + LOG("Emitting output variables from Bmi_Module_Formulation without unit conversion - see NGWPC-7604", LogLevel::WARNING); + } - void Bmi_Module_Formulation::create_formulation(boost::property_tree::ptree &config, geojson::PropertyMap *global) { - geojson::PropertyMap options = this->interpret_parameters(config, global); - inner_create_formulation(options, false); - } + std::string output_str; + for (const std::string& name : get_output_variable_names()) { + output_str += (output_str.empty() ? "" : ",") + std::to_string(get_var_value_as_double(0, name)); + } + return output_str; + } - void Bmi_Module_Formulation::create_formulation(geojson::PropertyMap properties) { - inner_create_formulation(properties, true); - } + double Bmi_Module_Formulation::get_response(time_step_t t_index, time_step_t t_delta) { + if (get_bmi_model() == nullptr) { + throw std::runtime_error("Trying to process response of improperly created BMI formulation of type '" + get_formulation_type() + "'."); + } + if (t_index < 0) { + throw std::invalid_argument("Getting response of negative time step in BMI formulation of type '" + get_formulation_type() + "' is not allowed."); + } + // Use (next_time_step_index - 1) so that second call with current time step index still works + if (t_index < (next_time_step_index - 1)) { + // TODO: consider whether we should (optionally) store and return historic values + throw std::invalid_argument("Getting response of previous time step in BMI formulation of type '" + get_formulation_type() + "' is not allowed."); + } - boost::span Bmi_Module_Formulation::get_available_variable_names() const { - return available_forcings; - } + // The time step delta size, expressed in the units internally used by the model + double t_delta_model_units; + if (next_time_step_index <= t_index) { + t_delta_model_units = get_bmi_model()->convert_seconds_to_model_time((double)t_delta); + double model_time = get_bmi_model()->GetCurrentTime(); + // Also, before running, make sure this doesn't cause a problem with model end_time + if (!get_allow_model_exceed_end_time()) { + int total_time_steps_to_process = abs((int)t_index - next_time_step_index) + 1; + if (get_bmi_model()->GetEndTime() < (model_time + (t_delta_model_units * total_time_steps_to_process))) { + throw std::invalid_argument("Cannot process BMI formulation of type '" + get_formulation_type() + "' to get response of future time step " + "that exceeds model end time."); + } + } + } - std::string Bmi_Module_Formulation::get_output_line_for_timestep(int timestep, std::string delimiter) { - // TODO: something must be added to store values if more than the current time step is wanted - // TODO: if such a thing is added, it should probably be configurable to turn it off - if (timestep != (next_time_step_index - 1)) { - throw std::invalid_argument("Only current time step valid when getting output for BMI C++ formulation"); + int update_method; + while (next_time_step_index <= t_index) { + double model_initial_time = get_bmi_model()->GetCurrentTime(); + set_model_inputs_prior_to_update(model_initial_time, t_delta); + try { + if (t_delta_model_units == get_bmi_model()->GetTimeStep()) { + update_method = 0; + get_bmi_model()->Update(); + } + else { + update_method = 1; + get_bmi_model()->UpdateUntil(model_initial_time + t_delta_model_units); + } + } catch (const std::exception &e) { + std::stringstream error_message; + error_message << "Model " << (update_method == 0 ? "Update" : "UpdateUntil") + << " failed on catchment " << this->get_catchment_id() + << ". t_index=" << t_index + << ", next_step_index=" << next_time_step_index << "\n"; + append_model_inputs_to_stream(model_initial_time, t_delta, error_message); + Logger::Log(LogLevel::FATAL, error_message.str()); + throw; + } + // TODO: again, consider whether we should store any historic response, ts_delta, or other var values + next_time_step_index++; + } + return get_var_value_as_double(0, get_bmi_main_output_var()); } - std::string output_str; - for (const std::string& name : get_output_variable_names()) { - output_str += (output_str.empty() ? "" : ",") + std::to_string(get_var_value_as_double(0, name)); + time_t Bmi_Module_Formulation::get_variable_time_begin(const std::string &variable_name) { + // TODO: come back and implement if actually necessary for this type; for now don't use + throw std::runtime_error("Bmi_Modular_Formulation does not yet implement get_variable_time_begin"); } - return output_str; - } - double Bmi_Module_Formulation::get_response(time_step_t t_index, time_step_t t_delta) { - if (get_bmi_model() == nullptr) { - throw std::runtime_error("Trying to process response of improperly created BMI formulation of type '" + get_formulation_type() + "'."); + long Bmi_Module_Formulation::get_data_start_time() const + { + return this->get_bmi_model()->GetStartTime(); } - if (t_index < 0) { - throw std::invalid_argument("Getting response of negative time step in BMI formulation of type '" + get_formulation_type() + "' is not allowed."); + + long Bmi_Module_Formulation::get_data_stop_time() const { + // TODO: come back and implement if actually necessary for this type; for now don't use + throw std::runtime_error("Bmi_Module_Formulation does not yet implement get_data_stop_time"); } - // Use (next_time_step_index - 1) so that second call with current time step index still works - if (t_index < (next_time_step_index - 1)) { - // TODO: consider whether we should (optionally) store and return historic values - throw std::invalid_argument("Getting response of previous time step in BMI formulation of type '" + get_formulation_type() + "' is not allowed."); + + long Bmi_Module_Formulation::record_duration() const { + throw std::runtime_error("Bmi_Module_Formulation does not yet implement record_duration"); } - // The time step delta size, expressed in the units internally used by the model - double t_delta_model_units; - if (next_time_step_index <= t_index) { - t_delta_model_units = get_bmi_model()->convert_seconds_to_model_time((double)t_delta); - double model_time = get_bmi_model()->GetCurrentTime(); - // Also, before running, make sure this doesn't cause a problem with model end_time - if (!get_allow_model_exceed_end_time()) { - int total_time_steps_to_process = abs((int)t_index - next_time_step_index) + 1; - if (get_bmi_model()->GetEndTime() < (model_time + (t_delta_model_units * total_time_steps_to_process))) { - throw std::invalid_argument("Cannot process BMI formulation of type '" + get_formulation_type() + "' to get response of future time step " - "that exceeds model end time."); - } - } + const double Bmi_Module_Formulation::get_model_current_time() const { + return get_bmi_model()->GetCurrentTime(); } - int update_method; - while (next_time_step_index <= t_index) { - double model_initial_time = get_bmi_model()->GetCurrentTime(); - set_model_inputs_prior_to_update(model_initial_time, t_delta); - try { - if (t_delta_model_units == get_bmi_model()->GetTimeStep()) { - update_method = 0; - get_bmi_model()->Update(); - } - else { - update_method = 1; - get_bmi_model()->UpdateUntil(model_initial_time + t_delta_model_units); - } - } catch (const std::exception &e) { - std::stringstream error_message; - error_message << "Model " << (update_method == 0 ? "Update" : "UpdateUntil") - << " failed on catchment " << this->get_catchment_id() - << ". t_index=" << t_index - << ", next_step_index=" << next_time_step_index << "\n"; - append_model_inputs_to_stream(model_initial_time, t_delta, error_message); - Logger::Log(LogLevel::FATAL, error_message.str()); - throw; - } - // TODO: again, consider whether we should store any historic response, ts_delta, or other var values - next_time_step_index++; - } - return get_var_value_as_double(0, get_bmi_main_output_var()); - } - - time_t Bmi_Module_Formulation::get_variable_time_begin(const std::string &variable_name) { - // TODO: come back and implement if actually necessary for this type; for now don't use - throw std::runtime_error("Bmi_Modular_Formulation does not yet implement get_variable_time_begin"); - } - - long Bmi_Module_Formulation::get_data_start_time() const - { - return this->get_bmi_model()->GetStartTime(); - } - - long Bmi_Module_Formulation::get_data_stop_time() const { - // TODO: come back and implement if actually necessary for this type; for now don't use - throw std::runtime_error("Bmi_Module_Formulation does not yet implement get_data_stop_time"); - } - - long Bmi_Module_Formulation::record_duration() const { - throw std::runtime_error("Bmi_Module_Formulation does not yet implement record_duration"); - } - - const double Bmi_Module_Formulation::get_model_current_time() const { - return get_bmi_model()->GetCurrentTime(); - } - - const double Bmi_Module_Formulation::get_model_end_time() const { - return get_bmi_model()->GetEndTime(); - } - - const std::vector& Bmi_Module_Formulation::get_required_parameters() const { - return REQUIRED_PARAMETERS; - } - - const std::string& Bmi_Module_Formulation::get_config_mapped_variable_name(const std::string &model_var_name) const { - // TODO: need to introduce validation elsewhere that all mapped names are valid AORC field constants. - if (bmi_var_names_map.find(model_var_name) != bmi_var_names_map.end()) - return bmi_var_names_map.at(model_var_name); - else - return model_var_name; - } - - size_t Bmi_Module_Formulation::get_ts_index_for_time(const time_t &epoch_time) const { - // TODO: come back and implement if actually necessary for this type; for now don't use - throw std::runtime_error("Bmi_Singular_Formulation does not yet implement get_ts_index_for_time"); - } - - struct unit_conversion_exception : public std::runtime_error { - unit_conversion_exception(std::string message) : std::runtime_error(message) {} - std::string provider_model_name; - std::string provider_bmi_var_name; - std::vector unconverted_values; - }; - - std::vector Bmi_Module_Formulation::get_values(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) - { - std::string output_name = selector.get_variable_name(); - time_t init_time = selector.get_init_time(); - long duration_s = selector.get_duration_secs(); - std::string output_units = selector.get_output_units(); - - // First make sure this is an available output - auto forcing_outputs = get_available_variable_names(); - if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { - throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); + const double Bmi_Module_Formulation::get_model_end_time() const { + return get_bmi_model()->GetEndTime(); } - // check if output is available from BMI - std::string bmi_var_name; - get_bmi_output_var_name(output_name, bmi_var_name); + const std::vector& Bmi_Module_Formulation::get_required_parameters() const { + return REQUIRED_PARAMETERS; + } - if( !bmi_var_name.empty() ) - { - auto model = get_bmi_model().get(); - //Get vector of double values for variable - auto values = models::bmi::GetValue(*model, bmi_var_name); - - // Convert units (normalize native + requested; skip if unspecified/equal) - std::string native_units_raw = get_bmi_model()->GetVarUnits(bmi_var_name); - std::string native_units = normalize_native_units(native_units_raw); - std::string desired_units = normalize_requested_units(output_units, native_units); - try { - if (desired_units.empty() || to_lower_copy(desired_units) == to_lower_copy(native_units)) { - return values; // no conversion required - } - UnitsHelper::convert_values(native_units, values.data(), desired_units, values.data(), values.size()); - return values; - } - catch (const std::runtime_error& e) { - unit_conversion_exception uce(e.what()); - uce.provider_model_name = get_id(); - uce.provider_bmi_var_name = bmi_var_name; - uce.unconverted_values = std::move(values); - throw uce; - } - } - //This is unlikely (impossible?) to throw since a pre-check on available names is done above. Assert instead? - throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); - } - - double Bmi_Module_Formulation::get_value(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) - { - std::string output_name = selector.get_variable_name(); - time_t init_time = selector.get_init_time(); - long duration_s = selector.get_duration_secs(); - std::string output_units = selector.get_output_units(); - - // First make sure this is an available output - auto forcing_outputs = get_available_variable_names(); - if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { - throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); + const std::string& Bmi_Module_Formulation::get_config_mapped_variable_name(const std::string &model_var_name) const { + // TODO: need to introduce validation elsewhere that all mapped names are valid AORC field constants. + if (bmi_var_names_map.find(model_var_name) != bmi_var_names_map.end()) + return bmi_var_names_map.at(model_var_name); + else + return model_var_name; } - // check if output is available from BMI - std::string bmi_var_name; - get_bmi_output_var_name(output_name, bmi_var_name); + size_t Bmi_Module_Formulation::get_ts_index_for_time(const time_t &epoch_time) const { + // TODO: come back and implement if actually necessary for this type; for now don't use + throw std::runtime_error("Bmi_Singular_Formulation does not yet implement get_ts_index_for_time"); + } - if( !bmi_var_name.empty() ) + std::vector Bmi_Module_Formulation::get_values(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) { - //Get forcing value from BMI variable - double value = get_var_value_as_double(0, bmi_var_name); - - // Convert units (normalize native + requested; skip if unspecified/equal) - std::string native_units_raw = get_bmi_model()->GetVarUnits(bmi_var_name); - std::string native_units = normalize_native_units(native_units_raw); - std::string desired_units = normalize_requested_units(output_units, native_units); - try { - if (desired_units.empty() || to_lower_copy(desired_units) == to_lower_copy(native_units)) { - return value; // no conversion required - } - return UnitsHelper::get_converted_value(native_units, value, desired_units); + std::string output_name = selector.get_variable_name(); + time_t init_time = selector.get_init_time(); + long duration_s = selector.get_duration_secs(); + std::string output_units = selector.get_output_units(); + + // First make sure this is an available output + auto forcing_outputs = get_available_variable_names(); + if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { + throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); } - catch (const std::runtime_error& e){ - unit_conversion_exception uce(e.what()); - uce.provider_model_name = get_id(); - uce.provider_bmi_var_name = bmi_var_name; - uce.unconverted_values.push_back(value); - throw uce; + // TODO: do this, or something better, later; right now, just assume anything using this as a provider is + // consistent with times + /* + if (last_model_response_delta == 0 && last_model_response_start_time == 0) { + throw runtime_error(get_formulation_type() + " does not properly set output time validity ranges " + "needed to provide outputs as forcings"); } - } - - //This is unlikely (impossible?) to throw since a pre-check on available names is done above. Assert instead? - throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); - } + */ + + // check if output is available from BMI + std::string bmi_var_name; + get_bmi_output_var_name(output_name, bmi_var_name); + + if( !bmi_var_name.empty() ) + { + auto model = get_bmi_model().get(); + //Get vector of double values for variable + //The return type of the vector here dependent on what + //needs to use it. For other BMI moudles, that is runtime dependent + //on the type of the requesting module + auto values = models::bmi::GetValue(*model, bmi_var_name); + + // Convert units + std::string native_units = get_bmi_model()->GetVarUnits(bmi_var_name); + + // minimal robustness: if no units requested, return native + if (output_units.empty()) { + return values; + } + auto norm = [](std::string u) { + std::transform(u.begin(), u.end(), u.begin(), [](unsigned char c){ return std::tolower(c); }); + if (u.empty() || u == "none" || u == "-" || u == "unitless" || u == "dimensionless") return std::string("1"); + return u; + }; + std::string native_units_norm = norm(native_units); + std::string output_units_norm = norm(output_units); + if (native_units_norm == output_units_norm) { + return values; + } + try { + UnitsHelper::convert_values(native_units_norm, values.data(), output_units_norm, values.data(), values.size()); + return values; + } + catch (const std::runtime_error& e) { + data_access::unit_conversion_exception uce(e.what()); + uce.provider_model_name = get_bmi_model()->get_model_name(); // provider component name + uce.provider_bmi_var_name = bmi_var_name; + uce.provider_units = native_units; // include units for the logger + uce.unconverted_values = std::move(values); + throw uce; + } - static bool is_var_name_in_collection(const std::vector &all_names, const std::string &var_name) { - return std::count(all_names.begin(), all_names.end(), var_name) > 0; - } + } + //This is unlikely (impossible?) to throw since a pre-check on available names is done above. Assert instead? + throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); + } - bool Bmi_Module_Formulation::is_bmi_input_variable(const std::string &var_name) const { - return is_var_name_in_collection(get_bmi_input_variables(), var_name); - } + double Bmi_Module_Formulation::get_value(const CatchmentAggrDataSelector& selector, data_access::ReSampleMethod m) + { + std::string output_name = selector.get_variable_name(); + time_t init_time = selector.get_init_time(); + long duration_s = selector.get_duration_secs(); + std::string output_units = selector.get_output_units(); + + // First make sure this is an available output + auto forcing_outputs = get_available_variable_names(); + if (std::find(forcing_outputs.begin(), forcing_outputs.end(), output_name) == forcing_outputs.end()) { + throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); + } + // TODO: do this, or something better, later; right now, just assume anything using this as a provider is + // consistent with times + /* + if (last_model_response_delta == 0 && last_model_response_start_time == 0) { + throw runtime_error(get_formulation_type() + " does not properly set output time validity ranges " + "needed to provide outputs as forcings"); + } + */ - bool Bmi_Module_Formulation::is_bmi_output_variable(const std::string &var_name) const { - return is_var_name_in_collection(get_bmi_output_variables(), var_name); - } + // check if output is available from BMI + std::string bmi_var_name; + get_bmi_output_var_name(output_name, bmi_var_name); - bool Bmi_Module_Formulation::is_property_sum_over_time_step(const std::string& name) const { - // TODO: verify with some kind of proof that "always true" is appropriate - return true; - } + if( !bmi_var_name.empty() ) + { + //Get forcing value from BMI variable + double value = get_var_value_as_double(0, bmi_var_name); - const std::vector Bmi_Module_Formulation::get_bmi_input_variables() const { - return get_bmi_model()->GetInputVarNames(); - } + // Convert units + std::string native_units = get_bmi_model()->GetVarUnits(bmi_var_name); - const std::vector Bmi_Module_Formulation::get_bmi_output_variables() const { - return get_bmi_model()->GetOutputVarNames(); - } + // minimal robustness: if no units requested, return native + if (output_units.empty()) { + return value; + } + auto norm = [](std::string u) { + std::transform(u.begin(), u.end(), u.begin(), [](unsigned char c){ return std::tolower(c); }); + if (u.empty() || u == "none" || u == "-" || u == "unitless" || u == "dimensionless") return std::string("1"); + return u; + }; + std::string native_units_norm = norm(native_units); + std::string output_units_norm = norm(output_units); + if (native_units_norm == output_units_norm) { + return value; + } - void Bmi_Module_Formulation::get_bmi_output_var_name(const std::string &name, std::string &bmi_var_name) - { - //check standard output names first - std::vector output_names = get_bmi_model()->GetOutputVarNames(); - if (std::find(output_names.begin(), output_names.end(), name) != output_names.end()) { - bmi_var_name = name; - } - else - { - //check mapped names - std::string mapped_name; - for (auto & iter : bmi_var_names_map) { - if (iter.second == name) { - mapped_name = iter.first; - break; + try { + return UnitsHelper::get_converted_value(native_units_norm, value, output_units_norm); } + catch (const std::runtime_error& e) { + data_access::unit_conversion_exception uce(e.what()); + uce.provider_model_name = get_bmi_model()->get_model_name(); // provider component name + uce.provider_bmi_var_name = bmi_var_name; + uce.provider_units = native_units; // include units for the logger + uce.unconverted_values.push_back(value); + throw uce; + } + } - //ensure mapped name maps to an output variable, see GH #393 =) - if (std::find(output_names.begin(), output_names.end(), mapped_name) != output_names.end()){ - bmi_var_name = mapped_name; - } - //else not an output variable + + //This is unlikely (impossible?) to throw since a pre-check on available names is done above. Assert instead? + throw std::runtime_error(get_formulation_type() + " received invalid output forcing name " + output_name); + } + + static bool is_var_name_in_collection(const std::vector &all_names, const std::string &var_name) { + return std::count(all_names.begin(), all_names.end(), var_name) > 0; } - } - void Bmi_Module_Formulation::determine_model_time_offset() { - set_bmi_model_start_time_forcing_offset_s( - // TODO: Look at making this epoch start configurable instead of from forcing - forcing->get_data_start_time() - convert_model_time(get_bmi_model()->GetStartTime())); - } + bool Bmi_Module_Formulation::is_bmi_input_variable(const std::string &var_name) const { + return is_var_name_in_collection(get_bmi_input_variables(), var_name); + } - const bool& Bmi_Module_Formulation::get_allow_model_exceed_end_time() const { - return allow_model_exceed_end_time; - } + bool Bmi_Module_Formulation::is_bmi_output_variable(const std::string &var_name) const { + return is_var_name_in_collection(get_bmi_output_variables(), var_name); + } - const std::string& Bmi_Module_Formulation::get_bmi_init_config() const { - return bmi_init_config; - } + bool Bmi_Module_Formulation::is_property_sum_over_time_step(const std::string& name) const { + // TODO: verify with some kind of proof that "always true" is appropriate + return true; + } - std::shared_ptr Bmi_Module_Formulation::get_bmi_model() const { - return bmi_model; - } + const std::vector Bmi_Module_Formulation::get_bmi_input_variables() const { + return get_bmi_model()->GetInputVarNames(); + } - const time_t& Bmi_Module_Formulation::get_bmi_model_start_time_forcing_offset_s() const { - return bmi_model_start_time_forcing_offset_s; - } + const std::vector Bmi_Module_Formulation::get_bmi_output_variables() const { + return get_bmi_model()->GetOutputVarNames(); + } - void Bmi_Module_Formulation::inner_create_formulation(geojson::PropertyMap properties, bool needs_param_validation) { - if (needs_param_validation) { - validate_parameters(properties); + void Bmi_Module_Formulation::get_bmi_output_var_name(const std::string &name, std::string &bmi_var_name) + { + //check standard output names first + std::vector output_names = get_bmi_model()->GetOutputVarNames(); + if (std::find(output_names.begin(), output_names.end(), name) != output_names.end()) { + bmi_var_name = name; + } + else + { + //check mapped names + std::string mapped_name; + for (auto & iter : bmi_var_names_map) { + if (iter.second == name) { + mapped_name = iter.first; + break; + } + } + //ensure mapped name maps to an output variable, see GH #393 =) + if (std::find(output_names.begin(), output_names.end(), mapped_name) != output_names.end()){ + bmi_var_name = mapped_name; + } + //else not an output variable + } } - // Required parameters first - set_bmi_init_config(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__INIT_CONFIG).as_string()); - set_bmi_main_output_var(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__MAIN_OUT_VAR).as_string()); - set_model_type_name(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__MODEL_TYPE).as_string()); - // Then optional ... + void Bmi_Module_Formulation::determine_model_time_offset() { + set_bmi_model_start_time_forcing_offset_s( + // TODO: Look at making this epoch start configurable instead of from forcing + forcing->get_data_start_time() - convert_model_time(get_bmi_model()->GetStartTime())); + } - auto uses_forcings_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__USES_FORCINGS); - if (uses_forcings_it != properties.end() && uses_forcings_it->second.as_boolean()) { - throw std::runtime_error("The '" BMI_REALIZATION_CFG_PARAM_OPT__USES_FORCINGS "' parameter was removed and cannot be set"); + const bool& Bmi_Module_Formulation::get_allow_model_exceed_end_time() const { + return allow_model_exceed_end_time; } - auto forcing_file_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__FORCING_FILE); - if (forcing_file_it != properties.end() && forcing_file_it->second.as_string() != "") { - throw std::runtime_error("The '" BMI_REALIZATION_CFG_PARAM_OPT__FORCING_FILE "' parameter was removed and cannot be set " + forcing_file_it->second.as_string()); + const std::string& Bmi_Module_Formulation::get_bmi_init_config() const { + return bmi_init_config; } - if (properties.find(BMI_REALIZATION_CFG_PARAM_OPT__ALLOW_EXCEED_END) != properties.end()) { - set_allow_model_exceed_end_time( - properties.at(BMI_REALIZATION_CFG_PARAM_OPT__ALLOW_EXCEED_END).as_boolean()); + std::shared_ptr Bmi_Module_Formulation::get_bmi_model() const { + return bmi_model; } - if (properties.find(BMI_REALIZATION_CFG_PARAM_OPT__FIXED_TIME_STEP) != properties.end()) { - set_bmi_model_time_step_fixed( - properties.at(BMI_REALIZATION_CFG_PARAM_OPT__FIXED_TIME_STEP).as_boolean()); + + const time_t& Bmi_Module_Formulation::get_bmi_model_start_time_forcing_offset_s() const { + return bmi_model_start_time_forcing_offset_s; } - auto std_names_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__VAR_STD_NAMES); - if (std_names_it != properties.end()) { - geojson::PropertyMap names_map = std_names_it->second.get_values(); - for (auto& names_it : names_map) { - bmi_var_names_map.insert( - std::pair(names_it.first, names_it.second.as_string())); - } - } - - // Do this next, since after checking whether other input variables are present in the properties, we can - // now construct the adapter and init the model - set_bmi_model(construct_model(properties)); - - //Check if any parameter values need to be set on the BMI model, - //and set them before it is run - set_initial_bmi_parameters(properties); - - // Make sure that this is able to interpret model time and convert to real time, since BMI model time is - // usually starting at 0 and just counting up - determine_model_time_offset(); - - // Output variable subset and order, if present - auto out_var_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUT_VARS); - if (out_var_it != properties.end()) { - std::vector out_vars_json_list = out_var_it->second.as_list(); - std::vector out_vars(out_vars_json_list.size()); - for (int i = 0; i < out_vars_json_list.size(); ++i) { - out_vars[i] = out_vars_json_list[i].as_string(); - } - set_output_variable_names(out_vars); - } - // Otherwise, just take what literally is provided by the model - else { - set_output_variable_names(get_bmi_model()->GetOutputVarNames()); - } - - // Output header fields, if present - auto out_headers_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUT_HEADER_FIELDS); - if (out_headers_it != properties.end()) { - std::vector out_headers_json_list = out_var_it->second.as_list(); - std::vector out_headers(out_headers_json_list.size()); - for (int i = 0; i < out_headers_json_list.size(); ++i) { - out_headers[i] = out_headers_json_list[i].as_string(); - } - set_output_header_fields(out_headers); - } - else { - set_output_header_fields(get_output_variable_names()); - } - - // Output precision, if present - auto out_precision_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUTPUT_PRECISION); - if (out_precision_it != properties.end()) { - set_output_precision(properties.at(BMI_REALIZATION_CFG_PARAM_OPT__OUTPUT_PRECISION).as_natural_number()); - } - - // Finally, make sure this is set - model_initialized = get_bmi_model()->is_model_initialized(); - - // Get output variable names - if (model_initialized) { - for (const std::string &output_var_name : get_bmi_model()->GetOutputVarNames()) { - available_forcings.push_back(output_var_name); - if (bmi_var_names_map.find(output_var_name) != bmi_var_names_map.end()) - available_forcings.push_back(bmi_var_names_map[output_var_name]); - } - } - } - /** - * @brief Template function for copying iterator range into contiguous array. - * - * This function will iterate the range and cast the iterator value to type T - * and copy that value into a C-like array of contiguous, dynamically allocated memory. - * This array is stored in a smart pointer with a custom array deleter. - * - * @tparam T - * @tparam Iterator - * @param begin - * @param end - * @return std::shared_ptr - */ - template - std::shared_ptr as_c_array(Iterator begin, Iterator end){ - //Make a shared pointer large enough to hold all elements - //This is a CONTIGUOUS array of type T - //Must provide a custom deleter to delete the array - std::shared_ptr ptr( new T[std::distance(begin, end)], [](T *p) { delete[] p; } ); - Iterator it = begin; - int i = 0; - while(it != end){ - //Be safe and cast the input to the desired type - ptr.get()[i] = static_cast(*it); - ++it; - ++i; - } - return ptr; - } - - /** - * @brief Gets values in iterator range, casted based on @p type then returned as typeless (void) pointer. - * - * @tparam Iterator - * @param type - * @param begin - * @param end - * @return std::shared_ptr - */ - template - std::shared_ptr get_values_as_type(std::string type, Iterator begin, Iterator end) - { - //Use std::vector range constructor to ensure contiguous storage of values - //Return the pointer to the contiguous storage - if (type == "double" || type == "double precision") - return as_c_array(begin, end); - - if (type == "float" || type == "real") - return as_c_array(begin, end); - - if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") - return as_c_array(begin, end); - - if (type == "unsigned short" || type == "unsigned short int") - return as_c_array(begin, end); - - if (type == "int" || type == "signed" || type == "signed int" || type == "integer") - return as_c_array(begin, end); - - if (type == "unsigned" || type == "unsigned int") - return as_c_array(begin, end); - - if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") - return as_c_array(begin, end); - - if (type == "unsigned long" || type == "unsigned long int") - return as_c_array(begin, end); - - if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") - return as_c_array(begin, end); - - if (type == "unsigned long long" || type == "unsigned long long int") - return as_c_array(begin, end); - - throw std::runtime_error("Unable to get values of iterable as type" + type + - " : no logic for converting values to variable's type."); - } - - - void Bmi_Module_Formulation::set_initial_bmi_parameters(geojson::PropertyMap properties) { - auto model = get_bmi_model(); - if( model == nullptr ) return; - //Now that the model is ready, we can set some intial parameters passed in the config - auto model_params = properties.find("model_params"); - - if (model_params != properties.end() ){ - - geojson::PropertyMap params = model_params->second.get_values(); - //Declare/init the possible vectors here - //reuse them for each loop iteration, make sure to clear them - std::vector long_vec; - std::vector double_vec; - //not_supported - //std::vector str_vec; - //std::vector bool_vec; - std::shared_ptr value_ptr; - for (auto& param : params) { - //Get some basic BMI info for this param - int varItemSize = get_bmi_model()->GetVarItemsize(param.first); - int totalBytes = get_bmi_model()->GetVarNbytes(param.first); - - //Figure out the c++ type to convert data to - std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(param.first), - varItemSize); - //TODO might consider refactoring as_vector and get_values_as_type - //(and by extension, as_c_array) into the JSONProperty class - //then instead of the PropertyVariant visitor filling vectors - //it could fill the c-like array and avoid another copy. - switch( param.second.get_type() ){ - case geojson::PropertyType::Natural: - param.second.as_vector(long_vec); - value_ptr = get_values_as_type(type, long_vec.begin(), long_vec.end()); - break; - case geojson::PropertyType::Real: - param.second.as_vector(double_vec); - value_ptr = get_values_as_type(type, double_vec.begin(), double_vec.end()); - break; - /* Not currently supporting string parameter values - case geojson::PropertyType::String: - param.second.as_vector(str_vec); - value_ptr = get_values_as_type(type, long_vec.begin(), long_vec.end()); - */ - /* Not currently supporting native bool (true/false) parameter values (use int 0/1) - case geojson::PropertyType::Boolean: - param.second.as_vector(bool_vec); - //data_ptr = bool_vec.data(); - */ - case geojson::PropertyType::List: - //In this case, only supporting numeric lists - //will retrieve as double (longs will get casted) - //TODO consider some additional introspection/optimization for this? - param.second.as_vector(double_vec); - if(double_vec.size() == 0){ - //logging::warning(("Cannot pass non-numeric lists as a BMI parameter, skipping "+param.first+"\n").c_str()); - bmiform_ss << "Cannot pass non-numeric lists as a BMI parameter, skipping " << param.first << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); - continue; - } - value_ptr = get_values_as_type(type, double_vec.begin(), double_vec.end()); - break; - default: - //logging::warning(("Cannot pass parameter of type "+geojson::get_propertytype_name(param.second.get_type())+" as a BMI parameter, skipping "+param.first+"\n").c_str()); - bmiform_ss << "Cannot pass parameter of type " << geojson::get_propertytype_name(param.second.get_type()) << " as a BMI parameter, skipping " << param.first << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); - continue; - } - try{ + void Bmi_Module_Formulation::inner_create_formulation(geojson::PropertyMap properties, bool needs_param_validation) { + if (needs_param_validation) { + validate_parameters(properties); + } + // Required parameters first + set_bmi_init_config(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__INIT_CONFIG).as_string()); + set_bmi_main_output_var(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__MAIN_OUT_VAR).as_string()); + set_model_type_name(properties.at(BMI_REALIZATION_CFG_PARAM_REQ__MODEL_TYPE).as_string()); + + // Then optional ... + + auto uses_forcings_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__USES_FORCINGS); + if (uses_forcings_it != properties.end() && uses_forcings_it->second.as_boolean()) { + throw std::runtime_error("The '" BMI_REALIZATION_CFG_PARAM_OPT__USES_FORCINGS "' parameter was removed and cannot be set"); + } + + auto forcing_file_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__FORCING_FILE); + if (forcing_file_it != properties.end() && forcing_file_it->second.as_string() != "") { + throw std::runtime_error("The '" BMI_REALIZATION_CFG_PARAM_OPT__FORCING_FILE "' parameter was removed and cannot be set " + forcing_file_it->second.as_string()); + } + + if (properties.find(BMI_REALIZATION_CFG_PARAM_OPT__ALLOW_EXCEED_END) != properties.end()) { + set_allow_model_exceed_end_time( + properties.at(BMI_REALIZATION_CFG_PARAM_OPT__ALLOW_EXCEED_END).as_boolean()); + } + if (properties.find(BMI_REALIZATION_CFG_PARAM_OPT__FIXED_TIME_STEP) != properties.end()) { + set_bmi_model_time_step_fixed( + properties.at(BMI_REALIZATION_CFG_PARAM_OPT__FIXED_TIME_STEP).as_boolean()); + } - // Finally, use the value obtained to set the model param - get_bmi_model()->SetValue(param.first, value_ptr.get()); + auto std_names_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__VAR_STD_NAMES); + if (std_names_it != properties.end()) { + geojson::PropertyMap names_map = std_names_it->second.get_values(); + for (auto& names_it : names_map) { + bmi_var_names_map.insert( + std::pair(names_it.first, names_it.second.as_string())); } - catch (const std::exception &e) - { - // logging::warning((std::string("Exception setting parameter value: ")+e.what()).c_str()); - // logging::warning(("Skipping parameter: "+param.first+"\n").c_str()); - bmiform_ss << "Exception setting parameter value: " << e.what() << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); - bmiform_ss << "Skipping parameter: " << param.first << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + } + + // Do this next, since after checking whether other input variables are present in the properties, we can + // now construct the adapter and init the model + set_bmi_model(construct_model(properties)); + + //Check if any parameter values need to be set on the BMI model, + //and set them before it is run + set_initial_bmi_parameters(properties); + + // Make sure that this is able to interpret model time and convert to real time, since BMI model time is + // usually starting at 0 and just counting up + determine_model_time_offset(); + + // Output variable subset and order, if present + auto out_var_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUT_VARS); + if (out_var_it != properties.end()) { + std::vector out_vars_json_list = out_var_it->second.as_list(); + std::vector out_vars(out_vars_json_list.size()); + for (int i = 0; i < out_vars_json_list.size(); ++i) { + out_vars[i] = out_vars_json_list[i].as_string(); } - catch (...) - { - // logging::warning((std::string("Unknown Exception setting parameter value: \n")).c_str()); - // logging::warning(("Skipping parameter: "+param.first+"\n").c_str()); - bmiform_ss << "Unknown Exception setting parameter value" << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); - bmiform_ss << "Skipping parameter: " << param.first << std::endl; - LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + set_output_variable_names(out_vars); + } + // Otherwise, just take what literally is provided by the model + else { + set_output_variable_names(get_bmi_model()->GetOutputVarNames()); + } + + // Output header fields, if present + auto out_headers_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUT_HEADER_FIELDS); + if (out_headers_it != properties.end()) { + std::vector out_headers_json_list = out_var_it->second.as_list(); + std::vector out_headers(out_headers_json_list.size()); + for (int i = 0; i < out_headers_json_list.size(); ++i) { + out_headers[i] = out_headers_json_list[i].as_string(); } - long_vec.clear(); - double_vec.clear(); - //Not supported - //str_vec.clear(); - //bool_vec.clear(); - } - - } - //TODO use SetValue(name, vector) overloads??? - //implment the overloads in each adapter - //ensure proper type is prepared before setting value - } - bool Bmi_Module_Formulation::is_bmi_model_time_step_fixed() const { - return bmi_model_time_step_fixed; - } - - bool Bmi_Module_Formulation::is_model_initialized() const { - return model_initialized; - } - - void Bmi_Module_Formulation::set_allow_model_exceed_end_time(bool allow_exceed_end) { - allow_model_exceed_end_time = allow_exceed_end; - } - - void Bmi_Module_Formulation::set_bmi_init_config(const std::string &init_config) { - bmi_init_config = init_config; - } - void Bmi_Module_Formulation::set_bmi_model(std::shared_ptr model) { - bmi_model = model; - } - - void Bmi_Module_Formulation::set_bmi_model_start_time_forcing_offset_s(const time_t &offset_s) { - bmi_model_start_time_forcing_offset_s = offset_s; - } - - void Bmi_Module_Formulation::set_bmi_model_time_step_fixed(bool is_fix_time_step) { - bmi_model_time_step_fixed = is_fix_time_step; - } - - void Bmi_Module_Formulation::set_model_initialized(bool is_initialized) { - model_initialized = is_initialized; - } - - // TODO: need to modify this to support arrays properly, since in general that's what BMI modules deal with - template - std::shared_ptr get_value_as_type(std::string type, T value) - { - if (type == "double" || type == "double precision") - return std::make_shared( static_cast(value) ); - - if (type == "float" || type == "real") - return std::make_shared( static_cast(value) ); - - if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") - return std::make_shared( static_cast(value) ); - - if (type == "unsigned short" || type == "unsigned short int") - return std::make_shared( static_cast(value) ); - - if (type == "int" || type == "signed" || type == "signed int" || type == "integer") - return std::make_shared( static_cast(value) ); - - if (type == "unsigned" || type == "unsigned int") - return std::make_shared( static_cast(value) ); - - if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") - return std::make_shared( static_cast(value) ); - - if (type == "unsigned long" || type == "unsigned long int") - return std::make_shared( static_cast(value) ); - - if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") - return std::make_shared( static_cast(value) ); - - if (type == "unsigned long long" || type == "unsigned long long int") - return std::make_shared( static_cast(value) ); - - throw std::runtime_error("Unable to get value of variable as type '" + type + - "': no logic for converting value to variable's type."); - } - - void Bmi_Module_Formulation::set_model_inputs_prior_to_update(const double &model_init_time, time_step_t t_delta) { - std::vector in_var_names = get_bmi_model()->GetInputVarNames(); - time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); - - // tiny helpers local to this function (no changes elsewhere) - auto to_lower = [](std::string s) { - std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); }); - return s; - }; - auto normalize_consumer_units = [&](const std::string& u)->std::string { - std::string s = to_lower(u); - if (s.empty() || s == "none" || s == "unitless" || s == "dimensionless" || s == "-") - return "1"; // internal canonical for dimensionless - return u; - }; - auto is_nested_provider_guard = [](const std::string& msg)->bool { - return msg.find("Multi BMI formulation can't use associated data provider as a nested module") != std::string::npos; - }; - - for (std::string & var_name : in_var_names) { - data_access::GenericDataProvider *provider; - std::string var_map_alias = get_config_mapped_variable_name(var_name); - if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_map_alias].get(); - } - else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_name].get(); + set_output_header_fields(out_headers); } else { - provider = forcing.get(); + set_output_header_fields(get_output_variable_names()); } - // array sizing - int nbytes = get_bmi_model()->GetVarNbytes(var_name); - int varItemSize = get_bmi_model()->GetVarItemsize(var_name); - int numItems = nbytes / varItemSize; - assert(nbytes % varItemSize == 0); + // Output precision, if present + auto out_precision_it = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__OUTPUT_PRECISION); + if (out_precision_it != properties.end()) { + set_output_precision(properties.at(BMI_REALIZATION_CFG_PARAM_OPT__OUTPUT_PRECISION).as_natural_number()); + } - std::shared_ptr value_ptr; + // Finally, make sure this is set + model_initialized = get_bmi_model()->is_model_initialized(); - // resolve actual C++ type the BMI adapter expects - std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), varItemSize); + // Get output variable names + if (model_initialized) { + for (const std::string &output_var_name : get_bmi_model()->GetOutputVarNames()) { + available_forcings.push_back(output_var_name); + if (bmi_var_names_map.find(output_var_name) != bmi_var_names_map.end()) + available_forcings.push_back(bmi_var_names_map[output_var_name]); + } + } + } + /** + * @brief Template function for copying iterator range into contiguous array. + * + * This function will iterate the range and cast the iterator value to type T + * and copy that value into a C-like array of contiguous, dynamically allocated memory. + * This array is stored in a smart pointer with a custom array deleter. + * + * @tparam T + * @tparam Iterator + * @param begin + * @param end + * @return std::shared_ptr + */ + template + std::shared_ptr as_c_array(Iterator begin, Iterator end){ + //Make a shared pointer large enough to hold all elements + //This is a CONTIGUOUS array of type T + //Must provide a custom deleter to delete the array + std::shared_ptr ptr( new T[std::distance(begin, end)], [](T *p) { delete[] p; } ); + Iterator it = begin; + int i = 0; + while(it != end){ + //Be safe and cast the input to the desired type + ptr.get()[i] = static_cast(*it); + ++it; + ++i; + } + return ptr; + } - // normalize consumer units once; if truly "dimensionless" (1), ask provider for "" to avoid mm->1 conversions - const std::string consumer_units_raw = get_bmi_model()->GetVarUnits(var_name); - const std::string consumer_units_norm = normalize_consumer_units(consumer_units_raw); - const std::string units_for_selector = (consumer_units_norm == "1") ? std::string("") : consumer_units_norm; + /** + * @brief Gets values in iterator range, casted based on @p type then returned as typeless (void) pointer. + * + * @tparam Iterator + * @param type + * @param begin + * @param end + * @return std::shared_ptr + */ + template + std::shared_ptr get_values_as_type(std::string type, Iterator begin, Iterator end) + { + //Use std::vector range constructor to ensure contiguous storage of values + //Return the pointer to the contiguous storage + if (type == "double" || type == "double precision") + return as_c_array(begin, end); - if (numItems != 1) { - // --- array input path --- - try { - auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(), - var_map_alias, model_epoch_time, t_delta, - units_for_selector)); - if(values.size() == 1){ -#ifndef NGEN_QUIET - std::stringstream ss; - ss << "WARN: broadcasting variable '" << var_name << "' from scalar to expected array\n"; - LOG(ss.str(), LogLevel::SEVERE); ss.str(""); -#endif - values.resize(numItems, values[0]); - } - else if (values.size() != numItems) { - throw std::runtime_error("Mismatch in item count for variable '" + var_name + "': model expects " + - std::to_string(numItems) + ", provider returned " + std::to_string(values.size()) + " items\n"); + if (type == "float" || type == "real") + return as_c_array(begin, end); + + if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") + return as_c_array(begin, end); + + if (type == "unsigned short" || type == "unsigned short int") + return as_c_array(begin, end); + + if (type == "int" || type == "signed" || type == "signed int" || type == "integer") + return as_c_array(begin, end); + + if (type == "unsigned" || type == "unsigned int") + return as_c_array(begin, end); + + if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") + return as_c_array(begin, end); + + if (type == "unsigned long" || type == "unsigned long int") + return as_c_array(begin, end); + + if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") + return as_c_array(begin, end); + + if (type == "unsigned long long" || type == "unsigned long long int") + return as_c_array(begin, end); + + throw std::runtime_error("Unable to get values of iterable as type" + type + + " : no logic for converting values to variable's type."); + } + + + void Bmi_Module_Formulation::set_initial_bmi_parameters(geojson::PropertyMap properties) { + auto model = get_bmi_model(); + if( model == nullptr ) return; + //Now that the model is ready, we can set some intial parameters passed in the config + auto model_params = properties.find("model_params"); + + if (model_params != properties.end() ){ + + geojson::PropertyMap params = model_params->second.get_values(); + //Declare/init the possible vectors here + //reuse them for each loop iteration, make sure to clear them + std::vector long_vec; + std::vector double_vec; + //not_supported + //std::vector str_vec; + //std::vector bool_vec; + std::shared_ptr value_ptr; + for (auto& param : params) { + //Get some basic BMI info for this param + int varItemSize = get_bmi_model()->GetVarItemsize(param.first); + int totalBytes = get_bmi_model()->GetVarNbytes(param.first); + + //Figure out the c++ type to convert data to + std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(param.first), + varItemSize); + //TODO might consider refactoring as_vector and get_values_as_type + //(and by extension, as_c_array) into the JSONProperty class + //then instead of the PropertyVariant visitor filling vectors + //it could fill the c-like array and avoid another copy. + switch( param.second.get_type() ){ + case geojson::PropertyType::Natural: + param.second.as_vector(long_vec); + value_ptr = get_values_as_type(type, long_vec.begin(), long_vec.end()); + break; + case geojson::PropertyType::Real: + param.second.as_vector(double_vec); + value_ptr = get_values_as_type(type, double_vec.begin(), double_vec.end()); + break; + /* Not currently supporting string parameter values + case geojson::PropertyType::String: + param.second.as_vector(str_vec); + value_ptr = get_values_as_type(type, long_vec.begin(), long_vec.end()); + */ + /* Not currently supporting native bool (true/false) parameter values (use int 0/1) + case geojson::PropertyType::Boolean: + param.second.as_vector(bool_vec); + //data_ptr = bool_vec.data(); + */ + case geojson::PropertyType::List: + //In this case, only supporting numeric lists + //will retrieve as double (longs will get casted) + //TODO consider some additional introspection/optimization for this? + param.second.as_vector(double_vec); + if(double_vec.size() == 0){ + //logging::warning(("Cannot pass non-numeric lists as a BMI parameter, skipping "+param.first+"\n").c_str()); + bmiform_ss << "Cannot pass non-numeric lists as a BMI parameter, skipping " << param.first << std::endl; + LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + continue; + } + value_ptr = get_values_as_type(type, double_vec.begin(), double_vec.end()); + break; + default: + //logging::warning(("Cannot pass parameter of type "+geojson::get_propertytype_name(param.second.get_type())+" as a BMI parameter, skipping "+param.first+"\n").c_str()); + bmiform_ss << "Cannot pass parameter of type " << geojson::get_propertytype_name(param.second.get_type()) << " as a BMI parameter, skipping " << param.first << std::endl; + LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + continue; } - value_ptr = get_values_as_type(type, values.begin(), values.end()); - } - catch (data_access::unit_conversion_exception &uce) { - // log once per unique producer/consumer pair - data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; - if (data_access::unit_errors_reported.insert(key).second) { - std::stringstream ss; - ss << "Unit conversion failure:" - << " requester '" << get_id() << "' catchment '" << get_catchment_id() << "' variable '" << var_map_alias << "'" - << " provider '" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" - << " raw values count " << uce.unconverted_values.size() - << " message \"" << uce.what() << "\""; - LOG(ss.str(), LogLevel::WARNING); + try{ + + // Finally, use the value obtained to set the model param + get_bmi_model()->SetValue(param.first, value_ptr.get()); } - // fall back: use unconverted values if present, else zeros - std::vector fallback(numItems, 0.0); - if (!uce.unconverted_values.empty()) { - if (uce.unconverted_values.size() == 1) std::fill(fallback.begin(), fallback.end(), uce.unconverted_values[0]); - else if (uce.unconverted_values.size() == (size_t)numItems) fallback = uce.unconverted_values; - else { - // size mismatch: repeat or truncate - for (int i=0; i zeros(numItems, 0.0); - value_ptr = get_values_as_type(type, zeros.begin(), zeros.end()); + catch (...) + { +// logging::warning((std::string("Unknown Exception setting parameter value: \n")).c_str()); +// logging::warning(("Skipping parameter: "+param.first+"\n").c_str()); + bmiform_ss << "Unknown Exception setting parameter value" << std::endl; + LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); + bmiform_ss << "Skipping parameter: " << param.first << std::endl; + LOG(bmiform_ss.str(), LogLevel::SEVERE); bmiform_ss.str(""); } - else throw; + long_vec.clear(); + double_vec.clear(); + //Not supported + //str_vec.clear(); + //bool_vec.clear(); } + } - else { - // --- scalar input path --- - try { - double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(), - var_map_alias, model_epoch_time, t_delta, - units_for_selector)); - value_ptr = get_value_as_type(type, value); + //TODO use SetValue(name, vector) overloads??? + //implment the overloads in each adapter + //ensure proper type is prepared before setting value + } + bool Bmi_Module_Formulation::is_bmi_model_time_step_fixed() const { + return bmi_model_time_step_fixed; + } + + bool Bmi_Module_Formulation::is_model_initialized() const { + return model_initialized; + } + + void Bmi_Module_Formulation::set_allow_model_exceed_end_time(bool allow_exceed_end) { + allow_model_exceed_end_time = allow_exceed_end; + } + + void Bmi_Module_Formulation::set_bmi_init_config(const std::string &init_config) { + bmi_init_config = init_config; + } + void Bmi_Module_Formulation::set_bmi_model(std::shared_ptr model) { + bmi_model = model; + } + + void Bmi_Module_Formulation::set_bmi_model_start_time_forcing_offset_s(const time_t &offset_s) { + bmi_model_start_time_forcing_offset_s = offset_s; + } + + void Bmi_Module_Formulation::set_bmi_model_time_step_fixed(bool is_fix_time_step) { + bmi_model_time_step_fixed = is_fix_time_step; + } + + void Bmi_Module_Formulation::set_model_initialized(bool is_initialized) { + model_initialized = is_initialized; + } + + // TODO: need to modify this to support arrays properly, since in general that's what BMI modules deal with + template + std::shared_ptr get_value_as_type(std::string type, T value) + { + if (type == "double" || type == "double precision") + return std::make_shared( static_cast(value) ); + + if (type == "float" || type == "real") + return std::make_shared( static_cast(value) ); + + if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") + return std::make_shared( static_cast(value) ); + + if (type == "unsigned short" || type == "unsigned short int") + return std::make_shared( static_cast(value) ); + + if (type == "int" || type == "signed" || type == "signed int" || type == "integer") + return std::make_shared( static_cast(value) ); + + if (type == "unsigned" || type == "unsigned int") + return std::make_shared( static_cast(value) ); + + if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") + return std::make_shared( static_cast(value) ); + + if (type == "unsigned long" || type == "unsigned long int") + return std::make_shared( static_cast(value) ); + + if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") + return std::make_shared( static_cast(value) ); + + if (type == "unsigned long long" || type == "unsigned long long int") + return std::make_shared( static_cast(value) ); + + throw std::runtime_error("Unable to get value of variable as type '" + type + + "': no logic for converting value to variable's type."); + } + + void Bmi_Module_Formulation::set_model_inputs_prior_to_update(const double &model_init_time, time_step_t t_delta) { + std::vector in_var_names = get_bmi_model()->GetInputVarNames(); + time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); + + for (std::string & var_name : in_var_names) { + data_access::GenericDataProvider *provider; + std::string var_map_alias = get_config_mapped_variable_name(var_name); + if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_map_alias].get(); } - catch (data_access::unit_conversion_exception &uce) { - data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; - if (data_access::unit_errors_reported.insert(key).second) { - std::stringstream ss; - ss << "Unit conversion failure:" - << " requester '" << get_id() << "' catchment '" << get_catchment_id() << "' variable '" << var_map_alias << "'" - << " provider '" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" - << " raw value " << (uce.unconverted_values.empty() ? std::numeric_limits::quiet_NaN() : uce.unconverted_values[0]) - << " message \"" << uce.what() << "\""; - LOG(ss.str(), LogLevel::WARNING); - } - // fallback to producer value if present, else 0.0 - const double fallback = uce.unconverted_values.empty() ? 0.0 : uce.unconverted_values[0]; - value_ptr = get_value_as_type(type, fallback); + else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_name].get(); + } + else { + provider = forcing.get(); } - catch (const std::exception &ex) { - if (is_nested_provider_guard(ex.what())) { - std::stringstream ss; - ss << "BMI coupling warning: nested provider disallowed for input '" << var_name - << "' (alias '" << var_map_alias << "') in catchment '" << get_catchment_id() - << "'. Using fallback 0 this step. Configure an explicit provider mapping for this input."; - LOG(ss.str(), LogLevel::WARNING); - value_ptr = get_value_as_type(type, 0.0); + + // TODO: probably need to actually allow this by default and warn, but have config option to activate + // this type of behavior + // TODO: account for arrays later + int nbytes = get_bmi_model()->GetVarNbytes(var_name); + int varItemSize = get_bmi_model()->GetVarItemsize(var_name); + int numItems = nbytes / varItemSize; + assert(nbytes % varItemSize == 0); + + std::shared_ptr value_ptr; + // Finally, use the value obtained to set the model input + std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), + varItemSize); + + // Normalize consumer units once per var; if dimensionless, avoid asking providers to convert to "1" + std::string consumer_units_raw = get_bmi_model()->GetVarUnits(var_name); + std::string units_for_selector = consumer_units_raw; + if (units_for_selector.empty() || units_for_selector == "none" || units_for_selector == "unitless" || units_for_selector == "dimensionless" || units_for_selector == "-") { + units_for_selector = ""; + } + + if (numItems != 1) { + //more than a single value needed for var_name + try { + auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, + units_for_selector)); + //need to marshal data types to the receiver as well + //this could be done a little more elegantly if the provider interface were + //"type aware", but for now, this will do (but requires yet another copy) + if(values.size() == 1){ + //FIXME this isn't generic broadcasting, but works for scalar implementations + #ifndef NGEN_QUIET + std::stringstream ss; + ss << "WARN: broadcasting variable '" << var_name << "' from scalar to expected array\n";; + LOG(ss.str(), LogLevel::SEVERE); ss.str(""); + #endif + values.resize(numItems, values[0]); + } else if (values.size() != numItems) { + throw std::runtime_error("Mismatch in item count for variable '" + var_name + "': model expects " + + std::to_string(numItems) + ", provider returned " + std::to_string(values.size()) + + " items\n"); + + } + value_ptr = get_values_as_type( type, values.begin(), values.end() ); + } catch (data_access::unit_conversion_exception &uce) { + data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; + auto ret = data_access::unit_errors_reported.insert(key); + bool new_error = ret.second; + if (new_error) { + std::stringstream ss; + ss << "Unit conversion failure:" + << " requester {'" << get_bmi_model()->get_model_name() << "' catchment '" << get_catchment_id() + << "' variable '" << var_name << "'" << " (alias '" << var_map_alias << "')" + << " units '" << get_bmi_model()->GetVarUnits(var_name) << "'}" + << " provider {'" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" + << " raw values count " << uce.unconverted_values.size() << "}" + << " message \"" << uce.what() << "\""; + LOG(ss.str(), LogLevel::WARNING); ss.str(""); + } + std::vector fallback(numItems, 0.0); + if (!uce.unconverted_values.empty()) { + if (uce.unconverted_values.size() == 1) { + std::fill(fallback.begin(), fallback.end(), uce.unconverted_values[0]); + } else if (uce.unconverted_values.size() == (size_t)numItems) { + fallback = uce.unconverted_values; + } else { + for (int i = 0; i < numItems; ++i) { + fallback[i] = uce.unconverted_values[i % uce.unconverted_values.size()]; + } + } + } + value_ptr = get_values_as_type(type, fallback.begin(), fallback.end()); + } + + } else { + try { + //scalar value + double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, + units_for_selector)); + value_ptr = get_value_as_type(type, value); + } catch (data_access::unit_conversion_exception &uce) { + data_access::unit_error_log_key key{get_id(), var_map_alias, uce.provider_model_name, uce.provider_bmi_var_name, uce.what()}; + auto ret = data_access::unit_errors_reported.insert(key); + bool new_error = ret.second; + if (new_error) { + std::stringstream ss; + ss << "Unit conversion failure:" + << " requester {'" << get_bmi_model()->get_model_name() << "' catchment '" << get_catchment_id() + << "' variable '" << var_name << "'" << " (alias '" << var_map_alias << "')" + << " units '" << get_bmi_model()->GetVarUnits(var_name) << "'}" + << " provider {'" << uce.provider_model_name << "' source variable '" << uce.provider_bmi_var_name << "'" + << " raw value " << uce.unconverted_values[0] << "}" + << " message \"" << uce.what() << "\""; + LOG(ss.str(), LogLevel::WARNING); ss.str(""); + } + value_ptr = get_value_as_type(type, uce.unconverted_values[0]); } - else throw; } + get_bmi_model()->SetValue(var_name, value_ptr.get()); } - - get_bmi_model()->SetValue(var_name, value_ptr.get()); } - } - void Bmi_Module_Formulation::append_model_inputs_to_stream(const double &model_init_time, time_step_t t_delta, std::stringstream &inputs) { - std::vector in_var_names = get_bmi_model()->GetInputVarNames(); - time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); - inputs << "Input variables were as follows:"; + void Bmi_Module_Formulation::append_model_inputs_to_stream(const double &model_init_time, time_step_t t_delta, std::stringstream &inputs) { + std::vector in_var_names = get_bmi_model()->GetInputVarNames(); + time_t model_epoch_time = convert_model_time(model_init_time) + get_bmi_model_start_time_forcing_offset_s(); + inputs << "Input variables were as follows:"; - for (std::string & var_name : in_var_names) { - data_access::GenericDataProvider *provider; - std::string var_map_alias = get_config_mapped_variable_name(var_name); - if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_map_alias].get(); - } - else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { - provider = input_forcing_providers[var_name].get(); - } - else { - provider = forcing.get(); + for (std::string & var_name : in_var_names) { + data_access::GenericDataProvider *provider; + std::string var_map_alias = get_config_mapped_variable_name(var_name); + if (input_forcing_providers.find(var_map_alias) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_map_alias].get(); + } + else if (var_map_alias != var_name && input_forcing_providers.find(var_name) != input_forcing_providers.end()) { + provider = input_forcing_providers[var_name].get(); + } + else { + provider = forcing.get(); + } + + // TODO: probably need to actually allow this by default and warn, but have config option to activate + // this type of behavior + // TODO: account for arrays later + int nbytes = get_bmi_model()->GetVarNbytes(var_name); + int varItemSize = get_bmi_model()->GetVarItemsize(var_name); + int numItems = nbytes / varItemSize; + + std::shared_ptr value_ptr; + // Finally, use the value obtained to set the model input + std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), + varItemSize); + + inputs << "\n" << var_map_alias << " = "; + if (numItems != 1) { + //more than a single value needed for var_name + auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, + get_bmi_model()->GetVarUnits(var_name))); + value_ptr = get_values_as_type( type, values.begin(), values.end() ); + // array like input: precipitation_mm_per_h = [0.2, 0.8, 1.8] + this->append_inputs(type, value_ptr, numItems, inputs); + + } else { + //scalar value + double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, + get_bmi_model()->GetVarUnits(var_name))); + this->append_input(type, value, inputs); + } } + } - // TODO: probably need to actually allow this by default and warn, but have config option to activate - // this type of behavior - // TODO: account for arrays later - int nbytes = get_bmi_model()->GetVarNbytes(var_name); - int varItemSize = get_bmi_model()->GetVarItemsize(var_name); - int numItems = nbytes / varItemSize; + template + void Bmi_Module_Formulation::append_inputs(std::shared_ptr values, int num_items, std::stringstream &inputs) { + T *array = (T*)values.get(); + inputs << "["; + for (int i = 0; i < num_items; ++i) { + if (i != 0) + inputs << ", "; + inputs << array[i]; + } + inputs << "]"; + } - std::shared_ptr value_ptr; - // Finally, use the value obtained to set the model input - std::string type = get_bmi_model()->get_analogous_cxx_type(get_bmi_model()->GetVarType(var_name), - varItemSize); - - inputs << "\n" << var_map_alias << " = "; - if (numItems != 1) { - //more than a single value needed for var_name - auto values = provider->get_values(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, - get_bmi_model()->GetVarUnits(var_name))); - value_ptr = get_values_as_type( type, values.begin(), values.end() ); - // array like input: precipitation_mm_per_h = [0.2, 0.8, 1.8] - this->append_inputs(type, value_ptr, numItems, inputs); - - } else { - //scalar value - double value = provider->get_value(CatchmentAggrDataSelector(this->get_catchment_id(),var_map_alias, model_epoch_time, t_delta, - get_bmi_model()->GetVarUnits(var_name))); - this->append_input(type, value, inputs); - } - } - } - - template - void Bmi_Module_Formulation::append_inputs(std::shared_ptr values, int num_items, std::stringstream &inputs) { - T *array = (T*)values.get(); - inputs << "["; - for (int i = 0; i < num_items; ++i) { - if (i != 0) - inputs << ", "; - inputs << array[i]; - } - inputs << "]"; - } - - void Bmi_Module_Formulation::append_inputs(std::string type, std::shared_ptr values, int num_items, std::stringstream &inputs) { + void Bmi_Module_Formulation::append_inputs(std::string type, std::shared_ptr values, int num_items, std::stringstream &inputs) { - if (type == "double" || type == "double precision") - this->append_inputs(values, num_items, inputs); + if (type == "double" || type == "double precision") + this->append_inputs(values, num_items, inputs); - else if (type == "float" || type == "real") - this->append_inputs(values, num_items, inputs); + else if (type == "float" || type == "real") + this->append_inputs(values, num_items, inputs); - else if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") - this->append_inputs(values, num_items, inputs); + else if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") + this->append_inputs(values, num_items, inputs); - else if (type == "unsigned short" || type == "unsigned short int") - this->append_inputs(values, num_items, inputs); + else if (type == "unsigned short" || type == "unsigned short int") + this->append_inputs(values, num_items, inputs); - else if (type == "int" || type == "signed" || type == "signed int" || type == "integer") - this->append_inputs(values, num_items, inputs); + else if (type == "int" || type == "signed" || type == "signed int" || type == "integer") + this->append_inputs(values, num_items, inputs); - else if (type == "unsigned" || type == "unsigned int") - this->append_inputs(values, num_items, inputs); + else if (type == "unsigned" || type == "unsigned int") + this->append_inputs(values, num_items, inputs); - else if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") - this->append_inputs(values, num_items, inputs); + else if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") + this->append_inputs(values, num_items, inputs); - else if (type == "unsigned long" || type == "unsigned long int") - this->append_inputs(values, num_items, inputs); + else if (type == "unsigned long" || type == "unsigned long int") + this->append_inputs(values, num_items, inputs); - else if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") - this->append_inputs(values, num_items, inputs); + else if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") + this->append_inputs(values, num_items, inputs); - else if (type == "unsigned long long" || type == "unsigned long long int") - this->append_inputs(values, num_items, inputs); + else if (type == "unsigned long long" || type == "unsigned long long int") + this->append_inputs(values, num_items, inputs); - } + } - template - void Bmi_Module_Formulation::append_input(std::string type, T value, std::stringstream &inputs) { + template + void Bmi_Module_Formulation::append_input(std::string type, T value, std::stringstream &inputs) { - if (type == "double" || type == "double precision") - inputs << static_cast(value); + if (type == "double" || type == "double precision") + inputs << static_cast(value); - else if (type == "float" || type == "real") - inputs << static_cast(value); + else if (type == "float" || type == "real") + inputs << static_cast(value); - else if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") - inputs << static_cast(value); + else if (type == "short" || type == "short int" || type == "signed short" || type == "signed short int") + inputs << static_cast(value); - else if (type == "unsigned short" || type == "unsigned short int") - inputs << static_cast(value); + else if (type == "unsigned short" || type == "unsigned short int") + inputs << static_cast(value); - else if (type == "int" || type == "signed" || type == "signed int" || type == "integer") - inputs << static_cast(value); + else if (type == "int" || type == "signed" || type == "signed int" || type == "integer") + inputs << static_cast(value); - else if (type == "unsigned" || type == "unsigned int") - inputs << static_cast(value); + else if (type == "unsigned" || type == "unsigned int") + inputs << static_cast(value); - else if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") - inputs << static_cast(value); + else if (type == "long" || type == "long int" || type == "signed long" || type == "signed long int") + inputs << static_cast(value); - else if (type == "unsigned long" || type == "unsigned long int") - inputs << static_cast(value); + else if (type == "unsigned long" || type == "unsigned long int") + inputs << static_cast(value); - else if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") - inputs << static_cast(value); + else if (type == "long long" || type == "long long int" || type == "signed long long" || type == "signed long long int") + inputs << static_cast(value); - else if (type == "unsigned long long" || type == "unsigned long long int") - inputs << static_cast(value); + else if (type == "unsigned long long" || type == "unsigned long long int") + inputs << static_cast(value); - } + } - const boost::span Bmi_Module_Formulation::get_serialization_state() const { - auto bmi = this->bmi_model; - // create a new serialized state, getting the amount of data that was saved - uint64_t* size = (uint64_t*)bmi->GetValuePtr("serialization_create"); - // get the pointer of the new state - char* serialized = (char*)bmi->GetValuePtr("serialization_state"); - const boost::span span(serialized, *size); - return span; - } + const boost::span Bmi_Module_Formulation::get_serialization_state() const { + auto bmi = this->bmi_model; + // create a new serialized state, getting the amount of data that was saved + uint64_t* size = (uint64_t*)bmi->GetValuePtr("serialization_create"); + // get the pointer of the new state + char* serialized = (char*)bmi->GetValuePtr("serialization_state"); + const boost::span span(serialized, *size); + return span; + } - void Bmi_Module_Formulation::load_serialization_state(const boost::span state) const { - auto bmi = this->bmi_model; - // grab the pointer to the underlying state data - void* data = (void*)state.data(); - // load the state through SetValue - bmi->SetValue("serialization_state", data); - } + void Bmi_Module_Formulation::load_serialization_state(const boost::span state) const { + auto bmi = this->bmi_model; + // grab the pointer to the underlying state data + void* data = (void*)state.data(); + // load the state through SetValue + bmi->SetValue("serialization_state", data); + } - void Bmi_Module_Formulation::free_serialization_state() const { - auto bmi = this->bmi_model; - // send message to clear memory associated with serialized data - void* _; // this pointer will be unused by SetValue - bmi->SetValue("serialization_free", _); - } + void Bmi_Module_Formulation::free_serialization_state() const { + auto bmi = this->bmi_model; + // send message to clear memory associated with serialized data + void* _; // this pointer will be unused by SetValue + bmi->SetValue("serialization_free", _); + } } - -