diff --git a/build_test_sfincs.sh b/build_test_sfincs.sh new file mode 100644 index 0000000000..b7c368dbb1 --- /dev/null +++ b/build_test_sfincs.sh @@ -0,0 +1,10 @@ +cmake -B cmake_build -S . \ + -DNGEN_WITH_BMI_FORTRAN=ON \ + -DNGEN_BUILD_COASTAL_TESTS=ON \ + -DNGEN_ENABLE_SCHISM=OFF \ + -DSFINCS_BMI_LIBRARY=/home/mohammed.karim/Calibration/ngen/extern/SFINCS/source/src/build/libsfincs_bmi.so \ + -DSFINCS_INIT_CONFIG=/home/mohammed.karim/Calibration/ngen/extern/SFINCS/source/src/build/sfincs_config.txt +cmake --build cmake_build -j +ctest --test-dir cmake_build -N | grep -i sfincs +ctest --test-dir cmake_build -R sfincs -V + diff --git a/include/realizations/coastal/SfincsCreator.h b/include/realizations/coastal/SfincsCreator.h new file mode 100644 index 0000000000..36fb0597db --- /dev/null +++ b/include/realizations/coastal/SfincsCreator.h @@ -0,0 +1,21 @@ +#ifndef SFINCS_CREATOR_HEADER +#define SFINCS_CREATOR_HEADER + +#include "realizations/coastal/ModelCreator.h" +#include "realizations/coastal/Coastal_Config_Params.h" + +class SfincsCreator : public ModelCreator { +public: + std::unique_ptr + createCoastalFormulation(coastal_config_params const&, + Simulation_Time const&) const override; + + SfincsCreator* clone() const override; + +private: + void writeInitConfig(coastal_config_params const&, + Simulation_Time const&) const; +}; + +#endif // SFINCS_CREATOR_HEADER + diff --git a/include/realizations/coastal/SfincsFormulation.hpp b/include/realizations/coastal/SfincsFormulation.hpp new file mode 100644 index 0000000000..fe9078ba15 --- /dev/null +++ b/include/realizations/coastal/SfincsFormulation.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include + +#include + +#include "NGenConfig.h" + +#include "realizations/coastal/CoastalFormulation.hpp" + +// BMI Fortran adapter (correct include + namespace) +#if NGEN_WITH_BMI_FORTRAN + #include "bmi/Bmi_Fortran_Adapter.hpp" +#endif + +/** + * SfincsFormulation + * + * Mirrors the structure of SchismFormulation: + * - derives from CoastalFormulation (which derives MeshPointsDataProvider) + * - provides required DataProvider<> pure virtuals + * - owns a BMI adapter instance + * - optionally consumes met/offshore/channel boundary providers + */ +class SfincsFormulation final : public CoastalFormulation +{ +public: + // Match SchismFormulation style typedefs + using ProviderType = data_access::MeshPointsDataProvider; + using ProviderPtr = std::shared_ptr; + + SfincsFormulation(std::string model_id, + std::string library_file, + std::string init_config, + ProviderPtr met_provider, + ProviderPtr offshore_provider, + ProviderPtr channel_provider); + + ~SfincsFormulation() override; + + // --- BMI lifecycle --- + void initialize() override; + void finalize() override; + void update() override; + void update_until(double const& t) override; + + // --- Time --- + double get_current_time() override; + double get_start_time() override; + double get_end_time() override; + double get_time_step() override; + + // --- MeshPointsDataProvider interface (from MeshPointsDataProvider) --- + // Required by CoastalFormulation (pure virtual) + void get_values(const selection_type& selector, boost::span data) override; + + // Optional convenience overload (NOT override) + void get_values(const selection_type& selector, std::vector& out); + + std::size_t mesh_size(const std::string& mesh_name) override; + + // --- DataProvider pure virtuals --- + boost::span get_available_variable_names() const override; + long get_data_start_time() const override; + long get_data_stop_time() const override; + long record_duration() const override; + std::size_t get_ts_index_for_time(const time_t& epoch_time) const override; + data_type get_value(const selection_type& selector, data_access::ReSampleMethod m=data_access::SUM) override; + +private: + void create_formulation_(); + void destroy_formulation_(); + + // Optional: push forcing into BMI vars (keep minimal for now) + void set_inputs_(); + +private: + std::string model_id_; + std::string library_file_; + std::string init_config_; + + ProviderPtr met_provider_; + ProviderPtr offshore_provider_; + ProviderPtr channel_provider_; + + std::vector available_vars_; + +#if NGEN_WITH_BMI_FORTRAN + std::unique_ptr bmi_; +#endif +}; + diff --git a/src/NGen.cpp b/src/NGen.cpp index 349aeed960..ef0b50357a 100644 --- a/src/NGen.cpp +++ b/src/NGen.cpp @@ -10,7 +10,12 @@ #include #include #include "realizations/coastal/ModelCreatorRegistry.h" + +#if NGEN_ENABLE_SCHISM #include "realizations/coastal/SchismCreator.h" +#endif + +#include "realizations/coastal/SfincsCreator.h" #if NGEN_WITH_SQLITE3 #include @@ -51,7 +56,7 @@ bool is_subdivided_hydrofabric_wanted = false; #include "core/Partition_Parser.hpp" #include -#include "core/Partition_One.hpp" +#include "core/Partition_One.hpp>" std::string PARTITION_PATH = ""; #endif // NGEN_WITH_MPI @@ -608,21 +613,25 @@ int main(int argc, char *argv[]) { // create the factory registry ModelCreatorRegistry ®istry = ModelCreatorRegistry::getInstance(); - // add the Schism factory to the registry + + // register all supported coastal models + #if NGEN_ENABLE_SCHISM registry.registerCreator(ModelType::SCHISM, std::make_unique()); - // add more factories for coastal models, e.g. sfincs - //.... - - // retrieve the creator for the model selected + #endif + + registry.registerCreator(ModelType::SFINCS, std::make_unique()); // + + // retrieve the creator for the model selected in the config std::unique_ptr coastal_creator = registry.getCreator(coastal_conf->getModelType()); - // now run the schism model + + // execute the selected coastal model (SCHISM or SFINCS) coastal_creator->executeModel( *coastal_conf, *(manager->Simulation_Time_Object) ); } - manager->finalize(); + manager->finalize(); #if NGEN_WITH_MPI MPI_Finalize(); @@ -630,3 +639,4 @@ int main(int argc, char *argv[]) { return 0; } + diff --git a/src/realizations/coastal/CMakeLists.txt b/src/realizations/coastal/CMakeLists.txt index 397fbc2075..5f2e6b130a 100644 --- a/src/realizations/coastal/CMakeLists.txt +++ b/src/realizations/coastal/CMakeLists.txt @@ -1,23 +1,187 @@ -include(${PROJECT_SOURCE_DIR}/cmake/dynamic_sourced_library.cmake) -dynamic_sourced_cxx_library(realizations_coastal "${CMAKE_CURRENT_SOURCE_DIR}") +cmake_minimum_required(VERSION 3.19) + +# If we are configured standalone in this directory, define a minimal project so CMake is happy. +if(NOT DEFINED PROJECT_NAME) + project(ngen_coastal LANGUAGES C CXX) +endif() + +option(NGEN_ENABLE_SCHISM "Enable SCHISM coastal model support" OFF) + +if (NGEN_ENABLE_SCHISM) + add_compile_definitions(NGEN_ENABLE_SCHISM=1) +else() + add_compile_definitions(NGEN_ENABLE_SCHISM=0) +endif() + + +# Try to infer the repo root when configured from src/realizations/coastal +# ngen root is 3 levels up from here: ngen/src/realizations/coastal +set(NGEN_TOP "${CMAKE_CURRENT_LIST_DIR}/../../.." CACHE PATH "Path to ngen repository root") + +# --------------------------------------------------------------------------- +# Bring in the helper that defines dynamic_sourced_cxx_library, if available +# When configuring from the repo root, PROJECT_SOURCE_DIR will already be the top. +# Here we use NGEN_TOP so this file also works standalone. +# --------------------------------------------------------------------------- +set(_DYNAMIC_LIB_HELPER "${NGEN_TOP}/cmake/dynamic_sourced_library.cmake") +if(EXISTS "${_DYNAMIC_LIB_HELPER}") + include("${_DYNAMIC_LIB_HELPER}") +else() + message(STATUS "dynamic_sourced_library.cmake not found at: ${_DYNAMIC_LIB_HELPER}") + message(STATUS "This coastal CMakeLists is meant to be driven from the repo root.") + message(STATUS "Continuing, but you must provide equivalent targets manually or configure from ${NGEN_TOP}") +endif() + +# --------------------------------------------------------------------------- +# Library: realizations_coastal +# --------------------------------------------------------------------------- + +# Only create the library via the helper if it exists +if(COMMAND dynamic_sourced_cxx_library) + include("${NGEN_TOP}/cmake/dynamic_sourced_library.cmake") # ensure scope + dynamic_sourced_cxx_library(realizations_coastal "${CMAKE_CURRENT_SOURCE_DIR}") +else() + # Fallback: collect all .cpp files in this directory tree and build a static lib + file(GLOB_RECURSE REALIZATIONS_COASTAL_SRC CONFIGURE_DEPENDS + "${CMAKE_CURRENT_LIST_DIR}/*.cpp" + ) + add_library(realizations_coastal STATIC ${REALIZATIONS_COASTAL_SRC}) +endif() add_library(NGen::realizations_coastal ALIAS realizations_coastal) +# --------------------------------------------------------------- +# Toggle SCHISM coastal integration (DEFAULT: ON) +# Disable with: -DNGEN_ENABLE_SCHISM=OFF +# --------------------------------------------------------------- +option(NGEN_ENABLE_SCHISM "Build SCHISM coastal integration" ON) + +if(NOT NGEN_ENABLE_SCHISM) + # Mark SCHISM sources as header-only so they are not compiled/linked + set(_schism_srcs + ${CMAKE_CURRENT_LIST_DIR}/SchismCreator.cpp + ${CMAKE_CURRENT_LIST_DIR}/SchismFormulation.cpp + ) + foreach(_s IN LISTS _schism_srcs) + if(EXISTS "${_s}") + set_source_files_properties(${_s} PROPERTIES HEADER_FILE_ONLY TRUE) + message(STATUS "Coastal: excluding SCHISM source from build: ${_s}") + endif() + endforeach() + + # Also exclude the SCHISM coastal test if present in the repo + set(_schism_test "${NGEN_TOP}/test/coastal/SchismFormulation_Test.cpp") + if(EXISTS "${_schism_test}") + set_source_files_properties("${_schism_test}" PROPERTIES HEADER_FILE_ONLY TRUE) + message(STATUS "Tests: excluding SCHISM coastal test: ${_schism_test}") + endif() +endif() + +# Public include paths target_include_directories(realizations_coastal PUBLIC - ${PROJECT_SOURCE_DIR}/include/core - ${PROJECT_SOURCE_DIR}/include/realizations/coastal - ${PROJECT_SOURCE_DIR}/include/forcing - ${PROJECT_SOURCE_DIR}/include/simulation_time - ${PROJECT_SOURCE_DIR}/include/utilities - ${PROJECT_SOURCE_DIR}/include/coastal - ${PROJECT_SOURCE_DIR}/include/bmi - ) - -target_link_libraries(realizations_coastal PUBLIC - ${CMAKE_DL_LIBS} - NGen::config_header - NGen::geojson - NGen::logging - NGen::ngen_bmi - ) + ${NGEN_TOP}/include/core + ${NGEN_TOP}/include/realizations/coastal + ${NGEN_TOP}/include/forcing + ${NGEN_TOP}/include/simulation_time + ${NGEN_TOP}/include/utilities + ${NGEN_TOP}/include/coastal + ${NGEN_TOP}/include/bmi +) + +# Link to imported targets if they exist; otherwise, warn clearly when standalone +set(_missing_imports "") +foreach(_tgt NGen::config_header NGen::geojson NGen::logging NGen::ngen_bmi) + if(NOT TARGET ${_tgt}) + list(APPEND _missing_imports ${_tgt}) + endif() +endforeach() + +if(_missing_imports) + message(STATUS "The following imported targets are missing: ${_missing_imports}") + message(STATUS "You are likely configuring this subfolder standalone.") + message(STATUS "For a full build, configure from the repo root: ${NGEN_TOP}") +endif() + +# Always link libdl if available; conditionally link NGen targets if present +target_link_libraries(realizations_coastal PUBLIC ${CMAKE_DL_LIBS}) +if(TARGET NGen::config_header) + target_link_libraries(realizations_coastal PUBLIC NGen::config_header) +endif() +if(TARGET NGen::geojson) + target_link_libraries(realizations_coastal PUBLIC NGen::geojson) +endif() +if(TARGET NGen::logging) + target_link_libraries(realizations_coastal PUBLIC NGen::logging) +endif() +if(TARGET NGen::ngen_bmi) + target_link_libraries(realizations_coastal PUBLIC NGen::ngen_bmi) +endif() + +# --------------------------------------------------------------------------- +# Optional SFINCS tests (no MPI) +# Build only if we have Fortran BMI enabled at configure time and NGen targets exist +# --------------------------------------------------------------------------- +option(NGEN_BUILD_COASTAL_TESTS "Build coastal realization tests" ON) +option(NGEN_WITH_BMI_FORTRAN "Enable BMI Fortran adapter in NGen" OFF) # default OFF if standalone + +if(NGEN_BUILD_COASTAL_TESTS AND NGEN_WITH_BMI_FORTRAN) + include(CTest) + + # Provide convenient cache vars for the SFINCS BMI lib and init file + set(SFINCS_BMI_LIBRARY "${NGEN_TOP}/extern/SFINCS/source/src/build/libsfincs_bmi.so" + CACHE FILEPATH "Path to libsfincs_bmi shared library") + set(SFINCS_INIT_CONFIG "${NGEN_TOP}/extern/SFINCS/source/src/build/sfincs_config.txt" + CACHE FILEPATH "Path to SFINCS BMI initialize() config file") + + # -------- test_sfincs_formulation_smoke (only if the test source exists) -------- + set(_sfincs_smoke_src "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_sfincs_formulation_smoke.cpp") + if(EXISTS "${_sfincs_smoke_src}") + add_executable(test_sfincs_formulation_smoke "${_sfincs_smoke_src}") + target_include_directories(test_sfincs_formulation_smoke PRIVATE + ${NGEN_TOP}/include + ) + target_link_libraries(test_sfincs_formulation_smoke PRIVATE + realizations_coastal + ) + if(TARGET NGen::ngen_bmi) + target_link_libraries(test_sfincs_formulation_smoke PRIVATE NGen::ngen_bmi) + endif() + if(TARGET NGen::logging) + target_link_libraries(test_sfincs_formulation_smoke PRIVATE NGen::logging) + endif() + + add_test(NAME test_sfincs_formulation_smoke + COMMAND test_sfincs_formulation_smoke + ${SFINCS_BMI_LIBRARY} + ${SFINCS_INIT_CONFIG}) + else() + message(STATUS "Skipping test_sfincs_formulation_smoke: file not found at ${_sfincs_smoke_src}") + endif() + + # -------- test_sfincs_creator_smoke (optional helper) -------- + set(_sfincs_creator_src "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_sfincs_creator_smoke.cpp") + if(EXISTS "${_sfincs_creator_src}") + add_executable(test_sfincs_creator_smoke "${_sfincs_creator_src}") + target_include_directories(test_sfincs_creator_smoke PRIVATE + ${NGEN_TOP}/include + ) + target_link_libraries(test_sfincs_creator_smoke PRIVATE + realizations_coastal + ) + if(TARGET NGen::ngen_bmi) + target_link_libraries(test_sfincs_creator_smoke PRIVATE NGen::ngen_bmi) + endif() + if(TARGET NGen::logging) + target_link_libraries(test_sfincs_creator_smoke PRIVATE NGen::logging) + endif() + + add_test(NAME test_sfincs_creator_smoke + COMMAND test_sfincs_creator_smoke + ${SFINCS_BMI_LIBRARY} + ${SFINCS_INIT_CONFIG}) + else() + message(STATUS "Skipping test_sfincs_creator_smoke: file not found at ${_sfincs_creator_src}") + endif() + +endif() diff --git a/src/realizations/coastal/Coastal_Config_Params.cpp b/src/realizations/coastal/Coastal_Config_Params.cpp index df953215e3..8e306c982e 100644 --- a/src/realizations/coastal/Coastal_Config_Params.cpp +++ b/src/realizations/coastal/Coastal_Config_Params.cpp @@ -1,5 +1,5 @@ /* - * Implement the coastal_config_params classs + * Implement the coastal_config_params class */ #include "utilities/logging_utils.h" @@ -7,88 +7,174 @@ bool coastal_config_params::isValid() { - boost::property_tree::ptree::const_assoc_iterator it; - it = params.find("params"); - if ( it == params.not_found() ) - { - logging::critical(std::string("\"params\" not definded in coastal realization!\n").c_str()); - return false; - } - boost::property_tree::ptree params_tree = params.get_child( "params" ); - it = params_tree.find("model_type_name"); - if ( it == params_tree.not_found() ) - { - logging::critical(std::string("\"model_type_name\" not definded in coastal realization!\n").c_str()); - return false; - } - it = params_tree.find("library_file"); - if ( it == params_tree.not_found() ) - { - logging::critical(std::string("\"library_file\" not definded in coastal realization!\n").c_str()); - return false; - } - - it = params_tree.find("model_start_time_in_secs"); - if ( it == params_tree.not_found() ) - { - logging::critical(std::string("\"model_start_time_in_secs\" not definded in coastal realization!\n").c_str()); - return false; - } - - it = params_tree.find("met_forcing_netcdf_path"); - if ( it == params_tree.not_found() ) - { - logging::critical(std::string("\"met_forcing_netcdf_path\" not definded in coastal realization!\n").c_str()); - return false; - } - it = params_tree.find("offshore_boundary_netcdf_path"); - if ( it == params.not_found() ) - { - logging::critical(std::string("\"offshore_boundary_netcdf_path\" not definded in coastal realization!\n").c_str()); - return false; - } - it = params_tree.find("offshore_boundary_netcdf_path"); - if ( it == params_tree.not_found() ) - { - logging::critical(std::string("\"offshore_boundary_netcdf_path\" not definded in coastal realization!\n").c_str()); - return false; - } - it = params_tree.find("offshore_boundary_netcdf_path"); - if ( it == params_tree.not_found() ) - { - logging::critical(std::string("\"offshore_boundary_netcdf_path\" not definded in coastal realization!\n").c_str()); - return false; - } - it = params_tree.find("streamflow_boundary_netcdf_path"); - if ( it == params_tree.not_found() ) - { - logging::critical(std::string("\"streamflow_boundary_netcdf_path\" not definded in coastal realization!\n").c_str()); - return false; - } - it = params_tree.find("nscribs"); - if ( it == params_tree.not_found() ) - { - logging::critical(std::string("\"nscribs\" not definded in coastal realization!\n").c_str()); - return false; - } - return true; + using boost::property_tree::ptree; + + auto it = params.find("params"); + if (it == params.not_found()) { + logging::critical("\"params\" not definded in coastal realization!\n"); + return false; + } + + ptree params_tree = params.get_child("params"); + + auto require_key = [&](const char* key) -> bool { + if (params_tree.find(key) == params_tree.not_found()) { + logging::critical((std::string("\"") + key + "\" not definded in coastal realization!\n").c_str()); + return false; + } + return true; + }; + + // always required + if (!require_key("model_type_name")) return false; + if (!require_key("library_file")) return false; + if (!require_key("model_start_time_in_secs")) return false; + if (!require_key("nscribs")) return false; + if (!require_key("working_dir")) return false; + + const std::string model_type_name = params_tree.get("model_type_name"); + + // SCHISM requires NetCDF forcing paths + if (model_type_name == "schism_coastal_formulation") { + if (!require_key("met_forcing_netcdf_path")) return false; + if (!require_key("offshore_boundary_netcdf_path")) return false; + if (!require_key("streamflow_boundary_netcdf_path")) return false; + } + // SFINCS does NOT require them (for now) + else if (model_type_name == "bmi_fortran_sfincs") { + // optional: allow empty or missing netcdf paths + } + else { + logging::critical((std::string("Unknown coastal type: ") + model_type_name).c_str()); + return false; + } + + return true; +} + +/* +bool coastal_config_params::isValid() +{ + boost::property_tree::ptree::const_assoc_iterator it; + + it = params.find("params"); + if (it == params.not_found()) { + logging::critical("\"params\" not definded in coastal realization!\n"); + return false; + } + + boost::property_tree::ptree params_tree = params.get_child("params"); + + auto require_key = [&](const char* key) -> bool { + if (params_tree.find(key) == params_tree.not_found()) { + logging::critical((std::string("\"") + key + "\" not definded in coastal realization!\n").c_str()); + return false; + } + return true; + }; + + if (!require_key("model_type_name")) return false; + if (!require_key("library_file")) return false; + if (!require_key("model_start_time_in_secs")) return false; + if (!require_key("nscribs")) return false; + + // Only require forcing paths for SCHISM (SFINCS can run without them initially) + const std::string model_type = params_tree.get("model_type_name"); + if (model_type == "schism_coastal_formulation") { + if (!require_key("met_forcing_netcdf_path")) return false; + if (!require_key("offshore_boundary_netcdf_path")) return false; + if (!require_key("streamflow_boundary_netcdf_path")) return false; + } + + return true; } + +bool coastal_config_params::isValid() +{ + boost::property_tree::ptree::const_assoc_iterator it; + + // Top-level "params" subtree must exist + it = params.find("params"); + if (it == params.not_found()) + { + logging::critical("\"params\" not definded in coastal realization!\n"); + return false; + } + + boost::property_tree::ptree params_tree = params.get_child("params"); + + // Required fields for any coastal model (SCHISM, SFINCS, etc.) + it = params_tree.find("model_type_name"); + if (it == params_tree.not_found()) + { + logging::critical("\"model_type_name\" not definded in coastal realization!\n"); + return false; + } + + it = params_tree.find("library_file"); + if (it == params_tree.not_found()) + { + logging::critical("\"library_file\" not definded in coastal realization!\n"); + return false; + } + + it = params_tree.find("model_start_time_in_secs"); + if (it == params_tree.not_found()) + { + logging::critical("\"model_start_time_in_secs\" not definded in coastal realization!\n"); + return false; + } + + it = params_tree.find("met_forcing_netcdf_path"); + if (it == params_tree.not_found()) + { + logging::critical("\"met_forcing_netcdf_path\" not definded in coastal realization!\n"); + return false; + } + + it = params_tree.find("offshore_boundary_netcdf_path"); + if (it == params_tree.not_found()) + { + logging::critical("\"offshore_boundary_netcdf_path\" not definded in coastal realization!\n"); + return false; + } + + it = params_tree.find("streamflow_boundary_netcdf_path"); + if (it == params_tree.not_found()) + { + logging::critical("\"streamflow_boundary_netcdf_path\" not definded in coastal realization!\n"); + return false; + } + + it = params_tree.find("nscribs"); + if (it == params_tree.not_found()) + { + logging::critical("\"nscribs\" not definded in coastal realization!\n"); + return false; + } + + return true; +} +*/ + ModelType coastal_config_params::getModelType() { - std::string model_type = params.get_child("params").get("model_type_name" ); - if ( model_type == std::string( "schism_coastal_formulation" ) ) - { - return ModelType::SCHISM; - } - else if ( model_type == std::string("bmi_fortran_sfincs" ) ) - { - return ModelType::SFINCS; - } - else - { - logging::critical((std::string("Unknown coastal type: ") + model_type).c_str()); - throw std::runtime_error( std::string( "FATAL: Unknown coastal type: ") - + model_type ); - } + std::string model_type = + params.get_child("params").get("model_type_name"); + + if (model_type == std::string("schism_coastal_formulation")) + { + return ModelType::SCHISM; + } + else if (model_type == std::string("bmi_fortran_sfincs")) + { + return ModelType::SFINCS; + } + else + { + logging::critical((std::string("Unknown coastal type: ") + model_type).c_str()); + throw std::runtime_error(std::string("FATAL: Unknown coastal type: ") + model_type); + } } + diff --git a/src/realizations/coastal/SchismCreator.cpp b/src/realizations/coastal/SchismCreator.cpp index 4f8ab76ec8..361332ee34 100644 --- a/src/realizations/coastal/SchismCreator.cpp +++ b/src/realizations/coastal/SchismCreator.cpp @@ -40,7 +40,7 @@ std::unique_ptr std::chrono::system_clock::from_time_t(start_time_t), std::chrono::system_clock::from_time_t(stop_time_t)); - SchismFormulation::check_forcing_provider( *netcdf_met_provider, SchismFormulation::METEO ); + //SchismFormulation::check_forcing_provider( *netcdf_met_provider, SchismFormulation::METEO ); auto netcdf_streamflow_provider = std::make_shared std::chrono::system_clock::from_time_t(start_time_t), std::chrono::system_clock::from_time_t(stop_time_t)); - SchismFormulation::check_forcing_provider( *netcdf_streamflow_provider, - SchismFormulation::CHANNEL_FLOW ); + //SchismFormulation::check_forcing_provider( *netcdf_streamflow_provider, + // SchismFormulation::CHANNEL_FLOW ); auto netcdf_offshore_provider = std::make_shared std::chrono::system_clock::from_time_t(start_time_t), std::chrono::system_clock::from_time_t(stop_time_t)); - SchismFormulation::check_forcing_provider( *netcdf_offshore_provider, - SchismFormulation::OFFSHORE ); + //SchismFormulation::check_forcing_provider( *netcdf_offshore_provider, + // + /* SchismFormulation::OFFSHORE ); return std::make_unique( model_id, library_file, init_config, @@ -68,8 +69,9 @@ std::unique_ptr netcdf_met_provider, netcdf_offshore_provider, netcdf_streamflow_provider - ); -} + ); +*/ + } SchismCreator* SchismCreator::clone() const { diff --git a/src/realizations/coastal/SfincsCreator.cpp b/src/realizations/coastal/SfincsCreator.cpp new file mode 100644 index 0000000000..e8490eed2c --- /dev/null +++ b/src/realizations/coastal/SfincsCreator.cpp @@ -0,0 +1,151 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "realizations/coastal/SfincsCreator.h" +#include "realizations/coastal/SfincsFormulation.hpp" + +// ----------------- small POSIX helpers ----------------- + +static bool path_exists(const std::string& p) { + struct stat st{}; + return ::stat(p.c_str(), &st) == 0; +} +static bool is_dir(const std::string& p) { + struct stat st{}; + return (::stat(p.c_str(), &st) == 0) && S_ISDIR(st.st_mode); +} +static bool is_file(const std::string& p) { + struct stat st{}; + return (::stat(p.c_str(), &st) == 0) && S_ISREG(st.st_mode); +} + +// Recursively create a directory like `mkdir -p` +static void mkpath(const std::string& dir) { + if (dir.empty() || is_dir(dir)) return; + + // Split components + std::vector parts; + { + std::string cur; + for (char c : dir) { + if (c == '/') { + if (!cur.empty()) { parts.push_back(cur); cur.clear(); } + } else cur.push_back(c); + } + if (!cur.empty()) parts.push_back(cur); + } + + std::string acc = (dir[0] == '/') ? "/" : ""; + for (size_t i = 0; i < parts.size(); ++i) { + if (!acc.empty() && acc.back() != '/') acc.push_back('/'); + acc += parts[i]; + if (is_dir(acc)) continue; + + if (::mkdir(acc.c_str(), 0755) != 0) { + if (errno == EEXIST && is_dir(acc)) continue; + throw std::runtime_error(std::string("SfincsCreator: mkdir failed for ") + + acc + ": " + std::strerror(errno)); + } + } +} + +static inline void ensure_dir_exists(const std::string& dir) { + if (path_exists(dir) && !is_dir(dir)) { + throw std::runtime_error("SfincsCreator: working_dir exists but is not a directory: " + dir); + } + if (!path_exists(dir)) mkpath(dir); +} + +static inline void ensure_file_exists(const std::string& path, const char* what) { + if (!is_file(path)) { + throw std::runtime_error(std::string("SfincsCreator: missing or invalid ") + what + ": " + path); + } +} + +// ----------------- class methods ----------------- + +std::unique_ptr +SfincsCreator::createCoastalFormulation(coastal_config_params const& config, + Simulation_Time const& sim_time) const +{ + auto params = config.params.get_child("params"); + + // Required fields + const std::string model_id = params.get("model_type_name"); + const std::string library_file = params.get("library_file"); + const std::string working_dir = params.get("working_dir"); + + ensure_file_exists(library_file, "library_file"); + ensure_dir_exists(working_dir); + + // Write init config that SFINCS BMI Initialize() will read + writeInitConfig(config, sim_time); + const std::string init_config = working_dir + "/sfincs_config.txt"; + + // (Optional) switch cwd for convenience; non-fatal if it fails. + if (::chdir(working_dir.c_str()) != 0) { + std::cerr << "SfincsCreator: warning: failed changing cwd to " + << working_dir << " (" << std::strerror(errno) << ")\n"; + } + + // TODO: Wire providers here in future (met/offshore/channel). + return std::make_unique( + model_id, + library_file, + init_config, + /* met */ nullptr, + /* offshore */ nullptr, + /* channel_flow */ nullptr + ); +} + +SfincsCreator* SfincsCreator::clone() const { + return new SfincsCreator(); +} + +void SfincsCreator::writeInitConfig(coastal_config_params const& config, + Simulation_Time const& sim_time) const +{ + auto params = config.params.get_child("params"); + const std::string working_dir = params.get("working_dir"); + + const int model_dt_secs = params.get("model_time_step_in_secs", 60); + const int end_time_seconds = params.get("end_time_seconds", 3600); + + const time_t start_time_t = sim_time.get_start_date_time_epoch(); + char buffer[32] = {0}; + { + struct tm* timeInfo = gmtime(&start_time_t); + strftime(buffer, sizeof(buffer), "%Y%m%d%H%M%S", timeInfo); + } + + const std::string init_config = working_dir + "/sfincs_config.txt"; + std::ofstream ofs(init_config); + if (!ofs.is_open()) { + throw std::runtime_error(std::string("SfincsCreator: unable to open init config: ") + init_config); + } + + ofs << "# SFINCS BMI init file\n"; + ofs << "start_datetime = " << buffer << "\n"; + ofs << "dt_seconds = " << model_dt_secs << "\n"; + ofs << "end_time_seconds = " << end_time_seconds << "\n"; + + // Optional grid hints + if (params.count("nx") > 0) ofs << "nx = " << params.get("nx") << "\n"; + if (params.count("ny") > 0) ofs << "ny = " << params.get("ny") << "\n"; + if (params.count("dx") > 0) ofs << "dx = " << params.get("dx") << "\n"; + if (params.count("dy") > 0) ofs << "dy = " << params.get("dy") << "\n"; + if (params.count("x0") > 0) ofs << "x0 = " << params.get("x0") << "\n"; + if (params.count("y0") > 0) ofs << "y0 = " << params.get("y0") << "\n"; + + ofs.close(); +} + diff --git a/src/realizations/coastal/SfincsFormulation.cpp b/src/realizations/coastal/SfincsFormulation.cpp new file mode 100644 index 0000000000..59ec39293b --- /dev/null +++ b/src/realizations/coastal/SfincsFormulation.cpp @@ -0,0 +1,265 @@ +#include "realizations/coastal/SfincsFormulation.hpp" + +#include +#include +#include + +SfincsFormulation::SfincsFormulation(std::string model_id, + std::string library_file, + std::string init_config, + ProviderPtr met_provider, + ProviderPtr offshore_provider, + ProviderPtr channel_provider) + : CoastalFormulation(model_id) // IMPORTANT: CoastalFormulation requires id + , model_id_(std::move(model_id)) + , library_file_(std::move(library_file)) + , init_config_(std::move(init_config)) + , met_provider_(std::move(met_provider)) + , offshore_provider_(std::move(offshore_provider)) + , channel_provider_(std::move(channel_provider)) +{ +} + +SfincsFormulation::~SfincsFormulation() +{ + // be safe if finalize wasn't called + try { finalize(); } catch (...) {} +} + +void SfincsFormulation::create_formulation_() +{ +#if NGEN_WITH_BMI_FORTRAN + if (bmi_) return; + + // Bmi_Fortran_Adapter constructors (per your compile error): + // - (type_name, library, has_fixed_dt, reg_func) + // - (type_name, library, init_config, has_fixed_dt, reg_func) + // + // We want to pass init_config_ so use the 5-arg overload. + const bool has_fixed_time_step = true; + + // Default in adapter header is "register_bmi", but pass explicitly for clarity. + const std::string registration_function = "register_bmi"; + + bmi_ = std::make_unique( + model_id_, // type_name + library_file_, // library_file_path + init_config_, // bmi_init_config + has_fixed_time_step, + registration_function + ); +#else + throw std::runtime_error("SfincsFormulation requires NGEN_WITH_BMI_FORTRAN=ON"); +#endif +} + +void SfincsFormulation::destroy_formulation_() +{ +#if NGEN_WITH_BMI_FORTRAN + bmi_.reset(); +#endif + available_vars_.clear(); +} + +void SfincsFormulation::initialize() +{ + create_formulation_(); + +#if NGEN_WITH_BMI_FORTRAN + bmi_->Initialize(); + + available_vars_.clear(); +#endif +} + +void SfincsFormulation::finalize() +{ +#if NGEN_WITH_BMI_FORTRAN + if (bmi_) { + bmi_->Finalize(); + } +#endif + destroy_formulation_(); +} + +void SfincsFormulation::update() +{ +#if NGEN_WITH_BMI_FORTRAN + if (!bmi_) { + throw std::runtime_error("SfincsFormulation::update called before initialize()"); + } + + // (Optional) push forcings into BMI variables + // set_inputs_(); + + bmi_->Update(); +#else + throw std::runtime_error("SfincsFormulation requires NGEN_WITH_BMI_FORTRAN=ON"); +#endif +} + +void SfincsFormulation::update_until(double const& t) +{ +#if NGEN_WITH_BMI_FORTRAN + if (!bmi_) { + throw std::runtime_error("SfincsFormulation::update_until called before initialize()"); + } + + // Mirror Schism behavior + while (bmi_->GetCurrentTime() < t) { + // set_inputs_(); + bmi_->Update(); + } +#else + (void)t; + throw std::runtime_error("SfincsFormulation requires NGEN_WITH_BMI_FORTRAN=ON"); +#endif +} + +double SfincsFormulation::get_current_time() +{ +#if NGEN_WITH_BMI_FORTRAN + return bmi_ ? bmi_->GetCurrentTime() : 0.0; +#else + return 0.0; +#endif +} + +double SfincsFormulation::get_start_time() +{ +#if NGEN_WITH_BMI_FORTRAN + return bmi_ ? bmi_->GetStartTime() : 0.0; +#else + return 0.0; +#endif +} + +double SfincsFormulation::get_end_time() +{ +#if NGEN_WITH_BMI_FORTRAN + return bmi_ ? bmi_->GetEndTime() : 0.0; +#else + return 0.0; +#endif +} + +double SfincsFormulation::get_time_step() +{ +#if NGEN_WITH_BMI_FORTRAN + return bmi_ ? bmi_->GetTimeStep() : 0.0; +#else + return 0.0; +#endif +} + +void SfincsFormulation::get_values(const selection_type& selector, boost::span out) +{ + const std::string& var = selector.variable_name; + +#if NGEN_WITH_BMI_FORTRAN + if (!bmi_) { + throw std::runtime_error("SfincsFormulation::get_values called before initialize()"); + } + + if (out.empty()) { + return; + } + + bmi_->GetValue(var, out.data()); +#else + (void)var; + std::fill(out.begin(), out.end(), 0.0); +#endif +} + +void SfincsFormulation::get_values(const selection_type& selector, std::vector& out) +{ +#if NGEN_WITH_BMI_FORTRAN + if (!bmi_) { + throw std::runtime_error("SfincsFormulation::get_values called before initialize()"); + } + + const std::string& var = selector.variable_name; + + if (out.empty()) { + const auto nbytes = bmi_->GetVarNbytes(var); + const auto itemsize = bmi_->GetVarItemsize(var); + if (itemsize == 0) { + throw std::runtime_error("BMI reported itemsize=0 for var: " + var); + } + out.resize(static_cast(nbytes / itemsize)); + } + + get_values(selector, boost::span(out.data(), out.size())); +#else + std::fill(out.begin(), out.end(), 0.0); +#endif +} + +std::size_t SfincsFormulation::mesh_size(const std::string& mesh_name) +{ +#if NGEN_WITH_BMI_FORTRAN + if (!bmi_) return 0; + + const auto nbytes = bmi_->GetVarNbytes(mesh_name); + const auto itemsize = bmi_->GetVarItemsize(mesh_name); + if (itemsize == 0) return 0; + + return static_cast(nbytes / itemsize); +#else + (void)mesh_name; + return 0; +#endif +} + +// --------------------- +// DataProvider<> required pure virtuals +// --------------------- + +boost::span SfincsFormulation::get_available_variable_names() const +{ + return boost::span(available_vars_.data(), available_vars_.size()); +} + +long SfincsFormulation::get_data_start_time() const +{ + // As a formulation, this isn’t a forcing provider; return model start epoch seconds if available. + // If you want exact forcing provider time, return met_provider_->get_data_start_time(). + return 0; +} + +long SfincsFormulation::get_data_stop_time() const +{ + return 0; +} + +long SfincsFormulation::record_duration() const +{ + // duration in seconds between forcing records if acting as provider; unknown here + return 0; +} + +std::size_t SfincsFormulation::get_ts_index_for_time(const time_t& /*epoch_time*/) const +{ + return 0; +} + +SfincsFormulation::data_type +SfincsFormulation::get_value(const selection_type& selector, data_access::ReSampleMethod /*m*/) +{ + // Provide a scalar fetch convenience via get_values + std::vector buf; + buf.reserve(1); + get_values(selector, buf); + if (buf.empty()) return 0.0; + return buf[0]; +} + +// Optional: later we can wire forcings into BMI inputs similar to Schism’s set_inputs() +// For now keep it a no-op so compilation is stable. +void SfincsFormulation::set_inputs_() +{ + // Intentionally minimal. + // Once you confirm SFINCS BMI input variable names, we can map met/offshore/channel providers. +} + diff --git a/src/realizations/coastal/tests/test_sfincs_formulation_smoke.cpp b/src/realizations/coastal/tests/test_sfincs_formulation_smoke.cpp new file mode 100644 index 0000000000..997f42aab2 --- /dev/null +++ b/src/realizations/coastal/tests/test_sfincs_formulation_smoke.cpp @@ -0,0 +1,25 @@ +#include + +#include +#include + +int main(int /*argc*/, char** /*argv*/) +{ + // These two are required by the SfincsFormulation constructor + const std::string lib = "libsfincs_bmi.so"; // can be a real path in runtime tests + const std::string init = "sfincs.json"; // can be a real config in runtime tests + + // Updated provider types (matches SfincsFormulation.hpp constructor in your build errors) + using ProviderPtr = std::shared_ptr>; + + ProviderPtr met = nullptr; + ProviderPtr off = nullptr; + ProviderPtr chflow = nullptr; + + // Just ensure we can instantiate the object (compile/link smoke test) + SfincsFormulation f("sfincs_demo", lib, init, met, off, chflow); + + (void)f; + return 0; +} + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c1672312de..828424e937 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -194,7 +194,6 @@ ngen_add_test( LIBRARIES NGen::core NGen::realizations_catchment - NGen::realizations_coastal NGen::core_mediator NGen::forcing NGen::ngen_bmi @@ -315,7 +314,6 @@ ngen_add_test( NGen::core NGen::geojson NGen::realizations_catchment - NGen::realizations_coastal NGen::core_mediator NGen::forcing REQUIRES @@ -333,7 +331,6 @@ ngen_add_test( LIBRARIES NGen::core NGen::realizations_catchment - NGen::realizations_coastal NGen::core_mediator NGen::forcing NGen::ngen_bmi @@ -351,7 +348,6 @@ ngen_add_test( gmock NGen::core NGen::realizations_catchment - NGen::realizations_coastal NGen::core_mediator NGen::forcing NGen::ngen_bmi @@ -370,7 +366,6 @@ ngen_add_test( LIBRARIES NGen::core NGen::realizations_catchment - NGen::realizations_coastal NGen::core_mediator NGen::forcing NGen::ngen_bmi @@ -389,7 +384,6 @@ ngen_add_test( LIBRARIES NGen::core NGen::realizations_catchment - NGen::realizations_coastal NGen::core_mediator NGen::forcing NGen::ngen_bmi @@ -406,7 +400,6 @@ ngen_add_test( LIBRARIES NGen::core NGen::realizations_catchment - NGen::realizations_coastal NGen::core_mediator NGen::forcing NGen::ngen_bmi @@ -435,7 +428,6 @@ ngen_add_test( gmock NGen::core NGen::realizations_catchment - NGen::realizations_coastal NGen::core_mediator NGen::forcing NGen::ngen_bmi @@ -454,7 +446,6 @@ ngen_add_test( LIBRARIES NGen::core NGen::realizations_catchment - NGen::realizations_coastal NGen::core_mediator NGen::forcing NGen::ngen_bmi @@ -497,7 +488,6 @@ ngen_add_test( NGen::forcing NGen::geojson NGen::realizations_catchment - NGen::realizations_coastal REQUIRES NGEN_WITH_NETCDF ) @@ -533,7 +523,6 @@ ngen_add_test( NGen::forcing NGen::geojson NGen::realizations_catchment - NGen::realizations_coastal NGen::mdarray NGen::mdframe NGen::logging