diff --git a/data/forcing/forcings-engine/Gauge_01073000_ESMF_Mesh.nc b/data/forcing/forcings-engine/Gauge_01073000_ESMF_Mesh.nc new file mode 100644 index 0000000000..6a51f5a7a5 Binary files /dev/null and b/data/forcing/forcings-engine/Gauge_01073000_ESMF_Mesh.nc differ diff --git a/data/forcing/forcings-engine/aorc/.keep b/data/forcing/forcings-engine/aorc/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data/forcing/forcings-engine/config_aorc.yml.in b/data/forcing/forcings-engine/config_aorc.yml.in new file mode 100644 index 0000000000..ca1b1cb71e --- /dev/null +++ b/data/forcing/forcings-engine/config_aorc.yml.in @@ -0,0 +1,64 @@ +time_step_seconds: 3600 +initial_time: 0 +NWM_VERSION: 4.0 +NWM_CONFIG: "AORC" +InputForcings: [12] +InputForcingDirectories: [''] +InputForcingTypes: ["NETCDF4"] +InputMandatory: [1] +OutputFrequency: 60 +SubOutputHour: 0 +SubOutFreq: 0 +ScratchDir: "@FORCINGS_ENGINE_SCRATCH_DIR@" +Output: 1 +compressOutput: 0 +floatOutput: 0 +AnAFlag: 0 +LookBack : -9999 +RefcstBDateProc: "202301170000" +RefcstEDateProc: "202301170100" +ForecastFrequency: 60 +ForecastShift: 0 +ForecastInputHorizons: [14400] +ForecastInputOffsets: [0] +GeogridIn: "data/forcing/forcings-engine/Gauge_01073000_ESMF_Mesh.nc" +SpatialMetaIn: "" +GRID_TYPE: "hydrofabric" +NodeCoords: 'nodeCoords' +ElemCoords: 'centerCoords' +ElemConn: 'elementConn' +ElemID: 'element_id' +NumElemConn: 'numElementConn' +HGTVAR: 'Element_Elevation' +SLOPE: 'Element_Slope' +SLOPE_AZIMUTH: 'Element_Slope_Azmuith' +IgnoredBorderWidths: [0] +RegridOpt: [1] +ForcingTemporalInterpolation: [0] +TemperatureBiasCorrection: [0] +PressureBiasCorrection: [0] +HumidityBiasCorrection: [0] +WindBiasCorrection: [0] +SwBiasCorrection: [0] +LwBiasCorrection: [0] +PrecipBiasCorrection: [0] +TemperatureDownscaling: [0] +ShortwaveDownscaling: [0] +PressureDownscaling: [0] +PrecipDownscaling: [0] +HumidityDownscaling: [0] +DownscalingParamDirs: ["@FORCINGS_ENGINE_SCRATCH_DIR@"] +SuppPcp: [] +SuppPcpForcingTypes: '' +SuppPcpDirectories: '' +SuppPcpParamDir: '' +RegridOptSuppPcp: [] +SuppPcpTemporalInterpolation: [] +SuppPcpInputOffsets: [] +SuppPcpMandatory: [] +RqiMethod: 0 +RqiThreshold: 0.9 +cfsEnsNumber: '' +custom_input_fcst_freq: [] +includeLQFrac: 0 + diff --git a/include/forcing/DataProvider.hpp b/include/forcing/DataProvider.hpp index a3488cd3ee..32cbb36029 100644 --- a/include/forcing/DataProvider.hpp +++ b/include/forcing/DataProvider.hpp @@ -21,7 +21,7 @@ namespace data_access BACK_FILL }; - template class DataProvider + template class DataProvider { /** This class provides a generic interface to data services * @@ -29,6 +29,9 @@ namespace data_access public: + using data_type = DataType; + using selection_type = SelectionType; + virtual ~DataProvider() = default; /** diff --git a/include/forcing/ForcingsEngineDataProvider.hpp b/include/forcing/ForcingsEngineDataProvider.hpp index 8cee931230..7b0194ad18 100644 --- a/include/forcing/ForcingsEngineDataProvider.hpp +++ b/include/forcing/ForcingsEngineDataProvider.hpp @@ -33,7 +33,7 @@ namespace detail { //! Parse time string from format. //! Utility function for ForcingsEngineLumpedDataProvider constructor. -time_t parse_time(const std::string& time, const std::string& fmt); +time_t parse_time(const std::string& time, const std::string& fmt = default_time_format); //! Check that requirements for running the forcings engine //! are available at runtime. If requirements are not available, diff --git a/include/forcing/ForcingsEngineLumpedDataProvider.hpp b/include/forcing/ForcingsEngineLumpedDataProvider.hpp new file mode 100644 index 0000000000..193d1fc51c --- /dev/null +++ b/include/forcing/ForcingsEngineLumpedDataProvider.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#if NGEN_WITH_PYTHON + +#include +#include + +namespace data_access { + +struct ForcingsEngineLumpedDataProvider final : + public ForcingsEngineDataProvider +{ + using base_type = ForcingsEngineDataProvider; + + ~ForcingsEngineLumpedDataProvider() override = default; + + ForcingsEngineLumpedDataProvider( + const std::string& init, + std::size_t time_begin_seconds, + std::size_t time_end_seconds, + const std::string& divide_id + ); + + data_type get_value( + const selection_type& selector, + data_access::ReSampleMethod m + ) override; + + std::vector get_values( + const selection_type& selector, + data_access::ReSampleMethod m + ) override; + + //! Get this provider's Divide ID. + std::size_t divide() const noexcept; + + //! Get this provider's Divide ID index within the Forcings Engine. + std::size_t divide_index() const noexcept; + + private: + std::size_t divide_id_; + std::size_t divide_idx_; +}; + +} // namespace data_access + +#endif // NGEN_WITH_PYTHON diff --git a/src/forcing/CMakeLists.txt b/src/forcing/CMakeLists.txt index 51ec7d22a8..d374032b2e 100644 --- a/src/forcing/CMakeLists.txt +++ b/src/forcing/CMakeLists.txt @@ -31,6 +31,7 @@ if(NGEN_WITH_PYTHON) target_sources(forcing PRIVATE "${CMAKE_CURRENT_LIST_DIR}/ForcingsEngineDataProvider.cpp" + "${CMAKE_CURRENT_LIST_DIR}/ForcingsEngineLumpedDataProvider.cpp" ) target_link_libraries(forcing PUBLIC pybind11::embed NGen::ngen_bmi) endif() diff --git a/src/forcing/ForcingsEngineLumpedDataProvider.cpp b/src/forcing/ForcingsEngineLumpedDataProvider.cpp new file mode 100644 index 0000000000..f22e89ec25 --- /dev/null +++ b/src/forcing/ForcingsEngineLumpedDataProvider.cpp @@ -0,0 +1,134 @@ +#include "DataProvider.hpp" +#include +#include + +namespace data_access { + +using Provider = ForcingsEngineLumpedDataProvider; +using BaseProvider = Provider::base_type; + +std::size_t convert_divide_id_stoi(const std::string& divide_id) +{ + auto separator = divide_id.find('-'); + const char* split = ( + separator == std::string::npos + ? ÷_id[0] + : ÷_id[separator + 1] + ); + + return std::atol(split); +} + +Provider::ForcingsEngineLumpedDataProvider( + const std::string& init, + std::size_t time_begin_seconds, + std::size_t time_end_seconds, + const std::string& divide_id +) + : BaseProvider(init, time_begin_seconds, time_end_seconds) +{ + divide_id_ = convert_divide_id_stoi(divide_id); + + // Check that CAT-ID is an available output name, otherwise we most likely aren't + // running the correct configuration of the forcings engine for this class. + const auto cat_id_pos = std::find(var_output_names_.begin(), var_output_names_.end(), "CAT-ID"); + if (cat_id_pos == var_output_names_.end()) { + throw std::runtime_error{ + "Failed to initialize ForcingsEngineLumpedDataProvider: `CAT-ID` is not an output variable of the forcings engine." + " Does " + init + " have `GRID_TYPE` set to 'hydrofabric'?" + }; + } + var_output_names_.erase(cat_id_pos); + + const auto size_id_dimension = static_cast( + bmi_->GetVarNbytes("CAT-ID") / bmi_->GetVarItemsize("CAT-ID") + ); + + // Copy CAT-ID values into instance vector + const auto cat_id_span = boost::span( + static_cast(bmi_->GetValuePtr("CAT-ID")), + size_id_dimension + ); + + auto divide_id_pos = std::find(cat_id_span.begin(), cat_id_span.end(), divide_id_); + if (divide_id_pos == cat_id_span.end()) { + // throw std::runtime_error{"Unable to find divide ID `" + divide_id + "` in given Forcings Engine domain"}; + divide_idx_ = static_cast(-1); + } else { + divide_idx_ = std::distance(cat_id_span.begin(), divide_id_pos); + } +} + +std::size_t Provider::divide() const noexcept +{ + return divide_id_; +} + +std::size_t Provider::divide_index() const noexcept +{ + return divide_idx_; +} + +Provider::data_type Provider::get_value( + const Provider::selection_type& selector, + data_access::ReSampleMethod m +) +{ + assert(divide_id_ == convert_divide_id_stoi(selector.get_id())); + + auto variable = ensure_variable(selector.get_variable_name()); + + if (m == ReSampleMethod::SUM || m == ReSampleMethod::MEAN) { + double acc = 0.0; + const auto start = clock_type::from_time_t(selector.get_init_time()); + assert(start >= time_begin_); + + const auto end = std::chrono::seconds{selector.get_duration_secs()} + start; + assert(end <= time_end_); + + auto current = start; + while (current < end) { + current += time_step_; + bmi_->UpdateUntil(std::chrono::duration_cast(current - time_begin_).count()); + acc += static_cast(bmi_->GetValuePtr(variable))[divide_idx_]; + } + + if (m == ReSampleMethod::MEAN) { + auto duration = std::chrono::duration_cast(current - start).count(); + auto num_time_steps = duration / time_step_.count(); + acc /= num_time_steps; + } + + return acc; + } + + throw std::runtime_error{"Given ReSampleMethod " + std::to_string(m) + " not implemented."}; +} + +std::vector Provider::get_values( + const Provider::selection_type& selector, + data_access::ReSampleMethod /* unused */ +) +{ + assert(divide_id_ == convert_divide_id_stoi(selector.get_id())); + + auto variable = ensure_variable(selector.get_variable_name()); + + const auto start = clock_type::from_time_t(selector.get_init_time()); + assert(start >= time_begin_); + + const auto end = std::chrono::seconds{selector.get_duration_secs()} + start; + assert(end <= time_end_); + + std::vector values; + auto current = start; + while (current < end) { + current += time_step_; + bmi_->UpdateUntil(std::chrono::duration_cast(current - time_begin_).count()); + values.push_back(static_cast(bmi_->GetValuePtr(variable))[divide_idx_]); + } + + return values; +} + +} // namespace data_access diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 80f68377b8..0520cf4e91 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -187,12 +187,25 @@ ngen_add_test( test_forcings_engine OBJECTS forcing/ForcingsEngineDataProvider_Test.cpp + forcing/ForcingsEngineLumpedDataProvider_Test.cpp LIBRARIES NGen::forcing REQUIRES NGEN_WITH_PYTHON ) +if(TARGET test_forcings_engine) + set(FORCINGS_ENGINE_SCRATCH_DIR "${CMAKE_CURRENT_BINARY_DIR}/forcings_engine_scratch") + file(MAKE_DIRECTORY "${FORCINGS_ENGINE_SCRATCH_DIR}") + configure_file( + "${CMAKE_CURRENT_LIST_DIR}/../data/forcing/forcings-engine/config_aorc.yml.in" # Input + "${CMAKE_CURRENT_BINARY_DIR}/config_aorc.yml" + ) + target_compile_definitions(test_forcings_engine + PUBLIC + -DNGEN_LUMPED_CONFIG_PATH="${CMAKE_CURRENT_BINARY_DIR}/config_aorc.yml") +endif() + ########################## Series Unit Tests ngen_add_test( test_mdarray diff --git a/test/forcing/ForcingsEngineLumpedDataProvider_Test.cpp b/test/forcing/ForcingsEngineLumpedDataProvider_Test.cpp new file mode 100644 index 0000000000..5ad93f3993 --- /dev/null +++ b/test/forcing/ForcingsEngineLumpedDataProvider_Test.cpp @@ -0,0 +1,132 @@ +#include + +#ifndef NGEN_LUMPED_CONFIG_PATH +#error "Lumped config file path not defined! Set `-DNGEN_LUMPED_CONFIG_PATH`" +#endif + +#include +#if NGEN_WITH_MPI +#include +#endif + +#include +#include +#include + +#include + +struct ForcingsEngineLumpedDataProviderTest + : public testing::Test +{ + using provider_type = data_access::ForcingsEngineLumpedDataProvider; + + ForcingsEngineLumpedDataProviderTest() + { + #if NGEN_WITH_MPI + MPI_Comm_size(MPI_COMM_WORLD, &mpi_.size); + MPI_Comm_rank(MPI_COMM_WORLD, &mpi_.rank); + #endif + } + + static constexpr const char* config_file = NGEN_LUMPED_CONFIG_PATH; + static const std::time_t time_start; + static const std::time_t time_end; + static std::shared_ptr gil_; + static std::unique_ptr provider_; + + struct { int initialized = 0, finalized = 0, size = 1, rank = 0; } mpi_{}; + + static void SetUpTestSuite(); + static void TearDownTestSuite(); +}; + +using TestFixture = ForcingsEngineLumpedDataProviderTest; + +constexpr const char* TestFixture::config_file; +const std::time_t TestFixture::time_start = data_access::detail::parse_time("2023-01-17 01:00:00"); +const std::time_t TestFixture::time_end = TestFixture::time_start + 3600 + 3600; + +std::shared_ptr TestFixture::gil_ = nullptr; +std::unique_ptr TestFixture::provider_ = nullptr; + +void TestFixture::SetUpTestSuite() +{ + #if NGEN_WITH_MPI + MPI_Init(nullptr, nullptr); + #endif + + TestFixture::gil_ = utils::ngenPy::InterpreterUtil::getInstance(); + + data_access::detail::assert_forcings_engine_requirements(); + + TestFixture::provider_ = std::make_unique( + /*init=*/TestFixture::config_file, + /*time_begin_seconds=*/TestFixture::time_start, + /*time_end_seconds=*/TestFixture::time_end, + /*divide_id=*/"cat-11223" + ); +} + +void TestFixture::TearDownTestSuite() +{ + data_access::detail::ForcingsEngineStorage::instances.clear(); + gil_.reset(); + + #if NGEN_WITH_MPI + PMPI_Finalize(); + #endif +} + +/** + * Tests for the flyweight-like design of provider storage by getting + * a new instance of the forcings engine and verifying that it points + * to the same address as the static initialized `provider_` member, + * based on matching `init`, and shared over distinct `divide_id`. + */ +TEST_F(ForcingsEngineLumpedDataProviderTest, Storage) +{ + auto new_inst = std::make_unique( + /*init=*/TestFixture::config_file, + /*time_begin_seconds=*/TestFixture::time_start, + /*time_end_seconds=*/TestFixture::time_end, + /*divide_id=*/"cat-11371" + ); + + ASSERT_EQ(new_inst->model(), provider_->model()); +} + +TEST_F(ForcingsEngineLumpedDataProviderTest, VariableAccess) +{ + ASSERT_EQ(provider_->divide(), 11223UL); + ASSERT_EQ(provider_->divide_index(), 0); + + constexpr std::array expected_variables = { + "U2D_ELEMENT", + "V2D_ELEMENT", + "LWDOWN_ELEMENT", + "SWDOWN_ELEMENT", + "T2D_ELEMENT", + "Q2D_ELEMENT", + "PSFC_ELEMENT", + "RAINRATE_ELEMENT" + }; + + const auto outputs = provider_->get_available_variable_names(); + + // Check that each expected variable is in the list of available outputs. + for (const auto& expected : expected_variables) { + EXPECT_NE( + std::find(outputs.begin(), outputs.end(), expected), + outputs.end() + ); + } + + auto selector = CatchmentAggrDataSelector{"cat-11223", "PSFC", time_start, 3600, "seconds"}; + auto result = provider_->get_value(selector, data_access::ReSampleMethod::SUM); + EXPECT_NEAR(result, 99580.52, 1e-2); + + selector = CatchmentAggrDataSelector{"cat-11223", "LWDOWN", time_start + 3600, 3600, "seconds"}; + auto result2 = provider_->get_values(selector, data_access::ReSampleMethod::SUM); + ASSERT_GT(result2.size(), 0); + EXPECT_NEAR(result2[0], 0, 1e-6); +}