From e03f459eb8fe5fca4d45030d8533169e833145db Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Fri, 8 Aug 2025 14:55:06 -0700 Subject: [PATCH 01/58] Fix a bunch of typos in comments --- include/routing/Routing_Py_Adapter.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/routing/Routing_Py_Adapter.hpp b/include/routing/Routing_Py_Adapter.hpp index d46445f12d..6a022cd0a4 100644 --- a/include/routing/Routing_Py_Adapter.hpp +++ b/include/routing/Routing_Py_Adapter.hpp @@ -32,7 +32,7 @@ namespace routing_py_adapter { * Function to run @p number_of_timesteps of routing, extracting * lateral inflows from @p flow_vector * - * FIXME This is current unimplemented and will require some addtional + * FIXME This is current unimplemented and will require some additional * work to properly map flows to the correct t-route network segments. * This may not even be the correct concept to implement this type of * "integrated" routing. But the basic idea is that after a catchment @@ -44,7 +44,7 @@ namespace routing_py_adapter { * vector for each identity and constructs the correct lateral inflow setup to make * a full routing pass. * - * See NOTE in @ref route(int, int) route() about python module availablity. + * See NOTE in @ref route(int, int) route() about python module availability. * * @param number_of_timesteps * @param delta_time @@ -61,14 +61,14 @@ namespace routing_py_adapter { * Currently, these parameters are ignored and are read instead from the yaml configuration * file contained in #t_route_config_path * - * NOTE this funtion uses a pybind11 embedded interpreter to load the t-route namespace package + * NOTE this function uses a pybind11 embedded interpreter to load the t-route namespace package * ngen-main and then executes the routing in the python interpreter. * It is assumed that the ngen-main module is available in the interpreters PYTHON_PATH. * If the module cannot be found, then a ModuleNotFoundError will be thrown. - * Similarly, ngen-main depends on severl other python modules. If any of these are not in the - * environments PYTHON_PATH, errors will occur. + * Similarly, ngen-main depends on several other python modules. If any of these are not in the + * environment's PYTHON_PATH, errors will occur. * - * It is reccommended to intall all t-route packages into a loaded virtual environment or + * It is recommended to install all t-route packages into a loaded virtual environment or * to the system site-packages. * * @param number_of_timesteps From 71070cad27150ac056b1ee0b9004aadf33639b79 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Tue, 16 Sep 2025 16:16:17 -0600 Subject: [PATCH 02/58] feat(bmi): Interface for generic BMI protocols --- include/utilities/bmi/protocol.hpp | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 include/utilities/bmi/protocol.hpp diff --git a/include/utilities/bmi/protocol.hpp b/include/utilities/bmi/protocol.hpp new file mode 100644 index 0000000000..cdbd509edf --- /dev/null +++ b/include/utilities/bmi/protocol.hpp @@ -0,0 +1,88 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ + +Version 0.1 +Virtual interface for BMI protocols +*/ + +#pragma once + +#include "Bmi_Adapter.hpp" +#include "JSONProperty.hpp" + + +namespace models{ namespace bmi{ namespace protocols{ + +struct Context{ + const int current_time_step; + const int total_steps; + const std::string& timestamp; + const std::string& id; +}; + +class NgenBmiProtocol{ + /** + * @brief Abstract interface for a generic BMI protocol + * + */ + + public: + /** + * @brief Run the BMI protocol + * + */ + virtual void run(const Context& ctx) const = 0; + + virtual ~NgenBmiProtocol() = default; + + private: + /** + * @brief Check if the BMI protocol is supported by the model + * + */ + virtual void check_support() = 0; + /** + * @brief Initialize the BMI protocol from a set of key/value properties + * + * @param properties + */ + virtual void initialize(const geojson::PropertyMap& properties) = 0; + + protected: + + /** + * @brief Constructor for subclasses to create NgenBmiProtocol objects + * + * @param model A shared pointer to a Bmi_Adapter object which should be + * initialized before being passed to this constructor. + */ + NgenBmiProtocol(std::shared_ptr model): model(model){} + + /** + * @brief The Bmi_Adapter object used by the protocol + * + */ + std::shared_ptr model; + + /** + * @brief Whether the protocol is supported by the model + * + */ + bool is_supported = false; +}; + +}}} From d2ff3be623ce117a690ca576434e610171fcd942 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Tue, 16 Sep 2025 17:15:02 -0600 Subject: [PATCH 03/58] feat(mass_balance): define the bmi mass balance protocol --- include/utilities/bmi/mass_balance.hpp | 185 +++++++++++++++++++++++++ src/utilities/bmi/mass_balance.cpp | 137 ++++++++++++++++++ 2 files changed, 322 insertions(+) create mode 100644 include/utilities/bmi/mass_balance.hpp create mode 100644 src/utilities/bmi/mass_balance.cpp diff --git a/include/utilities/bmi/mass_balance.hpp b/include/utilities/bmi/mass_balance.hpp new file mode 100644 index 0000000000..562dd7c18e --- /dev/null +++ b/include/utilities/bmi/mass_balance.hpp @@ -0,0 +1,185 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ + +Version 0.1 +Interface of the BMI mass balance protocol +*/ +#pragma once + +#include +#include +#include "protocol.hpp" + +namespace models{ namespace bmi{ namespace protocols{ + + /** Mass balance variable names **/ + constexpr const char* const INPUT_MASS_NAME = "ngen::mass_in"; + constexpr const char* const OUTPUT_MASS_NAME = "ngen::mass_out"; + constexpr const char* const STORED_MASS_NAME = "ngen::mass_stored"; + constexpr const char* const LEAKED_MASS_NAME = "ngen::mass_leaked"; + + /** Configuration keys for defining configurable properties of the protocol */ + //The top level object key which will contain the map of configuration options + constexpr const char* const CONFIGURATION_KEY = "mass_balance"; + //Configuration option keys + constexpr const char* const TOLERANCE_KEY = "tolerance"; + constexpr const char* const FATAL_KEY = "fatal"; + constexpr const char* const CHECK_KEY = "check"; + constexpr const char* const FREQUENCY_KEY = "frequency"; + + class NgenMassBalance : public NgenBmiProtocol { + /** @brief Mass Balance protocol + * + * This protocol `run()`s a simple mass balance calculation by querying the model for a + * set of mass balance state variables and computing the basic mass balance as + * balance = mass_in - mass_out - mass_stored - mass_leaked. It is then checked against + * a tolerance value to determine if the mass balance is acceptable. + */ + public: + + /** @brief Constructor for the NgenMassBalance protocol + * + * This constructor initializes the mass balance protocol with the given model and properties. + * + * @param model A shared pointer to a Bmi_Adapter object which should be + * initialized before being passed to this constructor. + */ + NgenMassBalance(std::shared_ptr model=nullptr) : + NgenBmiProtocol(model), check(false), is_fatal(false), + tolerance(1.0E-16), frequency(1){} + + /** + * @brief Run the mass balance protocol + * + * If the configured frequency is -1, the mass balance will only be checked at the end + * of the simulation. If the frequency is greater than 0, the mass balance will be checked + * at the specified frequency based on the current_time_step and the total_steps provided + * in the Context. + * + * Warns or errors at each check if total mass balance is not within the configured + * acceptable tolerance. + * + * throws: MassBalanceError if the mass balance is not within the configured + * acceptable tolerance and the protocol is configured to be fatal. + */ + void run(const Context& ctx) const override; + + virtual ~NgenMassBalance() override {}; + + private: + /** + * @brief Check if the mass balance protocol is supported by the model + * + * throws: MassBalanceIntegrationError if the mass balance protocol is not supported + */ + void check_support() override; + + /** + * @brief Check the model for support and initialize the mass balance protocol from the given properties. + * + * If the model does not support the mass balance protocol, an exception will be thrown, and no mass balance + * will performed when `run()` is called. + * + * A private initialize call is used since it only makes sense to check/run the protocol + * once the model adapter is fully constructed. This should be called by the owner of the + * NgenMassBalance instance once the model is ready. + * + * @param properties Configurable key/value properties for the mass balance protocol. + * If the map contains "mass_balance" object, then the following properties + * are used to configure the protocol: + * tolerance: double, default 1.0E-16. + * check: bool, default true. Whether to perform mass balance check. + * frequency: int, default 1. How often (in time steps) to check mass balance. + * fatal: bool, default false. Whether to treat mass balance errors as fatal. + * Otherwise, mass balance checking will be disabled (check will be false) + */ + void initialize(const geojson::PropertyMap& properties) override; + + // Configurable options/values + bool check; + bool is_fatal; + double tolerance; + // How often (in time steps) to check mass balance + int frequency; + + /** + * @brief Friend class for checking/managing support and initialization + * + * This allows the NgenBmiProtocols container class to access private members, + * particularly the check_support() and initialize() methods. + * + */ + friend class NgenBmiProtocols; + + }; + + class MassBalanceIntegration : public std::exception { + /** + * @brief Exception thrown when there is an error in mass balance integration + * + * This indicates that the BMI model isn't capable of supporting the mass balance protocol. + */ + + public: + + MassBalanceIntegration(char const *const message) noexcept : MassBalanceIntegration(std::string(message)) {} + + MassBalanceIntegration(std::string message) noexcept : std::exception(), what_message(std::move(message)) {} + + MassBalanceIntegration(MassBalanceIntegration &exception) noexcept : MassBalanceIntegration(exception.what_message) {} + + MassBalanceIntegration(MassBalanceIntegration &&exception) noexcept + : MassBalanceIntegration(std::move(exception.what_message)) {} + + char const *what() const noexcept override { + return what_message.c_str(); + } + + private: + + std::string what_message; + }; + + class MassBalanceError : public std::exception { + /** + * @brief Exception thrown when a mass balance error occurs + * + * This indicates that a mass balance error has occurred within the model. + */ + + public: + + MassBalanceError(char const *const message) noexcept : MassBalanceError(std::string(message)) {} + + MassBalanceError(std::string message) noexcept : std::exception(), what_message(std::move(message)) {} + + MassBalanceError(MassBalanceError &exception) noexcept : MassBalanceError(exception.what_message) {} + + MassBalanceError(MassBalanceError &&exception) noexcept + : MassBalanceError(std::move(exception.what_message)) {} + + char const *what() const noexcept override { + return what_message.c_str(); + } + + private: + + std::string what_message; + }; + +}}} + diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp new file mode 100644 index 0000000000..6fcdc97642 --- /dev/null +++ b/src/utilities/bmi/mass_balance.cpp @@ -0,0 +1,137 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ + +Version 0.1 +Implementation the BMI mass balance checking protocol +*/ + +#include "mass_balance.hpp" + +namespace models { namespace bmi { namespace protocols { + +void NgenMassBalance::run(const Context& ctx) const{ + bool check_step = false; + //if frequency was set to -1, only check at the end + if( frequency == -1 ){ + if(ctx.current_time_step == ctx.total_steps){ + check_step = true; + } + } + else{ + check_step = (ctx.current_time_step % frequency) == 0; + } + + if(model && is_supported && check && check_step) { + double mass_in, mass_out, mass_stored, mass_leaked, mass_balance; + model->GetValue(INPUT_MASS_NAME, &mass_in); + model->GetValue(OUTPUT_MASS_NAME, &mass_out); + model->GetValue(STORED_MASS_NAME, &mass_stored); + model->GetValue(LEAKED_MASS_NAME, &mass_leaked); + // TODO consider unit conversion if/when it becomes necessary + mass_balance = mass_in - mass_out - mass_stored - mass_leaked; + if ( std::abs(mass_balance) > tolerance ) { + std::stringstream ss; + ss << "at timestep " << std::to_string(ctx.current_time_step) + << " ("+ctx.timestamp+")" + << " at feature id " << ctx.id <GetComponentName() << "\n\t" << + INPUT_MASS_NAME << "(" << mass_in << ") - " << + OUTPUT_MASS_NAME << " (" << mass_out << ") - " << + STORED_MASS_NAME << " (" << mass_stored << ") - " << + LEAKED_MASS_NAME << " (" << mass_leaked << ") = " << + mass_balance << "\n\t" << "tolerance: " << tolerance << std::endl; + if(is_fatal) + throw MassBalanceError("mass_balance::error " + ss.str()); + else{ + std::cerr << "mass_balance::warning " + ss.str(); + } + + } + } +} + +void NgenMassBalance::check_support() { + if (model->is_model_initialized()) { + double mass_var; + std::vector units; + units.reserve(4); + try{ + for(const auto& name : + {INPUT_MASS_NAME, OUTPUT_MASS_NAME, STORED_MASS_NAME, LEAKED_MASS_NAME} + ){ + model->GetValue(name, &mass_var); + units.push_back( model->GetVarUnits(name) ); + } + //Compare all other units to the first one (+1) + if( std::equal( units.begin()+1, units.end(), units.begin() ) ) { + this->is_supported = true; + } + else{ + // It may be possible to do unit conversion and still do meaninful mass balance + // this could be added as an extended feature, but for now, I don't think this is + // worth the complexity. It is, however, worth the sanity check performed here + // to ensure the units are consistent. + throw MassBalanceError( "mass balance variables have incosistent units, cannot perform mass balance" ); + } + } catch (const std::exception &e) { + std::stringstream ss; + ss << "mass_balance::integration: Error getting mass balance values for module '" << model->GetComponentName() << "': " << e.what() << std::endl; + std::cout << ss.str(); + this->is_supported= false; + } + } else { + throw std::runtime_error("Cannot check mass balance for uninitialized model."); + } +} + +void NgenMassBalance::initialize(const geojson::PropertyMap& properties) +{ + //Ensure the model is capable of mass balance using the protocol + check_support(); + + //now check if the user has requested to use mass balance + auto protocol_it = properties.find(CONFIGURATION_KEY); + if ( is_supported && protocol_it != properties.end() ) { + geojson::PropertyMap mass_bal = protocol_it->second.get_values(); + + auto _it = mass_bal.find(TOLERANCE_KEY); + if( _it != mass_bal.end() ) tolerance = _it->second.as_real_number(); + + _it = mass_bal.find(FATAL_KEY); + if( _it != mass_bal.end() ) is_fatal = _it->second.as_boolean(); + + _it = mass_bal.find(CHECK_KEY); + if( _it != mass_bal.end() ) { + check = _it->second.as_boolean(); + } else { + //default to true if not specified + check = true; + } + + _it = mass_bal.find(FREQUENCY_KEY); + if( _it != mass_bal.end() ){ + frequency = _it->second.as_natural_number(); + } else { + frequency = 1; //default, check every timestep + } + } else{ + //no mass balance requested, or not supported, so don't check it + check = false; + } +} + +}}} // end namespace models::bmi::protocols \ No newline at end of file From 0d5965cbca399541db39660fd4a82a9ef199e4a3 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Tue, 16 Sep 2025 17:45:22 -0600 Subject: [PATCH 04/58] feat(bmi): light container for BMI protocol objects --- include/utilities/bmi/protocols.hpp | 62 +++++++++++++++++++++++++++++ src/utilities/bmi/protocols.cpp | 36 +++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 include/utilities/bmi/protocols.hpp create mode 100644 src/utilities/bmi/protocols.cpp diff --git a/include/utilities/bmi/protocols.hpp b/include/utilities/bmi/protocols.hpp new file mode 100644 index 0000000000..32c1792f9a --- /dev/null +++ b/include/utilities/bmi/protocols.hpp @@ -0,0 +1,62 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ + +Version 0.1 +Container and management for abstract BMI protocols +*/ +#pragma once + +#include +#include +#include +#include "Bmi_Adapter.hpp" +#include "JSONProperty.hpp" + +#include "mass_balance.hpp" + + +namespace models{ namespace bmi{ namespace protocols{ + +class NgenBmiProtocols { + /** + * @brief Container and management interface for BMI protocols for use in ngen + * + */ + + public: + /** + * @brief Construct a new Ngen Bmi Protocols object with a null model + * + */ + NgenBmiProtocols(); + + /** + * @brief Construct a new Ngen Bmi Protocols object for use with a known model + * + * @param model An initialized BMI model + * @param properties Properties for each protocol being initialized + */ + NgenBmiProtocols(std::shared_ptr model, const geojson::PropertyMap& properties); + + /** + * @brief Mass Balance Checker + * + */ + NgenMassBalance mass_balance; +}; + +}}} diff --git a/src/utilities/bmi/protocols.cpp b/src/utilities/bmi/protocols.cpp new file mode 100644 index 0000000000..aef86f40ed --- /dev/null +++ b/src/utilities/bmi/protocols.cpp @@ -0,0 +1,36 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ + +Version 0.1 +Container and management for abstract BMI protocols +*/ + +#include "protocols.hpp" + +namespace models{ namespace bmi{ namespace protocols{ + +NgenBmiProtocols::NgenBmiProtocols(): mass_balance(nullptr) {} + +NgenBmiProtocols::NgenBmiProtocols(std::shared_ptr model, const geojson::PropertyMap& properties) + : mass_balance(model) { + //initialize mass balance configurable properties + //This is done so that initialize is called with a valid model pointer, which is shared + //by all protocol instances + mass_balance.initialize(properties); +} + +}}} // end namespace models::bmi::protocols From e1152d8d203f7b53b54b60fb150caeb552059492 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Tue, 16 Sep 2025 17:48:30 -0600 Subject: [PATCH 05/58] build(ngen_bmi_protocols): add protocol library to build --- src/utilities/bmi/CMakeLists.txt | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/utilities/bmi/CMakeLists.txt diff --git a/src/utilities/bmi/CMakeLists.txt b/src/utilities/bmi/CMakeLists.txt new file mode 100644 index 0000000000..67a62eaf8d --- /dev/null +++ b/src/utilities/bmi/CMakeLists.txt @@ -0,0 +1,36 @@ +# Author: Nels Frazier +# Copyright (C) 2025 Lynker +# ------------------------------------------------------------------------ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------ +# Library Version 0.1 +# BMI mass balance checking protocol + +add_library(ngen_bmi_protocols protocols.cpp mass_balance.cpp) +add_library(NGen::bmi_protocols ALIAS ngen_bmi_protocols) + +target_include_directories(ngen_bmi_protocols PUBLIC + ${PROJECT_SOURCE_DIR}/include/bmi + ${PROJECT_SOURCE_DIR}/include/utilities/bmi + ${PROJECT_SOURCE_DIR}/include/geojson + ${NGEN_INC_DIR} +) + +target_link_libraries(ngen_bmi_protocols + PUBLIC + ${CMAKE_DL_LIBS} + Boost::boost # Headers-only Boost +) + +target_sources(ngen_bmi_protocols + PRIVATE + "${PROJECT_SOURCE_DIR}/src/bmi/Bmi_Adapter.cpp" +) From 57f220c25686c1aec639cbfddd6767c98fe7ebfe Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Tue, 16 Sep 2025 17:50:27 -0600 Subject: [PATCH 06/58] test(bmi_protocols): add mock for BMI protocol testing --- .../realizations/catchments/Bmi_Protocols.hpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/realizations/catchments/Bmi_Protocols.hpp diff --git a/test/realizations/catchments/Bmi_Protocols.hpp b/test/realizations/catchments/Bmi_Protocols.hpp new file mode 100644 index 0000000000..30b5570e9f --- /dev/null +++ b/test/realizations/catchments/Bmi_Protocols.hpp @@ -0,0 +1,19 @@ +#include + +class MassBalanceMock { + public: + + MassBalanceMock( bool fatal = false, double tolerance = 1e-12, int frequency = 1) + : properties() { + properties.put("check", true); + properties.put("tolerance", tolerance); + properties.put("fatal", fatal); + properties.put("frequency", frequency); + } + + const boost::property_tree::ptree& get() const { + return properties; + } + private: + boost::property_tree::ptree properties; +}; \ No newline at end of file From a54a6a308f3997649bb9bb33b73635551faa3c9c Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Tue, 16 Sep 2025 17:53:00 -0600 Subject: [PATCH 07/58] test(bmi_c): update bmi C test model code to implement the mass balance protocol semantics --- extern/test_bmi_c/include/bmi_test_bmi_c.h | 5 +++ extern/test_bmi_c/include/test_bmi_c.h | 3 ++ extern/test_bmi_c/src/bmi_test_bmi_c.c | 50 ++++++++++++++++++++++ extern/test_bmi_c/src/test_bmi_c.c | 2 + 4 files changed, 60 insertions(+) diff --git a/extern/test_bmi_c/include/bmi_test_bmi_c.h b/extern/test_bmi_c/include/bmi_test_bmi_c.h index 66a24a1572..3d4f40b4f1 100644 --- a/extern/test_bmi_c/include/bmi_test_bmi_c.h +++ b/extern/test_bmi_c/include/bmi_test_bmi_c.h @@ -1,6 +1,11 @@ #ifndef BMI_TEST_BMI_C_H #define BMI_TEST_BMI_C_H +#define NGEN_MASS_IN "ngen::mass_in" +#define NGEN_MASS_OUT "ngen::mass_out" +#define NGEN_MASS_STORED "ngen::mass_stored" +#define NGEN_MASS_LEAKED "ngen::mass_leaked" + #if defined(__cplusplus) extern "C" { #endif diff --git a/extern/test_bmi_c/include/test_bmi_c.h b/extern/test_bmi_c/include/test_bmi_c.h index fa990d1ebb..1e438259eb 100644 --- a/extern/test_bmi_c/include/test_bmi_c.h +++ b/extern/test_bmi_c/include/test_bmi_c.h @@ -31,6 +31,9 @@ struct test_bmi_c_model { int param_var_1; double param_var_2; double* param_var_3; + + double mass_stored; // Mass balance variable, for testing purposes + double mass_leaked; //Mass balance variable, for testing purposes }; typedef struct test_bmi_c_model test_bmi_c_model; diff --git a/extern/test_bmi_c/src/bmi_test_bmi_c.c b/extern/test_bmi_c/src/bmi_test_bmi_c.c index 522fe4651d..9f46eab0c8 100644 --- a/extern/test_bmi_c/src/bmi_test_bmi_c.c +++ b/extern/test_bmi_c/src/bmi_test_bmi_c.c @@ -9,6 +9,7 @@ #define INPUT_VAR_NAME_COUNT 2 #define OUTPUT_VAR_NAME_COUNT 2 #define PARAM_VAR_NAME_COUNT 3 +#define MASS_BALANCE_VAR_NAME_COUNT 4 // Don't forget to update Get_value/Get_value_at_indices (and setter) implementation if these are adjusted static const char *output_var_names[OUTPUT_VAR_NAME_COUNT] = { "OUTPUT_VAR_1", "OUTPUT_VAR_2" }; @@ -34,6 +35,13 @@ static const int param_var_item_count[PARAM_VAR_NAME_COUNT] = { 1, 1, 2 }; static const char *param_var_grids[PARAM_VAR_NAME_COUNT] = { 0, 0, 0 }; static const char *param_var_locations[PARAM_VAR_NAME_COUNT] = { "node", "node", "node" }; +static const char *mass_balance_var_names[MASS_BALANCE_VAR_NAME_COUNT] = { NGEN_MASS_IN, NGEN_MASS_OUT, NGEN_MASS_STORED, NGEN_MASS_LEAKED}; +static const char *mass_balance_var_types[MASS_BALANCE_VAR_NAME_COUNT] = { "double", "double", "double", "double"}; +static const char *mass_balance_var_units[MASS_BALANCE_VAR_NAME_COUNT] = { "m", "m", "m", "m" }; +static const int mass_balance_var_item_count[MASS_BALANCE_VAR_NAME_COUNT] = { 1, 1, 1, 1}; +static const char *mass_balance_var_grids[MASS_BALANCE_VAR_NAME_COUNT] = { 0, 0, 0, 0 }; +static const char *mass_balance_var_locations[MASS_BALANCE_VAR_NAME_COUNT] = { "node", "node", "node", "node" }; + static int Finalize (Bmi *self) { // Function assumes everything that is needed is retrieved from the model before Finalize is called. @@ -387,6 +395,23 @@ static int Get_value_ptr (Bmi *self, const char *name, void **dest) *dest = ((test_bmi_c_model *)(self->data))->param_var_3; return BMI_SUCCESS; } + + if (strcmp (name, NGEN_MASS_IN) == 0) { + *dest = ((test_bmi_c_model *)(self->data))->input_var_1; + return BMI_SUCCESS; + } + if (strcmp (name, NGEN_MASS_OUT) == 0) { + *dest = ((test_bmi_c_model *)(self->data))->output_var_1; + return BMI_SUCCESS; + } + if (strcmp (name, NGEN_MASS_STORED) == 0) { + *dest = &((test_bmi_c_model *)(self->data))->mass_stored; + return BMI_SUCCESS; + } + if (strcmp (name, NGEN_MASS_LEAKED) == 0) { + *dest = &((test_bmi_c_model *)(self->data))->mass_leaked; + return BMI_SUCCESS; + } return BMI_FAILURE; } @@ -483,6 +508,14 @@ static int Get_var_nbytes (Bmi *self, const char *name, int * nbytes) } } } + if (item_count < 1) { + for (i = 0; i < MASS_BALANCE_VAR_NAME_COUNT; i++) { + if (strcmp(name, mass_balance_var_names[i]) == 0) { + item_count = mass_balance_var_item_count[i]; + break; + } + } + } if (item_count < 1) item_count = ((test_bmi_c_model *) self->data)->num_time_steps; @@ -515,6 +548,13 @@ static int Get_var_type (Bmi *self, const char *name, char * type) return BMI_SUCCESS; } } + // Finally check to see if in mass balance array + for (i = 0; i < MASS_BALANCE_VAR_NAME_COUNT; i++) { + if (strcmp(name, mass_balance_var_names[i]) == 0) { + snprintf(type, BMI_MAX_TYPE_NAME, "%s", mass_balance_var_types[i]); + return BMI_SUCCESS; + } + } // If we get here, it means the variable name wasn't recognized type[0] = '\0'; return BMI_FAILURE; @@ -538,6 +578,13 @@ static int Get_var_units (Bmi *self, const char *name, char * units) return BMI_SUCCESS; } } + //Check for mass balance + for (i = 0; i < MASS_BALANCE_VAR_NAME_COUNT; i++) { + if (strcmp(name, mass_balance_var_names[i]) == 0) { + snprintf(units, BMI_MAX_UNITS_NAME, "%s", mass_balance_var_units[i]); + return BMI_SUCCESS; + } + } // If we get here, it means the variable name wasn't recognized units[0] = '\0'; return BMI_FAILURE; @@ -560,6 +607,9 @@ static int Initialize (Bmi *self, const char *file) else model = (test_bmi_c_model *) self->data; + model->mass_stored = 0.0; + model->mass_leaked = 0.0; + if (read_init_config(file, model) == BMI_FAILURE) return BMI_FAILURE; diff --git a/extern/test_bmi_c/src/test_bmi_c.c b/extern/test_bmi_c/src/test_bmi_c.c index c81d6b4406..d05dc50b3f 100644 --- a/extern/test_bmi_c/src/test_bmi_c.c +++ b/extern/test_bmi_c/src/test_bmi_c.c @@ -19,5 +19,7 @@ extern int run(test_bmi_c_model* model, long dt) } model->current_model_time += (double)dt; + model->mass_stored = *model->output_var_1 - *model->input_var_1; + model->mass_leaked = 0; return 0; } \ No newline at end of file From 15a24138c69be9ebf93d1fda7fd60f32f85a5699 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Tue, 16 Sep 2025 17:55:27 -0600 Subject: [PATCH 08/58] test(bmi_mass_balance): Test the protocol via the C and multi formulation tests --- .../test_bmi_python_config_2.yml | 2 + .../catchments/Bmi_C_Formulation_Test.cpp | 203 +++++++++++++++++- .../catchments/Bmi_Multi_Formulation_Test.cpp | 35 ++- 3 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 test/data/bmi/test_bmi_python/test_bmi_python_config_2.yml diff --git a/test/data/bmi/test_bmi_python/test_bmi_python_config_2.yml b/test/data/bmi/test_bmi_python/test_bmi_python_config_2.yml new file mode 100644 index 0000000000..edd5b81e35 --- /dev/null +++ b/test/data/bmi/test_bmi_python/test_bmi_python_config_2.yml @@ -0,0 +1,2 @@ +time_step_seconds: 3600 +initial_time: 0 \ No newline at end of file diff --git a/test/realizations/catchments/Bmi_C_Formulation_Test.cpp b/test/realizations/catchments/Bmi_C_Formulation_Test.cpp index 3b46c648b4..6b78f83a35 100644 --- a/test/realizations/catchments/Bmi_C_Formulation_Test.cpp +++ b/test/realizations/catchments/Bmi_C_Formulation_Test.cpp @@ -24,7 +24,7 @@ #include "FileChecker.h" #include "Formulation_Manager.hpp" #include - +#include "Bmi_Protocols.hpp" using ::testing::MatchesRegex; using namespace realization; @@ -116,6 +116,7 @@ class Bmi_C_Formulation_Test : public ::testing::Test { std::vector main_output_variable; std::vector registration_functions; std::vector uses_forcing_file; + // std::vector tries_mass_balance; std::vector> forcing_params_examples; std::vector config_properties; std::vector config_prop_ptree; @@ -148,6 +149,7 @@ void Bmi_C_Formulation_Test::SetUp() { main_output_variable = std::vector(EX_COUNT); registration_functions = std::vector(EX_COUNT); uses_forcing_file = std::vector(EX_COUNT); + // tries_mass_balance = std::vector(EX_COUNT); forcing_params_examples = std::vector>(EX_COUNT); config_properties = std::vector(EX_COUNT); config_prop_ptree = std::vector(EX_COUNT); @@ -161,6 +163,7 @@ void Bmi_C_Formulation_Test::SetUp() { main_output_variable[0] = "OUTPUT_VAR_1"; registration_functions[0] = "register_bmi"; uses_forcing_file[0] = false; + // tries_mass_balance[0] = true; catchment_ids[1] = "cat-27"; model_type_name[1] = "test_bmi_c"; @@ -170,6 +173,7 @@ void Bmi_C_Formulation_Test::SetUp() { main_output_variable[1] = "OUTPUT_VAR_1"; registration_functions[1] = "register_bmi"; uses_forcing_file[1] = false; + // tries_mass_balance[1] = false; std::string variables_with_rain_rate = " \"output_variables\": [\"OUTPUT_VAR_2\",\n" " \"OUTPUT_VAR_1\"],\n"; @@ -198,6 +202,9 @@ void Bmi_C_Formulation_Test::SetUp() { + variables_line + " \"uses_forcing_file\": " + (uses_forcing_file[i] ? "true" : "false") + "," " \"model_params\": { \"PARAM_VAR_1\": 42, \"PARAM_VAR_2\": 4.2, \"PARAM_VAR_3\": [4, 2]}" + + // (tries_mass_balance[i] ? \ + // ", \"mass_balance\": {\"tolerance\": 1e-12, \"fatal\":true}" \ + // :"" )+ " }," " \"forcing\": { \"path\": \"" + forcing_file[i] + "\", \"provider\": \"CsvPerFeature\"}" " }" @@ -387,6 +394,200 @@ TEST_F(Bmi_C_Formulation_Test, determine_model_time_offset_0_c) { ASSERT_EQ(get_friend_bmi_model_start_time_forcing_offset_s(formulation), expected_offset); } +using models::bmi::protocols::INPUT_MASS_NAME; +using models::bmi::protocols::OUTPUT_MASS_NAME; +using models::bmi::protocols::STORED_MASS_NAME; +using models::bmi::protocols::LEAKED_MASS_NAME; +using models::bmi::protocols::MassBalanceError; + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + // mass balance failure will throw an exception + ptree.put_child("mass_balance", MassBalanceMock(true).get()); + formulation.create_formulation(ptree); + formulation.check_mass_balance(0, 2, "t0"); + formulation.get_response(1, 3600); + formulation.check_mass_balance(1, 2, "t1"); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_warns) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(false).get()); + formulation.create_formulation(ptree); + //formulation.check_mass_balance(0, 1, "t0"); + formulation.get_response(1, 3600); + double mass_error = 100; + get_friend_bmi_model(formulation)->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + //ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceWarning); + testing::internal::CaptureStderr(); + formulation.check_mass_balance(0, 1, "t0"); + std::string output = testing::internal::GetCapturedStderr(); + std::cerr << output; + EXPECT_THAT(output, testing::HasSubstr("mass_balance::warning")); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_stored_fails) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true).get()); + formulation.create_formulation(ptree); + //formulation.check_mass_balance(0, 1, "t0"); + formulation.get_response(1, 3600); + double mass_error = 100; + get_friend_bmi_model(formulation)->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + //formulation.check_mass_balance(0, 1, "t0"); + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_in_fails_a) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5).get()); + formulation.create_formulation(ptree); + formulation.get_response(1, 3600); + double mass_error = 2; + get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_out_fails) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true).get()); + formulation.create_formulation(ptree); + formulation.get_response(1, 3600); + double mass_error = 2; + get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_leaked_fails) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true).get()); + formulation.create_formulation(ptree); + formulation.get_response(1, 3600); + double mass_error = 2; + get_friend_bmi_model(formulation)->SetValue(LEAKED_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_tolerance) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5).get()); + formulation.create_formulation(ptree); + //formulation.check_mass_balance(0, "t0"); + formulation.get_response(1, 3600); + double mass_error; + get_friend_bmi_model(formulation)->GetValue(INPUT_MASS_NAME, &mass_error); + mass_error += 1e-4; // Force a mass balance error not within tolerance + get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_tolerance_a) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5).get()); + formulation.create_formulation(ptree); + //formulation.check_mass_balance(0, 1, "t0"); + formulation.get_response(1, 3600); + double mass_error; + get_friend_bmi_model(formulation)->GetValue(INPUT_MASS_NAME, &mass_error); + mass_error += 1e-6; // Force a mass balance error within tolerance + get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + formulation.check_mass_balance(0, 1, "t0"); // Should not throw an error +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_off) { + int ex_index = 1; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + formulation.create_formulation(config_prop_ptree[ex_index]); + formulation.check_mass_balance(0, 2, "t0"); + formulation.get_response(1, 3600); + formulation.check_mass_balance(1, 2, "t1"); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_missing) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + + std::string catchment_path = "catchments." + catchment_ids[ex_index] + ".bmi_c"; + ptree.erase("mass_balance"); + formulation.create_formulation(ptree); + formulation.check_mass_balance(0, 2, "t0"); + formulation.get_response(1, 3600); + double mass_error = 100; + double more_mass_error = 99; + get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // Force a mass balance error + get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &more_mass_error); // Force a mass balance error + formulation.check_mass_balance(1, 2, "t1"); + +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_frequency) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5, 2).get()); + formulation.create_formulation(ptree); + double mass_error; + mass_error += 10; // Force a mass balance error above tolerance + get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // + //Check initial mass balance -- should error which indicates it was propoerly checked + //per frequency setting + ASSERT_THROW(formulation.check_mass_balance(0, 2, "t0"), MassBalanceError); + // Call mass balance check again, this should NOT error, since the actual check + // should be skipped due to the frequency setting + formulation.check_mass_balance(1, 2, "t1"); + // Check mass balance again, this SHOULD error since the previous mass balance + // will propagate, and it should now be checked based on the frequency + ASSERT_THROW(formulation.check_mass_balance(2, 2, "t2"), MassBalanceError); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_frequency_1) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5, -1).get()); + formulation.create_formulation(ptree); + double mass_error; + mass_error += 10; // Force a mass balance error above tolerance + get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // + //Check initial mass balance -- should NOT error + formulation.check_mass_balance(0, 2, "t0"); + // Call mass balance check again, this should NOT error, since the actual check + // should be skipped due to the frequency setting + formulation.check_mass_balance(1, 2, "t1"); + // Check mass balance again, this SHOULD error since the this is step 2/2 + // and it will now be checked based on the frequency (-1, check at end) + ASSERT_THROW(formulation.check_mass_balance(2, 2, "t2"), MassBalanceError); +} + #endif // NGEN_BMI_C_LIB_TESTS_ACTIVE #endif // NGEN_BMI_C_FORMULATION_TEST_CPP diff --git a/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp b/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp index 3ea307e6a7..db91bdf339 100644 --- a/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp +++ b/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp @@ -349,7 +349,7 @@ class Bmi_Multi_Formulation_Test : public ::testing::Test { return s; } - inline void buildExampleConfig(const int ex_index) { + inline void buildExampleConfig(const int ex_index, const int nested_count) { std::string config = "{\n" " \"global\": {},\n" @@ -364,10 +364,12 @@ class Bmi_Multi_Formulation_Test : public ::testing::Test { " \"init_config\": \"\",\n" " \"allow_exceed_end_time\": true,\n" " \"main_output_variable\": \"" + main_output_variables[ex_index] + "\",\n" - " \"modules\": [\n" - + buildExampleNestedModuleSubConfig(ex_index, 0) + ",\n" - + buildExampleNestedModuleSubConfig(ex_index, 1) + "\n" - " ],\n" + " \"modules\": [\n"; + for (int i = 0; i < nested_count - 1; ++i) { + config += buildExampleNestedModuleSubConfig(ex_index, i) + ",\n"; + } + config += buildExampleNestedModuleSubConfig(ex_index, nested_count - 1) + "\n"; + config += " ],\n" " \"uses_forcing_file\": false\n" + buildExampleOutputVariablesSubConfig(ex_index) + "\n" " }\n" @@ -423,7 +425,7 @@ class Bmi_Multi_Formulation_Test : public ::testing::Test { main_output_variables[ex_index] = nested_module_main_output_variables[ex_index][example_module_depth[ex_index] - 1]; specified_output_variables[ex_index] = output_variables; - buildExampleConfig(ex_index); + buildExampleConfig(ex_index, nested_module_lists[ex_index].size()); } @@ -451,7 +453,7 @@ void Bmi_Multi_Formulation_Test::SetUp() { // Define this manually to set how many nested modules per example, and implicitly how many examples. // This means example_module_depth.size() example scenarios with example_module_depth[i] nested modules in each scenario. - example_module_depth = {2, 2, 2, 2, 2, 2}; + example_module_depth = {2, 2, 2, 2, 2, 2, 3}; // Initialize the members for holding required input and result test data for individual example scenarios setupExampleDataCollections(); @@ -491,7 +493,14 @@ void Bmi_Multi_Formulation_Test::SetUp() { // Cases 4 and 5 Specifically to test output_variables failure cases... initializeTestExample(4, "cat-27", {std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, { "bogus_variable" }); initializeTestExample(5, "cat-27", {std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, { "OUTPUT_VAR_1" }); - + + #if NGEN_WITH_BMI_C + initializeTestExample(6, "cat-27", {std::string(BMI_C_TYPE), std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, {"OUTPUT_VAR_1__0"}); // Output var from C module... + #else + initializeTestExample(6, "cat-27", {std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, {"OUTPUT_VAR_1__0"}); // Output var from Fortran module... + + #endif // NGEN_WITH_PYTHON + } /** Simple test to make sure the model config from example 0 initializes. */ @@ -884,6 +893,16 @@ TEST_F(Bmi_Multi_Formulation_Test, GetAvailableVariableNames) { ); } } + +TEST_F(Bmi_Multi_Formulation_Test, MassBalanceCheck) { + int ex_index = 6; + + Bmi_Multi_Formulation formulation(catchment_ids[ex_index], std::make_unique(*forcing_params_examples[ex_index]), utils::StreamHandler()); + formulation.create_formulation(config_prop_ptree[ex_index]); + + formulation.check_mass_balance(0, 1, "t0"); +} + #endif // NGEN_WITH_BMI_C || NGEN_WITH_BMI_FORTRAN || NGEN_WITH_PYTHON #endif // NGEN_BMI_MULTI_FORMULATION_TEST_CPP From d62be51ba1caeba36b2581c9e4e0eb74ebb3cd40 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Tue, 16 Sep 2025 18:07:30 -0600 Subject: [PATCH 09/58] feat(ngen): add mass balance check for all bmi modules during runtime --- CMakeLists.txt | 2 ++ include/core/Layer.hpp | 9 +++++++++ .../realizations/catchment/Bmi_Module_Formulation.hpp | 8 ++++++++ include/realizations/catchment/Bmi_Multi_Formulation.hpp | 9 +++++++++ include/realizations/catchment/Formulation.hpp | 1 + src/realizations/catchment/Bmi_Module_Formulation.cpp | 3 +++ src/realizations/catchment/CMakeLists.txt | 1 + 7 files changed, 33 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 35152e9ab1..1ec2c55fb3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -321,6 +321,7 @@ add_subdirectory("src/forcing") add_subdirectory("src/utilities/mdarray") add_subdirectory("src/utilities/mdframe") add_subdirectory("src/utilities/logging") +add_subdirectory("src/utilities/bmi") target_link_libraries(ngen PUBLIC @@ -333,6 +334,7 @@ target_link_libraries(ngen NGen::forcing NGen::core_mediator NGen::logging + NGen::bmi_protocols ) if(NGEN_WITH_SQLITE) diff --git a/include/core/Layer.hpp b/include/core/Layer.hpp index d5b883f30f..bb4c0ff638 100644 --- a/include/core/Layer.hpp +++ b/include/core/Layer.hpp @@ -119,6 +119,8 @@ namespace ngen double response(0.0); try{ response = r_c->get_response(output_time_index, simulation_time.get_output_interval_seconds()); + // Check mass balance if able + r_c->check_mass_balance(output_time_index, simulation_time.get_total_output_times(), current_timestamp); } catch(models::external::State_Exception& e){ std::string msg = e.what(); @@ -127,6 +129,13 @@ namespace ngen +" at feature id "+id; throw models::external::State_Exception(msg); } + catch(std::exception& e){ + std::string msg = e.what(); + msg = msg+" at timestep "+std::to_string(output_time_index) + +" ("+current_timestamp+")" + +" at feature id "+id; + throw std::runtime_error(msg); + } std::string output = std::to_string(output_time_index)+","+current_timestamp+","+ r_c->get_output_line_for_timestep(output_time_index)+"\n"; r_c->write_output(output); diff --git a/include/realizations/catchment/Bmi_Module_Formulation.hpp b/include/realizations/catchment/Bmi_Module_Formulation.hpp index 259ac1ee3b..4f432d8ed0 100644 --- a/include/realizations/catchment/Bmi_Module_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Module_Formulation.hpp @@ -7,6 +7,7 @@ #include "Bmi_Adapter.hpp" #include #include "bmi_utilities.hpp" +#include "bmi/protocols.hpp" using data_access::MEAN; using data_access::SUM; @@ -250,6 +251,12 @@ namespace realization { const std::vector get_bmi_input_variables() const override; const std::vector get_bmi_output_variables() const override; + virtual void check_mass_balance(const int& iteration, const int& total_steps, const std::string& timestamp) const override { + //Create the protocol context, each member is const, and cannot change during the check + models::bmi::protocols::Context ctx{iteration, total_steps, timestamp, id}; + bmi_protocols.mass_balance.run(ctx); + } + protected: /** @@ -419,6 +426,7 @@ namespace realization { int next_time_step_index = 0; private: + models::bmi::protocols::NgenBmiProtocols bmi_protocols; /** * Whether model ``Update`` calls are allowed and handled in some way by the backing model for time steps after * the model's ``end_time``. diff --git a/include/realizations/catchment/Bmi_Multi_Formulation.hpp b/include/realizations/catchment/Bmi_Multi_Formulation.hpp index 5d07137e29..c9e4167eb4 100644 --- a/include/realizations/catchment/Bmi_Multi_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Multi_Formulation.hpp @@ -42,6 +42,15 @@ namespace realization { virtual ~Bmi_Multi_Formulation() {}; + virtual void check_mass_balance(const int& iteration, const int& total_steps, const std::string& timestamp) const final { + for( const auto &module : modules ) { + // TODO may need to check on outputs form each module indepdently??? + // Right now, the assumption is that if each component is mass balanced + // then the entire formulation is mass balanced + module->check_mass_balance(iteration, total_steps, timestamp); + } + }; + /** * Convert a time value from the model to an epoch time in seconds. * diff --git a/include/realizations/catchment/Formulation.hpp b/include/realizations/catchment/Formulation.hpp index 0c593d197c..18e1a43b32 100644 --- a/include/realizations/catchment/Formulation.hpp +++ b/include/realizations/catchment/Formulation.hpp @@ -44,6 +44,7 @@ namespace realization { virtual void create_formulation(boost::property_tree::ptree &config, geojson::PropertyMap *global = nullptr) = 0; virtual void create_formulation(geojson::PropertyMap properties) = 0; + virtual void check_mass_balance(const int& iteration, const int& total_steps, const std::string& timestamp) const = 0; protected: virtual const std::vector& get_required_parameters() const = 0; diff --git a/src/realizations/catchment/Bmi_Module_Formulation.cpp b/src/realizations/catchment/Bmi_Module_Formulation.cpp index 1a16c6b5ef..48345cb698 100644 --- a/src/realizations/catchment/Bmi_Module_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Module_Formulation.cpp @@ -382,6 +382,9 @@ namespace realization { 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]); } + //Initialize all NgenBmiProtocols with the valid adapter pointer and any properties + //provided in the read configuration. + bmi_protocols = models::bmi::protocols::NgenBmiProtocols(get_bmi_model(), properties); } } /** diff --git a/src/realizations/catchment/CMakeLists.txt b/src/realizations/catchment/CMakeLists.txt index db9bc0f04d..48771b18bf 100644 --- a/src/realizations/catchment/CMakeLists.txt +++ b/src/realizations/catchment/CMakeLists.txt @@ -21,5 +21,6 @@ target_link_libraries(realizations_catchment PUBLIC NGen::geojson NGen::logging NGen::ngen_bmi + NGen::bmi_protocols ) From 164acd4c82bdd054bb626bc52ef2274f95837f4f Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Sat, 27 Sep 2025 00:01:30 -0600 Subject: [PATCH 10/58] dep(expected): vendor expected-lite header lib for bmi protocols --- include/utilities/bmi/nonstd/expected.hpp | 3637 +++++++++++++++++ .../utilities/bmi/nonstd/expected.tweak.hpp | 4 + 2 files changed, 3641 insertions(+) create mode 100644 include/utilities/bmi/nonstd/expected.hpp create mode 100644 include/utilities/bmi/nonstd/expected.tweak.hpp diff --git a/include/utilities/bmi/nonstd/expected.hpp b/include/utilities/bmi/nonstd/expected.hpp new file mode 100644 index 0000000000..ae1790d131 --- /dev/null +++ b/include/utilities/bmi/nonstd/expected.hpp @@ -0,0 +1,3637 @@ +// Vendored from https://github.com/martinmoene/expected-lite/commit/a7510b213a668306fb038c934e27e53cc01141d4 +// This version targets C++11 and later. +// +// Copyright (C) 2016-2025 Martin Moene. +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// expected lite is based on: +// A proposal to add a utility class to represent expected monad +// by Vicente J. Botet Escriba and Pierre Talbot. http:://wg21.link/p0323 + +#ifndef NONSTD_EXPECTED_LITE_HPP +#define NONSTD_EXPECTED_LITE_HPP + +#define expected_lite_MAJOR 0 +#define expected_lite_MINOR 9 +#define expected_lite_PATCH 0 + +#define expected_lite_VERSION expected_STRINGIFY(expected_lite_MAJOR) "." expected_STRINGIFY(expected_lite_MINOR) "." expected_STRINGIFY(expected_lite_PATCH) + +#define expected_STRINGIFY( x ) expected_STRINGIFY_( x ) +#define expected_STRINGIFY_( x ) #x + +// expected-lite configuration: + +#define nsel_EXPECTED_DEFAULT 0 +#define nsel_EXPECTED_NONSTD 1 +#define nsel_EXPECTED_STD 2 + +// tweak header support: + +#ifdef __has_include +# if __has_include() +# include +# endif +#define expected_HAVE_TWEAK_HEADER 1 +#else +#define expected_HAVE_TWEAK_HEADER 0 +//# pragma message("expected.hpp: Note: Tweak header not supported.") +#endif + +// expected selection and configuration: + +#if !defined( nsel_CONFIG_SELECT_EXPECTED ) +# define nsel_CONFIG_SELECT_EXPECTED ( nsel_HAVE_STD_EXPECTED ? nsel_EXPECTED_STD : nsel_EXPECTED_NONSTD ) +#endif + +// Proposal revisions: +// +// DXXXXR0: -- +// N4015 : -2 (2014-05-26) +// N4109 : -1 (2014-06-29) +// P0323R0: 0 (2016-05-28) +// P0323R1: 1 (2016-10-12) +// -------: +// P0323R2: 2 (2017-06-15) +// P0323R3: 3 (2017-10-15) +// P0323R4: 4 (2017-11-26) +// P0323R5: 5 (2018-02-08) +// P0323R6: 6 (2018-04-02) +// P0323R7: 7 (2018-06-22) * +// +// expected-lite uses 2 and higher + +#ifndef nsel_P0323R +# define nsel_P0323R 7 +#endif + +// Monadic operations proposal revisions: +// +// P2505R0: 0 (2021-12-12) +// P2505R1: 1 (2022-02-10) +// P2505R2: 2 (2022-04-15) +// P2505R3: 3 (2022-06-05) +// P2505R4: 4 (2022-06-15) +// P2505R5: 5 (2022-09-20) * +// +// expected-lite uses 5 + +#ifndef nsel_P2505R +# define nsel_P2505R 5 +#endif + +// Lean and mean inclusion of Windows.h, if applicable; default on for MSVC: + +#if !defined(nsel_CONFIG_WIN32_LEAN_AND_MEAN) && defined(_MSC_VER) +# define nsel_CONFIG_WIN32_LEAN_AND_MEAN 1 +#else +# define nsel_CONFIG_WIN32_LEAN_AND_MEAN 0 +#endif + +// Control marking class expected with [[nodiscard]]]: + +#if !defined(nsel_CONFIG_NO_NODISCARD) +# define nsel_CONFIG_NO_NODISCARD 0 +#else +# define nsel_CONFIG_NO_NODISCARD 1 +#endif + +// Control presence of C++ exception handling (try and auto discover): + +#ifndef nsel_CONFIG_NO_EXCEPTIONS +# if defined(_MSC_VER) +# include // for _HAS_EXCEPTIONS +# endif +# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS) +# define nsel_CONFIG_NO_EXCEPTIONS 0 +# else +# define nsel_CONFIG_NO_EXCEPTIONS 1 +# endif +#endif + +// at default use SEH with MSVC for no C++ exceptions + +#if !defined(nsel_CONFIG_NO_EXCEPTIONS_SEH) && defined(_MSC_VER) +# define nsel_CONFIG_NO_EXCEPTIONS_SEH nsel_CONFIG_NO_EXCEPTIONS +#else +# define nsel_CONFIG_NO_EXCEPTIONS_SEH 0 +#endif + +// C++ language version detection (C++23 is speculative): +// Note: VC14.0/1900 (VS2015) lacks too much from C++14. + +#ifndef nsel_CPLUSPLUS +# if defined(_MSVC_LANG ) && !defined(__clang__) +# define nsel_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG ) +# else +# define nsel_CPLUSPLUS __cplusplus +# endif +#endif + +#define nsel_CPP98_OR_GREATER ( nsel_CPLUSPLUS >= 199711L ) +#define nsel_CPP11_OR_GREATER ( nsel_CPLUSPLUS >= 201103L ) +#define nsel_CPP14_OR_GREATER ( nsel_CPLUSPLUS >= 201402L ) +#define nsel_CPP17_OR_GREATER ( nsel_CPLUSPLUS >= 201703L ) +#define nsel_CPP20_OR_GREATER ( nsel_CPLUSPLUS >= 202002L ) +#define nsel_CPP23_OR_GREATER ( nsel_CPLUSPLUS >= 202300L ) + +// Use C++23 std::expected if available and requested: + +#if nsel_CPP23_OR_GREATER && defined(__has_include ) +# if __has_include( ) +# define nsel_HAVE_STD_EXPECTED 1 +# else +# define nsel_HAVE_STD_EXPECTED 0 +# endif +#else +# define nsel_HAVE_STD_EXPECTED 0 +#endif + +#define nsel_USES_STD_EXPECTED ( (nsel_CONFIG_SELECT_EXPECTED == nsel_EXPECTED_STD) || ((nsel_CONFIG_SELECT_EXPECTED == nsel_EXPECTED_DEFAULT) && nsel_HAVE_STD_EXPECTED) ) + +// +// in_place: code duplicated in any-lite, expected-lite, expected-lite, value-ptr-lite, variant-lite: +// + +#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES +#define nonstd_lite_HAVE_IN_PLACE_TYPES 1 + +// C++17 std::in_place in : + +#if nsel_CPP17_OR_GREATER + +#include + +namespace nonstd { + +using std::in_place; +using std::in_place_type; +using std::in_place_index; +using std::in_place_t; +using std::in_place_type_t; +using std::in_place_index_t; + +#define nonstd_lite_in_place_t( T) std::in_place_t +#define nonstd_lite_in_place_type_t( T) std::in_place_type_t +#define nonstd_lite_in_place_index_t(K) std::in_place_index_t + +#define nonstd_lite_in_place( T) std::in_place_t{} +#define nonstd_lite_in_place_type( T) std::in_place_type_t{} +#define nonstd_lite_in_place_index(K) std::in_place_index_t{} + +} // namespace nonstd + +#else // nsel_CPP17_OR_GREATER + +#include + +namespace nonstd { +namespace detail { + +template< class T > +struct in_place_type_tag {}; + +template< std::size_t K > +struct in_place_index_tag {}; + +} // namespace detail + +struct in_place_t {}; + +template< class T > +inline in_place_t in_place( detail::in_place_type_tag = detail::in_place_type_tag() ) +{ + return in_place_t(); +} + +template< std::size_t K > +inline in_place_t in_place( detail::in_place_index_tag = detail::in_place_index_tag() ) +{ + return in_place_t(); +} + +template< class T > +inline in_place_t in_place_type( detail::in_place_type_tag = detail::in_place_type_tag() ) +{ + return in_place_t(); +} + +template< std::size_t K > +inline in_place_t in_place_index( detail::in_place_index_tag = detail::in_place_index_tag() ) +{ + return in_place_t(); +} + +// mimic templated typedef: + +#define nonstd_lite_in_place_t( T) nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag ) +#define nonstd_lite_in_place_type_t( T) nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag ) +#define nonstd_lite_in_place_index_t(K) nonstd::in_place_t(&)( nonstd::detail::in_place_index_tag ) + +#define nonstd_lite_in_place( T) nonstd::in_place_type +#define nonstd_lite_in_place_type( T) nonstd::in_place_type +#define nonstd_lite_in_place_index(K) nonstd::in_place_index + +} // namespace nonstd + +#endif // nsel_CPP17_OR_GREATER +#endif // nonstd_lite_HAVE_IN_PLACE_TYPES + +// +// Using std::expected: +// + +#if nsel_USES_STD_EXPECTED + +#include + +namespace nonstd { + + using std::expected; + using std::unexpected; + using std::bad_expected_access; + using std::unexpect_t; + using std::unexpect; + + //[[deprecated("replace unexpected_type with unexpected")]] + + template< typename E > + using unexpected_type = unexpected; + + // Unconditionally provide make_unexpected(): + + template< typename E > + constexpr auto make_unexpected( E && value ) -> unexpected< typename std::decay::type > + { + return unexpected< typename std::decay::type >( std::forward(value) ); + } + + template + < + typename E, typename... Args, + typename = std::enable_if< + std::is_constructible::value + > + > + constexpr auto + make_unexpected( std::in_place_t inplace, Args &&... args ) -> unexpected_type< typename std::decay::type > + { + return unexpected_type< typename std::decay::type >( inplace, std::forward(args)...); + } +} // namespace nonstd + +#else // nsel_USES_STD_EXPECTED + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// additional includes: + +#if nsel_CONFIG_WIN32_LEAN_AND_MEAN +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +#endif + +#if nsel_CONFIG_NO_EXCEPTIONS +# if nsel_CONFIG_NO_EXCEPTIONS_SEH +# include // for ExceptionCodes +# else +// already included: +# endif +#else +# include +#endif + +// C++ feature usage: + +#if nsel_CPP11_OR_GREATER +# define nsel_constexpr constexpr +#else +# define nsel_constexpr /*constexpr*/ +#endif + +#if nsel_CPP14_OR_GREATER +# define nsel_constexpr14 constexpr +#else +# define nsel_constexpr14 /*constexpr*/ +#endif + +#if nsel_CPP17_OR_GREATER +# define nsel_inline17 inline +#else +# define nsel_inline17 /*inline*/ +#endif + +// Compiler versions: +// +// MSVC++ 6.0 _MSC_VER == 1200 nsel_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0) +// MSVC++ 7.0 _MSC_VER == 1300 nsel_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002) +// MSVC++ 7.1 _MSC_VER == 1310 nsel_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003) +// MSVC++ 8.0 _MSC_VER == 1400 nsel_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005) +// MSVC++ 9.0 _MSC_VER == 1500 nsel_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008) +// MSVC++ 10.0 _MSC_VER == 1600 nsel_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010) +// MSVC++ 11.0 _MSC_VER == 1700 nsel_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012) +// MSVC++ 12.0 _MSC_VER == 1800 nsel_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013) +// MSVC++ 14.0 _MSC_VER == 1900 nsel_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015) +// MSVC++ 14.1 _MSC_VER >= 1910 nsel_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017) +// MSVC++ 14.2 _MSC_VER >= 1920 nsel_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019) + +#if defined(_MSC_VER) && !defined(__clang__) +# define nsel_COMPILER_MSVC_VER (_MSC_VER ) +# define nsel_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * ( 5 + (_MSC_VER < 1900)) ) +#else +# define nsel_COMPILER_MSVC_VER 0 +# define nsel_COMPILER_MSVC_VERSION 0 +#endif + +#define nsel_COMPILER_VERSION( major, minor, patch ) ( 10 * ( 10 * (major) + (minor) ) + (patch) ) + +#if defined(__clang__) +# define nsel_COMPILER_CLANG_VERSION nsel_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__) +#else +# define nsel_COMPILER_CLANG_VERSION 0 +#endif + +#if defined(__GNUC__) && !defined(__clang__) +# define nsel_COMPILER_GNUC_VERSION nsel_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#else +# define nsel_COMPILER_GNUC_VERSION 0 +#endif + +// half-open range [lo..hi): +//#define nsel_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) ) + +// Method enabling + +#define nsel_REQUIRES_0(...) \ + template< bool B = (__VA_ARGS__), typename std::enable_if::type = 0 > + +#define nsel_REQUIRES_T(...) \ + , typename std::enable_if< (__VA_ARGS__), int >::type = 0 + +#define nsel_REQUIRES_R(R, ...) \ + typename std::enable_if< (__VA_ARGS__), R>::type + +#define nsel_REQUIRES_A(...) \ + , typename std::enable_if< (__VA_ARGS__), void*>::type = nullptr + +// Clang, GNUC, MSVC warning suppression macros: + +#ifdef __clang__ +# pragma clang diagnostic push +#elif defined __GNUC__ +# pragma GCC diagnostic push +#endif // __clang__ + +#if nsel_COMPILER_MSVC_VERSION >= 140 +# define nsel_DISABLE_MSVC_WARNINGS(codes) __pragma( warning(push) ) __pragma( warning(disable: codes) ) +#else +# define nsel_DISABLE_MSVC_WARNINGS(codes) +#endif + +#ifdef __clang__ +# define nsel_RESTORE_WARNINGS() _Pragma("clang diagnostic pop") +# define nsel_RESTORE_MSVC_WARNINGS() +#elif defined __GNUC__ +# define nsel_RESTORE_WARNINGS() _Pragma("GCC diagnostic pop") +# define nsel_RESTORE_MSVC_WARNINGS() +#elif nsel_COMPILER_MSVC_VERSION >= 140 +# define nsel_RESTORE_WARNINGS() __pragma( warning( pop ) ) +# define nsel_RESTORE_MSVC_WARNINGS() nsel_RESTORE_WARNINGS() +#else +# define nsel_RESTORE_WARNINGS() +# define nsel_RESTORE_MSVC_WARNINGS() +#endif + +// Suppress the following MSVC (GSL) warnings: +// - C26409: Avoid calling new and delete explicitly, use std::make_unique instead (r.11) + +nsel_DISABLE_MSVC_WARNINGS( 26409 ) + +// Presence of language and library features: + +#ifdef _HAS_CPP0X +# define nsel_HAS_CPP0X _HAS_CPP0X +#else +# define nsel_HAS_CPP0X 0 +#endif + +// Presence of language and library features: + +#define nsel_CPP11_000 (nsel_CPP11_OR_GREATER) +#define nsel_CPP17_000 (nsel_CPP17_OR_GREATER) + +// Presence of C++11 library features: + +#define nsel_HAVE_ADDRESSOF nsel_CPP11_000 + +// Presence of C++17 language features: + +#define nsel_HAVE_DEPRECATED nsel_CPP17_000 +#define nsel_HAVE_NODISCARD nsel_CPP17_000 + +// C++ feature usage: + +#if nsel_HAVE_DEPRECATED +# define nsel_deprecated(msg) [[deprecated(msg)]] +#else +# define nsel_deprecated(msg) /*[[deprecated]]*/ +#endif + +#if nsel_HAVE_NODISCARD && !nsel_CONFIG_NO_NODISCARD +# define nsel_NODISCARD [[nodiscard]] +#else +# define nsel_NODISCARD /*[[nodiscard]]*/ +#endif + +// +// expected: +// + +namespace nonstd { namespace expected_lite { + +// library features C++11: + +namespace std11 { + +// #if 0 && nsel_HAVE_ADDRESSOF +#if nsel_HAVE_ADDRESSOF + using std::addressof; +#else + template< class T > + T * addressof( T & arg ) noexcept + { + return &arg; + } + + template< class T > + const T * addressof( const T && ) = delete; +#endif +} // namespace std11 + +// type traits C++17: + +namespace std17 { + +#if nsel_CPP17_OR_GREATER + +using std::conjunction; +using std::is_swappable; +using std::is_nothrow_swappable; + +#else // nsel_CPP17_OR_GREATER + +namespace detail { + +using std::swap; + +struct is_swappable +{ + template< typename T, typename = decltype( swap( std::declval(), std::declval() ) ) > + static std::true_type test( int /* unused */); + + template< typename > + static std::false_type test(...); +}; + +struct is_nothrow_swappable +{ + // wrap noexcept(expr) in separate function as work-around for VC140 (VS2015): + + template< typename T > + static constexpr bool satisfies() + { + return noexcept( swap( std::declval(), std::declval() ) ); + } + + template< typename T > + static auto test( int ) -> std::integral_constant()>{} + + template< typename > + static auto test(...) -> std::false_type; +}; +} // namespace detail + +// is [nothrow] swappable: + +template< typename T > +struct is_swappable : decltype( detail::is_swappable::test(0) ){}; + +template< typename T > +struct is_nothrow_swappable : decltype( detail::is_nothrow_swappable::test(0) ){}; + +// conjunction: + +template< typename... > struct conjunction : std::true_type{}; +template< typename B1 > struct conjunction : B1{}; + +template< typename B1, typename... Bn > +struct conjunction : std::conditional, B1>::type{}; + +#endif // nsel_CPP17_OR_GREATER + +} // namespace std17 + +// type traits C++20: + +namespace std20 { + +#if defined(__cpp_lib_remove_cvref) + +using std::remove_cvref; + +#else + +template< typename T > +struct remove_cvref +{ + typedef typename std::remove_cv< typename std::remove_reference::type >::type type; +}; + +#endif + +} // namespace std20 + +// forward declaration: + +template< typename T, typename E > +class expected; + +namespace detail { + +#if nsel_P2505R >= 3 +template< typename T > +struct is_expected : std::false_type {}; + +template< typename T, typename E > +struct is_expected< expected< T, E > > : std::true_type {}; +#endif // nsel_P2505R >= 3 + +/// discriminated union to hold value or 'error'. + +template< typename T, typename E > +class storage_t_noncopy_nonmove_impl +{ + template< typename, typename > friend class nonstd::expected_lite::expected; + +public: + using value_type = T; + using error_type = E; + + // no-op construction + storage_t_noncopy_nonmove_impl() {} + ~storage_t_noncopy_nonmove_impl() {} + + explicit storage_t_noncopy_nonmove_impl( bool has_value ) + : m_has_value( has_value ) + {} + + void construct_value() + { + new( std11::addressof(m_value) ) value_type(); + } + + // void construct_value( value_type const & e ) + // { + // new( std11::addressof(m_value) ) value_type( e ); + // } + + // void construct_value( value_type && e ) + // { + // new( std11::addressof(m_value) ) value_type( std::move( e ) ); + // } + + template< class... Args > + void emplace_value( Args&&... args ) + { + new( std11::addressof(m_value) ) value_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_value( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_value) ) value_type( il, std::forward(args)... ); + } + + void destruct_value() + { + m_value.~value_type(); + } + + // void construct_error( error_type const & e ) + // { + // // new( std11::addressof(m_error) ) error_type( e ); + // } + + // void construct_error( error_type && e ) + // { + // // new( std11::addressof(m_error) ) error_type( std::move( e ) ); + // } + + template< class... Args > + void emplace_error( Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_error( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( il, std::forward(args)... ); + } + + void destruct_error() + { + m_error.~error_type(); + } + + constexpr value_type const & value() const & + { + return m_value; + } + + value_type & value() & + { + return m_value; + } + + constexpr value_type const && value() const && + { + return std::move( m_value ); + } + + nsel_constexpr14 value_type && value() && + { + return std::move( m_value ); + } + + value_type const * value_ptr() const + { + return std11::addressof(m_value); + } + + value_type * value_ptr() + { + return std11::addressof(m_value); + } + + error_type const & error() const & + { + return m_error; + } + + error_type & error() & + { + return m_error; + } + + constexpr error_type const && error() const && + { + return std::move( m_error ); + } + + nsel_constexpr14 error_type && error() && + { + return std::move( m_error ); + } + + bool has_value() const + { + return m_has_value; + } + + void set_has_value( bool v ) + { + m_has_value = v; + } + +private: + union + { + value_type m_value; + error_type m_error; + }; + + bool m_has_value = false; +}; + +template< typename T, typename E > +class storage_t_impl +{ + template< typename, typename > friend class nonstd::expected_lite::expected; + +public: + using value_type = T; + using error_type = E; + + // no-op construction + storage_t_impl() {} + ~storage_t_impl() {} + + explicit storage_t_impl( bool has_value ) + : m_has_value( has_value ) + {} + + void construct_value() + { + new( std11::addressof(m_value) ) value_type(); + } + + void construct_value( value_type const & e ) + { + new( std11::addressof(m_value) ) value_type( e ); + } + + void construct_value( value_type && e ) + { + new( std11::addressof(m_value) ) value_type( std::move( e ) ); + } + + template< class... Args > + void emplace_value( Args&&... args ) + { + new( std11::addressof(m_value) ) value_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_value( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_value) ) value_type( il, std::forward(args)... ); + } + + void destruct_value() + { + m_value.~value_type(); + } + + void construct_error( error_type const & e ) + { + new( std11::addressof(m_error) ) error_type( e ); + } + + void construct_error( error_type && e ) + { + new( std11::addressof(m_error) ) error_type( std::move( e ) ); + } + + template< class... Args > + void emplace_error( Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_error( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( il, std::forward(args)... ); + } + + void destruct_error() + { + m_error.~error_type(); + } + + constexpr value_type const & value() const & + { + return m_value; + } + + value_type & value() & + { + return m_value; + } + + constexpr value_type const && value() const && + { + return std::move( m_value ); + } + + nsel_constexpr14 value_type && value() && + { + return std::move( m_value ); + } + + value_type const * value_ptr() const + { + return std11::addressof(m_value); + } + + value_type * value_ptr() + { + return std11::addressof(m_value); + } + + error_type const & error() const & + { + return m_error; + } + + error_type & error() & + { + return m_error; + } + + constexpr error_type const && error() const && + { + return std::move( m_error ); + } + + nsel_constexpr14 error_type && error() && + { + return std::move( m_error ); + } + + bool has_value() const + { + return m_has_value; + } + + void set_has_value( bool v ) + { + m_has_value = v; + } + +private: + union + { + value_type m_value; + error_type m_error; + }; + + bool m_has_value = false; +}; + +/// discriminated union to hold only 'error'. + +template< typename E > +struct storage_t_impl< void, E > +{ + template< typename, typename > friend class nonstd::expected_lite::expected; + +public: + using value_type = void; + using error_type = E; + + // no-op construction + storage_t_impl() {} + ~storage_t_impl() {} + + explicit storage_t_impl( bool has_value ) + : m_has_value( has_value ) + {} + + void construct_error( error_type const & e ) + { + new( std11::addressof(m_error) ) error_type( e ); + } + + void construct_error( error_type && e ) + { + new( std11::addressof(m_error) ) error_type( std::move( e ) ); + } + + template< class... Args > + void emplace_error( Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_error( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( il, std::forward(args)... ); + } + + void destruct_error() + { + m_error.~error_type(); + } + + error_type const & error() const & + { + return m_error; + } + + error_type & error() & + { + return m_error; + } + + constexpr error_type const && error() const && + { + return std::move( m_error ); + } + + nsel_constexpr14 error_type && error() && + { + return std::move( m_error ); + } + + bool has_value() const + { + return m_has_value; + } + + void set_has_value( bool v ) + { + m_has_value = v; + } + +private: + union + { + char m_dummy; + error_type m_error; + }; + + bool m_has_value = false; +}; + +template< typename T, typename E, bool isConstructable, bool isMoveable > +class storage_t +{ +public: +}; + +template< typename T, typename E > +class storage_t : public storage_t_noncopy_nonmove_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_noncopy_nonmove_impl( has_value ) + {} + + storage_t( storage_t const & other ) = delete; + storage_t( storage_t && other ) = delete; + +}; + +template< typename T, typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) this->construct_value( other.value() ); + else this->construct_error( other.error() ); + } + + storage_t(storage_t && other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) this->construct_value( std::move( other.value() ) ); + else this->construct_error( std::move( other.error() ) ); + } +}; + +template< typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) ; + else this->construct_error( other.error() ); + } + + storage_t(storage_t && other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) ; + else this->construct_error( std::move( other.error() ) ); + } +}; + +template< typename T, typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) + : storage_t_impl(other.has_value()) + { + if ( this->has_value() ) this->construct_value( other.value() ); + else this->construct_error( other.error() ); + } + + storage_t( storage_t && other ) = delete; +}; + +template< typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) + : storage_t_impl(other.has_value()) + { + if ( this->has_value() ) ; + else this->construct_error( other.error() ); + } + + storage_t( storage_t && other ) = delete; +}; + +template< typename T, typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) = delete; + + storage_t( storage_t && other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) this->construct_value( std::move( other.value() ) ); + else this->construct_error( std::move( other.error() ) ); + } +}; + +template< typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) = delete; + + storage_t( storage_t && other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) ; + else this->construct_error( std::move( other.error() ) ); + } +}; + +#if nsel_P2505R >= 3 +// C++11 invoke implementation +template< typename > +struct is_reference_wrapper : std::false_type {}; +template< typename T > +struct is_reference_wrapper< std::reference_wrapper< T > > : std::true_type {}; + +template< typename FnT, typename ClassT, typename ObjectT, typename... Args + nsel_REQUIRES_T( + std::is_function::value + && ( std::is_same< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + || std::is_base_of< ClassT, typename std20::remove_cvref< ObjectT >::type >::value ) + ) +> +nsel_constexpr auto invoke_member_function_impl( FnT ClassT::* memfnptr, ObjectT && obj, Args && ... args ) + noexcept( noexcept( (std::forward< ObjectT >( obj ).*memfnptr)( std::forward< Args >( args )... ) ) ) + -> decltype( (std::forward< ObjectT >( obj ).*memfnptr)( std::forward< Args >( args )...) ) +{ + return (std::forward< ObjectT >( obj ).*memfnptr)( std::forward< Args >( args )... ); +} + +template< typename FnT, typename ClassT, typename ObjectT, typename... Args + nsel_REQUIRES_T( + std::is_function::value + && is_reference_wrapper< typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_function_impl( FnT ClassT::* memfnptr, ObjectT && obj, Args && ... args ) + noexcept( noexcept( (obj.get().*memfnptr)( std::forward< Args >( args ) ... ) ) ) + -> decltype( (obj.get().*memfnptr)( std::forward< Args >( args ) ... ) ) +{ + return (obj.get().*memfnptr)( std::forward< Args >( args ) ... ); +} + +template< typename FnT, typename ClassT, typename ObjectT, typename... Args + nsel_REQUIRES_T( + std::is_function::value + && !std::is_same< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + && !std::is_base_of< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + && !is_reference_wrapper< typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_function_impl( FnT ClassT::* memfnptr, ObjectT && obj, Args && ... args ) + noexcept( noexcept( ((*std::forward< ObjectT >( obj )).*memfnptr)( std::forward< Args >( args ) ... ) ) ) + -> decltype( ((*std::forward< ObjectT >( obj )).*memfnptr)( std::forward< Args >( args ) ... ) ) +{ + return ((*std::forward(obj)).*memfnptr)( std::forward< Args >( args ) ... ); +} + +template< typename MemberT, typename ClassT, typename ObjectT + nsel_REQUIRES_T( + std::is_same< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + || std::is_base_of< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_object_impl( MemberT ClassT::* memobjptr, ObjectT && obj ) + noexcept( noexcept( std::forward< ObjectT >( obj ).*memobjptr ) ) + -> decltype( std::forward< ObjectT >( obj ).*memobjptr ) +{ + return std::forward< ObjectT >( obj ).*memobjptr; +} + +template< typename MemberT, typename ClassT, typename ObjectT + nsel_REQUIRES_T( + is_reference_wrapper< typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_object_impl( MemberT ClassT::* memobjptr, ObjectT && obj ) + noexcept( noexcept( obj.get().*memobjptr ) ) + -> decltype( obj.get().*memobjptr ) +{ + return obj.get().*memobjptr; +} + +template< typename MemberT, typename ClassT, typename ObjectT + nsel_REQUIRES_T( + !std::is_same< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + && !std::is_base_of< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + && !is_reference_wrapper< typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_object_impl( MemberT ClassT::* memobjptr, ObjectT && obj ) + noexcept( noexcept( (*std::forward< ObjectT >( obj )).*memobjptr ) ) + -> decltype( (*std::forward< ObjectT >( obj )).*memobjptr ) +{ + return (*std::forward< ObjectT >( obj )).*memobjptr; +} + +template< typename F, typename... Args + nsel_REQUIRES_T( + std::is_member_function_pointer< typename std20::remove_cvref< F >::type >::value + ) +> +nsel_constexpr auto invoke( F && f, Args && ... args ) + noexcept( noexcept( invoke_member_function_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ) ) ) + -> decltype( invoke_member_function_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ) ) +{ + return invoke_member_function_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ); +} + +template< typename F, typename... Args + nsel_REQUIRES_T( + std::is_member_object_pointer< typename std20::remove_cvref< F >::type >::value + ) +> +nsel_constexpr auto invoke( F && f, Args && ... args ) + noexcept( noexcept( invoke_member_object_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ) ) ) + -> decltype( invoke_member_object_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ) ) +{ + return invoke_member_object_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ); +} + +template< typename F, typename... Args + nsel_REQUIRES_T( + !std::is_member_function_pointer< typename std20::remove_cvref< F >::type >::value + && !std::is_member_object_pointer< typename std20::remove_cvref< F >::type >::value + ) +> +nsel_constexpr auto invoke( F && f, Args && ... args ) + noexcept( noexcept( std::forward< F >( f )( std::forward< Args >( args ) ... ) ) ) + -> decltype( std::forward< F >( f )( std::forward< Args >( args ) ... ) ) +{ + return std::forward< F >( f )( std::forward< Args >( args ) ... ); +} + +template< typename F, typename ... Args > +using invoke_result_nocvref_t = typename std20::remove_cvref< decltype( ::nonstd::expected_lite::detail::invoke( std::declval< F >(), std::declval< Args >()... ) ) >::type; + +#if nsel_P2505R >= 5 +template< typename F, typename ... Args > +using transform_invoke_result_t = typename std::remove_cv< decltype( ::nonstd::expected_lite::detail::invoke( std::declval< F >(), std::declval< Args >()... ) ) >::type; +#else +template< typename F, typename ... Args > +using transform_invoke_result_t = invoke_result_nocvref_t +#endif // nsel_P2505R >= 5 + +template< typename T > +struct valid_expected_value_type : std::integral_constant< bool, std::is_destructible< T >::value && !std::is_reference< T >::value && !std::is_array< T >::value > {}; + +#endif // nsel_P2505R >= 3 +} // namespace detail + +/// x.x.5 Unexpected object type; unexpected_type; C++17 and later can also use aliased type unexpected. + +#if nsel_P0323R <= 2 +template< typename E = std::exception_ptr > +class unexpected_type +#else +template< typename E > +class unexpected_type +#endif // nsel_P0323R +{ +public: + using error_type = E; + + // x.x.5.2.1 Constructors + +// unexpected_type() = delete; + + constexpr unexpected_type( unexpected_type const & ) = default; + constexpr unexpected_type( unexpected_type && ) = default; + + template< typename... Args + nsel_REQUIRES_T( + std::is_constructible::value + ) + > + constexpr explicit unexpected_type( nonstd_lite_in_place_t(E), Args &&... args ) + : m_error( std::forward( args )...) + {} + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_constructible, Args&&...>::value + ) + > + constexpr explicit unexpected_type( nonstd_lite_in_place_t(E), std::initializer_list il, Args &&... args ) + : m_error( il, std::forward( args )...) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_same< typename std20::remove_cvref::type, nonstd_lite_in_place_t(E2) >::value + && !std::is_same< typename std20::remove_cvref::type, unexpected_type >::value + ) + > + constexpr explicit unexpected_type( E2 && error ) + : m_error( std::forward( error ) ) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible< E, E2>::value + && !std::is_constructible & >::value + && !std::is_constructible >::value + && !std::is_constructible const & >::value + && !std::is_constructible const >::value + && !std::is_convertible< unexpected_type &, E>::value + && !std::is_convertible< unexpected_type , E>::value + && !std::is_convertible< unexpected_type const &, E>::value + && !std::is_convertible< unexpected_type const , E>::value + && !std::is_convertible< E2 const &, E>::value /*=> explicit */ + ) + > + constexpr explicit unexpected_type( unexpected_type const & error ) + : m_error( E{ error.error() } ) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible< E, E2>::value + && !std::is_constructible & >::value + && !std::is_constructible >::value + && !std::is_constructible const & >::value + && !std::is_constructible const >::value + && !std::is_convertible< unexpected_type &, E>::value + && !std::is_convertible< unexpected_type , E>::value + && !std::is_convertible< unexpected_type const &, E>::value + && !std::is_convertible< unexpected_type const , E>::value + && std::is_convertible< E2 const &, E>::value /*=> explicit */ + ) + > + constexpr /*non-explicit*/ unexpected_type( unexpected_type const & error ) + : m_error( error.error() ) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible< E, E2>::value + && !std::is_constructible & >::value + && !std::is_constructible >::value + && !std::is_constructible const & >::value + && !std::is_constructible const >::value + && !std::is_convertible< unexpected_type &, E>::value + && !std::is_convertible< unexpected_type , E>::value + && !std::is_convertible< unexpected_type const &, E>::value + && !std::is_convertible< unexpected_type const , E>::value + && !std::is_convertible< E2 const &, E>::value /*=> explicit */ + ) + > + constexpr explicit unexpected_type( unexpected_type && error ) + : m_error( E{ std::move( error.error() ) } ) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible< E, E2>::value + && !std::is_constructible & >::value + && !std::is_constructible >::value + && !std::is_constructible const & >::value + && !std::is_constructible const >::value + && !std::is_convertible< unexpected_type &, E>::value + && !std::is_convertible< unexpected_type , E>::value + && !std::is_convertible< unexpected_type const &, E>::value + && !std::is_convertible< unexpected_type const , E>::value + && std::is_convertible< E2 const &, E>::value /*=> non-explicit */ + ) + > + constexpr /*non-explicit*/ unexpected_type( unexpected_type && error ) + : m_error( std::move( error.error() ) ) + {} + + // x.x.5.2.2 Assignment + + nsel_constexpr14 unexpected_type& operator=( unexpected_type const & ) = default; + nsel_constexpr14 unexpected_type& operator=( unexpected_type && ) = default; + + template< typename E2 = E > + nsel_constexpr14 unexpected_type & operator=( unexpected_type const & other ) + { + unexpected_type{ other.error() }.swap( *this ); + return *this; + } + + template< typename E2 = E > + nsel_constexpr14 unexpected_type & operator=( unexpected_type && other ) + { + unexpected_type{ std::move( other.error() ) }.swap( *this ); + return *this; + } + + // x.x.5.2.3 Observers + + nsel_constexpr14 E & error() & noexcept + { + return m_error; + } + + constexpr E const & error() const & noexcept + { + return m_error; + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + nsel_constexpr14 E && error() && noexcept + { + return std::move( m_error ); + } + + constexpr E const && error() const && noexcept + { + return std::move( m_error ); + } + +#endif + + // x.x.5.2.3 Observers - deprecated + + nsel_deprecated("replace value() with error()") + + nsel_constexpr14 E & value() & noexcept + { + return m_error; + } + + nsel_deprecated("replace value() with error()") + + constexpr E const & value() const & noexcept + { + return m_error; + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + nsel_deprecated("replace value() with error()") + + nsel_constexpr14 E && value() && noexcept + { + return std::move( m_error ); + } + + nsel_deprecated("replace value() with error()") + + constexpr E const && value() const && noexcept + { + return std::move( m_error ); + } + +#endif + + // x.x.5.2.4 Swap + + template< typename U=E > + nsel_REQUIRES_R( void, + std17::is_swappable::value + ) + swap( unexpected_type & other ) noexcept ( + std17::is_nothrow_swappable::value + ) + { + using std::swap; + swap( m_error, other.m_error ); + } + + // TODO: ??? unexpected_type: in-class friend operator==, != + +private: + error_type m_error; +}; + +#if nsel_CPP17_OR_GREATER + +/// template deduction guide: + +template< typename E > +unexpected_type( E ) -> unexpected_type< E >; + +#endif + +/// class unexpected_type, std::exception_ptr specialization (P0323R2) + +#if !nsel_CONFIG_NO_EXCEPTIONS +#if nsel_P0323R <= 2 + +// TODO: Should expected be specialized for particular E types such as exception_ptr and how? +// See p0323r7 2.1. Ergonomics, http://wg21.link/p0323 +template<> +class unexpected_type< std::exception_ptr > +{ +public: + using error_type = std::exception_ptr; + + unexpected_type() = delete; + + ~unexpected_type(){} + + explicit unexpected_type( std::exception_ptr const & error ) + : m_error( error ) + {} + + explicit unexpected_type(std::exception_ptr && error ) + : m_error( std::move( error ) ) + {} + + template< typename E > + explicit unexpected_type( E error ) + : m_error( std::make_exception_ptr( error ) ) + {} + + std::exception_ptr const & value() const + { + return m_error; + } + + std::exception_ptr & value() + { + return m_error; + } + +private: + std::exception_ptr m_error; +}; + +#endif // nsel_P0323R +#endif // !nsel_CONFIG_NO_EXCEPTIONS + +/// x.x.4, Unexpected equality operators + +template< typename E1, typename E2 > +constexpr bool operator==( unexpected_type const & x, unexpected_type const & y ) +{ + return x.error() == y.error(); +} + +template< typename E1, typename E2 > +constexpr bool operator!=( unexpected_type const & x, unexpected_type const & y ) +{ + return ! ( x == y ); +} + +#if nsel_P0323R <= 2 + +template< typename E > +constexpr bool operator<( unexpected_type const & x, unexpected_type const & y ) +{ + return x.error() < y.error(); +} + +template< typename E > +constexpr bool operator>( unexpected_type const & x, unexpected_type const & y ) +{ + return ( y < x ); +} + +template< typename E > +constexpr bool operator<=( unexpected_type const & x, unexpected_type const & y ) +{ + return ! ( y < x ); +} + +template< typename E > +constexpr bool operator>=( unexpected_type const & x, unexpected_type const & y ) +{ + return ! ( x < y ); +} + +#endif // nsel_P0323R + +/// x.x.5 Specialized algorithms + +template< typename E + nsel_REQUIRES_T( + std17::is_swappable::value + ) +> +void swap( unexpected_type & x, unexpected_type & y) noexcept ( noexcept ( x.swap(y) ) ) +{ + x.swap( y ); +} + +#if nsel_P0323R <= 2 + +// unexpected: relational operators for std::exception_ptr: + +inline constexpr bool operator<( unexpected_type const & /*x*/, unexpected_type const & /*y*/ ) +{ + return false; +} + +inline constexpr bool operator>( unexpected_type const & /*x*/, unexpected_type const & /*y*/ ) +{ + return false; +} + +inline constexpr bool operator<=( unexpected_type const & x, unexpected_type const & y ) +{ + return ( x == y ); +} + +inline constexpr bool operator>=( unexpected_type const & x, unexpected_type const & y ) +{ + return ( x == y ); +} + +#endif // nsel_P0323R + +// unexpected: traits + +#if nsel_P0323R <= 3 + +template< typename E > +struct is_unexpected : std::false_type {}; + +template< typename E > +struct is_unexpected< unexpected_type > : std::true_type {}; + +#endif // nsel_P0323R + +// unexpected: factory + +// keep make_unexpected() removed in p0323r2 for pre-C++17: + +template< typename E > +nsel_constexpr14 auto +make_unexpected( E && value ) -> unexpected_type< typename std::decay::type > +{ + return unexpected_type< typename std::decay::type >( std::forward(value) ); +} + +template +< + typename E, typename... Args, + typename = std::enable_if< + std::is_constructible::value + > +> +nsel_constexpr14 auto +make_unexpected( nonstd_lite_in_place_t(E), Args &&... args ) -> unexpected_type< typename std::decay::type > +{ + return std::move( unexpected_type< typename std::decay::type >( nonstd_lite_in_place(E), std::forward(args)...) ); +} + +#if nsel_P0323R <= 3 + +/*nsel_constexpr14*/ auto inline +make_unexpected_from_current_exception() -> unexpected_type< std::exception_ptr > +{ + return unexpected_type< std::exception_ptr >( std::current_exception() ); +} + +#endif // nsel_P0323R + +/// x.x.6, x.x.7 expected access error + +template< typename E > +class nsel_NODISCARD bad_expected_access; + +/// x.x.7 bad_expected_access: expected access error + +template <> +class nsel_NODISCARD bad_expected_access< void > : public std::exception +{ +public: + explicit bad_expected_access() + : std::exception() + {} +}; + +/// x.x.6 bad_expected_access: expected access error + +#if !nsel_CONFIG_NO_EXCEPTIONS + +template< typename E > +class nsel_NODISCARD bad_expected_access : public bad_expected_access< void > +{ +public: + using error_type = E; + + explicit bad_expected_access( error_type error ) + : m_error( error ) + {} + + virtual char const * what() const noexcept override + { + return "bad_expected_access"; + } + + nsel_constexpr14 error_type & error() & + { + return m_error; + } + + constexpr error_type const & error() const & + { + return m_error; + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + nsel_constexpr14 error_type && error() && + { + return std::move( m_error ); + } + + constexpr error_type const && error() const && + { + return std::move( m_error ); + } + +#endif + +private: + error_type m_error; +}; + +#endif // nsel_CONFIG_NO_EXCEPTIONS + +/// x.x.8 unexpect tag, in_place_unexpected tag: construct an error + +struct unexpect_t{}; +using in_place_unexpected_t = unexpect_t; + +nsel_inline17 constexpr unexpect_t unexpect{}; +nsel_inline17 constexpr unexpect_t in_place_unexpected{}; + +/// class error_traits + +#if nsel_CONFIG_NO_EXCEPTIONS + +namespace detail { + inline bool text( char const * /*text*/ ) { return true; } +} + +template< typename Error > +struct error_traits +{ + static void rethrow( Error const & /*e*/ ) + { +#if nsel_CONFIG_NO_EXCEPTIONS_SEH + RaiseException( EXCEPTION_ACCESS_VIOLATION, EXCEPTION_NONCONTINUABLE, 0, NULL ); +#else + assert( false && detail::text("throw bad_expected_access{ e };") ); +#endif + } +}; + +template<> +struct error_traits< std::exception_ptr > +{ + static void rethrow( std::exception_ptr const & /*e*/ ) + { +#if nsel_CONFIG_NO_EXCEPTIONS_SEH + RaiseException( EXCEPTION_ACCESS_VIOLATION, EXCEPTION_NONCONTINUABLE, 0, NULL ); +#else + assert( false && detail::text("throw bad_expected_access{ e };") ); +#endif + } +}; + +template<> +struct error_traits< std::error_code > +{ + static void rethrow( std::error_code const & /*e*/ ) + { +#if nsel_CONFIG_NO_EXCEPTIONS_SEH + RaiseException( EXCEPTION_ACCESS_VIOLATION, EXCEPTION_NONCONTINUABLE, 0, NULL ); +#else + assert( false && detail::text("throw std::system_error( e );") ); +#endif + } +}; + +#else // nsel_CONFIG_NO_EXCEPTIONS + +template< typename Error > +struct error_traits +{ + static void rethrow( Error const & e ) + { + throw bad_expected_access{ e }; + } +}; + +template<> +struct error_traits< std::exception_ptr > +{ + static void rethrow( std::exception_ptr const & e ) + { + std::rethrow_exception( e ); + } +}; + +template<> +struct error_traits< std::error_code > +{ + static void rethrow( std::error_code const & e ) + { + throw std::system_error( e ); + } +}; + +#endif // nsel_CONFIG_NO_EXCEPTIONS + +#if nsel_P2505R >= 3 +namespace detail { + +// from https://en.cppreference.com/w/cpp/utility/expected/unexpected: +// "the type of the unexpected value. The type must not be an array type, a non-object type, a specialization of std::unexpected, or a cv-qualified type." +template< typename T > +struct valid_unexpected_type : std::integral_constant< bool, + std::is_same< T, typename std20::remove_cvref< T >::type >::value + && std::is_object< T >::value + && !std::is_array< T >::value +> {}; + +template< typename T > +struct valid_unexpected_type< unexpected_type< T > > : std::false_type {}; + +} // namespace detail +#endif // nsel_P2505R >= 3 + +} // namespace expected_lite + +// provide nonstd::unexpected_type: + +using expected_lite::unexpected_type; + +namespace expected_lite { + +/// class expected + +#if nsel_P0323R <= 2 +template< typename T, typename E = std::exception_ptr > +class nsel_NODISCARD expected +#else +template< typename T, typename E > +class nsel_NODISCARD expected +#endif // nsel_P0323R +{ +private: + template< typename, typename > friend class expected; + +public: + using value_type = T; + using error_type = E; + using unexpected_type = nonstd::unexpected_type; + + template< typename U > + struct rebind + { + using type = expected; + }; + + // x.x.4.1 constructors + + nsel_REQUIRES_0( + std::is_default_constructible::value + ) + nsel_constexpr14 expected() + : contained( true ) + { + contained.construct_value(); + } + + nsel_constexpr14 expected( expected const & ) = default; + nsel_constexpr14 expected( expected && ) = default; + + template< typename U, typename G + nsel_REQUIRES_T( + std::is_constructible< T, U const &>::value + && std::is_constructible::value + && !std::is_constructible & >::value + && !std::is_constructible && >::value + && !std::is_constructible const & >::value + && !std::is_constructible const && >::value + && !std::is_convertible< expected & , T>::value + && !std::is_convertible< expected &&, T>::value + && !std::is_convertible< expected const & , T>::value + && !std::is_convertible< expected const &&, T>::value + && (!std::is_convertible::value || !std::is_convertible::value ) /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( expected const & other ) + : contained( other.has_value() ) + { + if ( has_value() ) contained.construct_value( T{ other.contained.value() } ); + else contained.construct_error( E{ other.contained.error() } ); + } + + template< typename U, typename G + nsel_REQUIRES_T( + std::is_constructible< T, U const &>::value + && std::is_constructible::value + && !std::is_constructible & >::value + && !std::is_constructible && >::value + && !std::is_constructible const & >::value + && !std::is_constructible const && >::value + && !std::is_convertible< expected & , T>::value + && !std::is_convertible< expected &&, T>::value + && !std::is_convertible< expected const &, T>::value + && !std::is_convertible< expected const &&, T>::value + && !(!std::is_convertible::value || !std::is_convertible::value ) /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( expected const & other ) + : contained( other.has_value() ) + { + if ( has_value() ) contained.construct_value( other.contained.value() ); + else contained.construct_error( other.contained.error() ); + } + + template< typename U, typename G + nsel_REQUIRES_T( + std::is_constructible< T, U>::value + && std::is_constructible::value + && !std::is_constructible & >::value + && !std::is_constructible && >::value + && !std::is_constructible const & >::value + && !std::is_constructible const && >::value + && !std::is_convertible< expected & , T>::value + && !std::is_convertible< expected &&, T>::value + && !std::is_convertible< expected const & , T>::value + && !std::is_convertible< expected const &&, T>::value + && (!std::is_convertible::value || !std::is_convertible::value ) /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( expected && other ) + : contained( other.has_value() ) + { + if ( has_value() ) contained.construct_value( T{ std::move( other.contained.value() ) } ); + else contained.construct_error( E{ std::move( other.contained.error() ) } ); + } + + template< typename U, typename G + nsel_REQUIRES_T( + std::is_constructible< T, U>::value + && std::is_constructible::value + && !std::is_constructible & >::value + && !std::is_constructible && >::value + && !std::is_constructible const & >::value + && !std::is_constructible const && >::value + && !std::is_convertible< expected & , T>::value + && !std::is_convertible< expected &&, T>::value + && !std::is_convertible< expected const & , T>::value + && !std::is_convertible< expected const &&, T>::value + && !(!std::is_convertible::value || !std::is_convertible::value ) /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( expected && other ) + : contained( other.has_value() ) + { + if ( has_value() ) contained.construct_value( std::move( other.contained.value() ) ); + else contained.construct_error( std::move( other.contained.error() ) ); + } + + template< typename U = T + nsel_REQUIRES_T( + std::is_copy_constructible::value + ) + > + nsel_constexpr14 expected( value_type const & value ) + : contained( true ) + { + contained.construct_value( value ); + } + + template< typename U = T + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_same::type, nonstd_lite_in_place_t(U)>::value + && !std::is_same< expected , typename std20::remove_cvref::type>::value + && !std::is_same, typename std20::remove_cvref::type>::value + && !std::is_convertible::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( U && value ) noexcept + ( + std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value + ) + : contained( true ) + { + contained.construct_value( T{ std::forward( value ) } ); + } + + template< typename U = T + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_same::type, nonstd_lite_in_place_t(U)>::value + && !std::is_same< expected , typename std20::remove_cvref::type>::value + && !std::is_same, typename std20::remove_cvref::type>::value + && std::is_convertible::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( U && value ) noexcept + ( + std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value + ) + : contained( true ) + { + contained.construct_value( std::forward( value ) ); + } + + // construct error: + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_convertible< G const &, E>::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( nonstd::unexpected_type const & error ) + : contained( false ) + { + contained.construct_error( E{ error.error() } ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value + && std::is_convertible< G const &, E>::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type const & error ) + : contained( false ) + { + contained.construct_error( error.error() ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_convertible< G&&, E>::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( nonstd::unexpected_type && error ) + : contained( false ) + { + contained.construct_error( E{ std::move( error.error() ) } ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value + && std::is_convertible< G&&, E>::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type && error ) + : contained( false ) + { + contained.construct_error( std::move( error.error() ) ); + } + + // in-place construction, value + + template< typename... Args + nsel_REQUIRES_T( + std::is_constructible::value + ) + > + nsel_constexpr14 explicit expected( nonstd_lite_in_place_t(T), Args&&... args ) + : contained( true ) + { + contained.emplace_value( std::forward( args )... ); + } + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_constructible, Args&&...>::value + ) + > + nsel_constexpr14 explicit expected( nonstd_lite_in_place_t(T), std::initializer_list il, Args&&... args ) + : contained( true ) + { + contained.emplace_value( il, std::forward( args )... ); + } + + // in-place construction, error + + template< typename... Args + nsel_REQUIRES_T( + std::is_constructible::value + ) + > + nsel_constexpr14 explicit expected( unexpect_t, Args&&... args ) + : contained( false ) + { + contained.emplace_error( std::forward( args )... ); + } + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_constructible, Args&&...>::value + ) + > + nsel_constexpr14 explicit expected( unexpect_t, std::initializer_list il, Args&&... args ) + : contained( false ) + { + contained.emplace_error( il, std::forward( args )... ); + } + + // x.x.4.2 destructor + + // TODO: ~expected: triviality + // Effects: If T is not cv void and is_trivially_destructible_v is false and bool(*this), calls val.~T(). If is_trivially_destructible_v is false and !bool(*this), calls unexpect.~unexpected(). + // Remarks: If either T is cv void or is_trivially_destructible_v is true, and is_trivially_destructible_v is true, then this destructor shall be a trivial destructor. + + ~expected() + { + if ( has_value() ) contained.destruct_value(); + else contained.destruct_error(); + } + + // x.x.4.3 assignment + + expected & operator=( expected const & other ) + { + expected( other ).swap( *this ); + return *this; + } + + expected & operator=( expected && other ) noexcept + ( + std::is_nothrow_move_constructible< T>::value + && std::is_nothrow_move_assignable< T>::value + && std::is_nothrow_move_constructible::value // added for missing + && std::is_nothrow_move_assignable< E>::value ) // nothrow above + { + expected( std::move( other ) ).swap( *this ); + return *this; + } + + template< typename U + nsel_REQUIRES_T( + !std::is_same, typename std20::remove_cvref::type>::value + && std17::conjunction, std::is_same> >::value + && std::is_constructible::value + && std::is_assignable< T&,U>::value + && std::is_nothrow_move_constructible::value ) + > + expected & operator=( U && value ) + { + expected( std::forward( value ) ).swap( *this ); + return *this; + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value && + std::is_copy_constructible::value // TODO: std::is_nothrow_copy_constructible + && std::is_copy_assignable::value + ) + > + expected & operator=( nonstd::unexpected_type const & error ) + { + expected( unexpect, error.error() ).swap( *this ); + return *this; + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value && + std::is_move_constructible::value // TODO: std::is_nothrow_move_constructible + && std::is_move_assignable::value + ) + > + expected & operator=( nonstd::unexpected_type && error ) + { + expected( unexpect, std::move( error.error() ) ).swap( *this ); + return *this; + } + + template< typename... Args + nsel_REQUIRES_T( + std::is_nothrow_constructible::value + ) + > + value_type & emplace( Args &&... args ) + { + expected( nonstd_lite_in_place(T), std::forward(args)... ).swap( *this ); + return value(); + } + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_nothrow_constructible&, Args&&...>::value + ) + > + value_type & emplace( std::initializer_list il, Args &&... args ) + { + expected( nonstd_lite_in_place(T), il, std::forward(args)... ).swap( *this ); + return value(); + } + + // x.x.4.4 swap + + template< typename U=T, typename G=E > + nsel_REQUIRES_R( void, + std17::is_swappable< U>::value + && std17::is_swappable::value + && ( std::is_move_constructible::value || std::is_move_constructible::value ) + ) + swap( expected & other ) noexcept + ( + std::is_nothrow_move_constructible::value && std17::is_nothrow_swappable::value && + std::is_nothrow_move_constructible::value && std17::is_nothrow_swappable::value + ) + { + using std::swap; + + if ( bool(*this) && bool(other) ) { swap( contained.value(), other.contained.value() ); } + else if ( ! bool(*this) && ! bool(other) ) { swap( contained.error(), other.contained.error() ); } + else if ( bool(*this) && ! bool(other) ) { error_type t( std::move( other.error() ) ); + other.contained.destruct_error(); + other.contained.construct_value( std::move( contained.value() ) ); + contained.destruct_value(); + contained.construct_error( std::move( t ) ); + bool has_value = contained.has_value(); + bool other_has_value = other.has_value(); + other.contained.set_has_value(has_value); + contained.set_has_value(other_has_value); + } + else if ( ! bool(*this) && bool(other) ) { other.swap( *this ); } + } + + // x.x.4.5 observers + + constexpr value_type const * operator ->() const + { + return assert( has_value() ), contained.value_ptr(); + } + + value_type * operator ->() + { + return assert( has_value() ), contained.value_ptr(); + } + + constexpr value_type const & operator *() const & + { + return assert( has_value() ), contained.value(); + } + + value_type & operator *() & + { + return assert( has_value() ), contained.value(); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + constexpr value_type const && operator *() const && + { + return std::move( ( assert( has_value() ), contained.value() ) ); + } + + nsel_constexpr14 value_type && operator *() && + { + return std::move( ( assert( has_value() ), contained.value() ) ); + } + +#endif + + constexpr explicit operator bool() const noexcept + { + return has_value(); + } + + constexpr bool has_value() const noexcept + { + return contained.has_value(); + } + + nsel_DISABLE_MSVC_WARNINGS( 4702 ) // warning C4702: unreachable code, see issue 65. + + constexpr value_type const & value() const & + { + return has_value() + ? ( contained.value() ) + : ( error_traits::rethrow( contained.error() ), contained.value() ); + } + + value_type & value() & + { + return has_value() + ? ( contained.value() ) + : ( error_traits::rethrow( contained.error() ), contained.value() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + constexpr value_type const && value() const && + { + return std::move( has_value() + ? ( contained.value() ) + : ( error_traits::rethrow( contained.error() ), contained.value() ) ); + } + + nsel_constexpr14 value_type && value() && + { + return std::move( has_value() + ? ( contained.value() ) + : ( error_traits::rethrow( contained.error() ), contained.value() ) ); + } + +#endif + nsel_RESTORE_MSVC_WARNINGS() + + constexpr error_type const & error() const & + { + return assert( ! has_value() ), contained.error(); + } + + error_type & error() & + { + return assert( ! has_value() ), contained.error(); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + constexpr error_type const && error() const && + { + return std::move( ( assert( ! has_value() ), contained.error() ) ); + } + + error_type && error() && + { + return std::move( ( assert( ! has_value() ), contained.error() ) ); + } + +#endif + + constexpr unexpected_type get_unexpected() const + { + return make_unexpected( contained.error() ); + } + + template< typename Ex > + bool has_exception() const + { + using ContainedEx = typename std::remove_reference< decltype( get_unexpected().error() ) >::type; + return ! has_value() && std::is_base_of< Ex, ContainedEx>::value; + } + + template< typename U + nsel_REQUIRES_T( + std::is_copy_constructible< T>::value + && std::is_convertible::value + ) + > + value_type value_or( U && v ) const & + { + return has_value() + ? contained.value() + : static_cast( std::forward( v ) ); + } + + template< typename U + nsel_REQUIRES_T( + std::is_move_constructible< T>::value + && std::is_convertible::value + ) + > + value_type value_or( U && v ) && + { + return has_value() + ? std::move( contained.value() ) + : static_cast( std::forward( v ) ); + } + +#if nsel_P2505R >= 4 + template< typename G = E + nsel_REQUIRES_T( + std::is_copy_constructible< E >::value + && std::is_convertible< G, E >::value + ) + > + nsel_constexpr error_type error_or( G && e ) const & + { + return has_value() + ? static_cast< E >( std::forward< G >( e ) ) + : contained.error(); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_move_constructible< E >::value + && std::is_convertible< G, E >::value + ) + > + nsel_constexpr14 error_type error_or( G && e ) && + { + return has_value() + ? static_cast< E >( std::forward< G >( e ) ) + : std::move( contained.error() ); + } +#endif // nsel_P2505R >= 4 + +#if nsel_P2505R >= 3 + // Monadic operations (P2505) + template< typename F + nsel_REQUIRES_T( + detail::is_expected < detail::invoke_result_nocvref_t< F, value_type & > > ::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, value_type & >::error_type, error_type >::value + && std::is_constructible< error_type, error_type & >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, value_type & > and_then( F && f ) & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, value_type & >( detail::invoke( std::forward< F >( f ), value() ) ) + : detail::invoke_result_nocvref_t< F, value_type & >( unexpect, error() ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, const value_type & >::error_type, error_type >::value + && std::is_constructible< error_type, const error_type & >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const value_type & > and_then( F && f ) const & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const value_type & >( detail::invoke( std::forward< F >( f ), value() ) ) + : detail::invoke_result_nocvref_t< F, const value_type & >( unexpect, error() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, value_type && >::error_type, error_type >::value + && std::is_constructible< error_type, error_type && >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, value_type && > and_then( F && f ) && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, value_type && >( detail::invoke( std::forward< F >( f ), std::move( value() ) ) ) + : detail::invoke_result_nocvref_t< F, value_type && >( unexpect, std::move( error() ) ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, const value_type & >::error_type, error_type >::value + && std::is_constructible< error_type, const error_type && >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const value_type && > and_then( F && f ) const && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const value_type && >( detail::invoke( std::forward< F >( f ), std::move( value() ) ) ) + : detail::invoke_result_nocvref_t< F, const value_type && >( unexpect, std::move( error() ) ); + } +#endif + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, error_type & >::value_type, value_type >::value + && std::is_constructible< value_type, value_type & >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, error_type & > or_else( F && f ) & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, error_type & >( value() ) + : detail::invoke_result_nocvref_t< F, error_type & >( detail::invoke( std::forward< F >( f ), error() ) ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, const error_type & >::value_type, value_type >::value + && std::is_constructible< value_type, const value_type & >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const error_type & > or_else( F && f ) const & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const error_type & >( value() ) + : detail::invoke_result_nocvref_t< F, const error_type & >( detail::invoke( std::forward< F >( f ), error() ) ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, error_type && >::value_type, value_type >::value + && std::is_constructible< value_type, value_type && >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, error_type && > or_else( F && f ) && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, error_type && >( std::move( value() ) ) + : detail::invoke_result_nocvref_t< F, error_type && >( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, const error_type && >::value_type, value_type >::value + && std::is_constructible< value_type, const value_type && >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const error_type && > or_else( F && f ) const && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const error_type && >( std::move( value() ) ) + : detail::invoke_result_nocvref_t< F, const error_type && >( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } +#endif + + template::value + && !std::is_void< detail::transform_invoke_result_t< F, value_type & > >::value + && detail::valid_expected_value_type< detail::transform_invoke_result_t< F, value_type & > >::value + ) + > + nsel_constexpr14 expected< detail::transform_invoke_result_t< F, value_type & >, error_type > transform( F && f ) & + { + return has_value() + ? expected< detail::transform_invoke_result_t< F, value_type & >, error_type >( detail::invoke( std::forward< F >( f ), **this ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F, value_type & > >::value + ) + > + nsel_constexpr14 expected< void, error_type > transform( F && f ) & + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ), **this ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + + template::value + && !std::is_void< detail::transform_invoke_result_t< F, const value_type & > >::value + && detail::valid_expected_value_type< detail::transform_invoke_result_t< F, const value_type & > >::value + ) + > + nsel_constexpr expected< detail::transform_invoke_result_t< F, const value_type & >, error_type > transform( F && f ) const & + { + return has_value() + ? expected< detail::transform_invoke_result_t< F, const value_type & >, error_type >( detail::invoke( std::forward< F >( f ), **this ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F, const value_type & > >::value + ) + > + nsel_constexpr expected< void, error_type > transform( F && f ) const & + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ), **this ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template::value + && !std::is_void< detail::transform_invoke_result_t< F, value_type && > >::value + && detail::valid_expected_value_type< detail::transform_invoke_result_t< F, value_type && > >::value + ) + > + nsel_constexpr14 expected< detail::transform_invoke_result_t< F, value_type && >, error_type > transform( F && f ) && + { + return has_value() + ? expected< detail::transform_invoke_result_t< F, value_type && >, error_type >( detail::invoke( std::forward< F >( f ), std::move( **this ) ) ) + : make_unexpected( std::move( error() ) ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F, value_type && > >::value + ) + > + nsel_constexpr14 expected< void, error_type > transform( F && f ) && + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ), **this ), expected< void, error_type >() ) + : make_unexpected( std::move( error() ) ); + } + + template::value + && !std::is_void< detail::transform_invoke_result_t< F, const value_type && > >::value + && detail::valid_expected_value_type< detail::transform_invoke_result_t< F, const value_type && > >::value + ) + > + nsel_constexpr expected< detail::transform_invoke_result_t< F, const value_type && >, error_type > transform( F && f ) const && + { + return has_value() + ? expected< detail::transform_invoke_result_t< F, const value_type && >, error_type >( detail::invoke( std::forward< F >( f ), std::move( **this ) ) ) + : make_unexpected( std::move( error() ) ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F, const value_type && > >::value + ) + > + nsel_constexpr expected< void, error_type > transform( F && f ) const && + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ), **this ), expected< void, error_type >() ) + : make_unexpected( std::move( error() ) ); + } +#endif + + template >::value + && std::is_constructible< value_type, value_type & >::value + ) + > + nsel_constexpr14 expected< value_type, detail::transform_invoke_result_t< F, error_type & > > transform_error( F && f ) & + { + return has_value() + ? expected< value_type, detail::transform_invoke_result_t< F, error_type & > >( in_place, **this ) + : make_unexpected( detail::invoke( std::forward< F >( f ), error() ) ); + } + + template >::value + && std::is_constructible< value_type, const value_type & >::value + ) + > + nsel_constexpr expected< value_type, detail::transform_invoke_result_t< F, const error_type & > > transform_error( F && f ) const & + { + return has_value() + ? expected< value_type, detail::transform_invoke_result_t< F, const error_type & > >( in_place, **this ) + : make_unexpected( detail::invoke( std::forward< F >( f ), error() ) ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_constructible< value_type, value_type && >::value + ) + > + nsel_constexpr14 expected< value_type, detail::transform_invoke_result_t< F, error_type && > > transform_error( F && f ) && + { + return has_value() + ? expected< value_type, detail::transform_invoke_result_t< F, error_type && > >( in_place, std::move( **this ) ) + : make_unexpected( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } + + template >::value + && std::is_constructible< value_type, const value_type && >::value + ) + > + nsel_constexpr expected< value_type, detail::transform_invoke_result_t< F, const error_type && > > transform_error( F && f ) const && + { + return has_value() + ? expected< value_type, detail::transform_invoke_result_t< F, const error_type && > >( in_place, std::move( **this ) ) + : make_unexpected( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } +#endif +#endif // nsel_P2505R >= 3 + // unwrap() + +// template +// constexpr expected expected,E>::unwrap() const&; + +// template +// constexpr expected expected::unwrap() const&; + +// template +// expected expected, E>::unwrap() &&; + +// template +// template expected expected::unwrap() &&; + + // factories + +// template< typename Ex, typename F> +// expected catch_exception(F&& f); + +// template< typename F> +// expected())),E> map(F&& func) ; + +// template< typename F> +// 'see below' bind(F&& func); + +// template< typename F> +// expected catch_error(F&& f); + +// template< typename F> +// 'see below' then(F&& func); + +private: + detail::storage_t + < + T + ,E + , std::is_copy_constructible::value && std::is_copy_constructible::value + , std::is_move_constructible::value && std::is_move_constructible::value + > + contained; +}; + +/// class expected, void specialization + +template< typename E > +class nsel_NODISCARD expected< void, E > +{ +private: + template< typename, typename > friend class expected; + +public: + using value_type = void; + using error_type = E; + using unexpected_type = nonstd::unexpected_type; + + // x.x.4.1 constructors + + constexpr expected() noexcept + : contained( true ) + {} + + nsel_constexpr14 expected( expected const & other ) = default; + nsel_constexpr14 expected( expected && other ) = default; + + constexpr explicit expected( nonstd_lite_in_place_t(void) ) + : contained( true ) + {} + + template< typename G = E + nsel_REQUIRES_T( + !std::is_convertible::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( nonstd::unexpected_type const & error ) + : contained( false ) + { + contained.construct_error( E{ error.error() } ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_convertible::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type const & error ) + : contained( false ) + { + contained.construct_error( error.error() ); + } + + template< typename G = E + nsel_REQUIRES_T( + !std::is_convertible::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( nonstd::unexpected_type && error ) + : contained( false ) + { + contained.construct_error( E{ std::move( error.error() ) } ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_convertible::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type && error ) + : contained( false ) + { + contained.construct_error( std::move( error.error() ) ); + } + + template< typename... Args + nsel_REQUIRES_T( + std::is_constructible::value + ) + > + nsel_constexpr14 explicit expected( unexpect_t, Args&&... args ) + : contained( false ) + { + contained.emplace_error( std::forward( args )... ); + } + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_constructible, Args&&...>::value + ) + > + nsel_constexpr14 explicit expected( unexpect_t, std::initializer_list il, Args&&... args ) + : contained( false ) + { + contained.emplace_error( il, std::forward( args )... ); + } + + // destructor + + ~expected() + { + if ( ! has_value() ) + { + contained.destruct_error(); + } + } + + // x.x.4.3 assignment + + expected & operator=( expected const & other ) + { + expected( other ).swap( *this ); + return *this; + } + + expected & operator=( expected && other ) noexcept + ( + std::is_nothrow_move_assignable::value && + std::is_nothrow_move_constructible::value ) + { + expected( std::move( other ) ).swap( *this ); + return *this; + } + + void emplace() + { + expected().swap( *this ); + } + + // x.x.4.4 swap + + template< typename G = E > + nsel_REQUIRES_R( void, + std17::is_swappable::value + && std::is_move_constructible::value + ) + swap( expected & other ) noexcept + ( + std::is_nothrow_move_constructible::value && std17::is_nothrow_swappable::value + ) + { + using std::swap; + + if ( ! bool(*this) && ! bool(other) ) { swap( contained.error(), other.contained.error() ); } + else if ( bool(*this) && ! bool(other) ) { contained.construct_error( std::move( other.error() ) ); + bool has_value = contained.has_value(); + bool other_has_value = other.has_value(); + other.contained.set_has_value(has_value); + contained.set_has_value(other_has_value); + } + else if ( ! bool(*this) && bool(other) ) { other.swap( *this ); } + } + + // x.x.4.5 observers + + constexpr explicit operator bool() const noexcept + { + return has_value(); + } + + constexpr bool has_value() const noexcept + { + return contained.has_value(); + } + + void value() const + { + if ( ! has_value() ) + { + error_traits::rethrow( contained.error() ); + } + } + + constexpr error_type const & error() const & + { + return assert( ! has_value() ), contained.error(); + } + + error_type & error() & + { + return assert( ! has_value() ), contained.error(); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + constexpr error_type const && error() const && + { + return std::move( ( assert( ! has_value() ), contained.error() ) ); + } + + error_type && error() && + { + return std::move( ( assert( ! has_value() ), contained.error() ) ); + } + +#endif + + constexpr unexpected_type get_unexpected() const + { + return make_unexpected( contained.error() ); + } + + template< typename Ex > + bool has_exception() const + { + using ContainedEx = typename std::remove_reference< decltype( get_unexpected().error() ) >::type; + return ! has_value() && std::is_base_of< Ex, ContainedEx>::value; + } + +#if nsel_P2505R >= 4 + template< typename G = E + nsel_REQUIRES_T( + std::is_copy_constructible< E >::value + && std::is_convertible< G, E >::value + ) + > + nsel_constexpr error_type error_or( G && e ) const & + { + return has_value() + ? static_cast< E >( std::forward< G >( e ) ) + : contained.error(); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_move_constructible< E >::value + && std::is_convertible< G, E >::value + ) + > + nsel_constexpr14 error_type error_or( G && e ) && + { + return has_value() + ? static_cast< E >( std::forward< G >( e ) ) + : std::move( contained.error() ); + } +#endif // nsel_P2505R >= 4 + +#if nsel_P2505R >= 3 + // Monadic operations (P2505) + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F >::error_type, error_type >::value + && std::is_constructible< error_type, error_type & >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F > and_then( F && f ) & + { + return has_value() + ? detail::invoke_result_nocvref_t< F >( detail::invoke( std::forward< F >( f ) ) ) + : detail::invoke_result_nocvref_t< F >( unexpect, error() ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F >::error_type, error_type >::value + && std::is_constructible< error_type, const error_type & >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F > and_then( F && f ) const & + { + return has_value() + ? detail::invoke_result_nocvref_t< F >( detail::invoke( std::forward< F >( f ) ) ) + : detail::invoke_result_nocvref_t< F >( unexpect, error() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F >::error_type, error_type >::value + && std::is_constructible< error_type, error_type && >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F > and_then( F && f ) && + { + return has_value() + ? detail::invoke_result_nocvref_t< F >( detail::invoke( std::forward< F >( f ) ) ) + : detail::invoke_result_nocvref_t< F >( unexpect, std::move( error() ) ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F >::error_type, error_type >::value + && std::is_constructible< error_type, const error_type && >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F > and_then( F && f ) const && + { + return has_value() + ? detail::invoke_result_nocvref_t< F >( detail::invoke( std::forward< F >( f ) ) ) + : detail::invoke_result_nocvref_t< F >( unexpect, std::move( error() ) ); + } +#endif + + template >::value + && std::is_void< typename detail::invoke_result_nocvref_t< F, error_type & >::value_type >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, error_type & > or_else( F && f ) & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, error_type & >() + : detail::invoke_result_nocvref_t< F, error_type & >( detail::invoke( std::forward< F >( f ), error() ) ); + } + + template >::value + && std::is_void< typename detail::invoke_result_nocvref_t< F, const error_type & >::value_type >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const error_type & > or_else( F && f ) const & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const error_type & >() + : detail::invoke_result_nocvref_t< F, const error_type & >( detail::invoke( std::forward< F >( f ), error() ) ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_void< typename detail::invoke_result_nocvref_t< F, error_type && >::value_type >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, error_type && > or_else( F && f ) && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, error_type && >() + : detail::invoke_result_nocvref_t< F, error_type && >( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } + + template >::value + && std::is_void< typename detail::invoke_result_nocvref_t< F, const error_type && >::value_type >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const error_type && > or_else( F && f ) const && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const error_type && >() + : detail::invoke_result_nocvref_t< F, const error_type && >( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } +#endif + + template::value + && !std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr14 expected< detail::transform_invoke_result_t< F >, error_type > transform( F && f ) & + { + return has_value() + ? expected< detail::transform_invoke_result_t< F >, error_type >( detail::invoke( std::forward< F >( f ) ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr14 expected< void, error_type > transform( F && f ) & + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ) ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + + template::value + && !std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr expected< detail::transform_invoke_result_t< F >, error_type > transform( F && f ) const & + { + return has_value() + ? expected< detail::transform_invoke_result_t< F >, error_type >( detail::invoke( std::forward< F >( f ) ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr expected< void, error_type > transform( F && f ) const & + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ) ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template::value + && !std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr14 expected< detail::transform_invoke_result_t< F >, error_type > transform( F && f ) && + { + return has_value() + ? expected< detail::transform_invoke_result_t< F >, error_type >( detail::invoke( std::forward< F >( f ) ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr14 expected< void, error_type > transform( F && f ) && + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ) ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + + template::value + && !std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr expected< detail::transform_invoke_result_t< F >, error_type > transform( F && f ) const && + { + return has_value() + ? expected< detail::transform_invoke_result_t< F >, error_type >( detail::invoke( std::forward< F >( f ) ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr expected< void, error_type > transform( F && f ) const && + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ) ), expected< void, error_type >() ) + : make_unexpected( error() ); + } +#endif + + template >::value + ) + > + nsel_constexpr14 expected< void, detail::transform_invoke_result_t< F, error_type & > > transform_error( F && f ) & + { + return has_value() + ? expected< void, detail::transform_invoke_result_t< F, error_type & > >() + : make_unexpected( detail::invoke( std::forward< F >( f ), error() ) ); + } + + template >::value + ) + > + nsel_constexpr expected< void, detail::transform_invoke_result_t< F, const error_type & > > transform_error( F && f ) const & + { + return has_value() + ? expected< void, detail::transform_invoke_result_t< F, const error_type & > >() + : make_unexpected( detail::invoke( std::forward< F >( f ), error() ) ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + ) + > + nsel_constexpr14 expected< void, detail::transform_invoke_result_t< F, error_type && > > transform_error( F && f ) && + { + return has_value() + ? expected< void, detail::transform_invoke_result_t< F, error_type && > >() + : make_unexpected( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } + + template >::value + ) + > + nsel_constexpr expected< void, detail::transform_invoke_result_t< F, const error_type && > > transform_error( F && f ) const && + { + return has_value() + ? expected< void, detail::transform_invoke_result_t< F, const error_type && > >() + : make_unexpected( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } +#endif +#endif // nsel_P2505R >= 3 + +// template constexpr 'see below' unwrap() const&; +// +// template 'see below' unwrap() &&; + + // factories + +// template< typename Ex, typename F> +// expected catch_exception(F&& f); +// +// template< typename F> +// expected map(F&& func) ; +// +// template< typename F> +// 'see below' bind(F&& func) ; +// +// template< typename F> +// expected catch_error(F&& f); +// +// template< typename F> +// 'see below' then(F&& func); + +private: + detail::storage_t + < + void + , E + , std::is_copy_constructible::value + , std::is_move_constructible::value + > + contained; +}; + +// x.x.4.6 expected<>: comparison operators + +template< typename T1, typename E1, typename T2, typename E2 + nsel_REQUIRES_T( + !std::is_void::value && !std::is_void::value + ) +> +constexpr bool operator==( expected const & x, expected const & y ) +{ + return bool(x) != bool(y) ? false : bool(x) ? *x == *y : x.error() == y.error(); +} + +template< typename T1, typename E1, typename T2, typename E2 + nsel_REQUIRES_T( + std::is_void::value && std::is_void::value + ) +> +constexpr bool operator==( expected const & x, expected const & y ) +{ + return bool(x) != bool(y) ? false : bool(x) || static_cast( x.error() == y.error() ); +} + +template< typename T1, typename E1, typename T2, typename E2 > +constexpr bool operator!=( expected const & x, expected const & y ) +{ + return !(x == y); +} + +#if nsel_P0323R <= 2 + +template< typename T, typename E > +constexpr bool operator<( expected const & x, expected const & y ) +{ + return (!y) ? false : (!x) ? true : *x < *y; +} + +template< typename T, typename E > +constexpr bool operator>( expected const & x, expected const & y ) +{ + return (y < x); +} + +template< typename T, typename E > +constexpr bool operator<=( expected const & x, expected const & y ) +{ + return !(y < x); +} + +template< typename T, typename E > +constexpr bool operator>=( expected const & x, expected const & y ) +{ + return !(x < y); +} + +#endif + +// x.x.4.7 expected: comparison with T + +template< typename T1, typename E1, typename T2 + nsel_REQUIRES_T( + !std::is_void::value + ) +> +constexpr bool operator==( expected const & x, T2 const & v ) +{ + return bool(x) ? *x == v : false; +} + +template< typename T1, typename E1, typename T2 + nsel_REQUIRES_T( + !std::is_void::value + ) +> +constexpr bool operator==(T2 const & v, expected const & x ) +{ + return bool(x) ? v == *x : false; +} + +template< typename T1, typename E1, typename T2 > +constexpr bool operator!=( expected const & x, T2 const & v ) +{ + return bool(x) ? *x != v : true; +} + +template< typename T1, typename E1, typename T2 > +constexpr bool operator!=( T2 const & v, expected const & x ) +{ + return bool(x) ? v != *x : true; +} + +#if nsel_P0323R <= 2 + +template< typename T, typename E > +constexpr bool operator<( expected const & x, T const & v ) +{ + return bool(x) ? *x < v : true; +} + +template< typename T, typename E > +constexpr bool operator<( T const & v, expected const & x ) +{ + return bool(x) ? v < *x : false; +} + +template< typename T, typename E > +constexpr bool operator>( T const & v, expected const & x ) +{ + return bool(x) ? *x < v : false; +} + +template< typename T, typename E > +constexpr bool operator>( expected const & x, T const & v ) +{ + return bool(x) ? v < *x : false; +} + +template< typename T, typename E > +constexpr bool operator<=( T const & v, expected const & x ) +{ + return bool(x) ? ! ( *x < v ) : false; +} + +template< typename T, typename E > +constexpr bool operator<=( expected const & x, T const & v ) +{ + return bool(x) ? ! ( v < *x ) : true; +} + +template< typename T, typename E > +constexpr bool operator>=( expected const & x, T const & v ) +{ + return bool(x) ? ! ( *x < v ) : false; +} + +template< typename T, typename E > +constexpr bool operator>=( T const & v, expected const & x ) +{ + return bool(x) ? ! ( v < *x ) : true; +} + +#endif // nsel_P0323R + +// x.x.4.8 expected: comparison with unexpected_type + +template< typename T1, typename E1 , typename E2 > +constexpr bool operator==( expected const & x, unexpected_type const & u ) +{ + return (!x) ? x.get_unexpected() == u : false; +} + +template< typename T1, typename E1 , typename E2 > +constexpr bool operator==( unexpected_type const & u, expected const & x ) +{ + return ( x == u ); +} + +template< typename T1, typename E1 , typename E2 > +constexpr bool operator!=( expected const & x, unexpected_type const & u ) +{ + return ! ( x == u ); +} + +template< typename T1, typename E1 , typename E2 > +constexpr bool operator!=( unexpected_type const & u, expected const & x ) +{ + return ! ( x == u ); +} + +#if nsel_P0323R <= 2 + +template< typename T, typename E > +constexpr bool operator<( expected const & x, unexpected_type const & u ) +{ + return (!x) ? ( x.get_unexpected() < u ) : false; +} + +template< typename T, typename E > +constexpr bool operator<( unexpected_type const & u, expected const & x ) +{ + return (!x) ? ( u < x.get_unexpected() ) : true ; +} + +template< typename T, typename E > +constexpr bool operator>( expected const & x, unexpected_type const & u ) +{ + return ( u < x ); +} + +template< typename T, typename E > +constexpr bool operator>( unexpected_type const & u, expected const & x ) +{ + return ( x < u ); +} + +template< typename T, typename E > +constexpr bool operator<=( expected const & x, unexpected_type const & u ) +{ + return ! ( u < x ); +} + +template< typename T, typename E > +constexpr bool operator<=( unexpected_type const & u, expected const & x) +{ + return ! ( x < u ); +} + +template< typename T, typename E > +constexpr bool operator>=( expected const & x, unexpected_type const & u ) +{ + return ! ( u > x ); +} + +template< typename T, typename E > +constexpr bool operator>=( unexpected_type const & u, expected const & x ) +{ + return ! ( x > u ); +} + +#endif // nsel_P0323R + +/// x.x.x Specialized algorithms + +template< typename T, typename E + nsel_REQUIRES_T( + ( std::is_void::value || std::is_move_constructible::value ) + && std::is_move_constructible::value + && std17::is_swappable::value + && std17::is_swappable::value ) +> +void swap( expected & x, expected & y ) noexcept ( noexcept ( x.swap(y) ) ) +{ + x.swap( y ); +} + +#if nsel_P0323R <= 3 + +template< typename T > +constexpr auto make_expected( T && v ) -> expected< typename std::decay::type > +{ + return expected< typename std::decay::type >( std::forward( v ) ); +} + +// expected specialization: + +auto inline make_expected() -> expected +{ + return expected( in_place ); +} + +template< typename T > +constexpr auto make_expected_from_current_exception() -> expected +{ + return expected( make_unexpected_from_current_exception() ); +} + +template< typename T > +auto make_expected_from_exception( std::exception_ptr v ) -> expected +{ + return expected( unexpected_type( std::forward( v ) ) ); +} + +template< typename T, typename E > +constexpr auto make_expected_from_error( E e ) -> expected::type> +{ + return expected::type>( make_unexpected( e ) ); +} + +template< typename F + nsel_REQUIRES_T( ! std::is_same::type, void>::value ) +> +/*nsel_constexpr14*/ +auto make_expected_from_call( F f ) -> expected< typename std::result_of::type > +{ + try + { + return make_expected( f() ); + } + catch (...) + { + return make_unexpected_from_current_exception(); + } +} + +template< typename F + nsel_REQUIRES_T( std::is_same::type, void>::value ) +> +/*nsel_constexpr14*/ +auto make_expected_from_call( F f ) -> expected +{ + try + { + f(); + return make_expected(); + } + catch (...) + { + return make_unexpected_from_current_exception(); + } +} + +#endif // nsel_P0323R + +} // namespace expected_lite + +using namespace expected_lite; + +// using expected_lite::expected; +// using ... + +} // namespace nonstd + +namespace std { + +// expected: hash support + +template< typename T, typename E > +struct hash< nonstd::expected > +{ + using result_type = std::size_t; + using argument_type = nonstd::expected; + + constexpr result_type operator()(argument_type const & arg) const + { + return arg ? std::hash{}(*arg) : result_type{}; + } +}; + +// TBD - ?? remove? see spec. +template< typename T, typename E > +struct hash< nonstd::expected > +{ + using result_type = std::size_t; + using argument_type = nonstd::expected; + + constexpr result_type operator()(argument_type const & arg) const + { + return arg ? std::hash{}(*arg) : result_type{}; + } +}; + +// TBD - implement +// bool(e), hash>()(e) shall evaluate to the hashing true; +// otherwise it evaluates to an unspecified value if E is exception_ptr or +// a combination of hashing false and hash()(e.error()). + +template< typename E > +struct hash< nonstd::expected > +{ +}; + +} // namespace std + +namespace nonstd { + +// void unexpected() is deprecated && removed in C++17 + +#if nsel_CPP17_OR_GREATER || nsel_COMPILER_MSVC_VERSION > 141 +template< typename E > +using unexpected = unexpected_type; +#endif + +} // namespace nonstd + +#undef nsel_REQUIRES +#undef nsel_REQUIRES_0 +#undef nsel_REQUIRES_T + +nsel_RESTORE_WARNINGS() + +#endif // nsel_USES_STD_EXPECTED + +#endif // NONSTD_EXPECTED_LITE_HPP diff --git a/include/utilities/bmi/nonstd/expected.tweak.hpp b/include/utilities/bmi/nonstd/expected.tweak.hpp new file mode 100644 index 0000000000..3a7be06a76 --- /dev/null +++ b/include/utilities/bmi/nonstd/expected.tweak.hpp @@ -0,0 +1,4 @@ +//tweaks for the expected library configuration/build +// see https://github.com/martinmoene/expected-lite/tree/master?tab=readme-ov-file#configuration +// for documentation on configuration +#define nsel_CONFIG_ WIN32_LEAN_AND_MEAN 0 From f01aea20a38156cf9aec5651a60fc97d2e2b343b Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Sat, 27 Sep 2025 00:05:22 -0600 Subject: [PATCH 11/58] feat(bmi-protocols)!: v0.2 of the protocols lib using expected semantics for error handling --- include/utilities/bmi/mass_balance.hpp | 101 ++++------------ include/utilities/bmi/protocol.hpp | 152 ++++++++++++++++++++++--- include/utilities/bmi/protocols.hpp | 34 +++++- src/utilities/bmi/mass_balance.cpp | 75 +++++++++--- src/utilities/bmi/protocols.cpp | 42 +++++-- 5 files changed, 284 insertions(+), 120 deletions(-) diff --git a/include/utilities/bmi/mass_balance.hpp b/include/utilities/bmi/mass_balance.hpp index 562dd7c18e..d5b9a6c22e 100644 --- a/include/utilities/bmi/mass_balance.hpp +++ b/include/utilities/bmi/mass_balance.hpp @@ -15,17 +15,23 @@ See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.2 +Conform to updated protocol interface +Removed integration and error exceptions in favor of ProtocolError + Version 0.1 Interface of the BMI mass balance protocol */ #pragma once + #include #include -#include "protocol.hpp" +#include +#include namespace models{ namespace bmi{ namespace protocols{ - + using nonstd::expected; /** Mass balance variable names **/ constexpr const char* const INPUT_MASS_NAME = "ngen::mass_in"; constexpr const char* const OUTPUT_MASS_NAME = "ngen::mass_out"; @@ -40,7 +46,7 @@ namespace models{ namespace bmi{ namespace protocols{ constexpr const char* const FATAL_KEY = "fatal"; constexpr const char* const CHECK_KEY = "check"; constexpr const char* const FREQUENCY_KEY = "frequency"; - + class NgenMassBalance : public NgenBmiProtocol { /** @brief Mass Balance protocol * @@ -58,9 +64,18 @@ namespace models{ namespace bmi{ namespace protocols{ * @param model A shared pointer to a Bmi_Adapter object which should be * initialized before being passed to this constructor. */ - NgenMassBalance(std::shared_ptr model=nullptr) : - NgenBmiProtocol(model), check(false), is_fatal(false), - tolerance(1.0E-16), frequency(1){} + NgenMassBalance(const ModelPtr& model, const Properties& properties); + + /** + * @brief Construct a new, default Ngen Mass Balance object + * + * By default, the protocol is considered unsupported and won't be checked + */ + NgenMassBalance(); + + virtual ~NgenMassBalance() override; + + private: /** * @brief Run the mass balance protocol @@ -76,17 +91,14 @@ namespace models{ namespace bmi{ namespace protocols{ * throws: MassBalanceError if the mass balance is not within the configured * acceptable tolerance and the protocol is configured to be fatal. */ - void run(const Context& ctx) const override; - - virtual ~NgenMassBalance() override {}; + auto run(const ModelPtr& model, const Context& ctx) const -> expected override; - private: /** * @brief Check if the mass balance protocol is supported by the model * * throws: MassBalanceIntegrationError if the mass balance protocol is not supported */ - void check_support() override; + [[nodiscard]] auto check_support(const ModelPtr& model) -> expected override; /** * @brief Check the model for support and initialize the mass balance protocol from the given properties. @@ -107,78 +119,15 @@ namespace models{ namespace bmi{ namespace protocols{ * fatal: bool, default false. Whether to treat mass balance errors as fatal. * Otherwise, mass balance checking will be disabled (check will be false) */ - void initialize(const geojson::PropertyMap& properties) override; + auto initialize(const ModelPtr& model, const Properties& properties) -> expected override; + private: // Configurable options/values bool check; bool is_fatal; double tolerance; // How often (in time steps) to check mass balance int frequency; - - /** - * @brief Friend class for checking/managing support and initialization - * - * This allows the NgenBmiProtocols container class to access private members, - * particularly the check_support() and initialize() methods. - * - */ - friend class NgenBmiProtocols; - - }; - - class MassBalanceIntegration : public std::exception { - /** - * @brief Exception thrown when there is an error in mass balance integration - * - * This indicates that the BMI model isn't capable of supporting the mass balance protocol. - */ - - public: - - MassBalanceIntegration(char const *const message) noexcept : MassBalanceIntegration(std::string(message)) {} - - MassBalanceIntegration(std::string message) noexcept : std::exception(), what_message(std::move(message)) {} - - MassBalanceIntegration(MassBalanceIntegration &exception) noexcept : MassBalanceIntegration(exception.what_message) {} - - MassBalanceIntegration(MassBalanceIntegration &&exception) noexcept - : MassBalanceIntegration(std::move(exception.what_message)) {} - - char const *what() const noexcept override { - return what_message.c_str(); - } - - private: - - std::string what_message; - }; - - class MassBalanceError : public std::exception { - /** - * @brief Exception thrown when a mass balance error occurs - * - * This indicates that a mass balance error has occurred within the model. - */ - - public: - - MassBalanceError(char const *const message) noexcept : MassBalanceError(std::string(message)) {} - - MassBalanceError(std::string message) noexcept : std::exception(), what_message(std::move(message)) {} - - MassBalanceError(MassBalanceError &exception) noexcept : MassBalanceError(exception.what_message) {} - - MassBalanceError(MassBalanceError &&exception) noexcept - : MassBalanceError(std::move(exception.what_message)) {} - - char const *what() const noexcept override { - return what_message.c_str(); - } - - private: - - std::string what_message; }; }}} diff --git a/include/utilities/bmi/protocol.hpp b/include/utilities/bmi/protocol.hpp index cdbd509edf..5e8caf22ca 100644 --- a/include/utilities/bmi/protocol.hpp +++ b/include/utilities/bmi/protocol.hpp @@ -15,6 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.2 +Enumerate protocol error types and add ProtocolError exception class +Implement error handling via expected and error_or_warning +Removed model member and required model reference in run(), check_support(), and initialize() +Minor refactoring and style changes + Version 0.1 Virtual interface for BMI protocols */ @@ -23,9 +29,53 @@ Virtual interface for BMI protocols #include "Bmi_Adapter.hpp" #include "JSONProperty.hpp" - +#include namespace models{ namespace bmi{ namespace protocols{ +using nonstd::expected; +using nonstd::make_unexpected; + +enum class Error{ + UNITIALIZED_MODEL, + UNSUPPORTED_PROTOCOL, + INTEGRATION_ERROR, + PROTOCOL_ERROR, + PROTOCOL_WARNING +}; + +class ProtocolError: public std::exception { + public: + ProtocolError () = delete; + ProtocolError(Error err, const std::string& message="") : err(std::move(err)), message(std::move(message)) {} + ProtocolError(const ProtocolError& other) = default; + ProtocolError(ProtocolError&& other) noexcept = default; + ProtocolError& operator=(const ProtocolError& other) = default; + ProtocolError& operator=(ProtocolError&& other) noexcept = default; + ~ProtocolError() = default; + + auto to_string() const -> std::string { + switch (err) { + case Error::UNITIALIZED_MODEL: return "Error(Uninitialized Model)::" + message; + case Error::UNSUPPORTED_PROTOCOL: return "Warning(Unsupported Protocol)::" + message; + case Error::INTEGRATION_ERROR: return "Error(Integration)::" + message; + case Error::PROTOCOL_ERROR: return "Error(Protocol)::" + message; + case Error::PROTOCOL_WARNING: return "Warning(Protocol)::" + message; + default: return "Unknown Error: " + message; + } + } + + auto error_code() const -> const Error& { return err; } + auto get_message() const -> const std::string& { return message; } + + char const *what() const noexcept override { + message = to_string(); + return message.c_str(); + } + + private: + Error err; + mutable std::string message; +}; struct Context{ const int current_time_step; @@ -34,6 +84,9 @@ struct Context{ const std::string& id; }; +using ModelPtr = std::shared_ptr; +using Properties = geojson::PropertyMap; + class NgenBmiProtocol{ /** * @brief Abstract interface for a generic BMI protocol @@ -41,48 +94,115 @@ class NgenBmiProtocol{ */ public: + /** - * @brief Run the BMI protocol + * @brief Construct a new Ngen Bmi Protocol object + * + * By default, the protocol is considered unsupported. + * Subclasses are responsible for implementing the check_support() method, + * and ensuring that is_supported is properly set based on the protocol's + * requirements. * */ - virtual void run(const Context& ctx) const = 0; + NgenBmiProtocol() : is_supported(false) {} virtual ~NgenBmiProtocol() = default; - private: + protected: /** - * @brief Check if the BMI protocol is supported by the model + * @brief Handle a ProtocolError by either throwing it or logging it as a warning + * + * @param err The ProtocolError to handle + * @return expected Returns an empty expected if the error was logged as a warning, + * otherwise throws the ProtocolError. * + * @throws ProtocolError if the error is of type PROTOCOL_ERROR */ - virtual void check_support() = 0; + static auto error_or_warning(const ProtocolError& err) -> expected { + // Log warnings, but throw errors + switch(err.error_code()){ + case Error::PROTOCOL_ERROR: + throw err; + break; + case Error::INTEGRATION_ERROR: + case Error::UNITIALIZED_MODEL: + case Error::UNSUPPORTED_PROTOCOL: + case Error::PROTOCOL_WARNING: + std::cerr << err.to_string() << std::endl; + return make_unexpected( ProtocolError(std::move(err) ) ); + default: + throw err; + } + assert (false && "Unreachable code reached in error_or_warning"); + } + /** - * @brief Initialize the BMI protocol from a set of key/value properties + * @brief Run the BMI protocol against the given model * - * @param properties + * Execute the logic of the protocol with the provided context and model. + * It is the caller's responsibility to ensure that the model provided is + * consistent with the model provided to the object's initialize() and + * check_support() methods, hence the protected nature of this function. + * + * @param ctx Contextual information for the protocol run + * @param model A shared pointer to a Bmi_Adapter object which should be + * initialized before being passed to this method. + * + * @return expected May contain a ProtocolError if + * the protocol fails for any reason. Errors of ProtocolError::PROTOCOL_WARNING + * severity should be logged as warnings, but not cause the simulation to fail. */ - virtual void initialize(const geojson::PropertyMap& properties) = 0; + [[nodiscard]] virtual auto run(const ModelPtr& model, const Context& ctx) const -> expected = 0; - protected: - /** - * @brief Constructor for subclasses to create NgenBmiProtocol objects + * @brief Check if the BMI protocol is supported by the model + * + * It is the caller's responsibility to ensure that the model provided is + * consistent with the model provided to the object's initialize() and + * run() methods, hence the protected nature of this function. * * @param model A shared pointer to a Bmi_Adapter object which should be - * initialized before being passed to this constructor. + * initialized before being passed to this method. + * + * @return expected May contain a ProtocolError if + * the protocol is not supported by the model. */ - NgenBmiProtocol(std::shared_ptr model): model(model){} + [[nodiscard]] virtual expected check_support(const ModelPtr& model) = 0; /** - * @brief The Bmi_Adapter object used by the protocol + * @brief Initialize the BMI protocol from a set of key/value properties * + * It is the caller's responsibility to ensure that the model provided is + * consistent with the model provided to the object's run() and + * check_support() methods, hence the protected nature of this function. + * + * @param properties key/value pairs for initializing the protocol + * @param model A shared pointer to a Bmi_Adapter object which should be + * initialized before being passed to this method. + * + * @return expected May contain a ProtocolError if + * initialization fails for any reason, since the protocol must + * be effectively "optional", failed initialization results in + * the protocol being disabled for the duration of the simulation. */ - std::shared_ptr model; + virtual auto initialize(const ModelPtr& model, const Properties& properties) -> expected = 0; /** * @brief Whether the protocol is supported by the model * */ bool is_supported = false; + + /** + * @brief Friend class for managing one or more protocols + * + * This allows the NgenBmiProtocols container class to access the protected `run()` + * method. This allows the container to ensure consistent application of the + * protocol with a particular bmi model instance throughout the lifecycle of a given + * protocol. + * + */ + friend class NgenBmiProtocols; }; }}} diff --git a/include/utilities/bmi/protocols.hpp b/include/utilities/bmi/protocols.hpp index 32c1792f9a..05115fe6c3 100644 --- a/include/utilities/bmi/protocols.hpp +++ b/include/utilities/bmi/protocols.hpp @@ -15,6 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.2 +Enumerate protocol types/names +The container now holds a single model pointer and passes it to each protocol +per the updated (v0.2) protocol interface +Keep protocols in a map for dynamic access by enumeration name +add operator<< for Protocol enum + Version 0.1 Container and management for abstract BMI protocols */ @@ -23,6 +30,7 @@ Container and management for abstract BMI protocols #include #include #include +#include #include "Bmi_Adapter.hpp" #include "JSONProperty.hpp" @@ -31,6 +39,12 @@ Container and management for abstract BMI protocols namespace models{ namespace bmi{ namespace protocols{ +enum class Protocol { + MASS_BALANCE +}; + +auto operator<<(std::ostream& os, Protocol p) -> std::ostream&; + class NgenBmiProtocols { /** * @brief Container and management interface for BMI protocols for use in ngen @@ -42,7 +56,7 @@ class NgenBmiProtocols { * @brief Construct a new Ngen Bmi Protocols object with a null model * */ - NgenBmiProtocols(); + NgenBmiProtocols() : model(nullptr){}; /** * @brief Construct a new Ngen Bmi Protocols object for use with a known model @@ -52,11 +66,23 @@ class NgenBmiProtocols { */ NgenBmiProtocols(std::shared_ptr model, const geojson::PropertyMap& properties); + auto run(const Protocol& protocol_name, const Context& ctx) const -> expected; + + private: + /** - * @brief Mass Balance Checker + * @brief All protocols managed by this container will utilize the same model + * + * This reduces the amount of pointer copying and references across a large simulation + * and it ensures that all protocols see the same model state. * */ - NgenMassBalance mass_balance; -}; + std::shared_ptr model; + /** + * @brief Map of protocol name to protocol instance + * + */ + std::unordered_map> protocols; + }; }}} diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index 6fcdc97642..b3f7d99541 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -15,6 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.2 +Implement error handling via expected and error_or_warning + Version 0.1 Implementation the BMI mass balance checking protocol */ @@ -23,7 +26,16 @@ Implementation the BMI mass balance checking protocol namespace models { namespace bmi { namespace protocols { -void NgenMassBalance::run(const Context& ctx) const{ +NgenMassBalance::NgenMassBalance(const ModelPtr& model, const Properties& properties) : + check(false), is_fatal(false), tolerance(1.0E-16), frequency(1){ + initialize(model, properties); +} + +NgenMassBalance::NgenMassBalance() : check(false) {} + +NgenMassBalance::~NgenMassBalance() = default; + +auto NgenMassBalance::run(const ModelPtr& model, const Context& ctx) const -> expected { bool check_step = false; //if frequency was set to -1, only check at the end if( frequency == -1 ){ @@ -34,7 +46,13 @@ void NgenMassBalance::run(const Context& ctx) const{ else{ check_step = (ctx.current_time_step % frequency) == 0; } - + if( model == nullptr ) { + return make_unexpected( ProtocolError( + Error::UNITIALIZED_MODEL, + "Cannot run mass balance protocol with null model." + ) + ); + } if(model && is_supported && check && check_step) { double mass_in, mass_out, mass_stored, mass_leaked, mass_balance; model->GetValue(INPUT_MASS_NAME, &mass_in); @@ -43,9 +61,17 @@ void NgenMassBalance::run(const Context& ctx) const{ model->GetValue(LEAKED_MASS_NAME, &mass_leaked); // TODO consider unit conversion if/when it becomes necessary mass_balance = mass_in - mass_out - mass_stored - mass_leaked; + // std::stringstream ss; + // ss << "Mass balance for " << model->GetComponentName() <<"\n\t" << + // INPUT_MASS_NAME << "(" << mass_in << ") - " << + // OUTPUT_MASS_NAME << " (" << mass_out << ") - " << + // STORED_MASS_NAME << " (" << mass_stored << ") = " << + // mass_balance << "\n\t" << "tolerance: " << tolerance; + // std::cout << ss.str() << std::endl; if ( std::abs(mass_balance) > tolerance ) { std::stringstream ss; - ss << "at timestep " << std::to_string(ctx.current_time_step) + ss << "mass_balance: " + << "at timestep " << std::to_string(ctx.current_time_step) << " ("+ctx.timestamp+")" << " at feature id " << ctx.id <GetComponentName() << "\n\t" << @@ -54,17 +80,18 @@ void NgenMassBalance::run(const Context& ctx) const{ STORED_MASS_NAME << " (" << mass_stored << ") - " << LEAKED_MASS_NAME << " (" << mass_leaked << ") = " << mass_balance << "\n\t" << "tolerance: " << tolerance << std::endl; - if(is_fatal) - throw MassBalanceError("mass_balance::error " + ss.str()); - else{ - std::cerr << "mass_balance::warning " + ss.str(); - } - + return make_unexpected( ProtocolError( + is_fatal ? Error::PROTOCOL_ERROR : Error::PROTOCOL_WARNING, + ss.str() + ) + ); } + } + return {}; } -void NgenMassBalance::check_support() { +auto NgenMassBalance::check_support(const ModelPtr& model) -> expected { if (model->is_model_initialized()) { double mass_var; std::vector units; @@ -79,29 +106,42 @@ void NgenMassBalance::check_support() { //Compare all other units to the first one (+1) if( std::equal( units.begin()+1, units.end(), units.begin() ) ) { this->is_supported = true; + return {}; } else{ // It may be possible to do unit conversion and still do meaninful mass balance // this could be added as an extended feature, but for now, I don't think this is // worth the complexity. It is, however, worth the sanity check performed here // to ensure the units are consistent. - throw MassBalanceError( "mass balance variables have incosistent units, cannot perform mass balance" ); + return make_unexpected( ProtocolError( + Error::INTEGRATION_ERROR, + "mass_balance: variables have incosistent units, cannot perform mass balance." + ) + ); } } catch (const std::exception &e) { std::stringstream ss; - ss << "mass_balance::integration: Error getting mass balance values for module '" << model->GetComponentName() << "': " << e.what() << std::endl; - std::cout << ss.str(); - this->is_supported= false; + ss << "mass_balance: Error getting mass balance values for module '" << model->GetComponentName() << "': " << e.what() << std::endl; + return make_unexpected( ProtocolError( + Error::INTEGRATION_ERROR, + ss.str() + ) + ); } } else { - throw std::runtime_error("Cannot check mass balance for uninitialized model."); + return make_unexpected( ProtocolError( + Error::UNITIALIZED_MODEL, + "Cannot check mass balance for uninitialized model. Disabling mass balance protocol." + ) + ); } + return {}; } -void NgenMassBalance::initialize(const geojson::PropertyMap& properties) +auto NgenMassBalance::initialize(const ModelPtr& model, const Properties& properties) -> expected { //Ensure the model is capable of mass balance using the protocol - check_support(); + check_support(model).or_else( error_or_warning ); //now check if the user has requested to use mass balance auto protocol_it = properties.find(CONFIGURATION_KEY); @@ -132,6 +172,7 @@ void NgenMassBalance::initialize(const geojson::PropertyMap& properties) //no mass balance requested, or not supported, so don't check it check = false; } + return {}; // important to return for the expected to be properly created! } }}} // end namespace models::bmi::protocols \ No newline at end of file diff --git a/src/utilities/bmi/protocols.cpp b/src/utilities/bmi/protocols.cpp index aef86f40ed..115cc9c7af 100644 --- a/src/utilities/bmi/protocols.cpp +++ b/src/utilities/bmi/protocols.cpp @@ -15,6 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.2 +Implement error handling via expected and error_or_warning + Version 0.1 Container and management for abstract BMI protocols */ @@ -23,14 +26,39 @@ Container and management for abstract BMI protocols namespace models{ namespace bmi{ namespace protocols{ -NgenBmiProtocols::NgenBmiProtocols(): mass_balance(nullptr) {} +auto operator<<(std::ostream& os, Protocol p) -> std::ostream& { + switch(p) { + case Protocol::MASS_BALANCE: os << "MASS_BALANCE"; break; + default: os << "UNKNOWN_PROTOCOL"; break; + } + return os; +} + + +NgenBmiProtocols::NgenBmiProtocols(ModelPtr model, const geojson::PropertyMap& properties) + : model(model) { + //Create and initialize mass balance configurable properties + protocols[Protocol::MASS_BALANCE] = std::make_unique(model, properties); +} -NgenBmiProtocols::NgenBmiProtocols(std::shared_ptr model, const geojson::PropertyMap& properties) - : mass_balance(model) { - //initialize mass balance configurable properties - //This is done so that initialize is called with a valid model pointer, which is shared - //by all protocol instances - mass_balance.initialize(properties); +auto NgenBmiProtocols::run(const Protocol& protocol_name, const Context& ctx) const -> expected { + // Consider using find() vs switch, especially if the number of protocols grows + expected result_or_err; + switch(protocol_name){ + case Protocol::MASS_BALANCE: + protocols.at(Protocol::MASS_BALANCE)->run(model, ctx) + .or_else( NgenBmiProtocol::error_or_warning ); + break; + default: + std::stringstream ss; + ss << "Error: Request for unsupported protocol: '" << protocol_name << "'."; + return NgenBmiProtocol::error_or_warning( ProtocolError( + Error::UNSUPPORTED_PROTOCOL, + ss.str() + ) + ); + } + return {}; } }}} // end namespace models::bmi::protocols From 2faf51ac271254444951b544bf2504761911c13d Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Sat, 27 Sep 2025 00:09:48 -0600 Subject: [PATCH 12/58] fix: update ngen and tests for v0.2 of bmi protocols --- .../catchment/Bmi_Module_Formulation.hpp | 2 +- .../catchments/Bmi_C_Formulation_Test.cpp | 55 +++++++++++++++---- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/include/realizations/catchment/Bmi_Module_Formulation.hpp b/include/realizations/catchment/Bmi_Module_Formulation.hpp index 4f432d8ed0..b65a23a1ac 100644 --- a/include/realizations/catchment/Bmi_Module_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Module_Formulation.hpp @@ -254,7 +254,7 @@ namespace realization { virtual void check_mass_balance(const int& iteration, const int& total_steps, const std::string& timestamp) const override { //Create the protocol context, each member is const, and cannot change during the check models::bmi::protocols::Context ctx{iteration, total_steps, timestamp, id}; - bmi_protocols.mass_balance.run(ctx); + bmi_protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, ctx); } protected: diff --git a/test/realizations/catchments/Bmi_C_Formulation_Test.cpp b/test/realizations/catchments/Bmi_C_Formulation_Test.cpp index 6b78f83a35..a135423197 100644 --- a/test/realizations/catchments/Bmi_C_Formulation_Test.cpp +++ b/test/realizations/catchments/Bmi_C_Formulation_Test.cpp @@ -398,7 +398,7 @@ using models::bmi::protocols::INPUT_MASS_NAME; using models::bmi::protocols::OUTPUT_MASS_NAME; using models::bmi::protocols::STORED_MASS_NAME; using models::bmi::protocols::LEAKED_MASS_NAME; -using models::bmi::protocols::MassBalanceError; +using models::bmi::protocols::ProtocolError; TEST_F(Bmi_C_Formulation_Test, check_mass_balance) { int ex_index = 0; @@ -429,7 +429,7 @@ TEST_F(Bmi_C_Formulation_Test, check_mass_balance_warns) { formulation.check_mass_balance(0, 1, "t0"); std::string output = testing::internal::GetCapturedStderr(); std::cerr << output; - EXPECT_THAT(output, testing::HasSubstr("mass_balance::warning")); + EXPECT_THAT(output, testing::HasSubstr("Warning(Protocol)::mass_balance:")); } TEST_F(Bmi_C_Formulation_Test, check_mass_balance_stored_fails) { @@ -444,7 +444,14 @@ TEST_F(Bmi_C_Formulation_Test, check_mass_balance_stored_fails) { double mass_error = 100; get_friend_bmi_model(formulation)->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error //formulation.check_mass_balance(0, 1, "t0"); - ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } } TEST_F(Bmi_C_Formulation_Test, check_mass_balance_in_fails_a) { @@ -457,7 +464,14 @@ TEST_F(Bmi_C_Formulation_Test, check_mass_balance_in_fails_a) { formulation.get_response(1, 3600); double mass_error = 2; get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error - ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } } TEST_F(Bmi_C_Formulation_Test, check_mass_balance_out_fails) { @@ -470,7 +484,14 @@ TEST_F(Bmi_C_Formulation_Test, check_mass_balance_out_fails) { formulation.get_response(1, 3600); double mass_error = 2; get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // Force a mass balance error - ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } } TEST_F(Bmi_C_Formulation_Test, check_mass_balance_leaked_fails) { @@ -483,7 +504,14 @@ TEST_F(Bmi_C_Formulation_Test, check_mass_balance_leaked_fails) { formulation.get_response(1, 3600); double mass_error = 2; get_friend_bmi_model(formulation)->SetValue(LEAKED_MASS_NAME, &mass_error); // Force a mass balance error - ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } } TEST_F(Bmi_C_Formulation_Test, check_mass_balance_tolerance) { @@ -499,7 +527,14 @@ TEST_F(Bmi_C_Formulation_Test, check_mass_balance_tolerance) { get_friend_bmi_model(formulation)->GetValue(INPUT_MASS_NAME, &mass_error); mass_error += 1e-4; // Force a mass balance error not within tolerance get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error - ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceError); + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } } TEST_F(Bmi_C_Formulation_Test, check_mass_balance_tolerance_a) { @@ -559,13 +594,13 @@ TEST_F(Bmi_C_Formulation_Test, check_mass_balance_frequency) { get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // //Check initial mass balance -- should error which indicates it was propoerly checked //per frequency setting - ASSERT_THROW(formulation.check_mass_balance(0, 2, "t0"), MassBalanceError); + ASSERT_THROW(formulation.check_mass_balance(0, 2, "t0"), ProtocolError); // Call mass balance check again, this should NOT error, since the actual check // should be skipped due to the frequency setting formulation.check_mass_balance(1, 2, "t1"); // Check mass balance again, this SHOULD error since the previous mass balance // will propagate, and it should now be checked based on the frequency - ASSERT_THROW(formulation.check_mass_balance(2, 2, "t2"), MassBalanceError); + ASSERT_THROW(formulation.check_mass_balance(2, 2, "t2"), ProtocolError); } TEST_F(Bmi_C_Formulation_Test, check_mass_balance_frequency_1) { @@ -585,7 +620,7 @@ TEST_F(Bmi_C_Formulation_Test, check_mass_balance_frequency_1) { formulation.check_mass_balance(1, 2, "t1"); // Check mass balance again, this SHOULD error since the this is step 2/2 // and it will now be checked based on the frequency (-1, check at end) - ASSERT_THROW(formulation.check_mass_balance(2, 2, "t2"), MassBalanceError); + ASSERT_THROW(formulation.check_mass_balance(2, 2, "t2"), ProtocolError); } #endif // NGEN_BMI_C_LIB_TESTS_ACTIVE From 8e8f06e69447975893da704942e40618d01880f5 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:34:18 -0600 Subject: [PATCH 13/58] feat(protocol)!: make the protocol a pure interface; implement is_supported() --- include/utilities/bmi/mass_balance.hpp | 12 ++++++++++++ include/utilities/bmi/protocol.hpp | 15 +++------------ src/utilities/bmi/mass_balance.cpp | 10 +++++++--- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/include/utilities/bmi/mass_balance.hpp b/include/utilities/bmi/mass_balance.hpp index d5b9a6c22e..b83c6ce8c5 100644 --- a/include/utilities/bmi/mass_balance.hpp +++ b/include/utilities/bmi/mass_balance.hpp @@ -14,6 +14,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.3 +Implement is_supported() Version 0.2 Conform to updated protocol interface @@ -121,7 +123,17 @@ namespace models{ namespace bmi{ namespace protocols{ */ auto initialize(const ModelPtr& model, const Properties& properties) -> expected override; + /** + * @brief Whether the protocol is supported by the model + * + * @return true the model exposes the required mass balance variables + * @return false the model does not support mass balance checking via this protocol + */ + bool is_supported() const override final; + private: + // Whether the protocol is supported by the model, false by default + bool supported = false; // Configurable options/values bool check; bool is_fatal; diff --git a/include/utilities/bmi/protocol.hpp b/include/utilities/bmi/protocol.hpp index 5e8caf22ca..c4c463479c 100644 --- a/include/utilities/bmi/protocol.hpp +++ b/include/utilities/bmi/protocol.hpp @@ -14,6 +14,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.3 +Remove "supported" member variable and added is_supported() method to protocol interface Version 0.2 Enumerate protocol error types and add ProtocolError exception class @@ -95,17 +97,6 @@ class NgenBmiProtocol{ public: - /** - * @brief Construct a new Ngen Bmi Protocol object - * - * By default, the protocol is considered unsupported. - * Subclasses are responsible for implementing the check_support() method, - * and ensuring that is_supported is properly set based on the protocol's - * requirements. - * - */ - NgenBmiProtocol() : is_supported(false) {} - virtual ~NgenBmiProtocol() = default; protected: @@ -191,7 +182,7 @@ class NgenBmiProtocol{ * @brief Whether the protocol is supported by the model * */ - bool is_supported = false; + virtual bool is_supported() const = 0; /** * @brief Friend class for managing one or more protocols diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index b3f7d99541..9e31e62c83 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -53,7 +53,7 @@ auto NgenMassBalance::run(const ModelPtr& model, const Context& ctx) const -> ex ) ); } - if(model && is_supported && check && check_step) { + if(model && supported && check && check_step) { double mass_in, mass_out, mass_stored, mass_leaked, mass_balance; model->GetValue(INPUT_MASS_NAME, &mass_in); model->GetValue(OUTPUT_MASS_NAME, &mass_out); @@ -105,7 +105,7 @@ auto NgenMassBalance::check_support(const ModelPtr& model) -> expectedis_supported = true; + this->supported = true; return {}; } else{ @@ -145,7 +145,7 @@ auto NgenMassBalance::initialize(const ModelPtr& model, const Properties& proper //now check if the user has requested to use mass balance auto protocol_it = properties.find(CONFIGURATION_KEY); - if ( is_supported && protocol_it != properties.end() ) { + if ( supported && protocol_it != properties.end() ) { geojson::PropertyMap mass_bal = protocol_it->second.get_values(); auto _it = mass_bal.find(TOLERANCE_KEY); @@ -175,4 +175,8 @@ auto NgenMassBalance::initialize(const ModelPtr& model, const Properties& proper return {}; // important to return for the expected to be properly created! } +bool NgenMassBalance::is_supported() const { + return this->supported; +} + }}} // end namespace models::bmi::protocols \ No newline at end of file From 710b40e928c28076ab013ff6176d6322fde232c5 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:38:42 -0600 Subject: [PATCH 14/58] fix(mass_balance): treat all negative frequency settings the same --- src/utilities/bmi/mass_balance.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index 9e31e62c83..154bafdc92 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -37,8 +37,8 @@ NgenMassBalance::~NgenMassBalance() = default; auto NgenMassBalance::run(const ModelPtr& model, const Context& ctx) const -> expected { bool check_step = false; - //if frequency was set to -1, only check at the end - if( frequency == -1 ){ + //if frequency was set to -1 (or any negative), only check at the end + if( frequency < 0 ){ if(ctx.current_time_step == ctx.total_steps){ check_step = true; } From 1f43faf30767c791f41e77ec0a9e6c890b916ee9 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:39:21 -0600 Subject: [PATCH 15/58] fix(mass_balance): don't check support with null model --- src/utilities/bmi/mass_balance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index 154bafdc92..5e5470d550 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -92,7 +92,7 @@ auto NgenMassBalance::run(const ModelPtr& model, const Context& ctx) const -> ex } auto NgenMassBalance::check_support(const ModelPtr& model) -> expected { - if (model->is_model_initialized()) { + if (model != nullptr && model->is_model_initialized()) { double mass_var; std::vector units; units.reserve(4); From 5dac9c8885d5473ea2de8c77c7dcfdd466b60178 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:39:49 -0600 Subject: [PATCH 16/58] fix(mass_balance): handle potential NaN tolernace --- src/utilities/bmi/mass_balance.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index 5e5470d550..6e27d714fa 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -150,7 +150,19 @@ auto NgenMassBalance::initialize(const ModelPtr& model, const Properties& proper auto _it = mass_bal.find(TOLERANCE_KEY); if( _it != mass_bal.end() ) tolerance = _it->second.as_real_number(); - + //as_real_number() *should* return a floating point NaN representation + //if the input value were presented as "NaN" -- non numberic values + //non numberic values will throw an exception (not handled here) + //it is expected that the user/configuration is responsible for providing + //a valid numeric value for tolerance + if( std::isnan(tolerance) ) { + check = false; //disable mass balance checking + return error_or_warning( ProtocolError( + Error::PROTOCOL_WARNING, + "mass_balance: tolerance value 'NaN' provided, disabling mass balance check." + ) + ); + } _it = mass_bal.find(FATAL_KEY); if( _it != mass_bal.end() ) is_fatal = _it->second.as_boolean(); From 4286299a243d55184df91a88b535951e35b268c2 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:45:10 -0600 Subject: [PATCH 17/58] fix(protocols): better default handling; add missing return --- include/utilities/bmi/protocols.hpp | 2 +- src/utilities/bmi/protocols.cpp | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/include/utilities/bmi/protocols.hpp b/include/utilities/bmi/protocols.hpp index 05115fe6c3..dbd3ab48cf 100644 --- a/include/utilities/bmi/protocols.hpp +++ b/include/utilities/bmi/protocols.hpp @@ -56,7 +56,7 @@ class NgenBmiProtocols { * @brief Construct a new Ngen Bmi Protocols object with a null model * */ - NgenBmiProtocols() : model(nullptr){}; + NgenBmiProtocols(); /** * @brief Construct a new Ngen Bmi Protocols object for use with a known model diff --git a/src/utilities/bmi/protocols.cpp b/src/utilities/bmi/protocols.cpp index 115cc9c7af..59abd9143c 100644 --- a/src/utilities/bmi/protocols.cpp +++ b/src/utilities/bmi/protocols.cpp @@ -34,6 +34,10 @@ auto operator<<(std::ostream& os, Protocol p) -> std::ostream& { return os; } +NgenBmiProtocols::NgenBmiProtocols() + : model(nullptr) { + protocols[Protocol::MASS_BALANCE] = std::make_unique(); +} NgenBmiProtocols::NgenBmiProtocols(ModelPtr model, const geojson::PropertyMap& properties) : model(model) { @@ -46,7 +50,7 @@ auto NgenBmiProtocols::run(const Protocol& protocol_name, const Context& ctx) co expected result_or_err; switch(protocol_name){ case Protocol::MASS_BALANCE: - protocols.at(Protocol::MASS_BALANCE)->run(model, ctx) + return protocols.at(Protocol::MASS_BALANCE)->run(model, ctx) .or_else( NgenBmiProtocol::error_or_warning ); break; default: From 7a8adfdb448b2bfe4dc6a6af4a705b38e13460d0 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:48:36 -0600 Subject: [PATCH 18/58] chore(mass_balance): alignment/padding friendly member ordering --- include/utilities/bmi/mass_balance.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/utilities/bmi/mass_balance.hpp b/include/utilities/bmi/mass_balance.hpp index b83c6ce8c5..f28d725892 100644 --- a/include/utilities/bmi/mass_balance.hpp +++ b/include/utilities/bmi/mass_balance.hpp @@ -16,6 +16,7 @@ limitations under the License. ------------------------------------------------------------------------ Version 0.3 Implement is_supported() +Re-align members for more better memory layout/padding Version 0.2 Conform to updated protocol interface @@ -132,14 +133,14 @@ namespace models{ namespace bmi{ namespace protocols{ bool is_supported() const override final; private: + double tolerance; + // How often (in time steps) to check mass balance + int frequency; // Whether the protocol is supported by the model, false by default bool supported = false; // Configurable options/values bool check; bool is_fatal; - double tolerance; - // How often (in time steps) to check mass balance - int frequency; }; }}} From 2aa90fac3341468023b8a1b2b8a2183be4064921 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:49:59 -0600 Subject: [PATCH 19/58] doc(mass_balance): update docstrings --- include/utilities/bmi/mass_balance.hpp | 14 +++++++++++--- src/utilities/bmi/mass_balance.cpp | 8 +------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/include/utilities/bmi/mass_balance.hpp b/include/utilities/bmi/mass_balance.hpp index f28d725892..cf3be9720a 100644 --- a/include/utilities/bmi/mass_balance.hpp +++ b/include/utilities/bmi/mass_balance.hpp @@ -17,6 +17,7 @@ limitations under the License. Version 0.3 Implement is_supported() Re-align members for more better memory layout/padding +Update docstrings Version 0.2 Conform to updated protocol interface @@ -91,15 +92,17 @@ namespace models{ namespace bmi{ namespace protocols{ * Warns or errors at each check if total mass balance is not within the configured * acceptable tolerance. * - * throws: MassBalanceError if the mass balance is not within the configured - * acceptable tolerance and the protocol is configured to be fatal. + * @return expected May contain a ProtocolError if + * the protocol fails for any reason. Errors of ProtocolError::PROTOCOL_WARNING + * severity should be logged as warnings, but not cause the simulation to fail. */ auto run(const ModelPtr& model, const Context& ctx) const -> expected override; /** * @brief Check if the mass balance protocol is supported by the model * - * throws: MassBalanceIntegrationError if the mass balance protocol is not supported + * @return expected May contain a ProtocolError if + * the protocol is not supported by the model. */ [[nodiscard]] auto check_support(const ModelPtr& model) -> expected override; @@ -121,6 +124,11 @@ namespace models{ namespace bmi{ namespace protocols{ * frequency: int, default 1. How often (in time steps) to check mass balance. * fatal: bool, default false. Whether to treat mass balance errors as fatal. * Otherwise, mass balance checking will be disabled (check will be false) + * + * @return expected May contain a ProtocolError if + * initialization fails for any reason, since the protocol must + * be effectively "optional", failed initialization results in + * the protocol being disabled for the duration of the simulation. */ auto initialize(const ModelPtr& model, const Properties& properties) -> expected override; diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index 6e27d714fa..98b4f16c56 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -14,6 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.3 (see mass_balance.hpp for details) Version 0.2 Implement error handling via expected and error_or_warning @@ -61,13 +62,6 @@ auto NgenMassBalance::run(const ModelPtr& model, const Context& ctx) const -> ex model->GetValue(LEAKED_MASS_NAME, &mass_leaked); // TODO consider unit conversion if/when it becomes necessary mass_balance = mass_in - mass_out - mass_stored - mass_leaked; - // std::stringstream ss; - // ss << "Mass balance for " << model->GetComponentName() <<"\n\t" << - // INPUT_MASS_NAME << "(" << mass_in << ") - " << - // OUTPUT_MASS_NAME << " (" << mass_out << ") - " << - // STORED_MASS_NAME << " (" << mass_stored << ") = " << - // mass_balance << "\n\t" << "tolerance: " << tolerance; - // std::cout << ss.str() << std::endl; if ( std::abs(mass_balance) > tolerance ) { std::stringstream ss; ss << "mass_balance: " From ad2c801f6f962404478d807c37f79f225ad8f175 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:51:20 -0600 Subject: [PATCH 20/58] doc(protocols): update docstrings --- include/utilities/bmi/protocols.hpp | 14 +++++++++++++- src/utilities/bmi/protocols.cpp | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/include/utilities/bmi/protocols.hpp b/include/utilities/bmi/protocols.hpp index dbd3ab48cf..723fcf1364 100644 --- a/include/utilities/bmi/protocols.hpp +++ b/include/utilities/bmi/protocols.hpp @@ -14,6 +14,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.2.1 +Fix NgenBmiProtocols constructor to initialize protocols map when model is null +Fix run() to return the expected returned by the wrapped call Version 0.2 Enumerate protocol types/names @@ -65,7 +68,16 @@ class NgenBmiProtocols { * @param properties Properties for each protocol being initialized */ NgenBmiProtocols(std::shared_ptr model, const geojson::PropertyMap& properties); - + + /** + * @brief Run a specific BMI protocol by name with a given context + * + * @param protocol_name The name of the protocol to run + * @param ctx The context of the current protocol run + * + * @return expected May contain a ProtocolError if + * the protocol fails for any reason. + */ auto run(const Protocol& protocol_name, const Context& ctx) const -> expected; private: diff --git a/src/utilities/bmi/protocols.cpp b/src/utilities/bmi/protocols.cpp index 59abd9143c..c78651a192 100644 --- a/src/utilities/bmi/protocols.cpp +++ b/src/utilities/bmi/protocols.cpp @@ -14,6 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ------------------------------------------------------------------------ +Version 0.2.1 (See bmi/protocols.hpp for details) Version 0.2 Implement error handling via expected and error_or_warning From 44289ed39347170c826d85c5bda6486fbc8150e5 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:52:39 -0600 Subject: [PATCH 21/58] test(test_bmi_cpp): implement mass balance protocol in cpp test model --- extern/test_bmi_cpp/include/test_bmi_cpp.hpp | 13 ++++++++++ extern/test_bmi_cpp/src/test_bmi_cpp.cpp | 27 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/extern/test_bmi_cpp/include/test_bmi_cpp.hpp b/extern/test_bmi_cpp/include/test_bmi_cpp.hpp index c15896def2..1ed9d29136 100644 --- a/extern/test_bmi_cpp/include/test_bmi_cpp.hpp +++ b/extern/test_bmi_cpp/include/test_bmi_cpp.hpp @@ -25,6 +25,11 @@ #define BMI_TYPE_NAME_SHORT "short" #define BMI_TYPE_NAME_LONG "long" +#define NGEN_MASS_IN "ngen::mass_in" +#define NGEN_MASS_OUT "ngen::mass_out" +#define NGEN_MASS_STORED "ngen::mass_stored" +#define NGEN_MASS_LEAKED "ngen::mass_leaked" + class TestBmiCpp : public bmi::Bmi { public: /** @@ -179,6 +184,11 @@ class TestBmiCpp : public bmi::Bmi { std::vector output_var_locations = { "node", "node" }; std::vector model_var_locations = {}; + std::vector mass_balance_var_names = { NGEN_MASS_IN, NGEN_MASS_OUT, NGEN_MASS_STORED, NGEN_MASS_LEAKED}; + std::vector mass_balance_var_types = { "double", "double", "double", "double"}; + std::vector mass_balance_var_units = { "m", "m", "m", "m" }; + std::vector mass_balance_var_locations = { "node", "node", "node", "node"}; + std::vector input_var_item_count = { 1, 1 }; std::vector output_var_item_count = { 1, 1 }; std::vector model_var_item_count = {}; @@ -223,6 +233,9 @@ class TestBmiCpp : public bmi::Bmi { std::unique_ptr model_var_1 = nullptr; std::unique_ptr model_var_2 = nullptr; + double mass_stored = 0.0; + double mass_leaked = 0.0; + /** * Read the BMI initialization config file and use its contents to set the state of the model. * diff --git a/extern/test_bmi_cpp/src/test_bmi_cpp.cpp b/extern/test_bmi_cpp/src/test_bmi_cpp.cpp index 32536e0c75..57019839fb 100644 --- a/extern/test_bmi_cpp/src/test_bmi_cpp.cpp +++ b/extern/test_bmi_cpp/src/test_bmi_cpp.cpp @@ -165,6 +165,19 @@ void* TestBmiCpp::GetValuePtr(std::string name){ } } + if (name == NGEN_MASS_STORED) { + return &this->mass_stored; + } + if (name == NGEN_MASS_LEAKED) { + return &this->mass_leaked; + } + if (name == NGEN_MASS_IN) { + return this->input_var_1.get(); + } + if (name == NGEN_MASS_OUT) { + return this->output_var_1.get(); + } + throw std::runtime_error("GetValuePtr called for unknown variable: "+name); } @@ -212,6 +225,10 @@ int TestBmiCpp::GetVarNbytes(std::string name){ if(iter != this->model_var_names.end()){ item_count = this->model_var_item_count[iter - this->model_var_names.begin()]; } + iter = std::find(this->mass_balance_var_names.begin(), this->mass_balance_var_names.end(), name); + if(iter != this->mass_balance_var_names.end()){ + item_count = 1; + } if(item_count == -1){ // This is probably impossible to reach--the same conditions above failing will cause a throw // in GetVarItemSize --> GetVarType (called earlier) instead. @@ -233,6 +250,10 @@ std::string TestBmiCpp::GetVarType(std::string name){ if(iter != this->model_var_names.end()){ return this->model_var_types[iter - this->model_var_names.begin()]; } + iter = std::find(this->mass_balance_var_names.begin(), this->mass_balance_var_names.end(), name); + if(iter != this->mass_balance_var_names.end()){ + return this->mass_balance_var_types[iter - this->mass_balance_var_names.begin()]; + } throw std::runtime_error("GetVarType called for non-existent variable: "+name+"" SOURCE_LOC ); } @@ -249,6 +270,10 @@ std::string TestBmiCpp::GetVarUnits(std::string name){ if(iter != this->model_var_names.end()){ return this->model_var_types[iter - this->model_var_names.begin()]; } + iter = std::find(this->mass_balance_var_names.begin(), this->mass_balance_var_names.end(), name); + if(iter != this->mass_balance_var_names.end()){ + return this->mass_balance_var_units[iter - this->mass_balance_var_names.begin()]; + } throw std::runtime_error("GetVarUnits called for non-existent variable: "+name+"" SOURCE_LOC); } @@ -517,4 +542,6 @@ void TestBmiCpp::run(long dt) *this->output_var_5 = *this->model_var_2 * 1.0; } this->current_model_time += (double)dt; + this->mass_stored = *this->output_var_1 - *this->input_var_1; + this->mass_leaked = 0; } From b6a6bdf37b764ca8cc76693124a780247b49d907 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:55:25 -0600 Subject: [PATCH 22/58] test(bmi_protocols): add standalone mass balance protocol unit tests --- test/CMakeLists.txt | 14 + test/utils/bmi/MockConfig.hpp | 70 +++++ test/utils/bmi/mass_balance_Test.cpp | 412 +++++++++++++++++++++++++++ 3 files changed, 496 insertions(+) create mode 100644 test/utils/bmi/MockConfig.hpp create mode 100644 test/utils/bmi/mass_balance_Test.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 79fd3897cc..a6a00efff0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -207,6 +207,20 @@ if(TARGET test_forcings_engine) add_compile_definitions(NGEN_LUMPED_CONFIG_PATH="${CMAKE_CURRENT_BINARY_DIR}/config_aorc.yml") endif() +########################## BMI Protocol Unit Tests + +ngen_add_test( + test_bmi_protocols + OBJECTS + utils/bmi/mass_balance_Test.cpp + LIBRARIES + NGen::bmi_protocols + NGen::ngen_bmi + gmock + DEPENDS + testbmicppmodel +) + ########################## Series Unit Tests ngen_add_test( test_mdarray diff --git a/test/utils/bmi/MockConfig.hpp b/test/utils/bmi/MockConfig.hpp new file mode 100644 index 0000000000..5aa3130d5c --- /dev/null +++ b/test/utils/bmi/MockConfig.hpp @@ -0,0 +1,70 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ +*/ +#pragma once +#include +#include "protocols.hpp" +#include "JSONProperty.hpp" + +static const auto noneConfig = std::map { + {"none", geojson::JSONProperty("none", true)} +}; + +static models::bmi::protocols::Context make_context(int current_time_step, int total_steps, const std::string& timestamp, const std::string& id) { + return models::bmi::protocols::Context{ + .current_time_step = current_time_step, + .total_steps = total_steps, + .timestamp = timestamp, + .id = id + }; +} + +class MassBalanceMock { + public: + + MassBalanceMock( bool fatal = false, double tolerance = 1e-12, int frequency = 1, bool check = true) + : properties() { + boost::property_tree::ptree config; + config.put("check", check); + config.put("tolerance", tolerance); + config.put("fatal", fatal); + config.put("frequency", frequency); + properties.add_child("mass_balance", config); + } + + MassBalanceMock( bool fatal, const char* tolerance){ + boost::property_tree::ptree config; + config.put("check", true); + config.put("tolerance", tolerance); + config.put("fatal", fatal); + config.put("frequency", 1); + properties.add_child("mass_balance", config); + } + + const boost::property_tree::ptree& get() const { + return properties.get_child("mass_balance"); + } + + const geojson::PropertyMap as_json_property() const { + auto props = geojson::JSONProperty("mass_balance", properties); + // geojson::JSONProperty::print_property(props, 1); + return props.get_values(); + } + + private: + boost::property_tree::ptree properties; +}; \ No newline at end of file diff --git a/test/utils/bmi/mass_balance_Test.cpp b/test/utils/bmi/mass_balance_Test.cpp new file mode 100644 index 0000000000..5669997f8a --- /dev/null +++ b/test/utils/bmi/mass_balance_Test.cpp @@ -0,0 +1,412 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ +*/ +#ifdef NGEN_BMI_CPP_LIB_TESTS_ACTIVE +//Test utitilities +// #include "bmi.hpp" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +//Mock configuration and context helpers +#include "MockConfig.hpp" +//BMI model/adapter +#include "FileChecker.h" +#include "Bmi_Cpp_Adapter.hpp" +#include +//Interface under test +#include "protocols.hpp" + +using ::testing::MatchesRegex; +//protocol symbols +using models::bmi::protocols::NgenBmiProtocols; +using models::bmi::protocols::INPUT_MASS_NAME; +using models::bmi::protocols::OUTPUT_MASS_NAME; +using models::bmi::protocols::STORED_MASS_NAME; +using models::bmi::protocols::LEAKED_MASS_NAME; +using models::bmi::protocols::ProtocolError; +using nonstd::expected_lite::expected; + +// Use the ngen bmi c++ test library +#ifndef BMI_TEST_CPP_LOCAL_LIB_NAME +#ifdef __APPLE__ +#define BMI_TEST_CPP_LOCAL_LIB_NAME "libtestbmicppmodel.dylib" +#else +#ifdef __GNUC__ + #define BMI_TEST_CPP_LOCAL_LIB_NAME "libtestbmicppmodel.so" + #endif // __GNUC__ +#endif // __APPLE__ +#endif // BMI_TEST_CPP_LOCAL_LIB_NAME + +#define CREATOR_FUNC "bmi_model_create" +#define DESTROYER_FUNC "bmi_model_destroy" + +using namespace models::bmi; + +// Copy of the struct def used within the test_bmi_c test library + +class Bmi_Cpp_Test_Adapter : public ::testing::Test { +protected: + + void SetUp() override; + + void TearDown() override; + + std::string file_search(const std::vector &parent_dir_options, const std::string& file_basename); + + std::string config_file_name_0; + std::string lib_file_name_0; + std::string bmi_module_type_name_0; + std::unique_ptr adapter; +}; + +void Bmi_Cpp_Test_Adapter::SetUp() { + /** + * @brief Set up the test environment for the protocol tests. + * + * The protocol requires a valid BMI model to operate on, so this setup + * function initializes a BMI C++ adapter using the test BMI C++ model + * + */ + // Uses the same config files as the C test model... + std::vector config_path_options = { + "test/data/bmi/test_bmi_c/", + "./test/data/bmi/test_bmi_c/", + "../test/data/bmi/test_bmi_c/", + "../../test/data/bmi/test_bmi_c/", + }; + std::string config_basename_0 = "test_bmi_c_config_0.txt"; + config_file_name_0 = file_search(config_path_options, config_basename_0); + + std::vector lib_dir_opts = { + "./extern/test_bmi_cpp/cmake_build/", + "../extern/test_bmi_cpp/cmake_build/", + "../../extern/test_bmi_cpp/cmake_build/" + }; + lib_file_name_0 = file_search(lib_dir_opts, BMI_TEST_CPP_LOCAL_LIB_NAME); + bmi_module_type_name_0 = "test_bmi_cpp"; + try { + adapter = std::make_unique(bmi_module_type_name_0, lib_file_name_0, config_file_name_0, + true, CREATOR_FUNC, DESTROYER_FUNC); + } + catch (const std::exception &e) { + std::clog << e.what() << std::endl; + throw e; + } + } + +void Bmi_Cpp_Test_Adapter::TearDown() { + +} + +std::string +Bmi_Cpp_Test_Adapter::file_search(const std::vector &parent_dir_options, const std::string& file_basename) { + // Build vector of names by building combinations of the path and basename options + std::vector name_combinations; + + // Build so that all path names are tried for given basename before trying a different basename option + for (auto & path_option : parent_dir_options) + name_combinations.push_back(path_option + file_basename); + + return utils::FileChecker::find_first_readable(name_combinations); +} + +class Bmi_Mass_Balance_Test : public Bmi_Cpp_Test_Adapter { +protected: + void SetUp() override { + Bmi_Cpp_Test_Adapter::SetUp(); + model = std::shared_ptr(adapter.release()); + model_name = model->GetComponentName(); + } + std::string time = "t0"; + std::string model_name; + std::shared_ptr model; +}; + +TEST_F(Bmi_Mass_Balance_Test, bad_model) { + model = nullptr; // simulate uninitialized model + auto properties = MassBalanceMock(true).as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_THAT(output, MatchesRegex("Error\\(Uninitialized Model\\).*Disabling mass balance protocol.\n")); + testing::internal::CaptureStderr(); + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( ! result.has_value() ); // should have an error!!! + EXPECT_EQ( result.error().error_code(), models::bmi::protocols::Error::UNITIALIZED_MODEL ); + output = testing::internal::GetCapturedStderr(); + EXPECT_THAT(output, MatchesRegex("Error\\(Uninitialized Model\\).*\n")); +} + +TEST_F(Bmi_Mass_Balance_Test, default_construct) { + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(); + testing::internal::CaptureStderr(); + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( ! result.has_value() ); // should have an error!!! + EXPECT_EQ( result.error().error_code(), models::bmi::protocols::Error::UNITIALIZED_MODEL ); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_THAT(output, MatchesRegex("Error\\(Uninitialized Model\\).*\n")); +} + +TEST_F(Bmi_Mass_Balance_Test, check) { + auto properties = MassBalanceMock(true).as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + model->Update(); + time = "t1"; + protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + std::string output = testing::internal::GetCapturedStderr(); + // Not warning/errors printed, and no exceptions thrown is success + EXPECT_EQ(output, ""); +} + +TEST_F(Bmi_Mass_Balance_Test, warns) { + auto properties = MassBalanceMock(false).as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + double mass_error = 100; + model->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + // The expected should be an error not a value (void in this case) + EXPECT_FALSE( result.has_value() ); // should have a warning!!! + EXPECT_THAT( result.error().to_string(), testing::HasSubstr("Warning(Protocol)::mass_balance:") ); + std::string output = testing::internal::GetCapturedStderr(); + // std::cerr << output; + //Warning was sent to stderr + EXPECT_THAT(output, testing::HasSubstr("Warning(Protocol)::mass_balance:")); +} + +TEST_F(Bmi_Mass_Balance_Test, storage_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + double mass_error = 2; + model->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + time = "t1"; + + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 1 \\(t1\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, in_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + time = "t1"; + double mass_error = 2; + model->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 1 \\(t1\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, out_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + time = "t1"; + double mass_error = 2; + model->SetValue(OUTPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 1 \\(t1\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, leaked_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + time = "t1"; + double mass_error = 2; + model->SetValue(LEAKED_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 1 \\(t1\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, tolerance_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property();; + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + double mass_error; + model->GetValue(INPUT_MASS_NAME, &mass_error); + mass_error += 1e-4; // Force a mass balance error not within tolerance + model->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 0 \\(t0\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, tolerance_passes) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + double mass_error; + model->GetValue(INPUT_MASS_NAME, &mass_error); + mass_error += 1e-6; // Force a mass balance error within tolerance + model->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + // should not thow! + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass +} + +TEST_F(Bmi_Mass_Balance_Test, disabled) { + auto properties = MassBalanceMock(true, 1e-5, 1, false).as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( result.has_value() ); // should pass, as mass balance is disabled + model->Update(); // advance model + time = "t1"; + double mass_error = 100; + model->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + // should not throw, as mass balance is disabled + EXPECT_TRUE( result.has_value() ); // should pass, as mass balance is disabled + std::string output = testing::internal::GetCapturedStderr(); + // No warnings/errors printed, and no exceptions thrown is success + EXPECT_EQ(output, ""); +} + +TEST_F(Bmi_Mass_Balance_Test, unconfigured) { + auto properties = noneConfig; + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( result.has_value() ); // should pass, as mass balance is disabled + model->Update(); // advance model + time = "t1"; + double mass_error = 100; + model->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + // should not throw, as mass balance is disabled + EXPECT_TRUE( result.has_value() ); // should pass, as mass balance is disabled + std::string output = testing::internal::GetCapturedStderr(); + // No warnings/errors printed, and no exceptions thrown is success + EXPECT_EQ(output, ""); +} + +TEST_F(Bmi_Mass_Balance_Test, frequency) { + auto properties = MassBalanceMock(true, 1e-5, 2).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + double mass_error = 10; // Force a mass balance error above tolerance + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + //Check initial mass balance -- should error which indicates it was propoerly checked + //per frequency setting + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)), ProtocolError); + time = "t1"; + model->Update(); // advance model + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + // Call mass balance check again, this should NOT error, since the actual check + // should be skipped due to the frequency setting + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass + time = "t2"; + model->Update(); // advance model + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + // Check mass balance again, this SHOULD error since the previous mass balance + // will propagate, and it should now be checked based on the frequency + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(2, 2, time, model_name)), ProtocolError); +} + +TEST_F(Bmi_Mass_Balance_Test, frequency_end) { + auto properties = MassBalanceMock(true, 1e-5, -1).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + double mass_error = 10; // Force a mass balance error above tolerance + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + //Check initial mass balance -- should not error due to frequency setting + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass + time = "t1"; + model->Update(); // advance model + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + // Call mass balance check again, this should NOT error, since the actual check + // should be skipped due to the frequency setting + result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass + time = "t2"; + model->Update(); // advance model + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + // Check mass balance again, this SHOULD error since its the last timestep and the previous mass balance + // will propagate, and it should now be checked based on the frequency + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(2, 2, time, model_name)), ProtocolError); +} + +TEST_F(Bmi_Mass_Balance_Test, nan) { + auto properties = MassBalanceMock(true, "NaN").as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + std::string output = testing::internal::GetCapturedStderr(); + // std::cerr << output; + EXPECT_THAT(output, testing::HasSubstr("Warning(Protocol)::mass_balance: tolerance value 'NaN'")); + double mass_error = 10; + model->SetValue(OUTPUT_MASS_NAME, &mass_error); // + // Would cause an error if tolerance were a number, should only see warning in the output above + // and this should pass without error... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 1, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass +} + +#endif // NGEN_BMI_CPP_LIB_TESTS_ACTIVE \ No newline at end of file From 14f280463b1b4900a9a13e1e8e87c8db4eb94f46 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 16:56:38 -0600 Subject: [PATCH 23/58] test: point formulation tests to use same protocol mock --- .../catchments/Bmi_C_Formulation_Test.cpp | 2 +- .../realizations/catchments/Bmi_Protocols.hpp | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 test/realizations/catchments/Bmi_Protocols.hpp diff --git a/test/realizations/catchments/Bmi_C_Formulation_Test.cpp b/test/realizations/catchments/Bmi_C_Formulation_Test.cpp index a135423197..e532f201e9 100644 --- a/test/realizations/catchments/Bmi_C_Formulation_Test.cpp +++ b/test/realizations/catchments/Bmi_C_Formulation_Test.cpp @@ -24,7 +24,7 @@ #include "FileChecker.h" #include "Formulation_Manager.hpp" #include -#include "Bmi_Protocols.hpp" +#include "../../utils/bmi/MockConfig.hpp" using ::testing::MatchesRegex; using namespace realization; diff --git a/test/realizations/catchments/Bmi_Protocols.hpp b/test/realizations/catchments/Bmi_Protocols.hpp deleted file mode 100644 index 30b5570e9f..0000000000 --- a/test/realizations/catchments/Bmi_Protocols.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#include - -class MassBalanceMock { - public: - - MassBalanceMock( bool fatal = false, double tolerance = 1e-12, int frequency = 1) - : properties() { - properties.put("check", true); - properties.put("tolerance", tolerance); - properties.put("fatal", fatal); - properties.put("frequency", frequency); - } - - const boost::property_tree::ptree& get() const { - return properties; - } - private: - boost::property_tree::ptree properties; -}; \ No newline at end of file From 8f40151ae899c4eef1ca0698be01941db9d882a9 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 17:18:44 -0600 Subject: [PATCH 24/58] fix(test): use older compatible static struct initialization in mock --- test/utils/bmi/MockConfig.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/utils/bmi/MockConfig.hpp b/test/utils/bmi/MockConfig.hpp index 5aa3130d5c..094ce440df 100644 --- a/test/utils/bmi/MockConfig.hpp +++ b/test/utils/bmi/MockConfig.hpp @@ -26,10 +26,10 @@ static const auto noneConfig = std::map { static models::bmi::protocols::Context make_context(int current_time_step, int total_steps, const std::string& timestamp, const std::string& id) { return models::bmi::protocols::Context{ - .current_time_step = current_time_step, - .total_steps = total_steps, - .timestamp = timestamp, - .id = id + current_time_step, + total_steps, + timestamp, + id }; } From d5d5006548fb46f332b6a86622b6947dad397f70 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Thu, 23 Oct 2025 17:24:41 -0600 Subject: [PATCH 25/58] ci(test_and_validate): add bmi protocol unit tests to workflow --- .github/workflows/test_and_validate.yml | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/test_and_validate.yml b/.github/workflows/test_and_validate.yml index 00279ae8d8..60f4e60aea 100644 --- a/.github/workflows/test_and_validate.yml +++ b/.github/workflows/test_and_validate.yml @@ -278,6 +278,35 @@ jobs: - name: Clean Up Unit Test Build uses: ./.github/actions/clean-build + # Run BMI protocol tests in linux/unix environment + test_bmi_protocols: + # The type of runner that the job will run on + strategy: + matrix: + os: [ubuntu-22.04, macos-12] + fail-fast: false + runs-on: ${{ matrix.os }} + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + - name: Build BMI Protocol Unit Tests + uses: ./.github/actions/ngen-build + with: + targets: "test_bmi_protocols" + build-cores: ${{ env.LINUX_NUM_PROC_CORES }} + + - name: run_bmi_protocol_tests + run: | + cd ./cmake_build/test/ + ./test_bmi_protocols + cd ../../ + timeout-minutes: 15 + + - name: Clean Up BMI Protocol Unit Test Build + uses: ./.github/actions/clean-build # TODO: fails due to compilation error, at least in large part due to use of POSIX functions not supported on Windows. # TODO: Need to determine whether Windows support (in particular, development environment support) is necessary. From cbc9ed6b0b3c3b2ed9d2f8648f065dab8c7a4349 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 08:00:58 -0600 Subject: [PATCH 26/58] chore: add the boost software license for expected --- include/utilities/bmi/nonstd/LICENSE.txt | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 include/utilities/bmi/nonstd/LICENSE.txt diff --git a/include/utilities/bmi/nonstd/LICENSE.txt b/include/utilities/bmi/nonstd/LICENSE.txt new file mode 100644 index 0000000000..36b7cd93cd --- /dev/null +++ b/include/utilities/bmi/nonstd/LICENSE.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. From 533b9993eb64b1be2e75924624f8d76389f71214 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 08:10:10 -0600 Subject: [PATCH 27/58] fix(build): fix expected.tweak macro name --- include/utilities/bmi/nonstd/expected.tweak.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/utilities/bmi/nonstd/expected.tweak.hpp b/include/utilities/bmi/nonstd/expected.tweak.hpp index 3a7be06a76..89a8e1a4ad 100644 --- a/include/utilities/bmi/nonstd/expected.tweak.hpp +++ b/include/utilities/bmi/nonstd/expected.tweak.hpp @@ -1,4 +1,4 @@ //tweaks for the expected library configuration/build // see https://github.com/martinmoene/expected-lite/tree/master?tab=readme-ov-file#configuration // for documentation on configuration -#define nsel_CONFIG_ WIN32_LEAN_AND_MEAN 0 +#define nsel_CONFIG_WIN32_LEAN_AND_MEAN 0 From 452017ae9f9639d5bcd0dc0473279e2d55d16295 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 08:41:40 -0600 Subject: [PATCH 28/58] fix(mass_balance): NaN in model vars should trigger mass balance error/warning --- src/utilities/bmi/mass_balance.cpp | 2 +- test/utils/bmi/mass_balance_Test.cpp | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index 98b4f16c56..49895f57db 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -62,7 +62,7 @@ auto NgenMassBalance::run(const ModelPtr& model, const Context& ctx) const -> ex model->GetValue(LEAKED_MASS_NAME, &mass_leaked); // TODO consider unit conversion if/when it becomes necessary mass_balance = mass_in - mass_out - mass_stored - mass_leaked; - if ( std::abs(mass_balance) > tolerance ) { + if ( std::abs(mass_balance) > tolerance || std::isnan(mass_balance)) { std::stringstream ss; ss << "mass_balance: " << "at timestep " << std::to_string(ctx.current_time_step) diff --git a/test/utils/bmi/mass_balance_Test.cpp b/test/utils/bmi/mass_balance_Test.cpp index 5669997f8a..dbc27c6415 100644 --- a/test/utils/bmi/mass_balance_Test.cpp +++ b/test/utils/bmi/mass_balance_Test.cpp @@ -409,4 +409,17 @@ TEST_F(Bmi_Mass_Balance_Test, nan) { EXPECT_TRUE( result.has_value() ); // should pass } +TEST_F(Bmi_Mass_Balance_Test, model_nan) { + auto properties = MassBalanceMock(true).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( result.has_value() ); // should pass + double mass_error = std::numeric_limits::quiet_NaN(); + model->SetValue(OUTPUT_MASS_NAME, &mass_error); // Force a NaN into the mass balance computation + time = "t1"; + // should cause an error since mass balance will be NaN using this value in its computation + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); +} + #endif // NGEN_BMI_CPP_LIB_TESTS_ACTIVE \ No newline at end of file From d86ad6640e34a230f6fb93997e741ae2bce883e4 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 09:27:35 -0600 Subject: [PATCH 29/58] fix(mass_balance): avoid div-by-zero, don't check mass balance with frequency 0 --- src/utilities/bmi/mass_balance.cpp | 28 +++++++++++++++++----------- test/utils/bmi/mass_balance_Test.cpp | 10 ++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index 49895f57db..4d3f02ab9f 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -37,24 +37,27 @@ NgenMassBalance::NgenMassBalance() : check(false) {} NgenMassBalance::~NgenMassBalance() = default; auto NgenMassBalance::run(const ModelPtr& model, const Context& ctx) const -> expected { - bool check_step = false; - //if frequency was set to -1 (or any negative), only check at the end - if( frequency < 0 ){ - if(ctx.current_time_step == ctx.total_steps){ - check_step = true; - } - } - else{ - check_step = (ctx.current_time_step % frequency) == 0; - } if( model == nullptr ) { return make_unexpected( ProtocolError( Error::UNITIALIZED_MODEL, "Cannot run mass balance protocol with null model." ) ); + } else if( !check || !supported ) { + return {}; } - if(model && supported && check && check_step) { + bool check_step = false; + //if frequency was set to -1 (or any negative), only check at the end + //use <= to avoid a potential divide by zero should frequency be 0 + //(though frequency 0 should have been caught during initialization and check disabled) + if( frequency > 0 ){ + check_step = (ctx.current_time_step % frequency) == 0; + } + else if(ctx.current_time_step == ctx.total_steps){ + check_step = true; + } + + if(check_step) { double mass_in, mass_out, mass_stored, mass_leaked, mass_balance; model->GetValue(INPUT_MASS_NAME, &mass_in); model->GetValue(OUTPUT_MASS_NAME, &mass_out); @@ -174,6 +177,9 @@ auto NgenMassBalance::initialize(const ModelPtr& model, const Properties& proper } else { frequency = 1; //default, check every timestep } + if ( frequency == 0 ) { + check = false; // can't check at frequency 0, disable mass balance checking + } } else{ //no mass balance requested, or not supported, so don't check it check = false; diff --git a/test/utils/bmi/mass_balance_Test.cpp b/test/utils/bmi/mass_balance_Test.cpp index dbc27c6415..9d834c3ce3 100644 --- a/test/utils/bmi/mass_balance_Test.cpp +++ b/test/utils/bmi/mass_balance_Test.cpp @@ -369,6 +369,16 @@ TEST_F(Bmi_Mass_Balance_Test, frequency) { ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(2, 2, time, model_name)), ProtocolError); } +TEST_F(Bmi_Mass_Balance_Test, frequency_zero) { + auto properties = MassBalanceMock(true, 1e-5, 0).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + double mass_error = 10; // Force a mass balance error above tolerance + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass, frequency 0 means never check +} + TEST_F(Bmi_Mass_Balance_Test, frequency_end) { auto properties = MassBalanceMock(true, 1e-5, -1).as_json_property(); auto context = make_context(0, 2, time, model_name); From c7ab4c8337fc9df47c338a00b6f3f34ca5ad0a9e Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 11:26:03 -0600 Subject: [PATCH 30/58] fix(mass_balance): use conditinonal macro for nodiscard attribute (c++17) --- include/utilities/bmi/mass_balance.hpp | 2 +- include/utilities/bmi/protocol.hpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/utilities/bmi/mass_balance.hpp b/include/utilities/bmi/mass_balance.hpp index cf3be9720a..e8883232ff 100644 --- a/include/utilities/bmi/mass_balance.hpp +++ b/include/utilities/bmi/mass_balance.hpp @@ -104,7 +104,7 @@ namespace models{ namespace bmi{ namespace protocols{ * @return expected May contain a ProtocolError if * the protocol is not supported by the model. */ - [[nodiscard]] auto check_support(const ModelPtr& model) -> expected override; + nsel_NODISCARD auto check_support(const ModelPtr& model) -> expected override; /** * @brief Check the model for support and initialize the mass balance protocol from the given properties. diff --git a/include/utilities/bmi/protocol.hpp b/include/utilities/bmi/protocol.hpp index c4c463479c..d08dd7681c 100644 --- a/include/utilities/bmi/protocol.hpp +++ b/include/utilities/bmi/protocol.hpp @@ -143,7 +143,7 @@ class NgenBmiProtocol{ * the protocol fails for any reason. Errors of ProtocolError::PROTOCOL_WARNING * severity should be logged as warnings, but not cause the simulation to fail. */ - [[nodiscard]] virtual auto run(const ModelPtr& model, const Context& ctx) const -> expected = 0; + nsel_NODISCARD virtual auto run(const ModelPtr& model, const Context& ctx) const -> expected = 0; /** * @brief Check if the BMI protocol is supported by the model @@ -158,7 +158,7 @@ class NgenBmiProtocol{ * @return expected May contain a ProtocolError if * the protocol is not supported by the model. */ - [[nodiscard]] virtual expected check_support(const ModelPtr& model) = 0; + nsel_NODISCARD virtual expected check_support(const ModelPtr& model) = 0; /** * @brief Initialize the BMI protocol from a set of key/value properties From ff74dc21c8fef9b9ec2238cca9f5593e22f6e915 Mon Sep 17 00:00:00 2001 From: Austin Raney Date: Fri, 31 Oct 2025 15:49:04 -0400 Subject: [PATCH 31/58] fix: check if model supports mass balance checks only if feature is enabled. --- src/utilities/bmi/mass_balance.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index 4d3f02ab9f..f542a682a0 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -137,12 +137,9 @@ auto NgenMassBalance::check_support(const ModelPtr& model) -> expected expected { - //Ensure the model is capable of mass balance using the protocol - check_support(model).or_else( error_or_warning ); - //now check if the user has requested to use mass balance auto protocol_it = properties.find(CONFIGURATION_KEY); - if ( supported && protocol_it != properties.end() ) { + if ( protocol_it != properties.end() ) { geojson::PropertyMap mass_bal = protocol_it->second.get_values(); auto _it = mass_bal.find(TOLERANCE_KEY); @@ -184,6 +181,10 @@ auto NgenMassBalance::initialize(const ModelPtr& model, const Properties& proper //no mass balance requested, or not supported, so don't check it check = false; } + if ( check ) { + //Ensure the model is capable of mass balance using the protocol + check_support(model).or_else( error_or_warning ); + } return {}; // important to return for the expected to be properly created! } @@ -191,4 +192,4 @@ bool NgenMassBalance::is_supported() const { return this->supported; } -}}} // end namespace models::bmi::protocols \ No newline at end of file +}}} // end namespace models::bmi::protocols From d6583fe3df39bd813a85ad45b88eb7a98c8e69a8 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 10:54:44 -0600 Subject: [PATCH 32/58] ci: update test runner to macos-15 --- .github/workflows/module_integration.yml | 2 +- .github/workflows/test_and_validate.yml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/module_integration.yml b/.github/workflows/module_integration.yml index b89c629079..1e757032ee 100644 --- a/.github/workflows/module_integration.yml +++ b/.github/workflows/module_integration.yml @@ -22,7 +22,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} diff --git a/.github/workflows/test_and_validate.yml b/.github/workflows/test_and_validate.yml index 60f4e60aea..3059750c3e 100644 --- a/.github/workflows/test_and_validate.yml +++ b/.github/workflows/test_and_validate.yml @@ -24,7 +24,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -59,7 +59,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -116,7 +116,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -151,7 +151,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -182,7 +182,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -215,7 +215,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -250,7 +250,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -283,7 +283,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} From 20c231d00e06f7466cef20859877fd2a8a903c37 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 11:59:17 -0600 Subject: [PATCH 33/58] fix(boost): updgrade boost for fixing https://github.com/boostorg/mpl/issues/69 --- .github/actions/ngen-build/action.yaml | 8 ++++---- CMakeLists.txt | 2 +- INSTALL.md | 10 +++++----- doc/BUILDS_AND_CMAKE.md | 2 +- doc/DEPENDENCIES.md | 10 +++++----- docker/CENTOS_4.8.5_NGEN_RUN.dockerfile | 6 +++--- docker/CENTOS_NGEN_RUN.dockerfile | 8 ++++---- docker/CENTOS_TEST.dockerfile | 6 +++--- docker/CENTOS_latest_NGEN_RUN.dockerfile | 6 +++--- docker/RHEL_TEST.dockerfile | 6 +++--- docker/ngen.dockerfile | 2 +- 11 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/actions/ngen-build/action.yaml b/.github/actions/ngen-build/action.yaml index 8c0f79730b..882ceef702 100644 --- a/.github/actions/ngen-build/action.yaml +++ b/.github/actions/ngen-build/action.yaml @@ -110,14 +110,14 @@ runs: id: cache-boost-dep uses: actions/cache@v4 with: - path: boost_1_79_0 + path: boost_1_86_0 key: unix-boost-dep - name: Get Boost Dependency if: steps.cache-boost-dep.outputs.cache-hit != 'true' run: | - curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download - tar xjf boost_1_79_0.tar.bz2 + curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download + tar xjf boost_1_86_0.tar.bz2 shell: bash - name: Set Pip Constraints @@ -186,7 +186,7 @@ runs: - name: Cmake Initialization id: cmake_init run: | - export BOOST_ROOT="$(pwd)/boost_1_79_0" + export BOOST_ROOT="$(pwd)/boost_1_86_0" export CFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -Werror" export CXXFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" . .venv/bin/activate diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ec2c55fb3..2d96422026 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -165,7 +165,7 @@ add_compile_definitions(NGEN_SHARED_LIB_EXTENSION) set(Boost_USE_STATIC_LIBS OFF) set(Boost_USE_MULTITHREADED ON) set(Boost_USE_STATIC_RUNTIME OFF) -find_package(Boost 1.79.0 REQUIRED) +find_package(Boost 1.86.0 REQUIRED) # ----------------------------------------------------------------------------- if(NGEN_WITH_SQLITE) diff --git a/INSTALL.md b/INSTALL.md index 8b73cb5f97..ad4fad92a2 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -35,15 +35,15 @@ cd ngen **Download the Boost Libraries:** ```shell -curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download \ - && tar -xjf boost_1_79_0.tar.bz2 \ - && rm boost_1_79_0.tar.bz2 +curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download \ + && tar -xjf boost_1_86_0.tar.bz2 \ + && rm boost_1_86_0.tar.bz2 ``` **Set the ENV for Boost and C compiler:** ```shell -set BOOST_ROOT="/boost_1_79_0" +set BOOST_ROOT="/boost_1_86_0" set CXX=/usr/bin/g++ ``` @@ -79,7 +79,7 @@ The following CMake command will configure the build: ```shell cmake -DCMAKE_CXX_COMPILER=/usr/bin/g++ \ - -DBOOST_ROOT=boost_1_79_0 \ + -DBOOST_ROOT=boost_1_86_0 \ -B /build \ -S . ``` diff --git a/doc/BUILDS_AND_CMAKE.md b/doc/BUILDS_AND_CMAKE.md index 1f1dd944b0..f17613262c 100644 --- a/doc/BUILDS_AND_CMAKE.md +++ b/doc/BUILDS_AND_CMAKE.md @@ -104,7 +104,7 @@ In some cases - in particular **Google Test** - the build system will need to be ## Boost ENV Variable -The Boost libraries must be available for the project to compile. The details are discussed more in the [Dependencies](DEPENDENCIES.md) doc, but as a helpful hint, the **BOOST_ROOT** environmental variable can be set to the path of the applicable [Boost root directory](https://www.boost.org/doc/libs/1_79_0/more/getting_started/unix-variants.html#the-boost-distribution). The project's [CMakeLists.txt](../CMakeLists.txt) is written to check for this env variable and use it to set the Boost include directory. +The Boost libraries must be available for the project to compile. The details are discussed more in the [Dependencies](DEPENDENCIES.md) doc, but as a helpful hint, the **BOOST_ROOT** environmental variable can be set to the path of the applicable [Boost root directory](https://www.boost.org/doc/libs/1_86_0/more/getting_started/unix-variants.html#the-boost-distribution). The project's [CMakeLists.txt](../CMakeLists.txt) is written to check for this env variable and use it to set the Boost include directory. Note that if the variable is not set, it may still be possible for CMake to find Boost, although a *status* message will be printed by CMake indicating **BOOST_ROOT** was not set. diff --git a/doc/DEPENDENCIES.md b/doc/DEPENDENCIES.md index 6c48b38f94..e5e16c1d33 100644 --- a/doc/DEPENDENCIES.md +++ b/doc/DEPENDENCIES.md @@ -7,7 +7,7 @@ | [Google Test](#google-test) | submodule | `release-1.10.0` | | | [C/C++ Compiler](#c-and-c-compiler) | external | see below | | | [CMake](#cmake) | external | \>= `3.17` | | -| [Boost (Headers Only)](#boost-headers-only) | external | `1.79.0` | headers only library | +| [Boost (Headers Only)](#boost-headers-only) | external | `1.86.0` | headers only library | | [Udunits libraries](https://www.unidata.ucar.edu/software/udunits) | external | >= 2.0 | Can be installed via package manager or from source | | [MPI](https://www.mpi-forum.org) | external | No current implementation or version requirements | Required for [multi-process distributed execution](DISTRIBUTED_PROCESSING.md) | | [Python 3 Libraries](#python-3-libraries) | external | \>= `3.8.0` | Can be [excluded](#overriding-python-dependency). | @@ -78,7 +78,7 @@ Currently, a version of CMake >= `3.14.0` is required. ## Boost (Headers Only) -Boost libraries are used by this project. In particular, [Boost.Geometry](https://www.boost.org/doc/libs/1_79_0/libs/geometry/doc/html/geometry/compilation.html) is used, but others are also. +Boost libraries are used by this project. In particular, [Boost.Geometry](https://www.boost.org/doc/libs/1_86_0/libs/geometry/doc/html/geometry/compilation.html) is used, but others are also. Currently, only headers-only Boost libraries are utilized. As such, they are not exhaustively listed here since getting one essentially gets them all. @@ -88,7 +88,7 @@ Since only headers-only libraries are needed, the Boost headers simply need to b There are a variety of different ways to get the Boost headers locally. Various OS may have packages specifically to install them, though one should take note of whether such packages provide a version of Boost that meets this project's requirements. -Alternatively, the Boost distribution itself can be manually downloaded and unpacked, as described for both [Unix-variants](https://www.boost.org/doc/libs/1_79_0/more/getting_started/unix-variants.html) and [Windows](https://www.boost.org/doc/libs/1_79_0/more/getting_started/windows.html) on the Boost website. +Alternatively, the Boost distribution itself can be manually downloaded and unpacked, as described for both [Unix-variants](https://www.boost.org/doc/libs/1_86_0/more/getting_started/unix-variants.html) and [Windows](https://www.boost.org/doc/libs/1_86_0/more/getting_started/windows.html) on the Boost website. #### Setting **BOOST_ROOT** @@ -96,11 +96,11 @@ If necessary, the project's CMake config is able to use the value of the **BOOST However, it will often be necessary to set **BOOST_ROOT** if Boost was manually set up by downloading the distribution. -The variable should be set to the value of the **boost root directory**, which is something like `/boost_1_79_0`. +The variable should be set to the value of the **boost root directory**, which is something like `/boost_1_86_0`. ### Version Requirements -At present, a version >= `1.79.0` is required. +At present, a version >= `1.86.0` is required. ## Udunits diff --git a/docker/CENTOS_4.8.5_NGEN_RUN.dockerfile b/docker/CENTOS_4.8.5_NGEN_RUN.dockerfile index 9569428d20..794d40827d 100644 --- a/docker/CENTOS_4.8.5_NGEN_RUN.dockerfile +++ b/docker/CENTOS_4.8.5_NGEN_RUN.dockerfile @@ -19,11 +19,11 @@ ENV CXX=/usr/bin/g++ RUN git submodule update --init --recursive -- test/googletest -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download -RUN tar -xjf boost_1_79_0.tar.bz2 +RUN tar -xjf boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="boost_1_79_0" +ENV BOOST_ROOT="boost_1_86_0" RUN cmake -B /ngen -S . diff --git a/docker/CENTOS_NGEN_RUN.dockerfile b/docker/CENTOS_NGEN_RUN.dockerfile index 8aee5b9a4b..afaa442d34 100644 --- a/docker/CENTOS_NGEN_RUN.dockerfile +++ b/docker/CENTOS_NGEN_RUN.dockerfile @@ -9,11 +9,11 @@ RUN yum update -y \ && dnf clean all \ && rm -rf /var/cache/yum -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download \ - && tar -xjf boost_1_79_0.tar.bz2 \ - && rm boost_1_79_0.tar.bz2 +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download \ + && tar -xjf boost_1_86_0.tar.bz2 \ + && rm boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="/boost_1_79_0" +ENV BOOST_ROOT="/boost_1_86_0" ENV CXX=/usr/bin/g++ diff --git a/docker/CENTOS_TEST.dockerfile b/docker/CENTOS_TEST.dockerfile index 55271d2755..334ac5d5d7 100644 --- a/docker/CENTOS_TEST.dockerfile +++ b/docker/CENTOS_TEST.dockerfile @@ -11,11 +11,11 @@ ENV CXX=/usr/bin/g++ RUN git submodule update --init --recursive -- test/googletest -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_86_0.tar.bz2/download -RUN tar -xjf boost_1_79_0.tar.bz2 +RUN tar -xjf boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="boost_1_79_0" +ENV BOOST_ROOT="boost_1_86_0" WORKDIR /ngen diff --git a/docker/CENTOS_latest_NGEN_RUN.dockerfile b/docker/CENTOS_latest_NGEN_RUN.dockerfile index c53a46572e..11963f5209 100644 --- a/docker/CENTOS_latest_NGEN_RUN.dockerfile +++ b/docker/CENTOS_latest_NGEN_RUN.dockerfile @@ -13,11 +13,11 @@ ENV CXX=/usr/bin/g++ RUN git submodule update --init --recursive -- test/googletest -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download -RUN tar -xjf boost_1_79_0.tar.bz2 +RUN tar -xjf boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="boost_1_79_0" +ENV BOOST_ROOT="boost_1_86_0" WORKDIR /ngen diff --git a/docker/RHEL_TEST.dockerfile b/docker/RHEL_TEST.dockerfile index 9fe3550639..3103bd8464 100644 --- a/docker/RHEL_TEST.dockerfile +++ b/docker/RHEL_TEST.dockerfile @@ -10,11 +10,11 @@ ENV CXX=/usr/bin/g++ RUN git submodule update --init --recursive -- test/googletest -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download -RUN tar -xjf boost_1_79_0.tar.bz2 +RUN tar -xjf boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="boost_1_79_0" +ENV BOOST_ROOT="boost_1_86_0" WORKDIR /ngen diff --git a/docker/ngen.dockerfile b/docker/ngen.dockerfile index 88d880b98f..2d50a7c1a9 100644 --- a/docker/ngen.dockerfile +++ b/docker/ngen.dockerfile @@ -7,7 +7,7 @@ RUN dnf update -y \ && dnf install -y --allowerasing tar git gcc-c++ gcc make cmake udunits2-devel coreutils \ && dnf clean all -ARG BOOST_VERSION="1.79.0" +ARG BOOST_VERSION="1.86.0" RUN export BOOST_ARCHIVE="boost_$(echo ${BOOST_VERSION} | tr '\.' '_').tar.gz" \ && export BOOST_URL="https://sourceforge.net/projects/boost/files/boost/${BOOST_VERSION}/${BOOST_ARCHIVE}/download" \ && cd / \ From 7090f4169d727879d095026368ef23eb60c127de Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 12:21:15 -0600 Subject: [PATCH 34/58] ci: avoid false positive vtable ODR when loading CPP test lib (https://github.com/google/sanitizers/issues/1017) --- .github/workflows/test_and_validate.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test_and_validate.yml b/.github/workflows/test_and_validate.yml index 3059750c3e..29b9e10afe 100644 --- a/.github/workflows/test_and_validate.yml +++ b/.github/workflows/test_and_validate.yml @@ -139,6 +139,7 @@ jobs: - name: Run Tests run: | cd ./cmake_build/test/ + export ASAN_OPTIONS=${ASAN_OPTIONS}:detect_odr_violation=0 ./test_bmi_cpp cd ../../ timeout-minutes: 15 @@ -272,6 +273,7 @@ jobs: - name: Run Unit Tests run: | . .venv/bin/activate + export ASAN_OPTIONS=${ASAN_OPTIONS}:detect_odr_violation=0 ./cmake_build/test/test_bmi_multi timeout-minutes: 15 @@ -301,6 +303,7 @@ jobs: - name: run_bmi_protocol_tests run: | cd ./cmake_build/test/ + export ASAN_OPTIONS=${ASAN_OPTIONS}:detect_odr_violation=0 ./test_bmi_protocols cd ../../ timeout-minutes: 15 From 0422c1d5b5288ce6f015b8377cf75ec72916295d Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 13:22:29 -0600 Subject: [PATCH 35/58] ci: for runners with cmake version 4, need to enforce a mininmum cmake version 3.5 until pybind11 is updated --- .github/actions/ngen-build/action.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/actions/ngen-build/action.yaml b/.github/actions/ngen-build/action.yaml index 882ceef702..4ed4d66e93 100644 --- a/.github/actions/ngen-build/action.yaml +++ b/.github/actions/ngen-build/action.yaml @@ -185,12 +185,18 @@ runs: - name: Cmake Initialization id: cmake_init + # NOTE: -DCMAKE_POLICY_VERSION_MINIMUM=3.5 is required to use cmake version 4 + # and with older pybind11 versions, the minimum cmake version is set to 3.4 + # which causes cmake configuration to fail. run: | export BOOST_ROOT="$(pwd)/boost_1_86_0" export CFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -Werror" export CXXFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" . .venv/bin/activate [ ! -d "$BOOST_ROOT" ] && echo "Error: no Boost root found at $BOOST_ROOT" && exit 1 + echo "Cmake Version:" + which cmake + cmake --version cmake -B ${{ inputs.build-dir }} \ -DNGEN_WITH_BMI_C:BOOL=${{ inputs.bmi_c }} \ -DNGEN_WITH_PYTHON:BOOL=${{ inputs.use_python }} \ @@ -199,7 +205,9 @@ runs: -DNGEN_WITH_ROUTING:BOOL=${{ inputs.use_troute }} \ -DNGEN_WITH_NETCDF:BOOL=${{ inputs.use_netcdf }} \ -DNGEN_WITH_SQLITE:BOOL=${{ inputs.use_sqlite }} \ - -DNGEN_WITH_MPI:BOOL=${{ inputs.use_mpi }} -S . + -DNGEN_WITH_MPI:BOOL=${{ inputs.use_mpi }} \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -S . + echo "build-dir=$(echo ${{ inputs.build-dir }})" >> $GITHUB_OUTPUT shell: bash From 74bd8d9fe4acd166ed747b798dd1a5fb4801c50d Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 15:36:06 -0600 Subject: [PATCH 36/58] ci: ensure test modules/submodules use address sanitizer --- .github/actions/ngen-submod-build/action.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/ngen-submod-build/action.yaml b/.github/actions/ngen-submod-build/action.yaml index 349434fa45..0c8fac3153 100644 --- a/.github/actions/ngen-submod-build/action.yaml +++ b/.github/actions/ngen-submod-build/action.yaml @@ -50,6 +50,8 @@ runs: - name: Cmake Initialization id: cmake_init run: | + echo CFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -Werror" >> $GITHUB_ENV + echo CXXFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" >> $GITHUB_ENV cmake -B ${{ inputs.mod-dir}}/${{ inputs.build-dir }} -S ${{ inputs.mod-dir }} ${{ inputs.cmake-flags }} echo "build-dir=$(echo ${{ inputs.mod-dir}}/${{ inputs.build-dir }})" >> $GITHUB_OUTPUT shell: bash From 4b4b85a1656a84b777c32b323c6a5aac46e014b6 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 16:12:28 -0600 Subject: [PATCH 37/58] ci(ngen-build): add action option to build extern or not --- .github/actions/ngen-build/action.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/actions/ngen-build/action.yaml b/.github/actions/ngen-build/action.yaml index 4ed4d66e93..f3471db7cc 100644 --- a/.github/actions/ngen-build/action.yaml +++ b/.github/actions/ngen-build/action.yaml @@ -52,6 +52,10 @@ inputs: required: false description: 'Enable mpi support, only available for Linux runners' default: 'OFF' + build_extern: + required: false + description: 'Use external dependencies where possible' + default: 'OFF' outputs: build-dir: description: "Directory build was performed in" @@ -198,6 +202,7 @@ runs: which cmake cmake --version cmake -B ${{ inputs.build-dir }} \ + -DNGEN_WITH_EXTERN_ALL:BOOL=${{ inputs.build_extern }} \ -DNGEN_WITH_BMI_C:BOOL=${{ inputs.bmi_c }} \ -DNGEN_WITH_PYTHON:BOOL=${{ inputs.use_python }} \ -DNGEN_WITH_UDUNITS:BOOL=${{ inputs.use_udunits }} \ From f2496d21eb1720ea1e11a9ad534dba899535dc38 Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Fri, 24 Oct 2025 16:11:06 -0600 Subject: [PATCH 38/58] ci: ignore python metaclass function in address sanitizer --- .github/actions/ngen-build/action.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/actions/ngen-build/action.yaml b/.github/actions/ngen-build/action.yaml index f3471db7cc..a963deb3b1 100644 --- a/.github/actions/ngen-build/action.yaml +++ b/.github/actions/ngen-build/action.yaml @@ -196,6 +196,12 @@ runs: export BOOST_ROOT="$(pwd)/boost_1_86_0" export CFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -Werror" export CXXFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" + if [ ${{ runner.os }} == 'macOS' ] + then + echo "fun:PyType_FromMetaclass" > /tmp/asan_ignore.txt + export CFLAGS="$CFLAGS -fsanitize-ignorelist=/tmp/asan_ignore.txt" + export CXXFLAGS="$CXXFLAGS -fsanitize-ignorelist=/tmp/asan_ignore.txt" + fi . .venv/bin/activate [ ! -d "$BOOST_ROOT" ] && echo "Error: no Boost root found at $BOOST_ROOT" && exit 1 echo "Cmake Version:" From 6b274ca59d8742beb871da08d4c6d37cf8cf91e4 Mon Sep 17 00:00:00 2001 From: Robert Bartel Date: Tue, 18 Nov 2025 10:38:00 -0500 Subject: [PATCH 39/58] Set -fno-commom flag to avoid ASan global issues. --- .github/actions/ngen-build/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/ngen-build/action.yaml b/.github/actions/ngen-build/action.yaml index a963deb3b1..ec24dc7b3a 100644 --- a/.github/actions/ngen-build/action.yaml +++ b/.github/actions/ngen-build/action.yaml @@ -199,8 +199,8 @@ runs: if [ ${{ runner.os }} == 'macOS' ] then echo "fun:PyType_FromMetaclass" > /tmp/asan_ignore.txt - export CFLAGS="$CFLAGS -fsanitize-ignorelist=/tmp/asan_ignore.txt" - export CXXFLAGS="$CXXFLAGS -fsanitize-ignorelist=/tmp/asan_ignore.txt" + export CFLAGS="$CFLAGS -fsanitize-ignorelist=/tmp/asan_ignore.txt -fno-common" + export CXXFLAGS="$CXXFLAGS -fsanitize-ignorelist=/tmp/asan_ignore.txt -fno-common" fi . .venv/bin/activate [ ! -d "$BOOST_ROOT" ] && echo "Error: no Boost root found at $BOOST_ROOT" && exit 1 From b1fe377d1526f3d9b78139e2bf95126d301444c5 Mon Sep 17 00:00:00 2001 From: Robert Bartel Date: Tue, 18 Nov 2025 12:58:20 -0500 Subject: [PATCH 40/58] Deactivate ODR violation detection for test_pet also. --- .github/workflows/test_and_validate.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_and_validate.yml b/.github/workflows/test_and_validate.yml index 29b9e10afe..adc1674073 100644 --- a/.github/workflows/test_and_validate.yml +++ b/.github/workflows/test_and_validate.yml @@ -76,7 +76,9 @@ jobs: build-cores: ${{ env.LINUX_NUM_PROC_CORES }} - name: Run Tests - run: ./cmake_build/test/compare_pet + run: | + export ASAN_OPTIONS=${ASAN_OPTIONS}:detect_odr_violation=0 + ./cmake_build/test/compare_pet timeout-minutes: 15 - name: Clean Up From c9ad196d5c31cac92a0aab3ac489d0fc0adb07d3 Mon Sep 17 00:00:00 2001 From: Robert Bartel Date: Wed, 3 Dec 2025 15:10:25 -0500 Subject: [PATCH 41/58] Tweak Actions to use -O0 in Mac runner. Use no optimizations inside Mac runners until we figure out the issue with ASan that is happening in that setup. --- .github/actions/ngen-build/action.yaml | 11 +++++++---- .github/actions/ngen-submod-build/action.yaml | 10 ++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/actions/ngen-build/action.yaml b/.github/actions/ngen-build/action.yaml index ec24dc7b3a..ba01ed6399 100644 --- a/.github/actions/ngen-build/action.yaml +++ b/.github/actions/ngen-build/action.yaml @@ -194,13 +194,16 @@ runs: # which causes cmake configuration to fail. run: | export BOOST_ROOT="$(pwd)/boost_1_86_0" - export CFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -Werror" - export CXXFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" + export CFLAGS="-fsanitize=address -g -fno-omit-frame-pointer -Werror" + export CXXFLAGS="-fsanitize=address -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" if [ ${{ runner.os }} == 'macOS' ] then echo "fun:PyType_FromMetaclass" > /tmp/asan_ignore.txt - export CFLAGS="$CFLAGS -fsanitize-ignorelist=/tmp/asan_ignore.txt -fno-common" - export CXXFLAGS="$CXXFLAGS -fsanitize-ignorelist=/tmp/asan_ignore.txt -fno-common" + export CFLAGS="$CFLAGS -O0 -fsanitize-ignorelist=/tmp/asan_ignore.txt -fno-common" + export CXXFLAGS="$CXXFLAGS -O0 -fsanitize-ignorelist=/tmp/asan_ignore.txt -fno-common" + else + export CFLAGS="$CFLAGS -O1" + export CXXFLAGS="$CXXFLAGS -O1" fi . .venv/bin/activate [ ! -d "$BOOST_ROOT" ] && echo "Error: no Boost root found at $BOOST_ROOT" && exit 1 diff --git a/.github/actions/ngen-submod-build/action.yaml b/.github/actions/ngen-submod-build/action.yaml index 0c8fac3153..f24fc5ba93 100644 --- a/.github/actions/ngen-submod-build/action.yaml +++ b/.github/actions/ngen-submod-build/action.yaml @@ -50,8 +50,14 @@ runs: - name: Cmake Initialization id: cmake_init run: | - echo CFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -Werror" >> $GITHUB_ENV - echo CXXFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" >> $GITHUB_ENV + if [ ${{ runner.os }} == 'macOS' ] + then + export OPT_LEVEL_FLAG="-O0" + else + export OPT_LEVEL_FLAG="-O1" + fi + echo CFLAGS="-fsanitize=address ${OPT_LEVEL_FLAG:?Optimization flag var not set} -g -fno-omit-frame-pointer -Werror" >> $GITHUB_ENV + echo CXXFLAGS="-fsanitize=address ${OPT_LEVEL_FLAG:?Optimization flag var not set} -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" >> $GITHUB_ENV cmake -B ${{ inputs.mod-dir}}/${{ inputs.build-dir }} -S ${{ inputs.mod-dir }} ${{ inputs.cmake-flags }} echo "build-dir=$(echo ${{ inputs.mod-dir}}/${{ inputs.build-dir }})" >> $GITHUB_OUTPUT shell: bash From 2be692b8b772367a56f4c86aa670abed50b4af9a Mon Sep 17 00:00:00 2001 From: Austin Raney Date: Tue, 13 Jan 2026 13:47:53 -0500 Subject: [PATCH 42/58] ci: don't cache python venv --- .github/actions/ngen-build/action.yaml | 27 ++------------------------ 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/.github/actions/ngen-build/action.yaml b/.github/actions/ngen-build/action.yaml index ba01ed6399..16596e2560 100644 --- a/.github/actions/ngen-build/action.yaml +++ b/.github/actions/ngen-build/action.yaml @@ -130,26 +130,9 @@ runs: echo "PIP_CONSTRAINT=$GITHUB_WORKSPACE/constraints.txt" >> $GITHUB_ENV shell: bash - - name: Cache Python Dependencies - id: cache-py3-dependencies - uses: actions/cache@v3 - with: - path: .venv - key: ${{ runner.os }}-python-deps - - name: Get Numpy Python Dependency - # Tried conditioning the cache/install of python with an extra check: - # inputs.use_python != 'OFF' && - # but what happens is that a runner not requiring python will create an empty cache - # and future runners will pull that and then fail... - # What we could do is try to create a master `requirements.txt` - # and/or a `test_requirements.txt` file that we can build the hash key from - # and read it from the repo...but we still have to always initialize the cache - # regardless of whether a given runner uses it, to avoid another runner failing to - # find it. Or just initialize this minimum requirement of numpy, and let the venv - # grow based on other runners needs, effectively building the cache with each new addition if: | - steps.cache-py3-dependencies.outputs.cache-hit != 'true' + inputs.use_python != 'OFF' run: | python3 -m venv .venv . .venv/bin/activate @@ -159,14 +142,8 @@ runs: shell: bash - name: Init Additional Python Dependencies - # Don't condition additonal installs on a cache hit - # What will happen, however, is that the venv will get updated - # and thus the cache will get updated - # so any pip install will find modules already installed... - # if: | - # inputs.additional_python_requirements != '' && - # steps.cache-py3-dependencies.outputs.cache-hit != 'true' if: | + inputs.use_python != 'OFF' && inputs.additional_python_requirements != '' run: | python3 -m venv .venv From 5408e56aac73a9bc4dac7e8f4500c3ed50ef2c55 Mon Sep 17 00:00:00 2001 From: Austin Raney Date: Tue, 13 Jan 2026 14:27:33 -0500 Subject: [PATCH 43/58] ci: add support for uv python tooling --- .github/actions/ngen-build/action.yaml | 36 +++++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/actions/ngen-build/action.yaml b/.github/actions/ngen-build/action.yaml index 16596e2560..c4558969b0 100644 --- a/.github/actions/ngen-build/action.yaml +++ b/.github/actions/ngen-build/action.yaml @@ -128,16 +128,32 @@ runs: run: | echo "numpy<2.0" > $GITHUB_WORKSPACE/constraints.txt echo "PIP_CONSTRAINT=$GITHUB_WORKSPACE/constraints.txt" >> $GITHUB_ENV + echo "UV_CONSTRAINT=$GITHUB_WORKSPACE/constraints.txt" >> $GITHUB_ENV + shell: bash + + - name: Add uv and Native Python Tooling Translations + if: | + inputs.use_python != 'OFF' + run: | + echo 'ACTIVATE_VENV_IF_USE_PYTHON=source .venv/bin/activate' >> $GITHUB_ENV + if command -v uv &>/dev/null; then + echo 'CREATE_VENV=uv venv .venv' >> $GITHUB_ENV + echo 'PIP_INSTALL=uv pip install' >> $GITHUB_ENV + else + echo 'CREATE_VENV=python3 -m venv .venv' >> $GITHUB_ENV + echo 'PIP_INSTALL=pip install' >> $GITHUB_ENV + fi shell: bash - name: Get Numpy Python Dependency if: | inputs.use_python != 'OFF' run: | - python3 -m venv .venv - . .venv/bin/activate - pip install pip - pip install numpy + $CREATE_VENV + $ACTIVATE_VENV_IF_USE_PYTHON + echo $(which python3) + $PIP_INSTALL pip + $PIP_INSTALL numpy deactivate shell: bash @@ -146,9 +162,8 @@ runs: inputs.use_python != 'OFF' && inputs.additional_python_requirements != '' run: | - python3 -m venv .venv - . .venv/bin/activate - pip install -r ${{ inputs.additional_python_requirements }} + $ACTIVATE_VENV_IF_USE_PYTHON + $PIP_INSTALL -r ${{ inputs.additional_python_requirements }} deactivate shell: bash @@ -182,7 +197,8 @@ runs: export CFLAGS="$CFLAGS -O1" export CXXFLAGS="$CXXFLAGS -O1" fi - . .venv/bin/activate + # NOTE: this is not defined if inputs.use_python != 'ON' + $ACTIVATE_VENV_IF_USE_PYTHON [ ! -d "$BOOST_ROOT" ] && echo "Error: no Boost root found at $BOOST_ROOT" && exit 1 echo "Cmake Version:" which cmake @@ -208,8 +224,8 @@ runs: # Build Targets # Disable leak detection during test enumeration export ASAN_OPTIONS=detect_leaks=false - # Activate venv so that test discovery run during build works - . .venv/bin/activate + # NOTE: this is not defined if inputs.use_python != 'ON' + $ACTIVATE_VENV_IF_USE_PYTHON cmake --build ${{ inputs.build-dir }} --target ${{ inputs.targets }} -- -j ${{ inputs.build-cores }} shell: bash From a29e64b0c36519a731657f75ed2c82ebe3378921 Mon Sep 17 00:00:00 2001 From: Austin Raney Date: Thu, 29 Jan 2026 13:37:07 -0500 Subject: [PATCH 44/58] fix: sqlite3 cmake import target Use SQLite::SQLite3 target defined by FindSQLite3 module. https://cmake.org/cmake/help/latest/module/FindSQLite3.html#imported-targets --- src/geopackage/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geopackage/CMakeLists.txt b/src/geopackage/CMakeLists.txt index 7786671af3..afe1d0c822 100644 --- a/src/geopackage/CMakeLists.txt +++ b/src/geopackage/CMakeLists.txt @@ -9,4 +9,4 @@ add_library(geopackage proj.cpp add_library(NGen::geopackage ALIAS geopackage) target_include_directories(geopackage PUBLIC ${PROJECT_SOURCE_DIR}/include/geopackage) target_include_directories(geopackage PUBLIC ${PROJECT_SOURCE_DIR}/include/utilities) -target_link_libraries(geopackage PUBLIC NGen::geojson Boost::boost sqlite3) +target_link_libraries(geopackage PUBLIC NGen::geojson Boost::boost SQLite::SQLite3) From 9ad6b7bf561fb2f96065511b382bc43a83167f10 Mon Sep 17 00:00:00 2001 From: Austin Raney Date: Thu, 29 Jan 2026 14:50:32 -0500 Subject: [PATCH 45/58] fix: iso_c_bmi_fortran cmake install of pc files Use CMAKE_CURRENT_BINARY_DIR over CMAKE_BINARY_DIR reference location of pc files regardless of build context. https://cmake.org/cmake/help/latest/variable/CMAKE_CURRENT_BINARY_DIR.html#variable:CMAKE_CURRENT_BINARY_DIR --- extern/iso_c_fortran_bmi/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/iso_c_fortran_bmi/CMakeLists.txt b/extern/iso_c_fortran_bmi/CMakeLists.txt index b55642e83f..9c1a17b2a8 100644 --- a/extern/iso_c_fortran_bmi/CMakeLists.txt +++ b/extern/iso_c_fortran_bmi/CMakeLists.txt @@ -33,5 +33,5 @@ install(TARGETS iso_c_bmi install(DIRECTORY ${CMAKE_Fortran_MODULE_DIRECTORY} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) configure_file(iso_c_bmi.pc.in iso_c_bmi.pc @ONLY) -install(FILES ${CMAKE_BINARY_DIR}/iso_c_bmi.pc +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/iso_c_bmi.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) From 8ec361e146e5506a119599bc6dd2755aeab45a2e Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Wed, 4 Feb 2026 08:45:20 -0700 Subject: [PATCH 46/58] fix(mpi): prevent potential deadlock scenario --- .github/workflows/test_and_validate.yml | 2 +- .../core/nexus/HY_PointHydroNexusRemote.hpp | 1 + src/core/nexus/HY_PointHydroNexusRemote.cpp | 101 ++-- test/core/nexus/NexusRemoteTests.cpp | 434 ++++++++++++++++++ 4 files changed, 502 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test_and_validate.yml b/.github/workflows/test_and_validate.yml index adc1674073..2a4757c4f1 100644 --- a/.github/workflows/test_and_validate.yml +++ b/.github/workflows/test_and_validate.yml @@ -106,7 +106,7 @@ jobs: use_mpi: 'ON' - name: run_tests - run: mpirun --allow-run-as-root -np 2 ./cmake_build/test/test_remote_nexus + run: mpirun --allow-run-as-root --oversubscribe -np 4 ./cmake_build/test/test_remote_nexus timeout-minutes: 15 - name: Clean Up diff --git a/include/core/nexus/HY_PointHydroNexusRemote.hpp b/include/core/nexus/HY_PointHydroNexusRemote.hpp index 269c242c8d..4d73632058 100644 --- a/include/core/nexus/HY_PointHydroNexusRemote.hpp +++ b/include/core/nexus/HY_PointHydroNexusRemote.hpp @@ -71,6 +71,7 @@ class HY_PointHydroNexusRemote : public HY_PointHydroNexus communication_type get_communicator_type() { return type; } private: + void post_receives(); void process_communications(); int world_rank; diff --git a/src/core/nexus/HY_PointHydroNexusRemote.cpp b/src/core/nexus/HY_PointHydroNexusRemote.cpp index a703dfdc71..1ee91e50f9 100644 --- a/src/core/nexus/HY_PointHydroNexusRemote.cpp +++ b/src/core/nexus/HY_PointHydroNexusRemote.cpp @@ -18,7 +18,7 @@ void MPI_Handle_Error(int status) } else { - MPI_Abort(MPI_COMM_WORLD,1); + MPI_Abort(MPI_COMM_WORLD, status); } } @@ -117,6 +117,29 @@ HY_PointHydroNexusRemote::~HY_PointHydroNexusRemote() } } +void HY_PointHydroNexusRemote::post_receives() +{ + // Post receives if not already posted (for pure receiver nexuses) + if (stored_receives.empty()) + { + for (int rank : upstream_ranks) + { + stored_receives.push_back({}); + stored_receives.back().buffer = std::make_shared(); + int tag = extract(id); + + MPI_Handle_Error(MPI_Irecv( + stored_receives.back().buffer.get(), + 1, + time_step_and_flow_type, + rank, + tag, + MPI_COMM_WORLD, + &stored_receives.back().mpi_request)); + } + } +} + double HY_PointHydroNexusRemote::get_downstream_flow(std::string catchment_id, time_step_t t, double percent_flow) { double remote_flow = 0.0; @@ -130,31 +153,15 @@ double HY_PointHydroNexusRemote::get_downstream_flow(std::string catchment_id, t } else if ( type == receiver || type == sender_receiver ) { - for ( int rank : upstream_ranks ) - { - int status; - - stored_receives.resize(stored_receives.size() + 1); - stored_receives.back().buffer = std::make_shared(); - - int tag = extract(id); - - //Receive downstream_flow from Upstream Remote Nexus to this Downstream Remote Nexus - status = MPI_Irecv( - stored_receives.back().buffer.get(), - 1, - time_step_and_flow_type, - rank, - tag, - MPI_COMM_WORLD, - &stored_receives.back().mpi_request); - - MPI_Handle_Error(status); - - //std::cerr << "Creating receive with target_rank=" << rank << " on tag=" << tag << "\n"; - } - - //std::cerr << "Waiting on receives\n"; + post_receives(); + // Wait for receives to complete + // This ensures all upstream flows are received before returning + // and that we have matched all sends with receives for a given time step. + // As long as the functions are called appropriately, e.g. one call to + // `add_upstream_flow` per upstream catchment per time step, followed + // by a call to `get_downstream_flow` for each downstream catchment per time step, + // this loop will terminate and ensures the synchronization of flows between + // ranks. while ( stored_receives.size() > 0 ) { process_communications(); @@ -167,6 +174,28 @@ double HY_PointHydroNexusRemote::get_downstream_flow(std::string catchment_id, t void HY_PointHydroNexusRemote::add_upstream_flow(double val, std::string catchment_id, time_step_t t) { + // Process any completed communications to free resources + // If no communications are pending, this call will do nothing. + process_communications(); + // NOTE: It is possible for a partition to get "too far" ahead since the sends are now + // truely asynchronous. For pure receivers and sender_receivers, this isn't a problem + // because the get_downstream_flow function will block until all receives are processed. + // However, for pure senders, this could be a problem. + // We can use this spinlock here to limit how far ahead a partition can get. + // in this case, approximately 100 time steps per downstream catchment... + while( stored_sends.size() > downstream_ranks.size()*100 ) + { + process_communications(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + // Post receives before sending to prevent deadlock + // When stored_receives is empty, we need to post for incoming messages + if ((type == receiver || type == sender_receiver) && stored_receives.empty()) + { + post_receives(); + } + // first add flow to local copy HY_PointHydroNexus::add_upstream_flow(val, catchment_id, t); @@ -205,23 +234,25 @@ void HY_PointHydroNexusRemote::add_upstream_flow(double val, std::string catchme int tag = extract(id); //Send downstream_flow from this Upstream Remote Nexus to the Downstream Remote Nexus - MPI_Isend( + MPI_Handle_Error( + MPI_Isend( stored_sends.back().buffer.get(), 1, time_step_and_flow_type, *downstream_ranks.begin(), //TODO currently only support a SINGLE downstream message pairing tag, MPI_COMM_WORLD, - &stored_sends.back().mpi_request); - - //std::cerr << "Creating send with target_rank=" << *downstream_ranks.begin() << " on tag=" << tag << "\n"; + &stored_sends.back().mpi_request) + ); + //std::cerr << "Creating send with target_rank=" << *downstream_ranks.begin() << " on tag=" << tag << "\n"; - while ( stored_sends.size() > 0 ) - { - process_communications(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } + // Send is async, the next call to add_upstream_flow will test and ensure the send has completed + // and free the memory associated with the send. + // This prevents a potential deadlock situation where a send isn't able to complete + // because the remote receiver is also trying to send and the underlying mpi buffers/protocol + // are forced into a rendevous protocol. So we ensure that we always post receives before sends. + // and that we always test for completed sends before freeing the memory associated with the send. } } } diff --git a/test/core/nexus/NexusRemoteTests.cpp b/test/core/nexus/NexusRemoteTests.cpp index 25964759b2..8904602b09 100644 --- a/test/core/nexus/NexusRemoteTests.cpp +++ b/test/core/nexus/NexusRemoteTests.cpp @@ -683,4 +683,438 @@ TEST_F(Nexus_Remote_Test, DISABLED_TestTree1) } + +/****************************************************************************** + * MPI DEADLOCK TESTS + * ================== + * As of commit 9ad6b7bf561fb2f96065511b382bc43a83167f10 + * A potential MPI deadlock scenario was discovered in the HY_PointHydroNexusRemote class. + * + * These tests demonstrate and verify a fix for MPI communication issues + * observed in production when running large domains with many partitions. + * + * ============================================================================= + * What can be shown: + * ============================================================================= + * + * 1. Deadlock prone code: + * The original HY_PointHydroNexusRemote::add_upstream_flow() does: + * a. MPI_Isend() - non-blocking, returns immediately + * b. while(stored_sends.size() > 0) { MPI_Test(); } - BLOCKS until + * the send is confirmed complete by MPI + * + * This pattern is problematic because it blocks progress until the send + * completes, preventing the code from posting receives. + * + * 2. WITH FORCED RENDEZVOUS, THIS PATTERN DEADLOCKS: + * When we force rendezvous protocol using --mca btl_tcp_eager_limit 80, + * the buggy pattern reliably deadlocks. Rendezvous requires a posted + * MPI_Irecv before the send can complete. + * + * 3. PRODUCTION RUNS WERE HANGING: + * Large-scale runs with 384+ partitions and 831K catchments __across multiple nodes__ + * were observed to be hanging (one node was making progress while others were not). + * + * 4. THE FIX IS CORRECT: + * Pre-posting receives (calling MPI_Irecv before MPI_Isend) is the standard + * MPI best practice and eliminates any rendezvous-related deadlock risk. + * + * ============================================================================= + * Assumptions about this fix that are hard to confirm: + * ============================================================================= + * + * We ASSUMED that production hangs occurred because: + * - High connection count (~38,000 connections) exhausted the eager buffer pool + * - This forced MPI to use rendezvous protocol even for small messages + * - Rendezvous + buggy pattern = deadlock + * + * HOWEVER: MPI documentation consistently states that rendezvous protocol is + * triggered by MESSAGE SIZE exceeding eager_limit, NOT by buffer pool exhaustion. + * We cannot find documentation supporting the "pool exhaustion triggers + * rendezvous" theory. + * + * Other possibilities for production hangs (unconfirmed) + * - TCP buffer exhaustion: if the sender's socket buffer fills up before + * the receiver can process messages, subsequent sends can block. + * And if two or more nodes get into this state, they can deadlock. + * - Network fabric issues at scale + * - Something else entirely that our fix happened to address + * + * ============================================================================= + * THE FIX IS STILL CORRECT: + * ============================================================================= + * + * Regardless of the exact production trigger, pre-posting receives is: + * 1. MPI best practice for avoiding deadlock + * 2. Required for correctness under rendezvous protocol + * 3. Harmless under eager protocol (just posts receives earlier) + * + * The fix eliminates a class of potential deadlocks even if we're uncertain + * about the exact mechanism that triggered the production hang. + * + * ============================================================================= + * TRIGGERING THE DEADLOCK IN TESTS: + * ============================================================================= + * + * We use --mca btl_tcp_eager_limit 80 to FORCE rendezvous protocol per-message. + * This is a TEST WORKAROUND that demonstrates the buggy pattern CAN deadlock. + * + * This is NOT necessarily the exact production failure mode - it's a way to + * reliably trigger the deadlock pattern in a controlled test environment. + * + * To reproduce in tests, force tcp communcation and set the eager limit to + * the minimum value (80 bytes -- openmpi_info may show different limits for + * different BTLs/environments). + * + * mpirun --mca btl tcp,self --mca btl_tcp_eager_limit 80 ... + * + * Parameters: + * --mca btl tcp,self : Disable shared memory, use TCP only + * --mca btl_tcp_eager_limit 80 : Force rendezvous per-message (minimum value) + * + * NOTE: Shared memory BTL uses copy-based communication that doesn't require + * a synchronous handshake, so --mca btl_sm_eager_limit 80 will NOT trigger + * the deadlock. + * + ******************************************************************************/ + + +/** + * ============================================================================= + * DISABLED_TestRawMpiDeadlockPattern + * ============================================================================= + * + * PURPOSE: Document and demonstrate the MPI communication pattern that causes + * deadlock when rendezvous protocol is triggered. + * This is the pattern that ngen used up to and including + * commit 9ad6b7bf561fb2f96065511b382bc43a83167f10 + * This test uses RAW MPI calls (not the HY_PointHydroNexusRemote class) to + * clearly illustrate the problematic pattern that existed in the original code. + * + * ============================================================================= + * THE PROBLEMATIC PATTERN (from original add_upstream_flow): + * ============================================================================= + * 1. MPI_Isend (non-blocking send) + * 2. Loop on MPI_Test waiting for send to complete <-- BLOCKS HERE + * 3. MPI_Irecv (never reached if step 2 blocks) + * + * ============================================================================= + * WHY NGEN PARTITIONS CREATE BIDIRECTIONAL COMMUNICATION: + * ============================================================================= + * + * Real hydrological networks are complex. When we partition the domain, the + * partition boundary cuts ACROSS the drainage network, not along it. + * This creates BIDIRECTIONAL communication between partitions. + * + * Analysis of CONUS (384 partitions, 831K catchments) confirms: + * - 182/384 partitions (47%) have BIDIRECTIONAL communication + * + * ============================================================================= + * TEST TOPOLOGY (simplified bidirectional chain): + * ============================================================================= + * + * Rank 0 <────> Rank 1 <────> Rank 2 <────> Rank 3 + * + * Each rank both SENDS to AND RECEIVES from its neighbors. + * + * ============================================================================= + * TO REPRODUCE DEADLOCK: + * ============================================================================= + * timeout 10 mpirun --mca btl tcp,self --mca btl_tcp_eager_limit 80 -n 4 \ + * ./test/test_remote_nexus --gtest_filter="*TestRawMpiDeadlockPattern*" \ + * --gtest_also_run_disabled_tests + * + * EXPECTED: Exit code 124 (timeout killed it) - confirms true deadlock + * + * ============================================================================= + */ +TEST_F(Nexus_Remote_Test, DISABLED_TestRawMpiDeadlockPattern) +{ + if (mpi_num_procs < 4) { + GTEST_SKIP() << "Requires at least 4 MPI ranks to demonstrate bidirectional deadlock"; + } + + // Bidirectional chain topology + std::vector downstream_ranks; + std::vector upstream_ranks; + + if (mpi_rank == 0) { + downstream_ranks.push_back(1); + upstream_ranks.push_back(1); + } else if (mpi_rank == 1) { + downstream_ranks.push_back(0); + downstream_ranks.push_back(2); + upstream_ranks.push_back(0); + upstream_ranks.push_back(2); + } else if (mpi_rank == 2) { + downstream_ranks.push_back(1); + downstream_ranks.push_back(3); + upstream_ranks.push_back(1); + upstream_ranks.push_back(3); + } else if (mpi_rank == 3) { + downstream_ranks.push_back(2); + upstream_ranks.push_back(2); + } + + bool is_sender = !downstream_ranks.empty(); + bool is_receiver = !upstream_ranks.empty(); + + // Create MPI datatype for our message + struct message_t { + long time_step; + long id; + double flow; + }; + + MPI_Datatype msg_type; + int counts[3] = {1, 1, 1}; + MPI_Aint displacements[3] = {0, sizeof(long), 2 * sizeof(long)}; + MPI_Datatype types[3] = {MPI_LONG, MPI_LONG, MPI_DOUBLE}; + MPI_Type_create_struct(3, counts, displacements, types, &msg_type); + MPI_Type_commit(&msg_type); + + const int NUM_MESSAGES = 3500; + const int TAG_BASE = 1000; + + std::cerr << "Rank " << mpi_rank << ": Starting bidirectional deadlock pattern\n"; + + MPI_Barrier(MPI_COMM_WORLD); + + // Storage for async operations + std::map> send_buffers; + std::map> recv_buffers; + std::map> send_requests; + std::map> recv_requests; + + for (int r : downstream_ranks) { + send_buffers[r].resize(NUM_MESSAGES); + send_requests[r].resize(NUM_MESSAGES); + } + for (int r : upstream_ranks) { + recv_buffers[r].resize(NUM_MESSAGES); + recv_requests[r].resize(NUM_MESSAGES); + } + + // Timing asymmetry emulates "work" that causes ranks to run at different "speeds" + if (mpi_rank == 1) { + volatile double dummy = 0.0; + for (int i = 0; i < 100000000; ++i) { + dummy += std::sin(i * 0.0001) * std::cos(i * 0.0002); + } + } + + // THE PROBLEMATIC PATTERN: All ranks try to complete sends BEFORE posting receives + if (is_sender) + { + for (int downstream : downstream_ranks) + { + for (int i = 0; i < NUM_MESSAGES; ++i) + { + send_buffers[downstream][i] = {i, mpi_rank, 100.0 + i}; + int tag = TAG_BASE + mpi_rank * 10000 + downstream * 100 + i; + + MPI_Isend(&send_buffers[downstream][i], 1, msg_type, downstream, tag, + MPI_COMM_WORLD, &send_requests[downstream][i]); + + // BLOCKING WAIT - THIS IS THE "BUG"! + // Under eager protocols, this test returns immediately + // Under rendezvous protocols, this test blocks until + // the receiver posts a matching Irecv. + // Similar logic applies to a full TCP buffer, MPI gets blocked waiting + // for TCP buffer to free up, which requires the receiver to + // read the data. + int flag = 0; + while (!flag) { + MPI_Test(&send_requests[downstream][i], &flag, MPI_STATUS_IGNORE); + } + } + } + } + + // Post receives - NEVER REACHED IN DEADLOCK + if (is_receiver) + { + for (int upstream : upstream_ranks) + { + for (int i = 0; i < NUM_MESSAGES; ++i) + { + int tag = TAG_BASE + upstream * 10000 + mpi_rank * 100 + i; + MPI_Irecv(&recv_buffers[upstream][i], 1, msg_type, upstream, tag, + MPI_COMM_WORLD, &recv_requests[upstream][i]); + } + MPI_Waitall(NUM_MESSAGES, recv_requests[upstream].data(), MPI_STATUSES_IGNORE); + } + } + + MPI_Type_free(&msg_type); + MPI_Barrier(MPI_COMM_WORLD); + std::cerr << "Rank " << mpi_rank << ": Test passed (eager buffer was sufficient)\n"; +} + + +/** + * ============================================================================= + * TestRemoteNexusDeadlockFree + * ============================================================================= + * + * PURPOSE: Verify that HY_PointHydroNexusRemote does NOT deadlock, even with + * extremely small MPI eager buffers that force rendezvous protocol. + * + * This test uses the SAME BIDIRECTIONAL topology as DISABLED_TestRawMpiDeadlockPattern + * but uses the HY_PointHydroNexusRemote class instead of raw MPI calls. + * + * ============================================================================= + * THE FIX: AUTO-POSTED RECEIVES + * ============================================================================= + * + * The HY_PointHydroNexusRemote class now auto-posts MPI_Irecv BEFORE sending. + * This breaks the deadlock cycle because peers can always complete their sends. + * + * ============================================================================= + * TO RUN (with small eager buffer to force rendezvous): + * ============================================================================= + * mpirun --mca btl tcp,self --mca btl_tcp_eager_limit 80 -n 4 \ + * ./test/test_remote_nexus --gtest_filter="*TestRemoteNexusDeadlockFree*" + * + * EXPECTED: Exit code 0 - test passes without deadlock + * + * ============================================================================= + */ +TEST_F(Nexus_Remote_Test, TestRemoteNexusDeadlockFree) +{ + if (mpi_num_procs < 4) { + GTEST_SKIP() << "Requires at least 4 MPI ranks for bidirectional topology"; + } + + // Same bidirectional topology as the deadlock test + std::vector downstream_ranks; + std::vector upstream_ranks; + + if (mpi_rank == 0) { + downstream_ranks.push_back(1); + upstream_ranks.push_back(1); + } else if (mpi_rank == 1) { + downstream_ranks.push_back(0); + downstream_ranks.push_back(2); + upstream_ranks.push_back(0); + upstream_ranks.push_back(2); + } else if (mpi_rank == 2) { + downstream_ranks.push_back(1); + downstream_ranks.push_back(3); + upstream_ranks.push_back(1); + upstream_ranks.push_back(3); + } else if (mpi_rank == 3) { + downstream_ranks.push_back(2); + upstream_ranks.push_back(2); + } + + bool is_sender = !downstream_ranks.empty(); + bool is_receiver = !upstream_ranks.empty(); + + const int NUM_CONNECTIONS = 50; + + std::cerr << "Rank " << mpi_rank << ": Setting up bidirectional topology\n"; + + // Create sender nexuses for each downstream rank + std::map>> senders; + for (int downstream : downstream_ranks) + { + for (int i = 0; i < NUM_CONNECTIONS; ++i) + { + int nex_num = mpi_rank * 100000 + downstream * 1000 + i; + std::string nex_id = "nex-" + std::to_string(nex_num); + std::string my_cat = "cat-send-" + std::to_string(mpi_rank) + "-" + std::to_string(downstream) + "-" + std::to_string(i); + std::string their_cat = "cat-recv-" + std::to_string(downstream) + "-" + std::to_string(mpi_rank) + "-" + std::to_string(i); + + HY_PointHydroNexusRemote::catcment_location_map_t loc_map; + loc_map[their_cat] = downstream; + + std::vector receiving = {their_cat}; + std::vector contributing = {my_cat}; + + senders[downstream].push_back(std::make_shared( + nex_id, receiving, contributing, loc_map)); + } + } + + // Create receiver nexuses for each upstream rank + std::map>> receivers; + for (int upstream : upstream_ranks) + { + for (int i = 0; i < NUM_CONNECTIONS; ++i) + { + int nex_num = upstream * 100000 + mpi_rank * 1000 + i; + std::string nex_id = "nex-" + std::to_string(nex_num); + std::string their_cat = "cat-send-" + std::to_string(upstream) + "-" + std::to_string(mpi_rank) + "-" + std::to_string(i); + std::string my_cat = "cat-recv-" + std::to_string(mpi_rank) + "-" + std::to_string(upstream) + "-" + std::to_string(i); + + HY_PointHydroNexusRemote::catcment_location_map_t loc_map; + loc_map[their_cat] = upstream; + + std::vector receiving = {my_cat}; + std::vector contributing = {their_cat}; + + receivers[upstream].push_back(std::make_shared( + nex_id, receiving, contributing, loc_map)); + } + } + + MPI_Barrier(MPI_COMM_WORLD); + + // Timing asymmetry emulates "work" that causes ranks to run at different "speeds" + if (mpi_rank == 1) { + volatile double dummy = 0.0; + for (int i = 0; i < 100000000; ++i) { + dummy += std::sin(i * 0.0001) * std::cos(i * 0.0002); + } + } + + long ts = 0; + double flow_value = 42.0; + + // Send all flows - with the fix, receives are auto-posted before sending + if (is_sender) + { + std::cerr << "Rank " << mpi_rank << ": Starting sends (receives auto-posted by fix)\n"; + + for (auto& kv : senders) + { + int downstream = kv.first; + auto& nexuses = kv.second; + for (int i = 0; i < NUM_CONNECTIONS; ++i) + { + std::string my_cat = "cat-send-" + std::to_string(mpi_rank) + "-" + std::to_string(downstream) + "-" + std::to_string(i); + nexuses[i]->add_upstream_flow(flow_value, my_cat, ts); + } + } + std::cerr << "Rank " << mpi_rank << ": All sends completed!\n"; + } + + // Receive all flows + if (is_receiver) + { + std::cerr << "Rank " << mpi_rank << ": Receiving from upstream\n"; + for (auto& kv : receivers) + { + int upstream = kv.first; + auto& nexuses = kv.second; + for (int i = 0; i < NUM_CONNECTIONS; ++i) + { + std::string my_cat = "cat-recv-" + std::to_string(mpi_rank) + "-" + std::to_string(upstream) + "-" + std::to_string(i); + double received = nexuses[i]->get_downstream_flow(my_cat, ts, 100.0); + ASSERT_DOUBLE_EQ(flow_value, received); + } + } + } + + senders.clear(); + receivers.clear(); + + MPI_Barrier(MPI_COMM_WORLD); + std::cerr << "Rank " << mpi_rank << ": Test PASSED - no deadlock with remote nexus\n"; +} + + +//#endif // NGEN_MPI_TESTS_ACTIVE + //#endif // NGEN_MPI_TESTS_ACTIVE From 54c9fe91a36259ec9af6bfb386772f56106f913f Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Wed, 4 Feb 2026 11:54:33 -0700 Subject: [PATCH 47/58] fix(mpi): better handling of pending communication in remote nexus destructor --- src/core/nexus/HY_PointHydroNexusRemote.cpp | 29 ++++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/core/nexus/HY_PointHydroNexusRemote.cpp b/src/core/nexus/HY_PointHydroNexusRemote.cpp index 1ee91e50f9..6e96277e65 100644 --- a/src/core/nexus/HY_PointHydroNexusRemote.cpp +++ b/src/core/nexus/HY_PointHydroNexusRemote.cpp @@ -92,9 +92,9 @@ HY_PointHydroNexusRemote::HY_PointHydroNexusRemote(std::string nexus_id, Catchme HY_PointHydroNexusRemote::~HY_PointHydroNexusRemote() { - long wait_time = 0; - - // This destructore might be called after MPI_Finalize so do not attempt communication if + const unsigned int timeout = 120000; // timeout threshold in milliseconds + unsigned int wait_time = 0; + // This destructor might be called after MPI_Finalize so do not attempt communication if // this has occured int mpi_finalized; MPI_Finalized(&mpi_finalized); @@ -105,15 +105,24 @@ HY_PointHydroNexusRemote::~HY_PointHydroNexusRemote() process_communications(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - - wait_time += 1; - - if ( wait_time > 120000 ) + if( wait_time < timeout && wait_time > 0 ){ // don't sleep if first call clears comms! + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + else { - // TODO log warning message that some comunications could not complete - + std::cerr << "HY_PointHydroNexusRemote: "<< id + << " destructor timed out after " << timeout/1000 + << " seconds waiting on pending MPI communications\n"; + // The return is is probably best, logging the error. + // There is no good way to recover from this. + // Throwing an exception from destructors is generally not a good idea + // as it can lead to undefined behavior. + // and using std::exit forces the program to terminate immediately, + // even if this situation is recoverable/acceptable in some cases. + return; } + wait_time += 1; + MPI_Finalized(&mpi_finalized); } } From 5e3ca56fae32466d743806eae1df6544a279901e Mon Sep 17 00:00:00 2001 From: Austin Raney Date: Thu, 5 Feb 2026 18:04:26 -0500 Subject: [PATCH 48/58] chore: add ngen and partitionGenerator as cmake --install targets --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d96422026..7769a71053 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -489,3 +489,7 @@ ngen_dependent_multiline_message(NGEN_WITH_PYTHON message(STATUS "---------------------------------------------------------------------") configure_file("${NGEN_INC_DIR}/NGenConfig.h.in" "${CMAKE_CURRENT_BINARY_DIR}/include/NGenConfig.h") + +include(GNUInstallDirs) +install(TARGETS ngen OPTIONAL) +install(TARGETS partitionGenerator OPTIONAL) From cec2028c21e7cd26308f591085643fd4b7162a36 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Mon, 8 Sep 2025 10:08:14 -0700 Subject: [PATCH 49/58] Drop unused Network construction --- src/partitionGenerator.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/partitionGenerator.cpp b/src/partitionGenerator.cpp index 17cc0e1f98..7abb81c7e5 100644 --- a/src/partitionGenerator.cpp +++ b/src/partitionGenerator.cpp @@ -451,11 +451,8 @@ int main(int argc, char* argv[]) std::string link_key = "toid"; - Network catchment_network(catchment_collection, &link_key); //Assumes dendritic, can add check in network if needed. PartitionVSet catchment_part, nexus_part; - - //catchment_network.print_network(); //build the remote connections from network // read the nexus hydrofabric, reuse the catchments From bbc25f4943946c8ed07256101013671dbae413dc Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 10 Sep 2025 09:04:53 -0700 Subject: [PATCH 50/58] Print CMake root source directory in configuration summary --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7769a71053..578ca3f31f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -403,6 +403,7 @@ message(STATUS "---------------------------------------------------------------- ngen_multiline_message( "NGen version: ${ngen_VERSION}" "Build configuration summary:" +" Source directory: ${CMAKE_SOURCE_DIR}" " Generator: ${CMAKE_GENERATOR}" " Build type: ${CMAKE_BUILD_TYPE}" " System: ${CMAKE_SYSTEM_NAME}" From 600db577dd7af100fb1f6e4986a13b6e86a3c97e Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 10 Sep 2025 09:38:34 -0700 Subject: [PATCH 51/58] GeoJSON: Add definitions for a 'Sentinel' feature type besides the ones in the standard --- include/geojson/FeatureVisitor.hpp | 2 ++ include/geojson/features/FeatureBase.hpp | 21 ++++++++------ include/geojson/features/Features.hpp | 3 +- include/geojson/features/SentinelFeature.hpp | 29 ++++++++++++++++++++ 4 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 include/geojson/features/SentinelFeature.hpp diff --git a/include/geojson/FeatureVisitor.hpp b/include/geojson/FeatureVisitor.hpp index 0e85703026..a97ede9b55 100644 --- a/include/geojson/FeatureVisitor.hpp +++ b/include/geojson/FeatureVisitor.hpp @@ -9,6 +9,7 @@ namespace geojson { class MultiLineStringFeature; class MultiPolygonFeature; class CollectionFeature; + class SentinelFeature; /** * An abstract class used to operate on the children of FeatureBase @@ -27,6 +28,7 @@ namespace geojson { virtual void visit(MultiLineStringFeature *feature) = 0; virtual void visit(MultiPolygonFeature *feature) = 0; virtual void visit(CollectionFeature* feature) = 0; + virtual void visit(SentinelFeature *feature) = 0; virtual ~FeatureVisitor() = default; }; diff --git a/include/geojson/features/FeatureBase.hpp b/include/geojson/features/FeatureBase.hpp index 38aabb10cf..d81bc9539a 100644 --- a/include/geojson/features/FeatureBase.hpp +++ b/include/geojson/features/FeatureBase.hpp @@ -40,16 +40,21 @@ namespace geojson { /** * Describes a type of features + * + * These are numbered in accordance with the GeoPackage + * 'well-known binary' (WKB) feature types, other than the + * additional 'Sentinel' value defined here */ enum class FeatureType { - None, /*!< Represents an empty feature with no sort of geometry */ - Point, /*!< Represents a feature that contains a single Point geometry */ - LineString, /*!< Represents a feature that is represented by a series of interconnected points */ - Polygon, /*!< Represents a feature that is represented by a defined area */ - MultiPoint, /*!< Represents a feature that is represented by many points */ - MultiLineString, /*!< Represents a feature that is represented by multiple series of interconnected points */ - MultiPolygon, /*!< Represents a feature that is represented by multiple areas */ - GeometryCollection /*!< Represents a feature that contains a collection of different types of geometry */ + None = 0, /*!< Represents an empty feature with no sort of geometry */ + Point = 1, /*!< Represents a feature that contains a single Point geometry */ + LineString = 2, /*!< Represents a feature that is represented by a series of interconnected points */ + Polygon = 3, /*!< Represents a feature that is represented by a defined area */ + MultiPoint = 4, /*!< Represents a feature that is represented by many points */ + MultiLineString = 5, /*!< Represents a feature that is represented by multiple series of interconnected points */ + MultiPolygon = 6, /*!< Represents a feature that is represented by multiple areas */ + GeometryCollection = 7, /*!< Represents a feature that contains a collection of different types of geometry */ + Sentinel = 100 /*!< Represents a 'dummy' feature included for computational consistency */ }; /** diff --git a/include/geojson/features/Features.hpp b/include/geojson/features/Features.hpp index cb71830054..5eeb9ad5ba 100644 --- a/include/geojson/features/Features.hpp +++ b/include/geojson/features/Features.hpp @@ -9,5 +9,6 @@ #include "MultiLineStringFeature.hpp" #include "MultiPolygonFeature.hpp" #include "CollectionFeature.hpp" +#include "SentinelFeature.hpp" -#endif // GEOJSON_FEATURE_H \ No newline at end of file +#endif // GEOJSON_FEATURE_H diff --git a/include/geojson/features/SentinelFeature.hpp b/include/geojson/features/SentinelFeature.hpp new file mode 100644 index 0000000000..ea9362c254 --- /dev/null +++ b/include/geojson/features/SentinelFeature.hpp @@ -0,0 +1,29 @@ +#ifndef GEOJSON_SENTINEL_FEATURE_H +#define GEOJSON_SENTINEL_FEATURE_H + +#include "FeatureBase.hpp" +#include +#include + +#include +#include +#include +#include + +namespace geojson { + + class SentinelFeature : public FeatureBase { + public: + SentinelFeature( + std::string new_id + ) : FeatureBase(std::move(new_id)) { + this->type = geojson::FeatureType::Sentinel; + } + + void visit(FeatureVisitor& visitor) override { + visitor.visit(this); + } + }; +} + +#endif // GEOJSON_SENTINEL_FEATURE_H From 023308e13dc361017f4e1c93da5dc4f411fe701f Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 10 Sep 2025 14:50:05 -0700 Subject: [PATCH 52/58] Add sentinel wb-SENTINEL-nex-NNN features downstream of otherwise-terminal nexuses NJF removed LOG call from original commit --- src/partitionGenerator.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/partitionGenerator.cpp b/src/partitionGenerator.cpp index 7abb81c7e5..ba4e6a8bec 100644 --- a/src/partitionGenerator.cpp +++ b/src/partitionGenerator.cpp @@ -484,6 +484,29 @@ int main(int argc, char* argv[]) //Do this before linking features so that the alt ids can lookup the correct feature global_nexus_collection->update_ids("id"); global_nexus_collection->link_features_from_property(nullptr, &link_key); + + // Store these separately to avoid iterator invalidation from inserting them eagerly + std::vector> sentinels; + + for (auto& feature : *global_nexus_collection) + { + auto id = feature->get_id(); + auto type = id.substr(0,3); + if (hy_features::identifiers::isNexus(type)) { + if (feature->get_number_of_destination_features() == 0) { + std::string sentinel_id = "wb-TERMINAL_SENTINEL-" + feature->get_id(); + geojson::Feature sentinel_feature = std::make_shared(sentinel_id); + sentinels.push_back(sentinel_feature); + feature->add_destination_feature(sentinel_feature.get()); + } + } + } + + for (auto& sentinel : sentinels) + { + global_nexus_collection->add_feature(sentinel); + } + // make a global network Network global_network(global_nexus_collection); From 195677cca786de3f713feef577874eb144d51db9 Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 10 Sep 2025 14:51:00 -0700 Subject: [PATCH 53/58] Remove validation of nexus partition boundaries arbitrarily at partition cut lines --- src/partitionGenerator.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/partitionGenerator.cpp b/src/partitionGenerator.cpp index ba4e6a8bec..7f1bf1586e 100644 --- a/src/partitionGenerator.cpp +++ b/src/partitionGenerator.cpp @@ -131,8 +131,6 @@ void generate_partitions(network::Network& network, const int& num_partitions, c nexus_set.reserve(partition_size); std::string part_id, partition_str; - std::string up_nexus; - std::string down_nexus; for(const auto& catchment : network.filter("cat", network::SortOrder::TransposedDepthFirstPreorder)){ if (partition < remainder) partition_size = partition_size_plus1; @@ -159,14 +157,7 @@ void generate_partitions(network::Network& network, const int& num_partitions, c counter++; if(counter == partition_size) { - //std::cout<<"nexus "< destinations = network.get_destination_ids(catchment); - if(destinations.size() == 0){ - std::cerr<<"Error: Catchment "< Date: Wed, 10 Sep 2025 14:59:44 -0700 Subject: [PATCH 54/58] Adjust partition sizes for added sentinel features --- src/partitionGenerator.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/partitionGenerator.cpp b/src/partitionGenerator.cpp index 7f1bf1586e..25dcc95038 100644 --- a/src/partitionGenerator.cpp +++ b/src/partitionGenerator.cpp @@ -104,15 +104,16 @@ void write_remote_connections(const PartitionVSet& catchment_part, const Partiti * * @param network * @param num_partitions - * @param num_catchments - * @param catchment_part + * @param catchment_part - which catchments will be simulation on each partition + * @param nexus_part - which nexuses have contributing catchments on each partition */ -void generate_partitions(network::Network& network, const int& num_partitions, const int& num_catchments, PartitionVSet& catchment_part, - PartitionVSet& nexus_part) +void generate_partitions(network::Network& network, const int& num_partitions, PartitionVSet& catchment_part, PartitionVSet& nexus_part) { + auto catchments = network.filter("cat", network::SortOrder::TransposedDepthFirstPreorder); + int partition = 0; int counter = 0; - int total = num_catchments; + int total = size(catchments); int partition_size = total/num_partitions; int partition_size_norm = partition_size; int remainder; @@ -131,7 +132,7 @@ void generate_partitions(network::Network& network, const int& num_partitions, c nexus_set.reserve(partition_size); std::string part_id, partition_str; - for(const auto& catchment : network.filter("cat", network::SortOrder::TransposedDepthFirstPreorder)){ + for(const auto& catchment : catchments){ if (partition < remainder) partition_size = partition_size_plus1; else @@ -496,7 +497,7 @@ int main(int argc, char* argv[]) Network global_network(global_nexus_collection); //Generate the partitioning - generate_partitions(global_network, num_partitions, num_catchments, catchment_part, nexus_part); + generate_partitions(global_network, num_partitions, catchment_part, nexus_part); //global_network.print_network(); From d4d23dc36f6ceea8547913711df252f8e2298b4b Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Wed, 10 Sep 2025 16:12:08 -0700 Subject: [PATCH 55/58] Add missing support for added SentinelFeature in FeatureVisitor test --- test/geojson/FeatureCollection_Test.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/geojson/FeatureCollection_Test.cpp b/test/geojson/FeatureCollection_Test.cpp index 18684ac21c..27cfa77f63 100644 --- a/test/geojson/FeatureCollection_Test.cpp +++ b/test/geojson/FeatureCollection_Test.cpp @@ -55,6 +55,10 @@ class Visitor : public geojson::FeatureVisitor { this->types.push_back("CollectionFeature"); } + void visit(geojson::SentinelFeature *feature) override { + this->types.push_back("SentinelFeature"); + } + std::string get(int index) { if( index >= 0 && index < types.size() ) return this->types[index]; From a8ef7334d587d7f481f3eceadde05031b997163a Mon Sep 17 00:00:00 2001 From: Phil Miller Date: Thu, 11 Sep 2025 10:15:27 -0700 Subject: [PATCH 56/58] Comment about what's going on with sentinel flowpaths --- src/NGen.cpp | 7 +++++++ src/partitionGenerator.cpp | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/NGen.cpp b/src/NGen.cpp index 1c0cf2bf28..27f54f2e13 100644 --- a/src/NGen.cpp +++ b/src/NGen.cpp @@ -373,6 +373,13 @@ int main(int argc, char *argv[]) { // TODO: Instead of iterating through a collection of FeatureBase objects mapping to catchments, we instead want to iterate through HY_Catchment objects geojson::GeoJSON catchment_collection; + // As part of the fix for NOAA-OWP/ngen#284 / NGWPC-6553, + // partitioning may insert sentinel flowpaths downstream of + // terminal nexuses. Those sentinels will not exist in the + // catchmentDataFile. Their listing in catchment_subset_ids works + // because the respective geoFOO::read() functions return the + // intersection of features in the file and the specified subset, + // rather than erroring on missing features. if (boost::algorithm::ends_with(catchmentDataFile, "gpkg")) { #if NGEN_WITH_SQLITE3 catchment_collection = ngen::geopackage::read(catchmentDataFile, "divides", catchment_subset_ids); diff --git a/src/partitionGenerator.cpp b/src/partitionGenerator.cpp index 25dcc95038..d15162ba19 100644 --- a/src/partitionGenerator.cpp +++ b/src/partitionGenerator.cpp @@ -471,9 +471,23 @@ int main(int argc, char* argv[]) global_nexus_collection->update_ids("id"); global_nexus_collection->link_features_from_property(nullptr, &link_key); - // Store these separately to avoid iterator invalidation from inserting them eagerly + // ngen misbehaves in gathering and storing output when a terminal + // nexus is fed by multiple catchments partitioned to different + // processes. This was recorded as NOAA-OWP/ngen#284 and NGWPC-6553. + // + // Address that here by inserting sentinel flowpaths downstream of + // those nexuses. Those sentinels will be assigned to specific + // processes, which will then properly receive all of the flow for + // the nexus in question. + // + // These features will not exist in the hydrofabric + // GeoJSON/GeoPackage. As implemented, that means that they will + // simply be ignored when ngen figures out what features to + // simulate on each process. + // + // Store the sentinels separately to avoid iterator invalidation + // from inserting them eagerly std::vector> sentinels; - for (auto& feature : *global_nexus_collection) { auto id = feature->get_id(); @@ -487,7 +501,6 @@ int main(int argc, char* argv[]) } } } - for (auto& sentinel : sentinels) { global_nexus_collection->add_feature(sentinel); From de28c2af33305627266a4115190e8b23c6306f0f Mon Sep 17 00:00:00 2001 From: hellkite500 Date: Mon, 2 Feb 2026 09:11:32 -0700 Subject: [PATCH 57/58] fix: don't error out on sentinal features --- src/partitionGenerator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/partitionGenerator.cpp b/src/partitionGenerator.cpp index d15162ba19..279a1d0235 100644 --- a/src/partitionGenerator.cpp +++ b/src/partitionGenerator.cpp @@ -144,7 +144,7 @@ void generate_partitions(network::Network& network, const int& num_partitions, P for( auto downstream : network.get_destination_ids(catchment) ){ nexus_set.emplace(downstream); } - if(nexus_set.size() == 0){ + if(nexus_set.size() == 0 && catchment.find("SENTINEL") == std::string::npos){ std::cerr<<"Error: Catchment "< Date: Fri, 20 Feb 2026 13:55:10 -0500 Subject: [PATCH 58/58] Merge resolution from OWP Master --- CMakeLists.txt | 2 + include/core/Layer.hpp | 81 +---------- .../catchment/Bmi_Module_Formulation.hpp | 8 ++ include/routing/Routing_Py_Adapter.hpp | 135 ------------------ src/geopackage/CMakeLists.txt | 2 +- src/partitionGenerator.cpp | 7 +- .../catchment/Bmi_Module_Formulation.cpp | 20 +++ src/utilities/bmi/CMakeLists.txt | 1 + .../catchments/Bmi_Multi_Formulation_Test.cpp | 10 +- 9 files changed, 47 insertions(+), 219 deletions(-) delete mode 100644 include/routing/Routing_Py_Adapter.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b3df53ca32..7c96c93b27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -322,6 +322,7 @@ add_subdirectory("src/utilities") add_subdirectory("src/utilities/mdarray") add_subdirectory("src/utilities/mdframe") add_subdirectory("src/utilities/logging") +add_subdirectory("src/utilities/python") add_subdirectory("src/utilities/bmi") target_link_libraries(ngen @@ -335,6 +336,7 @@ target_link_libraries(ngen NGen::forcing NGen::core_mediator NGen::logging + NGen::parallel NGen::bmi_protocols ) diff --git a/include/core/Layer.hpp b/include/core/Layer.hpp index 8718346a7e..5c3c4481fa 100644 --- a/include/core/Layer.hpp +++ b/include/core/Layer.hpp @@ -104,82 +104,11 @@ namespace ngen /*** * @brief Run one simulation timestep for each model in this layer */ - virtual void update_models() - { - auto idx = simulation_time.next_timestep_index(); - auto step = simulation_time.get_output_interval_seconds(); - - //std::cout<<"Output Time Index: "<(r); - double response(0.0); - try{ - response = r_c->get_response(output_time_index, simulation_time.get_output_interval_seconds()); - // Check mass balance if able - r_c->check_mass_balance(output_time_index, simulation_time.get_total_output_times(), current_timestamp); - } - catch(models::external::State_Exception& e){ - std::string msg = e.what(); - msg = msg+" at timestep "+std::to_string(output_time_index) - +" ("+current_timestamp+")" - +" at feature id "+id; - throw models::external::State_Exception(msg); - } - catch(std::exception& e){ - std::string msg = e.what(); - msg = msg+" at timestep "+std::to_string(output_time_index) - +" ("+current_timestamp+")" - +" at feature id "+id; - throw std::runtime_error(msg); - } - std::string output = std::to_string(output_time_index)+","+current_timestamp+","+ - r_c->get_output_line_for_timestep(output_time_index)+"\n"; - r_c->write_output(output); - //TODO put this somewhere else. For now, just trying to ensure we get m^3/s into nexus output - double area; - try{ - area = catchment_data->get_feature(id)->get_property("areasqkm").as_real_number(); - } - catch(std::invalid_argument &e) - { - area = catchment_data->get_feature(id)->get_property("area_sqkm").as_real_number(); - } - double response_m_s = response * (area * 1000000); - //TODO put this somewhere else as well, for now, an implicit assumption is that a module's get_response returns - //m/timestep - //since we are operating on a 1 hour (3600s) dt, we need to scale the output appropriately - //so no response is m^2/hr...m^2/hr * 1hr/3600s = m^3/hr - double response_m_h = response_m_s / 3600.0; - //update the nexus with this flow - for(auto& nexus : features.destination_nexuses(id)) { - //TODO in a DENDRITIC network, only one destination nexus per catchment - //If there is more than one, some form of catchment partitioning will be required. - //for now, only contribute to the first one in the list - if(nexus == nullptr){ - throw std::runtime_error("Invalid (null) nexus instantiation downstream of "+id+". "+SOURCE_LOC); - } - nexus->add_upstream_flow(response_m_h, id, output_time_index); - /*std::cerr << "Add water to nexus ID = " << nexus->get_id() << " from catchment ID = " << id << " value = " - << response << ", ID = " << id << ", time-index = " << output_time_index << std::endl; */ - break; - } - - } //done catchments - - ++output_time_index; - if ( output_time_index < simulation_time.get_total_output_times() ) - { - simulation_time.advance_timestep(); - } - } - + virtual void update_models(boost::span catchment_outflows, + std::unordered_map &catchment_indexes, + boost::span nexus_downstream_flows, + std::unordered_map &nexus_indexes, + int current_step); protected: diff --git a/include/realizations/catchment/Bmi_Module_Formulation.hpp b/include/realizations/catchment/Bmi_Module_Formulation.hpp index ebe4e6477d..cd6dda4969 100644 --- a/include/realizations/catchment/Bmi_Module_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Module_Formulation.hpp @@ -7,6 +7,9 @@ #include "Bmi_Adapter.hpp" #include #include "bmi_utilities.hpp" +#include + +#include #include "bmi/protocols.hpp" using data_access::MEAN; @@ -287,6 +290,11 @@ namespace realization { const std::vector get_bmi_input_variables() const override; const std::vector get_bmi_output_variables() const override; + const boost::span get_serialization_state() const; + void load_serialization_state(const boost::span state) const; + void free_serialization_state() const; + void set_realization_file_format(bool is_legacy_format); + virtual void check_mass_balance(const int& iteration, const int& total_steps, const std::string& timestamp) const override { //Create the protocol context, each member is const, and cannot change during the check models::bmi::protocols::Context ctx{iteration, total_steps, timestamp, id}; diff --git a/include/routing/Routing_Py_Adapter.hpp b/include/routing/Routing_Py_Adapter.hpp deleted file mode 100644 index 6a022cd0a4..0000000000 --- a/include/routing/Routing_Py_Adapter.hpp +++ /dev/null @@ -1,135 +0,0 @@ -#ifndef NGEN_ROUTING_PY_ADAPTER_H -#define NGEN_ROUTING_PY_ADAPTER_H - -#include - -#if NGEN_WITH_PYTHON - -#include -#include -#include -#include "pybind11/pybind11.h" -#include "pybind11/pytypes.h" -#include "pybind11/numpy.h" -#include -#include "python/InterpreterUtil.hpp" - -namespace py = pybind11; - -namespace routing_py_adapter { - - class Routing_Py_Adapter { - - public: - /** - * @brief Construct Routing_Py_Adapter with configured paths - * - * @param t_route_config_file_with_path path to a t-route yaml configuration file - */ - Routing_Py_Adapter(std::string t_route_config_file_with_path); - - /** - * Function to run @p number_of_timesteps of routing, extracting - * lateral inflows from @p flow_vector - * - * FIXME This is current unimplemented and will require some additional - * work to properly map flows to the correct t-route network segments. - * This may not even be the correct concept to implement this type of - * "integrated" routing. But the basic idea is that after a catchment - * update occurs, it contributes some amount of lateral flow to the channel - * as represented by @p flow_vector and this flow should be routed down stream - * for @p number_of_timesteps . Note though, that all lateral inflows are required - * for a single routing pass of the network, so this would probably end up being a - * flow map that we pass to a custom t-route function that extracts the lateral inflow - * vector for each identity and constructs the correct lateral inflow setup to make - * a full routing pass. - * - * See NOTE in @ref route(int, int) route() about python module availability. - * - * @param number_of_timesteps - * @param delta_time - * @param flow_vector - */ - void route(int number_of_timesteps, int delta_time, - const std::vector &flow_vector); - - - /** - * Function to run a full set of routing computations using the nexus output files - * from an ngen simulation. - * - * Currently, these parameters are ignored and are read instead from the yaml configuration - * file contained in #t_route_config_path - * - * NOTE this function uses a pybind11 embedded interpreter to load the t-route namespace package - * ngen-main and then executes the routing in the python interpreter. - * It is assumed that the ngen-main module is available in the interpreters PYTHON_PATH. - * If the module cannot be found, then a ModuleNotFoundError will be thrown. - * Similarly, ngen-main depends on several other python modules. If any of these are not in the - * environment's PYTHON_PATH, errors will occur. - * - * It is recommended to install all t-route packages into a loaded virtual environment or - * to the system site-packages. - * - * @param number_of_timesteps - * @param delta_time - */ - void route(int number_of_timesteps, int delta_time); - - - template - void convert_vector_to_numpy_array(const std::vector &flow_vector) - { - /** - * NOTE: Currently not using this function because current plan is to have routing read Nexus - * values from Nexus output files instead of passing in-memory values to t-route. If - * the ability to pass in-memory values to t-route is needed, then the following TODOs are - * needed. - * TODO: Retrieve and convert map of Nexus IDs with Nexus flow vectors to Python dictionary - * of NumPy arrays - * TODO: Retrieve single time index vector for all Nexus flow values and convert to NumPy array - */ - - //Allocate py::array (to pass the result of the C++ function to Python) - auto result = py::array_t(flow_vector.size()); - auto result_buffer = result.request(); - T *result_ptr = (T *) result_buffer.ptr; - - //Copy std::vector -> py::array - std::memcpy(result_ptr, flow_vector.data(), flow_vector.size()*sizeof(T)); - - //Create object for receive_flow_values subroutine - py::object receive_flow_values = t_route_module.attr("receive_flow_values"); - - //Call receive_flow_values subroutine - receive_flow_values(result); - - } - - - private: - - - /** Handle to the interpreter util. - * - * Order is important, must be constructed before anything depending on it - * and destructed after all dependent members. - */ - std::shared_ptr interpreter; - - /** A binding to the Python numpy package/module. */ - py::object np; - - /** A binding to the t-route module. */ - py::object t_route_module; - - /** Path to a t-route yaml configuration file */ - std::string t_route_config_path; - }; - -} - - -#endif //NGEN_WITH_PYTHON - -#endif //NGEN_ROUTING_PY_ADAPTER_H diff --git a/src/geopackage/CMakeLists.txt b/src/geopackage/CMakeLists.txt index afe1d0c822..0e544b800b 100644 --- a/src/geopackage/CMakeLists.txt +++ b/src/geopackage/CMakeLists.txt @@ -9,4 +9,4 @@ add_library(geopackage proj.cpp add_library(NGen::geopackage ALIAS geopackage) target_include_directories(geopackage PUBLIC ${PROJECT_SOURCE_DIR}/include/geopackage) target_include_directories(geopackage PUBLIC ${PROJECT_SOURCE_DIR}/include/utilities) -target_link_libraries(geopackage PUBLIC NGen::geojson Boost::boost SQLite::SQLite3) +target_link_libraries(geopackage PUBLIC NGen::geojson Boost::boost SQLite::SQLite3 NGen::logging) diff --git a/src/partitionGenerator.cpp b/src/partitionGenerator.cpp index 0b95453490..f8b48ee616 100644 --- a/src/partitionGenerator.cpp +++ b/src/partitionGenerator.cpp @@ -147,9 +147,9 @@ void generate_partitions(network::Network& network, const int& num_partitions, P for( auto downstream : network.get_destination_ids(catchment) ){ nexus_set.emplace(downstream); } - if(nexus_set.size() == 0 && catchment.find("SENTINEL") == std::string::npos){ - std::cerr<<"Error: Catchment "<(sentinel_id); sentinels.push_back(sentinel_feature); feature->add_destination_feature(sentinel_feature.get()); + LOG("Nexus " + feature->get_id() + " has no destination features; adding " + sentinel_id + " below it", LogLevel::INFO); } } } diff --git a/src/realizations/catchment/Bmi_Module_Formulation.cpp b/src/realizations/catchment/Bmi_Module_Formulation.cpp index 83afc19648..da478a294e 100644 --- a/src/realizations/catchment/Bmi_Module_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Module_Formulation.cpp @@ -583,11 +583,31 @@ namespace realization { available_forcing_units[output_var_name] = get_bmi_model()->GetVarUnits(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]); + available_forcing_units[bmi_var_names_map[output_var_name]] = get_bmi_model()->GetVarUnits(output_var_name); //units come from the model output variable. + } } //Initialize all NgenBmiProtocols with the valid adapter pointer and any properties //provided in the read configuration. bmi_protocols = models::bmi::protocols::NgenBmiProtocols(get_bmi_model(), properties); } + + //check if units have not been specified. If not, default to native units. + std::string blank_string = ""; + auto &names = get_output_variable_names(); + if(output_var_units.size() == 0){ + output_var_units.resize(names.size(), blank_string); + } + + for (int i = 0; i < names.size(); ++i) { + if (output_var_units[i] == blank_string){ + output_var_units[i] = get_provider_units_for_variable(names[i]); + } + } + + //check if output variable indices (for vector variables) are specified in config. If not, default to zero (first index). + if(output_var_indices.size() == 0){ + output_var_indices.resize(names.size(), 0); + } } /** * @brief Template function for copying iterator range into contiguous array. diff --git a/src/utilities/bmi/CMakeLists.txt b/src/utilities/bmi/CMakeLists.txt index 67a62eaf8d..5d86e87f70 100644 --- a/src/utilities/bmi/CMakeLists.txt +++ b/src/utilities/bmi/CMakeLists.txt @@ -28,6 +28,7 @@ target_link_libraries(ngen_bmi_protocols PUBLIC ${CMAKE_DL_LIBS} Boost::boost # Headers-only Boost + NGen::logging ) target_sources(ngen_bmi_protocols diff --git a/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp b/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp index 94e661e132..e0773e6767 100644 --- a/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp +++ b/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp @@ -388,6 +388,7 @@ class Bmi_Multi_Formulation_Test : public ::testing::Test { } inline void buildExampleConfig(const int ex_index, const int nested_count) { + std::string outputVariablesSubConfig = (ex_index == 6) ? buildExampleOutputVariablesSubConfig(ex_index, true) : buildExampleOutputVariablesSubConfig(ex_index) + "\n"; std::string config = "{\n" " \"global\": {},\n" @@ -491,7 +492,7 @@ void Bmi_Multi_Formulation_Test::SetUp() { // Define this manually to set how many nested modules per example, and implicitly how many examples. // This means example_module_depth.size() example scenarios with example_module_depth[i] nested modules in each scenario. - example_module_depth = {2, 2, 2, 2, 2, 2, 3}; + example_module_depth = {2, 2, 2, 2, 2, 2, 2, 3}; // Initialize the members for holding required input and result test data for individual example scenarios setupExampleDataCollections(); @@ -532,13 +533,14 @@ void Bmi_Multi_Formulation_Test::SetUp() { initializeTestExample(4, "cat-27", {std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, { "bogus_variable" }); initializeTestExample(5, "cat-27", {std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, { "OUTPUT_VAR_1" }); + initializeTestExample(6, "cat-27", {std::string(BMI_CPP_TYPE), std::string(BMI_FORTRAN_TYPE)}, { "OUTPUT_VAR_3","OUTPUT_VAR_3","OUTPUT_VAR_3" }); + #if NGEN_WITH_BMI_C - initializeTestExample(6, "cat-27", {std::string(BMI_C_TYPE), std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, {"OUTPUT_VAR_1__0"}); // Output var from C module... + initializeTestExample(7, "cat-27", {std::string(BMI_C_TYPE), std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, {"OUTPUT_VAR_1__0"}); // Output var from C module... #else - initializeTestExample(6, "cat-27", {std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, {"OUTPUT_VAR_1__0"}); // Output var from Fortran module... + initializeTestExample(7, "cat-27", {std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, {"OUTPUT_VAR_1__0"}); // Output var from Fortran module... #endif // NGEN_WITH_PYTHON - } /** Simple test to make sure the model config from example 0 initializes. */