diff --git a/.github/workflows/ngwpc-cicd.yml b/.github/workflows/ngwpc-cicd.yml index 2e293bef98..03791a9e98 100644 --- a/.github/workflows/ngwpc-cicd.yml +++ b/.github/workflows/ngwpc-cicd.yml @@ -23,6 +23,14 @@ on: description: 'NGEN_FORCING_IMAGE_TAG' required: false type: string + EWTS_ORG: + description: 'EWTS_ORG' + required: false + type: string + EWTS_REF: + description: 'EWTS_REF' + required: false + type: string permissions: contents: read @@ -149,7 +157,7 @@ jobs: BOOST_VER=1.86.0 BOOST_UNDERSCORE=1_86_0 - PREFIX=/opt/boost-${BOOST_VER} + PREFIX=/usr/local curl -fL --retry 10 --retry-delay 2 --max-time 600 \ -o /tmp/boost.tar.bz2 \ @@ -161,7 +169,7 @@ jobs: ./bootstrap.sh --prefix="${PREFIX}" # Build Boost libraries - ./b2 -j"$(nproc)" install \ + sudo ./b2 -j"$(nproc)" install \ --with-system \ --with-filesystem \ --with-program_options \ @@ -170,14 +178,35 @@ jobs: --with-date_time \ --with-serialization echo "BOOST_ROOT=${PREFIX}" >> "$GITHUB_ENV" - echo "CMAKE_PREFIX_PATH=${PREFIX}:${CMAKE_PREFIX_PATH:-}" >> "$GITHUB_ENV" + + - name: Build and install EWTS + run: | + set -euo pipefail + EWTS_ORG="${{ inputs.EWTS_ORG || 'NGWPC' }}" + EWTS_REF="${{ inputs.EWTS_REF || 'development' }}" + EWTS_PREFIX=/opt/ewts + + git clone --depth 1 -b "${EWTS_REF}" \ + "https://github.com/${EWTS_ORG}/nwm-ewts.git" /tmp/nwm-ewts \ + || (git clone "https://github.com/${EWTS_ORG}/nwm-ewts.git" /tmp/nwm-ewts && \ + cd /tmp/nwm-ewts && git checkout "${EWTS_REF}") + + cd /tmp/nwm-ewts + cmake -S . -B cmake_build \ + -DCMAKE_BUILD_TYPE=Release \ + -DEWTS_WITH_NGEN=ON \ + -DEWTS_BUILD_SHARED=ON + cmake --build cmake_build -j "$(nproc)" + sudo cmake --install cmake_build --prefix "${EWTS_PREFIX}" + + echo "EWTS_PREFIX=${EWTS_PREFIX}" >> "$GITHUB_ENV" - name: Build C++ code env: PYTHONPATH: ${{ env.PYTHONPATH }} run: | cmake -B cmake_build -S . \ - -DCMAKE_PREFIX_PATH="${BOOST_ROOT}" \ + -DCMAKE_PREFIX_PATH="${EWTS_PREFIX};${BOOST_ROOT}" \ -DBoost_NO_SYSTEM_PATHS=ON \ -DBOOST_ROOT="${BOOST_ROOT}" \ -DPYTHON_EXECUTABLE=$(which python3) \ @@ -241,6 +270,8 @@ jobs: build-args: | ORG=${{ needs.setup.outputs.org }} NGEN_FORCING_IMAGE_TAG=${{ inputs.NGEN_FORCING_IMAGE_TAG || 'latest' }} + EWTS_ORG=${{ inputs.EWTS_ORG || 'NGWPC' }} + EWTS_REF=${{ inputs.EWTS_REF || 'development' }} IMAGE_SOURCE=https://github.com/${{ github.repository }} IMAGE_VENDOR=${{ github.repository_owner }} IMAGE_VERSION=${{ needs.setup.outputs.clean_ref }} @@ -286,10 +317,10 @@ jobs: - build steps: - name: Install Trivy - uses: aquasecurity/setup-trivy@v0.2.2 + uses: aquasecurity/setup-trivy@v0.2.6 with: cache: true - version: v0.68.2 + version: v0.69.3 - name: Trivy scan env: diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c96c93b27..a320eab3a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,3 +1,7 @@ +# +# ngen/CMakeLists.txt +# + # Ensure CMake policies have defaults depending on the CMake version used # between the two versions specified. e.g. if 3.18 is used, then 3.18 defaults # will be used instead of 3.17 defaults. @@ -118,6 +122,30 @@ project(ngen VERSION 0.3.0) add_executable(ngen "${NGEN_SRC_DIR}/NGen.cpp") +# --- EWTS (installed from nwm-ewts) --- +find_package(ewts CONFIG REQUIRED) + +if(DEFINED ewts_VERSION AND NOT "${ewts_VERSION}" STREQUAL "") + set(NGEN_EWTS_VERSION "${ewts_VERSION}") +else() + set(NGEN_EWTS_VERSION "") +endif() + +if(DEFINED EWTS_NGWPC_VERSION AND NOT "${EWTS_NGWPC_VERSION}" STREQUAL "") + set(NGEN_EWTS_NGWPC_VERSION "${EWTS_NGWPC_VERSION}") +else() + set(NGEN_EWTS_NGWPC_VERSION "") +endif() + +get_filename_component(EWTS_PREFIX "${ewts_DIR}" DIRECTORY) +get_filename_component(EWTS_PREFIX "${EWTS_PREFIX}" DIRECTORY) +get_filename_component(EWTS_PREFIX "${EWTS_PREFIX}" DIRECTORY) + +message(STATUS "Found EWTS: ${EWTS_PREFIX} (found version ${ewts_VERSION})") + +# NGen itself uses the NGen-specific EWTS logger/bridge +target_link_libraries(ngen PRIVATE ewts::ewts_ngen_bridge) + # Dependencies ================================================================ # ----------------------------------------------------------------------------- @@ -165,7 +193,12 @@ 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.86.0 REQUIRED) +if(CMAKE_CXX_STANDARD LESS 17) + # requires non-header filesystem for state saving if C++ 11 or lower + find_package(Boost 1.86.0 REQUIRED COMPONENTS system filesystem) +else() + find_package(Boost 1.86.0 REQUIRED) +endif() # ----------------------------------------------------------------------------- if(NGEN_WITH_SQLITE) @@ -281,7 +314,6 @@ if(UDUNITS_QUIET) add_compile_definitions(UDUNITS_QUIET) endif() - # ----------------------------------------------------------------------------- # Project Targets # ----------------------------------------------------------------------------- @@ -318,6 +350,7 @@ add_subdirectory("src/geojson") add_subdirectory("src/bmi") add_subdirectory("src/realizations/catchment") add_subdirectory("src/forcing") +add_subdirectory("src/state_save_restore") add_subdirectory("src/utilities") add_subdirectory("src/utilities/mdarray") add_subdirectory("src/utilities/mdframe") @@ -337,7 +370,9 @@ target_link_libraries(ngen NGen::core_mediator NGen::logging NGen::parallel + NGen::state_save_restore NGen::bmi_protocols + NGen::state_save_restore ) if(NGEN_WITH_SQLITE) @@ -351,6 +386,7 @@ if(NGEN_WITH_ROUTING) endif() add_executable(partitionGenerator src/partitionGenerator.cpp) +target_link_libraries(partitionGenerator PRIVATE ewts::ewts_ngen_bridge) target_link_libraries(partitionGenerator PUBLIC NGen::logging) target_include_directories(partitionGenerator PUBLIC "${PROJECT_BINARY_DIR}/include") if(NGEN_WITH_SQLITE) @@ -441,7 +477,10 @@ ngen_multiline_message( " Boost:" " Version: ${Boost_VERSION}" " Include: ${Boost_INCLUDE_DIRS}" -" Library: ${Boost_LIBRARY_DIRS}") +" Library: ${Boost_LIBRARY_DIRS}" +" EWTS:" +" Package Version: ${NGEN_EWTS_VERSION}" +" NGWPC Version: ${NGEN_EWTS_NGWPC_VERSION}") ngen_dependent_multiline_message(INTEL_DPCPP " Intel DPC++:" " Version: ${SYCL_LANGUAGE_VERSION}" diff --git a/Dockerfile b/Dockerfile index 6ece41e410..c08f6cb206 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ ############################## ARG ORG=ngwpc ARG NGEN_FORCING_IMAGE_TAG=latest -ARG NGEN_FORCING_IMAGE=ghcr.io/ngwpc/ngen-bmi-forcing:${NGEN_FORCING_IMAGE_TAG} +ARG NGEN_FORCING_IMAGE=ghcr.io/${ORG}/ngen-bmi-forcing:${NGEN_FORCING_IMAGE_TAG} FROM ${NGEN_FORCING_IMAGE} AS base @@ -222,17 +222,127 @@ RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache \ WORKDIR /ngen-app/ -# TODO This will invalidate the cache for all subsequent stages so we don't really want to do this -# Copy the remainder of your application code -COPY . /ngen-app/ngen/ +############################## +# Stage: EWTS Build – Error, Warning and Trapping System +############################## +# EWTS is built in its own stage so that: +# - It is cached independently from ngen source changes (COPY . /ngen-app/ngen/ +# happens later in the submodules stage). +# - Iterative ngen/submodule development doesn't re-trigger the EWTS clone+build. +# - EWTS_ORG / EWTS_REF can be pinned without affecting other stages' caches. +# +# EWTS provides a unified logging framework used by ngen core and ALL C, C++, Fortran, +# and Python submodules. Libraries are created for C, C++ and Fortran submodules +# (cfe, evapotranspiration, LASAM, noah-owp-modular, snow17, sac-sma, +# SoilFreezeThaw, SoilMoistureProfiles, topmodel, ueb-bmi) and a Python package is +# used by Python sumbodules (lstm, topoflow-glacier and t-route). +# +# How the plumbing works: +# 1. We build EWTS here and install it to /opt/ewts. +# 2. Every cmake call in the submodules stage passes +# -DCMAKE_PREFIX_PATH=/opt/ewts so that +# find_package(ewts CONFIG REQUIRED) in each submodule's CMakeLists.txt +# can locate the ewtsConfig.cmake package file. +# 3. The following gives each submodule access to the EWTS targets: +# ewts::ewts_c – C runtime (cfe, evapotranspiration, topmodel) +# ewts::ewts_cpp – C++ runtime logger (used by LASAM, SoilFreezeThaw, SoilMoistureProfiles) +# ewts::ewts_fortran – Fortran runtime (noah-owp-modular sac-sma,, snow17) +# ewts::ewts_ngen_bridge – ngen↔EWTS bridge lib (linked by ngen itself) +# EWTS Python wheel – pip intalled package (lstm, topoflow-glacier, t-route) +# +# Build args – override at build time to pin a branch, tag, or full commit SHA: +# docker build --build-arg EWTS_REF=v1.2.3 ... +# docker build --build-arg EWTS_REF=abc123def456 ... +############################## +FROM base AS ewts-build + +SHELL [ "/usr/bin/scl", "enable", "gcc-toolset-10" ] + +ARG EWTS_ORG=NGWPC +ARG EWTS_REF=development + +# Install path for the built EWTS libraries, headers, cmake config, and +# Fortran .mod files. /opt/ewts follows the FHS convention for add-on +# packages (same pattern as /opt/boost already in this image) and avoids +# /tmp which can be cleaned unexpectedly. +ENV EWTS_PREFIX=/opt/ewts + +# Point the development fallback to the cloned source tree so that +# compiler.sh can pip-install EWTS from source if the wheel is missing. +ENV EWTS_PY_ROOT=/tmp/nwm-ewts/runtime/python/ewts + +# Clone nwm-ewts, build, install, capture git metadata for provenance, +# then remove the source tree. +# Try shallow clone by branch/tag name first; fall back to full clone + checkout +# for bare commit SHAs (which git clone -b doesn't support). +RUN --mount=type=cache,target=/root/.cache/cmake,id=cmake-ewts \ + set -eux && \ + (git clone --depth 1 -b "${EWTS_REF}" \ + "https://github.com/${EWTS_ORG}/nwm-ewts.git" /tmp/nwm-ewts \ + || (git clone "https://github.com/${EWTS_ORG}/nwm-ewts.git" /tmp/nwm-ewts && \ + cd /tmp/nwm-ewts && git checkout "${EWTS_REF}")) && \ + cd /tmp/nwm-ewts && \ + # ── Build EWTS ── + # This produces: C, C++, Fortran shared libs + ngen bridge + Python wheel. + # -DEWTS_WITH_NGEN=ON enables the ngen bridge (ewts_ngen_bridge.so) which + # provides the C shim ewts_ngen_log() that ngen's core calls into. + # -DEWTS_BUILD_SHARED=ON builds .so's so submodule DSOs can link at runtime. + cmake -S . -B cmake_build \ + -DCMAKE_BUILD_TYPE=Release \ + -DEWTS_WITH_NGEN=ON \ + -DEWTS_BUILD_SHARED=ON && \ + cmake --build cmake_build -j "$(nproc)" && \ + cmake --install cmake_build --prefix ${EWTS_PREFIX} && \ + # ── Capture EWTS git provenance ── + # Saved as a JSON sidecar so the git-info merge step at the bottom of this + # Dockerfile can include EWTS metadata alongside ngen + submodules. + jq -n \ + --arg commit_hash "$(git rev-parse HEAD)" \ + --arg branch "$(git branch -r --contains HEAD 2>/dev/null | grep -v '\->' | sed 's|origin/||' | head -n1 | xargs || echo "${EWTS_REF}")" \ + --arg tags "$(git tag --points-at HEAD 2>/dev/null | tr '\n' ' ')" \ + --arg author "$(git log -1 --pretty=format:'%an')" \ + --arg commit_date "$(date -u -d @$(git log -1 --pretty=format:'%ct') +'%Y-%m-%d %H:%M:%S UTC')" \ + --arg message "$(git log -1 --pretty=format:'%s' | tr '\n' ';')" \ + --arg build_date "$(date -u +'%Y-%m-%d %H:%M:%S UTC')" \ + '{"nwm-ewts": {commit_hash: $commit_hash, branch: $branch, tags: $tags, author: $author, commit_date: $commit_date, message: $message, build_date: $build_date}}' \ + > /ngen-app/nwm-ewts_git_info.json && \ + # ── Cleanup source (keep Python source as fallback for compiler.sh) ── + cd / && \ + rm -rf /tmp/nwm-ewts/cmake_build /tmp/nwm-ewts/.git + +# Install the EWTS Python wheel into the venv. +# This is what makes "import ewts" work for Python-based submodules. +# For example, lstm's bmi_lstm.py does: import ewts; LOG = ewts.get_logger(ewts.LSTM_ID) +RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache \ + set -eux && \ + pip install ${EWTS_PREFIX}/python/dist/ewts-*.whl + +# Make EWTS shared libraries (.so) discoverable at runtime. +# Without this, ngen and every submodule DSO would fail with: +# "error while loading shared libraries: libewts_ngen_bridge.so: cannot open" +# We include both lib/ and lib64/ because cmake may install to either depending +# on the platform/distro convention. +ENV LD_LIBRARY_PATH="${EWTS_PREFIX}/lib:${EWTS_PREFIX}/lib64:${LD_LIBRARY_PATH}" ############################## # Stage: Submodules Build ############################## -FROM base AS submodules +# Inherits from ewts-build so /opt/ewts is already present. +# The ngen source COPY happens here — changing ngen code only invalidates +# this stage and below, not the EWTS build above. +############################## +FROM ewts-build AS submodules SHELL [ "/usr/bin/scl", "enable", "gcc-toolset-10" ] +WORKDIR /ngen-app/ + +# Copy the ngen application source. +# This is placed here (not in base) so that +# ngen code changes only invalidate the submodules stage, leaving the base and +# ewts-build stages cached. +COPY . /ngen-app/ngen/ + WORKDIR /ngen-app/ngen/ # Copy only the requirements files first for dependency installation caching @@ -260,12 +370,17 @@ RUN --mount=type=cache,target=/root/.cache/t-route,id=t-route-build \ find /ngen-app/ngen/extern/t-route -name "*.a" -exec rm -f {} + # Configure the build with cache for CMake +# -DCMAKE_PREFIX_PATH=${EWTS_PREFIX} tells cmake where +# to find the ewtsConfig.cmake package file so that ngen's +# CMakeLists.txt line find_package(ewts CONFIG REQUIRED) succeeds. +# ngen links against ewts::ewts_ngen_bridge (the C++/MPI bridge). RUN --mount=type=cache,target=/root/.cache/cmake,id=cmake-ngen \ set -eux && \ export FFLAGS="-fPIC" && \ export FCFLAGS="-fPIC" && \ export CMAKE_Fortran_FLAGS="-fPIC" && \ cmake -B cmake_build -S . \ + -DCMAKE_PREFIX_PATH=${EWTS_PREFIX} \ -DNGEN_WITH_MPI=ON \ -DNGEN_WITH_NETCDF=ON \ -DNGEN_WITH_SQLITE=ON \ @@ -284,47 +399,72 @@ RUN --mount=type=cache,target=/root/.cache/cmake,id=cmake-ngen \ find /ngen-app/ngen/cmake_build -name "*.a" -exec rm -f {} + && \ find /ngen-app/ngen/cmake_build -name "*.o" -exec rm -f {} + +# ────────────────────────────────────────────────────────────────────────────── # Build each submodule in a separate layer, using cache for CMake as well +# +# IMPORTANT: Every submodule's CMakeLists.txt now contains: +# find_package(ewts CONFIG REQUIRED) +# so we MUST pass -DCMAKE_PREFIX_PATH=${EWTS_PREFIX} to each cmake call. +# Without it, cmake cannot locate ewtsConfig.cmake and the build fails with: +# "Could not find a package configuration file provided by "ewts"..." +# +# What each submodule links: +# LASAM → ewts::ewts_cpp + ewts::ewts_ngen_bridge +# snow17 → ewts::ewts_fortran + ewts::ewts_ngen_bridge +# sac-sma → ewts::ewts_fortran + ewts::ewts_ngen_bridge +# SoilMoistureProfiles → (check its CMakeLists.txt for specifics) +# SoilFreezeThaw → (check its CMakeLists.txt for specifics) +# cfe → (check its CMakeLists.txt for specifics) +# ueb-bmi → (check its CMakeLists.txt for specifics) +# ────────────────────────────────────────────────────────────────────────────── + RUN --mount=type=cache,target=/root/.cache/cmake,id=cmake-lasam \ set -eux && \ - cmake -B extern/LASAM/cmake_build -S extern/LASAM/ -DNGEN=ON -DBOOST_ROOT=/opt/boost && \ + cmake -B extern/LASAM/cmake_build -S extern/LASAM/ \ + -DCMAKE_PREFIX_PATH=${EWTS_PREFIX} -DNGEN=ON -DBOOST_ROOT=/opt/boost && \ cmake --build extern/LASAM/cmake_build/ && \ find /ngen-app/ngen/extern/LASAM -name '*.o' -exec rm -f {} + RUN --mount=type=cache,target=/root/.cache/cmake,id=cmake-snow17 \ set -eux && \ - cmake -B extern/snow17/cmake_build -S extern/snow17/ -DBOOST_ROOT=/opt/boost && \ + cmake -B extern/snow17/cmake_build -S extern/snow17/ \ + -DCMAKE_PREFIX_PATH=${EWTS_PREFIX} -DBOOST_ROOT=/opt/boost && \ cmake --build extern/snow17/cmake_build/ && \ find /ngen-app/ngen/extern/snow17 -name '*.o' -exec rm -f {} + RUN --mount=type=cache,target=/root/.cache/cmake,id=cmake-sac-sma \ set -eux && \ - cmake -B extern/sac-sma/cmake_build -S extern/sac-sma/ -DBOOST_ROOT=/opt/boost && \ + cmake -B extern/sac-sma/cmake_build -S extern/sac-sma/ \ + -DCMAKE_PREFIX_PATH=${EWTS_PREFIX} -DBOOST_ROOT=/opt/boost && \ cmake --build extern/sac-sma/cmake_build/ && \ find /ngen-app/ngen/extern/sac-sma -name '*.o' -exec rm -f {} + RUN --mount=type=cache,target=/root/.cache/cmake,id=cmake-soilmoistureprofiles \ set -eux && \ - cmake -B extern/SoilMoistureProfiles/cmake_build -S extern/SoilMoistureProfiles/SoilMoistureProfiles/ -DNGEN=ON -DBOOST_ROOT=/opt/boost && \ + cmake -B extern/SoilMoistureProfiles/cmake_build -S extern/SoilMoistureProfiles/SoilMoistureProfiles/ \ + -DCMAKE_PREFIX_PATH=${EWTS_PREFIX} -DNGEN=ON -DBOOST_ROOT=/opt/boost && \ cmake --build extern/SoilMoistureProfiles/cmake_build/ && \ find /ngen-app/ngen/extern/SoilMoistureProfiles -name '*.o' -exec rm -f {} + RUN --mount=type=cache,target=/root/.cache/cmake,id=cmake-soilfreezethaw \ set -eux && \ - cmake -B extern/SoilFreezeThaw/cmake_build -S extern/SoilFreezeThaw/SoilFreezeThaw/ -DNGEN=ON -DBOOST_ROOT=/opt/boost && \ + cmake -B extern/SoilFreezeThaw/cmake_build -S extern/SoilFreezeThaw/SoilFreezeThaw/ \ + -DCMAKE_PREFIX_PATH=${EWTS_PREFIX} -DNGEN=ON -DBOOST_ROOT=/opt/boost && \ cmake --build extern/SoilFreezeThaw/cmake_build/ && \ find /ngen-app/ngen/extern/SoilFreezeThaw -name '*.o' -exec rm -f {} + RUN --mount=type=cache,target=/root/.cache/cmake,id=cmake-ueb-bmi \ set -eux && \ - cmake -B extern/ueb-bmi/cmake_build -S extern/ueb-bmi/ -DBMICXX_INCLUDE_DIRS=/ngen-app/ngen/extern/bmi-cxx/ -DBOOST_ROOT=/opt/boost && \ + cmake -B extern/ueb-bmi/cmake_build -S extern/ueb-bmi/ \ + -DUEB_SUPPRESS_OUTPUTS=ON -DCMAKE_PREFIX_PATH=${EWTS_PREFIX} \ + -DBMICXX_INCLUDE_DIRS=/ngen-app/ngen/extern/bmi-cxx/ -DBOOST_ROOT=/opt/boost && \ cmake --build extern/ueb-bmi/cmake_build/ && \ find /ngen-app/ngen/extern/ueb-bmi/ -name '*.o' -exec rm -f {} + RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache \ set -eux; \ cd extern/lstm; \ - pip install . ./lstm_ewts + pip install . RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache \ set -eux; \ @@ -414,10 +554,12 @@ RUN set -eux && \ echo "$info" > /ngen-app/submodules-json/git_info_"$sub_key".json; \ done; \ \ - # Merge the main repository JSON with all submodule JSON files as top-level objects - jq -s 'add' $GIT_INFO_PATH /ngen-app/submodules-json/*.json > /ngen-app/merged_git_info.json && \ + # Merge the main repository JSON + submodule JSONs + EWTS provenance into one file. + # The EWTS json was created during the EWTS build step above; including it + # here means `cat /ngen-app/ngen_git_info.json` shows EWTS version info too. + jq -s 'add' $GIT_INFO_PATH /ngen-app/submodules-json/*.json /ngen-app/nwm-ewts_git_info.json > /ngen-app/merged_git_info.json && \ mv /ngen-app/merged_git_info.json $GIT_INFO_PATH && \ - rm -rf /ngen-app/submodules-json + rm -rf /ngen-app/submodules-json /ngen-app/nwm-ewts_git_info.json # Extend PYTHONPATH for LSTM models (preserve venv path from ngen-bmi-forcing) ENV PYTHONPATH="${PYTHONPATH}:/ngen-app/ngen/extern/lstm:/ngen-app/ngen/extern/lstm/lstm" @@ -430,4 +572,3 @@ SHELL ["/bin/bash", "-c"] ENTRYPOINT [ "/ngen-app/bin/run-ngen.sh" ] CMD [ "--info" ] - diff --git a/data/example_state_saving_config.json b/data/example_state_saving_config.json new file mode 100644 index 0000000000..cba48afcdb --- /dev/null +++ b/data/example_state_saving_config.json @@ -0,0 +1,109 @@ +{ + "global": { + "formulations": [ + { + "name": "bmi_c++", + "params": { + "model_type_name": "test_bmi_cpp", + "library_file": "./extern/test_bmi_cpp/cmake_build/libtestbmicppmodel.so", + "init_config": "./data/bmi/c/test/test_bmi_c_config.ini", + "main_output_variable": "OUTPUT_VAR_2", + "variables_names_map" : { + "INPUT_VAR_2": "TMP_2maboveground", + "INPUT_VAR_1": "precip_rate" + }, + "create_function": "bmi_model_create", + "destroy_function": "bmi_model_destroy", + "uses_forcing_file": false + } + } + ], + "forcing": { + "file_pattern": ".*{{id}}.*.csv", + "path": "./data/forcing/" + } + }, + "state_saving": { + "label": "end", + "path": "state_end", + "type": "FilePerUnit", + "when": "EndOfRun" + }, + "time": { + "start_time": "2015-12-01 00:00:00", + "end_time": "2015-12-30 23:00:00", + "output_interval": 3600 + }, + "output_root": "./output_dir/", + "catchments": { + "cat-27": { + "formulations": [ + { + "name": "bmi_c++", + "params": { + "model_type_name": "test_bmi_cpp", + "library_file": "./extern/test_bmi_cpp/cmake_build/libtestbmicppmodel.so", + "init_config": "./data/bmi/c/test/test_bmi_c_config.ini", + "main_output_variable": "OUTPUT_VAR_2", + "variables_names_map" : { + "INPUT_VAR_2": "TMP_2maboveground", + "INPUT_VAR_1": "precip_rate" + }, + "create_function": "bmi_model_create", + "destroy_function": "bmi_model_destroy", + "uses_forcing_file": false + } + } + ], + "forcing": { + "path": "./data/forcing/cat-27_2015-12-01 00_00_00_2015-12-30 23_00_00.csv" + } + }, + "cat-52": { + "formulations": [ + { + "name": "bmi_c++", + "params": { + "model_type_name": "test_bmi_cpp", + "library_file": "./extern/test_bmi_cpp/cmake_build/libtestbmicppmodel.so", + "init_config": "./data/bmi/c/test/test_bmi_c_config.ini", + "main_output_variable": "OUTPUT_VAR_2", + "variables_names_map" : { + "INPUT_VAR_2": "TMP_2maboveground", + "INPUT_VAR_1": "precip_rate" + }, + "create_function": "bmi_model_create", + "destroy_function": "bmi_model_destroy", + "uses_forcing_file": false + } + } + ], + "forcing": { + "path": "./data/forcing/cat-52_2015-12-01 00_00_00_2015-12-30 23_00_00.csv" + } + }, + "cat-67": { + "formulations": [ + { + "name": "bmi_c++", + "params": { + "model_type_name": "test_bmi_cpp", + "library_file": "./extern/test_bmi_cpp/cmake_build/libtestbmicppmodel.so", + "init_config": "./data/bmi/c/test/test_bmi_c_config.ini", + "main_output_variable": "OUTPUT_VAR_2", + "variables_names_map" : { + "INPUT_VAR_2": "TMP_2maboveground", + "INPUT_VAR_1": "precip_rate" + }, + "create_function": "bmi_model_create", + "destroy_function": "bmi_model_destroy", + "uses_forcing_file": false + } + } + ], + "forcing": { + "path": "./data/forcing/cat-67_2015-12-01 00_00_00_2015-12-30 23_00_00.csv" + } + } + } +} diff --git a/data/example_state_saving_config_multi.json b/data/example_state_saving_config_multi.json new file mode 100644 index 0000000000..ecfaa272fd --- /dev/null +++ b/data/example_state_saving_config_multi.json @@ -0,0 +1,117 @@ +{ + "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_c++", + "params": { + "model_type_name": "bmi_c++_sloth", + "library_file": "./extern/sloth/cmake_build/libslothmodel.so", + "init_config": "/dev/null", + "allow_exceed_end_time": true, + "main_output_variable": "z", + "uses_forcing_file": false, + "model_params": { + "sloth_ice_fraction_schaake(1,double,m,node)": 0.0, + "sloth_ice_fraction_xinanjiang(1,double,1,node)": 0.0, + "sloth_smp(1,double,1,node)": 0.0 + } + } + }, + { + "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", + "ice_fraction_schaake" : "sloth_ice_fraction_schaake", + "ice_fraction_xinanjiang" : "sloth_ice_fraction_xinanjiang", + "soil_moisture_profile" : "sloth_smp" + }, + "uses_forcing_file": false + } + } + ], + "uses_forcing_file": false + } + } + ], + "forcing": { + "file_pattern": ".*{{id}}.*..csv", + "path": "./data/forcing/", + "provider": "CsvPerFeature" + } + }, + "state_saving": [{ + "direction": "save", + "label": "end", + "path": "state_end", + "type": "FilePerUnit", + "when": "EndOfRun" + }], + "time": { + "start_time": "2015-12-01 00:00:00", + "end_time": "2015-12-30 23:00:00", + "output_interval": 3600 + }, + "output_root": "./output_dir/" +} diff --git a/extern/LASAM b/extern/LASAM index 764dc82e8f..e3411b3318 160000 --- a/extern/LASAM +++ b/extern/LASAM @@ -1 +1 @@ -Subproject commit 764dc82e8fb5a160e646a8f0fde35f964fd42234 +Subproject commit e3411b331854303f7344db795f3afc1e3269810a diff --git a/extern/SoilFreezeThaw/SoilFreezeThaw b/extern/SoilFreezeThaw/SoilFreezeThaw index ab641a8209..815a970af3 160000 --- a/extern/SoilFreezeThaw/SoilFreezeThaw +++ b/extern/SoilFreezeThaw/SoilFreezeThaw @@ -1 +1 @@ -Subproject commit ab641a820920acb788dc47513a1e0ccbf31483c2 +Subproject commit 815a970af30e407785bc767a4a5f5dffa2708eb5 diff --git a/extern/SoilMoistureProfiles/SoilMoistureProfiles b/extern/SoilMoistureProfiles/SoilMoistureProfiles index 41c802cb48..d29f268223 160000 --- a/extern/SoilMoistureProfiles/SoilMoistureProfiles +++ b/extern/SoilMoistureProfiles/SoilMoistureProfiles @@ -1 +1 @@ -Subproject commit 41c802cb4862e7bd01f6081816a093135bd51282 +Subproject commit d29f26822391740549642a9818065ea296688b2e diff --git a/extern/cfe/CMakeLists.txt b/extern/cfe/CMakeLists.txt index 6ef23bf9d5..52bfb303a4 100644 --- a/extern/cfe/CMakeLists.txt +++ b/extern/cfe/CMakeLists.txt @@ -22,9 +22,9 @@ find_package(Boost 1.79.0 REQUIRED COMPONENTS serialization) if(WIN32) - add_library(cfebmi cfe/src/bmi_cfe.c cfe/src/cfe.c cfe/src/giuh.c cfe/src/logger.c cfe/src/conceptual_reservoir.c cfe/src/nash_cascade.c cfe/src/bmi_serialization.cxx) + add_library(cfebmi cfe/src/bmi_cfe.c cfe/src/cfe.c cfe/src/giuh.c cfe/src/conceptual_reservoir.c cfe/src/nash_cascade.c cfe/src/bmi_serialization.cxx) else() - add_library(cfebmi SHARED cfe/src/bmi_cfe.c cfe/src/cfe.c cfe/src/giuh.c cfe/src/logger.c cfe/src/conceptual_reservoir.c cfe/src/nash_cascade.c cfe/src/bmi_serialization.cxx) + add_library(cfebmi SHARED cfe/src/bmi_cfe.c cfe/src/cfe.c cfe/src/giuh.c cfe/src/conceptual_reservoir.c cfe/src/nash_cascade.c cfe/src/bmi_serialization.cxx) endif() target_include_directories(cfebmi PRIVATE cfe/include) @@ -35,6 +35,17 @@ set_target_properties(cfebmi PROPERTIES PUBLIC_HEADER cfe/include/bmi_cfe.h) target_link_libraries(cfebmi PRIVATE Boost::serialization) +# --- EWTS (installed from nwm-ewts) --- +find_package(ewts CONFIG REQUIRED) + +# Always use EWTS runtime logger for C +target_link_libraries(cfebmi PRIVATE ewts::ewts_c) + +# Built with ngen bridge +target_link_libraries(cfebmi PRIVATE ewts::ewts_ngen_bridge) +target_compile_definitions(cfebmi PRIVATE EWTS_HAVE_NGEN_BRIDGE) + + # Code requires minimum of C99 standard to compile set_target_properties(cfebmi PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON) diff --git a/extern/cfe/cfe b/extern/cfe/cfe index f60afb6a8a..1e42e47d9e 160000 --- a/extern/cfe/cfe +++ b/extern/cfe/cfe @@ -1 +1 @@ -Subproject commit f60afb6a8a49e388e6263664239ff27742132d26 +Subproject commit 1e42e47d9edae96e337f0379f71deb4638cb45a6 diff --git a/extern/evapotranspiration/evapotranspiration b/extern/evapotranspiration/evapotranspiration index 096208ad62..fa10f12c46 160000 --- a/extern/evapotranspiration/evapotranspiration +++ b/extern/evapotranspiration/evapotranspiration @@ -1 +1 @@ -Subproject commit 096208ad624e07216617f770a3447eb829266112 +Subproject commit fa10f12c46bf6cda028f843292cac7b26d8b6ac0 diff --git a/extern/lstm b/extern/lstm index 85a3301dae..b3ccaeb458 160000 --- a/extern/lstm +++ b/extern/lstm @@ -1 +1 @@ -Subproject commit 85a3301daeff761a54b6ebda6fee7aac977a62ce +Subproject commit b3ccaeb45897693d7ebb05dc38788a5766a6921e diff --git a/extern/noah-owp-modular/CMakeLists.txt b/extern/noah-owp-modular/CMakeLists.txt index 526e6c9239..1aeb29f7ea 100644 --- a/extern/noah-owp-modular/CMakeLists.txt +++ b/extern/noah-owp-modular/CMakeLists.txt @@ -1,3 +1,7 @@ +# +# noah-owp-modular CMakeLists.txt +# + cmake_minimum_required(VERSION 3.12...3.26) # NGen NOAH-OWP-MODULAR listfile shim @@ -28,6 +32,12 @@ target_compile_options(surfacebmi PRIVATE -cpp -ffree-line-length-none) target_include_directories(surfacebmi INTERFACE "${_SURFACEBMI_BINARY_DIR}/mod") target_compile_definitions(surfacebmi PRIVATE BMI_ACTIVE) +# --- EWTS (installed from nwm-ewts) --- +find_package(ewts CONFIG REQUIRED) + +# Always use EWTS runtime logger for Fortran +target_link_libraries(surfacebmi PRIVATE ewts::ewts_fortran) + if(NGEN_IS_MAIN_PROJECT) # This ensures we can build NOAH-OWP-Modular with NGen support, but @@ -41,6 +51,10 @@ if(NGEN_IS_MAIN_PROJECT) target_link_libraries(surfacebmi PUBLIC iso_c_bmi) target_compile_definitions(surfacebmi PRIVATE NGEN_FORCING_ACTIVE NGEN_OUTPUT_ACTIVE NGEN_ACTIVE) + + # Built with ngen bridge + target_link_libraries(surfacebmi PRIVATE ewts::ewts_ngen_bridge) + target_compile_definitions(surfacebmi PRIVATE EWTS_HAVE_NGEN_BRIDGE) else() find_path(NETCDF_MODULE_DIR netcdf.mod PATHS "${NETCDF_ROOT}/include" diff --git a/extern/noah-owp-modular/noah-owp-modular b/extern/noah-owp-modular/noah-owp-modular index 25579b4948..029fc17a35 160000 --- a/extern/noah-owp-modular/noah-owp-modular +++ b/extern/noah-owp-modular/noah-owp-modular @@ -1 +1 @@ -Subproject commit 25579b4948e28e5afd0bed3e99e08a806fd9fc7c +Subproject commit 029fc17a3552356c1a62afd31bcf0a00f70948e4 diff --git a/extern/sac-sma/CMakeLists.txt b/extern/sac-sma/CMakeLists.txt index 4d06eaa19e..2646aa7ab2 100644 --- a/extern/sac-sma/CMakeLists.txt +++ b/extern/sac-sma/CMakeLists.txt @@ -1,3 +1,7 @@ +# +# ngen/extern/sac-sma/CMakeLists.txt +# + cmake_minimum_required(VERSION 3.12) enable_language( Fortran ) add_subdirectory(../iso_c_fortran_bmi ${CMAKE_BINARY_DIR}/iso_c_bmi) @@ -83,7 +87,17 @@ target_link_libraries( sacbmi PUBLIC iso_c_bmi ) set_target_properties(sacbmi PROPERTIES VERSION ${PROJECT_VERSION}) #TODO is this needed for fortran? -#set_target_properties(surfacebmi PROPERTIES PUBLIC_HEADER ${BMI_SOURCE}) +#set_target_properties(sacbmi PROPERTIES PUBLIC_HEADER ${BMI_SOURCE}) + +# --- EWTS (installed from nwm-ewts) --- +find_package(ewts CONFIG REQUIRED) + +# Always use EWTS runtime logger for Fortran +target_link_libraries(sacbmi PRIVATE ewts::ewts_fortran) + +# Built with ngen bridge +target_link_libraries(sacbmi PRIVATE ewts::ewts_ngen_bridge) +target_compile_definitions(sacbmi PRIVATE EWTS_HAVE_NGEN_BRIDGE) include(GNUInstallDirs) diff --git a/extern/sac-sma/sac-sma b/extern/sac-sma/sac-sma index b40f61ca9e..ca16b6e705 160000 --- a/extern/sac-sma/sac-sma +++ b/extern/sac-sma/sac-sma @@ -1 +1 @@ -Subproject commit b40f61ca9e82d4c4d0fc6171b314714af0160ab3 +Subproject commit ca16b6e705129913a5bca897428faf8d9a7561ee diff --git a/extern/sloth b/extern/sloth index ee0d982ccc..fc9c09823c 160000 --- a/extern/sloth +++ b/extern/sloth @@ -1 +1 @@ -Subproject commit ee0d982ccc07663cfea7bf0ac4d645841e19ccc1 +Subproject commit fc9c09823c90ca70a388f032fa19d88fc4a96091 diff --git a/extern/snow17 b/extern/snow17 index 10c2510bfa..e73f49ba8d 160000 --- a/extern/snow17 +++ b/extern/snow17 @@ -1 +1 @@ -Subproject commit 10c2510bfa45743a3828ea0fc890f79974b48390 +Subproject commit e73f49ba8d3180af48cfc0726bebc7d08a792035 diff --git a/extern/t-route b/extern/t-route index 4c100cb7b9..e2f7d5dcb7 160000 --- a/extern/t-route +++ b/extern/t-route @@ -1 +1 @@ -Subproject commit 4c100cb7b94ff61b8028b4169571057ccdcea02f +Subproject commit e2f7d5dcb7efcc684523f759b438ad250543ccdd diff --git a/extern/topmodel/CMakeLists.txt b/extern/topmodel/CMakeLists.txt index 0bc32b327b..604a11c45e 100644 --- a/extern/topmodel/CMakeLists.txt +++ b/extern/topmodel/CMakeLists.txt @@ -19,9 +19,9 @@ set(Boost_USE_STATIC_RUNTIME OFF) find_package(Boost 1.79.0 REQUIRED COMPONENTS serialization) if(WIN32) - add_library(topmodelbmi topmodel/src/bmi_topmodel.c topmodel/src/topmodel.c topmodel/src/logger.c topmodel/src/bmi_serialization.cpp) + add_library(topmodelbmi topmodel/src/bmi_topmodel.c topmodel/src/topmodel.c topmodel/src/bmi_serialization.cpp) else() - add_library(topmodelbmi SHARED topmodel/src/bmi_topmodel.c topmodel/src/topmodel.c topmodel/src/logger.c topmodel/src/bmi_serialization.cpp) + add_library(topmodelbmi SHARED topmodel/src/bmi_topmodel.c topmodel/src/topmodel.c topmodel/src/bmi_serialization.cpp) endif() target_include_directories(topmodelbmi PRIVATE topmodel/include) @@ -32,6 +32,16 @@ set_target_properties(topmodelbmi PROPERTIES PUBLIC_HEADER topmodel/include/bmi_ target_link_libraries(topmodelbmi PRIVATE Boost::serialization) +# --- EWTS (installed from nwm-ewts) --- +find_package(ewts CONFIG REQUIRED) + +# Always use EWTS runtime logger for C +target_link_libraries(topmodelbmi PRIVATE ewts::ewts_c) + +# Built with ngen bridge +target_link_libraries(topmodelbmi PRIVATE ewts::ewts_ngen_bridge) +target_compile_definitions(topmodelbmi PRIVATE EWTS_HAVE_NGEN_BRIDGE) + # Code requires minimum of C99 standard to compile set_target_properties(topmodelbmi PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON) diff --git a/extern/topmodel/topmodel b/extern/topmodel/topmodel index fa4f7e56db..e608a31687 160000 --- a/extern/topmodel/topmodel +++ b/extern/topmodel/topmodel @@ -1 +1 @@ -Subproject commit fa4f7e56dbe46df8cc0d7ca9095102290170b866 +Subproject commit e608a31687c00835a5484d83df277a6587989ff1 diff --git a/extern/topoflow-glacier b/extern/topoflow-glacier index 6ccd49938a..4da1b75b8f 160000 --- a/extern/topoflow-glacier +++ b/extern/topoflow-glacier @@ -1 +1 @@ -Subproject commit 6ccd49938ab79d61a36791cc25144f3e8dd1229b +Subproject commit 4da1b75b8fe0fe98a247a4a23f7054089c30a1e5 diff --git a/extern/ueb-bmi b/extern/ueb-bmi index 299976367a..cb18da2a41 160000 --- a/extern/ueb-bmi +++ b/extern/ueb-bmi @@ -1 +1 @@ -Subproject commit 299976367a5329602fc1443f932e9cbf6de4ace6 +Subproject commit cb18da2a411f6e6f43c4cc13845c1c2837a1346a diff --git a/include/bmi/Bmi_C_Adapter.hpp b/include/bmi/Bmi_C_Adapter.hpp index 9ac7b1620f..e2068f8b59 100644 --- a/include/bmi/Bmi_C_Adapter.hpp +++ b/include/bmi/Bmi_C_Adapter.hpp @@ -3,7 +3,7 @@ #include #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include "bmi.h" #include "AbstractCLibBmiAdapter.hpp" diff --git a/include/bmi/Bmi_Cpp_Adapter.hpp b/include/bmi/Bmi_Cpp_Adapter.hpp index 91a67b1601..66abb1f648 100644 --- a/include/bmi/Bmi_Cpp_Adapter.hpp +++ b/include/bmi/Bmi_Cpp_Adapter.hpp @@ -3,7 +3,7 @@ #include #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include "bmi.hpp" #include "AbstractCLibBmiAdapter.hpp" diff --git a/include/bmi/Bmi_Fortran_Adapter.hpp b/include/bmi/Bmi_Fortran_Adapter.hpp index f3818c96e8..c168819e9d 100644 --- a/include/bmi/Bmi_Fortran_Adapter.hpp +++ b/include/bmi/Bmi_Fortran_Adapter.hpp @@ -2,7 +2,7 @@ #define NGEN_BMI_FORTRAN_ADAPTER_HPP #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #if NGEN_WITH_BMI_FORTRAN diff --git a/include/bmi/Bmi_Py_Adapter.hpp b/include/bmi/Bmi_Py_Adapter.hpp index d66124d534..3e47fc2d16 100644 --- a/include/bmi/Bmi_Py_Adapter.hpp +++ b/include/bmi/Bmi_Py_Adapter.hpp @@ -2,7 +2,7 @@ #define NGEN_BMI_PY_ADAPTER_H #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #if NGEN_WITH_PYTHON @@ -541,33 +541,30 @@ namespace models { int itemSize = GetVarItemsize(name); std::string py_type = GetVarType(name); std::string cxx_type = get_analogous_cxx_type(py_type, (size_t) itemSize); - - if (cxx_type == "short") { - set_value(name, (short *) src); - } else if (cxx_type == "int") { - set_value(name, (int *) src); - } else if (cxx_type == "long") { - set_value(name, (long *) src); - } else if (cxx_type == "long long") { - //FIXME this gets dicey -- if a python numpy array is of type np.int64 (long long), - //but a c++ int* is passed to this function as src, it will fail in undefined ways... - //the template type overload may be perferred for doing SetValue from framework components - //such as forcing providers... - set_value(name, (long long *) src); - } else if (cxx_type == "float") { - set_value(name, (float *) src); - } else if (cxx_type == "double") { - set_value(name, (double *) src); - } else if (cxx_type == "long double") { - set_value(name, (long double *) src); - } else { + // macro for checking type and setting value + #define BMI_PY_SET_VALUE(type) if (cxx_type == #type) {\ + this->set_value(name, static_cast(src)); } + BMI_PY_SET_VALUE(signed char) + else BMI_PY_SET_VALUE(unsigned char) + else BMI_PY_SET_VALUE(short) + else BMI_PY_SET_VALUE(unsigned short) + else BMI_PY_SET_VALUE(int) + else BMI_PY_SET_VALUE(unsigned int) + else BMI_PY_SET_VALUE(long) + else BMI_PY_SET_VALUE(unsigned long) + else BMI_PY_SET_VALUE(long long) + else BMI_PY_SET_VALUE(unsigned long long) + else BMI_PY_SET_VALUE(float) + else BMI_PY_SET_VALUE(double) + else BMI_PY_SET_VALUE(long double) + else { std::string throw_msg; throw_msg.assign("Bmi_Py_Adapter cannot set values for variable '" + name + "' that has unrecognized C++ type '" + cxx_type + "'"); LOG(throw_msg, LogLevel::WARNING); throw std::runtime_error(throw_msg); } + #undef BMI_PY_SET_VALUE } - /** * Set the values of the given BMI variable for the model, sourcing new data from the provided vector. * @@ -597,6 +594,24 @@ namespace models { } } + /** + * Set the value of a variable. This version of setting a variable will send an array with the `size` specified instead of checking the BMI for its current size of the variable. + * Ownership of the pointer will remain in C++, so the consuming BMI should not maintain a reference to the values beyond the scope of its `set_value` method. + * + * @param name The name of the BMI variable. + * @param src Pointer to the data that will be sent to the BMI. + * @param size The number of items represented by the pointer. + */ + template + void set_value_unchecked(const std::string &name, T *src, size_t size) { + // declare readonly array info with the pointer and size + py::buffer_info info(src, static_cast(size), true); + // create the array with the info and NULL handler so python doesn't take ownership + py::array_t src_array(info, nullptr); + // pass the array to python to read; the BMI should not attempt to maintain a reference beyond the scope of this function to prevent trying to use freed memory + bmi_model->attr("set_value")(name, src_array); + } + /** * Set values for a model's BMI variable at specified indices. * diff --git a/include/core/Layer.hpp b/include/core/Layer.hpp index 5c3c4481fa..643f1b6970 100644 --- a/include/core/Layer.hpp +++ b/include/core/Layer.hpp @@ -2,7 +2,7 @@ #define __NGEN_LAYER__ #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include "LayerData.hpp" #include "Simulation_Time.hpp" @@ -16,6 +16,9 @@ namespace hy_features class HY_Features_MPI; } +class State_Snapshot_Saver; +class State_Snapshot_Loader; + namespace ngen { @@ -110,6 +113,10 @@ namespace ngen std::unordered_map &nexus_indexes, int current_step); + virtual void save_state_snapshot(std::shared_ptr snapshot_saver); + virtual void load_state_snapshot(std::shared_ptr snapshot_loader); + virtual void load_hot_start(std::shared_ptr snapshot_loader); + protected: const LayerDescription description; diff --git a/include/core/NgenSimulation.hpp b/include/core/NgenSimulation.hpp index 00e5ef49eb..3189045edc 100644 --- a/include/core/NgenSimulation.hpp +++ b/include/core/NgenSimulation.hpp @@ -12,6 +12,13 @@ namespace hy_features class HY_Features_MPI; } +class State_Snapshot_Saver; +class State_Snapshot_Loader; + +#if NGEN_WITH_ROUTING +#include "bmi/Bmi_Py_Adapter.hpp" +#endif // NGEN_WITH_ROUTING + #include #include #include @@ -48,6 +55,9 @@ class NgenSimulation */ void run_catchments(); + // Tear down of any items stored on the NgenSimulation object that could throw errors and, thus, should be kept separate from the deconstructor. + void finalize(); + /** * Run t-route on the stored nexus outflow values for the full configured duration of the simulation */ @@ -59,6 +69,17 @@ class NgenSimulation size_t get_num_output_times() const; std::string get_timestamp_for_step(int step) const; + void save_state_snapshot(std::shared_ptr snapshot_saver); + void load_state_snapshot(std::shared_ptr snapshot_loader); + /** + * Saves a snapshot state that's intended to be run at the end of a simulation. + * + * This version of saving will include T-Route BMI data and exclude the nexus outflow data stored during the catchment processing. + */ + void save_end_of_run(std::shared_ptr snapshot_saver); + // Load a snapshot of the end of a previous run. This will create a T-Route python adapter if the loader finds a unit for it and the config path is not empty. + void load_hot_start(std::shared_ptr snapshot_loader, const std::string &t_route_config_file_with_path); + private: void advance_models_one_output_step(); @@ -74,6 +95,12 @@ class NgenSimulation std::vector catchment_outflows_; std::unordered_map nexus_indexes_; std::vector nexus_downstream_flows_; +#if NGEN_WITH_ROUTING + std::unique_ptr py_troute_; +#endif // NGEN_WITH_ROUTING + void make_troute(const std::string &t_route_config_file_with_path); + + std::string unit_name() const; int mpi_rank_; int mpi_num_procs_; diff --git a/include/core/Partition_Parser.hpp b/include/core/Partition_Parser.hpp index d479aec04c..c05c88021e 100644 --- a/include/core/Partition_Parser.hpp +++ b/include/core/Partition_Parser.hpp @@ -17,7 +17,7 @@ #include #include "JSONProperty.hpp" #include "Partition_Data.hpp" -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" class Partitions_Parser { diff --git a/include/core/mediator/UnitsHelper.hpp b/include/core/mediator/UnitsHelper.hpp index 7d61f90d30..6887389ee5 100644 --- a/include/core/mediator/UnitsHelper.hpp +++ b/include/core/mediator/UnitsHelper.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_UNITSHELPER_H #define NGEN_UNITSHELPER_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" // FIXME: Workaround to handle UDUNITS2 includes with differing paths. // Not exactly sure why CMake can't handle this, but even with diff --git a/include/core/nexus/HY_PointHydroNexusRemote.hpp b/include/core/nexus/HY_PointHydroNexusRemote.hpp index 34d98f9a96..aa3857d67b 100644 --- a/include/core/nexus/HY_PointHydroNexusRemote.hpp +++ b/include/core/nexus/HY_PointHydroNexusRemote.hpp @@ -1,6 +1,6 @@ #ifndef HY_POINTHDRONEXUSREMOTE_H #define HY_POINTHDRONEXUSREMOTE_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #if NGEN_WITH_MPI @@ -72,6 +72,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/include/forcing/CsvPerFeatureForcingProvider.hpp b/include/forcing/CsvPerFeatureForcingProvider.hpp index 1570c67b2e..a8b4bbd22c 100644 --- a/include/forcing/CsvPerFeatureForcingProvider.hpp +++ b/include/forcing/CsvPerFeatureForcingProvider.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_CSVPERFEATUREFORCING_H #define NGEN_CSVPERFEATUREFORCING_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include diff --git a/include/forcing/ForcingsEngineDataProvider.hpp b/include/forcing/ForcingsEngineDataProvider.hpp index 37df90d59b..5987c9bf48 100644 --- a/include/forcing/ForcingsEngineDataProvider.hpp +++ b/include/forcing/ForcingsEngineDataProvider.hpp @@ -16,7 +16,7 @@ #include "DataProvider.hpp" #include "bmi/Bmi_Py_Adapter.hpp" -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" namespace data_access { diff --git a/include/forcing/OptionalWrappedDataProvider.hpp b/include/forcing/OptionalWrappedDataProvider.hpp index 3a2c2c7c94..06dab46c61 100644 --- a/include/forcing/OptionalWrappedDataProvider.hpp +++ b/include/forcing/OptionalWrappedDataProvider.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_OPTIONALWRAPPEDPROVIDER_HPP #define NGEN_OPTIONALWRAPPEDPROVIDER_HPP -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include diff --git a/include/geojson/JSONProperty.hpp b/include/geojson/JSONProperty.hpp index edcdb3a781..61e2836fd6 100644 --- a/include/geojson/JSONProperty.hpp +++ b/include/geojson/JSONProperty.hpp @@ -9,7 +9,7 @@ #include #include #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" namespace geojson { class JSONProperty; diff --git a/include/geojson/features/FeatureBase.hpp b/include/geojson/features/FeatureBase.hpp index a11669808f..2c89aa2474 100644 --- a/include/geojson/features/FeatureBase.hpp +++ b/include/geojson/features/FeatureBase.hpp @@ -1,6 +1,6 @@ #ifndef GEOJSON_FEATURE_H #define GEOJSON_FEATURE_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include "JSONGeometry.hpp" #include "JSONProperty.hpp" diff --git a/include/realizations/catchment/Bmi_Formulation.hpp b/include/realizations/catchment/Bmi_Formulation.hpp index f2a13074e8..91236fea42 100644 --- a/include/realizations/catchment/Bmi_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Formulation.hpp @@ -41,6 +41,9 @@ class Bmi_Formulation_Test; class Bmi_C_Formulation_Test; class Bmi_C_Pet_IT; +class State_Snapshot_Saver; +class State_Snapshot_Loader; + namespace realization { /** @@ -68,6 +71,30 @@ namespace realization { virtual ~Bmi_Formulation() {}; + /** + * Passes a serialized representation of the model's state to ``saver`` + * + * Asks the model to serialize its state, queries the pointer + * and length, passes that to saver, and then releases it + */ + virtual void save_state(std::shared_ptr saver) = 0; + + /** + * Passes a serialized representation of the model's state to ``loader`` + * + * Asks saver to find data for the BMI and passes that data to the BMI for loading. + */ + virtual void load_state(std::shared_ptr loader) = 0; + + /** + * Passes a serialized representation of the model's state to ``loader`` + * + * Asks saver to find data for the BMI and passes that data to the BMI for loading. + * + * Differes from `load_state` by also restting the internal time value to its initial state. + */ + virtual void load_hot_start(std::shared_ptr loader) = 0; + /** * Convert a time value from the model to an epoch time in seconds. * diff --git a/include/realizations/catchment/Bmi_Fortran_Formulation.hpp b/include/realizations/catchment/Bmi_Fortran_Formulation.hpp index 4f6863b883..d37ca469e7 100644 --- a/include/realizations/catchment/Bmi_Fortran_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Fortran_Formulation.hpp @@ -5,6 +5,7 @@ #if NGEN_WITH_BMI_FORTRAN +#include #include "Bmi_Module_Formulation.hpp" #include @@ -23,6 +24,18 @@ namespace realization { std::string get_formulation_type() const override; + /** + * Requests the BMI to copy its current state into memory. The state will remain in memory until either a new state is made or `free_serialization_state` is called. + * Because the Fortran BMI has no messaging for 64-bit integers, this overload will use the 32-bit integer interface. + * + * @return Span of the serialized data. + */ + const boost::span get_serialization_state() override; + + void load_serialization_state(const boost::span state) override; + + void free_serialization_state() override; + protected: /** @@ -49,6 +62,10 @@ namespace realization { friend class ::Bmi_Multi_Formulation_Test; friend class ::Bmi_Formulation_Test; friend class ::Bmi_Fortran_Formulation_Test; + + private: + // location to store serialized state from the BMI because pointer interfaces are not available for Fotran + std::vector serialized_state; }; } diff --git a/include/realizations/catchment/Bmi_Module_Formulation.hpp b/include/realizations/catchment/Bmi_Module_Formulation.hpp index cd6dda4969..6a0abadd1d 100644 --- a/include/realizations/catchment/Bmi_Module_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Module_Formulation.hpp @@ -7,7 +7,9 @@ #include "Bmi_Adapter.hpp" #include #include "bmi_utilities.hpp" -#include + +#include +#include "bmi/protocols.hpp" #include #include "bmi/protocols.hpp" @@ -49,12 +51,14 @@ namespace realization { void create_formulation(geojson::PropertyMap properties) override; /** - * Passes a serialized representation of the model's state to ``saver`` - * - * Asks the model to serialize its state, queries the pointer - * and length, passes that to saver, and then releases it + * Create a save state, save it using the `State_Snapshot_Saver`, then clear the save state from memory. + * `this->get_id()` will be used as the unique ID for the saver. */ - void save_state(std::shared_ptr saver) const; + void save_state(std::shared_ptr saver) override; + + void load_state(std::shared_ptr loader) override; + + void load_hot_start(std::shared_ptr loader) override; /** * Get the collection of forcing output property names this instance can provide. @@ -290,9 +294,21 @@ 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; + /** + * Requests the BMI to copy its current state into memory. The state will remain in memory until either a new state is made or `free_serialization_state` is called. + * + * @return Span of the serialized data. + */ + virtual const boost::span get_serialization_state(); + /** + * Requests the BMI to load data from a previously saved state. This has a side effect of freeing a current state if it currently exists. + */ + virtual void load_serialization_state(const boost::span state); + /** + * Requests the BMI to clear a currently saved state from memory. + * Existing state pointers should not be used as the stored data may be freed depending on implementation. + */ + virtual void free_serialization_state(); 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 { diff --git a/include/realizations/catchment/Bmi_Multi_Formulation.hpp b/include/realizations/catchment/Bmi_Multi_Formulation.hpp index 64ee733db0..3387f9fa1e 100644 --- a/include/realizations/catchment/Bmi_Multi_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Multi_Formulation.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_BMI_MULTI_FORMULATION_HPP #define NGEN_BMI_MULTI_FORMULATION_HPP -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include @@ -15,6 +15,7 @@ #include "ConfigurationException.hpp" #include "ExternalIntegrationException.hpp" +#include #define BMI_REALIZATION_CFG_PARAM_REQ__MODULES "modules" #define BMI_REALIZATION_CFG_PARAM_OPT__DEFAULT_OUT_VALS "default_output_values" @@ -56,6 +57,12 @@ namespace realization { } }; + void save_state(std::shared_ptr saver) override; + + void load_state(std::shared_ptr loader) override; + + void load_hot_start(std::shared_ptr loader) override; + /** * Convert a time value from the model to an epoch time in seconds. * @@ -620,7 +627,7 @@ namespace realization { // Since this is a nested formulation, support usage of the '{{id}}' syntax for init config file paths. Catchment_Formulation::config_pattern_substitution(properties, BMI_REALIZATION_CFG_PARAM_REQ__INIT_CONFIG, - "{{id}}", id); + "{{id}}", Catchment_Formulation::config_pattern_id_replacement(id)); // Call create_formulation to perform the rest of the typical initialization steps for the formulation. mod->create_formulation(properties); @@ -674,6 +681,7 @@ namespace realization { bool is_realization_legacy_format() const; private: + friend class boost::serialization::access; /** * Setup a deferred provider for a nested module, tracking the class as needed. @@ -773,6 +781,8 @@ namespace realization { friend Bmi_Multi_Formulation_Test; friend class ::Bmi_Cpp_Multi_Array_Test; + template + void serialize(Archive& ar, const unsigned int version); }; } diff --git a/include/realizations/catchment/Bmi_Py_Formulation.hpp b/include/realizations/catchment/Bmi_Py_Formulation.hpp index 76904165c9..d3830d6282 100644 --- a/include/realizations/catchment/Bmi_Py_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Py_Formulation.hpp @@ -35,6 +35,13 @@ namespace realization { bool is_bmi_output_variable(const std::string &var_name) const override; + /** + * Requests the BMI to load data from a previously saved state. This has a side effect of freeing a current state if it currently exists. + * + * The python BMI requires additional messaging for pre-allocating memory for load + */ + void load_serialization_state(const boost::span state) override; + protected: std::shared_ptr construct_model(const geojson::PropertyMap &properties) override; diff --git a/include/realizations/catchment/Catchment_Formulation.hpp b/include/realizations/catchment/Catchment_Formulation.hpp index 5a777c6857..b9ababa3b2 100644 --- a/include/realizations/catchment/Catchment_Formulation.hpp +++ b/include/realizations/catchment/Catchment_Formulation.hpp @@ -7,7 +7,7 @@ #include #include "GenericDataProvider.hpp" -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #define DEFAULT_FORMULATION_OUTPUT_DELIMITER "," @@ -32,6 +32,12 @@ namespace realization { static void config_pattern_substitution(geojson::PropertyMap &properties, const std::string &key, const std::string &pattern, const std::string &replacement); + /**Remove leading non-numeric characters from the ID string. + * + * This may be needed to correct NGEN adding an identifying prefix to the ID with system file names without the prefix. + */ + static std::string config_pattern_id_replacement(const std::string &id); + /** * Get a header line appropriate for a file made up of entries from this type's implementation of * ``get_output_line_for_timestep``. diff --git a/include/realizations/catchment/Formulation.hpp b/include/realizations/catchment/Formulation.hpp index ad8c6f097c..5b63d509ff 100644 --- a/include/realizations/catchment/Formulation.hpp +++ b/include/realizations/catchment/Formulation.hpp @@ -1,6 +1,6 @@ #ifndef FORMULATION_H #define FORMULATION_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include diff --git a/include/realizations/catchment/Formulation_Constructors.hpp b/include/realizations/catchment/Formulation_Constructors.hpp index a1afd75f46..c24b686111 100644 --- a/include/realizations/catchment/Formulation_Constructors.hpp +++ b/include/realizations/catchment/Formulation_Constructors.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_FORMULATION_CONSTRUCTORS_H #define NGEN_FORMULATION_CONSTRUCTORS_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include diff --git a/include/realizations/catchment/Formulation_Manager.hpp b/include/realizations/catchment/Formulation_Manager.hpp index 4006409231..5584b03115 100644 --- a/include/realizations/catchment/Formulation_Manager.hpp +++ b/include/realizations/catchment/Formulation_Manager.hpp @@ -2,7 +2,7 @@ #define NGEN_FORMULATION_MANAGER_H #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include @@ -200,7 +200,7 @@ namespace realization { this->add_formulation( this->construct_formulation_from_config( simulation_time_config, - catchment_config.first, + "cat-" + catchment_config.first, catchment_formulation, output_stream ) @@ -553,7 +553,7 @@ namespace realization { global_copy.formulation.parameters, BMI_REALIZATION_CFG_PARAM_REQ__INIT_CONFIG, "{{id}}", - identifier + Catchment_Formulation::config_pattern_id_replacement(identifier) ); } else { ss.str(""); ss << "init_config is present but empty for identifier: " << identifier << std::endl; @@ -665,7 +665,9 @@ namespace realization { // Replace {{id}} if present if (id_index != std::string::npos) { - filepattern = filepattern.replace(id_index, sizeof("{{id}}") - 1, identifier); + // account generate the regex to search for the ID with or without a prefix + std::string pattern_id = Catchment_Formulation::config_pattern_id_replacement(identifier); + filepattern = filepattern.replace(id_index, sizeof("{{id}}") - 1, pattern_id); } // Compile the file pattern as a regex diff --git a/include/realizations/config/layer.hpp b/include/realizations/config/layer.hpp index fffef3c5d9..c947079306 100644 --- a/include/realizations/config/layer.hpp +++ b/include/realizations/config/layer.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_REALIZATION_CONFIG_LAYER_H #define NGEN_REALIZATION_CONFIG_LAYER_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include diff --git a/include/realizations/config/time.hpp b/include/realizations/config/time.hpp index 0825eef698..b00ed4e09f 100644 --- a/include/realizations/config/time.hpp +++ b/include/realizations/config/time.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_REALIZATION_CONFIG_TIME_H #define NGEN_REALIZATION_CONFIG_TIME_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include diff --git a/include/simulation_time/Simulation_Time.hpp b/include/simulation_time/Simulation_Time.hpp index c017e2c267..d2ea4969d4 100644 --- a/include/simulation_time/Simulation_Time.hpp +++ b/include/simulation_time/Simulation_Time.hpp @@ -1,7 +1,7 @@ #ifndef SIMULATION_TIME_H #define SIMULATION_TIME_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include diff --git a/include/state_save_restore/File_Per_Unit.hpp b/include/state_save_restore/File_Per_Unit.hpp new file mode 100644 index 0000000000..faec8d966a --- /dev/null +++ b/include/state_save_restore/File_Per_Unit.hpp @@ -0,0 +1,38 @@ +#ifndef NGEN_FILE_PER_UNIT_HPP +#define NGEN_FILE_PER_UNIT_HPP + +#include + +class File_Per_Unit_Saver : public State_Saver +{ +public: + File_Per_Unit_Saver(std::string base_path); + ~File_Per_Unit_Saver(); + + std::shared_ptr initialize_snapshot(State_Durability durability) override; + + std::shared_ptr initialize_checkpoint_snapshot(snapshot_time_t epoch, State_Durability durability) override; + + void finalize() override; + +private: + std::string base_path_; +}; + + +class File_Per_Unit_Loader : public State_Loader +{ +public: + File_Per_Unit_Loader(std::string dir_path); + ~File_Per_Unit_Loader() = default; + + void finalize() override { }; + + std::shared_ptr initialize_snapshot() override; + + std::shared_ptr initialize_checkpoint_snapshot(State_Saver::snapshot_time_t epoch) override; +private: + std::string dir_path_; +}; + +#endif // NGEN_FILE_PER_UNIT_HPP diff --git a/include/state_save_restore/State_Save_Restore.hpp b/include/state_save_restore/State_Save_Restore.hpp new file mode 100644 index 0000000000..57a88b5f3e --- /dev/null +++ b/include/state_save_restore/State_Save_Restore.hpp @@ -0,0 +1,183 @@ +#ifndef NGEN_STATE_SAVE_RESTORE_HPP +#define NGEN_STATE_SAVE_RESTORE_HPP + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "State_Save_Utils.hpp" + +class State_Saver; +class State_Loader; +class State_Snapshot_Saver; + +class State_Save_Config +{ +public: + /** + * Expects the tree @param config that potentially contains a "state_saving" key + * + * + */ + State_Save_Config(boost::property_tree::ptree const& config); + + /** + * Get state loaders that perform before the catchments are run. + * + * @return `std::pair`s of the label from the config and an instance of the loader. + */ + std::vector>> start_of_run_loaders() const; + + /** + * Get state savers that perform after the catchments have run to completion. + * + * @return `std::pair`s of the label from the config and an instance of the saver. + */ + std::vector>> end_of_run_savers() const; + + /** + * Get state loader that is intended to be performed before catchment processing starts. + * + * The returned pointer may be NULL if no configuration was made for existing data. + */ + std::unique_ptr hot_start() const; + + struct instance + { + instance(std::string const& direction, std::string const& label, std::string const& path, std::string const& mechanism, std::string const& timing); + + State_Save_Direction direction_; + State_Save_Mechanism mechanism_; + State_Save_When timing_; + std::string label_; + std::string path_; + + std::string mechanism_string() const; + }; + +private: + std::vector instances_; +}; + +class State_Saver +{ +public: + using snapshot_time_t = std::chrono::time_point; + + // Flag type to indicate whether state saving needs to ensure + // stability of saved data wherever it is stored before returning + // success + enum class State_Durability { relaxed, strict }; + + State_Saver() = default; + virtual ~State_Saver() = default; + + static snapshot_time_t snapshot_time_now(); + + /** + * Return an object suitable for saving a simulation state as of a + * particular moment in time, @param epoch + * + * @param durability indicates whether callers expect all + * potential errors to be checked and reported before finalize() + * and/or State_Snapshot_Saver::finish_saving() return + */ + virtual std::shared_ptr initialize_snapshot(State_Durability durability) = 0; + + virtual std::shared_ptr initialize_checkpoint_snapshot(snapshot_time_t epoch, State_Durability durability) = 0; + + /** + * Execute any logic necessary to cleanly finish usage, and + * potentially report errors, before destructors would + * execute. E.g. closing files opened in parallel across MPI + * ranks. + */ + virtual void finalize() = 0; +}; + +class State_Snapshot_Saver +{ +public: + State_Snapshot_Saver() = delete; + State_Snapshot_Saver(State_Saver::State_Durability durability); + virtual ~State_Snapshot_Saver() = default; + + /** + * Capture the data from a single unit of the simulation + */ + virtual void save_unit(std::string const& unit_name, boost::span data) = 0; + + /** + * Execute logic to complete the saving process + * + * Data may be flushed here, and delayed errors may be detected + * and reported here. With relaxed durability, error reports may + * not come until the parent State_Saver::finalize() call is made, + * or ever. + */ + virtual void finish_saving() = 0; + +protected: + State_Saver::State_Durability durability_; +}; + +class State_Snapshot_Loader; + +class State_Loader +{ +public: + State_Loader() = default; + virtual ~State_Loader() = default; + + /** + * Return an object suitable for loading a simulation state as of + * a particular moment in time, @param epoch + */ + virtual std::shared_ptr initialize_snapshot() = 0; + + virtual std::shared_ptr initialize_checkpoint_snapshot(State_Saver::snapshot_time_t epoch) = 0; + + /** + * Execute any logic necessary to cleanly finish usage, and + * potentially report errors, before destructors would + * execute. E.g. closing files opened in parallel across MPI + * ranks. + */ + virtual void finalize() = 0; +}; + +class State_Snapshot_Loader +{ +public: + State_Snapshot_Loader() = default; + virtual ~State_Snapshot_Loader() = default; + + /** + * Check if data of a unit name exists. + */ + virtual bool has_unit(const std::string &unit_name) = 0; + + /** + * Load data from whatever source, and pass it to @param unit_loader->load() + */ + virtual void load_unit(const std::string &unit_name, std::vector &data) = 0; + + /** + * Execute logic to complete the saving process + * + * Data may be flushed here, and delayed errors may be detected + * and reported here. With relaxed durability, error reports may + * not come until the parent State_Saver::finalize() call is made, + * or ever. + */ + virtual void finish_saving() = 0; +}; + +#endif // NGEN_STATE_SAVE_RESTORE_HPP diff --git a/include/state_save_restore/State_Save_Utils.hpp b/include/state_save_restore/State_Save_Utils.hpp new file mode 100644 index 0000000000..9713b660af --- /dev/null +++ b/include/state_save_restore/State_Save_Utils.hpp @@ -0,0 +1,30 @@ +#ifndef NGEN_STATE_SAVE_UTILS_HPP +#define NGEN_STATE_SAVE_UTILS_HPP + +namespace StateSaveNames { + const auto CREATE = "serialization_create"; + const auto STATE = "serialization_state"; + const auto FREE = "serialization_free"; + const auto SIZE = "serialization_size"; + const auto RESET = "reset_time"; +} + +enum class State_Save_Direction { + None = 0, + Save, + Load +}; + +enum class State_Save_Mechanism { + None = 0, + FilePerUnit +}; + +enum class State_Save_When { + None = 0, + EndOfRun, + FirstOfMonth, + StartOfRun +}; + +#endif diff --git a/include/state_save_restore/vecbuf.hpp b/include/state_save_restore/vecbuf.hpp new file mode 100644 index 0000000000..a20bc70e73 --- /dev/null +++ b/include/state_save_restore/vecbuf.hpp @@ -0,0 +1,125 @@ +#ifndef HPP_STRING_VECBUF +#define HPP_STRING_VECBUF +// https://gist.github.com/stephanlachnit/4a06f8475afd144e73235e2a2584b000 +// SPDX-FileCopyrightText: 2023 Stephan Lachnit +// SPDX-License-Identifier: MIT + +#include +#include +#include + +template> +class vecbuf : public std::basic_streambuf { +public: + using streambuf = std::basic_streambuf; + using char_type = typename streambuf::char_type; + using int_type = typename streambuf::int_type; + using traits_type = typename streambuf::traits_type; + using vector = std::vector; + using value_type = typename vector::value_type; + using size_type = typename vector::size_type; + + // Constructor for vecbuf with optional initial capacity + vecbuf(size_type capacity = 0) : vector_() { reserve(capacity); } + + // Forwarder for std::vector::shrink_to_fit() + constexpr void shrink_to_fit() { vector_.shrink_to_fit(); } + + // Forwarder for std::vector::clear() + constexpr void clear() { vector_.clear(); } + + // Forwarder for std::vector::resize(size) + constexpr void resize(size_type size) { vector_.resize(size); } + + // Forwarder for std::vector::reserve + constexpr void reserve(size_type capacity) { vector_.reserve(capacity); setp_from_vector(); } + + // Increase the capacity of the buffer by reserving the current_size + additional_capacity + constexpr void reserve_additional(size_type additional_capacity) { reserve(size() + additional_capacity); } + + // Forwarder for std::vector::data + constexpr value_type* data() { return vector_.data(); } + + // Forwarder for std::vector::size + constexpr size_type size() const { return vector_.size(); } + + // Forwarder for std::vector::capacity + constexpr size_type capacity() const { return vector_.capacity(); } + + // Implements std::basic_streambuf::xsputn + std::streamsize xsputn(const char_type* s, std::streamsize count) override { + try { + reserve_additional(count); + } + catch (const std::bad_alloc& error) { + // reserve did not work, use slow algorithm + return xsputn_slow(s, count); + } + // reserve worked, use fast algorithm + return xsputn_fast(s, count); + } + +protected: + // Calculates value to std::basic_streambuf::pbase from vector + constexpr value_type* pbase_from_vector() const { return const_cast(vector_.data()); } + + // Calculates value to std::basic_streambuf::pptr from vector + constexpr value_type* pptr_from_vector() const { return const_cast(vector_.data() + vector_.size()); } + + // Calculates value to std::basic_streambuf::epptr from vector + constexpr value_type* epptr_from_vector() const { return const_cast(vector_.data()) + vector_.capacity(); } + + // Sets the values for std::basic_streambuf::pbase, std::basic_streambuf::pptr and std::basic_streambuf::epptr from vector + constexpr void setp_from_vector() { streambuf::setp(pbase_from_vector(), epptr_from_vector()); streambuf::pbump(size()); } + +private: + // std::vector containing the data + vector vector_; + + // Fast implementation of std::basic_streambuf::xsputn if reserve_additional(count) succeeded + std::streamsize xsputn_fast(const char_type* s, std::streamsize count) { + // store current pptr (end of vector location) + auto* old_pptr = pptr_from_vector(); + // resize the vector, does not move since space already reserved + vector_.resize(vector_.size() + count); + // directly memcpy new content to old pptr (end of vector before it was resized) + traits_type::copy(old_pptr, s, count); + // reserve() already calls setp_from_vector(), only adjust pptr to new epptr + streambuf::pbump(count); + + return count; + } + + // Slow implementation of std::basic_streambuf::xsputn if reserve_additional(count) did not succeed, might calls std::basic_streambuf::overflow() + std::streamsize xsputn_slow(const char_type* s, std::streamsize count) { + // reserving entire vector failed, emplace char for char + std::streamsize written = 0; + while (written < count) { + try { + // copy one char, should throw eventually std::bad_alloc + vector_.emplace_back(s[written]); + } + catch (const std::bad_alloc& error) { + // try overflow(), if eof return, else continue + int_type c = this->overflow(traits_type::to_int_type(s[written])); + if (traits_type::eq_int_type(c, traits_type::eof())) { + return written; + } + } + // update pbase, pptr and epptr + setp_from_vector(); + written++; + } + return written; + } + +}; + +class membuf : public std::streambuf { +public: + membuf(char *begin, size_t size) { + this->setg(begin, begin, begin + size); + } +}; + +#endif diff --git a/include/utilities/CSV_Reader.h b/include/utilities/CSV_Reader.h index 21adf3d745..bcda340385 100644 --- a/include/utilities/CSV_Reader.h +++ b/include/utilities/CSV_Reader.h @@ -1,6 +1,6 @@ #ifndef CSV_Reader_H #define CSV_Reader_H -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include diff --git a/include/utilities/FileChecker.h b/include/utilities/FileChecker.h index b9304587a0..92b2ee1045 100644 --- a/include/utilities/FileChecker.h +++ b/include/utilities/FileChecker.h @@ -6,7 +6,8 @@ #include #include #include -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" namespace utils { @@ -122,7 +123,7 @@ namespace utils { else { std::stringstream ss; ss << description << " path " << path << " not readable" << std::endl; - LOG(ss.str(), LogLevel::INFO); ss.str(""); + LOG(LogLevel::INFO, ss.str()); ss.str(""); return false; } } diff --git a/include/utilities/Logger.hpp b/include/utilities/Logger.hpp deleted file mode 100644 index 50f6c91220..0000000000 --- a/include/utilities/Logger.hpp +++ /dev/null @@ -1,84 +0,0 @@ -#ifndef LOGGER_HPP -#define LOGGER_HPP - -#include -#include -#include -#include -#include -#include -#include -#include - -enum class LogLevel { - NONE = 0, - DEBUG = 1, - INFO = 2, - WARNING = 3, - SEVERE = 4, - FATAL = 5, -}; - -/** -* Logger Class Used to Output Details of Current Application Flow -*/ -class Logger { - public: - Logger(); - ~Logger() = default; - - // Methods - static void Log(std::string message, LogLevel messageLevel=LogLevel::INFO); - static void Log(LogLevel messageLevel, const char* message, ...); - static void Log(LogLevel messageLevel, std::string message); - bool IsLoggingEnabled(void); - LogLevel GetLogLevel(void); - void SetLogPreferences(LogLevel level=LogLevel::INFO); - - static __always_inline void logMsgAndThrowError(const std::string& message) { - Log(message, LogLevel::SEVERE); - throw std::runtime_error(message); - }; - - static Logger* GetLogger(); - - private: - // Methods - static std::string ConvertLogLevelToString(LogLevel level); - static LogLevel ConvertStringToLogLevel(const std::string& logLevel); - std::string CreateDateString(void); - bool CreateDirectory(const std::string& path); - static std::string CreateTimestamp(bool appendMS=true, bool iso=true); - bool DirectoryExists(const std::string& path); - std::string ExtractFirstNDirs(const std::string& path, int numDirs); - bool FileExists(const std::string& path); - bool FindAndOpenLogConfigFile(std::string path, std::ifstream& configFileStream); - std::string GetLogFilePath(void); - std::string GetParentDirName(const std::string& path); - bool IsValidEnvVarName(const std::string& name); - bool LogFileReady(void); - bool ParseLoggerConfigFile(std::ifstream& jsonFile); - void ReadConfigFile(std::string searchPath); - void SetupLogFile(void); - void ManageLoggingEnvVars(bool set=true); - static std::string ToUpper(const std::string& str); - static std::string TrimString(const std::string& str); - - // Variables - bool loggerInitialized = false; - bool loggingEnabled = true; - std::fstream logFile; - std::string logFileDir = ""; - std::string logFilePath = ""; - LogLevel logLevel = LogLevel::INFO; - std::string moduleName = ""; - std::string ngenResultsDir = ""; - bool openedOnce = false; - - std::unordered_map moduleLogLevels; -}; - -// Placed here to ensure the class is declared before setting this preprocessor symbol -#define LOG Logger::Log - -#endif diff --git a/include/utilities/bmi/mass_balance.hpp b/include/utilities/bmi/mass_balance.hpp index e8883232ff..046b1002e8 100644 --- a/include/utilities/bmi/mass_balance.hpp +++ b/include/utilities/bmi/mass_balance.hpp @@ -152,4 +152,3 @@ namespace models{ namespace bmi{ namespace protocols{ }; }}} - diff --git a/include/utilities/bmi_utilities.hpp b/include/utilities/bmi_utilities.hpp index eea56f6de2..b33f4390bf 100644 --- a/include/utilities/bmi_utilities.hpp +++ b/include/utilities/bmi_utilities.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_BMI_UTILITIES_HPP #define NGEN_BMI_UTILITIES_HPP -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include diff --git a/include/utilities/mdarray/mdarray.hpp b/include/utilities/mdarray/mdarray.hpp index e5a6fa867e..7a229e0e93 100644 --- a/include/utilities/mdarray/mdarray.hpp +++ b/include/utilities/mdarray/mdarray.hpp @@ -5,7 +5,7 @@ #include #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" namespace ngen { diff --git a/include/utilities/mdframe/mdframe.hpp b/include/utilities/mdframe/mdframe.hpp index 4fa97dd5f3..79f66c332b 100644 --- a/include/utilities/mdframe/mdframe.hpp +++ b/include/utilities/mdframe/mdframe.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_MDFRAME_DEFINITION_HPP #define NGEN_MDFRAME_DEFINITION_HPP -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include diff --git a/include/utilities/python/HydrofabricSubsetter.hpp b/include/utilities/python/HydrofabricSubsetter.hpp index eaf71e42e4..0c0bfc1f39 100644 --- a/include/utilities/python/HydrofabricSubsetter.hpp +++ b/include/utilities/python/HydrofabricSubsetter.hpp @@ -1,6 +1,6 @@ #ifndef NGEN_HYDROFABRICSUBSETTER_HPP #define NGEN_HYDROFABRICSUBSETTER_HPP -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include diff --git a/src/NGen.cpp b/src/NGen.cpp index 9fefc64468..128f7a0871 100644 --- a/src/NGen.cpp +++ b/src/NGen.cpp @@ -17,7 +17,7 @@ #include "NGenConfig.h" -#include +#include "ewts_ngen/logger.hpp" #include #include @@ -54,6 +54,8 @@ #include #include +#include + void ngen::exec_info::runtime_summary(std::ostream& stream) noexcept { stream << "Runtime configuration summary:\n"; @@ -446,14 +448,16 @@ int run_ngen(int argc, char* argv[], int mpi_num_procs, int mpi_rank) { #if NGEN_WITH_SQLITE3 try { nexus_collection = ngen::geopackage::read(nexusDataFile, "nexus", nexus_subset_ids); - } catch (...) { + } catch (std::exception &e) { // Handle all exceptions std::string msg = "Geopackage error occurred reading nexuses: " + nexusDataFile; LOG(msg,LogLevel::FATAL); - throw std::runtime_error(msg); + LOG(LogLevel::FATAL, e.what()); + throw; } #else - Logger::logMsgAndThrowError("SQLite3 support required to read GeoPackage files."); + LOG(LogLevel::FATAL, "SQLite3 support required to read GeoPackage files."); + throw std::runtime_error("SQLite3 support required to read GeoPackage files."); #endif } else { nexus_collection = geojson::read(nexusDataFile, nexus_subset_ids); @@ -477,15 +481,17 @@ int run_ngen(int argc, char* argv[], int mpi_num_procs, int mpi_rank) { try { catchment_collection = ngen::geopackage::read(catchmentDataFile, "divides", catchment_subset_ids); - } catch (...) { + } catch (std::exception &e) { // Handle all exceptions std::string msg = "Geopackage error occurred reading divides: " + catchmentDataFile; LOG(msg,LogLevel::FATAL); - throw std::runtime_error(msg); + LOG(LogLevel::FATAL, e.what()); + throw; } #else - Logger::logMsgAndThrowError("SQLite3 support required to read GeoPackage files."); + LOG(LogLevel::FATAL, "SQLite3 support required to read GeoPackage files."); + throw std::runtime_error("SQLite3 support required to read GeoPackage files."); #endif } else { catchment_collection = geojson::read(catchmentDataFile, catchment_subset_ids); @@ -514,9 +520,10 @@ int run_ngen(int argc, char* argv[], int mpi_num_procs, int mpi_rank) { } auto simulation_time_config = realization::config::Time(*possible_simulation_time).make_params(); - sim_time = std::make_shared(simulation_time_config); + auto state_saving_config = State_Save_Config(realization_config); + ss << "Initializing formulations" << std::endl; LOG(ss.str(), LogLevel::INFO); ss.str(""); @@ -696,8 +703,25 @@ int run_ngen(int argc, char* argv[], int mpi_num_procs, int mpi_rank) { std::chrono::duration time_elapsed_init = time_done_init - time_start; LOG("[TIMING]: Init: " + std::to_string(time_elapsed_init.count()), LogLevel::INFO); + { // optionally run hot start loader if set in state saving config + auto hot_start_loader = state_saving_config.hot_start(); + if (hot_start_loader) { + LOG(LogLevel::INFO, "Loading hot start data from prior snapshot."); + std::shared_ptr snapshot_loader = hot_start_loader->initialize_snapshot(); + simulation->load_hot_start(snapshot_loader, manager->get_t_route_config_file_with_path()); + } + } + simulation->run_catchments(); + for (const auto& saver : state_saving_config.end_of_run_savers()) { + LOG(LogLevel::INFO, "Saving end of run data for state " + saver.first); + std::shared_ptr snapshot = saver.second->initialize_snapshot( + State_Saver::State_Durability::strict + ); + simulation->save_end_of_run(snapshot); + } + #if NGEN_WITH_MPI MPI_Barrier(MPI_COMM_WORLD); #endif @@ -733,8 +757,6 @@ int run_ngen(int argc, char* argv[], int mpi_num_procs, int mpi_rank) { std::chrono::duration time_elapsed_nexus_output = time_done_nexus_output - time_done_simulation; LOG("[TIMING]: Nexus outflow file writing: " + std::to_string(time_elapsed_nexus_output.count()), LogLevel::INFO); - manager->finalize(); - #if NGEN_WITH_MPI MPI_Barrier(MPI_COMM_WORLD); #endif @@ -761,6 +783,16 @@ int run_ngen(int argc, char* argv[], int mpi_num_procs, int mpi_rank) { LOG("[TIMING]: Coastal: " + std::to_string(time_elapsed_coastal.count()), LogLevel::INFO); #endif + // run any end-of-run state saving after T-Route has finished but before starting to tear down data structures + for (const auto& end_saver : state_saving_config.end_of_run_savers()) { + LOG(LogLevel::INFO, "Saving end of run simulation data for state saving config " + end_saver.first); + std::shared_ptr snapshot = end_saver.second->initialize_snapshot(State_Saver::State_Durability::strict); + simulation->save_end_of_run(snapshot); + } + + simulation->finalize(); + manager->finalize(); + auto time_done_total = std::chrono::steady_clock::now(); std::chrono::duration time_elapsed_total = time_done_total - time_start; diff --git a/src/bmi/AbstractCLibBmiAdapter.cpp b/src/bmi/AbstractCLibBmiAdapter.cpp index dd7565a272..058aa07ab4 100644 --- a/src/bmi/AbstractCLibBmiAdapter.cpp +++ b/src/bmi/AbstractCLibBmiAdapter.cpp @@ -5,7 +5,8 @@ #include "utilities/logging_utils.h" #include -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" namespace models { namespace bmi { @@ -37,7 +38,8 @@ void AbstractCLibBmiAdapter::dynamic_library_load() { if (bmi_registration_function.empty()) { this->init_exception_msg = "Can't init " + this->model_name + "; empty name given for library's registration function."; - Logger::logMsgAndThrowError(this->init_exception_msg); + LOG(LogLevel::FATAL, this->init_exception_msg); + throw std::runtime_error(this->init_exception_msg); } if (dyn_lib_handle != nullptr) { std::string message = "AbstractCLibBmiAdapter::dynamic_library_load: ignoring attempt to reload dynamic shared library '" + bmi_lib_file + "' for " + this->model_name; @@ -54,7 +56,8 @@ void AbstractCLibBmiAdapter::dynamic_library_load() { if (bmi_lib_file.length() == 0) { this->init_exception_msg = "Can't init " + this->model_name + "; library file path is empty"; - Logger::logMsgAndThrowError(this->init_exception_msg); + LOG(LogLevel::FATAL, this->init_exception_msg); + throw std::runtime_error(this->init_exception_msg); } if (bmi_lib_file.substr(idx) == ".so") { alt_bmi_lib_file = bmi_lib_file.substr(0, idx) + ".dylib"; @@ -77,7 +80,8 @@ void AbstractCLibBmiAdapter::dynamic_library_load() { } else { this->init_exception_msg = "Can't init " + this->model_name + "; unreadable shared library file '" + bmi_lib_file + "'"; - Logger::logMsgAndThrowError(this->init_exception_msg); + LOG(LogLevel::FATAL, this->init_exception_msg); + throw std::runtime_error(this->init_exception_msg); } } @@ -102,10 +106,10 @@ void* AbstractCLibBmiAdapter::dynamic_load_symbol( bool is_null_valid ) { if (dyn_lib_handle == nullptr) { - Logger::logMsgAndThrowError( - "Cannot load symbol '" + symbol_name + - "' without handle to shared library (bmi_lib_file = '" + bmi_lib_file + "')" - ); + this->init_exception_msg = "Cannot load symbol '" + symbol_name + + "' without handle to shared library (bmi_lib_file = '" + bmi_lib_file + "')"; + LOG(LogLevel::FATAL, this->init_exception_msg); + throw std::runtime_error(this->init_exception_msg); } // Call first to ensure any previous error is cleared before trying to load the symbol dlerror(); diff --git a/src/bmi/Bmi_Adapter.cpp b/src/bmi/Bmi_Adapter.cpp index 516efb1da2..a527efd091 100644 --- a/src/bmi/Bmi_Adapter.cpp +++ b/src/bmi/Bmi_Adapter.cpp @@ -1,8 +1,10 @@ +#include #include "bmi/Bmi_Adapter.hpp" #include "bmi/State_Exception.hpp" #include "utilities/FileChecker.h" #include "utilities/logging_utils.h" -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" using namespace std; std::stringstream str_stream; @@ -27,7 +29,8 @@ Bmi_Adapter::Bmi_Adapter( init_exception_msg = "Cannot create and initialize " + this->model_name + " using unreadable file '" + this->bmi_init_config + "'. Error: " + std::strerror(errno); - Logger::logMsgAndThrowError(init_exception_msg); + LOG(LogLevel::FATAL, init_exception_msg); + throw std::runtime_error(init_exception_msg); } str_stream << "Bmi_Adapter: Model name: " << this->model_name << std::endl; LOG(str_stream.str(), LogLevel::INFO); str_stream.str(""); @@ -43,7 +46,7 @@ double Bmi_Adapter::get_time_convert_factor() { input_units = GetTimeUnits(); } catch(std::exception &e){ - //Re-throwing any exception as a runtime_error so we don't lose + //Re-throwing any exception as a std::runtime_error so we don't lose //the error context/message. We will lose the original exception type, though //When a python exception is raised from the py adapter subclass, the //pybind exception is lost and all we see is a generic "uncaught exception" @@ -70,9 +73,8 @@ void Bmi_Adapter::Initialize() { // previous message errno = 0; if (model_initialized && !init_exception_msg.empty()) { - Logger::logMsgAndThrowError( - "Previous " + model_name + " init attempt had exception: \n\t" + init_exception_msg - ); + LOG(LogLevel::FATAL, init_exception_msg); + throw std::runtime_error(init_exception_msg); } // If there was previous init attempt w/ (implicitly) no exception on previous attempt, just // return @@ -82,7 +84,8 @@ void Bmi_Adapter::Initialize() { init_exception_msg = "Cannot initialize " + model_name + " using unreadable file '" + bmi_init_config + "'. Error: " + std::strerror(errno); ; - Logger::logMsgAndThrowError(init_exception_msg); + LOG(LogLevel::FATAL, init_exception_msg); + throw std::runtime_error(init_exception_msg); } else { try { // TODO: make this same name as used with other testing (adjust name in docstring above @@ -104,10 +107,10 @@ void Bmi_Adapter::Initialize() { void Bmi_Adapter::Initialize(std::string config_file) { if (config_file != bmi_init_config && model_initialized) { - Logger::logMsgAndThrowError( - "Model init previously attempted; cannot change config from " + bmi_init_config + - " to " + config_file - ); + LOG(LogLevel::FATAL, "Model init previously attempted; cannot change config from " + bmi_init_config + + " to " + config_file); + throw std::runtime_error("Model init previously attempted; cannot change config from " + bmi_init_config + + " to " + config_file); } str_stream << __FILE__ << ":" << __LINE__ << " Bmi_Adapter::Initialize: config_file = " << config_file << std::endl; LOG(str_stream.str(), LogLevel::INFO); str_stream.str(""); @@ -123,7 +126,8 @@ void Bmi_Adapter::Initialize(std::string config_file) { } catch (models::external::State_Exception& e) { throw e; } catch (std::exception& e) { - Logger::logMsgAndThrowError(e.what()); + LOG(LogLevel::FATAL, e.what()); + throw std::runtime_error(e.what()); } } diff --git a/src/bmi/Bmi_C_Adapter.cpp b/src/bmi/Bmi_C_Adapter.cpp index e85678946f..02ce6bcb71 100644 --- a/src/bmi/Bmi_C_Adapter.cpp +++ b/src/bmi/Bmi_C_Adapter.cpp @@ -2,7 +2,8 @@ #include #include -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" using namespace models::bmi; @@ -120,7 +121,8 @@ Bmi_C_Adapter::Bmi_C_Adapter(Bmi_C_Adapter &adapter) : model_name(adapter.model_ std::string Bmi_C_Adapter::GetComponentName() { char component_name[BMI_MAX_COMPONENT_NAME]; if (bmi_model->get_component_name(bmi_model.get(), component_name) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get model component name."); + LOG(LogLevel::FATAL, "failed to get model component name."); + std::runtime_error("failed to get model component name."); } return {component_name}; } @@ -129,7 +131,8 @@ double Bmi_C_Adapter::GetCurrentTime() { double current_time; int result = bmi_model->get_current_time(bmi_model.get(), ¤t_time); if (result != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get current model time."); + LOG(LogLevel::FATAL, "failed to get current model time."); + std::runtime_error("failed to get current model time."); } return current_time; } @@ -138,7 +141,8 @@ double Bmi_C_Adapter::GetEndTime() { double end_time; int result = bmi_model->get_end_time(bmi_model.get(), &end_time); if (result != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get model end time."); + LOG(LogLevel::FATAL, " failed to get model end time."); + std::runtime_error("failed to get model end time."); } return end_time; } @@ -171,7 +175,8 @@ double Bmi_C_Adapter::GetStartTime() { double start_time; int result = bmi_model->get_start_time(bmi_model.get(), &start_time); if (result != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get model start time."); + LOG(LogLevel::FATAL, "failed to get model start time."); + std::runtime_error("failed to get model start time."); } return start_time; } @@ -180,7 +185,8 @@ std::string Bmi_C_Adapter::GetTimeUnits() { char time_units_cstr[BMI_MAX_UNITS_NAME]; int result = bmi_model->get_time_units(bmi_model.get(), time_units_cstr); if (result != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to read time units from model."); + LOG(LogLevel::FATAL, "failed to read time units from model."); + std::runtime_error("failed to read time units from model."); } return std::string(time_units_cstr); } @@ -195,7 +201,8 @@ int Bmi_C_Adapter::GetVarItemsize(std::string name) { int size; int success = bmi_model->get_var_itemsize(bmi_model.get(), name.c_str(), &size); if (success != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable item size for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable item size for " + name + "."); + std::runtime_error("failed to get variable item size for " + name + "."); } return size; } @@ -204,7 +211,8 @@ int Bmi_C_Adapter::GetVarNbytes(std::string name) { int size; int success = bmi_model->get_var_nbytes(bmi_model.get(), name.c_str(), &size); if (success != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable array size (i.e., nbytes) for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable array size (i.e., nbytes) for " + name + "."); + std::runtime_error("failed to get variable array size (i.e., nbytes) for " + name + "."); } return size; } @@ -213,7 +221,8 @@ std::string Bmi_C_Adapter::GetVarType(std::string name) { char type_c_str[BMI_MAX_TYPE_NAME]; int success = bmi_model->get_var_type(bmi_model.get(), name.c_str(), type_c_str); if (success != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable type for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable type for " + name + "."); + std::runtime_error("failed to get variable type for " + name + "."); } return std::string(type_c_str); } @@ -222,7 +231,8 @@ std::string Bmi_C_Adapter::GetVarUnits(std::string name) { char units_c_str[BMI_MAX_UNITS_NAME]; int success = bmi_model->get_var_units(bmi_model.get(), name.c_str(), units_c_str); if (success != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable units for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable units for " + name + "."); + std::runtime_error("failed to get variable units for " + name + "."); } return std::string(units_c_str); } @@ -231,7 +241,7 @@ std::string Bmi_C_Adapter::GetVarLocation(std::string name) { char location_c_str[BMI_MAX_LOCATION_NAME]; int success = bmi_model->get_var_location(bmi_model.get(), name.c_str(), location_c_str); if (success != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable location for " + name + "."); + std::runtime_error("failed to get variable location for " + name + "."); } return std::string(location_c_str); } @@ -240,7 +250,8 @@ int Bmi_C_Adapter::GetVarGrid(std::string name) { int grid; int success = bmi_model->get_var_grid(bmi_model.get(), name.c_str(), &grid); if (success != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable grid for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable grid for " + name + "."); + std::runtime_error("failed to get variable grid for " + name + "."); } return grid; } @@ -249,7 +260,8 @@ std::string Bmi_C_Adapter::GetGridType(int grid_id) { char gridtype_c_str[BMI_MAX_TYPE_NAME]; int success = bmi_model->get_grid_type(bmi_model.get(), grid_id, gridtype_c_str); if (success != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid type for grid ID " + std::to_string(grid_id) + "."); + LOG(LogLevel::FATAL, "failed to get grid type for grid ID " + std::to_string(grid_id) + "."); + std::runtime_error("failed to get grid type for grid ID " + std::to_string(grid_id) + "."); } return std::string(gridtype_c_str); } @@ -258,7 +270,8 @@ int Bmi_C_Adapter::GetGridRank(int grid_id) { int gridrank; int success = bmi_model->get_grid_rank(bmi_model.get(), grid_id, &gridrank); if (success != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid rank for grid ID " + std::to_string(grid_id) + "."); + LOG(LogLevel::FATAL, "failed to get grid rank for grid ID " + std::to_string(grid_id) + "."); + std::runtime_error("failed to get grid rank for grid ID " + std::to_string(grid_id) + "."); } return gridrank; } @@ -267,7 +280,8 @@ int Bmi_C_Adapter::GetGridSize(int grid_id) { int gridsize; int success = bmi_model->get_grid_size(bmi_model.get(), grid_id, &gridsize); if (success != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid size for grid ID " + std::to_string(grid_id) + "."); + LOG(LogLevel::FATAL, "failed to get grid size for grid ID " + std::to_string(grid_id) + "."); + std::runtime_error("failed to get grid size for grid ID " + std::to_string(grid_id) + "."); } return gridsize; } @@ -282,7 +296,8 @@ std::shared_ptr> Bmi_C_Adapter::inner_get_variable_name variableCount = (is_input_variables) ? inner_get_input_item_count() : inner_get_output_item_count(); } catch (const std::exception &e) { - Logger::logMsgAndThrowError(model_name + " failed to count of " + varType + " variable names array."); + LOG(LogLevel::FATAL, "failed to count of " + varType + " variable names array."); + std::runtime_error("failed to count of " + varType + " variable names array."); } // With variable count now obtained, create the vector @@ -305,7 +320,8 @@ std::shared_ptr> Bmi_C_Adapter::inner_get_variable_name names_result = bmi_model->get_output_var_names(bmi_model.get(), names_array.data()); } if (names_result != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get array of output variables names."); + LOG(LogLevel::FATAL, "failed to get array of output variables names."); + std::runtime_error("failed to get array of output variables names."); } // Then convert from array of C strings to vector of strings, freeing the allocated space as we go @@ -369,44 +385,51 @@ void Bmi_C_Adapter::UpdateUntil(double time) { void Bmi_C_Adapter::GetGridShape(const int grid, int *shape) { if (bmi_model->get_grid_shape(bmi_model.get(), grid, shape) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " shape."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " shape."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " shape."); } } void Bmi_C_Adapter::GetGridSpacing(const int grid, double *spacing) { if (bmi_model->get_grid_spacing(bmi_model.get(), grid, spacing) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " spacing."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " spacing."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " spacing."); } } void Bmi_C_Adapter::GetGridOrigin(const int grid, double *origin) { if (bmi_model->get_grid_origin(bmi_model.get(), grid, origin) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " origin."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " origin."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " origin."); } } void Bmi_C_Adapter::GetGridX(const int grid, double *x) { if (bmi_model->get_grid_x(bmi_model.get(), grid, x) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " x."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " x."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " x."); } } void Bmi_C_Adapter::GetGridY(const int grid, double *y) { if (bmi_model->get_grid_y(bmi_model.get(), grid, y) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " y."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " y."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " y."); } } void Bmi_C_Adapter::GetGridZ(const int grid, double *z) { if (bmi_model->get_grid_z(bmi_model.get(), grid, z) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " z."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " z."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " z."); } } int Bmi_C_Adapter::GetGridNodeCount(const int grid) { int count; if (bmi_model->get_grid_node_count(bmi_model.get(), grid, &count) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " node count."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " node count."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " node count."); } return count; } @@ -414,7 +437,8 @@ int Bmi_C_Adapter::GetGridNodeCount(const int grid) { int Bmi_C_Adapter::GetGridEdgeCount(const int grid) { int count; if (bmi_model->get_grid_edge_count(bmi_model.get(), grid, &count) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " edge count."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " edge count."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " edge count."); } return count; } @@ -422,31 +446,36 @@ int Bmi_C_Adapter::GetGridEdgeCount(const int grid) { int Bmi_C_Adapter::GetGridFaceCount(const int grid) { int count; if (bmi_model->get_grid_face_count(bmi_model.get(), grid, &count) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " face count."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " face count."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " face count."); } return count; } void Bmi_C_Adapter::GetGridEdgeNodes(const int grid, int *edge_nodes) { if (bmi_model->get_grid_edge_nodes(bmi_model.get(), grid, edge_nodes) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " edge nodes."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " edge nodes."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " edge nodes."); } } void Bmi_C_Adapter::GetGridFaceEdges(const int grid, int *face_edges) { if (bmi_model->get_grid_face_edges(bmi_model.get(), grid, face_edges) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " face edges."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " face edges."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " face edges."); } } void Bmi_C_Adapter::GetGridFaceNodes(const int grid, int *face_nodes) { if (bmi_model->get_grid_face_nodes(bmi_model.get(), grid, face_nodes) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " face nodes."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " face nodes."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " face nodes."); } } void Bmi_C_Adapter::GetGridNodesPerFace(const int grid, int *nodes_per_face) { if (bmi_model->get_grid_nodes_per_face(bmi_model.get(), grid, nodes_per_face) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " nodes per face."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " nodes per face."); + std::runtime_error("failed to get grid " + std::to_string(grid) + " nodes per face."); } } diff --git a/src/bmi/Bmi_Fortran_Adapter.cpp b/src/bmi/Bmi_Fortran_Adapter.cpp index de507b4721..4b0c61ff24 100644 --- a/src/bmi/Bmi_Fortran_Adapter.cpp +++ b/src/bmi/Bmi_Fortran_Adapter.cpp @@ -1,5 +1,5 @@ #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #if NGEN_WITH_BMI_FORTRAN #include "bmi/Bmi_Fortran_Adapter.hpp" @@ -9,7 +9,8 @@ using namespace models::bmi; std::string Bmi_Fortran_Adapter::GetComponentName() { char component_name[BMI_MAX_COMPONENT_NAME]; if (get_component_name(&bmi_model->handle, component_name) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get model component name."); + LOG(LogLevel::FATAL, "failed to get model component name."); + throw std::runtime_error("failed to get model component name."); } return {component_name}; } @@ -17,7 +18,8 @@ std::string Bmi_Fortran_Adapter::GetComponentName() { double Bmi_Fortran_Adapter::GetCurrentTime() { double current_time; if (get_current_time(&bmi_model->handle, ¤t_time) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get current model time."); + LOG(LogLevel::FATAL, "failed to get current model time."); + throw std::runtime_error("failed to get current model time."); } return current_time; } @@ -25,7 +27,8 @@ double Bmi_Fortran_Adapter::GetCurrentTime() { double Bmi_Fortran_Adapter::GetEndTime() { double end_time; if (get_end_time(&bmi_model->handle, &end_time) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get model end time."); + LOG(LogLevel::FATAL, "failed to get model end time."); + throw std::runtime_error("failed to get model end time."); } return end_time; } @@ -47,7 +50,8 @@ std::vector Bmi_Fortran_Adapter::GetOutputVarNames() { double Bmi_Fortran_Adapter::GetStartTime() { double start_time; if (get_start_time(&bmi_model->handle, &start_time) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get model start time."); + LOG(LogLevel::FATAL, "failed to get model start time."); + throw std::runtime_error("failed to get model start time."); } return start_time; } @@ -57,7 +61,8 @@ double Bmi_Fortran_Adapter::GetTimeStep() { //return *get_bmi_model_time_step_size_ptr(); double ts; if (get_time_step(&bmi_model->handle, &ts) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get model time step size."); + LOG(LogLevel::FATAL, "failed to get model time step size."); + throw std::runtime_error("failed to get model time step size."); } return ts; } @@ -65,7 +70,8 @@ double Bmi_Fortran_Adapter::GetTimeStep() { std::string Bmi_Fortran_Adapter::GetTimeUnits() { char time_units_cstr[BMI_MAX_UNITS_NAME]; if (get_time_units(&bmi_model->handle, time_units_cstr) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to read time units from model."); + LOG(LogLevel::FATAL, "failed to read time units from model."); + throw std::runtime_error("failed to read time units from model."); } return {time_units_cstr}; } @@ -79,13 +85,15 @@ void Bmi_Fortran_Adapter::GetValueAtIndices(std::string name, void *dest, int *i " indices."); } */ - Logger::logMsgAndThrowError("Fortran BMI module integration does not currently support getting values by index"); + LOG(LogLevel::FATAL, "Fortran BMI module integration does not currently support getting values by index"); + throw std::runtime_error("Fortran BMI module integration does not currently support getting values by index"); } int Bmi_Fortran_Adapter::GetVarItemsize(std::string name) { int size; if (get_var_itemsize(&bmi_model->handle, name.c_str(), &size) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable item size for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable item size for " + name + "."); + throw std::runtime_error("failed to get variable item size for " + name + "."); } return size; } @@ -93,7 +101,8 @@ int Bmi_Fortran_Adapter::GetVarItemsize(std::string name) { int Bmi_Fortran_Adapter::GetVarNbytes(std::string name) { int size; if (get_var_nbytes(&bmi_model->handle, name.c_str(), &size) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable array size (i.e., nbytes) for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable array size (i.e., nbytes) for " + name + "."); + throw std::runtime_error("failed to get variable array size (i.e., nbytes) for " + name + "."); } return size; } @@ -105,7 +114,8 @@ std::string Bmi_Fortran_Adapter::GetVarType(std::string name) { std::string Bmi_Fortran_Adapter::GetVarUnits(std::string name) { char units_c_str[BMI_MAX_UNITS_NAME]; if (get_var_units(&bmi_model->handle, name.c_str(), units_c_str) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable units for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable units for " + name + "."); + throw std::runtime_error("failed to get variable units for " + name + "."); } return std::string(units_c_str); } @@ -113,7 +123,8 @@ std::string Bmi_Fortran_Adapter::GetVarUnits(std::string name) { std::string Bmi_Fortran_Adapter::GetVarLocation(std::string name) { char location_c_str[BMI_MAX_LOCATION_NAME]; if (get_var_location(&bmi_model->handle, name.c_str(), location_c_str) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable location for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable location for " + name + "."); + throw std::runtime_error("failed to get variable location for " + name + "."); } return std::string(location_c_str); } @@ -121,7 +132,8 @@ std::string Bmi_Fortran_Adapter::GetVarLocation(std::string name) { int Bmi_Fortran_Adapter::GetVarGrid(std::string name) { int grid; if (get_var_grid(&bmi_model->handle, name.c_str(), &grid) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get variable grid for " + name + "."); + LOG(LogLevel::FATAL, "failed to get variable grid for " + name + "."); + throw std::runtime_error("failed to get variable grid for " + name + "."); } return grid; } @@ -129,7 +141,8 @@ int Bmi_Fortran_Adapter::GetVarGrid(std::string name) { std::string Bmi_Fortran_Adapter::GetGridType(int grid_id) { char gridtype_c_str[BMI_MAX_TYPE_NAME]; if (get_grid_type(&bmi_model->handle, &grid_id, gridtype_c_str) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid type for grid ID " + std::to_string(grid_id) + "."); + LOG(LogLevel::FATAL, "failed to get grid type for grid ID " + std::to_string(grid_id) + "."); + throw std::runtime_error("failed to get grid type for grid ID " + std::to_string(grid_id) + "."); } return std::string(gridtype_c_str); } @@ -137,7 +150,8 @@ std::string Bmi_Fortran_Adapter::GetGridType(int grid_id) { int Bmi_Fortran_Adapter::GetGridRank(int grid_id) { int gridrank; if (get_grid_rank(&bmi_model->handle, &grid_id, &gridrank) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid rank for grid ID " + std::to_string(grid_id) + "."); + LOG(LogLevel::FATAL, "failed to get grid rank for grid ID " + std::to_string(grid_id) + "."); + throw std::runtime_error("failed to get grid rank for grid ID " + std::to_string(grid_id) + "."); } return gridrank; } @@ -145,7 +159,8 @@ int Bmi_Fortran_Adapter::GetGridRank(int grid_id) { int Bmi_Fortran_Adapter::GetGridSize(int grid_id) { int gridsize; if (get_grid_size(&bmi_model->handle, &grid_id, &gridsize) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid size for grid ID " + std::to_string(grid_id) + "."); + LOG(LogLevel::FATAL, "failed to get grid size for grid ID " + std::to_string(grid_id) + "."); + throw std::runtime_error("failed to get grid size for grid ID " + std::to_string(grid_id) + "."); } return gridsize; } @@ -167,7 +182,8 @@ void Bmi_Fortran_Adapter::SetValueAtIndices(std::string name, int *inds, int cou "Failed to set specified indexes for " + name + " variable of " + model_name); } */ - Logger::logMsgAndThrowError("Fortran BMI module integration does not currently support setting values by index"); + LOG(LogLevel::FATAL, "Fortran BMI module integration does not currently support setting values by index"); + throw std::runtime_error("Fortran BMI module integration does not currently support setting values by index"); } /** @@ -201,44 +217,51 @@ void Bmi_Fortran_Adapter::UpdateUntil(double time) { void Bmi_Fortran_Adapter::GetGridShape(int grid, int *shape) { if (get_grid_shape(&bmi_model->handle, &grid, shape) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " shape."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " shape."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " shape."); } } void Bmi_Fortran_Adapter::GetGridSpacing(int grid, double *spacing) { if (get_grid_spacing(&bmi_model->handle, &grid, spacing) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " spacing."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " spacing."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " spacing."); } } void Bmi_Fortran_Adapter::GetGridOrigin(int grid, double *origin) { if (get_grid_origin(&bmi_model->handle, &grid, origin) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " origin."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " origin."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " origin."); } } void Bmi_Fortran_Adapter::GetGridX(int grid, double *x) { if (get_grid_x(&bmi_model->handle, &grid, x) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " x."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " x."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " x."); } } void Bmi_Fortran_Adapter::GetGridY(int grid, double *y) { if (get_grid_y(&bmi_model->handle, &grid, y) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " y."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " y."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " y."); } } void Bmi_Fortran_Adapter::GetGridZ(int grid, double *z) { if (get_grid_z(&bmi_model->handle, &grid, z) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " z."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " z."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " z."); } } int Bmi_Fortran_Adapter::GetGridNodeCount(int grid) { int count; if (get_grid_node_count(&bmi_model->handle, &grid, &count) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " node count."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " node count."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " node count."); } return count; } @@ -246,7 +269,8 @@ int Bmi_Fortran_Adapter::GetGridNodeCount(int grid) { int Bmi_Fortran_Adapter::GetGridEdgeCount(int grid) { int count; if (get_grid_edge_count(&bmi_model->handle, &grid, &count) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " edge count."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " edge count."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " edge count."); } return count; } @@ -254,32 +278,37 @@ int Bmi_Fortran_Adapter::GetGridEdgeCount(int grid) { int Bmi_Fortran_Adapter::GetGridFaceCount(int grid) { int count; if (get_grid_face_count(&bmi_model->handle, &grid, &count) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " face count."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " face count."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " face count."); } return count; } void Bmi_Fortran_Adapter::GetGridEdgeNodes(int grid, int *edge_nodes) { if (get_grid_edge_nodes(&bmi_model->handle, &grid, edge_nodes) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " edge nodes."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " edge nodes."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " edge nodes."); } } void Bmi_Fortran_Adapter::GetGridFaceEdges(int grid, int *face_edges) { if (get_grid_face_edges(&bmi_model->handle, &grid, face_edges) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " face edges."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " face edges."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " face edges."); } } void Bmi_Fortran_Adapter::GetGridFaceNodes(int grid, int *face_nodes) { if (get_grid_face_nodes(&bmi_model->handle, &grid, face_nodes) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " face nodes."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " face nodes."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " face nodes."); } } void Bmi_Fortran_Adapter::GetGridNodesPerFace(int grid, int *nodes_per_face) { if (get_grid_nodes_per_face(&bmi_model->handle, &grid, nodes_per_face) != BMI_SUCCESS) { - Logger::logMsgAndThrowError(model_name + " failed to get grid " + std::to_string(grid) + " nodes per face."); + LOG(LogLevel::FATAL, "failed to get grid " + std::to_string(grid) + " nodes per face."); + throw std::runtime_error("failed to get grid " + std::to_string(grid) + " nodes per face."); } } diff --git a/src/bmi/Bmi_Py_Adapter.cpp b/src/bmi/Bmi_Py_Adapter.cpp index 7436e2b48c..d4d667fedc 100644 --- a/src/bmi/Bmi_Py_Adapter.cpp +++ b/src/bmi/Bmi_Py_Adapter.cpp @@ -5,7 +5,7 @@ #include #include #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include "bmi/Bmi_Py_Adapter.hpp" @@ -102,27 +102,39 @@ void Bmi_Py_Adapter::GetValue(std::string name, void *dest) { catch (std::runtime_error &e) { std::string msg = "Encountered error getting C++ type during call to GetValue: \n"; msg += e.what(); - Logger::logMsgAndThrowError(msg); + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } - - if (cxx_type == "short") { - copy_to_array(name, (short *) dest); + if (cxx_type == "signed char") { + this->copy_to_array(name, static_cast(dest)); + } else if (cxx_type == "unsigned char") { + this->copy_to_array(name, static_cast(dest)); + } else if (cxx_type == "short") { + this->copy_to_array(name, static_cast(dest)); + } else if (cxx_type == "unsigned short") { + this->copy_to_array(name, static_cast(dest)); } else if (cxx_type == "int") { - copy_to_array(name, (int *) dest); + this->copy_to_array(name, static_cast(dest)); + } else if (cxx_type == "unsigned int") { + this->copy_to_array(name, static_cast(dest)); } else if (cxx_type == "long") { - copy_to_array(name, (long *) dest); + this->copy_to_array(name, static_cast(dest)); + } else if (cxx_type == "unsigned long") { + this->copy_to_array(name, static_cast(dest)); } else if (cxx_type == "long long") { - copy_to_array(name, (long long *) dest); + this->copy_to_array(name, static_cast(dest)); + } else if (cxx_type == "unsigned long long") { + this->copy_to_array(name, static_cast(dest)); } else if (cxx_type == "float") { - copy_to_array(name, (float *) dest); + this->copy_to_array(name, static_cast(dest)); } else if (cxx_type == "double") { - copy_to_array(name, (double *) dest); + this->copy_to_array(name, static_cast(dest)); } else if (cxx_type == "long double") { - copy_to_array(name, (long double *) dest); + this->copy_to_array(name, static_cast(dest)); } else { - Logger::logMsgAndThrowError("Bmi_Py_Adapter can't get value of unsupported type: " + cxx_type); + LOG(LogLevel::FATAL, "Bmi_Py_Adapter can't get value of unsupported type: " + cxx_type); + throw std::runtime_error("Bmi_Py_Adapter can't get value of unsupported type: " + cxx_type); } - } void Bmi_Py_Adapter::GetValueAtIndices(std::string name, void *dest, int *inds, int count) { @@ -176,43 +188,44 @@ std::string Bmi_Py_Adapter::get_bmi_type_simple_name() const { * type and size of the variable in question via @ref GetVarType and @ref GetVarItemsize to infer the native * type for this variable (i.e., the actual type for the values pointed to by ``src``). It then uses this * as the type param in a nested called to the template-based @ref set_value_at_indices. If such a type - * param cannot be determined, a ``runtime_error`` is thrown. + * param cannot be determined, a ``std::runtime_error`` is thrown. * * @param name The name of the involved BMI variable. * @param inds A C++ integer array of indices to update, corresponding to each value in ``src``. * @param count Number of elements in the ``inds`` and ``src`` arrays. * @param src A C++ array containing the new values to be set in the BMI variable. - * @throws runtime_error Thrown if @ref GetVarType and @ref GetVarItemsize functions return a combination for + * @throws std::runtime_error Thrown if @ref GetVarType and @ref GetVarItemsize functions return a combination for * which there is not support for mapping to a native type in the framework. * @see set_value_at_indices */ void Bmi_Py_Adapter::SetValueAtIndices(std::string name, int *inds, int count, void *src) { std::string val_type = GetVarType(name); size_t val_item_size = (size_t)GetVarItemsize(name); - - // The available types and how they are handled here should match what is in get_value_at_indices - if (val_type == "int" && val_item_size == sizeof(short)) { - set_value_at_indices(name, inds, count, src, val_type); - } else if (val_type == "int" && val_item_size == sizeof(int)) { - set_value_at_indices(name, inds, count, src, val_type); - } else if (val_type == "int" && val_item_size == sizeof(long)) { - set_value_at_indices(name, inds, count, src, val_type); - } else if (val_type == "int" && val_item_size == sizeof(long long)) { - set_value_at_indices(name, inds, count, src, val_type); - } else if (val_type == "float" && val_item_size == sizeof(float)) { - set_value_at_indices(name, inds, count, src, val_type); - } else if (val_type == "float" && val_item_size == sizeof(double)) { - set_value_at_indices(name, inds, count, src, val_type); - } else if (val_type == "float64" && val_item_size == sizeof(double)) { - set_value_at_indices(name, inds, count, src, val_type); - } else if (val_type == "float" && val_item_size == sizeof(long double)) { - set_value_at_indices(name, inds, count, src, val_type); - } else { - Logger::logMsgAndThrowError( - "(Bmi_Py_Adapter) Failed attempt to SET values of BMI variable '" + name + "' from '" + + std::string cxx_type = this->get_analogous_cxx_type(val_type, val_item_size); + + // macro for checking type and calling `set_value_at_indices` with that type + #define BMI_PY_SET_VALUE_INDEX(type) if (cxx_type == #type) { this->set_value_at_indices(name, inds, count, src, val_type); } + BMI_PY_SET_VALUE_INDEX(signed char) + else BMI_PY_SET_VALUE_INDEX(unsigned char) + else BMI_PY_SET_VALUE_INDEX(short) + else BMI_PY_SET_VALUE_INDEX(unsigned short) + else BMI_PY_SET_VALUE_INDEX(int) + else BMI_PY_SET_VALUE_INDEX(unsigned int) + else BMI_PY_SET_VALUE_INDEX(long) + else BMI_PY_SET_VALUE_INDEX(unsigned long) + else BMI_PY_SET_VALUE_INDEX(long long) + else BMI_PY_SET_VALUE_INDEX(unsigned long long) + else BMI_PY_SET_VALUE_INDEX(float) + else BMI_PY_SET_VALUE_INDEX(double) + else BMI_PY_SET_VALUE_INDEX(long double) + else { + std::string msg = "(Bmi_Py_Adapter) Failed attempt to SET values of BMI variable '" + name + "' from '" + model_name + "' model: model advertises unsupported combination of type (" + val_type + - ") and size (" + std::to_string(val_item_size) + ")."); + ") and size (" + std::to_string(val_item_size) + ")."; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } + #undef BMI_PY_SET_VALUE_INDEX } void Bmi_Py_Adapter::Update() { diff --git a/src/bmi/CMakeLists.txt b/src/bmi/CMakeLists.txt index aafcc1b6f4..7c8bbd9d1c 100644 --- a/src/bmi/CMakeLists.txt +++ b/src/bmi/CMakeLists.txt @@ -16,6 +16,8 @@ target_link_libraries(ngen_bmi NGen::core_mediator ) +target_link_libraries(ngen_bmi PUBLIC ewts::ewts_ngen_bridge) + target_sources(ngen_bmi PRIVATE "${CMAKE_CURRENT_LIST_DIR}/Bmi_Adapter.cpp" diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1d28f9f628..8793567a65 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -11,6 +11,10 @@ target_link_libraries(core PRIVATE NGen::parallel ) +target_link_libraries(core PUBLIC + ewts::ewts_ngen_bridge + ) + target_include_directories(core PUBLIC ${PROJECT_SOURCE_DIR}/include ${PROJECT_SOURCE_DIR}/include/simulation_time diff --git a/src/core/HY_Features.cpp b/src/core/HY_Features.cpp index b3653ce7e0..3462c6292e 100644 --- a/src/core/HY_Features.cpp +++ b/src/core/HY_Features.cpp @@ -1,7 +1,7 @@ #include #include #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" using namespace hy_features; diff --git a/src/core/HY_Features_MPI.cpp b/src/core/HY_Features_MPI.cpp index 3490e4f1e6..785cd0e3ec 100644 --- a/src/core/HY_Features_MPI.cpp +++ b/src/core/HY_Features_MPI.cpp @@ -1,7 +1,7 @@ #include #include #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #if NGEN_WITH_MPI diff --git a/src/core/Layer.cpp b/src/core/Layer.cpp index aac9ced099..432b918aa3 100644 --- a/src/core/Layer.cpp +++ b/src/core/Layer.cpp @@ -1,5 +1,6 @@ #include #include +#include #if NGEN_WITH_MPI #include "HY_Features_MPI.hpp" @@ -92,3 +93,36 @@ void ngen::Layer::update_models(boost::span catchment_outflows, simulation_time.advance_timestep(); } } + +void ngen::Layer::save_state_snapshot(std::shared_ptr snapshot_saver) +{ + // XXX Handle any of this class's own state as a meta-data unit + + for (auto const& id : processing_units) { + auto r = features.catchment_at(id); + auto r_c = std::dynamic_pointer_cast(r); + r_c->save_state(snapshot_saver); + } +} + +void ngen::Layer::load_state_snapshot(std::shared_ptr snapshot_loader) +{ + // XXX Handle any of this class's own state as a meta-data unit + + for (auto const& id : processing_units) { + auto r = features.catchment_at(id); + auto r_c = std::dynamic_pointer_cast(r); + r_c->load_state(snapshot_loader); + } +} + +void ngen::Layer::load_hot_start(std::shared_ptr snapshot_loader) +{ + // XXX Handle any of this class's own state as a meta-data unit + + for (auto const& id : processing_units) { + auto r = features.catchment_at(id); + auto r_c = std::dynamic_pointer_cast(r); + r_c->load_hot_start(snapshot_loader); + } +} diff --git a/src/core/NgenSimulation.cpp b/src/core/NgenSimulation.cpp index c5cce69103..9addca80d4 100644 --- a/src/core/NgenSimulation.cpp +++ b/src/core/NgenSimulation.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include "ewts_ngen/logger.hpp" #if NGEN_WITH_MPI #include "HY_Features_MPI.hpp" @@ -8,12 +8,15 @@ #include "HY_Features.hpp" #endif -#if NGEN_WITH_ROUTING -#include "bmi/Bmi_Py_Adapter.hpp" -#endif // NGEN_WITH_ROUTING - +#include "state_save_restore/State_Save_Utils.hpp" +#include "state_save_restore/State_Save_Restore.hpp" #include "parallel_utils.h" +namespace { + const auto NGEN_UNIT_NAME = "ngen"; + const auto TROUTE_UNIT_NAME = "troute"; +} + NgenSimulation::NgenSimulation( Simulation_Time const& sim_time, std::vector> layers, @@ -54,6 +57,15 @@ void NgenSimulation::run_catchments() } } +void NgenSimulation::finalize() { +#if NGEN_WITH_ROUTING + if (this->py_troute_) { + this->py_troute_->Finalize(); + this->py_troute_.reset(); + } +#endif // NGEN_WITH_ROUTING +} + void NgenSimulation::advance_models_one_output_step() { // The Inner loop will advance all layers unless doing so will break one of two constraints @@ -108,6 +120,93 @@ void NgenSimulation::advance_models_one_output_step() } +void NgenSimulation::save_state_snapshot(std::shared_ptr snapshot_saver) +{ + // TODO: save the current nexus data + auto unit_name = this->unit_name(); + // XXX Handle self, then recursively pass responsibility to Layers + for (auto& layer : layers_) { + layer->save_state_snapshot(snapshot_saver); + } +} + +void NgenSimulation::save_end_of_run(std::shared_ptr snapshot_saver) +{ + for (auto& layer : layers_) { + layer->save_state_snapshot(snapshot_saver); + } +#if NGEN_WITH_ROUTING + if (this->mpi_rank_ == 0 && this->py_troute_) { + uint64_t serialization_size; + this->py_troute_->SetValue(StateSaveNames::CREATE, &serialization_size); + this->py_troute_->GetValue(StateSaveNames::SIZE, &serialization_size); + void *troute_state = this->py_troute_->GetValuePtr(StateSaveNames::STATE); + boost::span span(static_cast(troute_state), serialization_size); + snapshot_saver->save_unit(TROUTE_UNIT_NAME, span); + this->py_troute_->SetValue(StateSaveNames::FREE, &serialization_size); + } +#endif // NGEN_WITH_ROUTING +} + +void NgenSimulation::load_state_snapshot(std::shared_ptr snapshot_loader) { + // TODO: load the state data related to nexus outflows + auto unit_name = this->unit_name(); + for (auto& layer : layers_) { + layer->load_state_snapshot(snapshot_loader); + } +} + +void NgenSimulation::load_hot_start(std::shared_ptr snapshot_loader, const std::string &t_route_config_file_with_path) { + for (auto& layer : layers_) { + layer->load_hot_start(snapshot_loader); + } +#if NGEN_WITH_ROUTING + if (this->mpi_rank_ == 0) { + bool config_file_set = !t_route_config_file_with_path.empty(); + bool snapshot_exists = snapshot_loader->has_unit(TROUTE_UNIT_NAME); + if (config_file_set && snapshot_exists) { + LOG(LogLevel::DEBUG, "Loading T-Route data from snapshot."); + std::vector troute_data; + snapshot_loader->load_unit(TROUTE_UNIT_NAME, troute_data); + if (py_troute_ == NULL) { + this->make_troute(t_route_config_file_with_path); + } + py_troute_->set_value_unchecked(StateSaveNames::STATE, troute_data.data(), troute_data.size()); + double rt; // unused by the BMI but needed for messaging + py_troute_->SetValue(StateSaveNames::RESET, &rt); + } else if (!config_file_set && !snapshot_exists) { + LOG(LogLevel::DEBUG, "No data set for loading T-Route."); + } else if (config_file_set && !snapshot_exists) { + LOG(LogLevel::WARNING, "A T-Route config file was provided but the load data does not contain T-Route data. T-Route will be run as a cold start."); + } else if (!config_file_set && snapshot_exists) { + LOG(LogLevel::WARNING, "A T-Route hot start snapshot exists but no config file was provided. T-Route will not be loaded or run,"); + } + } +#endif // NGEN_WITH_ROUTING +} + + +void NgenSimulation::make_troute(const std::string &t_route_config_file_with_path) { +#if NGEN_WITH_ROUTING + this->py_troute_ = std::make_unique( + "T-Route", + t_route_config_file_with_path, + "troute_nwm_bmi.troute_bmi.BmiTroute", + true + ); +#endif // NGEN_WITH_ROUTING +} + + +std::string NgenSimulation::unit_name() const { +#if NGEN_WITH_MPI + return "ngen_" + std::to_string(this->mpi_rank_); +#else + return "ngen_0"; +#endif // NGEN_WITH_MPI +} + + int NgenSimulation::get_nexus_index(std::string const& nexus_id) const { auto iter = nexus_indexes_.find(nexus_id); @@ -203,14 +302,13 @@ void NgenSimulation::run_routing(NgenSimulation::hy_features_t &features, std::s int delta_time = sim_time_->get_output_interval_seconds(); // model for routing - models::bmi::Bmi_Py_Adapter py_troute("T-Route", t_route_config_file_with_path, "troute_nwm_bmi.troute_bmi.BmiTroute", true); + if (this->py_troute_ == NULL) { + this->make_troute(t_route_config_file_with_path); + } + this->py_troute_->set_value_unchecked("ngen_dt", &delta_time, 1); - // tell BMI to resize nexus containers - int64_t nexus_count = routing_nexus_indexes->size(); - py_troute.SetValue("land_surface_water_source__volume_flow_rate__count", &nexus_count); - py_troute.SetValue("land_surface_water_source__id__count", &nexus_count); // set up nexus id indexes - std::vector nexus_df_index(nexus_count); + std::vector nexus_df_index(routing_nexus_indexes->size()); for (const auto& key_value : *routing_nexus_indexes) { int id_index = key_value.second; @@ -228,14 +326,11 @@ void NgenSimulation::run_routing(NgenSimulation::hy_features_t &features, std::s } nexus_df_index[id_index] = id_as_int; } - py_troute.SetValue("land_surface_water_source__id", nexus_df_index.data()); - for (int i = 0; i < number_of_timesteps; ++i) { - py_troute.SetValue("land_surface_water_source__volume_flow_rate", - routing_nexus_downflows->data() + (i * nexus_count)); - py_troute.Update(); - } - // Finalize will write the output file - py_troute.Finalize(); + // use unchecked messaging to allow the BMI to change its container size + py_troute_->set_value_unchecked("land_surface_water_source__id", nexus_df_index.data(), nexus_df_index.size()); + py_troute_->set_value_unchecked("land_surface_water_source__volume_flow_rate", routing_nexus_downflows->data(), routing_nexus_downflows->size()); + // run the T-Route model and create outputs through Update + py_troute_->Update(); } #endif // NGEN_WITH_ROUTING } diff --git a/src/core/SurfaceLayer.cpp b/src/core/SurfaceLayer.cpp index 312f37d3ba..fd06c11c79 100644 --- a/src/core/SurfaceLayer.cpp +++ b/src/core/SurfaceLayer.cpp @@ -1,5 +1,5 @@ #include "SurfaceLayer.hpp" -#include +#include "ewts_ngen/logger.hpp" #if NGEN_WITH_MPI #include "HY_Features_MPI.hpp" diff --git a/src/core/mediator/CMakeLists.txt b/src/core/mediator/CMakeLists.txt index 66b0bc28c8..dcfb2fde6e 100644 --- a/src/core/mediator/CMakeLists.txt +++ b/src/core/mediator/CMakeLists.txt @@ -3,6 +3,8 @@ dynamic_sourced_cxx_library(core_mediator "${CMAKE_CURRENT_SOURCE_DIR}") add_library(NGen::core_mediator ALIAS core_mediator) +target_link_libraries(core_mediator PUBLIC ewts::ewts_ngen_bridge) + if(NGEN_WITH_UDUNITS) target_link_libraries(core_mediator PUBLIC libudunits2) endif() diff --git a/src/core/nexus/CMakeLists.txt b/src/core/nexus/CMakeLists.txt index b35a8c1f08..2e45b39e02 100644 --- a/src/core/nexus/CMakeLists.txt +++ b/src/core/nexus/CMakeLists.txt @@ -3,6 +3,8 @@ dynamic_sourced_cxx_library(core_nexus "${CMAKE_CURRENT_SOURCE_DIR}") add_library(NGen::core_nexus ALIAS core_nexus) +target_link_libraries(core_nexus PUBLIC ewts::ewts_ngen_bridge) + target_include_directories(core_nexus PUBLIC ${PROJECT_SOURCE_DIR}/include/core ${PROJECT_SOURCE_DIR}/include/core/nexus diff --git a/src/core/nexus/HY_PointHydroNexus.cpp b/src/core/nexus/HY_PointHydroNexus.cpp index b38561e2b8..26dd15bbbe 100644 --- a/src/core/nexus/HY_PointHydroNexus.cpp +++ b/src/core/nexus/HY_PointHydroNexus.cpp @@ -2,7 +2,7 @@ #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" typedef boost::error_info errmsg_info; diff --git a/src/core/nexus/HY_PointHydroNexusRemote.cpp b/src/core/nexus/HY_PointHydroNexusRemote.cpp index 1f1e0ab544..3dd9323949 100644 --- a/src/core/nexus/HY_PointHydroNexusRemote.cpp +++ b/src/core/nexus/HY_PointHydroNexusRemote.cpp @@ -6,6 +6,7 @@ #include #include +#include #include // TODO add loggin to this function @@ -126,7 +127,8 @@ double HY_PointHydroNexusRemote::get_downstream_flow(std::string catchment_id, t //because the `add_upstream_flow` call triggers a `send` which removes from the local accounting //all available water and sends it to the remote counterpart for this nexus. std::string msg = "Nexus "+id+" attempted to get_downstream_flow, but its communicator type is sender only."; - Logger::logMsgAndThrowError(msg); + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } else if ( type == receiver || type == sender_receiver ) { @@ -281,4 +283,4 @@ int HY_PointHydroNexusRemote::get_world_rank() return world_rank; } -#endif // NGEN_WITH_MPI \ No newline at end of file +#endif // NGEN_WITH_MPI diff --git a/src/forcing/CMakeLists.txt b/src/forcing/CMakeLists.txt index f64456c873..b02e3c4ff8 100644 --- a/src/forcing/CMakeLists.txt +++ b/src/forcing/CMakeLists.txt @@ -19,6 +19,7 @@ target_link_libraries(forcing PUBLIC NGen::config_header Threads::Threads ) +target_link_libraries(forcing PUBLIC ewts::ewts_ngen_bridge) target_sources(forcing PRIVATE "${CMAKE_CURRENT_LIST_DIR}/NullForcingProvider.cpp") diff --git a/src/forcing/ForcingsEngineDataProvider.cpp b/src/forcing/ForcingsEngineDataProvider.cpp index 91a732f42d..be87ace619 100644 --- a/src/forcing/ForcingsEngineDataProvider.cpp +++ b/src/forcing/ForcingsEngineDataProvider.cpp @@ -3,7 +3,8 @@ #include // timegm #include // std::get_time -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" namespace data_access { namespace detail { @@ -30,9 +31,10 @@ void assert_forcings_engine_requirements() auto mod = interpreter_->getModule(forcings_engine_python_module); auto cls = mod.attr(forcings_engine_python_class).cast(); } catch(std::exception& e) { - Logger::logMsgAndThrowError( - "Failed to initialize ForcingsEngine: ForcingsEngine python module is not installed or is not properly configured. (" + std::string{e.what()} + ")" - ); + std::string msg = "Failed to initialize ForcingsEngine: ForcingsEngine python module is not installed or is not properly configured. (" + + std::string{e.what()} + ")"; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } } @@ -40,7 +42,9 @@ void assert_forcings_engine_requirements() { const auto* wgrib2_exec = std::getenv("WGRIB2"); if (wgrib2_exec == nullptr) { - Logger::logMsgAndThrowError("Failed to initialize ForcingsEngine: $WGRIB2 is not defined"); + std::string msg = "Failed to initialize ForcingsEngine: $WGRIB2 is not defined"; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } } } diff --git a/src/forcing/NetCDFPerFeatureDataProvider.cpp b/src/forcing/NetCDFPerFeatureDataProvider.cpp index ad936ca5d0..9940521252 100644 --- a/src/forcing/NetCDFPerFeatureDataProvider.cpp +++ b/src/forcing/NetCDFPerFeatureDataProvider.cpp @@ -8,7 +8,8 @@ #include #include #include -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" using namespace std; std::stringstream netcdf_ss; @@ -103,14 +104,18 @@ NetCDFPerFeatureDataProvider::NetCDFPerFeatureDataProvider(std::string input_pat // some sanity checks if ( id_dim_count > 1) { - Logger::logMsgAndThrowError("Provided NetCDF file has an \"ids\" variable with more than 1 dimension"); + std::string msg = "Provided NetCDF file has an \"ids\" variable with more than 1 dimension"; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } auto id_dim = ids.getDim(0); if (id_dim.isNull() ) { - Logger::logMsgAndThrowError("Provided NetCDF file has a NULL dimension for variable \"ids\""); + std::string msg = "Provided NetCDF file has a NULL dimension for variable \"ids\""; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } auto num_ids = id_dim.getSize(); @@ -135,8 +140,6 @@ NetCDFPerFeatureDataProvider::NetCDFPerFeatureDataProvider(std::string input_pat // correct string release nc_free_string(num_ids,&string_buffers[0]); -// Modified code to handle units, epoch start, and reading all time values correctly - KSL - // Get the time variable - getVar collects all values at once and stores in memory // Extremely large timespans could be problematic, but for ngen use cases, this should not be a problem auto time_var = nc_file->getVar("Time"); @@ -147,8 +150,25 @@ NetCDFPerFeatureDataProvider::NetCDFPerFeatureDataProvider(std::string input_pat std::vector raw_time(num_times); try { - time_var.getVar(raw_time.data()); - } catch(const netCDF::exceptions::NcException& e) { + auto dim_count = time_var.getDimCount(); + // Old-format files have dimensions (catchment, time), new-format + // files generated by the forcings engine have just (time) + if (dim_count == 2) { + if (time_var.getDim(0).getName() != "catchment-id" || time_var.getDim(1).getName() != "time") { + std::string message = "In NetCDF file '" + input_path + "', 'Time' variable dimensions don't match expectations"; + std::string throw_msg; throw_msg.assign(message); + LOG(throw_msg, LogLevel::WARNING); + throw std::runtime_error(throw_msg); + } + time_var.getVar({0ul, 0ul}, {1ul, num_times}, raw_time.data()); + } else if (dim_count == 1) { + time_var.getVar({0ul}, {num_times}, raw_time.data()); + } else { + throw std::runtime_error("Unexpected " + std::to_string(dim_count) + + " dimensions on Time variable in NetCDF file '" + + input_path + "'"); + } + } catch(const std::exception& e) { netcdf_ss << "Error reading time variable: " << e.what() << std::endl; LOG(netcdf_ss.str(), LogLevel::WARNING); netcdf_ss.str(""); throw; @@ -157,7 +177,6 @@ NetCDFPerFeatureDataProvider::NetCDFPerFeatureDataProvider(std::string input_pat std::string time_units; try { time_var.getAtt("units").getValues(time_units); - } catch(const netCDF::exceptions::NcException& e) { netcdf_ss << "Error reading time units: " << e.what() << std::endl; LOG(netcdf_ss.str(), LogLevel::WARNING); netcdf_ss.str(""); @@ -169,27 +188,46 @@ NetCDFPerFeatureDataProvider::NetCDFPerFeatureDataProvider(std::string input_pat double time_scale_factor = 1.0; std::time_t epoch_start_time = 0; - //The following makes some assumptions that NetCDF output from the forcing engine will be relatively uniform - //Specifically, it assumes time values are in units since the Unix Epoch. - //If the forcings engine outputs additional unit formats, this will need to be expanded - if (time_units.find("minutes since") != std::string::npos) { + std::string time_base_unit; + auto since_index = time_units.find("since"); + if (since_index != std::string::npos) { + time_base_unit = time_units.substr(0, since_index - 1); + + std::string datetime_str = time_units.substr(since_index + 6); + std::tm tm = {}; + std::istringstream ss(datetime_str); + ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); // This may be particularly inflexible + epoch_start_time = timegm(&tm); // timegm may not be available in all environments/OSes ie: Windows + } else { + time_base_unit = time_units; + } + + if (time_base_unit == "minutes") { time_unit = TIME_MINUTES; time_scale_factor = 60.0; - } else if (time_units.find("hours since") != std::string::npos) { + } else if (time_base_unit == "hours") { time_unit = TIME_HOURS; time_scale_factor = 3600.0; - } else { + } else if (time_base_unit == "seconds" || time_base_unit == "s") { time_unit = TIME_SECONDS; time_scale_factor = 1.0; + } else if (time_base_unit == "milliseconds" || time_base_unit == "ms") { + time_unit = TIME_MILLISECONDS; + time_scale_factor = 1.0e-3; + } else if (time_base_unit == "microseconds" || time_base_unit == "us") { + time_unit = TIME_MICROSECONDS; + time_scale_factor = 1.0e-6; + } else if (time_base_unit == "nanoseconds" || time_base_unit == "ns") { + time_unit = TIME_NANOSECONDS; + time_scale_factor = 1.0e-9; + } else { + std::string message = "In NetCDF file '" + input_path + "', time unit '" + time_base_unit + "' could not be converted"; + std::string throw_msg; throw_msg.assign(message); + LOG(throw_msg, LogLevel::WARNING); + throw std::runtime_error(throw_msg); } - //This is also based on the NetCDF from the forcings engine, and may not be super flexible - std::string datetime_str = time_units.substr(time_units.find("since") + 6); - std::tm tm = {}; - std::istringstream ss(datetime_str); - ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); //This may be particularly inflexible - epoch_start_time = timegm(&tm); //timegm may not be available in all environments/OSes ie: Windows + time_vals.resize(raw_time.size()); -// End modification - KSL std::transform(raw_time.begin(), raw_time.end(), time_vals.begin(), [&](const auto& n) { @@ -208,19 +246,28 @@ NetCDFPerFeatureDataProvider::NetCDFPerFeatureDataProvider(std::string input_pat netcdf_ss << "Error: Time intervals are not constant in forcing file\n" << std::endl; log_stream << netcdf_ss.str(); LOG(netcdf_ss.str(), LogLevel::WARNING); netcdf_ss.str(""); - Logger::logMsgAndThrowError("Time intervals in forcing file are not constant"); + std::string msg = "Time intervals in forcing file are not constant"; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } } #endif netcdf_ss << "All time intervals are constant within tolerance." << std::endl; - LOG(netcdf_ss.str(), LogLevel::SEVERE); netcdf_ss.str(""); + LOG(netcdf_ss.str(), LogLevel::DEBUG); netcdf_ss.str(""); // determine start_time and stop_time; start_time = time_vals[0]; stop_time = time_vals.back() + time_stride; sim_to_data_time_offset = sim_start_date_time_epoch - start_time; + + netcdf_ss << "NetCDF Forcing from file '" << input_path << "'" + << "Start time " << (time_t)start_time + << ", Stop time " << (time_t)stop_time + << ", sim_start_date_time_epoch " << sim_start_date_time_epoch + ; + LOG(netcdf_ss.str(), LogLevel::DEBUG); netcdf_ss.str(""); } NetCDFPerFeatureDataProvider::~NetCDFPerFeatureDataProvider() = default; @@ -304,7 +351,8 @@ double NetCDFPerFeatureDataProvider::get_value(const CatchmentAggrDataSelector& auto stride = idx2 - idx1; - std::vector start, count; + std::vector start(2), count(2); + std::vector var_index_map(2); auto cat_pos = id_pos[selector.get_id()]; @@ -325,16 +373,35 @@ double NetCDFPerFeatureDataProvider::get_value(const CatchmentAggrDataSelector& //TODO: Currently assuming a whole variable cache slice across all catchments for a single timestep...but some stuff here to support otherwise. // For reference: https://stackoverflow.com/a/72030286 -//Modified to work with NetCDF dimension shapes and fix errors - KSL size_t cache_slices_t_n = (read_len + cache_slice_t_size - 1) / cache_slice_t_size; // Ceiling division to ensure remainders have a slice - - //Explicitly setting dimension shapes auto dims = ncvar.getDims(); - size_t catchment_dim_size = dims[1].getSize(); - size_t time_dim_size = dims[0].getSize(); - //Cache slicing - modified to work with dimensions structure + int dim_time, dim_catchment; + if (dims.size() != 2) { + std::string message = "Variable dimension count isn't 2"; + std::string throw_msg; throw_msg.assign(message); + LOG(throw_msg, LogLevel::WARNING); + throw std::runtime_error(throw_msg); + } + if (dims[0].getName() == "time" && dims[1].getName() == "catchment-id") { + // Forcings Engine NetCDF output case + dim_time = 0; + dim_catchment = 1; + } else if (dims[1].getName() == "time" && dims[0].getName() == "catchment-id") { + // Classic NetCDF file case + dim_time = 1; + dim_catchment = 0; + } else { + std::string message = "Variable dimensions aren't 'time' and 'catchment-id'"; + std::string throw_msg; throw_msg.assign(message); + LOG(throw_msg, LogLevel::WARNING); + throw std::runtime_error(throw_msg); + } + + size_t time_dim_size = dims[dim_time].getSize(); + size_t catchment_dim_size = dims[dim_catchment].getSize(); + for( size_t i = 0; i < cache_slices_t_n; i++ ) { std::shared_ptr> cached; size_t cache_t_idx = idx1 + i * cache_slice_t_size; @@ -345,14 +412,18 @@ double NetCDFPerFeatureDataProvider::get_value(const CatchmentAggrDataSelector& cached = value_cache.get(key).get(); } else { cached = std::make_shared>(catchment_dim_size * slice_size); - start.clear(); - start.push_back(cache_t_idx); // start from correct time index - start.push_back(0); // Start from the first catchment - count.clear(); - count.push_back(slice_size); // Read the calculated slice size for time - count.push_back(catchment_dim_size); // Read all catchments + start[dim_time] = cache_t_idx; // start from correct time index + start[dim_catchment] = 0; // Start from the first catchment + count[dim_time] = slice_size; // Read the calculated slice size for time + count[dim_catchment] = catchment_dim_size; // Read all catchments + // Whichever order the file stores the data in, the + // resulting array should have all catchments for a given + // time step contiguous + var_index_map[dim_time] = catchment_dim_size; + var_index_map[dim_catchment] = 1; + try { - ncvar.getVar(start,count,&(*cached)[0]); + ncvar.getVar(start,count, {1l, 1l}, var_index_map, cached->data()); value_cache.insert(key, cached); } catch (netCDF::exceptions::NcException& e) { netcdf_ss << "NetCDF exception: " << e.what() << std::endl; @@ -377,7 +448,7 @@ double NetCDFPerFeatureDataProvider::get_value(const CatchmentAggrDataSelector& } } } -// End modification + rvalue = 0.0; double a , b = 0.0; diff --git a/src/forcing/NullForcingProvider.cpp b/src/forcing/NullForcingProvider.cpp index 54b244a4ff..2a699e6ec1 100644 --- a/src/forcing/NullForcingProvider.cpp +++ b/src/forcing/NullForcingProvider.cpp @@ -1,5 +1,5 @@ #include "NullForcingProvider.hpp" -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include #include diff --git a/src/geojson/CMakeLists.txt b/src/geojson/CMakeLists.txt index c1f6ab8804..d9d26d586e 100644 --- a/src/geojson/CMakeLists.txt +++ b/src/geojson/CMakeLists.txt @@ -11,4 +11,8 @@ target_include_directories(geojson PUBLIC target_link_libraries(geojson PUBLIC Boost::boost # Headers-only Boost NGen::logging + ewts::ewts_ngen_bridge ) + + + diff --git a/src/geojson/JSONProperty.cpp b/src/geojson/JSONProperty.cpp index be8bba7f56..32044dd5f7 100644 --- a/src/geojson/JSONProperty.cpp +++ b/src/geojson/JSONProperty.cpp @@ -1,5 +1,5 @@ #include "JSONProperty.hpp" -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" using namespace geojson; diff --git a/src/geopackage/CMakeLists.txt b/src/geopackage/CMakeLists.txt index 0e544b800b..c438e03095 100644 --- a/src/geopackage/CMakeLists.txt +++ b/src/geopackage/CMakeLists.txt @@ -10,3 +10,5 @@ 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 NGen::logging) +target_link_libraries(geopackage PUBLIC ewts::ewts_ngen_bridge) + diff --git a/src/geopackage/feature.cpp b/src/geopackage/feature.cpp index 2d20014735..202f4a45c2 100644 --- a/src/geopackage/feature.cpp +++ b/src/geopackage/feature.cpp @@ -1,5 +1,5 @@ #include "geopackage.hpp" -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" // Points don't have a bounding box, so we can say its bbox is itself inline void build_point_bbox(const geojson::geometry& geom, std::vector& bbox) diff --git a/src/geopackage/geometry.cpp b/src/geopackage/geometry.cpp index 5f77072d4b..602ec25db8 100644 --- a/src/geopackage/geometry.cpp +++ b/src/geopackage/geometry.cpp @@ -2,7 +2,8 @@ #include "EndianCopy.hpp" #include "wkb.hpp" #include "proj.hpp" -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" geojson::geometry ngen::geopackage::build_geometry( const ngen::sqlite::database::iterator& row, @@ -12,7 +13,8 @@ geojson::geometry ngen::geopackage::build_geometry( { const std::vector geometry_blob = row.get>(geom_col); if (geometry_blob[0] != 'G' || geometry_blob[1] != 'P') { - Logger::logMsgAndThrowError("expected geopackage WKB, but found invalid format instead"); + LOG(LogLevel::FATAL, "expected geopackage WKB, but found invalid format instead"); + throw std::runtime_error("expected geopackage WKB, but found invalid format instead"); } int index = 3; // skip version diff --git a/src/geopackage/ngen_sqlite.cpp b/src/geopackage/ngen_sqlite.cpp index 0bf28bb608..81a136e479 100644 --- a/src/geopackage/ngen_sqlite.cpp +++ b/src/geopackage/ngen_sqlite.cpp @@ -2,7 +2,7 @@ #include #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" namespace ngen { namespace sqlite { diff --git a/src/geopackage/proj.cpp b/src/geopackage/proj.cpp index f1bd075485..70d7e2db22 100644 --- a/src/geopackage/proj.cpp +++ b/src/geopackage/proj.cpp @@ -1,5 +1,5 @@ #include "proj.hpp" -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" namespace ngen { namespace srs { diff --git a/src/geopackage/read.cpp b/src/geopackage/read.cpp index c4031a00a4..8f9efa8283 100644 --- a/src/geopackage/read.cpp +++ b/src/geopackage/read.cpp @@ -2,19 +2,24 @@ #include #include -#include +#include +#include "ewts_ngen/logger.hpp" std::stringstream read_ss(""); void check_table_name(const std::string& table) { if (boost::algorithm::starts_with(table, "sqlite_")) { - Logger::logMsgAndThrowError("table `" + table + "` is not queryable"); + std::string msg = "table `" + table + "` is not queryable"; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } std::regex allowed("[^-A-Za-z0-9_ ]+"); if (std::regex_match(table, allowed)) { - Logger::logMsgAndThrowError("table `" + table + "` contains invalid characters"); + std::string msg = "table `" + table + "` contains invalid characters"; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } } @@ -27,12 +32,6 @@ std::shared_ptr ngen::geopackage::read( // Check for malicious/invalid layer input check_table_name(layer); std::vector features; - if (ids.size() > 0) - features.reserve(ids.size()); - double min_x = std::numeric_limits::infinity(); - double min_y = std::numeric_limits::infinity(); - double max_x = -std::numeric_limits::infinity(); - double max_y = -std::numeric_limits::infinity(); LOG(LogLevel::DEBUG, "Establishing connection to geopackage %s.", gpkg_path.c_str()); ngen::sqlite::database db{gpkg_path}; @@ -53,107 +52,125 @@ std::shared_ptr ngen::geopackage::read( errmsg += ", "; errquery.next(); } - - Logger::logMsgAndThrowError(errmsg); + LOG(LogLevel::FATAL, errmsg); + throw std::runtime_error(errmsg); } - // Introspect if the layer is divides to see which ID field is in use - std::string id_column = "id"; - if(layer == "divides"){ - try { - //TODO: A bit primitive. Actually introspect the schema somehow? https://www.sqlite.org/c3ref/funclist.html - auto query_get_first_row = db.query("SELECT divide_id FROM " + layer + " LIMIT 1"); - id_column = "divide_id"; - } - catch (const std::exception& e){ - #ifndef NGEN_QUIET - // output debug info on what is read exactly - read_ss << "WARN: Using legacy ID column \"id\" in layer " << layer << " is DEPRECATED and may stop working at any time." << std::endl; - LOG(read_ss.str(), LogLevel::WARNING); read_ss.str(""); - #endif - } + std::string id_column; + std::string feature_query; + if (layer == "divides") { + id_column = "div_id"; + feature_query = + "SELECT " + "('cat-' || divides.div_id) AS id, " + "('nex-' || flowpaths.dn_nex_id) AS toid, " + "flowpaths.slope AS So, " + "divides.area_sqkm AS areasqkm, " // faster for later code to rename the field here + "divides.geom AS geom " + "FROM divides " + "LEFT JOIN flowpaths " + "ON divides.div_id = flowpaths.div_id"; + } else if (layer == "nexus") { + id_column = "nex_id"; + feature_query = + "SELECT " + "('nex-' || nexus.nex_id) AS id, " + "CASE " + "WHEN flowpaths.div_id IS NULL THEN 'terminal' " + "ELSE ('cat-' || flowpaths.div_id) " + "END AS toid, " + "CASE " + "WHEN flowpaths.slope IS NULL THEN 0.0 " + "ELSE flowpaths.slope " + "END AS So, " + "nexus.geom AS geom " + "FROM nexus " + "LEFT JOIN flowpaths " + "ON nexus.dn_fp_id = flowpaths.fp_id"; + } else { + Logger::LogAndThrow("Geopackage read only accepts layers `divides` and `nexus`. The layer entered was " + layer); } - // execute sub-queries if the number of IDs gets too long or once if ids.size() == 0 - int bind_limit = 900; - boost::span id_span(ids); - for (int i = 0; i < ids.size() || (i == 0 && ids.size() == 0); i += bind_limit) { - int span_size = (i + bind_limit >= ids.size()) ? (ids.size() - i) : bind_limit; - boost::span sub_ids = id_span.subspan(i, span_size); - - // Layer exists, getting statement for it - // - // this creates a string in the form: - // WHERE id IN (?, ?, ?, ...) - // so that it can be bound by SQLite. - // This is safer than trying to concatenate - // the IDs together. - std::string joined_ids = ""; - if (!sub_ids.empty()) { - joined_ids = " WHERE "+id_column+" IN (?"; - for (size_t i = 1; i < sub_ids.size(); i++) { - joined_ids += ", ?"; + std::string joined_ids = ""; + if (!ids.empty()) { + std::stringstream filter; + filter << " WHERE " << layer << '.' << id_column << " IN ("; + for (size_t i = 0; i < ids.size(); ++i) { + if (i != 0) + filter << ','; + auto &filter_id = ids[i]; + size_t sep_index = filter_id.find('-'); + if (sep_index == std::string::npos) { + sep_index = 0; + } else { + sep_index++; } - joined_ids += ")"; + int id_num = std::atoi(filter_id.c_str() + sep_index); + if (id_num <= 0) + Logger::LogAndThrow("Could not convert input " + layer + " ID into a number: " + filter_id); + filter << id_num; } + filter << ')'; + joined_ids = filter.str(); + } - // Get number of features - auto query_get_layer_count = db.query("SELECT COUNT(*) FROM " + layer + joined_ids, sub_ids); - query_get_layer_count.next(); - const int layer_feature_count = query_get_layer_count.get(0); - - #ifndef NGEN_QUIET - // output debug info on what is read exactly - read_ss << "Reading " << layer_feature_count << " features from layer " << layer << " using ID column `"<< id_column << "`"; - if (!sub_ids.empty()) { - read_ss << " (id subset:"; - for (auto& id : sub_ids) { - read_ss << " " << id; - } - read_ss << ")"; - } - read_ss << std::endl; - LOG(read_ss.str(), LogLevel::DEBUG); read_ss.str(""); - #endif - - // Get layer feature metadata (geometry column name + type) - auto query_get_layer_geom_meta = db.query("SELECT column_name FROM gpkg_geometry_columns WHERE table_name = ?", layer); - query_get_layer_geom_meta.next(); - const std::string layer_geometry_column = query_get_layer_geom_meta.get(0); - - // Get layer - LOG(LogLevel::DEBUG, "Reading %d features from layer %s.", layer_feature_count, layer.c_str()); - auto query_get_layer = db.query("SELECT * FROM " + layer + joined_ids, sub_ids); - query_get_layer.next(); - - // build features out of layer query - if (ids.size() == 0) - features.reserve(layer_feature_count); - while(!query_get_layer.done()) { - geojson::Feature feature = build_feature( - query_get_layer, - id_column, - layer_geometry_column - ); - - features.push_back(feature); - query_get_layer.next(); - } + // Get number of features + auto query_get_layer_count = db.query("SELECT COUNT(*) FROM " + layer + joined_ids); + query_get_layer_count.next(); + const int layer_feature_count = query_get_layer_count.get(0); + features.reserve(layer_feature_count); + if (!ids.empty() && ids.size() != layer_feature_count) { + LOG(LogLevel::WARNING, "The number of input IDs (%d) does not equal the number of features with those IDs in the geopackage (%d) for layer %s.", + ids.size(), layer_feature_count, layer.c_str()); + } - // get layer bounding box from features - // - // GeoPackage contains a bounding box in the SQLite DB, - // however, it is in the SRS of the GPKG. By creating - // the bbox after the features are built, the projection - // is already done. This also should be fairly cheap to do. - for (const auto& feature : features) { - const auto& bbox = feature->get_bounding_box(); - min_x = bbox[0] < min_x ? bbox[0] : min_x; - min_y = bbox[1] < min_y ? bbox[1] : min_y; - max_x = bbox[2] > max_x ? bbox[2] : max_x; - max_y = bbox[3] > max_y ? bbox[3] : max_y; + #ifndef NGEN_QUIET + // output debug info on what is read exactly + read_ss << "Reading " << layer_feature_count << " features from layer " << layer << " using ID column `"<< id_column << "`"; + if (!ids.empty()) { + read_ss << " (id subset:"; + for (auto& id : ids) { + read_ss << " " << id; } + read_ss << ")"; + } + read_ss << std::endl; + LOG(read_ss.str(), LogLevel::DEBUG); read_ss.str(""); + #endif + + // Get layer + LOG(LogLevel::DEBUG, "Reading %d features from layer %s.", layer_feature_count, layer.c_str()); + auto query_get_layer = db.query(feature_query + joined_ids); + query_get_layer.next(); + + // build features out of layer query + while(!query_get_layer.done()) { + geojson::Feature feature = build_feature( + query_get_layer, + "id", + "geom" + ); + + features.push_back(feature); + query_get_layer.next(); + } + // get layer bounding box from features + // + // GeoPackage contains a bounding box in the SQLite DB, + // however, it is in the SRS of the GPKG. By creating + // the bbox after the features are built, the projection + // is already done. This also should be fairly cheap to do. + double min_x = std::numeric_limits::infinity(); + double min_y = std::numeric_limits::infinity(); + double max_x = -std::numeric_limits::infinity(); + double max_y = -std::numeric_limits::infinity(); + for (const auto& feature : features) { + const auto& bbox = feature->get_bounding_box(); + min_x = bbox[0] < min_x ? bbox[0] : min_x; + min_y = bbox[1] < min_y ? bbox[1] : min_y; + max_x = bbox[2] > max_x ? bbox[2] : max_x; + max_y = bbox[3] > max_y ? bbox[3] : max_y; } auto fc = std::make_shared( diff --git a/src/geopackage/wkb.cpp b/src/geopackage/wkb.cpp index cdcb6f1130..1f2d2fd0bf 100644 --- a/src/geopackage/wkb.cpp +++ b/src/geopackage/wkb.cpp @@ -1,6 +1,7 @@ #include "wkb.hpp" #include "proj.hpp" -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" namespace ngen { namespace geopackage { @@ -19,12 +20,12 @@ enum wkb_geom_t { void throw_if_not_type(uint32_t given, wkb_geom_t expected) { if (given != expected) { - Logger::logMsgAndThrowError( - "expected WKB geometry type " + + std::string msg = "expected WKB geometry type " + std::to_string(expected) + ", but received " + - std::to_string(given) - ); + std::to_string(given); + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } } @@ -164,7 +165,8 @@ typename wkb::multipolygon_t wkb::read_multipolygon(const boost::span buffer) { if (buffer.size() < 5) { - Logger::logMsgAndThrowError("buffer reached end before encountering WKB"); + LOG(LogLevel::FATAL, "buffer reached end before encountering WKB"); + throw std::runtime_error("buffer reached end before encountering WKB"); } int index = 0; @@ -192,7 +194,7 @@ typename wkb::geometry wkb::read(const boost::span buffer) "this reader only implements OGC geometry types 1-6, " "but received type " + std::to_string(type) ); - LOG(throw_msg, LogLevel::WARNING); + LOG(LogLevel::FATAL, throw_msg); throw std::runtime_error(throw_msg); } diff --git a/src/partitionGenerator.cpp b/src/partitionGenerator.cpp index f8b48ee616..a4ae3940fb 100644 --- a/src/partitionGenerator.cpp +++ b/src/partitionGenerator.cpp @@ -20,7 +20,7 @@ #endif #include "core/Partition_Parser.hpp" -#include +#include "ewts_ngen/logger.hpp" std::stringstream partgen_ss(""); @@ -218,7 +218,7 @@ void generate_partitions(network::Network& network, const int& num_partitions, P * @param catchment_partitions The global set of partitions * @return int partition number containing the id * - * @throws runtime_error if no partition contains the requested id + * @throws std::runtime_error if no partition contains the requested id */ int find_remote_rank(const std::string& id, const PartitionVSet& catchment_partitions) { @@ -237,7 +237,8 @@ int find_remote_rank(const std::string& id, const PartitionVSet& catchment_parti } if(pos < 0){ std::string msg = "find_remote_rank: Could not find feature id "+id+" in any partition"; - Logger::logMsgAndThrowError(msg); + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } return pos; } @@ -430,18 +431,20 @@ int main(int argc, char* argv[]) geojson::GeoJSON catchment_collection; if (boost::algorithm::ends_with(catchmentDataFile, "gpkg")) { - #if NGEN_WITH_SQLITE3 +#if NGEN_WITH_SQLITE3 try { - catchment_collection = ngen::geopackage::read(catchmentDataFile, "divides", catchment_subset_ids); - } catch (...) { + catchment_collection = ngen::geopackage::read(catchmentDataFile, "divides", catchment_subset_ids); + } catch (std::exception &e) { // Handle all exceptions std::string msg = "Geopackage error occurred reading divides: " + catchmentDataFile; - LOG(msg,LogLevel::FATAL); - throw std::runtime_error(msg); + LOG(msg, LogLevel::FATAL); + LOG(LogLevel::FATAL, e.what()); + throw; } - #else - Logger::logMsgAndThrowError("SQLite3 support required to read GeoPackage files."); - #endif +#else + LOG(LogLevel::FATAL, "SQLite3 support required to read GeoPackage files"); + throw std::runtime_error("SQLite3 support required to read GeoPackage files"); +#endif } else { @@ -453,8 +456,10 @@ int main(int argc, char* argv[]) //Check that the number of partitions is less or equal to the number of catchment if (num_catchments < num_partitions) { - Logger::logMsgAndThrowError("Input error: total number of catchments: " + std::to_string(num_catchments) + \ - ", cannot be less than the number of partitions: " + std::to_string(num_partitions)); + std::string msg = "Input error: total number of catchments: " + std::to_string(num_catchments) + \ + ", cannot be less than the number of partitions: " + std::to_string(num_partitions); + LOG(msg,LogLevel::FATAL, msg); + throw std::runtime_error(msg); } std::string link_key = "toid"; @@ -467,18 +472,20 @@ int main(int argc, char* argv[]) geojson::GeoJSON global_nexus_collection; if (boost::algorithm::ends_with(nexusDataFile, "gpkg")) { - #if NGEN_WITH_SQLITE3 +#if NGEN_WITH_SQLITE3 try { global_nexus_collection = ngen::geopackage::read(nexusDataFile, "nexus", nexus_subset_ids); - } catch (...) { + } catch (std::exception &e) { // Handle all exceptions std::string msg = "Geopackage error occurred reading nexuses: " + nexusDataFile; LOG(msg,LogLevel::FATAL); - throw std::runtime_error(msg); + LOG(LogLevel::FATAL, e.what()); + throw; } - #else - Logger::logMsgAndThrowError("SQLite3 support required to read GeoPackage files."); - #endif +#else + LOG(LogLevel::FATAL, "SQLite3 support required to read GeoPackage files."); + throw std::runtime_error("SQLite3 support required to read GeoPackage files."); +#endif } else { diff --git a/src/realizations/catchment/Bmi_C_Formulation.cpp b/src/realizations/catchment/Bmi_C_Formulation.cpp index c7d4398941..cfc2eaa82c 100644 --- a/src/realizations/catchment/Bmi_C_Formulation.cpp +++ b/src/realizations/catchment/Bmi_C_Formulation.cpp @@ -1,7 +1,8 @@ #include "Bmi_C_Formulation.hpp" using namespace realization; using namespace models::bmi; -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" Bmi_C_Formulation::Bmi_C_Formulation(std::string id, std::shared_ptr forcing_provider, utils::StreamHandler output_stream) : Bmi_Module_Formulation(id, forcing_provider, output_stream) { } @@ -19,7 +20,9 @@ std::string Bmi_C_Formulation::get_formulation_type() const { std::shared_ptr Bmi_C_Formulation::construct_model(const geojson::PropertyMap& properties) { auto library_file_iter = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__LIB_FILE); if (library_file_iter == properties.end()) { - Logger::logMsgAndThrowError("BMI C formulation requires path to library file, but none provided in config"); + std::string msg = "BMI C formulation requires path to library file, but none provided in config"; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } std::string lib_file = library_file_iter->second.as_string(); auto reg_func_itr = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__REGISTRATION_FUNC); @@ -72,7 +75,11 @@ double Bmi_C_Formulation::get_var_value_as_double(const int& index, const std::s if (type == "unsigned long long" || type == "unsigned long long int") return (double) (model->GetValuePtr(var_name))[index]; - Logger::logMsgAndThrowError("Unable to get value of variable " + var_name + " from " + get_model_type_name() + " as double: no logic for converting variable type " + type); + std::string msg = "Unable to get value of variable " + var_name + " from " + + get_model_type_name() + " as double: no logic for converting variable type " + type; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); + return 1.0; } diff --git a/src/realizations/catchment/Bmi_Cpp_Formulation.cpp b/src/realizations/catchment/Bmi_Cpp_Formulation.cpp index 921675ffe7..dffb0e2649 100644 --- a/src/realizations/catchment/Bmi_Cpp_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Cpp_Formulation.cpp @@ -1,5 +1,6 @@ #include "Bmi_Cpp_Formulation.hpp" -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" using namespace realization; using namespace models::bmi; @@ -23,7 +24,9 @@ std::string Bmi_Cpp_Formulation::get_formulation_type() const { std::shared_ptr Bmi_Cpp_Formulation::construct_model(const geojson::PropertyMap& properties) { auto json_prop_itr = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__LIB_FILE); if (json_prop_itr == properties.end()) { - Logger::logMsgAndThrowError("BMI C++ formulation requires path to library file, but none provided in config"); + std::string msg = "BMI C++ formulation requires path to library file, but none provided in config"; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } std::string lib_file = json_prop_itr->second.as_string(); @@ -84,8 +87,11 @@ double Bmi_Cpp_Formulation::get_var_value_as_double(const int& index, const std: if (type == "unsigned long long" || type == "unsigned long long int") return (double) (model->GetValuePtr(var_name))[index]; - Logger::logMsgAndThrowError("Unable to get value of variable " + var_name + " from " + get_model_type_name() + - " as double: no logic for converting variable type " + type); + std::string msg = "Unable to get value of variable " + var_name + " from " + get_model_type_name() + + " as double: no logic for converting variable type " + type; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); + return 1.0; } diff --git a/src/realizations/catchment/Bmi_Fortran_Formulation.cpp b/src/realizations/catchment/Bmi_Fortran_Formulation.cpp index 390a6fb4e7..04b7e7e51a 100644 --- a/src/realizations/catchment/Bmi_Fortran_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Fortran_Formulation.cpp @@ -1,11 +1,13 @@ #include -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" #if NGEN_WITH_BMI_FORTRAN #include "Bmi_Fortran_Formulation.hpp" #include "Bmi_Fortran_Adapter.hpp" #include "Constants.h" +#include "state_save_restore/State_Save_Utils.hpp" using namespace realization; using namespace models::bmi; @@ -28,7 +30,9 @@ Bmi_Fortran_Formulation::Bmi_Fortran_Formulation(std::string id, std::shared_ptr std::shared_ptr Bmi_Fortran_Formulation::construct_model(const geojson::PropertyMap& properties) { auto library_file_iter = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__LIB_FILE); if (library_file_iter == properties.end()) { - Logger::logMsgAndThrowError("BMI C formulation requires path to library file, but none provided in config"); + std::string msg = "BMI C formulation requires path to library file, but none provided in config."; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } std::string lib_file = library_file_iter->second.as_string(); auto reg_func_itr = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__REGISTRATION_FUNC); @@ -53,7 +57,7 @@ double Bmi_Fortran_Formulation::get_var_value_as_double(const int &index, const // don't fit or might convert inappropriately std::string type = model->GetVarType(var_name); //Can cause a segfault here if GetValue returns an empty vector...a "fix" in bmi_utilities GetValue - //will throw a relevant runtime_error if the vector is empty, so this is safe to use this way for now... + //will throw a relevant std::runtime_error if the vector is empty, so this is safe to use this way for now... if (type == "long double") return (double) (models::bmi::GetValue(*model, var_name))[index]; @@ -87,10 +91,54 @@ double Bmi_Fortran_Formulation::get_var_value_as_double(const int &index, const if (type == "unsigned long long" || type == "unsigned long long int") return (double) (models::bmi::GetValue(*model, var_name))[index]; - Logger::logMsgAndThrowError("Unable to get value of variable " + var_name + " from " + get_model_type_name() + - " as double: no logic for converting variable type " + type); + std::string msg = "Unable to get value of variable " + var_name + " from " + get_model_type_name() + + " as double: no logic for converting variable type " + type; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); return 1.0; } +const boost::span Bmi_Fortran_Formulation::get_serialization_state() { + auto model = this->get_bmi_model(); + // create the serialized state on the Fortran BMI + int size_int = 0; + model->SetValue(StateSaveNames::CREATE, &size_int); + model->GetValue(StateSaveNames::SIZE, &size_int); + // resize the state to the array to the size of the Fortran's backing array + this->serialized_state.resize(size_int); + // since GetValuePtr on the Fortran BMI does not work currently, store the data on the formulation + model->GetValue(StateSaveNames::STATE, this->serialized_state.data()); + // the BMI can have its state freed immediately since the data is now stored on the formulation + model->SetValue(StateSaveNames::FREE, &size_int); + // return a span of the data stored on the formulation + const boost::span span(this->serialized_state.data(), this->serialized_state.size()); + return span; +} + +void Bmi_Fortran_Formulation::load_serialization_state(const boost::span state) { + auto model = this->get_bmi_model(); + int item_size = model->GetVarItemsize(StateSaveNames::STATE); + // assert the number of chars aligns with the storage array to prevent reading out of bounds + if (state.size() % item_size != 0) { + std::string error = "Fortran Deserialization: The number of bytes in the state (" + std::to_string(state.size()) + + ") must be a multiple of the size of the storage unit (" + std::to_string(item_size) + ")"; + LOG(LogLevel::SEVERE, error); + throw std::runtime_error(error); + } + // setting size is a workaround for loading the state. + // The BMI Fortran interface shapes the incoming pointer to the same size as the data currently backing the BMI's variable. + // By setting the size, the BMI can lie about the size of its state variable to that interface. + int false_nbytes = state.size(); + model->SetValue(StateSaveNames::SIZE, &false_nbytes); + model->SetValue(StateSaveNames::STATE, state.data()); +} + +void Bmi_Fortran_Formulation::free_serialization_state() { + // The serialized data needs to be stored on the formluation since GetValuePtr is not available on Fortran BMIs. + // The backing BMI's serialization data should already be freed during `get_serialization_state`, so clearing the formulation's data is all that is needed. + this->serialized_state.clear(); + this->serialized_state.shrink_to_fit(); +} + #endif // NGEN_WITH_BMI_FORTRAN diff --git a/src/realizations/catchment/Bmi_Module_Formulation.cpp b/src/realizations/catchment/Bmi_Module_Formulation.cpp index da478a294e..359a257f88 100644 --- a/src/realizations/catchment/Bmi_Module_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Module_Formulation.cpp @@ -1,7 +1,10 @@ #include "Bmi_Module_Formulation.hpp" #include "utilities/logging_utils.h" #include -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" +#include "state_save_restore/State_Save_Utils.hpp" +#include std::stringstream bmiform_ss; @@ -15,19 +18,29 @@ namespace realization { inner_create_formulation(properties, true); } - void Bmi_Module_Formulation::save_state(std::shared_ptr saver) const { - auto model = get_bmi_model(); + void Bmi_Module_Formulation::save_state(std::shared_ptr saver) { + uint64_t size = 1; + boost::span data = this->get_serialization_state(); - size_t size = 1; - model->SetValue("serialization_create", &size); - model->GetValue("serialization_size", &size); + // Rely on Formulation_Manager also using this->get_id() + // as a unique key for the individual catchment + // formulations + saver->save_unit(this->get_id(), data); - auto serialization_state = static_cast(model->GetValuePtr("serialization_state")); - boost::span data(serialization_state, size); + this->free_serialization_state(); + } - saver->save(data); + void Bmi_Module_Formulation::load_state(std::shared_ptr loader) { + std::vector buffer; + loader->load_unit(this->get_id(), buffer); + boost::span data(buffer.data(), buffer.size()); + this->load_serialization_state(data); + } - model->SetValue("serialization_free", &size); + void Bmi_Module_Formulation::load_hot_start(std::shared_ptr loader) { + this->load_state(loader); + double rt; + this->get_bmi_model()->SetValue(StateSaveNames::FREE, &rt); } boost::span Bmi_Module_Formulation::get_available_variable_names() const { @@ -235,7 +248,7 @@ namespace realization { // consistent with times /* if (last_model_response_delta == 0 && last_model_response_start_time == 0) { - throw runtime_error(get_formulation_type() + " does not properly set output time validity ranges " + throw std::runtime_error(get_formulation_type() + " does not properly set output time validity ranges " "needed to provide outputs as forcings"); } */ @@ -296,7 +309,7 @@ namespace realization { // consistent with times /* if (last_model_response_delta == 0 && last_model_response_start_time == 0) { - throw runtime_error(get_formulation_type() + " does not properly set output time validity ranges " + throw std::runtime_error(get_formulation_type() + " does not properly set output time validity ranges " "needed to provide outputs as forcings"); } */ @@ -1082,28 +1095,28 @@ namespace realization { } - const boost::span Bmi_Module_Formulation::get_serialization_state() const { - auto bmi = this->bmi_model; - // create a new serialized state, getting the amount of data that was saved - uint64_t* size = (uint64_t*)bmi->GetValuePtr("serialization_create"); - // get the pointer of the new state - char* serialized = (char*)bmi->GetValuePtr("serialization_state"); - const boost::span span(serialized, *size); + const boost::span Bmi_Module_Formulation::get_serialization_state() { + auto model = get_bmi_model(); + uint64_t size = 0; + model->SetValue(StateSaveNames::CREATE, &size); + model->GetValue(StateSaveNames::SIZE, &size); + auto serialization_state = static_cast(model->GetValuePtr(StateSaveNames::STATE)); + const boost::span span(serialization_state, size); return span; } - void Bmi_Module_Formulation::load_serialization_state(const boost::span state) const { + void Bmi_Module_Formulation::load_serialization_state(const boost::span state) { auto bmi = this->bmi_model; // grab the pointer to the underlying state data void* data = (void*)state.data(); // load the state through SetValue - bmi->SetValue("serialization_state", data); + bmi->SetValue(StateSaveNames::STATE, data); } - void Bmi_Module_Formulation::free_serialization_state() const { + void Bmi_Module_Formulation::free_serialization_state() { auto bmi = this->bmi_model; // send message to clear memory associated with serialized data void* _; // this pointer will be unused by SetValue - bmi->SetValue("serialization_free", _); + bmi->SetValue(StateSaveNames::FREE, _); } } diff --git a/src/realizations/catchment/Bmi_Multi_Formulation.cpp b/src/realizations/catchment/Bmi_Multi_Formulation.cpp index 145b7dfb73..ed5b296193 100644 --- a/src/realizations/catchment/Bmi_Multi_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Multi_Formulation.cpp @@ -11,10 +11,56 @@ #include "Bmi_C_Formulation.hpp" #include "Bmi_Fortran_Formulation.hpp" #include "Bmi_Py_Formulation.hpp" -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" +#include + +#include "state_save_restore/vecbuf.hpp" +#include "state_save_restore/State_Save_Utils.hpp" +#include + +#include +#include +#include using namespace realization; + +void Bmi_Multi_Formulation::save_state(std::shared_ptr saver) { + LOG(LogLevel::DEBUG, "Saving state for Multi-BMI %s", this->get_id()); + vecbuf data; + boost::archive::binary_oarchive archive(data); + // serialization function handles freeing the sub-BMI states after archiving them + archive << (*this); + // it's recommended to keep data pointers around until serialization completes, + // so freeing the BMI states is done after the data buffer has been completely written to + for (const nested_module_ptr &m : modules) { + auto bmi = dynamic_cast(m.get()); + bmi->free_serialization_state(); + } + boost::span span(data.data(), data.size()); + saver->save_unit(this->get_id(), span); +} + +void Bmi_Multi_Formulation::load_state(std::shared_ptr loader) { + LOG(LogLevel::DEBUG, "Loading save state for Multi-BMI %s", this->get_id()); + std::vector data; + loader->load_unit(this->get_id(), data); + membuf stream(data.data(), data.size()); + boost::archive::binary_iarchive archive(stream); + archive >> (*this); +} + +void Bmi_Multi_Formulation::load_hot_start(std::shared_ptr loader) { + this->load_state(loader); + double rt; + LOG(LogLevel::DEBUG, "Resetting time for sub-BMIs"); + // Multi-BMI's current forwards its primary BMI's current time, so no additional action needed for the formulation's reset time + for (const nested_module_ptr &m : modules) { + auto bmi = dynamic_cast(m.get()); + bmi->get_bmi_model()->SetValue(StateSaveNames::RESET, &rt); + } +} + void Bmi_Multi_Formulation::create_multi_formulation(geojson::PropertyMap properties, bool needs_param_validation) { if (needs_param_validation) { validate_parameters(properties); @@ -83,12 +129,15 @@ void Bmi_Multi_Formulation::create_multi_formulation(geojson::PropertyMap proper #endif // NGEN_WITH_PYTHON } if (inactive_type_requested) { - Logger::logMsgAndThrowError( - get_formulation_type() + " could not initialize sub formulation of type " + type_name + - " due to support for this type not being activated."); + std::string msg = get_formulation_type() + " could not initialize sub formulation of type " + type_name + + " due to support for this type not being activated."; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } if (module == nullptr) { - Logger::logMsgAndThrowError(get_formulation_type() + " received unexpected subtype formulation " + type_name); + std::string msg = get_formulation_type() + " received unexpected subtype formulation " + type_name; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } modules[i] = module; @@ -482,7 +531,9 @@ std::string Bmi_Multi_Formulation::get_output_line_for_timestep(int timestep, st void Bmi_Multi_Formulation::update(time_step_t t_index, time_step_t t_delta) { if (modules.empty()) { - Logger::logMsgAndThrowError("Trying to get response of improperly created empty BMI multi-module formulation."); + std::string msg = "Trying to get response of improperly created empty BMI multi-module formulation."; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } if (t_index < 0) { throw std::invalid_argument( @@ -609,6 +660,34 @@ void Bmi_Multi_Formulation::set_realization_file_format(bool is_legacy_format){ legacy_json_format = is_legacy_format; } +template +void Bmi_Multi_Formulation::serialize(Archive &ar, const unsigned int version) { + uint64_t data_size; + std::vector buffer; + for (const nested_module_ptr &m : modules) { + auto bmi = dynamic_cast(m.get()); + // if saving, make the BMI's state and record its size and data + if (Archive::is_saving::value) { + LOG(LogLevel::DEBUG, "Saving state from sub-BMI " + bmi->get_model_type_name()); + boost::span span = bmi->get_serialization_state(); + data_size = span.size(); + ar & data_size; + ar & boost::serialization::make_array(span.data(), data_size); + // it's recommended to keep raw pointers alive throughout the entire seiralization process, + // so responsibility for freeing the BMIs' state is left to the caller of this function + } + // if loading, get the current data size stored at the front, then load that much data as a char blob passed to the BMI + else { + LOG(LogLevel::DEBUG, "Loading state from sub-BMI " + bmi->get_model_type_name()); + ar & data_size; + buffer.resize(data_size); + ar & boost::serialization::make_array(buffer.data(), data_size); + boost::span span(buffer.data(), data_size); + bmi->load_serialization_state(span); + } + } +} + //Function to find whether any item in the string vector is empty or blank int find_empty_string_index(const std::vector& str_vector) { for (int i = 0; i < str_vector.size(); ++i) { diff --git a/src/realizations/catchment/Bmi_Py_Formulation.cpp b/src/realizations/catchment/Bmi_Py_Formulation.cpp index 7d266db9f8..c6f14b3ff9 100644 --- a/src/realizations/catchment/Bmi_Py_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Py_Formulation.cpp @@ -1,5 +1,7 @@ #include -#include "Logger.hpp" +#include +#include "ewts_ngen/logger.hpp" +#include "state_save_restore/State_Save_Utils.hpp" #if NGEN_WITH_PYTHON @@ -16,7 +18,9 @@ Bmi_Py_Formulation::Bmi_Py_Formulation(std::string id, std::shared_ptr Bmi_Py_Formulation::construct_model(const geojson::PropertyMap &properties) { auto python_type_name_iter = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__PYTHON_TYPE_NAME); if (python_type_name_iter == properties.end()) { - Logger::logMsgAndThrowError("BMI Python formulation requires Python model class type, but none given in config"); + std::string msg = "BMI Python formulation requires Python model class type, but none given in config"; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); } //Load a custom module path, if provided auto python_module_path_iter = properties.find(BMI_REALIZATION_CFG_PARAM_OPT__PYTHON_MODULE_PATH); @@ -53,52 +57,34 @@ double Bmi_Py_Formulation::get_var_value_as_double(const int &index, const std:: std::string val_type = model->GetVarType(var_name); size_t val_item_size = (size_t)model->GetVarItemsize(var_name); + std::string cxx_type = model->get_analogous_cxx_type(val_type, val_item_size); //void *dest; int indices[1]; indices[0] = index; - - // The available types and how they are handled here should match what is in SetValueAtIndices - if (val_type == "int" && val_item_size == sizeof(short)) { - short dest; - model->get_value_at_indices(var_name, &dest, indices, 1, false); - return (double)dest; - } - if (val_type == "int" && val_item_size == sizeof(int)) { - int dest; - model->get_value_at_indices(var_name, &dest, indices, 1, false); - return (double)dest; - } - if (val_type == "int" && val_item_size == sizeof(long)) { - long dest; - model->get_value_at_indices(var_name, &dest, indices, 1, false); - return (double)dest; - } - if (val_type == "int" && val_item_size == sizeof(long long)) { - long long dest; - model->get_value_at_indices(var_name, &dest, indices, 1, false); - return (double)dest; - } - if (val_type == "float" || val_type == "float16" || val_type == "float32" || val_type == "float64") { - if (val_item_size == sizeof(float)) { - float dest; - model->get_value_at_indices(var_name, &dest, indices, 1, false); - return (double) dest; - } - if (val_item_size == sizeof(double)) { - double dest; - model->get_value_at_indices(var_name, &dest, indices, 1, false); - return dest; - } - if (val_item_size == sizeof(long double)) { - long double dest; - model->get_value_at_indices(var_name, &dest, indices, 1, false); - return (double) dest; - } - } - - Logger::logMsgAndThrowError("Unable to get value of variable " + var_name + " from " + get_model_type_name() + - " as double: no logic for converting variable type " + val_type); + // macro for both checking and converting based on type from get_analogous_cxx_type +#define PY_BMI_DOUBLE_AT_INDEX(type) if (cxx_type == #type) {\ + type dest;\ + model->get_value_at_indices(var_name, &dest, indices, 1, false);\ + return static_cast(dest);} + PY_BMI_DOUBLE_AT_INDEX(signed char) + else PY_BMI_DOUBLE_AT_INDEX(unsigned char) + else PY_BMI_DOUBLE_AT_INDEX(short) + else PY_BMI_DOUBLE_AT_INDEX(unsigned short) + else PY_BMI_DOUBLE_AT_INDEX(int) + else PY_BMI_DOUBLE_AT_INDEX(unsigned int) + else PY_BMI_DOUBLE_AT_INDEX(long) + else PY_BMI_DOUBLE_AT_INDEX(unsigned long) + else PY_BMI_DOUBLE_AT_INDEX(long long) + else PY_BMI_DOUBLE_AT_INDEX(unsigned long long) + else PY_BMI_DOUBLE_AT_INDEX(float) + else PY_BMI_DOUBLE_AT_INDEX(double) + else PY_BMI_DOUBLE_AT_INDEX(long double) +#undef PY_BMI_DOUBLE_AT_INDEX + std::string msg = "Unable to get value of variable " + var_name + " from " + get_model_type_name() + + " as double: no logic for converting variable type " + val_type; + LOG(LogLevel::FATAL, msg); + throw std::runtime_error(msg); return 1.0; } @@ -117,4 +103,10 @@ bool Bmi_Py_Formulation::is_model_initialized() const { return get_bmi_model()->is_model_initialized(); } +void Bmi_Py_Formulation::load_serialization_state(const boost::span state) { + auto bmi = std::dynamic_pointer_cast(get_bmi_model()); + // load the state through the set value function that does not enforce the input size is the same as the current BMI's size + bmi->set_value_unchecked(StateSaveNames::STATE, state.data(), state.size()); +} + #endif //NGEN_WITH_PYTHON diff --git a/src/realizations/catchment/CMakeLists.txt b/src/realizations/catchment/CMakeLists.txt index 48771b18bf..ffe82683b2 100644 --- a/src/realizations/catchment/CMakeLists.txt +++ b/src/realizations/catchment/CMakeLists.txt @@ -3,6 +3,15 @@ dynamic_sourced_cxx_library(realizations_catchment "${CMAKE_CURRENT_SOURCE_DIR}" add_library(NGen::realizations_catchment ALIAS realizations_catchment) +# ----------------------------------------------------------------------------- +# Find the Boost library and configure usage +set(Boost_USE_STATIC_LIBS OFF) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) +find_package(Boost 1.79.0 REQUIRED COMPONENTS serialization) + +target_link_libraries(realizations_catchment PRIVATE Boost::serialization) + target_include_directories(realizations_catchment PUBLIC ${PROJECT_SOURCE_DIR}/include/core ${PROJECT_SOURCE_DIR}/include/core/catchment @@ -13,7 +22,7 @@ target_include_directories(realizations_catchment PUBLIC ${PROJECT_SOURCE_DIR}/include/geojson ${PROJECT_SOURCE_DIR}/include/bmi ) - + target_link_libraries(realizations_catchment PUBLIC ${CMAKE_DL_LIBS} NGen::config_header @@ -24,3 +33,6 @@ target_link_libraries(realizations_catchment PUBLIC NGen::bmi_protocols ) +target_link_libraries(realizations_catchment PUBLIC ewts::ewts_ngen_bridge) + + diff --git a/src/realizations/catchment/Catchment_Formulation.cpp b/src/realizations/catchment/Catchment_Formulation.cpp index 00b22e0cf0..71c2972d0d 100644 --- a/src/realizations/catchment/Catchment_Formulation.cpp +++ b/src/realizations/catchment/Catchment_Formulation.cpp @@ -59,6 +59,17 @@ namespace realization { // LOG(ss.str(), LogLevel::DEBUG); } + std::string Catchment_Formulation::config_pattern_id_replacement(const std::string &id) { + size_t index = id.find_last_of('-'); + if (index != std::string::npos && ++index < id.length()) { + // check if first character after the last hyphen is a number + if (static_cast(id[index]) - static_cast('0') <= 9) { + return id.substr(index); + } + } + return id; + } + std::string Catchment_Formulation::get_output_header_line(std::string delimiter) const { return "Total Discharge"; } diff --git a/src/state_save_restore/CMakeLists.txt b/src/state_save_restore/CMakeLists.txt new file mode 100644 index 0000000000..b068d6d4ab --- /dev/null +++ b/src/state_save_restore/CMakeLists.txt @@ -0,0 +1,17 @@ +include(${PROJECT_SOURCE_DIR}/cmake/dynamic_sourced_library.cmake) +dynamic_sourced_cxx_library(state_save_restore "${CMAKE_CURRENT_SOURCE_DIR}") + +add_library(NGen::state_save_restore ALIAS state_save_restore) +target_link_libraries(state_save_restore PUBLIC + NGen::config_header + Boost::boost # Headers-only Boost + Boost::system + Boost::filesystem + ewts::ewts_ngen_bridge + ) + +target_include_directories(state_save_restore PUBLIC + ${PROJECT_SOURCE_DIR}/include + ) + + diff --git a/src/state_save_restore/File_Per_Unit.cpp b/src/state_save_restore/File_Per_Unit.cpp new file mode 100644 index 0000000000..6d355d4c93 --- /dev/null +++ b/src/state_save_restore/File_Per_Unit.cpp @@ -0,0 +1,196 @@ +#include +#include "ewts_ngen/logger.hpp" + +#if __has_include() && __cpp_lib_filesystem >= 201703L + #include + using namespace std::filesystem; + #warning "Using STD Filesystem" +#elif __has_include() && defined(__cpp_lib_filesystem) + #include + using namespace std::experimental::filesystem; + #warning "Using Filesystem TS" +#elif __has_include() + #include + using namespace boost::filesystem; + #warning "Using Boost.Filesystem" +#else + #error "No Filesystem library implementation available" +#endif + +#include +#include + +namespace unit_saving_utils { + std::string format_epoch(State_Saver::snapshot_time_t epoch) + { + time_t t = std::chrono::system_clock::to_time_t(epoch); + std::tm tm = *std::gmtime(&t); + + std::stringstream tss; + tss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S"); + return tss.str(); + } +} + +// This class is only declared and defined here, in the .cpp file, +// because it is strictly an implementation detail of the top-level +// File_Per_Unit_Saver class +class File_Per_Unit_Snapshot_Saver : public State_Snapshot_Saver +{ + friend class File_Per_Unit_Saver; + + public: + File_Per_Unit_Snapshot_Saver() = delete; + File_Per_Unit_Snapshot_Saver(path base_path, State_Saver::State_Durability durability); + ~File_Per_Unit_Snapshot_Saver(); + +public: + void save_unit(std::string const& unit_name, boost::span data) override; + void finish_saving() override; + +private: + path dir_path_; +}; + +File_Per_Unit_Saver::File_Per_Unit_Saver(std::string base_path) + : base_path_(std::move(base_path)) +{ + auto dir_path = path(base_path_); + create_directories(dir_path); +} + +File_Per_Unit_Saver::~File_Per_Unit_Saver() = default; + +std::shared_ptr File_Per_Unit_Saver::initialize_snapshot(State_Durability durability) { + // TODO + return std::make_shared(path(this->base_path_), durability); +} + +std::shared_ptr File_Per_Unit_Saver::initialize_checkpoint_snapshot(snapshot_time_t epoch, State_Durability durability) +{ + path checkpoint_path = path(this->base_path_) / unit_saving_utils::format_epoch(epoch); + create_directory(checkpoint_path); + return std::make_shared(checkpoint_path, durability); +} + +void File_Per_Unit_Saver::finalize() +{ + // nothing to be done +} + +File_Per_Unit_Snapshot_Saver::File_Per_Unit_Snapshot_Saver(path base_path, State_Saver::State_Durability durability) + : State_Snapshot_Saver(durability) + , dir_path_(base_path) +{ + create_directory(dir_path_); +} + +File_Per_Unit_Snapshot_Saver::~File_Per_Unit_Snapshot_Saver() = default; + +void File_Per_Unit_Snapshot_Saver::save_unit(std::string const& unit_name, boost::span data) +{ + auto file_path = dir_path_ / unit_name; + try { + std::ofstream stream(file_path.string(), std::ios_base::out | std::ios_base::binary); + stream.write(data.data(), data.size()); + stream.close(); + } catch (std::exception &e) { + LOG("Failed to write state save data for unit '" + unit_name + "' in file '" + file_path.string() + "'", LogLevel::WARNING); + throw; + } +} + +void File_Per_Unit_Snapshot_Saver::finish_saving() +{ + if (durability_ == State_Saver::State_Durability::strict) { + // fsync() or whatever + } +} + + +// This class is only declared and defined here, in the .cpp file, +// because it is strictly an implementation detail of the top-level +// File_Per_Unit_Saver class +class File_Per_Unit_Snapshot_Loader : public State_Snapshot_Loader +{ + friend class State_Snapshot_Loader; +public: + File_Per_Unit_Snapshot_Loader() = default; + File_Per_Unit_Snapshot_Loader(path dir_path); + ~File_Per_Unit_Snapshot_Loader() override = default; + + bool has_unit(const std::string &unit_name) override; + + /** + * Load data from whatever source and store it in the `data` vector. + * + * @param data The location where the loaded data will be stored. This will be resized to the amount of data loaded. + */ + void load_unit(const std::string &unit_name, std::vector &data) override; + + /** + * Execute logic to complete the saving process + * + * Data may be flushed here, and delayed errors may be detected + * and reported here. With relaxed durability, error reports may + * not come until the parent State_Saver::finalize() call is made, + * or ever. + */ + void finish_saving() override { }; + +private: + path dir_path_; + std::vector data_; +}; + +File_Per_Unit_Snapshot_Loader::File_Per_Unit_Snapshot_Loader(path dir_path) + : dir_path_(dir_path) +{ + +} + +bool File_Per_Unit_Snapshot_Loader::has_unit(const std::string &unit_name) { + auto file_path = dir_path_ / unit_name; + return exists(file_path.string()); +} + +void File_Per_Unit_Snapshot_Loader::load_unit(std::string const& unit_name, std::vector &data) { + auto file_path = dir_path_ / unit_name; + std::uintmax_t size; + try { + size = file_size(file_path.string()); + } catch (std::exception &e) { + LOG("Failed to read state save data size for unit '" + unit_name + "' in file '" + file_path.string() + "'", LogLevel::WARNING); + throw; + } + std::ifstream stream(file_path.string(), std::ios_base::binary); + if (!stream) { + LOG("Failed to open state save data for unit '" + unit_name + "' in file '" + file_path.string() + "'", LogLevel::WARNING); + throw; + } + try { + data.resize(size); + stream.read(data.data(), size); + } catch (std::exception &e) { + LOG("Failed to read state save data for unit '" + unit_name + "' in file '" + file_path.string() + "'", LogLevel::WARNING); + throw; + } +} + +File_Per_Unit_Loader::File_Per_Unit_Loader(std::string dir_path) + : dir_path_(dir_path) +{ + +} + +std::shared_ptr File_Per_Unit_Loader::initialize_snapshot() +{ + return std::make_shared(path(dir_path_)); +} + +std::shared_ptr File_Per_Unit_Loader::initialize_checkpoint_snapshot(State_Saver::snapshot_time_t epoch) +{ + path checkpoint_path = path(dir_path_) / unit_saving_utils::format_epoch(epoch);; + return std::make_shared(checkpoint_path); +} + diff --git a/src/state_save_restore/State_Save_Restore.cpp b/src/state_save_restore/State_Save_Restore.cpp new file mode 100644 index 0000000000..4a54b1a21c --- /dev/null +++ b/src/state_save_restore/State_Save_Restore.cpp @@ -0,0 +1,154 @@ +#include +#include + +#include "ewts_ngen/logger.hpp" + +#include +#include + +#include + +State_Save_Config::State_Save_Config(boost::property_tree::ptree const& tree) +{ + auto maybe = tree.get_child_optional("state_saving"); + + // Default initialization will represent the "not enabled" case + if (!maybe) { + LOG("State saving not configured", LogLevel::INFO); + return; + } + + bool hot_start = false; + for (const auto& saving_config : *maybe) { + try { + auto& subtree = saving_config.second; + auto direction = subtree.get("direction"); + auto what = subtree.get("label"); + auto where = subtree.get("path"); + auto how = subtree.get("type"); + auto when = subtree.get("when"); + + instance i{direction, what, where, how, when}; + if (i.timing_ == State_Save_When::StartOfRun && i.direction_ == State_Save_Direction::Load) { + if (hot_start) + throw std::runtime_error("Only one hot start state saving configuration is allowed."); + hot_start = true; + } + instances_.push_back(i); + } catch (std::exception &e) { + LOG("Bad state saving config: " + std::string(e.what()), LogLevel::WARNING); + throw; + } + } + + LOG("State saving configured", LogLevel::INFO); +} + +std::vector>> State_Save_Config::start_of_run_loaders() const { + std::vector>> loaders; + for (const auto &i : this->instances_) { + if (i.timing_ == State_Save_When::StartOfRun && i.direction_ == State_Save_Direction::Load) { + if (i.mechanism_ == State_Save_Mechanism::FilePerUnit) { + auto loader = std::make_shared(i.path_); + auto pair = std::make_pair(i.label_, loader); + loaders.push_back(pair); + } else { + LOG(LogLevel::WARNING, "State_Save_Config: Loading mechanism " + i.mechanism_string() + " is not supported for start of run loading."); + } + } + } + return loaders; +} + +std::vector>> State_Save_Config::end_of_run_savers() const { + std::vector>> savers; + for (const auto &i : this->instances_) { + if (i.timing_ == State_Save_When::EndOfRun && i.direction_ == State_Save_Direction::Save) { + if (i.mechanism_ == State_Save_Mechanism::FilePerUnit) { + auto saver = std::make_shared(i.path_); + auto pair = std::make_pair(i.label_, saver); + savers.push_back(pair); + } else { + LOG(LogLevel::WARNING, "State_Save_Config: Saving mechanism " + i.mechanism_string() + " is not supported for start of run saving."); + } + } + } + return savers; +} + +std::unique_ptr State_Save_Config::hot_start() const { + for (const auto &i : this->instances_) { + if (i.direction_ == State_Save_Direction::Load && i.timing_ == State_Save_When::StartOfRun) { + if (i.mechanism_ == State_Save_Mechanism::FilePerUnit) { + return std::make_unique(i.path_); + } else { + LOG(LogLevel::WARNING, "State_Save_Config: Saving mechanism " + i.mechanism_string() + " is not supported for start of run saving."); + } + } + } + return std::unique_ptr(); +} + +State_Save_Config::instance::instance(std::string const& direction, std::string const& label, std::string const& path, std::string const& mechanism, std::string const& timing) + : label_(label) + , path_(path) +{ + if (direction == "save") { + direction_ = State_Save_Direction::Save; + } else if (direction == "load") { + direction_ = State_Save_Direction::Load; + } else { + std::string message = "Unrecognized state saving direction '" + direction + "'"; + std::string throw_msg; throw_msg.assign(message); + LOG(throw_msg, LogLevel::WARNING); + throw std::runtime_error(throw_msg); + } + + if (mechanism == "FilePerUnit") { + mechanism_ = State_Save_Mechanism::FilePerUnit; + } else { + std::string message = "Unrecognized state saving mechanism '" + mechanism + "'"; + std::string throw_msg; throw_msg.assign(message); + LOG(throw_msg, LogLevel::WARNING); + throw std::runtime_error(throw_msg); + } + + if (timing == "EndOfRun") { + timing_ = State_Save_When::EndOfRun; + } else if (timing == "FirstOfMonth") { + timing_ = State_Save_When::FirstOfMonth; + } else if (timing == "StartOfRun") { + timing_ = State_Save_When::StartOfRun; + } else { + std::string message = "Unrecognized state saving timing '" + timing + "'"; + std::string throw_msg; throw_msg.assign(message); + LOG(throw_msg, LogLevel::WARNING); + throw std::runtime_error(throw_msg); + } +} + +std::string State_Save_Config::instance::instance::mechanism_string() const { + switch (mechanism_) { + case State_Save_Mechanism::None: + return "None"; + case State_Save_Mechanism::FilePerUnit: + return "FilePerUnit"; + default: + return "Other"; + } +} + +State_Snapshot_Saver::State_Snapshot_Saver(State_Saver::State_Durability durability) + : durability_(durability) +{ + +} + +State_Saver::snapshot_time_t State_Saver::snapshot_time_now() { +#if __cplusplus < 201703L // C++ < 17 + auto now = std::chrono::system_clock::now(); + return std::chrono::time_point_cast(now); +#else + return std::chrono::floor(std::chrono::system_clock::now()); +#endif +} diff --git a/src/utilities/CMakeLists.txt b/src/utilities/CMakeLists.txt index 1b0788f3b6..c9b340fa46 100644 --- a/src/utilities/CMakeLists.txt +++ b/src/utilities/CMakeLists.txt @@ -12,6 +12,8 @@ target_link_libraries(ngen_parallel NGen::logging ) +target_link_libraries(ngen_parallel PUBLIC ewts::ewts_ngen_bridge) + if(NGEN_WITH_MPI) target_link_libraries(ngen_parallel PUBLIC diff --git a/src/utilities/bmi/CMakeLists.txt b/src/utilities/bmi/CMakeLists.txt index 5d86e87f70..a98ad04af5 100644 --- a/src/utilities/bmi/CMakeLists.txt +++ b/src/utilities/bmi/CMakeLists.txt @@ -17,7 +17,7 @@ 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 +target_include_directories(ngen_bmi_protocols PUBLIC ${PROJECT_SOURCE_DIR}/include/bmi ${PROJECT_SOURCE_DIR}/include/utilities/bmi ${PROJECT_SOURCE_DIR}/include/geojson @@ -31,6 +31,8 @@ target_link_libraries(ngen_bmi_protocols NGen::logging ) +target_link_libraries(ngen_bmi_protocols PUBLIC ewts::ewts_ngen_bridge) + target_sources(ngen_bmi_protocols PRIVATE "${PROJECT_SOURCE_DIR}/src/bmi/Bmi_Adapter.cpp" diff --git a/src/utilities/bmi/mass_balance.cpp b/src/utilities/bmi/mass_balance.cpp index f542a682a0..05cfb04810 100644 --- a/src/utilities/bmi/mass_balance.cpp +++ b/src/utilities/bmi/mass_balance.cpp @@ -29,7 +29,7 @@ namespace models { namespace bmi { namespace protocols { NgenMassBalance::NgenMassBalance(const ModelPtr& model, const Properties& properties) : check(false), is_fatal(false), tolerance(1.0E-16), frequency(1){ - initialize(model, properties); + auto init = initialize(model, properties); } NgenMassBalance::NgenMassBalance() : check(false) {} @@ -183,7 +183,7 @@ auto NgenMassBalance::initialize(const ModelPtr& model, const Properties& proper } if ( check ) { //Ensure the model is capable of mass balance using the protocol - check_support(model).or_else( error_or_warning ); + auto result = check_support(model).or_else( error_or_warning ); } return {}; // important to return for the expected to be properly created! } diff --git a/src/utilities/logging/CMakeLists.txt b/src/utilities/logging/CMakeLists.txt index 465cff20b0..c6745c01ea 100644 --- a/src/utilities/logging/CMakeLists.txt +++ b/src/utilities/logging/CMakeLists.txt @@ -1,7 +1,9 @@ -add_library(logging logging_utils.cpp Logger.cpp) +add_library(logging logging_utils.cpp) add_library(NGen::logging ALIAS logging) target_include_directories(logging PUBLIC ${PROJECT_SOURCE_DIR}/include/utilities) +target_link_libraries(logging PUBLIC ewts::ewts_ngen_bridge) + target_link_libraries(logging PUBLIC Boost::boost ) diff --git a/src/utilities/logging/Logger.cpp b/src/utilities/logging/Logger.cpp deleted file mode 100644 index fb4c8c785a..0000000000 --- a/src/utilities/logging/Logger.cpp +++ /dev/null @@ -1,671 +0,0 @@ -#include "Logger.hpp" -#include -#include -#include -#include -#include // For getenv() -#include -#include -#include // For file handling -#include -#include -#include -#include // For std::string -#include -#include -#include -#include -#include -#include -#include - -using namespace std; - -const std::string MODULE_NAME = "ngen"; -const std::string LOG_DIR_NGENCERF = "/ngencerf/data"; // ngenCERF log directory string if environement var empty. -const std::string LOG_DIR_DEFAULT = "run-logs"; // Default parent log directory string if env var empty & ngencerf dosn't exist -const std::string LOG_FILE_EXT = "log"; // Log file name extension -const std::string DS = "/"; // Directory separator -const unsigned int LOG_MODULE_NAME_LEN = 8; // Width of module name for log entries - -const std::string EV_EWTS_LOGGING = "NGEN_EWTS_LOGGING"; // Enable/disable of Error Warning and Trapping System -const std::string EV_NGEN_LOGFILEPATH = "NGEN_LOG_FILE_PATH"; // ngen log file - -const std::string CONFIG_FILENAME = "ngen_logging.json"; // ngen logging config file - -// String to LogLevel map -static const std::unordered_map logLevelMap = { - {"NONE", LogLevel::NONE}, {"0", LogLevel::NONE}, - {"DEBUG", LogLevel::DEBUG}, {"1", LogLevel::DEBUG}, - {"INFO", LogLevel::INFO}, {"2", LogLevel::INFO}, - {"WARNING", LogLevel::WARNING}, {"3", LogLevel::WARNING}, - {"SEVERE", LogLevel::SEVERE}, {"4", LogLevel::SEVERE}, - {"FATAL", LogLevel::FATAL}, {"5", LogLevel::FATAL}, -}; - -// Reverse map: LogLevel to String -static const std::unordered_map logLevelToStringMap = { - {LogLevel::NONE, "NONE "}, - {LogLevel::DEBUG, "DEBUG "}, - {LogLevel::INFO, "INFO "}, - {LogLevel::WARNING, "WARNING"}, - {LogLevel::SEVERE, "SEVERE "}, - {LogLevel::FATAL, "FATAL "}, -}; - -const std::unordered_map moduleNamesMap = { - {"NGEN", "NGEN"}, - {"CFE-S", "CFE"}, - {"CFE-X", "CFE"}, - {"LASAM", "LASAM"}, - {"NOAH-OWP-MODULAR", "NOAHOWP"}, - {"PET", "PET"}, - {"SAC-SMA", "SACSMA"}, - {"SFT", "SFT"}, - {"SMP", "SMP"}, - {"SNOW-17", "SNOW17"}, - {"TOPMODEL", "TOPMODEL"}, - {"TOPOFLOW-GLACIER", "TFGLACR"}, - {"T-ROUTE", "TROUTE"}, - {"UEB", "UEB_BMI"}, - {"LSTM", "LSTM"}, - {"FORCING", "FORCING"} -}; - -bool Logger::DirectoryExists(const std::string& path) { - struct stat info; - if (stat(path.c_str(), &info) != 0) { - return false; // Cannot access path - } - return (info.st_mode & S_IFDIR) != 0; -} - -/** - * Create the directory checking both the call - * to execute the command and the result of the command - */ -bool Logger::CreateDirectory(const std::string& path) { - - if (!DirectoryExists(path)) { - std::string mkdir_cmd = "mkdir -p " + path; - int status = system(mkdir_cmd.c_str()); - - if (status == -1) { - std::cerr << "[CRITICAL] " << MODULE_NAME << " system() failed to run mkdir.\n"; - return false; - } else if (WIFEXITED(status)) { - int exitCode = WEXITSTATUS(status); - if (exitCode != 0) { - std::cerr << "[CRITICAL] " << MODULE_NAME << " mkdir command failed with exit code: " << exitCode << "\n"; - return false; - } - } else { - std::cerr << "[CRITICAL] " << MODULE_NAME << " mkdir terminated abnormally.\n"; - return false; - } - } - return true; -} - -/** - * Open log file and return open status. If already open, - * ensure the write pointer is at the end of the file. - * - * return bool true if open and good, false otherwise - */ -bool Logger::LogFileReady(void) { - - if (openedOnce && logFile.is_open() && logFile.good()) { - logFile.seekp(0, std::ios::end); // Ensure write pointer is at the actual file end - return true; - } - else if (openedOnce) { - // Somehow the logfile was closed. Open in append mode so - // previosly logged messages are not lost - logFile.open(logFilePath, ios::out | ios::app); // This will silently fail if already open. - if (logFile.good()) return true; - } - return false; -} - -void Logger::SetupLogFile(void) { - - // Determine the log file directory and log file name. - // Use name from environment variable if set, otherwise use a default - if (!ngenResultsDir.empty()) { - logFileDir = ngenResultsDir + DS + "logs"; - if (CreateDirectory(logFileDir)) - logFilePath = logFileDir + DS + MODULE_NAME + "." + LOG_FILE_EXT; - } - if (logFilePath.empty()) { - // Get parent log directory - if (DirectoryExists(LOG_DIR_NGENCERF)) { - logFileDir = LOG_DIR_NGENCERF + DS + LOG_DIR_DEFAULT; - } - else { - const char *home = getenv("HOME"); // Get users home directory pathname - std::string dir = (home) ? home : "~"; - logFileDir = dir + DS + LOG_DIR_DEFAULT; - } - - // Ensure parent log direcotry exists - if (CreateDirectory(logFileDir)) { - // Get full log directory path - const char* envUsername = std::getenv("USER"); - std::string dirName = (envUsername) ? envUsername : CreateDateString(); - logFileDir = logFileDir + DS + dirName; - - // Set the full path if log directory exists/created - if (CreateDirectory(logFileDir)) - logFilePath = logFileDir + DS + MODULE_NAME + "_" + CreateTimestamp(false,false) + "." + LOG_FILE_EXT; - } - } - - // Attempt to open log file - if (!logFilePath.empty()) { - logFile.open(logFilePath, ios::out | ios::trunc); // Truncating ensures keeping only the last calibration iteration. - if (logFile.is_open()) { - openedOnce = true; - std::cout << "[DEBUG] " << MODULE_NAME << " Log File: " << logFilePath << std::endl; - return; - } - } - std::cout << "[WARNING] " << MODULE_NAME << " Unable to create log file "; - if (!logFilePath.empty()) { - std::cout << logFilePath; - } - else if (!logFileDir.empty()) { - std::cout << logFileDir; - } - std::cout << " (Perhaps check permissions)" << std::endl; - std::cout << "[WARNING] " << MODULE_NAME << " Log entries will be written to stdout" << std::endl; -} - -std::string Logger::ToUpper(const std::string& input) { - std::string result = input; - std::transform(result.begin(), result.end(), result.begin(), - [](unsigned char c){ return std::toupper(c); }); - return result; -} - -std::string Logger::ExtractFirstNDirs(const std::string& path, int numDirs) { - size_t pos = 0; - int slashCount = 0; - - while (pos < path.length() && slashCount < numDirs) { - if (path[pos] == '/') { - ++slashCount; - } - ++pos; - } - - // If the path starts with '/', keep it as is; otherwise return substring - return path.substr(0, pos); -} - -std::string CleanJsonToken(const std::string& token) { - std::string s = token; - if (!s.empty() && s.front() == '"') s.erase(0, 1); - if (!s.empty() && s.back() == ',') s.pop_back(); - if (!s.empty() && s.back() == '"') s.pop_back(); - return s; -} - -/* - JSON file format exmaple: - { - "logging_enabled": true, - "modules": { - "ngen": "INFO", - "CFE-S": "INFO", - "UEB": "INFO", - "Noah-OWP-Modular": "DEBUG", - "T-Route": "INFO" - } - } -*/ - -bool Logger::ParseLoggerConfigFile(std::ifstream& jsonFile) -{ - // Rewind file in case it's been partially read - jsonFile.clear(); - jsonFile.seekg(0, std::ios::beg); - - try { - // Read the JSON into a property tree - boost::property_tree::ptree config; - boost::property_tree::read_json(jsonFile, config); - - // Read logging_enabled flag - try { - loggingEnabled = config.get("logging_enabled", true); // default true if missing - std::cout << "[DEBUG] " << MODULE_NAME << " Logging " - << (loggingEnabled ? "ENABLED" : "DISABLED") << std::endl; - } - catch (const boost::property_tree::ptree_bad_data& e) { - std::cout << "[ERROR] " << MODULE_NAME << " JSON data error: " << e.what() << std::endl; - return false; - } - - // Read modules subtree only if logging enabled - if (loggingEnabled) { - bool atLeastOneModuleFound = false; - if (auto modulesOpt = config.get_child_optional("modules")) { - for (const auto& kv : *modulesOpt) { - std::string moduleName = ToUpper(kv.first); - std::string levelStr = ToUpper(kv.second.get_value()); - - auto it = moduleNamesMap.find(moduleName); - if (it != moduleNamesMap.end()) { - atLeastOneModuleFound = true; - moduleLogLevels[moduleName] = ConvertStringToLogLevel(levelStr); - std::cout << "[DEBUG] " << MODULE_NAME << " Found Log level " - << kv.first << "=" - << ConvertLogLevelToString(moduleLogLevels[moduleName]) - << std::endl; - if (moduleName == "NGEN") logLevel = moduleLogLevels[moduleName]; - } else { - std::cout << "[ERROR] " << MODULE_NAME << " Ignoring unknown module " << moduleName << std::endl; - } - } - } else { - std::cout << "[ERROR] " << MODULE_NAME << " Missing 'modules' section in logging.json." << std::endl; - } - return atLeastOneModuleFound; - } - return true; - } - catch (const boost::property_tree::json_parser_error& e) { - std::cout << "[ERROR] " << MODULE_NAME << " JSON parse error: " << e.what() << std::endl; - } - catch (const std::exception& e) { - std::cout << "[ERROR] " << MODULE_NAME << " Exception while parsing config: " << e.what() << std::endl; - } - return false; -} - -void Logger::ReadConfigFile(std::string searchPath) { - - bool success = false; - std::ifstream jsonFile; - - // Set logger defaults - moduleLogLevels.clear(); - loggingEnabled = true; - - // Open and Parse config file - if (searchPath.empty()) { - std::cout << "[WARNING] " << MODULE_NAME << " Logging config file cannot be read from NGEN_RESULTS_DIR environment variable because not set or empty." << std::endl; - std::cout << "[WARNING] " << MODULE_NAME << " Using defaults for logging." << std::endl; - } else { - if (FindAndOpenLogConfigFile(searchPath, jsonFile)) { - if (jsonFile.peek() != std::ifstream::traits_type::eof()) { - std::cout << "[DEBUG] " << MODULE_NAME << " parsing logging config file " << searchPath << std::endl; - success = ParseLoggerConfigFile(jsonFile); - } - } - } - if (loggingEnabled && !success) { - std::cout << "[WARNING] " << MODULE_NAME << " Issue with logging config file " << CONFIG_FILENAME << " in " << ((searchPath.empty())?"undefined path":searchPath) << "." << std::endl; - std::cout << "[WARNING] " << MODULE_NAME << " Using default logging configuration of enabled and log level INFO for all known modules" << std::endl; - for (const auto kv : moduleNamesMap) { - std::string moduleName = ToUpper(kv.first); - moduleLogLevels[moduleName] = LogLevel::INFO; - } - } -} - -bool Logger::IsValidEnvVarName(const std::string& name) { - if (name.empty()) return false; - - // First character must be a letter or underscore - if (!std::isalpha(name[0]) && name[0] != '_') return false; - - // All other characters must be alphanumeric or underscore - for (size_t i = 1; i < name.size(); ++i) { - if (!std::isalnum(name[i]) && name[i] != '_') return false; - } - - return true; -} - -void Logger::ManageLoggingEnvVars(bool set) { - - // Set logger env vars common to all modules - if (set) { - Log("ngen Logger setup: Setting Module Logger Environment Variables", LogLevel::DEBUG); - if (!logFilePath.empty()) { - // Set the log file env var - setenv("NGEN_LOG_FILE_PATH", logFilePath.c_str(), 1); - std::cout << "[DEBUG] " << MODULE_NAME << " Set env var NGEN_LOG_FILE_PATH=" << logFilePath << std::endl; - } - else { - std::cout << "[WARNING] " << MODULE_NAME << " NGEN_LOG_FILE_PATH env var not set. Modules writing to their default logs." << std::endl; - } - - // Set the logging enabled/disabled env var - setenv((EV_EWTS_LOGGING).c_str(), ((loggingEnabled)?"ENABLED":"DISABLED"), 1); - std::string logMsg = std::string("Set Logging ") + ((loggingEnabled)?"ENABLED":"DISABLED"); - std::cout << logMsg << "(envVar=" << EV_EWTS_LOGGING << ")" << std::endl; - if (!logFilePath.empty()) { - LogLevel saveLevel = logLevel; - logLevel = LogLevel::INFO; // Ensure this INFO message is always logged - Log(logMsg, logLevel); - logLevel = saveLevel; - } - } - else { - Log("Logger setup: Unset existing Module Logger Environment Variables", LogLevel::DEBUG); - } - - // Set logger env vars unique to each module in the formulation - // Note: moduleLogLevels is populated from the logging config file which - // only contains the log levels for module in the formulation - for (const auto& modulePair : moduleLogLevels) { - std::string envVar; - std::string moduleNameForEnvVar; - - const std::string& moduleName = modulePair.first; - LogLevel level = modulePair.second; - - // Look up the module env var name in moduleNamesMap - auto it = moduleNamesMap.find(moduleName); - if (it != moduleNamesMap.end()) { - moduleNameForEnvVar = it->second; - if (!IsValidEnvVarName(moduleNameForEnvVar)) { - std::string logMsg = std::string("Invalid env var name ") + moduleNameForEnvVar + - std::string(" for module ") + moduleName; - Log(logMsg, LogLevel::WARNING); - continue; - } - } - else { - std::string logMsg = std::string("Unknown module in logLevels: ") + moduleName; - Log(logMsg, LogLevel::WARNING); - continue; - } - - if (set) { - // Sets the log level envirnoment variable - envVar = moduleNameForEnvVar + "_LOGLEVEL"; - std::string ll = ConvertLogLevelToString(level); - setenv(envVar.c_str(), ll.c_str(), 1); - std::string logMsg = std::string("Set ") + moduleName - + ((moduleName != "NGEN")?" Log Level env var to ":" Log Level to ") - + TrimString(ll); - std::cout << logMsg; - if (moduleName != "NGEN") std::cout << " (" << envVar << ")"; - std::cout << std::endl; - if (!logFilePath.empty()) { - LogLevel saveLevel = logLevel; - logLevel = LogLevel::INFO; // Ensure this INFO message is always logged - Log(logMsg, logLevel); - logLevel = saveLevel; - } - } - else { - if (moduleName != "NGEN") { - // It is possible that individual submodules may be writing to their own - // logs if there was an issue accessing the ngen log file. The log file used - // by each module is stored in its own environment variable. Unsetting this - // environment variable will cause the modules to truncate their logs. - // This is important when running calibrations since iterative runs of ngen - // can number in the 1000's and it is only necessary to retain the last ngen run - envVar = moduleNameForEnvVar + "_LOGFILEPATH"; - unsetenv(envVar.c_str()); - envVar = moduleNameForEnvVar + "_LOGLEVEL"; - unsetenv(envVar.c_str()); - } - } - } -} - -/** -* Configure Logger Preferences and open log file -* @param level: LogLevel::WARNING by Default -* @return void -*/ -void Logger::SetLogPreferences(LogLevel level) { - - if (!loggerInitialized) { - loggerInitialized = true; // Only call this once - - // Unset any existing related environment vars - ManageLoggingEnvVars(false); - - // Determine the log file directory and log file name. - // Use name from environment variable if set, otherwise use a default - const char* envVar = std::getenv("NGEN_RESULTS_DIR"); // Currently set by ngen-cal but envision set for WCOSS at some point - if (envVar != nullptr && envVar[0] != '\0') { - ngenResultsDir = envVar; - std::cout << "[DEBUG] " << MODULE_NAME << " Found envVar NGEN_RESULTS_DIR = " << ngenResultsDir << std::endl; - } - - ReadConfigFile(ngenResultsDir); - - if (loggingEnabled) { - - // Make sure the module name used for logging is all uppercase and LOG_MODULE_NAME_LEN characters wide. - moduleName = MODULE_NAME; - std::string upperName = moduleName.substr(0, LOG_MODULE_NAME_LEN); // Truncate to LOG_MODULE_NAME_LEN chars max - std::transform(upperName.begin(), upperName.end(), upperName.begin(), ::toupper); - - std::ostringstream oss; - oss << std::left << std::setw(LOG_MODULE_NAME_LEN) << std::setfill(' ') << upperName; - moduleName = oss.str(); - - SetupLogFile(); - - // Set the environment variables for the module loggers - ManageLoggingEnvVars(true); - } - } -} - -void Logger::Log(LogLevel messageLevel, const char* message, ...) { - va_list args; - va_start(args, message); - - // Make a copy to calculate required size - va_list args_copy; - va_copy(args_copy, args); - int requiredLen = vsnprintf(nullptr, 0, message, args_copy); - va_end(args_copy); - - if (requiredLen > 0) { - std::vector buffer(requiredLen + 1); // +1 for null terminator - vsnprintf(buffer.data(), buffer.size(), message, args); - - va_end(args); - - Log(std::string(buffer.data()), messageLevel); - } else { - va_end(args); // still need to clean up - } -} - -/** - * Log given message with defined parameters and generate message to pass on Console or File - * @param message: Log Message - * @param messageLevel: Log Level, LogLevel::INFO by default - */ -void Logger::Log(LogLevel messageLevel, std::string message) { - Log(message, messageLevel); -} -/** -* Log given message with defined parameters and generate message to pass on Console or File -* @param message: Log Message -* @param messageLevel: Log Level, LogLevel::INFO by default -*/ -void Logger::Log(std::string message, LogLevel messageLevel) { - Logger *logger = GetLogger(); - - // Log only when appropriate - if ((logger->loggingEnabled) && (messageLevel >= logger->logLevel)) { - std::string logType = ConvertLogLevelToString(messageLevel); - std::string logPrefix = CreateTimestamp() + " " + logger->moduleName + " " + logType; - - // Log message, creating individual entries for a multi-line message - std::istringstream logMsg(message); - std::string line; - if (logger->LogFileReady()) { - while (std::getline(logMsg, line)) { - logger->logFile << logPrefix + " " + line << std::endl; - } - logger->logFile.flush(); - } - else { - // Log file not found. Write to stdout. - while (std::getline(logMsg, line)) { - std::cout << logPrefix + " " + line << std::endl; - } - std::cout << std::flush; - } - } -} - -Logger* Logger::GetLogger() -{ - static Logger* logger = nullptr; - if (logger == nullptr) { - logger = new Logger; - logger->SetLogPreferences(); - } - return logger; -} - -Logger::Logger() -{ - -} - -// Function to trim leading and trailing spaces -std::string Logger::TrimString(const std::string& str) { - // Trim leading spaces - size_t first = str.find_first_not_of(" \t\n\r\f\v"); - if (first == std::string::npos) { - return ""; // No non-whitespace characters - } - - // Trim trailing spaces - size_t last = str.find_last_not_of(" \t\n\r\f\v"); - - // Return the trimmed string - return str.substr(first, last - first + 1); -} - -std::string Logger::ConvertLogLevelToString(LogLevel level) { - auto it = logLevelToStringMap.find(level); - if (it != logLevelToStringMap.end()) { - return it->second; // Found valid named or numeric log level - } - return "NONE"; -} - -/** -* Convert String Representation of Log Level to LogLevel Type -* @param levelStr : String log level -* @return LogLevel -*/ -LogLevel Logger::ConvertStringToLogLevel(const std::string& levelStr) { - std::string level = TrimString(levelStr); - if (!level.empty()) { - // Convert string to LogLevel (supports both names and numbers) - auto it = logLevelMap.find(level); - if (it != logLevelMap.end()) { - return it->second; // Found valid named or numeric log level - } - - // Try parsing as an integer (for cases where an invalid numeric value is given) - try { - int levelNum = std::stoi(level); - if (levelNum >= 0 && levelNum <= 5) { - return static_cast(levelNum); - } - } catch (...) { - // Ignore errors (e.g., if std::stoi fails for non-numeric input) - } - } - return LogLevel::NONE; -} - -std::string Logger::CreateTimestamp(bool appendMS, bool iso) { - using namespace std::chrono; - - // Get current time point - auto now = system_clock::now(); - auto now_time_t = system_clock::to_time_t(now); - - // Get milliseconds - auto ms = duration_cast(now.time_since_epoch()) % 1000; - - // Convert to UTC time - std::tm utc_tm; - gmtime_r(&now_time_t, &utc_tm); - - // Format date/time with strftime - char buffer[32]; - if (iso) { - std::strftime(buffer, sizeof(buffer), "%Y-%m-%dT%H:%M:%S", &utc_tm); - } - else { - std::strftime(buffer, sizeof(buffer), "%Y%m%dT%H%M%S", &utc_tm); - } - - if (appendMS) { - // Combine with milliseconds - std::ostringstream oss; - oss << buffer << '.' << std::setw(3) << std::setfill('0') << ms.count(); - return oss.str(); - } - return std::string(buffer); -} - -std::string Logger::CreateDateString(void) { - std::time_t tt = std::time(0); - std::tm* timeinfo = std::gmtime(&tt); // Use std::localtime(&tt) if you want local time - - char buffer[11]; // Enough for "YYYY-MM-DD" + null terminator - std::strftime(buffer, sizeof(buffer), "%F", timeinfo); // %F == %Y-%m-%d - - std::stringstream ss; - ss << buffer; - - return ss.str(); -} - -std::string Logger::GetLogFilePath(void) { - return logFilePath; -} - -bool Logger::FileExists(const std::string& path) { - struct stat statbuf{}; - return stat(path.c_str(), &statbuf) == 0 && S_ISREG(statbuf.st_mode); -} - -std::string Logger::GetParentDirName(const std::string& path) { - size_t pos = path.find_last_of('/'); - if (pos == std::string::npos || pos == 0) return "/"; - return path.substr(0, pos); -} - -bool Logger::FindAndOpenLogConfigFile(std::string path, std::ifstream& configFileStream) { - while (!path.empty() && path != "/") { - std::string candidate = path + DS + CONFIG_FILENAME; - if (FileExists(candidate)) { - std::cout << "[DEBUG] " << MODULE_NAME << " Opening logger config file " << candidate << std::endl; - configFileStream.open(candidate); - return configFileStream.is_open(); - } - path = GetParentDirName(path); - } - return false; -} - -LogLevel Logger::GetLogLevel(void) { - return logLevel; -} - -bool Logger::IsLoggingEnabled(void) { - return loggingEnabled; -} diff --git a/src/utilities/logging/logging_utils.cpp b/src/utilities/logging/logging_utils.cpp index 94a0f80567..43f4fc8620 100644 --- a/src/utilities/logging/logging_utils.cpp +++ b/src/utilities/logging/logging_utils.cpp @@ -2,7 +2,7 @@ #include #include #include "logging_utils.h" -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" namespace logging { diff --git a/src/utilities/mdframe/CMakeLists.txt b/src/utilities/mdframe/CMakeLists.txt index cafdf651fc..a254268f54 100644 --- a/src/utilities/mdframe/CMakeLists.txt +++ b/src/utilities/mdframe/CMakeLists.txt @@ -7,6 +7,7 @@ NGen::config_header NGen::mdarray NGen::logging ) +target_link_libraries(mdframe PUBLIC ewts::ewts_ngen_bridge) if(NGEN_WITH_NETCDF) target_link_libraries(mdframe PUBLIC NetCDF) diff --git a/src/utilities/mdframe/handler_csv.cpp b/src/utilities/mdframe/handler_csv.cpp index 6f9d291943..8e4526ee97 100644 --- a/src/utilities/mdframe/handler_csv.cpp +++ b/src/utilities/mdframe/handler_csv.cpp @@ -1,5 +1,5 @@ #include -#include "Logger.hpp" +#include "ewts_ngen/logger.hpp" #include "mdframe/mdframe.hpp" #include @@ -41,9 +41,10 @@ void cartesian_indices(const boost::span shape, std::vector variable_subset; @@ -65,7 +66,8 @@ void mdframe::to_csv(const std::string& path, bool header) const } if (variable_subset.empty()) { - Logger::logMsgAndThrowError("cannot output CSV with no output variables"); + LOG(LogLevel::FATAL, "cannot output CSV with no output variables"); + throw std::runtime_error("cannot output CSV with no output variables"); } // Calculate total number of rows across all subdimensions (not including header) diff --git a/src/utilities/mdframe/handler_netcdf.cpp b/src/utilities/mdframe/handler_netcdf.cpp index d36c5a9a65..0858a080d8 100644 --- a/src/utilities/mdframe/handler_netcdf.cpp +++ b/src/utilities/mdframe/handler_netcdf.cpp @@ -82,7 +82,8 @@ void mdframe::to_netcdf(const std::string& path) const namespace ngen { void mdframe::to_netcdf(const std::string& path) const { - Logger::logMsgAndThrowError("This functionality isn't available. Compile NGen with NGEN_WITH_NETCDF=ON to enable NetCDF support"); + LOG(LogLevel::FATAL, "This functionality isn't available. Compile NGen with NGEN_WITH_NETCDF=ON to enable NetCDF support"); + throw std::runtime_error("This functionality isn't available. Compile NGen with NGEN_WITH_NETCDF=ON to enable NetCDF support"); } } diff --git a/src/utilities/python/CMakeLists.txt b/src/utilities/python/CMakeLists.txt index 692bbe8c10..1779770a8b 100644 --- a/src/utilities/python/CMakeLists.txt +++ b/src/utilities/python/CMakeLists.txt @@ -1,6 +1,9 @@ add_library(ngen_python InterpreterUtil.cpp) add_library(NGen::python ALIAS ngen_python) +find_package(ewts CONFIG REQUIRED) # if not already in scope +target_link_libraries(ngen_python PUBLIC ewts::ewts_ngen_bridge) + target_include_directories(ngen_python PUBLIC ${PROJECT_SOURCE_DIR}/include/) target_link_libraries(ngen_python PUBLIC diff --git a/src/utilities/python/InterpreterUtil.cpp b/src/utilities/python/InterpreterUtil.cpp index a4555f3fea..be292349c5 100644 --- a/src/utilities/python/InterpreterUtil.cpp +++ b/src/utilities/python/InterpreterUtil.cpp @@ -4,7 +4,7 @@ #if NGEN_WITH_PYTHON #include -#include +#include "ewts_ngen/logger.hpp" #include #include