diff --git a/include/realizations/catchment/Bmi_Multi_Formulation.hpp b/include/realizations/catchment/Bmi_Multi_Formulation.hpp index 7b8090b57c..7f9effee02 100644 --- a/include/realizations/catchment/Bmi_Multi_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Multi_Formulation.hpp @@ -579,7 +579,7 @@ namespace realization { 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"); + 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); } diff --git a/src/core/mediator/UnitsHelper.cpp b/src/core/mediator/UnitsHelper.cpp index 6c3ea17ff2..f15e90f269 100644 --- a/src/core/mediator/UnitsHelper.cpp +++ b/src/core/mediator/UnitsHelper.cpp @@ -58,12 +58,31 @@ 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) { - if(in_units == out_units){ - return value; // Early-out optimization + 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_units, out_units); + auto converter = get_converter(in_norm, out_norm); double r = cv_convert_double(converter.get(), value); return r; @@ -71,21 +90,39 @@ double UnitsHelper::get_converted_value(const std::string &in_units, const doubl 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){ + 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 { - memcpy(out_values, in_values, sizeof(double)*count); + 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_units, out_units); + + auto converter = get_converter(in_norm, out_norm); 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 883f82d679..4e9d279bab 100644 --- a/src/realizations/catchment/Bmi_Module_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Module_Formulation.cpp @@ -176,18 +176,35 @@ namespace realization { // 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, values.data(), output_units, values.data(), values.size()); + UnitsHelper::convert_values(native_units_norm, values.data(), output_units_norm, values.data(), values.size()); return values; } - catch (const std::runtime_error& e) { + catch (const std::runtime_error& e) { data_access::unit_conversion_exception uce(e.what()); - uce.provider_model_name = get_bmi_model()->get_model_name(); + 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; - uce.unconverted_values = std::move(values); + uce.provider_units = native_units; // include units for the logger + 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); @@ -225,24 +242,40 @@ namespace realization { // 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 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; + } + try { - return UnitsHelper::get_converted_value(native_units, value, output_units); + return UnitsHelper::get_converted_value(native_units_norm, value, output_units_norm); } - catch (const std::runtime_error& e){ + catch (const std::runtime_error& e) { data_access::unit_conversion_exception uce(e.what()); - uce.provider_model_name = get_bmi_model()->get_model_name(); + 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; + uce.provider_units = native_units; // include units for the logger 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); } - 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; } @@ -692,34 +725,72 @@ namespace realization { // 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 - 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"); + 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()); } - 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))); + 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()}; @@ -743,7 +814,6 @@ namespace realization { } } - 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();