generated from NOAA-OWP/owp-open-source-project-template
-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Forcings Engine Data Provider Base Interface #839
Merged
program--
merged 12 commits into
NOAA-OWP:master
from
program--:jsm-forcings-engine-provider
Jul 1, 2024
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
7e8860f
refactor: simplify generic data provider to type alias
program-- d5b7d3d
refactor: separate NullForcingProvider into header/source files
program-- ecdee98
feat: implement Forcings Engine data provider base class
program-- fc891af
rev: simplify construction; include ctime header; safer fp conversion
program-- 72b2125
rev: fix bmi_ member shadowing; add more docs; rm time_current_index_
program-- fc299fb
Typo fix in comment
PhilMiller 17656e0
Add header necessitated by #840 dropping some includes
PhilMiller a77ba22
rev: remove StreamHandler arg to match #840 changes
program-- a5e7b32
rev: std headers; const ref string arg; rm StreamHandler include
program-- 03e3880
rev: add forcings engine test file; add explicit template instatiation
program-- b01255d
rev: Drop argument that was rebased out of Bmi_Py_Adapter constructor
PhilMiller 7489c7e
rev: add NGEN_WITH_PYTHON guard to ForcingsEngineDataProvider.hpp
program-- File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
#pragma once | ||
|
||
#include <NGenConfig.h> | ||
|
||
#if NGEN_WITH_PYTHON | ||
|
||
#include <cmath> | ||
#include <chrono> | ||
#include <memory> | ||
#include <stdexcept> | ||
#include <string> | ||
#include <unordered_map> | ||
#include <vector> | ||
|
||
#include "DataProvider.hpp" | ||
#include "bmi/Bmi_Py_Adapter.hpp" | ||
|
||
namespace data_access { | ||
|
||
//! Python module name for NextGen Forcings Engine | ||
static constexpr auto forcings_engine_python_module = "NextGen_Forcings_Engine"; | ||
|
||
//! Python classname for NextGen Forcings Engine BMI class | ||
static constexpr auto forcings_engine_python_class = "NWMv3_Forcing_Engine_BMI_model"; | ||
|
||
//! Full Python classpath for Forcings Engine BMI class | ||
static constexpr auto forcings_engine_python_classpath = "NextGen_Forcings_Engine.NWMv3_Forcing_Engine_BMI_model"; | ||
|
||
//! Default time format used for parsing timestamp strings | ||
static constexpr auto default_time_format = "%Y-%m-%d %H:%M:%S"; | ||
|
||
namespace detail { | ||
|
||
//! Parse time string from format. | ||
//! Utility function for ForcingsEngineLumpedDataProvider constructor. | ||
time_t parse_time(const std::string& time, const std::string& fmt); | ||
|
||
//! Check that requirements for running the forcings engine | ||
//! are available at runtime. If requirements are not available, | ||
//! then this function throws. | ||
void assert_forcings_engine_requirements(); | ||
|
||
//! Storage for Forcings Engine-specific BMI instances. | ||
struct ForcingsEngineStorage { | ||
//! Key type for Forcings Engine storage, storing file paths to initialization files. | ||
using key_type = std::string; | ||
|
||
//! BMI adapter type used by the Python-based Forcings Engine. | ||
using bmi_type = models::bmi::Bmi_Py_Adapter; | ||
|
||
//! Value type stored, shared pointer to BMI adapter. | ||
using value_type = std::shared_ptr<bmi_type>; | ||
|
||
static ForcingsEngineStorage instances; | ||
|
||
//! Get a Forcings Engine instance. | ||
//! @param key Initialization file path for Forcings Engine instance. | ||
//! @return Shared pointer to a Forcings Engine BMI instance, or @c nullptr if it has not | ||
//! been created yet. | ||
value_type get(const key_type& key) | ||
{ | ||
auto pos = data_.find(key); | ||
if (pos == data_.end()) { | ||
return nullptr; | ||
} | ||
|
||
return pos->second; | ||
} | ||
|
||
//! Associate a Forcings Engine instance to a file path. | ||
//! @param key Initialization file path for Forcings Engine instance. | ||
//! @param value Shared pointer to a Forcings Engine BMI instance. | ||
void set(const key_type& key, value_type value) | ||
{ | ||
data_[key] = value; | ||
} | ||
|
||
//! Clear all references to Forcings Engine instances. | ||
//! @note This will not necessarily destroy the Forcings Engine instances. Since they | ||
//! are reference counted, it will only decrement their instance by one. | ||
void clear() | ||
{ | ||
data_.clear(); | ||
} | ||
|
||
private: | ||
//! Instance map of underlying BMI models. | ||
std::unordered_map<key_type, value_type> data_; | ||
}; | ||
|
||
} // namespace detail | ||
|
||
|
||
//! Forcings Engine Data Provider | ||
//! @tparam DataType Data type for values returned from derived classes | ||
//! @tparam SelectionType Selector type for querying data from derived classes | ||
template<typename DataType, typename SelectionType> | ||
struct ForcingsEngineDataProvider | ||
: public DataProvider<DataType, SelectionType> | ||
{ | ||
using data_type = DataType; | ||
using selection_type = SelectionType; | ||
using clock_type = std::chrono::system_clock; | ||
|
||
~ForcingsEngineDataProvider() override = default; | ||
|
||
boost::span<const std::string> get_available_variable_names() override | ||
{ | ||
return var_output_names_; | ||
} | ||
|
||
long get_data_start_time() override | ||
{ | ||
return clock_type::to_time_t(time_begin_); | ||
} | ||
|
||
long get_data_stop_time() override | ||
{ | ||
return clock_type::to_time_t(time_end_); | ||
} | ||
|
||
long record_duration() override | ||
{ | ||
return std::chrono::duration_cast<std::chrono::seconds>(time_step_).count(); | ||
} | ||
|
||
size_t get_ts_index_for_time(const time_t& epoch_time) override | ||
{ | ||
const auto epoch = clock_type::from_time_t(epoch_time); | ||
|
||
if (epoch < time_begin_ || epoch > time_end_) { | ||
throw std::out_of_range{ | ||
"epoch " + std::to_string(epoch.time_since_epoch().count()) + | ||
" out of range of " + std::to_string(time_begin_.time_since_epoch().count()) + ", " | ||
+ std::to_string(time_end_.time_since_epoch().count()) | ||
}; | ||
} | ||
|
||
return (epoch - time_begin_) / time_step_; | ||
} | ||
|
||
std::shared_ptr<models::bmi::Bmi_Py_Adapter> model() noexcept | ||
{ | ||
return bmi_; | ||
} | ||
|
||
/* Remaining virtual member functions from DataProvider must be implemented | ||
by derived classes. */ | ||
|
||
data_type get_value( | ||
const selection_type& selector, | ||
data_access::ReSampleMethod m | ||
) override = 0; | ||
|
||
std::vector<data_type> get_values( | ||
const selection_type& selector, | ||
data_access::ReSampleMethod m | ||
) override = 0; | ||
|
||
protected: | ||
using storage_type = detail::ForcingsEngineStorage; | ||
|
||
//! Forcings Engine Data Provider Constructor | ||
//! | ||
//! @note Derived implementations should delegate to this constructor | ||
//! to acquire a shared forcings engine instance. | ||
ForcingsEngineDataProvider( | ||
const std::string& init, | ||
std::size_t time_begin_seconds, | ||
std::size_t time_end_seconds | ||
) | ||
: time_begin_(std::chrono::seconds{time_begin_seconds}) | ||
, time_end_(std::chrono::seconds{time_end_seconds}) | ||
{ | ||
// Get a forcings engine instance if it exists for this initialization file | ||
bmi_ = storage_type::instances.get(init); | ||
|
||
// If it doesn't exist, create it and assign it to the storage map | ||
if (bmi_ == nullptr) { | ||
// Outside of this branch, this->bmi_ != nullptr after this | ||
bmi_ = std::make_shared<models::bmi::Bmi_Py_Adapter>( | ||
"ForcingsEngine", | ||
init, | ||
forcings_engine_python_classpath, | ||
/*has_fixed_time_step=*/true | ||
); | ||
|
||
storage_type::instances.set(init, bmi_); | ||
} | ||
|
||
// Now, initialize the BMI dependent instance members | ||
// NOTE: using std::lround instead of static_cast will prevent potential UB | ||
time_step_ = std::chrono::seconds{std::lround(bmi_->GetTimeStep())}; | ||
var_output_names_ = bmi_->GetOutputVarNames(); | ||
} | ||
|
||
//! Forcings Engine instance | ||
std::shared_ptr<models::bmi::Bmi_Py_Adapter> bmi_ = nullptr; | ||
|
||
//! Output variable names | ||
std::vector<std::string> var_output_names_{}; | ||
|
||
private: | ||
//! Initialization config file path | ||
std::string init_; | ||
|
||
//! Calendar time for simulation beginning | ||
clock_type::time_point time_begin_{}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We probably need an official style rule on member data names (or names in general), we have at least 4 styles used between various files |
||
|
||
//! Calendar time for simulation end | ||
clock_type::time_point time_end_{}; | ||
|
||
//! Duration of a single simulation tick | ||
clock_type::duration time_step_{}; | ||
}; | ||
|
||
} // namespace data_access | ||
|
||
#endif // NGEN_WITH_PYTHON |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
#include <forcing/ForcingsEngineDataProvider.hpp> | ||
program-- marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#include <utilities/python/InterpreterUtil.hpp> | ||
|
||
#include <ctime> // timegm | ||
#include <iomanip> // std::get_time | ||
|
||
namespace data_access { | ||
namespace detail { | ||
|
||
// Initialize instance storage | ||
ForcingsEngineStorage ForcingsEngineStorage::instances{}; | ||
|
||
time_t parse_time(const std::string& time, const std::string& fmt) | ||
{ | ||
std::tm tm_ = {}; | ||
std::stringstream tmstr{time}; | ||
tmstr >> std::get_time(&tm_, fmt.c_str()); | ||
|
||
// Note: `timegm` is available for Linux and macOS via time.h, but not Windows. | ||
return timegm(&tm_); | ||
} | ||
|
||
void assert_forcings_engine_requirements() | ||
{ | ||
// Check that the python module is installed. | ||
{ | ||
auto interpreter_ = utils::ngenPy::InterpreterUtil::getInstance(); | ||
try { | ||
auto mod = interpreter_->getModule(forcings_engine_python_module); | ||
auto cls = mod.attr(forcings_engine_python_class).cast<py::object>(); | ||
} catch(std::exception& e) { | ||
throw std::runtime_error{ | ||
"Failed to initialize ForcingsEngine: ForcingsEngine python module is not installed or is not properly configured. (" + std::string{e.what()} + ")" | ||
}; | ||
} | ||
} | ||
|
||
// Check that the WGRIB2 environment variable is defined | ||
{ | ||
const auto* wgrib2_exec = std::getenv("WGRIB2"); | ||
if (wgrib2_exec == nullptr) { | ||
throw std::runtime_error{"Failed to initialize ForcingsEngine: $WGRIB2 is not defined"}; | ||
} | ||
} | ||
} | ||
|
||
} // namespace detail | ||
} // namespace data_access |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is probably a fatal error on this function is fine I would still want a check form of the function but it is not important