diff --git a/.github/workflows/module_integration.yml b/.github/workflows/module_integration.yml index ab4b3834d1..189f478f5f 100644 --- a/.github/workflows/module_integration.yml +++ b/.github/workflows/module_integration.yml @@ -50,6 +50,13 @@ jobs: mod-dir: "extern/cfe/" targets: "cfebmi" + - name: Build PET + id: submod_build_5 + uses: ./.github/actions/ngen-submod-build + with: + mod-dir: "extern/evapotranspiration/evapotranspiration" + targets: "petbmi" + - name: Build Ngen uses: ./.github/actions/ngen-build with: @@ -65,6 +72,11 @@ jobs: inputfile='data/example_bmi_multi_realization_config.json' ./cmake_build/ngen data/catchment_data.geojson "cat-27" data/nexus_data.geojson "nex-26" $inputfile + - name: Run surfacebmi, cfebmi and petbmi + run: | + inputfile='data/example_bmi_multi_realization_config_w_noah_pet_cfe.json' + ./cmake_build/ngen data/catchment_data.geojson "cat-27" data/nexus_data.geojson "nex-26" $inputfile + # Run t-route/pybind integration test test_troute_integration: # The type of runner that the job will run on diff --git a/.github/workflows/test_and_validate.yml b/.github/workflows/test_and_validate.yml index 62826f6174..a08f1659d4 100644 --- a/.github/workflows/test_and_validate.yml +++ b/.github/workflows/test_and_validate.yml @@ -45,6 +45,40 @@ jobs: - name: Clean Up uses: ./.github/actions/clean-build + # Test PET + test_pet: + # The type of runner that the job will run on + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + 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@v2 + + - name: Build PET Submodule + id: submod_build_5 + uses: ./.github/actions/ngen-submod-build + with: + mod-dir: "extern/evapotranspiration/evapotranspiration" + targets: "petbmi" + + - name: Build PET Tests + uses: ./.github/actions/ngen-build + with: + targets: "compare_pet" + build-cores: ${{ env.LINUX_NUM_PROC_CORES }} + + - name: Run Tests + run: ./cmake_build/test/compare_pet + timeout-minutes: 15 + + - name: Clean Up + uses: ./.github/actions/clean-build + # Test MPI remote nexus behavior in linux test_mpi_remote_nexus: # The type of runner that the job will run on diff --git a/data/bmi/c/pet/cat-27_bmi_config.ini b/data/bmi/c/pet/cat-27_bmi_config.ini new file mode 100644 index 0000000000..1a8a577b03 --- /dev/null +++ b/data/bmi/c/pet/cat-27_bmi_config.ini @@ -0,0 +1,21 @@ +verbose=0 +pet_method=5 +forcing_file=BMI +run_unit_tests=0 +yes_aorc=1 +yes_wrf=0 +wind_speed_measurement_height_m=10.0 +humidity_measurement_height_m=2.0 +vegetation_height_m=0.12 +zero_plane_displacement_height_m=0.0003 +momentum_transfer_roughness_length=0.0 +heat_transfer_roughness_length_m=0.0 +surface_longwave_emissivity=1.0 +surface_shortwave_albedo=0.22 +cloud_base_height_known=FALSE +latitude_degrees=37.25 +longitude_degrees=-97.5554 +site_elevation_m=303.33 +time_step_size_s=3600 +num_timesteps=720 +shortwave_radiation_provided=0 diff --git a/data/bmi/c/pet/cat-67_bmi_config.ini b/data/bmi/c/pet/cat-67_bmi_config.ini new file mode 100644 index 0000000000..03d7482c3b --- /dev/null +++ b/data/bmi/c/pet/cat-67_bmi_config.ini @@ -0,0 +1,21 @@ +verbose=0 +pet_method=1 +forcing_file=BMI +run_unit_tests=0 +yes_aorc=1 +yes_wrf=0 +wind_speed_measurement_height_m=10.0 +humidity_measurement_height_m=2.0 +vegetation_height_m=0.12 +zero_plane_displacement_height_m=0.0003 +momentum_transfer_roughness_length=0.0 +heat_transfer_roughness_length_m=0.0 +surface_longwave_emissivity=1.0 +surface_shortwave_albedo=0.22 +cloud_base_height_known=FALSE +latitude_degrees=37.865211 +longitude_degrees=-98.12345 +site_elevation_m=303.333 +time_step_size_s=3600 +num_timesteps=720 +shortwave_radiation_provided=0 diff --git a/data/example_bmi_multi_realization_config_w_noah_pet_cfe.json b/data/example_bmi_multi_realization_config_w_noah_pet_cfe.json new file mode 100644 index 0000000000..cf98b045a2 --- /dev/null +++ b/data/example_bmi_multi_realization_config_w_noah_pet_cfe.json @@ -0,0 +1,90 @@ +{ + "global": { + "formulations": [ + { + "name": "bmi_multi", + "params": { + "model_type_name": "bmi_multi_noahowp_cfe", + "forcing_file": "", + "init_config": "", + "allow_exceed_end_time": true, + "main_output_variable": "Q_OUT", + "modules": [ + { + "name": "bmi_fortran", + "params": { + "model_type_name": "bmi_fortran_noahowp", + "library_file": "./extern/noah-owp-modular/cmake_build/libsurfacebmi", + "forcing_file": "", + "init_config": "./data/bmi/fortran/noah-owp-modular-init-{{id}}.namelist.input", + "allow_exceed_end_time": true, + "main_output_variable": "QINSUR", + "variables_names_map": { + "PRCPNONC": "atmosphere_water__liquid_equivalent_precipitation_rate", + "Q2": "atmosphere_air_water~vapor__relative_saturation", + "SFCTMP": "land_surface_air__temperature", + "UU": "land_surface_wind__x_component_of_velocity", + "VV": "land_surface_wind__y_component_of_velocity", + "LWDN": "land_surface_radiation~incoming~longwave__energy_flux", + "SOLDN": "land_surface_radiation~incoming~shortwave__energy_flux", + "SFCPRS": "land_surface_air__pressure" + }, + "uses_forcing_file": false + } + }, + { + "name": "bmi_c", + "params": { + "model_type_name": "bmi_c_pet", + "library_file": "./extern/evapotranspiration/evapotranspiration/cmake_build/libpetbmi", + "forcing_file": "", + "init_config": "./data/bmi/c/pet/{{id}}_bmi_config.ini", + "allow_exceed_end_time": true, + "main_output_variable": "water_potential_evaporation_flux", + "registration_function": "register_bmi_pet", + "variables_names_map": { + "water_potential_evaporation_flux": "potential_evapotranspiration" + }, + "uses_forcing_file": false + } + }, + { + "name": "bmi_c", + "params": { + "model_type_name": "bmi_c_cfe", + "library_file": "./extern/cfe/cmake_build/libcfebmi", + "forcing_file": "", + "init_config": "./data/bmi/c/cfe/{{id}}_bmi_config.ini", + "allow_exceed_end_time": true, + "main_output_variable": "Q_OUT", + "registration_function": "register_bmi_cfe", + "variables_names_map": { + "water_potential_evaporation_flux": "potential_evapotranspiration", + "atmosphere_air_water~vapor__relative_saturation": "SPFH_2maboveground", + "land_surface_air__temperature": "TMP_2maboveground", + "land_surface_wind__x_component_of_velocity": "UGRD_10maboveground", + "land_surface_wind__y_component_of_velocity": "VGRD_10maboveground", + "land_surface_radiation~incoming~longwave__energy_flux": "DLWRF_surface", + "land_surface_radiation~incoming~shortwave__energy_flux": "DSWRF_surface", + "land_surface_air__pressure": "PRES_surface" + }, + "uses_forcing_file": false + } + } + ], + "uses_forcing_file": false + } + } + ], + "forcing": { + "file_pattern": ".*{{id}}.*..csv", + "path": "./data/forcing/", + "provider": "CsvPerFeature" + } + }, + "time": { + "start_time": "2015-12-01 00:00:00", + "end_time": "2015-12-30 23:00:00", + "output_interval": 3600 + } +} diff --git a/include/realizations/catchment/Bmi_C_Formulation.hpp b/include/realizations/catchment/Bmi_C_Formulation.hpp index a70ca60d9c..ecba492858 100644 --- a/include/realizations/catchment/Bmi_C_Formulation.hpp +++ b/include/realizations/catchment/Bmi_C_Formulation.hpp @@ -188,6 +188,7 @@ namespace realization { friend class ::Bmi_Formulation_Test; friend class ::Bmi_C_Formulation_Test; friend class ::Bmi_C_Cfe_IT; + friend class ::Bmi_C_Pet_IT; private: diff --git a/include/realizations/catchment/Bmi_Formulation.hpp b/include/realizations/catchment/Bmi_Formulation.hpp index bb92c3a839..cc43e69ae2 100644 --- a/include/realizations/catchment/Bmi_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Formulation.hpp @@ -47,6 +47,7 @@ class Bmi_Formulation_Test; class Bmi_C_Formulation_Test; class Bmi_C_Cfe_IT; +class Bmi_C_Pet_IT; using namespace std; diff --git a/include/realizations/catchment/Bmi_Module_Formulation.hpp b/include/realizations/catchment/Bmi_Module_Formulation.hpp index e1b463841c..694b7b4c73 100644 --- a/include/realizations/catchment/Bmi_Module_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Module_Formulation.hpp @@ -23,6 +23,7 @@ class Bmi_Multi_Formulation_Test; class Bmi_C_Formulation_Test; class Bmi_Cpp_Formulation_Test; class Bmi_C_Cfe_IT; +class Bmi_C_Pet_IT; class Bmi_Cpp_Multi_Array_Test; namespace realization { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8cdab076db..e99ea4af29 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -250,6 +250,20 @@ add_test( ${NETCDF_LIBRARIES} ) +########################## Comparison tests for the BMI PET implementation +# TODO: this probably needs to be added to integration testing also +add_test( + compare_pet + 1 + realizations/catchments/Bmi_C_Pet_IT.cpp + NGen::core + NGen::realizations_catchment + NGen::core_mediator + NGen::forcing + libudunits2 + ${NETCDF_LIBRARIES} +) + ########################## Internal Torchlib LSTM Tests if(LSTM_TORCH_LIB_ACTIVE) add_test( diff --git a/test/realizations/catchments/Bmi_C_Pet_IT.cpp b/test/realizations/catchments/Bmi_C_Pet_IT.cpp new file mode 100644 index 0000000000..2998d638b5 --- /dev/null +++ b/test/realizations/catchments/Bmi_C_Pet_IT.cpp @@ -0,0 +1,276 @@ +#ifndef BMI_TEST_C_LOCAL_LIB_NAME +#ifdef __APPLE__ + #define BMI_TEST_C_LOCAL_LIB_NAME "libpetbmi.dylib" +#else +#ifdef __GNUC__ + #define BMI_TEST_C_LOCAL_LIB_NAME "libpetbmi.so" + #endif // __GNUC__ +#endif // __APPLE__ +#endif // BMI_TEST_C_LOCAL_LIB_NAME + +#include +#include +#include +#include "gtest/gtest.h" +#include "Bmi_Module_Formulation.hpp" +#include "Bmi_C_Formulation.hpp" +#include +#include +#include "FileChecker.h" +#include "Formulation_Manager.hpp" +#include "Forcing.h" + +using namespace realization; +using namespace std; + +/** + * Integration test for the BMI PET model library. + */ +class Bmi_C_Pet_IT : public ::testing::Test { +protected: + + static std::string find_file(std::vector dir_opts, const std::string& basename) { + std::vector file_opts(dir_opts.size()); + for (int i = 0; i < dir_opts.size(); ++i) + file_opts[i] = dir_opts[i] + basename; + return utils::FileChecker::find_first_readable(file_opts); + } + + static std::string get_friend_bmi_init_config(const Bmi_C_Formulation& formulation) { + return formulation.get_bmi_init_config(); + } + + static std::string get_friend_bmi_main_output_var(const Bmi_C_Formulation& formulation) { + return formulation.get_bmi_main_output_var(); + } + + static std::string get_friend_forcing_file_path(const Bmi_C_Formulation& formulation) { + return formulation.get_forcing_file_path(); + } + + static bool get_friend_is_bmi_using_forcing_file(const Bmi_C_Formulation& formulation) { + return formulation.is_bmi_using_forcing_file(); + } + + static std::string get_friend_model_type_name(Bmi_C_Formulation& formulation) { + return formulation.get_model_type_name(); + } + + static double get_friend_var_value_as_double(Bmi_C_Formulation& formulation, const string& var_name) { + return formulation.get_var_value_as_double(var_name); + } + + void SetUp() override; + + void TearDown() override; + + std::vector forcing_dir_opts; + std::vector bmi_init_cfg_dir_opts; + std::vector lib_dir_opts; + + std::vector config_json; + std::vector catchment_ids; + std::vector model_type_name; + std::vector forcing_file; + std::vector lib_file; + std::vector init_config; + std::vector main_output_variable; + std::vector registration_functions; + std::vector uses_forcing_file; + std::vector> forcing_params_examples; + std::vector config_properties; + std::vector config_prop_ptree; + +}; + +void Bmi_C_Pet_IT::SetUp() { + testing::Test::SetUp(); + +#define EX_COUNT 2 + + forcing_dir_opts = {"./data/forcing/", "../data/forcing/", "../../data/forcing/"}; + bmi_init_cfg_dir_opts = {"./data/bmi/c/pet/", "../data/bmi/c/pet/", "../../data/bmi/c/pet/"}; + lib_dir_opts = {"./extern/evapotranspiration/evapotranspiration/cmake_build/", "../extern/evapotranspiration/evapotranspiration/cmake_build/", "../../extern/evapotranspiration/evapotranspiration/cmake_build/"}; + + config_json = std::vector(EX_COUNT); + catchment_ids = std::vector(EX_COUNT); + model_type_name = std::vector(EX_COUNT); + forcing_file = std::vector(EX_COUNT); + lib_file = std::vector(EX_COUNT); + registration_functions = std::vector(EX_COUNT); + init_config = std::vector(EX_COUNT); + main_output_variable = std::vector(EX_COUNT); + uses_forcing_file = std::vector(EX_COUNT); + forcing_params_examples = std::vector>(EX_COUNT); + config_properties = std::vector(EX_COUNT); + config_prop_ptree = std::vector(EX_COUNT); + + /* Set up the basic/explicit example index details in the arrays */ + catchment_ids[0] = "cat-27"; + model_type_name[0] = "bmi_c_pet"; + forcing_file[0] = find_file(forcing_dir_opts, "cat-27_2015-12-01 00_00_00_2015-12-30 23_00_00.csv"); + lib_file[0] = find_file(lib_dir_opts, BMI_TEST_C_LOCAL_LIB_NAME); + registration_functions[0] = "register_bmi_pet"; + init_config[0] = find_file(bmi_init_cfg_dir_opts, "cat-27_bmi_config.ini"); + main_output_variable[0] = "water_potential_evaporation_flux"; + uses_forcing_file[0] = true; + + catchment_ids[1] = "cat-67"; + model_type_name[1] = "bmi_c_pet"; + forcing_file[1] = find_file(forcing_dir_opts, "cat-67_2015-12-01 00_00_00_2015-12-30 23_00_00.csv"); + lib_file[1] = find_file(lib_dir_opts, BMI_TEST_C_LOCAL_LIB_NAME); + registration_functions[1] = "register_bmi_pet"; + init_config[1] = find_file(bmi_init_cfg_dir_opts, "cat-67_bmi_config.ini"); + main_output_variable[1] = "water_potential_evaporation_flux"; + uses_forcing_file[1] = true; + + std::string output_variables = " \"output_variables\": [\"water_potential_evaporation_flux\"],\n"; + + /* Set up the derived example details */ + int i = 0; + for (int i = 0; i < EX_COUNT; i++) { + std::shared_ptr params = std::make_shared( + forcing_params(forcing_file[i], "legacy", "2015-12-01 00:00:00", "2015-12-30 23:00:00")); + std::string variables_line = (i == 0) ? output_variables : ""; + forcing_params_examples[i] = params; + config_json[i] = "{" + " \"global\": {}," + " \"catchments\": {" + " \"" + catchment_ids[i] + "\": {" + " \"bmi_c\": {" + " \"model_type_name\": \"" + model_type_name[i] + "\"," + " \"library_file\": \"" + lib_file[i] + "\"," + " \"forcing_file\": \"" + forcing_file[i] + "\"," + " \"init_config\": \"" + init_config[i] + "\"," + " \"main_output_variable\": \"" + main_output_variable[i] + "\"," + " \"" + BMI_REALIZATION_CFG_PARAM_OPT__OUTPUT_PRECISION + "\": 9, " + " \"" + BMI_REALIZATION_CFG_PARAM_OPT__VAR_STD_NAMES + "\": { " + " \"land_surface_air__temperature\": \"" + AORC_FIELD_NAME_TEMP_2M_AG + "\"," + " \"land_surface_air__pressure\": \"" + AORC_FIELD_NAME_PRESSURE_SURFACE + "\"," + " \"atmosphere_air_water~vapor__relative_saturation\": \"" + AORC_FIELD_NAME_SPEC_HUMID_2M_AG + "\"," + " \"land_surface_radiation~incoming~shortwave__energy_flux\": \"" + AORC_FIELD_NAME_SOLAR_SHORTWAVE + "\"," + " \"land_surface_radiation~incoming~longwave__energy_flux\": \"" + AORC_FIELD_NAME_SOLAR_LONGWAVE + "\"," + " \"land_surface_wind__x_component_of_velocity\": \"" + AORC_FIELD_NAME_WIND_U_10M_AG + "\"," + " \"land_surface_wind__y_component_of_velocity\": \"" + AORC_FIELD_NAME_WIND_V_10M_AG + "\"" + " }," + " \"registration_function\": \"" + registration_functions[i] + "\"," + + variables_line + + " \"uses_forcing_file\": " + (uses_forcing_file[i] ? "true" : "false") + "" + " }," + " \"forcing\": { \"path\": \"" + forcing_file[i] + "\"}" + " }" + " }" + "}"; + std::stringstream stream; + stream << config_json[i]; + //cout << stream.str(); + boost::property_tree::ptree loaded_tree; + boost::property_tree::json_parser::read_json(stream, loaded_tree); + config_prop_ptree[i] = loaded_tree.get_child("catchments").get_child(catchment_ids[i]).get_child("bmi_c"); + } +} + +void Bmi_C_Pet_IT::TearDown() { + Test::TearDown(); +} + +/** Test to make sure the model initializes. */ +TEST_F(Bmi_C_Pet_IT, Test_InitModel) { + int ex_index = 0; + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_unique(*forcing_params_examples[ex_index]), utils::StreamHandler()); + formulation.create_formulation(config_prop_ptree[ex_index]); + + ASSERT_EQ(get_friend_model_type_name(formulation), model_type_name[ex_index]); + ASSERT_EQ(get_friend_forcing_file_path(formulation), forcing_file[ex_index]); + ASSERT_EQ(get_friend_bmi_init_config(formulation), init_config[ex_index]); + ASSERT_EQ(get_friend_bmi_main_output_var(formulation), main_output_variable[ex_index]); + ASSERT_EQ(get_friend_is_bmi_using_forcing_file(formulation), uses_forcing_file[ex_index]); +} + +/** Test of get response. */ +TEST_F(Bmi_C_Pet_IT, Test_GetResponse) { + // Used to select the example config from what the testing Setup() function sets up. + int ex_index = 0; + + // Compare within a margin of error + double error_margin = 0.001; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_unique(*forcing_params_examples[ex_index]), utils::StreamHandler()); + + formulation.create_formulation(config_prop_ptree[ex_index]); + + double response = formulation.get_response(25, 3600); + + double expected = 5.19727e-08; + + ASSERT_NEAR(expected, response, error_margin); + +} +/** Compare of BMI PET and PET output benchmark results */ +TEST_F(Bmi_C_Pet_IT, Test_Cat27_Method5_CompareBenchmark) { + // Used to select the example config from what the testing Setup() function sets up. + int ex_index = 0; + + // Compare within a margin of error + double error_margin = 0.000001; + + /* Output variables for BMI pet are: + * + * (0) "water_potential_evaporation_flux", + */ + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_unique(*forcing_params_examples[ex_index]), utils::StreamHandler()); + + formulation.create_formulation(config_prop_ptree[ex_index]); + + double output_benchmark[10] = {0.000000016, 0.000000000, 0.000000088, 0.000000064, + 0.000000060, 0.000000054, 0.000000000, 0.000000037, + 0.000000000, 0.000000262}; + int k = 0; + + for (int i = 0; i < 10; i++) + { + formulation.get_response(k, 3600); + + double val = get_friend_var_value_as_double(formulation, "water_potential_evaporation_flux"); + + printf("Timestep index %d: response=%.9f\tbenchmark=%.9f\n", k, val, output_benchmark[i]); + + k += 10; //increase time step index by 10 + + ASSERT_NEAR(val, output_benchmark[i], error_margin); + } +} + +/** Compare of BMI PET and PET output benchmark results */ +TEST_F(Bmi_C_Pet_IT, Test_Cat67_Method1_CompareBenchmark) { + // Used to select the example config from what the testing Setup() function sets up. + int ex_index = 1; + + // Compare within a margin of error + double error_margin = 0.000001; + + /* Output variables for BMI pet are: + * + * (0) "water_potential_evaporation_flux", + */ + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_unique(*forcing_params_examples[ex_index]), utils::StreamHandler()); + + formulation.create_formulation(config_prop_ptree[ex_index]); + + double output_benchmark[10] = {0.000000000, 0.000000000, 0.000000081, 0.000000000, + 0.000000055, 0.000000000, 0.000000000, 0.000000000, + 0.000000000, 0.000000109}; + int k = 0; + for (int i = 0; i < 10; i++) + { + formulation.get_response(k, 3600); + + double val = get_friend_var_value_as_double(formulation, "water_potential_evaporation_flux"); + + printf("Timestep index %d: response=%.9f\tbenchmark=%.9f\n", k, val, output_benchmark[i]); + + k += 10; //increase time step index by 10 + + ASSERT_NEAR(val, output_benchmark[i], error_margin); + } +}