diff --git a/.github/actions/ngen-build/action.yaml b/.github/actions/ngen-build/action.yaml index 8c0f79730b..c4558969b0 100644 --- a/.github/actions/ngen-build/action.yaml +++ b/.github/actions/ngen-build/action.yaml @@ -52,6 +52,10 @@ inputs: required: false description: 'Enable mpi support, only available for Linux runners' default: 'OFF' + build_extern: + required: false + description: 'Use external dependencies where possible' + default: 'OFF' outputs: build-dir: description: "Directory build was performed in" @@ -110,64 +114,56 @@ runs: id: cache-boost-dep uses: actions/cache@v4 with: - path: boost_1_79_0 + path: boost_1_86_0 key: unix-boost-dep - name: Get Boost Dependency if: steps.cache-boost-dep.outputs.cache-hit != 'true' run: | - curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download - tar xjf boost_1_79_0.tar.bz2 + curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download + tar xjf boost_1_86_0.tar.bz2 shell: bash - name: Set Pip Constraints run: | echo "numpy<2.0" > $GITHUB_WORKSPACE/constraints.txt echo "PIP_CONSTRAINT=$GITHUB_WORKSPACE/constraints.txt" >> $GITHUB_ENV + echo "UV_CONSTRAINT=$GITHUB_WORKSPACE/constraints.txt" >> $GITHUB_ENV shell: bash - - name: Cache Python Dependencies - id: cache-py3-dependencies - uses: actions/cache@v3 - with: - path: .venv - key: ${{ runner.os }}-python-deps + - name: Add uv and Native Python Tooling Translations + if: | + inputs.use_python != 'OFF' + run: | + echo 'ACTIVATE_VENV_IF_USE_PYTHON=source .venv/bin/activate' >> $GITHUB_ENV + if command -v uv &>/dev/null; then + echo 'CREATE_VENV=uv venv .venv' >> $GITHUB_ENV + echo 'PIP_INSTALL=uv pip install' >> $GITHUB_ENV + else + echo 'CREATE_VENV=python3 -m venv .venv' >> $GITHUB_ENV + echo 'PIP_INSTALL=pip install' >> $GITHUB_ENV + fi + shell: bash - name: Get Numpy Python Dependency - # Tried conditioning the cache/install of python with an extra check: - # inputs.use_python != 'OFF' && - # but what happens is that a runner not requiring python will create an empty cache - # and future runners will pull that and then fail... - # What we could do is try to create a master `requirements.txt` - # and/or a `test_requirements.txt` file that we can build the hash key from - # and read it from the repo...but we still have to always initialize the cache - # regardless of whether a given runner uses it, to avoid another runner failing to - # find it. Or just initialize this minimum requirement of numpy, and let the venv - # grow based on other runners needs, effectively building the cache with each new addition if: | - steps.cache-py3-dependencies.outputs.cache-hit != 'true' + inputs.use_python != 'OFF' run: | - python3 -m venv .venv - . .venv/bin/activate - pip install pip - pip install numpy + $CREATE_VENV + $ACTIVATE_VENV_IF_USE_PYTHON + echo $(which python3) + $PIP_INSTALL pip + $PIP_INSTALL numpy deactivate shell: bash - name: Init Additional Python Dependencies - # Don't condition additonal installs on a cache hit - # What will happen, however, is that the venv will get updated - # and thus the cache will get updated - # so any pip install will find modules already installed... - # if: | - # inputs.additional_python_requirements != '' && - # steps.cache-py3-dependencies.outputs.cache-hit != 'true' if: | + inputs.use_python != 'OFF' && inputs.additional_python_requirements != '' run: | - python3 -m venv .venv - . .venv/bin/activate - pip install -r ${{ inputs.additional_python_requirements }} + $ACTIVATE_VENV_IF_USE_PYTHON + $PIP_INSTALL -r ${{ inputs.additional_python_requirements }} deactivate shell: bash @@ -185,13 +181,30 @@ runs: - name: Cmake Initialization id: cmake_init + # NOTE: -DCMAKE_POLICY_VERSION_MINIMUM=3.5 is required to use cmake version 4 + # and with older pybind11 versions, the minimum cmake version is set to 3.4 + # which causes cmake configuration to fail. run: | - export BOOST_ROOT="$(pwd)/boost_1_79_0" - export CFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -Werror" - export CXXFLAGS="-fsanitize=address -O1 -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" - . .venv/bin/activate + export BOOST_ROOT="$(pwd)/boost_1_86_0" + export CFLAGS="-fsanitize=address -g -fno-omit-frame-pointer -Werror" + export CXXFLAGS="-fsanitize=address -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" + if [ ${{ runner.os }} == 'macOS' ] + then + echo "fun:PyType_FromMetaclass" > /tmp/asan_ignore.txt + export CFLAGS="$CFLAGS -O0 -fsanitize-ignorelist=/tmp/asan_ignore.txt -fno-common" + export CXXFLAGS="$CXXFLAGS -O0 -fsanitize-ignorelist=/tmp/asan_ignore.txt -fno-common" + else + export CFLAGS="$CFLAGS -O1" + export CXXFLAGS="$CXXFLAGS -O1" + fi + # NOTE: this is not defined if inputs.use_python != 'ON' + $ACTIVATE_VENV_IF_USE_PYTHON [ ! -d "$BOOST_ROOT" ] && echo "Error: no Boost root found at $BOOST_ROOT" && exit 1 + echo "Cmake Version:" + which cmake + cmake --version cmake -B ${{ inputs.build-dir }} \ + -DNGEN_WITH_EXTERN_ALL:BOOL=${{ inputs.build_extern }} \ -DNGEN_WITH_BMI_C:BOOL=${{ inputs.bmi_c }} \ -DNGEN_WITH_PYTHON:BOOL=${{ inputs.use_python }} \ -DNGEN_WITH_UDUNITS:BOOL=${{ inputs.use_udunits }} \ @@ -199,7 +212,9 @@ runs: -DNGEN_WITH_ROUTING:BOOL=${{ inputs.use_troute }} \ -DNGEN_WITH_NETCDF:BOOL=${{ inputs.use_netcdf }} \ -DNGEN_WITH_SQLITE:BOOL=${{ inputs.use_sqlite }} \ - -DNGEN_WITH_MPI:BOOL=${{ inputs.use_mpi }} -S . + -DNGEN_WITH_MPI:BOOL=${{ inputs.use_mpi }} \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -S . + echo "build-dir=$(echo ${{ inputs.build-dir }})" >> $GITHUB_OUTPUT shell: bash @@ -209,8 +224,8 @@ runs: # Build Targets # Disable leak detection during test enumeration export ASAN_OPTIONS=detect_leaks=false - # Activate venv so that test discovery run during build works - . .venv/bin/activate + # NOTE: this is not defined if inputs.use_python != 'ON' + $ACTIVATE_VENV_IF_USE_PYTHON cmake --build ${{ inputs.build-dir }} --target ${{ inputs.targets }} -- -j ${{ inputs.build-cores }} shell: bash diff --git a/.github/actions/ngen-submod-build/action.yaml b/.github/actions/ngen-submod-build/action.yaml index 349434fa45..f24fc5ba93 100644 --- a/.github/actions/ngen-submod-build/action.yaml +++ b/.github/actions/ngen-submod-build/action.yaml @@ -50,6 +50,14 @@ runs: - name: Cmake Initialization id: cmake_init run: | + if [ ${{ runner.os }} == 'macOS' ] + then + export OPT_LEVEL_FLAG="-O0" + else + export OPT_LEVEL_FLAG="-O1" + fi + echo CFLAGS="-fsanitize=address ${OPT_LEVEL_FLAG:?Optimization flag var not set} -g -fno-omit-frame-pointer -Werror" >> $GITHUB_ENV + echo CXXFLAGS="-fsanitize=address ${OPT_LEVEL_FLAG:?Optimization flag var not set} -g -fno-omit-frame-pointer -pedantic-errors -Werror -Wpessimizing-move -Wparentheses -Wrange-loop-construct -Wsuggest-override" >> $GITHUB_ENV cmake -B ${{ inputs.mod-dir}}/${{ inputs.build-dir }} -S ${{ inputs.mod-dir }} ${{ inputs.cmake-flags }} echo "build-dir=$(echo ${{ inputs.mod-dir}}/${{ inputs.build-dir }})" >> $GITHUB_OUTPUT shell: bash diff --git a/.github/workflows/module_integration.yml b/.github/workflows/module_integration.yml index b89c629079..1e757032ee 100644 --- a/.github/workflows/module_integration.yml +++ b/.github/workflows/module_integration.yml @@ -22,7 +22,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} diff --git a/.github/workflows/test_and_validate.yml b/.github/workflows/test_and_validate.yml index 00279ae8d8..2a4757c4f1 100644 --- a/.github/workflows/test_and_validate.yml +++ b/.github/workflows/test_and_validate.yml @@ -24,7 +24,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -59,7 +59,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -76,7 +76,9 @@ jobs: build-cores: ${{ env.LINUX_NUM_PROC_CORES }} - name: Run Tests - run: ./cmake_build/test/compare_pet + run: | + export ASAN_OPTIONS=${ASAN_OPTIONS}:detect_odr_violation=0 + ./cmake_build/test/compare_pet timeout-minutes: 15 - name: Clean Up @@ -104,7 +106,7 @@ jobs: use_mpi: 'ON' - name: run_tests - run: mpirun --allow-run-as-root -np 2 ./cmake_build/test/test_remote_nexus + run: mpirun --allow-run-as-root --oversubscribe -np 4 ./cmake_build/test/test_remote_nexus timeout-minutes: 15 - name: Clean Up @@ -116,7 +118,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -139,6 +141,7 @@ jobs: - name: Run Tests run: | cd ./cmake_build/test/ + export ASAN_OPTIONS=${ASAN_OPTIONS}:detect_odr_violation=0 ./test_bmi_cpp cd ../../ timeout-minutes: 15 @@ -151,7 +154,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -182,7 +185,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -215,7 +218,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -250,7 +253,7 @@ jobs: # The type of runner that the job will run on strategy: matrix: - os: [ubuntu-22.04, macos-12] + os: [ubuntu-22.04, macos-15] fail-fast: false runs-on: ${{ matrix.os }} @@ -272,12 +275,43 @@ jobs: - name: Run Unit Tests run: | . .venv/bin/activate + export ASAN_OPTIONS=${ASAN_OPTIONS}:detect_odr_violation=0 ./cmake_build/test/test_bmi_multi timeout-minutes: 15 - name: Clean Up Unit Test Build uses: ./.github/actions/clean-build + # Run BMI protocol tests in linux/unix environment + test_bmi_protocols: + # The type of runner that the job will run on + strategy: + matrix: + os: [ubuntu-22.04, macos-15] + fail-fast: false + runs-on: ${{ matrix.os }} + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + - name: Build BMI Protocol Unit Tests + uses: ./.github/actions/ngen-build + with: + targets: "test_bmi_protocols" + build-cores: ${{ env.LINUX_NUM_PROC_CORES }} + + - name: run_bmi_protocol_tests + run: | + cd ./cmake_build/test/ + export ASAN_OPTIONS=${ASAN_OPTIONS}:detect_odr_violation=0 + ./test_bmi_protocols + cd ../../ + timeout-minutes: 15 + + - name: Clean Up BMI Protocol Unit Test Build + uses: ./.github/actions/clean-build # TODO: fails due to compilation error, at least in large part due to use of POSIX functions not supported on Windows. # TODO: Need to determine whether Windows support (in particular, development environment support) is necessary. diff --git a/CMakeLists.txt b/CMakeLists.txt index ecf309e715..7c96c93b27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -165,7 +165,7 @@ 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.79.0 REQUIRED) +find_package(Boost 1.86.0 REQUIRED) # ----------------------------------------------------------------------------- if(NGEN_WITH_SQLITE) @@ -323,6 +323,7 @@ add_subdirectory("src/utilities/mdarray") add_subdirectory("src/utilities/mdframe") add_subdirectory("src/utilities/logging") add_subdirectory("src/utilities/python") +add_subdirectory("src/utilities/bmi") target_link_libraries(ngen PUBLIC @@ -336,6 +337,7 @@ target_link_libraries(ngen NGen::core_mediator NGen::logging NGen::parallel + NGen::bmi_protocols ) if(NGEN_WITH_SQLITE) @@ -493,3 +495,7 @@ ngen_dependent_multiline_message(NGEN_WITH_PYTHON message(STATUS "---------------------------------------------------------------------") configure_file("${NGEN_INC_DIR}/NGenConfig.h.in" "${CMAKE_CURRENT_BINARY_DIR}/include/NGenConfig.h") + +include(GNUInstallDirs) +install(TARGETS ngen OPTIONAL) +install(TARGETS partitionGenerator OPTIONAL) diff --git a/Dockerfile b/Dockerfile index 0eaa26dc3c..b9e4d6431c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,7 @@ ENV LANG="C.UTF-8" \ HDF5_VERSION="1.10.11" \ NETCDF_C_VERSION="4.7.4" \ NETCDF_FORTRAN_VERSION="4.5.4" \ - BOOST_VERSION="1.83.0" + BOOST_VERSION="1.86.0" # runtime dependencies RUN set -eux && \ diff --git a/INSTALL.md b/INSTALL.md index 8b73cb5f97..ad4fad92a2 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -35,15 +35,15 @@ cd ngen **Download the Boost Libraries:** ```shell -curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download \ - && tar -xjf boost_1_79_0.tar.bz2 \ - && rm boost_1_79_0.tar.bz2 +curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download \ + && tar -xjf boost_1_86_0.tar.bz2 \ + && rm boost_1_86_0.tar.bz2 ``` **Set the ENV for Boost and C compiler:** ```shell -set BOOST_ROOT="/boost_1_79_0" +set BOOST_ROOT="/boost_1_86_0" set CXX=/usr/bin/g++ ``` @@ -79,7 +79,7 @@ The following CMake command will configure the build: ```shell cmake -DCMAKE_CXX_COMPILER=/usr/bin/g++ \ - -DBOOST_ROOT=boost_1_79_0 \ + -DBOOST_ROOT=boost_1_86_0 \ -B /build \ -S . ``` diff --git a/doc/BUILDS_AND_CMAKE.md b/doc/BUILDS_AND_CMAKE.md index 1f1dd944b0..f17613262c 100644 --- a/doc/BUILDS_AND_CMAKE.md +++ b/doc/BUILDS_AND_CMAKE.md @@ -104,7 +104,7 @@ In some cases - in particular **Google Test** - the build system will need to be ## Boost ENV Variable -The Boost libraries must be available for the project to compile. The details are discussed more in the [Dependencies](DEPENDENCIES.md) doc, but as a helpful hint, the **BOOST_ROOT** environmental variable can be set to the path of the applicable [Boost root directory](https://www.boost.org/doc/libs/1_79_0/more/getting_started/unix-variants.html#the-boost-distribution). The project's [CMakeLists.txt](../CMakeLists.txt) is written to check for this env variable and use it to set the Boost include directory. +The Boost libraries must be available for the project to compile. The details are discussed more in the [Dependencies](DEPENDENCIES.md) doc, but as a helpful hint, the **BOOST_ROOT** environmental variable can be set to the path of the applicable [Boost root directory](https://www.boost.org/doc/libs/1_86_0/more/getting_started/unix-variants.html#the-boost-distribution). The project's [CMakeLists.txt](../CMakeLists.txt) is written to check for this env variable and use it to set the Boost include directory. Note that if the variable is not set, it may still be possible for CMake to find Boost, although a *status* message will be printed by CMake indicating **BOOST_ROOT** was not set. diff --git a/doc/DEPENDENCIES.md b/doc/DEPENDENCIES.md index 6c48b38f94..e5e16c1d33 100644 --- a/doc/DEPENDENCIES.md +++ b/doc/DEPENDENCIES.md @@ -7,7 +7,7 @@ | [Google Test](#google-test) | submodule | `release-1.10.0` | | | [C/C++ Compiler](#c-and-c-compiler) | external | see below | | | [CMake](#cmake) | external | \>= `3.17` | | -| [Boost (Headers Only)](#boost-headers-only) | external | `1.79.0` | headers only library | +| [Boost (Headers Only)](#boost-headers-only) | external | `1.86.0` | headers only library | | [Udunits libraries](https://www.unidata.ucar.edu/software/udunits) | external | >= 2.0 | Can be installed via package manager or from source | | [MPI](https://www.mpi-forum.org) | external | No current implementation or version requirements | Required for [multi-process distributed execution](DISTRIBUTED_PROCESSING.md) | | [Python 3 Libraries](#python-3-libraries) | external | \>= `3.8.0` | Can be [excluded](#overriding-python-dependency). | @@ -78,7 +78,7 @@ Currently, a version of CMake >= `3.14.0` is required. ## Boost (Headers Only) -Boost libraries are used by this project. In particular, [Boost.Geometry](https://www.boost.org/doc/libs/1_79_0/libs/geometry/doc/html/geometry/compilation.html) is used, but others are also. +Boost libraries are used by this project. In particular, [Boost.Geometry](https://www.boost.org/doc/libs/1_86_0/libs/geometry/doc/html/geometry/compilation.html) is used, but others are also. Currently, only headers-only Boost libraries are utilized. As such, they are not exhaustively listed here since getting one essentially gets them all. @@ -88,7 +88,7 @@ Since only headers-only libraries are needed, the Boost headers simply need to b There are a variety of different ways to get the Boost headers locally. Various OS may have packages specifically to install them, though one should take note of whether such packages provide a version of Boost that meets this project's requirements. -Alternatively, the Boost distribution itself can be manually downloaded and unpacked, as described for both [Unix-variants](https://www.boost.org/doc/libs/1_79_0/more/getting_started/unix-variants.html) and [Windows](https://www.boost.org/doc/libs/1_79_0/more/getting_started/windows.html) on the Boost website. +Alternatively, the Boost distribution itself can be manually downloaded and unpacked, as described for both [Unix-variants](https://www.boost.org/doc/libs/1_86_0/more/getting_started/unix-variants.html) and [Windows](https://www.boost.org/doc/libs/1_86_0/more/getting_started/windows.html) on the Boost website. #### Setting **BOOST_ROOT** @@ -96,11 +96,11 @@ If necessary, the project's CMake config is able to use the value of the **BOOST However, it will often be necessary to set **BOOST_ROOT** if Boost was manually set up by downloading the distribution. -The variable should be set to the value of the **boost root directory**, which is something like `/boost_1_79_0`. +The variable should be set to the value of the **boost root directory**, which is something like `/boost_1_86_0`. ### Version Requirements -At present, a version >= `1.79.0` is required. +At present, a version >= `1.86.0` is required. ## Udunits diff --git a/docker/CENTOS_4.8.5_NGEN_RUN.dockerfile b/docker/CENTOS_4.8.5_NGEN_RUN.dockerfile index 9569428d20..794d40827d 100644 --- a/docker/CENTOS_4.8.5_NGEN_RUN.dockerfile +++ b/docker/CENTOS_4.8.5_NGEN_RUN.dockerfile @@ -19,11 +19,11 @@ ENV CXX=/usr/bin/g++ RUN git submodule update --init --recursive -- test/googletest -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download -RUN tar -xjf boost_1_79_0.tar.bz2 +RUN tar -xjf boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="boost_1_79_0" +ENV BOOST_ROOT="boost_1_86_0" RUN cmake -B /ngen -S . diff --git a/docker/CENTOS_NGEN_RUN.dockerfile b/docker/CENTOS_NGEN_RUN.dockerfile index 8aee5b9a4b..afaa442d34 100644 --- a/docker/CENTOS_NGEN_RUN.dockerfile +++ b/docker/CENTOS_NGEN_RUN.dockerfile @@ -9,11 +9,11 @@ RUN yum update -y \ && dnf clean all \ && rm -rf /var/cache/yum -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download \ - && tar -xjf boost_1_79_0.tar.bz2 \ - && rm boost_1_79_0.tar.bz2 +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download \ + && tar -xjf boost_1_86_0.tar.bz2 \ + && rm boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="/boost_1_79_0" +ENV BOOST_ROOT="/boost_1_86_0" ENV CXX=/usr/bin/g++ diff --git a/docker/CENTOS_TEST.dockerfile b/docker/CENTOS_TEST.dockerfile index 55271d2755..418b28b086 100644 --- a/docker/CENTOS_TEST.dockerfile +++ b/docker/CENTOS_TEST.dockerfile @@ -11,11 +11,11 @@ ENV CXX=/usr/bin/g++ RUN git submodule update --init --recursive -- test/googletest -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download -RUN tar -xjf boost_1_79_0.tar.bz2 +RUN tar -xjf boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="boost_1_79_0" +ENV BOOST_ROOT="boost_1_86_0" WORKDIR /ngen diff --git a/docker/CENTOS_latest_NGEN_RUN.dockerfile b/docker/CENTOS_latest_NGEN_RUN.dockerfile index c53a46572e..11963f5209 100644 --- a/docker/CENTOS_latest_NGEN_RUN.dockerfile +++ b/docker/CENTOS_latest_NGEN_RUN.dockerfile @@ -13,11 +13,11 @@ ENV CXX=/usr/bin/g++ RUN git submodule update --init --recursive -- test/googletest -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download -RUN tar -xjf boost_1_79_0.tar.bz2 +RUN tar -xjf boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="boost_1_79_0" +ENV BOOST_ROOT="boost_1_86_0" WORKDIR /ngen diff --git a/docker/RHEL_TEST.dockerfile b/docker/RHEL_TEST.dockerfile index 9fe3550639..3103bd8464 100644 --- a/docker/RHEL_TEST.dockerfile +++ b/docker/RHEL_TEST.dockerfile @@ -10,11 +10,11 @@ ENV CXX=/usr/bin/g++ RUN git submodule update --init --recursive -- test/googletest -RUN curl -L -o boost_1_79_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.79.0/boost_1_79_0.tar.bz2/download +RUN curl -L -o boost_1_86_0.tar.bz2 https://sourceforge.net/projects/boost/files/boost/1.86.0/boost_1_86_0.tar.bz2/download -RUN tar -xjf boost_1_79_0.tar.bz2 +RUN tar -xjf boost_1_86_0.tar.bz2 -ENV BOOST_ROOT="boost_1_79_0" +ENV BOOST_ROOT="boost_1_86_0" WORKDIR /ngen diff --git a/docker/ngen.dockerfile b/docker/ngen.dockerfile index 88d880b98f..2d50a7c1a9 100644 --- a/docker/ngen.dockerfile +++ b/docker/ngen.dockerfile @@ -7,7 +7,7 @@ RUN dnf update -y \ && dnf install -y --allowerasing tar git gcc-c++ gcc make cmake udunits2-devel coreutils \ && dnf clean all -ARG BOOST_VERSION="1.79.0" +ARG BOOST_VERSION="1.86.0" RUN export BOOST_ARCHIVE="boost_$(echo ${BOOST_VERSION} | tr '\.' '_').tar.gz" \ && export BOOST_URL="https://sourceforge.net/projects/boost/files/boost/${BOOST_VERSION}/${BOOST_ARCHIVE}/download" \ && cd / \ diff --git a/extern/iso_c_fortran_bmi/CMakeLists.txt b/extern/iso_c_fortran_bmi/CMakeLists.txt index b55642e83f..9c1a17b2a8 100644 --- a/extern/iso_c_fortran_bmi/CMakeLists.txt +++ b/extern/iso_c_fortran_bmi/CMakeLists.txt @@ -33,5 +33,5 @@ install(TARGETS iso_c_bmi install(DIRECTORY ${CMAKE_Fortran_MODULE_DIRECTORY} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) configure_file(iso_c_bmi.pc.in iso_c_bmi.pc @ONLY) -install(FILES ${CMAKE_BINARY_DIR}/iso_c_bmi.pc +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/iso_c_bmi.pc DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) diff --git a/extern/test_bmi_c/include/bmi_test_bmi_c.h b/extern/test_bmi_c/include/bmi_test_bmi_c.h index 66a24a1572..3d4f40b4f1 100644 --- a/extern/test_bmi_c/include/bmi_test_bmi_c.h +++ b/extern/test_bmi_c/include/bmi_test_bmi_c.h @@ -1,6 +1,11 @@ #ifndef BMI_TEST_BMI_C_H #define BMI_TEST_BMI_C_H +#define NGEN_MASS_IN "ngen::mass_in" +#define NGEN_MASS_OUT "ngen::mass_out" +#define NGEN_MASS_STORED "ngen::mass_stored" +#define NGEN_MASS_LEAKED "ngen::mass_leaked" + #if defined(__cplusplus) extern "C" { #endif diff --git a/extern/test_bmi_c/include/test_bmi_c.h b/extern/test_bmi_c/include/test_bmi_c.h index fa990d1ebb..1e438259eb 100644 --- a/extern/test_bmi_c/include/test_bmi_c.h +++ b/extern/test_bmi_c/include/test_bmi_c.h @@ -31,6 +31,9 @@ struct test_bmi_c_model { int param_var_1; double param_var_2; double* param_var_3; + + double mass_stored; // Mass balance variable, for testing purposes + double mass_leaked; //Mass balance variable, for testing purposes }; typedef struct test_bmi_c_model test_bmi_c_model; diff --git a/extern/test_bmi_c/src/bmi_test_bmi_c.c b/extern/test_bmi_c/src/bmi_test_bmi_c.c index 187ca666cd..dc9499647e 100644 --- a/extern/test_bmi_c/src/bmi_test_bmi_c.c +++ b/extern/test_bmi_c/src/bmi_test_bmi_c.c @@ -9,6 +9,7 @@ #define INPUT_VAR_NAME_COUNT 2 #define OUTPUT_VAR_NAME_COUNT 2 #define PARAM_VAR_NAME_COUNT 3 +#define MASS_BALANCE_VAR_NAME_COUNT 4 // Don't forget to update Get_value/Get_value_at_indices (and setter) implementation if these are adjusted static const char *output_var_names[OUTPUT_VAR_NAME_COUNT] = { "OUTPUT_VAR_1", "OUTPUT_VAR_2" }; @@ -34,6 +35,13 @@ static const int param_var_item_count[PARAM_VAR_NAME_COUNT] = { 1, 1, 2 }; static const char *param_var_grids[PARAM_VAR_NAME_COUNT] = { 0, 0, 0 }; static const char *param_var_locations[PARAM_VAR_NAME_COUNT] = { "node", "node", "node" }; +static const char *mass_balance_var_names[MASS_BALANCE_VAR_NAME_COUNT] = { NGEN_MASS_IN, NGEN_MASS_OUT, NGEN_MASS_STORED, NGEN_MASS_LEAKED}; +static const char *mass_balance_var_types[MASS_BALANCE_VAR_NAME_COUNT] = { "double", "double", "double", "double"}; +static const char *mass_balance_var_units[MASS_BALANCE_VAR_NAME_COUNT] = { "m", "m", "m", "m" }; +static const int mass_balance_var_item_count[MASS_BALANCE_VAR_NAME_COUNT] = { 1, 1, 1, 1}; +static const char *mass_balance_var_grids[MASS_BALANCE_VAR_NAME_COUNT] = { 0, 0, 0, 0 }; +static const char *mass_balance_var_locations[MASS_BALANCE_VAR_NAME_COUNT] = { "node", "node", "node", "node" }; + static int Finalize (Bmi *self) { // Function assumes everything that is needed is retrieved from the model before Finalize is called. @@ -387,6 +395,23 @@ static int Get_value_ptr (Bmi *self, const char *name, void **dest) *dest = ((test_bmi_c_model *)(self->data))->param_var_3; return BMI_SUCCESS; } + + if (strcmp (name, NGEN_MASS_IN) == 0) { + *dest = ((test_bmi_c_model *)(self->data))->input_var_1; + return BMI_SUCCESS; + } + if (strcmp (name, NGEN_MASS_OUT) == 0) { + *dest = ((test_bmi_c_model *)(self->data))->output_var_1; + return BMI_SUCCESS; + } + if (strcmp (name, NGEN_MASS_STORED) == 0) { + *dest = &((test_bmi_c_model *)(self->data))->mass_stored; + return BMI_SUCCESS; + } + if (strcmp (name, NGEN_MASS_LEAKED) == 0) { + *dest = &((test_bmi_c_model *)(self->data))->mass_leaked; + return BMI_SUCCESS; + } return BMI_FAILURE; } @@ -483,6 +508,14 @@ static int Get_var_nbytes (Bmi *self, const char *name, int * nbytes) } } } + if (item_count < 1) { + for (i = 0; i < MASS_BALANCE_VAR_NAME_COUNT; i++) { + if (strcmp(name, mass_balance_var_names[i]) == 0) { + item_count = mass_balance_var_item_count[i]; + break; + } + } + } if (item_count < 1) item_count = ((test_bmi_c_model *) self->data)->num_time_steps; @@ -515,6 +548,13 @@ static int Get_var_type (Bmi *self, const char *name, char * type) return BMI_SUCCESS; } } + // Finally check to see if in mass balance array + for (i = 0; i < MASS_BALANCE_VAR_NAME_COUNT; i++) { + if (strcmp(name, mass_balance_var_names[i]) == 0) { + snprintf(type, BMI_MAX_TYPE_NAME, "%s", mass_balance_var_types[i]); + return BMI_SUCCESS; + } + } // If we get here, it means the variable name wasn't recognized type[0] = '\0'; return BMI_FAILURE; @@ -538,6 +578,13 @@ static int Get_var_units (Bmi *self, const char *name, char * units) return BMI_SUCCESS; } } + //Check for mass balance + for (i = 0; i < MASS_BALANCE_VAR_NAME_COUNT; i++) { + if (strcmp(name, mass_balance_var_names[i]) == 0) { + snprintf(units, BMI_MAX_UNITS_NAME, "%s", mass_balance_var_units[i]); + return BMI_SUCCESS; + } + } // If we get here, it means the variable name wasn't recognized units[0] = '\0'; return BMI_FAILURE; @@ -560,6 +607,9 @@ static int Initialize (Bmi *self, const char *file) else model = (test_bmi_c_model *) self->data; + model->mass_stored = 0.0; + model->mass_leaked = 0.0; + if (read_init_config(file, model) == BMI_FAILURE) return BMI_FAILURE; diff --git a/extern/test_bmi_c/src/test_bmi_c.c b/extern/test_bmi_c/src/test_bmi_c.c index c81d6b4406..d05dc50b3f 100644 --- a/extern/test_bmi_c/src/test_bmi_c.c +++ b/extern/test_bmi_c/src/test_bmi_c.c @@ -19,5 +19,7 @@ extern int run(test_bmi_c_model* model, long dt) } model->current_model_time += (double)dt; + model->mass_stored = *model->output_var_1 - *model->input_var_1; + model->mass_leaked = 0; return 0; } \ No newline at end of file diff --git a/extern/test_bmi_cpp/include/test_bmi_cpp.hpp b/extern/test_bmi_cpp/include/test_bmi_cpp.hpp index 4b2c31a429..2200eaa20c 100644 --- a/extern/test_bmi_cpp/include/test_bmi_cpp.hpp +++ b/extern/test_bmi_cpp/include/test_bmi_cpp.hpp @@ -25,6 +25,11 @@ #define BMI_TYPE_NAME_SHORT "short" #define BMI_TYPE_NAME_LONG "long" +#define NGEN_MASS_IN "ngen::mass_in" +#define NGEN_MASS_OUT "ngen::mass_out" +#define NGEN_MASS_STORED "ngen::mass_stored" +#define NGEN_MASS_LEAKED "ngen::mass_leaked" + class TestBmiCpp : public bmi::Bmi { public: /** @@ -179,6 +184,11 @@ class TestBmiCpp : public bmi::Bmi { std::vector output_var_locations = { "node", "node" }; std::vector model_var_locations = {}; + std::vector mass_balance_var_names = { NGEN_MASS_IN, NGEN_MASS_OUT, NGEN_MASS_STORED, NGEN_MASS_LEAKED}; + std::vector mass_balance_var_types = { "double", "double", "double", "double"}; + std::vector mass_balance_var_units = { "m", "m", "m", "m" }; + std::vector mass_balance_var_locations = { "node", "node", "node", "node"}; + std::vector input_var_item_count = { 1, 1 }; std::vector output_var_item_count = { 1, 1 }; std::vector model_var_item_count = {}; @@ -223,6 +233,9 @@ class TestBmiCpp : public bmi::Bmi { std::unique_ptr model_var_1 = nullptr; std::unique_ptr model_var_2 = nullptr; + double mass_stored = 0.0; + double mass_leaked = 0.0; + /** * Read the BMI initialization config file and use its contents to set the state of the model. * diff --git a/extern/test_bmi_cpp/src/test_bmi_cpp.cpp b/extern/test_bmi_cpp/src/test_bmi_cpp.cpp index 32536e0c75..57019839fb 100644 --- a/extern/test_bmi_cpp/src/test_bmi_cpp.cpp +++ b/extern/test_bmi_cpp/src/test_bmi_cpp.cpp @@ -165,6 +165,19 @@ void* TestBmiCpp::GetValuePtr(std::string name){ } } + if (name == NGEN_MASS_STORED) { + return &this->mass_stored; + } + if (name == NGEN_MASS_LEAKED) { + return &this->mass_leaked; + } + if (name == NGEN_MASS_IN) { + return this->input_var_1.get(); + } + if (name == NGEN_MASS_OUT) { + return this->output_var_1.get(); + } + throw std::runtime_error("GetValuePtr called for unknown variable: "+name); } @@ -212,6 +225,10 @@ int TestBmiCpp::GetVarNbytes(std::string name){ if(iter != this->model_var_names.end()){ item_count = this->model_var_item_count[iter - this->model_var_names.begin()]; } + iter = std::find(this->mass_balance_var_names.begin(), this->mass_balance_var_names.end(), name); + if(iter != this->mass_balance_var_names.end()){ + item_count = 1; + } if(item_count == -1){ // This is probably impossible to reach--the same conditions above failing will cause a throw // in GetVarItemSize --> GetVarType (called earlier) instead. @@ -233,6 +250,10 @@ std::string TestBmiCpp::GetVarType(std::string name){ if(iter != this->model_var_names.end()){ return this->model_var_types[iter - this->model_var_names.begin()]; } + iter = std::find(this->mass_balance_var_names.begin(), this->mass_balance_var_names.end(), name); + if(iter != this->mass_balance_var_names.end()){ + return this->mass_balance_var_types[iter - this->mass_balance_var_names.begin()]; + } throw std::runtime_error("GetVarType called for non-existent variable: "+name+"" SOURCE_LOC ); } @@ -249,6 +270,10 @@ std::string TestBmiCpp::GetVarUnits(std::string name){ if(iter != this->model_var_names.end()){ return this->model_var_types[iter - this->model_var_names.begin()]; } + iter = std::find(this->mass_balance_var_names.begin(), this->mass_balance_var_names.end(), name); + if(iter != this->mass_balance_var_names.end()){ + return this->mass_balance_var_units[iter - this->mass_balance_var_names.begin()]; + } throw std::runtime_error("GetVarUnits called for non-existent variable: "+name+"" SOURCE_LOC); } @@ -517,4 +542,6 @@ void TestBmiCpp::run(long dt) *this->output_var_5 = *this->model_var_2 * 1.0; } this->current_model_time += (double)dt; + this->mass_stored = *this->output_var_1 - *this->input_var_1; + this->mass_leaked = 0; } diff --git a/include/core/nexus/HY_PointHydroNexusRemote.hpp b/include/core/nexus/HY_PointHydroNexusRemote.hpp index 34d98f9a96..ebac2e9ace 100644 --- a/include/core/nexus/HY_PointHydroNexusRemote.hpp +++ b/include/core/nexus/HY_PointHydroNexusRemote.hpp @@ -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/realizations/catchment/Bmi_Module_Formulation.hpp b/include/realizations/catchment/Bmi_Module_Formulation.hpp index 139ee1412f..cd6dda4969 100644 --- a/include/realizations/catchment/Bmi_Module_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Module_Formulation.hpp @@ -10,6 +10,7 @@ #include #include +#include "bmi/protocols.hpp" using data_access::MEAN; using data_access::SUM; @@ -294,6 +295,12 @@ namespace realization { void free_serialization_state() const; 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 { + //Create the protocol context, each member is const, and cannot change during the check + models::bmi::protocols::Context ctx{iteration, total_steps, timestamp, id}; + bmi_protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, ctx); + } + protected: /** @@ -507,6 +514,7 @@ namespace realization { bool is_realization_legacy_format() const; private: + models::bmi::protocols::NgenBmiProtocols bmi_protocols; /** * Whether model ``Update`` calls are allowed and handled in some way by the backing model for time steps after * the model's ``end_time``. diff --git a/include/realizations/catchment/Bmi_Multi_Formulation.hpp b/include/realizations/catchment/Bmi_Multi_Formulation.hpp index 2ecaac5867..64ee733db0 100644 --- a/include/realizations/catchment/Bmi_Multi_Formulation.hpp +++ b/include/realizations/catchment/Bmi_Multi_Formulation.hpp @@ -47,6 +47,15 @@ namespace realization { virtual ~Bmi_Multi_Formulation() {}; + virtual void check_mass_balance(const int& iteration, const int& total_steps, const std::string& timestamp) const final { + for( const auto &module : modules ) { + // TODO may need to check on outputs form each module indepdently??? + // Right now, the assumption is that if each component is mass balanced + // then the entire formulation is mass balanced + module->check_mass_balance(iteration, total_steps, timestamp); + } + }; + /** * Convert a time value from the model to an epoch time in seconds. * diff --git a/include/realizations/catchment/Formulation.hpp b/include/realizations/catchment/Formulation.hpp index 44ff6b8af8..ad8c6f097c 100644 --- a/include/realizations/catchment/Formulation.hpp +++ b/include/realizations/catchment/Formulation.hpp @@ -45,6 +45,7 @@ namespace realization { virtual void create_formulation(boost::property_tree::ptree &config, geojson::PropertyMap *global = nullptr) = 0; virtual void create_formulation(geojson::PropertyMap properties) = 0; + virtual void check_mass_balance(const int& iteration, const int& total_steps, const std::string& timestamp) const = 0; protected: virtual const std::vector& get_required_parameters() const = 0; diff --git a/include/utilities/bmi/mass_balance.hpp b/include/utilities/bmi/mass_balance.hpp new file mode 100644 index 0000000000..e8883232ff --- /dev/null +++ b/include/utilities/bmi/mass_balance.hpp @@ -0,0 +1,155 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ +Version 0.3 +Implement is_supported() +Re-align members for more better memory layout/padding +Update docstrings + +Version 0.2 +Conform to updated protocol interface +Removed integration and error exceptions in favor of ProtocolError + +Version 0.1 +Interface of the BMI mass balance protocol +*/ +#pragma once + + +#include +#include +#include +#include + +namespace models{ namespace bmi{ namespace protocols{ + using nonstd::expected; + /** Mass balance variable names **/ + constexpr const char* const INPUT_MASS_NAME = "ngen::mass_in"; + constexpr const char* const OUTPUT_MASS_NAME = "ngen::mass_out"; + constexpr const char* const STORED_MASS_NAME = "ngen::mass_stored"; + constexpr const char* const LEAKED_MASS_NAME = "ngen::mass_leaked"; + + /** Configuration keys for defining configurable properties of the protocol */ + //The top level object key which will contain the map of configuration options + constexpr const char* const CONFIGURATION_KEY = "mass_balance"; + //Configuration option keys + constexpr const char* const TOLERANCE_KEY = "tolerance"; + constexpr const char* const FATAL_KEY = "fatal"; + constexpr const char* const CHECK_KEY = "check"; + constexpr const char* const FREQUENCY_KEY = "frequency"; + + class NgenMassBalance : public NgenBmiProtocol { + /** @brief Mass Balance protocol + * + * This protocol `run()`s a simple mass balance calculation by querying the model for a + * set of mass balance state variables and computing the basic mass balance as + * balance = mass_in - mass_out - mass_stored - mass_leaked. It is then checked against + * a tolerance value to determine if the mass balance is acceptable. + */ + public: + + /** @brief Constructor for the NgenMassBalance protocol + * + * This constructor initializes the mass balance protocol with the given model and properties. + * + * @param model A shared pointer to a Bmi_Adapter object which should be + * initialized before being passed to this constructor. + */ + NgenMassBalance(const ModelPtr& model, const Properties& properties); + + /** + * @brief Construct a new, default Ngen Mass Balance object + * + * By default, the protocol is considered unsupported and won't be checked + */ + NgenMassBalance(); + + virtual ~NgenMassBalance() override; + + private: + + /** + * @brief Run the mass balance protocol + * + * If the configured frequency is -1, the mass balance will only be checked at the end + * of the simulation. If the frequency is greater than 0, the mass balance will be checked + * at the specified frequency based on the current_time_step and the total_steps provided + * in the Context. + * + * Warns or errors at each check if total mass balance is not within the configured + * acceptable tolerance. + * + * @return expected May contain a ProtocolError if + * the protocol fails for any reason. Errors of ProtocolError::PROTOCOL_WARNING + * severity should be logged as warnings, but not cause the simulation to fail. + */ + auto run(const ModelPtr& model, const Context& ctx) const -> expected override; + + /** + * @brief Check if the mass balance protocol is supported by the model + * + * @return expected May contain a ProtocolError if + * the protocol is not supported by the model. + */ + nsel_NODISCARD auto check_support(const ModelPtr& model) -> expected override; + + /** + * @brief Check the model for support and initialize the mass balance protocol from the given properties. + * + * If the model does not support the mass balance protocol, an exception will be thrown, and no mass balance + * will performed when `run()` is called. + * + * A private initialize call is used since it only makes sense to check/run the protocol + * once the model adapter is fully constructed. This should be called by the owner of the + * NgenMassBalance instance once the model is ready. + * + * @param properties Configurable key/value properties for the mass balance protocol. + * If the map contains "mass_balance" object, then the following properties + * are used to configure the protocol: + * tolerance: double, default 1.0E-16. + * check: bool, default true. Whether to perform mass balance check. + * frequency: int, default 1. How often (in time steps) to check mass balance. + * fatal: bool, default false. Whether to treat mass balance errors as fatal. + * Otherwise, mass balance checking will be disabled (check will be false) + * + * @return expected May contain a ProtocolError if + * initialization fails for any reason, since the protocol must + * be effectively "optional", failed initialization results in + * the protocol being disabled for the duration of the simulation. + */ + auto initialize(const ModelPtr& model, const Properties& properties) -> expected override; + + /** + * @brief Whether the protocol is supported by the model + * + * @return true the model exposes the required mass balance variables + * @return false the model does not support mass balance checking via this protocol + */ + bool is_supported() const override final; + + private: + double tolerance; + // How often (in time steps) to check mass balance + int frequency; + // Whether the protocol is supported by the model, false by default + bool supported = false; + // Configurable options/values + bool check; + bool is_fatal; + }; + +}}} + diff --git a/include/utilities/bmi/nonstd/LICENSE.txt b/include/utilities/bmi/nonstd/LICENSE.txt new file mode 100644 index 0000000000..36b7cd93cd --- /dev/null +++ b/include/utilities/bmi/nonstd/LICENSE.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/include/utilities/bmi/nonstd/expected.hpp b/include/utilities/bmi/nonstd/expected.hpp new file mode 100644 index 0000000000..ae1790d131 --- /dev/null +++ b/include/utilities/bmi/nonstd/expected.hpp @@ -0,0 +1,3637 @@ +// Vendored from https://github.com/martinmoene/expected-lite/commit/a7510b213a668306fb038c934e27e53cc01141d4 +// This version targets C++11 and later. +// +// Copyright (C) 2016-2025 Martin Moene. +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// expected lite is based on: +// A proposal to add a utility class to represent expected monad +// by Vicente J. Botet Escriba and Pierre Talbot. http:://wg21.link/p0323 + +#ifndef NONSTD_EXPECTED_LITE_HPP +#define NONSTD_EXPECTED_LITE_HPP + +#define expected_lite_MAJOR 0 +#define expected_lite_MINOR 9 +#define expected_lite_PATCH 0 + +#define expected_lite_VERSION expected_STRINGIFY(expected_lite_MAJOR) "." expected_STRINGIFY(expected_lite_MINOR) "." expected_STRINGIFY(expected_lite_PATCH) + +#define expected_STRINGIFY( x ) expected_STRINGIFY_( x ) +#define expected_STRINGIFY_( x ) #x + +// expected-lite configuration: + +#define nsel_EXPECTED_DEFAULT 0 +#define nsel_EXPECTED_NONSTD 1 +#define nsel_EXPECTED_STD 2 + +// tweak header support: + +#ifdef __has_include +# if __has_include() +# include +# endif +#define expected_HAVE_TWEAK_HEADER 1 +#else +#define expected_HAVE_TWEAK_HEADER 0 +//# pragma message("expected.hpp: Note: Tweak header not supported.") +#endif + +// expected selection and configuration: + +#if !defined( nsel_CONFIG_SELECT_EXPECTED ) +# define nsel_CONFIG_SELECT_EXPECTED ( nsel_HAVE_STD_EXPECTED ? nsel_EXPECTED_STD : nsel_EXPECTED_NONSTD ) +#endif + +// Proposal revisions: +// +// DXXXXR0: -- +// N4015 : -2 (2014-05-26) +// N4109 : -1 (2014-06-29) +// P0323R0: 0 (2016-05-28) +// P0323R1: 1 (2016-10-12) +// -------: +// P0323R2: 2 (2017-06-15) +// P0323R3: 3 (2017-10-15) +// P0323R4: 4 (2017-11-26) +// P0323R5: 5 (2018-02-08) +// P0323R6: 6 (2018-04-02) +// P0323R7: 7 (2018-06-22) * +// +// expected-lite uses 2 and higher + +#ifndef nsel_P0323R +# define nsel_P0323R 7 +#endif + +// Monadic operations proposal revisions: +// +// P2505R0: 0 (2021-12-12) +// P2505R1: 1 (2022-02-10) +// P2505R2: 2 (2022-04-15) +// P2505R3: 3 (2022-06-05) +// P2505R4: 4 (2022-06-15) +// P2505R5: 5 (2022-09-20) * +// +// expected-lite uses 5 + +#ifndef nsel_P2505R +# define nsel_P2505R 5 +#endif + +// Lean and mean inclusion of Windows.h, if applicable; default on for MSVC: + +#if !defined(nsel_CONFIG_WIN32_LEAN_AND_MEAN) && defined(_MSC_VER) +# define nsel_CONFIG_WIN32_LEAN_AND_MEAN 1 +#else +# define nsel_CONFIG_WIN32_LEAN_AND_MEAN 0 +#endif + +// Control marking class expected with [[nodiscard]]]: + +#if !defined(nsel_CONFIG_NO_NODISCARD) +# define nsel_CONFIG_NO_NODISCARD 0 +#else +# define nsel_CONFIG_NO_NODISCARD 1 +#endif + +// Control presence of C++ exception handling (try and auto discover): + +#ifndef nsel_CONFIG_NO_EXCEPTIONS +# if defined(_MSC_VER) +# include // for _HAS_EXCEPTIONS +# endif +# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS) +# define nsel_CONFIG_NO_EXCEPTIONS 0 +# else +# define nsel_CONFIG_NO_EXCEPTIONS 1 +# endif +#endif + +// at default use SEH with MSVC for no C++ exceptions + +#if !defined(nsel_CONFIG_NO_EXCEPTIONS_SEH) && defined(_MSC_VER) +# define nsel_CONFIG_NO_EXCEPTIONS_SEH nsel_CONFIG_NO_EXCEPTIONS +#else +# define nsel_CONFIG_NO_EXCEPTIONS_SEH 0 +#endif + +// C++ language version detection (C++23 is speculative): +// Note: VC14.0/1900 (VS2015) lacks too much from C++14. + +#ifndef nsel_CPLUSPLUS +# if defined(_MSVC_LANG ) && !defined(__clang__) +# define nsel_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG ) +# else +# define nsel_CPLUSPLUS __cplusplus +# endif +#endif + +#define nsel_CPP98_OR_GREATER ( nsel_CPLUSPLUS >= 199711L ) +#define nsel_CPP11_OR_GREATER ( nsel_CPLUSPLUS >= 201103L ) +#define nsel_CPP14_OR_GREATER ( nsel_CPLUSPLUS >= 201402L ) +#define nsel_CPP17_OR_GREATER ( nsel_CPLUSPLUS >= 201703L ) +#define nsel_CPP20_OR_GREATER ( nsel_CPLUSPLUS >= 202002L ) +#define nsel_CPP23_OR_GREATER ( nsel_CPLUSPLUS >= 202300L ) + +// Use C++23 std::expected if available and requested: + +#if nsel_CPP23_OR_GREATER && defined(__has_include ) +# if __has_include( ) +# define nsel_HAVE_STD_EXPECTED 1 +# else +# define nsel_HAVE_STD_EXPECTED 0 +# endif +#else +# define nsel_HAVE_STD_EXPECTED 0 +#endif + +#define nsel_USES_STD_EXPECTED ( (nsel_CONFIG_SELECT_EXPECTED == nsel_EXPECTED_STD) || ((nsel_CONFIG_SELECT_EXPECTED == nsel_EXPECTED_DEFAULT) && nsel_HAVE_STD_EXPECTED) ) + +// +// in_place: code duplicated in any-lite, expected-lite, expected-lite, value-ptr-lite, variant-lite: +// + +#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES +#define nonstd_lite_HAVE_IN_PLACE_TYPES 1 + +// C++17 std::in_place in : + +#if nsel_CPP17_OR_GREATER + +#include + +namespace nonstd { + +using std::in_place; +using std::in_place_type; +using std::in_place_index; +using std::in_place_t; +using std::in_place_type_t; +using std::in_place_index_t; + +#define nonstd_lite_in_place_t( T) std::in_place_t +#define nonstd_lite_in_place_type_t( T) std::in_place_type_t +#define nonstd_lite_in_place_index_t(K) std::in_place_index_t + +#define nonstd_lite_in_place( T) std::in_place_t{} +#define nonstd_lite_in_place_type( T) std::in_place_type_t{} +#define nonstd_lite_in_place_index(K) std::in_place_index_t{} + +} // namespace nonstd + +#else // nsel_CPP17_OR_GREATER + +#include + +namespace nonstd { +namespace detail { + +template< class T > +struct in_place_type_tag {}; + +template< std::size_t K > +struct in_place_index_tag {}; + +} // namespace detail + +struct in_place_t {}; + +template< class T > +inline in_place_t in_place( detail::in_place_type_tag = detail::in_place_type_tag() ) +{ + return in_place_t(); +} + +template< std::size_t K > +inline in_place_t in_place( detail::in_place_index_tag = detail::in_place_index_tag() ) +{ + return in_place_t(); +} + +template< class T > +inline in_place_t in_place_type( detail::in_place_type_tag = detail::in_place_type_tag() ) +{ + return in_place_t(); +} + +template< std::size_t K > +inline in_place_t in_place_index( detail::in_place_index_tag = detail::in_place_index_tag() ) +{ + return in_place_t(); +} + +// mimic templated typedef: + +#define nonstd_lite_in_place_t( T) nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag ) +#define nonstd_lite_in_place_type_t( T) nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag ) +#define nonstd_lite_in_place_index_t(K) nonstd::in_place_t(&)( nonstd::detail::in_place_index_tag ) + +#define nonstd_lite_in_place( T) nonstd::in_place_type +#define nonstd_lite_in_place_type( T) nonstd::in_place_type +#define nonstd_lite_in_place_index(K) nonstd::in_place_index + +} // namespace nonstd + +#endif // nsel_CPP17_OR_GREATER +#endif // nonstd_lite_HAVE_IN_PLACE_TYPES + +// +// Using std::expected: +// + +#if nsel_USES_STD_EXPECTED + +#include + +namespace nonstd { + + using std::expected; + using std::unexpected; + using std::bad_expected_access; + using std::unexpect_t; + using std::unexpect; + + //[[deprecated("replace unexpected_type with unexpected")]] + + template< typename E > + using unexpected_type = unexpected; + + // Unconditionally provide make_unexpected(): + + template< typename E > + constexpr auto make_unexpected( E && value ) -> unexpected< typename std::decay::type > + { + return unexpected< typename std::decay::type >( std::forward(value) ); + } + + template + < + typename E, typename... Args, + typename = std::enable_if< + std::is_constructible::value + > + > + constexpr auto + make_unexpected( std::in_place_t inplace, Args &&... args ) -> unexpected_type< typename std::decay::type > + { + return unexpected_type< typename std::decay::type >( inplace, std::forward(args)...); + } +} // namespace nonstd + +#else // nsel_USES_STD_EXPECTED + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// additional includes: + +#if nsel_CONFIG_WIN32_LEAN_AND_MEAN +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +#endif + +#if nsel_CONFIG_NO_EXCEPTIONS +# if nsel_CONFIG_NO_EXCEPTIONS_SEH +# include // for ExceptionCodes +# else +// already included: +# endif +#else +# include +#endif + +// C++ feature usage: + +#if nsel_CPP11_OR_GREATER +# define nsel_constexpr constexpr +#else +# define nsel_constexpr /*constexpr*/ +#endif + +#if nsel_CPP14_OR_GREATER +# define nsel_constexpr14 constexpr +#else +# define nsel_constexpr14 /*constexpr*/ +#endif + +#if nsel_CPP17_OR_GREATER +# define nsel_inline17 inline +#else +# define nsel_inline17 /*inline*/ +#endif + +// Compiler versions: +// +// MSVC++ 6.0 _MSC_VER == 1200 nsel_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0) +// MSVC++ 7.0 _MSC_VER == 1300 nsel_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002) +// MSVC++ 7.1 _MSC_VER == 1310 nsel_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003) +// MSVC++ 8.0 _MSC_VER == 1400 nsel_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005) +// MSVC++ 9.0 _MSC_VER == 1500 nsel_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008) +// MSVC++ 10.0 _MSC_VER == 1600 nsel_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010) +// MSVC++ 11.0 _MSC_VER == 1700 nsel_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012) +// MSVC++ 12.0 _MSC_VER == 1800 nsel_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013) +// MSVC++ 14.0 _MSC_VER == 1900 nsel_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015) +// MSVC++ 14.1 _MSC_VER >= 1910 nsel_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017) +// MSVC++ 14.2 _MSC_VER >= 1920 nsel_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019) + +#if defined(_MSC_VER) && !defined(__clang__) +# define nsel_COMPILER_MSVC_VER (_MSC_VER ) +# define nsel_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * ( 5 + (_MSC_VER < 1900)) ) +#else +# define nsel_COMPILER_MSVC_VER 0 +# define nsel_COMPILER_MSVC_VERSION 0 +#endif + +#define nsel_COMPILER_VERSION( major, minor, patch ) ( 10 * ( 10 * (major) + (minor) ) + (patch) ) + +#if defined(__clang__) +# define nsel_COMPILER_CLANG_VERSION nsel_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__) +#else +# define nsel_COMPILER_CLANG_VERSION 0 +#endif + +#if defined(__GNUC__) && !defined(__clang__) +# define nsel_COMPILER_GNUC_VERSION nsel_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#else +# define nsel_COMPILER_GNUC_VERSION 0 +#endif + +// half-open range [lo..hi): +//#define nsel_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) ) + +// Method enabling + +#define nsel_REQUIRES_0(...) \ + template< bool B = (__VA_ARGS__), typename std::enable_if::type = 0 > + +#define nsel_REQUIRES_T(...) \ + , typename std::enable_if< (__VA_ARGS__), int >::type = 0 + +#define nsel_REQUIRES_R(R, ...) \ + typename std::enable_if< (__VA_ARGS__), R>::type + +#define nsel_REQUIRES_A(...) \ + , typename std::enable_if< (__VA_ARGS__), void*>::type = nullptr + +// Clang, GNUC, MSVC warning suppression macros: + +#ifdef __clang__ +# pragma clang diagnostic push +#elif defined __GNUC__ +# pragma GCC diagnostic push +#endif // __clang__ + +#if nsel_COMPILER_MSVC_VERSION >= 140 +# define nsel_DISABLE_MSVC_WARNINGS(codes) __pragma( warning(push) ) __pragma( warning(disable: codes) ) +#else +# define nsel_DISABLE_MSVC_WARNINGS(codes) +#endif + +#ifdef __clang__ +# define nsel_RESTORE_WARNINGS() _Pragma("clang diagnostic pop") +# define nsel_RESTORE_MSVC_WARNINGS() +#elif defined __GNUC__ +# define nsel_RESTORE_WARNINGS() _Pragma("GCC diagnostic pop") +# define nsel_RESTORE_MSVC_WARNINGS() +#elif nsel_COMPILER_MSVC_VERSION >= 140 +# define nsel_RESTORE_WARNINGS() __pragma( warning( pop ) ) +# define nsel_RESTORE_MSVC_WARNINGS() nsel_RESTORE_WARNINGS() +#else +# define nsel_RESTORE_WARNINGS() +# define nsel_RESTORE_MSVC_WARNINGS() +#endif + +// Suppress the following MSVC (GSL) warnings: +// - C26409: Avoid calling new and delete explicitly, use std::make_unique instead (r.11) + +nsel_DISABLE_MSVC_WARNINGS( 26409 ) + +// Presence of language and library features: + +#ifdef _HAS_CPP0X +# define nsel_HAS_CPP0X _HAS_CPP0X +#else +# define nsel_HAS_CPP0X 0 +#endif + +// Presence of language and library features: + +#define nsel_CPP11_000 (nsel_CPP11_OR_GREATER) +#define nsel_CPP17_000 (nsel_CPP17_OR_GREATER) + +// Presence of C++11 library features: + +#define nsel_HAVE_ADDRESSOF nsel_CPP11_000 + +// Presence of C++17 language features: + +#define nsel_HAVE_DEPRECATED nsel_CPP17_000 +#define nsel_HAVE_NODISCARD nsel_CPP17_000 + +// C++ feature usage: + +#if nsel_HAVE_DEPRECATED +# define nsel_deprecated(msg) [[deprecated(msg)]] +#else +# define nsel_deprecated(msg) /*[[deprecated]]*/ +#endif + +#if nsel_HAVE_NODISCARD && !nsel_CONFIG_NO_NODISCARD +# define nsel_NODISCARD [[nodiscard]] +#else +# define nsel_NODISCARD /*[[nodiscard]]*/ +#endif + +// +// expected: +// + +namespace nonstd { namespace expected_lite { + +// library features C++11: + +namespace std11 { + +// #if 0 && nsel_HAVE_ADDRESSOF +#if nsel_HAVE_ADDRESSOF + using std::addressof; +#else + template< class T > + T * addressof( T & arg ) noexcept + { + return &arg; + } + + template< class T > + const T * addressof( const T && ) = delete; +#endif +} // namespace std11 + +// type traits C++17: + +namespace std17 { + +#if nsel_CPP17_OR_GREATER + +using std::conjunction; +using std::is_swappable; +using std::is_nothrow_swappable; + +#else // nsel_CPP17_OR_GREATER + +namespace detail { + +using std::swap; + +struct is_swappable +{ + template< typename T, typename = decltype( swap( std::declval(), std::declval() ) ) > + static std::true_type test( int /* unused */); + + template< typename > + static std::false_type test(...); +}; + +struct is_nothrow_swappable +{ + // wrap noexcept(expr) in separate function as work-around for VC140 (VS2015): + + template< typename T > + static constexpr bool satisfies() + { + return noexcept( swap( std::declval(), std::declval() ) ); + } + + template< typename T > + static auto test( int ) -> std::integral_constant()>{} + + template< typename > + static auto test(...) -> std::false_type; +}; +} // namespace detail + +// is [nothrow] swappable: + +template< typename T > +struct is_swappable : decltype( detail::is_swappable::test(0) ){}; + +template< typename T > +struct is_nothrow_swappable : decltype( detail::is_nothrow_swappable::test(0) ){}; + +// conjunction: + +template< typename... > struct conjunction : std::true_type{}; +template< typename B1 > struct conjunction : B1{}; + +template< typename B1, typename... Bn > +struct conjunction : std::conditional, B1>::type{}; + +#endif // nsel_CPP17_OR_GREATER + +} // namespace std17 + +// type traits C++20: + +namespace std20 { + +#if defined(__cpp_lib_remove_cvref) + +using std::remove_cvref; + +#else + +template< typename T > +struct remove_cvref +{ + typedef typename std::remove_cv< typename std::remove_reference::type >::type type; +}; + +#endif + +} // namespace std20 + +// forward declaration: + +template< typename T, typename E > +class expected; + +namespace detail { + +#if nsel_P2505R >= 3 +template< typename T > +struct is_expected : std::false_type {}; + +template< typename T, typename E > +struct is_expected< expected< T, E > > : std::true_type {}; +#endif // nsel_P2505R >= 3 + +/// discriminated union to hold value or 'error'. + +template< typename T, typename E > +class storage_t_noncopy_nonmove_impl +{ + template< typename, typename > friend class nonstd::expected_lite::expected; + +public: + using value_type = T; + using error_type = E; + + // no-op construction + storage_t_noncopy_nonmove_impl() {} + ~storage_t_noncopy_nonmove_impl() {} + + explicit storage_t_noncopy_nonmove_impl( bool has_value ) + : m_has_value( has_value ) + {} + + void construct_value() + { + new( std11::addressof(m_value) ) value_type(); + } + + // void construct_value( value_type const & e ) + // { + // new( std11::addressof(m_value) ) value_type( e ); + // } + + // void construct_value( value_type && e ) + // { + // new( std11::addressof(m_value) ) value_type( std::move( e ) ); + // } + + template< class... Args > + void emplace_value( Args&&... args ) + { + new( std11::addressof(m_value) ) value_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_value( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_value) ) value_type( il, std::forward(args)... ); + } + + void destruct_value() + { + m_value.~value_type(); + } + + // void construct_error( error_type const & e ) + // { + // // new( std11::addressof(m_error) ) error_type( e ); + // } + + // void construct_error( error_type && e ) + // { + // // new( std11::addressof(m_error) ) error_type( std::move( e ) ); + // } + + template< class... Args > + void emplace_error( Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_error( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( il, std::forward(args)... ); + } + + void destruct_error() + { + m_error.~error_type(); + } + + constexpr value_type const & value() const & + { + return m_value; + } + + value_type & value() & + { + return m_value; + } + + constexpr value_type const && value() const && + { + return std::move( m_value ); + } + + nsel_constexpr14 value_type && value() && + { + return std::move( m_value ); + } + + value_type const * value_ptr() const + { + return std11::addressof(m_value); + } + + value_type * value_ptr() + { + return std11::addressof(m_value); + } + + error_type const & error() const & + { + return m_error; + } + + error_type & error() & + { + return m_error; + } + + constexpr error_type const && error() const && + { + return std::move( m_error ); + } + + nsel_constexpr14 error_type && error() && + { + return std::move( m_error ); + } + + bool has_value() const + { + return m_has_value; + } + + void set_has_value( bool v ) + { + m_has_value = v; + } + +private: + union + { + value_type m_value; + error_type m_error; + }; + + bool m_has_value = false; +}; + +template< typename T, typename E > +class storage_t_impl +{ + template< typename, typename > friend class nonstd::expected_lite::expected; + +public: + using value_type = T; + using error_type = E; + + // no-op construction + storage_t_impl() {} + ~storage_t_impl() {} + + explicit storage_t_impl( bool has_value ) + : m_has_value( has_value ) + {} + + void construct_value() + { + new( std11::addressof(m_value) ) value_type(); + } + + void construct_value( value_type const & e ) + { + new( std11::addressof(m_value) ) value_type( e ); + } + + void construct_value( value_type && e ) + { + new( std11::addressof(m_value) ) value_type( std::move( e ) ); + } + + template< class... Args > + void emplace_value( Args&&... args ) + { + new( std11::addressof(m_value) ) value_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_value( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_value) ) value_type( il, std::forward(args)... ); + } + + void destruct_value() + { + m_value.~value_type(); + } + + void construct_error( error_type const & e ) + { + new( std11::addressof(m_error) ) error_type( e ); + } + + void construct_error( error_type && e ) + { + new( std11::addressof(m_error) ) error_type( std::move( e ) ); + } + + template< class... Args > + void emplace_error( Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_error( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( il, std::forward(args)... ); + } + + void destruct_error() + { + m_error.~error_type(); + } + + constexpr value_type const & value() const & + { + return m_value; + } + + value_type & value() & + { + return m_value; + } + + constexpr value_type const && value() const && + { + return std::move( m_value ); + } + + nsel_constexpr14 value_type && value() && + { + return std::move( m_value ); + } + + value_type const * value_ptr() const + { + return std11::addressof(m_value); + } + + value_type * value_ptr() + { + return std11::addressof(m_value); + } + + error_type const & error() const & + { + return m_error; + } + + error_type & error() & + { + return m_error; + } + + constexpr error_type const && error() const && + { + return std::move( m_error ); + } + + nsel_constexpr14 error_type && error() && + { + return std::move( m_error ); + } + + bool has_value() const + { + return m_has_value; + } + + void set_has_value( bool v ) + { + m_has_value = v; + } + +private: + union + { + value_type m_value; + error_type m_error; + }; + + bool m_has_value = false; +}; + +/// discriminated union to hold only 'error'. + +template< typename E > +struct storage_t_impl< void, E > +{ + template< typename, typename > friend class nonstd::expected_lite::expected; + +public: + using value_type = void; + using error_type = E; + + // no-op construction + storage_t_impl() {} + ~storage_t_impl() {} + + explicit storage_t_impl( bool has_value ) + : m_has_value( has_value ) + {} + + void construct_error( error_type const & e ) + { + new( std11::addressof(m_error) ) error_type( e ); + } + + void construct_error( error_type && e ) + { + new( std11::addressof(m_error) ) error_type( std::move( e ) ); + } + + template< class... Args > + void emplace_error( Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( std::forward(args)...); + } + + template< class U, class... Args > + void emplace_error( std::initializer_list il, Args&&... args ) + { + new( std11::addressof(m_error) ) error_type( il, std::forward(args)... ); + } + + void destruct_error() + { + m_error.~error_type(); + } + + error_type const & error() const & + { + return m_error; + } + + error_type & error() & + { + return m_error; + } + + constexpr error_type const && error() const && + { + return std::move( m_error ); + } + + nsel_constexpr14 error_type && error() && + { + return std::move( m_error ); + } + + bool has_value() const + { + return m_has_value; + } + + void set_has_value( bool v ) + { + m_has_value = v; + } + +private: + union + { + char m_dummy; + error_type m_error; + }; + + bool m_has_value = false; +}; + +template< typename T, typename E, bool isConstructable, bool isMoveable > +class storage_t +{ +public: +}; + +template< typename T, typename E > +class storage_t : public storage_t_noncopy_nonmove_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_noncopy_nonmove_impl( has_value ) + {} + + storage_t( storage_t const & other ) = delete; + storage_t( storage_t && other ) = delete; + +}; + +template< typename T, typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) this->construct_value( other.value() ); + else this->construct_error( other.error() ); + } + + storage_t(storage_t && other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) this->construct_value( std::move( other.value() ) ); + else this->construct_error( std::move( other.error() ) ); + } +}; + +template< typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) ; + else this->construct_error( other.error() ); + } + + storage_t(storage_t && other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) ; + else this->construct_error( std::move( other.error() ) ); + } +}; + +template< typename T, typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) + : storage_t_impl(other.has_value()) + { + if ( this->has_value() ) this->construct_value( other.value() ); + else this->construct_error( other.error() ); + } + + storage_t( storage_t && other ) = delete; +}; + +template< typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) + : storage_t_impl(other.has_value()) + { + if ( this->has_value() ) ; + else this->construct_error( other.error() ); + } + + storage_t( storage_t && other ) = delete; +}; + +template< typename T, typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) = delete; + + storage_t( storage_t && other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) this->construct_value( std::move( other.value() ) ); + else this->construct_error( std::move( other.error() ) ); + } +}; + +template< typename E > +class storage_t : public storage_t_impl +{ +public: + storage_t() = default; + ~storage_t() = default; + + explicit storage_t( bool has_value ) + : storage_t_impl( has_value ) + {} + + storage_t( storage_t const & other ) = delete; + + storage_t( storage_t && other ) + : storage_t_impl( other.has_value() ) + { + if ( this->has_value() ) ; + else this->construct_error( std::move( other.error() ) ); + } +}; + +#if nsel_P2505R >= 3 +// C++11 invoke implementation +template< typename > +struct is_reference_wrapper : std::false_type {}; +template< typename T > +struct is_reference_wrapper< std::reference_wrapper< T > > : std::true_type {}; + +template< typename FnT, typename ClassT, typename ObjectT, typename... Args + nsel_REQUIRES_T( + std::is_function::value + && ( std::is_same< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + || std::is_base_of< ClassT, typename std20::remove_cvref< ObjectT >::type >::value ) + ) +> +nsel_constexpr auto invoke_member_function_impl( FnT ClassT::* memfnptr, ObjectT && obj, Args && ... args ) + noexcept( noexcept( (std::forward< ObjectT >( obj ).*memfnptr)( std::forward< Args >( args )... ) ) ) + -> decltype( (std::forward< ObjectT >( obj ).*memfnptr)( std::forward< Args >( args )...) ) +{ + return (std::forward< ObjectT >( obj ).*memfnptr)( std::forward< Args >( args )... ); +} + +template< typename FnT, typename ClassT, typename ObjectT, typename... Args + nsel_REQUIRES_T( + std::is_function::value + && is_reference_wrapper< typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_function_impl( FnT ClassT::* memfnptr, ObjectT && obj, Args && ... args ) + noexcept( noexcept( (obj.get().*memfnptr)( std::forward< Args >( args ) ... ) ) ) + -> decltype( (obj.get().*memfnptr)( std::forward< Args >( args ) ... ) ) +{ + return (obj.get().*memfnptr)( std::forward< Args >( args ) ... ); +} + +template< typename FnT, typename ClassT, typename ObjectT, typename... Args + nsel_REQUIRES_T( + std::is_function::value + && !std::is_same< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + && !std::is_base_of< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + && !is_reference_wrapper< typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_function_impl( FnT ClassT::* memfnptr, ObjectT && obj, Args && ... args ) + noexcept( noexcept( ((*std::forward< ObjectT >( obj )).*memfnptr)( std::forward< Args >( args ) ... ) ) ) + -> decltype( ((*std::forward< ObjectT >( obj )).*memfnptr)( std::forward< Args >( args ) ... ) ) +{ + return ((*std::forward(obj)).*memfnptr)( std::forward< Args >( args ) ... ); +} + +template< typename MemberT, typename ClassT, typename ObjectT + nsel_REQUIRES_T( + std::is_same< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + || std::is_base_of< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_object_impl( MemberT ClassT::* memobjptr, ObjectT && obj ) + noexcept( noexcept( std::forward< ObjectT >( obj ).*memobjptr ) ) + -> decltype( std::forward< ObjectT >( obj ).*memobjptr ) +{ + return std::forward< ObjectT >( obj ).*memobjptr; +} + +template< typename MemberT, typename ClassT, typename ObjectT + nsel_REQUIRES_T( + is_reference_wrapper< typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_object_impl( MemberT ClassT::* memobjptr, ObjectT && obj ) + noexcept( noexcept( obj.get().*memobjptr ) ) + -> decltype( obj.get().*memobjptr ) +{ + return obj.get().*memobjptr; +} + +template< typename MemberT, typename ClassT, typename ObjectT + nsel_REQUIRES_T( + !std::is_same< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + && !std::is_base_of< ClassT, typename std20::remove_cvref< ObjectT >::type >::value + && !is_reference_wrapper< typename std20::remove_cvref< ObjectT >::type >::value + ) +> +nsel_constexpr auto invoke_member_object_impl( MemberT ClassT::* memobjptr, ObjectT && obj ) + noexcept( noexcept( (*std::forward< ObjectT >( obj )).*memobjptr ) ) + -> decltype( (*std::forward< ObjectT >( obj )).*memobjptr ) +{ + return (*std::forward< ObjectT >( obj )).*memobjptr; +} + +template< typename F, typename... Args + nsel_REQUIRES_T( + std::is_member_function_pointer< typename std20::remove_cvref< F >::type >::value + ) +> +nsel_constexpr auto invoke( F && f, Args && ... args ) + noexcept( noexcept( invoke_member_function_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ) ) ) + -> decltype( invoke_member_function_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ) ) +{ + return invoke_member_function_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ); +} + +template< typename F, typename... Args + nsel_REQUIRES_T( + std::is_member_object_pointer< typename std20::remove_cvref< F >::type >::value + ) +> +nsel_constexpr auto invoke( F && f, Args && ... args ) + noexcept( noexcept( invoke_member_object_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ) ) ) + -> decltype( invoke_member_object_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ) ) +{ + return invoke_member_object_impl( std::forward< F >( f ), std::forward< Args >( args ) ... ); +} + +template< typename F, typename... Args + nsel_REQUIRES_T( + !std::is_member_function_pointer< typename std20::remove_cvref< F >::type >::value + && !std::is_member_object_pointer< typename std20::remove_cvref< F >::type >::value + ) +> +nsel_constexpr auto invoke( F && f, Args && ... args ) + noexcept( noexcept( std::forward< F >( f )( std::forward< Args >( args ) ... ) ) ) + -> decltype( std::forward< F >( f )( std::forward< Args >( args ) ... ) ) +{ + return std::forward< F >( f )( std::forward< Args >( args ) ... ); +} + +template< typename F, typename ... Args > +using invoke_result_nocvref_t = typename std20::remove_cvref< decltype( ::nonstd::expected_lite::detail::invoke( std::declval< F >(), std::declval< Args >()... ) ) >::type; + +#if nsel_P2505R >= 5 +template< typename F, typename ... Args > +using transform_invoke_result_t = typename std::remove_cv< decltype( ::nonstd::expected_lite::detail::invoke( std::declval< F >(), std::declval< Args >()... ) ) >::type; +#else +template< typename F, typename ... Args > +using transform_invoke_result_t = invoke_result_nocvref_t +#endif // nsel_P2505R >= 5 + +template< typename T > +struct valid_expected_value_type : std::integral_constant< bool, std::is_destructible< T >::value && !std::is_reference< T >::value && !std::is_array< T >::value > {}; + +#endif // nsel_P2505R >= 3 +} // namespace detail + +/// x.x.5 Unexpected object type; unexpected_type; C++17 and later can also use aliased type unexpected. + +#if nsel_P0323R <= 2 +template< typename E = std::exception_ptr > +class unexpected_type +#else +template< typename E > +class unexpected_type +#endif // nsel_P0323R +{ +public: + using error_type = E; + + // x.x.5.2.1 Constructors + +// unexpected_type() = delete; + + constexpr unexpected_type( unexpected_type const & ) = default; + constexpr unexpected_type( unexpected_type && ) = default; + + template< typename... Args + nsel_REQUIRES_T( + std::is_constructible::value + ) + > + constexpr explicit unexpected_type( nonstd_lite_in_place_t(E), Args &&... args ) + : m_error( std::forward( args )...) + {} + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_constructible, Args&&...>::value + ) + > + constexpr explicit unexpected_type( nonstd_lite_in_place_t(E), std::initializer_list il, Args &&... args ) + : m_error( il, std::forward( args )...) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_same< typename std20::remove_cvref::type, nonstd_lite_in_place_t(E2) >::value + && !std::is_same< typename std20::remove_cvref::type, unexpected_type >::value + ) + > + constexpr explicit unexpected_type( E2 && error ) + : m_error( std::forward( error ) ) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible< E, E2>::value + && !std::is_constructible & >::value + && !std::is_constructible >::value + && !std::is_constructible const & >::value + && !std::is_constructible const >::value + && !std::is_convertible< unexpected_type &, E>::value + && !std::is_convertible< unexpected_type , E>::value + && !std::is_convertible< unexpected_type const &, E>::value + && !std::is_convertible< unexpected_type const , E>::value + && !std::is_convertible< E2 const &, E>::value /*=> explicit */ + ) + > + constexpr explicit unexpected_type( unexpected_type const & error ) + : m_error( E{ error.error() } ) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible< E, E2>::value + && !std::is_constructible & >::value + && !std::is_constructible >::value + && !std::is_constructible const & >::value + && !std::is_constructible const >::value + && !std::is_convertible< unexpected_type &, E>::value + && !std::is_convertible< unexpected_type , E>::value + && !std::is_convertible< unexpected_type const &, E>::value + && !std::is_convertible< unexpected_type const , E>::value + && std::is_convertible< E2 const &, E>::value /*=> explicit */ + ) + > + constexpr /*non-explicit*/ unexpected_type( unexpected_type const & error ) + : m_error( error.error() ) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible< E, E2>::value + && !std::is_constructible & >::value + && !std::is_constructible >::value + && !std::is_constructible const & >::value + && !std::is_constructible const >::value + && !std::is_convertible< unexpected_type &, E>::value + && !std::is_convertible< unexpected_type , E>::value + && !std::is_convertible< unexpected_type const &, E>::value + && !std::is_convertible< unexpected_type const , E>::value + && !std::is_convertible< E2 const &, E>::value /*=> explicit */ + ) + > + constexpr explicit unexpected_type( unexpected_type && error ) + : m_error( E{ std::move( error.error() ) } ) + {} + + template< typename E2 + nsel_REQUIRES_T( + std::is_constructible< E, E2>::value + && !std::is_constructible & >::value + && !std::is_constructible >::value + && !std::is_constructible const & >::value + && !std::is_constructible const >::value + && !std::is_convertible< unexpected_type &, E>::value + && !std::is_convertible< unexpected_type , E>::value + && !std::is_convertible< unexpected_type const &, E>::value + && !std::is_convertible< unexpected_type const , E>::value + && std::is_convertible< E2 const &, E>::value /*=> non-explicit */ + ) + > + constexpr /*non-explicit*/ unexpected_type( unexpected_type && error ) + : m_error( std::move( error.error() ) ) + {} + + // x.x.5.2.2 Assignment + + nsel_constexpr14 unexpected_type& operator=( unexpected_type const & ) = default; + nsel_constexpr14 unexpected_type& operator=( unexpected_type && ) = default; + + template< typename E2 = E > + nsel_constexpr14 unexpected_type & operator=( unexpected_type const & other ) + { + unexpected_type{ other.error() }.swap( *this ); + return *this; + } + + template< typename E2 = E > + nsel_constexpr14 unexpected_type & operator=( unexpected_type && other ) + { + unexpected_type{ std::move( other.error() ) }.swap( *this ); + return *this; + } + + // x.x.5.2.3 Observers + + nsel_constexpr14 E & error() & noexcept + { + return m_error; + } + + constexpr E const & error() const & noexcept + { + return m_error; + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + nsel_constexpr14 E && error() && noexcept + { + return std::move( m_error ); + } + + constexpr E const && error() const && noexcept + { + return std::move( m_error ); + } + +#endif + + // x.x.5.2.3 Observers - deprecated + + nsel_deprecated("replace value() with error()") + + nsel_constexpr14 E & value() & noexcept + { + return m_error; + } + + nsel_deprecated("replace value() with error()") + + constexpr E const & value() const & noexcept + { + return m_error; + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + nsel_deprecated("replace value() with error()") + + nsel_constexpr14 E && value() && noexcept + { + return std::move( m_error ); + } + + nsel_deprecated("replace value() with error()") + + constexpr E const && value() const && noexcept + { + return std::move( m_error ); + } + +#endif + + // x.x.5.2.4 Swap + + template< typename U=E > + nsel_REQUIRES_R( void, + std17::is_swappable::value + ) + swap( unexpected_type & other ) noexcept ( + std17::is_nothrow_swappable::value + ) + { + using std::swap; + swap( m_error, other.m_error ); + } + + // TODO: ??? unexpected_type: in-class friend operator==, != + +private: + error_type m_error; +}; + +#if nsel_CPP17_OR_GREATER + +/// template deduction guide: + +template< typename E > +unexpected_type( E ) -> unexpected_type< E >; + +#endif + +/// class unexpected_type, std::exception_ptr specialization (P0323R2) + +#if !nsel_CONFIG_NO_EXCEPTIONS +#if nsel_P0323R <= 2 + +// TODO: Should expected be specialized for particular E types such as exception_ptr and how? +// See p0323r7 2.1. Ergonomics, http://wg21.link/p0323 +template<> +class unexpected_type< std::exception_ptr > +{ +public: + using error_type = std::exception_ptr; + + unexpected_type() = delete; + + ~unexpected_type(){} + + explicit unexpected_type( std::exception_ptr const & error ) + : m_error( error ) + {} + + explicit unexpected_type(std::exception_ptr && error ) + : m_error( std::move( error ) ) + {} + + template< typename E > + explicit unexpected_type( E error ) + : m_error( std::make_exception_ptr( error ) ) + {} + + std::exception_ptr const & value() const + { + return m_error; + } + + std::exception_ptr & value() + { + return m_error; + } + +private: + std::exception_ptr m_error; +}; + +#endif // nsel_P0323R +#endif // !nsel_CONFIG_NO_EXCEPTIONS + +/// x.x.4, Unexpected equality operators + +template< typename E1, typename E2 > +constexpr bool operator==( unexpected_type const & x, unexpected_type const & y ) +{ + return x.error() == y.error(); +} + +template< typename E1, typename E2 > +constexpr bool operator!=( unexpected_type const & x, unexpected_type const & y ) +{ + return ! ( x == y ); +} + +#if nsel_P0323R <= 2 + +template< typename E > +constexpr bool operator<( unexpected_type const & x, unexpected_type const & y ) +{ + return x.error() < y.error(); +} + +template< typename E > +constexpr bool operator>( unexpected_type const & x, unexpected_type const & y ) +{ + return ( y < x ); +} + +template< typename E > +constexpr bool operator<=( unexpected_type const & x, unexpected_type const & y ) +{ + return ! ( y < x ); +} + +template< typename E > +constexpr bool operator>=( unexpected_type const & x, unexpected_type const & y ) +{ + return ! ( x < y ); +} + +#endif // nsel_P0323R + +/// x.x.5 Specialized algorithms + +template< typename E + nsel_REQUIRES_T( + std17::is_swappable::value + ) +> +void swap( unexpected_type & x, unexpected_type & y) noexcept ( noexcept ( x.swap(y) ) ) +{ + x.swap( y ); +} + +#if nsel_P0323R <= 2 + +// unexpected: relational operators for std::exception_ptr: + +inline constexpr bool operator<( unexpected_type const & /*x*/, unexpected_type const & /*y*/ ) +{ + return false; +} + +inline constexpr bool operator>( unexpected_type const & /*x*/, unexpected_type const & /*y*/ ) +{ + return false; +} + +inline constexpr bool operator<=( unexpected_type const & x, unexpected_type const & y ) +{ + return ( x == y ); +} + +inline constexpr bool operator>=( unexpected_type const & x, unexpected_type const & y ) +{ + return ( x == y ); +} + +#endif // nsel_P0323R + +// unexpected: traits + +#if nsel_P0323R <= 3 + +template< typename E > +struct is_unexpected : std::false_type {}; + +template< typename E > +struct is_unexpected< unexpected_type > : std::true_type {}; + +#endif // nsel_P0323R + +// unexpected: factory + +// keep make_unexpected() removed in p0323r2 for pre-C++17: + +template< typename E > +nsel_constexpr14 auto +make_unexpected( E && value ) -> unexpected_type< typename std::decay::type > +{ + return unexpected_type< typename std::decay::type >( std::forward(value) ); +} + +template +< + typename E, typename... Args, + typename = std::enable_if< + std::is_constructible::value + > +> +nsel_constexpr14 auto +make_unexpected( nonstd_lite_in_place_t(E), Args &&... args ) -> unexpected_type< typename std::decay::type > +{ + return std::move( unexpected_type< typename std::decay::type >( nonstd_lite_in_place(E), std::forward(args)...) ); +} + +#if nsel_P0323R <= 3 + +/*nsel_constexpr14*/ auto inline +make_unexpected_from_current_exception() -> unexpected_type< std::exception_ptr > +{ + return unexpected_type< std::exception_ptr >( std::current_exception() ); +} + +#endif // nsel_P0323R + +/// x.x.6, x.x.7 expected access error + +template< typename E > +class nsel_NODISCARD bad_expected_access; + +/// x.x.7 bad_expected_access: expected access error + +template <> +class nsel_NODISCARD bad_expected_access< void > : public std::exception +{ +public: + explicit bad_expected_access() + : std::exception() + {} +}; + +/// x.x.6 bad_expected_access: expected access error + +#if !nsel_CONFIG_NO_EXCEPTIONS + +template< typename E > +class nsel_NODISCARD bad_expected_access : public bad_expected_access< void > +{ +public: + using error_type = E; + + explicit bad_expected_access( error_type error ) + : m_error( error ) + {} + + virtual char const * what() const noexcept override + { + return "bad_expected_access"; + } + + nsel_constexpr14 error_type & error() & + { + return m_error; + } + + constexpr error_type const & error() const & + { + return m_error; + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + nsel_constexpr14 error_type && error() && + { + return std::move( m_error ); + } + + constexpr error_type const && error() const && + { + return std::move( m_error ); + } + +#endif + +private: + error_type m_error; +}; + +#endif // nsel_CONFIG_NO_EXCEPTIONS + +/// x.x.8 unexpect tag, in_place_unexpected tag: construct an error + +struct unexpect_t{}; +using in_place_unexpected_t = unexpect_t; + +nsel_inline17 constexpr unexpect_t unexpect{}; +nsel_inline17 constexpr unexpect_t in_place_unexpected{}; + +/// class error_traits + +#if nsel_CONFIG_NO_EXCEPTIONS + +namespace detail { + inline bool text( char const * /*text*/ ) { return true; } +} + +template< typename Error > +struct error_traits +{ + static void rethrow( Error const & /*e*/ ) + { +#if nsel_CONFIG_NO_EXCEPTIONS_SEH + RaiseException( EXCEPTION_ACCESS_VIOLATION, EXCEPTION_NONCONTINUABLE, 0, NULL ); +#else + assert( false && detail::text("throw bad_expected_access{ e };") ); +#endif + } +}; + +template<> +struct error_traits< std::exception_ptr > +{ + static void rethrow( std::exception_ptr const & /*e*/ ) + { +#if nsel_CONFIG_NO_EXCEPTIONS_SEH + RaiseException( EXCEPTION_ACCESS_VIOLATION, EXCEPTION_NONCONTINUABLE, 0, NULL ); +#else + assert( false && detail::text("throw bad_expected_access{ e };") ); +#endif + } +}; + +template<> +struct error_traits< std::error_code > +{ + static void rethrow( std::error_code const & /*e*/ ) + { +#if nsel_CONFIG_NO_EXCEPTIONS_SEH + RaiseException( EXCEPTION_ACCESS_VIOLATION, EXCEPTION_NONCONTINUABLE, 0, NULL ); +#else + assert( false && detail::text("throw std::system_error( e );") ); +#endif + } +}; + +#else // nsel_CONFIG_NO_EXCEPTIONS + +template< typename Error > +struct error_traits +{ + static void rethrow( Error const & e ) + { + throw bad_expected_access{ e }; + } +}; + +template<> +struct error_traits< std::exception_ptr > +{ + static void rethrow( std::exception_ptr const & e ) + { + std::rethrow_exception( e ); + } +}; + +template<> +struct error_traits< std::error_code > +{ + static void rethrow( std::error_code const & e ) + { + throw std::system_error( e ); + } +}; + +#endif // nsel_CONFIG_NO_EXCEPTIONS + +#if nsel_P2505R >= 3 +namespace detail { + +// from https://en.cppreference.com/w/cpp/utility/expected/unexpected: +// "the type of the unexpected value. The type must not be an array type, a non-object type, a specialization of std::unexpected, or a cv-qualified type." +template< typename T > +struct valid_unexpected_type : std::integral_constant< bool, + std::is_same< T, typename std20::remove_cvref< T >::type >::value + && std::is_object< T >::value + && !std::is_array< T >::value +> {}; + +template< typename T > +struct valid_unexpected_type< unexpected_type< T > > : std::false_type {}; + +} // namespace detail +#endif // nsel_P2505R >= 3 + +} // namespace expected_lite + +// provide nonstd::unexpected_type: + +using expected_lite::unexpected_type; + +namespace expected_lite { + +/// class expected + +#if nsel_P0323R <= 2 +template< typename T, typename E = std::exception_ptr > +class nsel_NODISCARD expected +#else +template< typename T, typename E > +class nsel_NODISCARD expected +#endif // nsel_P0323R +{ +private: + template< typename, typename > friend class expected; + +public: + using value_type = T; + using error_type = E; + using unexpected_type = nonstd::unexpected_type; + + template< typename U > + struct rebind + { + using type = expected; + }; + + // x.x.4.1 constructors + + nsel_REQUIRES_0( + std::is_default_constructible::value + ) + nsel_constexpr14 expected() + : contained( true ) + { + contained.construct_value(); + } + + nsel_constexpr14 expected( expected const & ) = default; + nsel_constexpr14 expected( expected && ) = default; + + template< typename U, typename G + nsel_REQUIRES_T( + std::is_constructible< T, U const &>::value + && std::is_constructible::value + && !std::is_constructible & >::value + && !std::is_constructible && >::value + && !std::is_constructible const & >::value + && !std::is_constructible const && >::value + && !std::is_convertible< expected & , T>::value + && !std::is_convertible< expected &&, T>::value + && !std::is_convertible< expected const & , T>::value + && !std::is_convertible< expected const &&, T>::value + && (!std::is_convertible::value || !std::is_convertible::value ) /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( expected const & other ) + : contained( other.has_value() ) + { + if ( has_value() ) contained.construct_value( T{ other.contained.value() } ); + else contained.construct_error( E{ other.contained.error() } ); + } + + template< typename U, typename G + nsel_REQUIRES_T( + std::is_constructible< T, U const &>::value + && std::is_constructible::value + && !std::is_constructible & >::value + && !std::is_constructible && >::value + && !std::is_constructible const & >::value + && !std::is_constructible const && >::value + && !std::is_convertible< expected & , T>::value + && !std::is_convertible< expected &&, T>::value + && !std::is_convertible< expected const &, T>::value + && !std::is_convertible< expected const &&, T>::value + && !(!std::is_convertible::value || !std::is_convertible::value ) /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( expected const & other ) + : contained( other.has_value() ) + { + if ( has_value() ) contained.construct_value( other.contained.value() ); + else contained.construct_error( other.contained.error() ); + } + + template< typename U, typename G + nsel_REQUIRES_T( + std::is_constructible< T, U>::value + && std::is_constructible::value + && !std::is_constructible & >::value + && !std::is_constructible && >::value + && !std::is_constructible const & >::value + && !std::is_constructible const && >::value + && !std::is_convertible< expected & , T>::value + && !std::is_convertible< expected &&, T>::value + && !std::is_convertible< expected const & , T>::value + && !std::is_convertible< expected const &&, T>::value + && (!std::is_convertible::value || !std::is_convertible::value ) /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( expected && other ) + : contained( other.has_value() ) + { + if ( has_value() ) contained.construct_value( T{ std::move( other.contained.value() ) } ); + else contained.construct_error( E{ std::move( other.contained.error() ) } ); + } + + template< typename U, typename G + nsel_REQUIRES_T( + std::is_constructible< T, U>::value + && std::is_constructible::value + && !std::is_constructible & >::value + && !std::is_constructible && >::value + && !std::is_constructible const & >::value + && !std::is_constructible const && >::value + && !std::is_convertible< expected & , T>::value + && !std::is_convertible< expected &&, T>::value + && !std::is_convertible< expected const & , T>::value + && !std::is_convertible< expected const &&, T>::value + && !(!std::is_convertible::value || !std::is_convertible::value ) /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( expected && other ) + : contained( other.has_value() ) + { + if ( has_value() ) contained.construct_value( std::move( other.contained.value() ) ); + else contained.construct_error( std::move( other.contained.error() ) ); + } + + template< typename U = T + nsel_REQUIRES_T( + std::is_copy_constructible::value + ) + > + nsel_constexpr14 expected( value_type const & value ) + : contained( true ) + { + contained.construct_value( value ); + } + + template< typename U = T + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_same::type, nonstd_lite_in_place_t(U)>::value + && !std::is_same< expected , typename std20::remove_cvref::type>::value + && !std::is_same, typename std20::remove_cvref::type>::value + && !std::is_convertible::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( U && value ) noexcept + ( + std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value + ) + : contained( true ) + { + contained.construct_value( T{ std::forward( value ) } ); + } + + template< typename U = T + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_same::type, nonstd_lite_in_place_t(U)>::value + && !std::is_same< expected , typename std20::remove_cvref::type>::value + && !std::is_same, typename std20::remove_cvref::type>::value + && std::is_convertible::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( U && value ) noexcept + ( + std::is_nothrow_move_constructible::value && + std::is_nothrow_move_constructible::value + ) + : contained( true ) + { + contained.construct_value( std::forward( value ) ); + } + + // construct error: + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_convertible< G const &, E>::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( nonstd::unexpected_type const & error ) + : contained( false ) + { + contained.construct_error( E{ error.error() } ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value + && std::is_convertible< G const &, E>::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type const & error ) + : contained( false ) + { + contained.construct_error( error.error() ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value + && !std::is_convertible< G&&, E>::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( nonstd::unexpected_type && error ) + : contained( false ) + { + contained.construct_error( E{ std::move( error.error() ) } ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value + && std::is_convertible< G&&, E>::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type && error ) + : contained( false ) + { + contained.construct_error( std::move( error.error() ) ); + } + + // in-place construction, value + + template< typename... Args + nsel_REQUIRES_T( + std::is_constructible::value + ) + > + nsel_constexpr14 explicit expected( nonstd_lite_in_place_t(T), Args&&... args ) + : contained( true ) + { + contained.emplace_value( std::forward( args )... ); + } + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_constructible, Args&&...>::value + ) + > + nsel_constexpr14 explicit expected( nonstd_lite_in_place_t(T), std::initializer_list il, Args&&... args ) + : contained( true ) + { + contained.emplace_value( il, std::forward( args )... ); + } + + // in-place construction, error + + template< typename... Args + nsel_REQUIRES_T( + std::is_constructible::value + ) + > + nsel_constexpr14 explicit expected( unexpect_t, Args&&... args ) + : contained( false ) + { + contained.emplace_error( std::forward( args )... ); + } + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_constructible, Args&&...>::value + ) + > + nsel_constexpr14 explicit expected( unexpect_t, std::initializer_list il, Args&&... args ) + : contained( false ) + { + contained.emplace_error( il, std::forward( args )... ); + } + + // x.x.4.2 destructor + + // TODO: ~expected: triviality + // Effects: If T is not cv void and is_trivially_destructible_v is false and bool(*this), calls val.~T(). If is_trivially_destructible_v is false and !bool(*this), calls unexpect.~unexpected(). + // Remarks: If either T is cv void or is_trivially_destructible_v is true, and is_trivially_destructible_v is true, then this destructor shall be a trivial destructor. + + ~expected() + { + if ( has_value() ) contained.destruct_value(); + else contained.destruct_error(); + } + + // x.x.4.3 assignment + + expected & operator=( expected const & other ) + { + expected( other ).swap( *this ); + return *this; + } + + expected & operator=( expected && other ) noexcept + ( + std::is_nothrow_move_constructible< T>::value + && std::is_nothrow_move_assignable< T>::value + && std::is_nothrow_move_constructible::value // added for missing + && std::is_nothrow_move_assignable< E>::value ) // nothrow above + { + expected( std::move( other ) ).swap( *this ); + return *this; + } + + template< typename U + nsel_REQUIRES_T( + !std::is_same, typename std20::remove_cvref::type>::value + && std17::conjunction, std::is_same> >::value + && std::is_constructible::value + && std::is_assignable< T&,U>::value + && std::is_nothrow_move_constructible::value ) + > + expected & operator=( U && value ) + { + expected( std::forward( value ) ).swap( *this ); + return *this; + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value && + std::is_copy_constructible::value // TODO: std::is_nothrow_copy_constructible + && std::is_copy_assignable::value + ) + > + expected & operator=( nonstd::unexpected_type const & error ) + { + expected( unexpect, error.error() ).swap( *this ); + return *this; + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_constructible::value && + std::is_move_constructible::value // TODO: std::is_nothrow_move_constructible + && std::is_move_assignable::value + ) + > + expected & operator=( nonstd::unexpected_type && error ) + { + expected( unexpect, std::move( error.error() ) ).swap( *this ); + return *this; + } + + template< typename... Args + nsel_REQUIRES_T( + std::is_nothrow_constructible::value + ) + > + value_type & emplace( Args &&... args ) + { + expected( nonstd_lite_in_place(T), std::forward(args)... ).swap( *this ); + return value(); + } + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_nothrow_constructible&, Args&&...>::value + ) + > + value_type & emplace( std::initializer_list il, Args &&... args ) + { + expected( nonstd_lite_in_place(T), il, std::forward(args)... ).swap( *this ); + return value(); + } + + // x.x.4.4 swap + + template< typename U=T, typename G=E > + nsel_REQUIRES_R( void, + std17::is_swappable< U>::value + && std17::is_swappable::value + && ( std::is_move_constructible::value || std::is_move_constructible::value ) + ) + swap( expected & other ) noexcept + ( + std::is_nothrow_move_constructible::value && std17::is_nothrow_swappable::value && + std::is_nothrow_move_constructible::value && std17::is_nothrow_swappable::value + ) + { + using std::swap; + + if ( bool(*this) && bool(other) ) { swap( contained.value(), other.contained.value() ); } + else if ( ! bool(*this) && ! bool(other) ) { swap( contained.error(), other.contained.error() ); } + else if ( bool(*this) && ! bool(other) ) { error_type t( std::move( other.error() ) ); + other.contained.destruct_error(); + other.contained.construct_value( std::move( contained.value() ) ); + contained.destruct_value(); + contained.construct_error( std::move( t ) ); + bool has_value = contained.has_value(); + bool other_has_value = other.has_value(); + other.contained.set_has_value(has_value); + contained.set_has_value(other_has_value); + } + else if ( ! bool(*this) && bool(other) ) { other.swap( *this ); } + } + + // x.x.4.5 observers + + constexpr value_type const * operator ->() const + { + return assert( has_value() ), contained.value_ptr(); + } + + value_type * operator ->() + { + return assert( has_value() ), contained.value_ptr(); + } + + constexpr value_type const & operator *() const & + { + return assert( has_value() ), contained.value(); + } + + value_type & operator *() & + { + return assert( has_value() ), contained.value(); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + constexpr value_type const && operator *() const && + { + return std::move( ( assert( has_value() ), contained.value() ) ); + } + + nsel_constexpr14 value_type && operator *() && + { + return std::move( ( assert( has_value() ), contained.value() ) ); + } + +#endif + + constexpr explicit operator bool() const noexcept + { + return has_value(); + } + + constexpr bool has_value() const noexcept + { + return contained.has_value(); + } + + nsel_DISABLE_MSVC_WARNINGS( 4702 ) // warning C4702: unreachable code, see issue 65. + + constexpr value_type const & value() const & + { + return has_value() + ? ( contained.value() ) + : ( error_traits::rethrow( contained.error() ), contained.value() ); + } + + value_type & value() & + { + return has_value() + ? ( contained.value() ) + : ( error_traits::rethrow( contained.error() ), contained.value() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + constexpr value_type const && value() const && + { + return std::move( has_value() + ? ( contained.value() ) + : ( error_traits::rethrow( contained.error() ), contained.value() ) ); + } + + nsel_constexpr14 value_type && value() && + { + return std::move( has_value() + ? ( contained.value() ) + : ( error_traits::rethrow( contained.error() ), contained.value() ) ); + } + +#endif + nsel_RESTORE_MSVC_WARNINGS() + + constexpr error_type const & error() const & + { + return assert( ! has_value() ), contained.error(); + } + + error_type & error() & + { + return assert( ! has_value() ), contained.error(); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + constexpr error_type const && error() const && + { + return std::move( ( assert( ! has_value() ), contained.error() ) ); + } + + error_type && error() && + { + return std::move( ( assert( ! has_value() ), contained.error() ) ); + } + +#endif + + constexpr unexpected_type get_unexpected() const + { + return make_unexpected( contained.error() ); + } + + template< typename Ex > + bool has_exception() const + { + using ContainedEx = typename std::remove_reference< decltype( get_unexpected().error() ) >::type; + return ! has_value() && std::is_base_of< Ex, ContainedEx>::value; + } + + template< typename U + nsel_REQUIRES_T( + std::is_copy_constructible< T>::value + && std::is_convertible::value + ) + > + value_type value_or( U && v ) const & + { + return has_value() + ? contained.value() + : static_cast( std::forward( v ) ); + } + + template< typename U + nsel_REQUIRES_T( + std::is_move_constructible< T>::value + && std::is_convertible::value + ) + > + value_type value_or( U && v ) && + { + return has_value() + ? std::move( contained.value() ) + : static_cast( std::forward( v ) ); + } + +#if nsel_P2505R >= 4 + template< typename G = E + nsel_REQUIRES_T( + std::is_copy_constructible< E >::value + && std::is_convertible< G, E >::value + ) + > + nsel_constexpr error_type error_or( G && e ) const & + { + return has_value() + ? static_cast< E >( std::forward< G >( e ) ) + : contained.error(); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_move_constructible< E >::value + && std::is_convertible< G, E >::value + ) + > + nsel_constexpr14 error_type error_or( G && e ) && + { + return has_value() + ? static_cast< E >( std::forward< G >( e ) ) + : std::move( contained.error() ); + } +#endif // nsel_P2505R >= 4 + +#if nsel_P2505R >= 3 + // Monadic operations (P2505) + template< typename F + nsel_REQUIRES_T( + detail::is_expected < detail::invoke_result_nocvref_t< F, value_type & > > ::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, value_type & >::error_type, error_type >::value + && std::is_constructible< error_type, error_type & >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, value_type & > and_then( F && f ) & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, value_type & >( detail::invoke( std::forward< F >( f ), value() ) ) + : detail::invoke_result_nocvref_t< F, value_type & >( unexpect, error() ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, const value_type & >::error_type, error_type >::value + && std::is_constructible< error_type, const error_type & >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const value_type & > and_then( F && f ) const & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const value_type & >( detail::invoke( std::forward< F >( f ), value() ) ) + : detail::invoke_result_nocvref_t< F, const value_type & >( unexpect, error() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, value_type && >::error_type, error_type >::value + && std::is_constructible< error_type, error_type && >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, value_type && > and_then( F && f ) && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, value_type && >( detail::invoke( std::forward< F >( f ), std::move( value() ) ) ) + : detail::invoke_result_nocvref_t< F, value_type && >( unexpect, std::move( error() ) ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, const value_type & >::error_type, error_type >::value + && std::is_constructible< error_type, const error_type && >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const value_type && > and_then( F && f ) const && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const value_type && >( detail::invoke( std::forward< F >( f ), std::move( value() ) ) ) + : detail::invoke_result_nocvref_t< F, const value_type && >( unexpect, std::move( error() ) ); + } +#endif + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, error_type & >::value_type, value_type >::value + && std::is_constructible< value_type, value_type & >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, error_type & > or_else( F && f ) & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, error_type & >( value() ) + : detail::invoke_result_nocvref_t< F, error_type & >( detail::invoke( std::forward< F >( f ), error() ) ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, const error_type & >::value_type, value_type >::value + && std::is_constructible< value_type, const value_type & >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const error_type & > or_else( F && f ) const & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const error_type & >( value() ) + : detail::invoke_result_nocvref_t< F, const error_type & >( detail::invoke( std::forward< F >( f ), error() ) ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, error_type && >::value_type, value_type >::value + && std::is_constructible< value_type, value_type && >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, error_type && > or_else( F && f ) && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, error_type && >( std::move( value() ) ) + : detail::invoke_result_nocvref_t< F, error_type && >( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F, const error_type && >::value_type, value_type >::value + && std::is_constructible< value_type, const value_type && >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const error_type && > or_else( F && f ) const && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const error_type && >( std::move( value() ) ) + : detail::invoke_result_nocvref_t< F, const error_type && >( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } +#endif + + template::value + && !std::is_void< detail::transform_invoke_result_t< F, value_type & > >::value + && detail::valid_expected_value_type< detail::transform_invoke_result_t< F, value_type & > >::value + ) + > + nsel_constexpr14 expected< detail::transform_invoke_result_t< F, value_type & >, error_type > transform( F && f ) & + { + return has_value() + ? expected< detail::transform_invoke_result_t< F, value_type & >, error_type >( detail::invoke( std::forward< F >( f ), **this ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F, value_type & > >::value + ) + > + nsel_constexpr14 expected< void, error_type > transform( F && f ) & + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ), **this ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + + template::value + && !std::is_void< detail::transform_invoke_result_t< F, const value_type & > >::value + && detail::valid_expected_value_type< detail::transform_invoke_result_t< F, const value_type & > >::value + ) + > + nsel_constexpr expected< detail::transform_invoke_result_t< F, const value_type & >, error_type > transform( F && f ) const & + { + return has_value() + ? expected< detail::transform_invoke_result_t< F, const value_type & >, error_type >( detail::invoke( std::forward< F >( f ), **this ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F, const value_type & > >::value + ) + > + nsel_constexpr expected< void, error_type > transform( F && f ) const & + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ), **this ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template::value + && !std::is_void< detail::transform_invoke_result_t< F, value_type && > >::value + && detail::valid_expected_value_type< detail::transform_invoke_result_t< F, value_type && > >::value + ) + > + nsel_constexpr14 expected< detail::transform_invoke_result_t< F, value_type && >, error_type > transform( F && f ) && + { + return has_value() + ? expected< detail::transform_invoke_result_t< F, value_type && >, error_type >( detail::invoke( std::forward< F >( f ), std::move( **this ) ) ) + : make_unexpected( std::move( error() ) ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F, value_type && > >::value + ) + > + nsel_constexpr14 expected< void, error_type > transform( F && f ) && + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ), **this ), expected< void, error_type >() ) + : make_unexpected( std::move( error() ) ); + } + + template::value + && !std::is_void< detail::transform_invoke_result_t< F, const value_type && > >::value + && detail::valid_expected_value_type< detail::transform_invoke_result_t< F, const value_type && > >::value + ) + > + nsel_constexpr expected< detail::transform_invoke_result_t< F, const value_type && >, error_type > transform( F && f ) const && + { + return has_value() + ? expected< detail::transform_invoke_result_t< F, const value_type && >, error_type >( detail::invoke( std::forward< F >( f ), std::move( **this ) ) ) + : make_unexpected( std::move( error() ) ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F, const value_type && > >::value + ) + > + nsel_constexpr expected< void, error_type > transform( F && f ) const && + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ), **this ), expected< void, error_type >() ) + : make_unexpected( std::move( error() ) ); + } +#endif + + template >::value + && std::is_constructible< value_type, value_type & >::value + ) + > + nsel_constexpr14 expected< value_type, detail::transform_invoke_result_t< F, error_type & > > transform_error( F && f ) & + { + return has_value() + ? expected< value_type, detail::transform_invoke_result_t< F, error_type & > >( in_place, **this ) + : make_unexpected( detail::invoke( std::forward< F >( f ), error() ) ); + } + + template >::value + && std::is_constructible< value_type, const value_type & >::value + ) + > + nsel_constexpr expected< value_type, detail::transform_invoke_result_t< F, const error_type & > > transform_error( F && f ) const & + { + return has_value() + ? expected< value_type, detail::transform_invoke_result_t< F, const error_type & > >( in_place, **this ) + : make_unexpected( detail::invoke( std::forward< F >( f ), error() ) ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_constructible< value_type, value_type && >::value + ) + > + nsel_constexpr14 expected< value_type, detail::transform_invoke_result_t< F, error_type && > > transform_error( F && f ) && + { + return has_value() + ? expected< value_type, detail::transform_invoke_result_t< F, error_type && > >( in_place, std::move( **this ) ) + : make_unexpected( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } + + template >::value + && std::is_constructible< value_type, const value_type && >::value + ) + > + nsel_constexpr expected< value_type, detail::transform_invoke_result_t< F, const error_type && > > transform_error( F && f ) const && + { + return has_value() + ? expected< value_type, detail::transform_invoke_result_t< F, const error_type && > >( in_place, std::move( **this ) ) + : make_unexpected( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } +#endif +#endif // nsel_P2505R >= 3 + // unwrap() + +// template +// constexpr expected expected,E>::unwrap() const&; + +// template +// constexpr expected expected::unwrap() const&; + +// template +// expected expected, E>::unwrap() &&; + +// template +// template expected expected::unwrap() &&; + + // factories + +// template< typename Ex, typename F> +// expected catch_exception(F&& f); + +// template< typename F> +// expected())),E> map(F&& func) ; + +// template< typename F> +// 'see below' bind(F&& func); + +// template< typename F> +// expected catch_error(F&& f); + +// template< typename F> +// 'see below' then(F&& func); + +private: + detail::storage_t + < + T + ,E + , std::is_copy_constructible::value && std::is_copy_constructible::value + , std::is_move_constructible::value && std::is_move_constructible::value + > + contained; +}; + +/// class expected, void specialization + +template< typename E > +class nsel_NODISCARD expected< void, E > +{ +private: + template< typename, typename > friend class expected; + +public: + using value_type = void; + using error_type = E; + using unexpected_type = nonstd::unexpected_type; + + // x.x.4.1 constructors + + constexpr expected() noexcept + : contained( true ) + {} + + nsel_constexpr14 expected( expected const & other ) = default; + nsel_constexpr14 expected( expected && other ) = default; + + constexpr explicit expected( nonstd_lite_in_place_t(void) ) + : contained( true ) + {} + + template< typename G = E + nsel_REQUIRES_T( + !std::is_convertible::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( nonstd::unexpected_type const & error ) + : contained( false ) + { + contained.construct_error( E{ error.error() } ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_convertible::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type const & error ) + : contained( false ) + { + contained.construct_error( error.error() ); + } + + template< typename G = E + nsel_REQUIRES_T( + !std::is_convertible::value /*=> explicit */ + ) + > + nsel_constexpr14 explicit expected( nonstd::unexpected_type && error ) + : contained( false ) + { + contained.construct_error( E{ std::move( error.error() ) } ); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_convertible::value /*=> non-explicit */ + ) + > + nsel_constexpr14 /*non-explicit*/ expected( nonstd::unexpected_type && error ) + : contained( false ) + { + contained.construct_error( std::move( error.error() ) ); + } + + template< typename... Args + nsel_REQUIRES_T( + std::is_constructible::value + ) + > + nsel_constexpr14 explicit expected( unexpect_t, Args&&... args ) + : contained( false ) + { + contained.emplace_error( std::forward( args )... ); + } + + template< typename U, typename... Args + nsel_REQUIRES_T( + std::is_constructible, Args&&...>::value + ) + > + nsel_constexpr14 explicit expected( unexpect_t, std::initializer_list il, Args&&... args ) + : contained( false ) + { + contained.emplace_error( il, std::forward( args )... ); + } + + // destructor + + ~expected() + { + if ( ! has_value() ) + { + contained.destruct_error(); + } + } + + // x.x.4.3 assignment + + expected & operator=( expected const & other ) + { + expected( other ).swap( *this ); + return *this; + } + + expected & operator=( expected && other ) noexcept + ( + std::is_nothrow_move_assignable::value && + std::is_nothrow_move_constructible::value ) + { + expected( std::move( other ) ).swap( *this ); + return *this; + } + + void emplace() + { + expected().swap( *this ); + } + + // x.x.4.4 swap + + template< typename G = E > + nsel_REQUIRES_R( void, + std17::is_swappable::value + && std::is_move_constructible::value + ) + swap( expected & other ) noexcept + ( + std::is_nothrow_move_constructible::value && std17::is_nothrow_swappable::value + ) + { + using std::swap; + + if ( ! bool(*this) && ! bool(other) ) { swap( contained.error(), other.contained.error() ); } + else if ( bool(*this) && ! bool(other) ) { contained.construct_error( std::move( other.error() ) ); + bool has_value = contained.has_value(); + bool other_has_value = other.has_value(); + other.contained.set_has_value(has_value); + contained.set_has_value(other_has_value); + } + else if ( ! bool(*this) && bool(other) ) { other.swap( *this ); } + } + + // x.x.4.5 observers + + constexpr explicit operator bool() const noexcept + { + return has_value(); + } + + constexpr bool has_value() const noexcept + { + return contained.has_value(); + } + + void value() const + { + if ( ! has_value() ) + { + error_traits::rethrow( contained.error() ); + } + } + + constexpr error_type const & error() const & + { + return assert( ! has_value() ), contained.error(); + } + + error_type & error() & + { + return assert( ! has_value() ), contained.error(); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + + constexpr error_type const && error() const && + { + return std::move( ( assert( ! has_value() ), contained.error() ) ); + } + + error_type && error() && + { + return std::move( ( assert( ! has_value() ), contained.error() ) ); + } + +#endif + + constexpr unexpected_type get_unexpected() const + { + return make_unexpected( contained.error() ); + } + + template< typename Ex > + bool has_exception() const + { + using ContainedEx = typename std::remove_reference< decltype( get_unexpected().error() ) >::type; + return ! has_value() && std::is_base_of< Ex, ContainedEx>::value; + } + +#if nsel_P2505R >= 4 + template< typename G = E + nsel_REQUIRES_T( + std::is_copy_constructible< E >::value + && std::is_convertible< G, E >::value + ) + > + nsel_constexpr error_type error_or( G && e ) const & + { + return has_value() + ? static_cast< E >( std::forward< G >( e ) ) + : contained.error(); + } + + template< typename G = E + nsel_REQUIRES_T( + std::is_move_constructible< E >::value + && std::is_convertible< G, E >::value + ) + > + nsel_constexpr14 error_type error_or( G && e ) && + { + return has_value() + ? static_cast< E >( std::forward< G >( e ) ) + : std::move( contained.error() ); + } +#endif // nsel_P2505R >= 4 + +#if nsel_P2505R >= 3 + // Monadic operations (P2505) + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F >::error_type, error_type >::value + && std::is_constructible< error_type, error_type & >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F > and_then( F && f ) & + { + return has_value() + ? detail::invoke_result_nocvref_t< F >( detail::invoke( std::forward< F >( f ) ) ) + : detail::invoke_result_nocvref_t< F >( unexpect, error() ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F >::error_type, error_type >::value + && std::is_constructible< error_type, const error_type & >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F > and_then( F && f ) const & + { + return has_value() + ? detail::invoke_result_nocvref_t< F >( detail::invoke( std::forward< F >( f ) ) ) + : detail::invoke_result_nocvref_t< F >( unexpect, error() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F >::error_type, error_type >::value + && std::is_constructible< error_type, error_type && >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F > and_then( F && f ) && + { + return has_value() + ? detail::invoke_result_nocvref_t< F >( detail::invoke( std::forward< F >( f ) ) ) + : detail::invoke_result_nocvref_t< F >( unexpect, std::move( error() ) ); + } + + template >::value + && std::is_same< typename detail::invoke_result_nocvref_t< F >::error_type, error_type >::value + && std::is_constructible< error_type, const error_type && >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F > and_then( F && f ) const && + { + return has_value() + ? detail::invoke_result_nocvref_t< F >( detail::invoke( std::forward< F >( f ) ) ) + : detail::invoke_result_nocvref_t< F >( unexpect, std::move( error() ) ); + } +#endif + + template >::value + && std::is_void< typename detail::invoke_result_nocvref_t< F, error_type & >::value_type >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, error_type & > or_else( F && f ) & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, error_type & >() + : detail::invoke_result_nocvref_t< F, error_type & >( detail::invoke( std::forward< F >( f ), error() ) ); + } + + template >::value + && std::is_void< typename detail::invoke_result_nocvref_t< F, const error_type & >::value_type >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const error_type & > or_else( F && f ) const & + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const error_type & >() + : detail::invoke_result_nocvref_t< F, const error_type & >( detail::invoke( std::forward< F >( f ), error() ) ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + && std::is_void< typename detail::invoke_result_nocvref_t< F, error_type && >::value_type >::value + ) + > + nsel_constexpr14 detail::invoke_result_nocvref_t< F, error_type && > or_else( F && f ) && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, error_type && >() + : detail::invoke_result_nocvref_t< F, error_type && >( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } + + template >::value + && std::is_void< typename detail::invoke_result_nocvref_t< F, const error_type && >::value_type >::value + ) + > + nsel_constexpr detail::invoke_result_nocvref_t< F, const error_type && > or_else( F && f ) const && + { + return has_value() + ? detail::invoke_result_nocvref_t< F, const error_type && >() + : detail::invoke_result_nocvref_t< F, const error_type && >( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } +#endif + + template::value + && !std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr14 expected< detail::transform_invoke_result_t< F >, error_type > transform( F && f ) & + { + return has_value() + ? expected< detail::transform_invoke_result_t< F >, error_type >( detail::invoke( std::forward< F >( f ) ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr14 expected< void, error_type > transform( F && f ) & + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ) ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + + template::value + && !std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr expected< detail::transform_invoke_result_t< F >, error_type > transform( F && f ) const & + { + return has_value() + ? expected< detail::transform_invoke_result_t< F >, error_type >( detail::invoke( std::forward< F >( f ) ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr expected< void, error_type > transform( F && f ) const & + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ) ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template::value + && !std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr14 expected< detail::transform_invoke_result_t< F >, error_type > transform( F && f ) && + { + return has_value() + ? expected< detail::transform_invoke_result_t< F >, error_type >( detail::invoke( std::forward< F >( f ) ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr14 expected< void, error_type > transform( F && f ) && + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ) ), expected< void, error_type >() ) + : make_unexpected( error() ); + } + + template::value + && !std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr expected< detail::transform_invoke_result_t< F >, error_type > transform( F && f ) const && + { + return has_value() + ? expected< detail::transform_invoke_result_t< F >, error_type >( detail::invoke( std::forward< F >( f ) ) ) + : make_unexpected( error() ); + } + + template::value + && std::is_void< detail::transform_invoke_result_t< F > >::value + ) + > + nsel_constexpr expected< void, error_type > transform( F && f ) const && + { + return has_value() + ? ( detail::invoke( std::forward< F >( f ) ), expected< void, error_type >() ) + : make_unexpected( error() ); + } +#endif + + template >::value + ) + > + nsel_constexpr14 expected< void, detail::transform_invoke_result_t< F, error_type & > > transform_error( F && f ) & + { + return has_value() + ? expected< void, detail::transform_invoke_result_t< F, error_type & > >() + : make_unexpected( detail::invoke( std::forward< F >( f ), error() ) ); + } + + template >::value + ) + > + nsel_constexpr expected< void, detail::transform_invoke_result_t< F, const error_type & > > transform_error( F && f ) const & + { + return has_value() + ? expected< void, detail::transform_invoke_result_t< F, const error_type & > >() + : make_unexpected( detail::invoke( std::forward< F >( f ), error() ) ); + } + +#if !nsel_COMPILER_GNUC_VERSION || nsel_COMPILER_GNUC_VERSION >= 490 + template >::value + ) + > + nsel_constexpr14 expected< void, detail::transform_invoke_result_t< F, error_type && > > transform_error( F && f ) && + { + return has_value() + ? expected< void, detail::transform_invoke_result_t< F, error_type && > >() + : make_unexpected( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } + + template >::value + ) + > + nsel_constexpr expected< void, detail::transform_invoke_result_t< F, const error_type && > > transform_error( F && f ) const && + { + return has_value() + ? expected< void, detail::transform_invoke_result_t< F, const error_type && > >() + : make_unexpected( detail::invoke( std::forward< F >( f ), std::move( error() ) ) ); + } +#endif +#endif // nsel_P2505R >= 3 + +// template constexpr 'see below' unwrap() const&; +// +// template 'see below' unwrap() &&; + + // factories + +// template< typename Ex, typename F> +// expected catch_exception(F&& f); +// +// template< typename F> +// expected map(F&& func) ; +// +// template< typename F> +// 'see below' bind(F&& func) ; +// +// template< typename F> +// expected catch_error(F&& f); +// +// template< typename F> +// 'see below' then(F&& func); + +private: + detail::storage_t + < + void + , E + , std::is_copy_constructible::value + , std::is_move_constructible::value + > + contained; +}; + +// x.x.4.6 expected<>: comparison operators + +template< typename T1, typename E1, typename T2, typename E2 + nsel_REQUIRES_T( + !std::is_void::value && !std::is_void::value + ) +> +constexpr bool operator==( expected const & x, expected const & y ) +{ + return bool(x) != bool(y) ? false : bool(x) ? *x == *y : x.error() == y.error(); +} + +template< typename T1, typename E1, typename T2, typename E2 + nsel_REQUIRES_T( + std::is_void::value && std::is_void::value + ) +> +constexpr bool operator==( expected const & x, expected const & y ) +{ + return bool(x) != bool(y) ? false : bool(x) || static_cast( x.error() == y.error() ); +} + +template< typename T1, typename E1, typename T2, typename E2 > +constexpr bool operator!=( expected const & x, expected const & y ) +{ + return !(x == y); +} + +#if nsel_P0323R <= 2 + +template< typename T, typename E > +constexpr bool operator<( expected const & x, expected const & y ) +{ + return (!y) ? false : (!x) ? true : *x < *y; +} + +template< typename T, typename E > +constexpr bool operator>( expected const & x, expected const & y ) +{ + return (y < x); +} + +template< typename T, typename E > +constexpr bool operator<=( expected const & x, expected const & y ) +{ + return !(y < x); +} + +template< typename T, typename E > +constexpr bool operator>=( expected const & x, expected const & y ) +{ + return !(x < y); +} + +#endif + +// x.x.4.7 expected: comparison with T + +template< typename T1, typename E1, typename T2 + nsel_REQUIRES_T( + !std::is_void::value + ) +> +constexpr bool operator==( expected const & x, T2 const & v ) +{ + return bool(x) ? *x == v : false; +} + +template< typename T1, typename E1, typename T2 + nsel_REQUIRES_T( + !std::is_void::value + ) +> +constexpr bool operator==(T2 const & v, expected const & x ) +{ + return bool(x) ? v == *x : false; +} + +template< typename T1, typename E1, typename T2 > +constexpr bool operator!=( expected const & x, T2 const & v ) +{ + return bool(x) ? *x != v : true; +} + +template< typename T1, typename E1, typename T2 > +constexpr bool operator!=( T2 const & v, expected const & x ) +{ + return bool(x) ? v != *x : true; +} + +#if nsel_P0323R <= 2 + +template< typename T, typename E > +constexpr bool operator<( expected const & x, T const & v ) +{ + return bool(x) ? *x < v : true; +} + +template< typename T, typename E > +constexpr bool operator<( T const & v, expected const & x ) +{ + return bool(x) ? v < *x : false; +} + +template< typename T, typename E > +constexpr bool operator>( T const & v, expected const & x ) +{ + return bool(x) ? *x < v : false; +} + +template< typename T, typename E > +constexpr bool operator>( expected const & x, T const & v ) +{ + return bool(x) ? v < *x : false; +} + +template< typename T, typename E > +constexpr bool operator<=( T const & v, expected const & x ) +{ + return bool(x) ? ! ( *x < v ) : false; +} + +template< typename T, typename E > +constexpr bool operator<=( expected const & x, T const & v ) +{ + return bool(x) ? ! ( v < *x ) : true; +} + +template< typename T, typename E > +constexpr bool operator>=( expected const & x, T const & v ) +{ + return bool(x) ? ! ( *x < v ) : false; +} + +template< typename T, typename E > +constexpr bool operator>=( T const & v, expected const & x ) +{ + return bool(x) ? ! ( v < *x ) : true; +} + +#endif // nsel_P0323R + +// x.x.4.8 expected: comparison with unexpected_type + +template< typename T1, typename E1 , typename E2 > +constexpr bool operator==( expected const & x, unexpected_type const & u ) +{ + return (!x) ? x.get_unexpected() == u : false; +} + +template< typename T1, typename E1 , typename E2 > +constexpr bool operator==( unexpected_type const & u, expected const & x ) +{ + return ( x == u ); +} + +template< typename T1, typename E1 , typename E2 > +constexpr bool operator!=( expected const & x, unexpected_type const & u ) +{ + return ! ( x == u ); +} + +template< typename T1, typename E1 , typename E2 > +constexpr bool operator!=( unexpected_type const & u, expected const & x ) +{ + return ! ( x == u ); +} + +#if nsel_P0323R <= 2 + +template< typename T, typename E > +constexpr bool operator<( expected const & x, unexpected_type const & u ) +{ + return (!x) ? ( x.get_unexpected() < u ) : false; +} + +template< typename T, typename E > +constexpr bool operator<( unexpected_type const & u, expected const & x ) +{ + return (!x) ? ( u < x.get_unexpected() ) : true ; +} + +template< typename T, typename E > +constexpr bool operator>( expected const & x, unexpected_type const & u ) +{ + return ( u < x ); +} + +template< typename T, typename E > +constexpr bool operator>( unexpected_type const & u, expected const & x ) +{ + return ( x < u ); +} + +template< typename T, typename E > +constexpr bool operator<=( expected const & x, unexpected_type const & u ) +{ + return ! ( u < x ); +} + +template< typename T, typename E > +constexpr bool operator<=( unexpected_type const & u, expected const & x) +{ + return ! ( x < u ); +} + +template< typename T, typename E > +constexpr bool operator>=( expected const & x, unexpected_type const & u ) +{ + return ! ( u > x ); +} + +template< typename T, typename E > +constexpr bool operator>=( unexpected_type const & u, expected const & x ) +{ + return ! ( x > u ); +} + +#endif // nsel_P0323R + +/// x.x.x Specialized algorithms + +template< typename T, typename E + nsel_REQUIRES_T( + ( std::is_void::value || std::is_move_constructible::value ) + && std::is_move_constructible::value + && std17::is_swappable::value + && std17::is_swappable::value ) +> +void swap( expected & x, expected & y ) noexcept ( noexcept ( x.swap(y) ) ) +{ + x.swap( y ); +} + +#if nsel_P0323R <= 3 + +template< typename T > +constexpr auto make_expected( T && v ) -> expected< typename std::decay::type > +{ + return expected< typename std::decay::type >( std::forward( v ) ); +} + +// expected specialization: + +auto inline make_expected() -> expected +{ + return expected( in_place ); +} + +template< typename T > +constexpr auto make_expected_from_current_exception() -> expected +{ + return expected( make_unexpected_from_current_exception() ); +} + +template< typename T > +auto make_expected_from_exception( std::exception_ptr v ) -> expected +{ + return expected( unexpected_type( std::forward( v ) ) ); +} + +template< typename T, typename E > +constexpr auto make_expected_from_error( E e ) -> expected::type> +{ + return expected::type>( make_unexpected( e ) ); +} + +template< typename F + nsel_REQUIRES_T( ! std::is_same::type, void>::value ) +> +/*nsel_constexpr14*/ +auto make_expected_from_call( F f ) -> expected< typename std::result_of::type > +{ + try + { + return make_expected( f() ); + } + catch (...) + { + return make_unexpected_from_current_exception(); + } +} + +template< typename F + nsel_REQUIRES_T( std::is_same::type, void>::value ) +> +/*nsel_constexpr14*/ +auto make_expected_from_call( F f ) -> expected +{ + try + { + f(); + return make_expected(); + } + catch (...) + { + return make_unexpected_from_current_exception(); + } +} + +#endif // nsel_P0323R + +} // namespace expected_lite + +using namespace expected_lite; + +// using expected_lite::expected; +// using ... + +} // namespace nonstd + +namespace std { + +// expected: hash support + +template< typename T, typename E > +struct hash< nonstd::expected > +{ + using result_type = std::size_t; + using argument_type = nonstd::expected; + + constexpr result_type operator()(argument_type const & arg) const + { + return arg ? std::hash{}(*arg) : result_type{}; + } +}; + +// TBD - ?? remove? see spec. +template< typename T, typename E > +struct hash< nonstd::expected > +{ + using result_type = std::size_t; + using argument_type = nonstd::expected; + + constexpr result_type operator()(argument_type const & arg) const + { + return arg ? std::hash{}(*arg) : result_type{}; + } +}; + +// TBD - implement +// bool(e), hash>()(e) shall evaluate to the hashing true; +// otherwise it evaluates to an unspecified value if E is exception_ptr or +// a combination of hashing false and hash()(e.error()). + +template< typename E > +struct hash< nonstd::expected > +{ +}; + +} // namespace std + +namespace nonstd { + +// void unexpected() is deprecated && removed in C++17 + +#if nsel_CPP17_OR_GREATER || nsel_COMPILER_MSVC_VERSION > 141 +template< typename E > +using unexpected = unexpected_type; +#endif + +} // namespace nonstd + +#undef nsel_REQUIRES +#undef nsel_REQUIRES_0 +#undef nsel_REQUIRES_T + +nsel_RESTORE_WARNINGS() + +#endif // nsel_USES_STD_EXPECTED + +#endif // NONSTD_EXPECTED_LITE_HPP diff --git a/include/utilities/bmi/nonstd/expected.tweak.hpp b/include/utilities/bmi/nonstd/expected.tweak.hpp new file mode 100644 index 0000000000..89a8e1a4ad --- /dev/null +++ b/include/utilities/bmi/nonstd/expected.tweak.hpp @@ -0,0 +1,4 @@ +//tweaks for the expected library configuration/build +// see https://github.com/martinmoene/expected-lite/tree/master?tab=readme-ov-file#configuration +// for documentation on configuration +#define nsel_CONFIG_WIN32_LEAN_AND_MEAN 0 diff --git a/include/utilities/bmi/protocol.hpp b/include/utilities/bmi/protocol.hpp new file mode 100644 index 0000000000..d08dd7681c --- /dev/null +++ b/include/utilities/bmi/protocol.hpp @@ -0,0 +1,199 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ +Version 0.3 +Remove "supported" member variable and added is_supported() method to protocol interface + +Version 0.2 +Enumerate protocol error types and add ProtocolError exception class +Implement error handling via expected and error_or_warning +Removed model member and required model reference in run(), check_support(), and initialize() +Minor refactoring and style changes + +Version 0.1 +Virtual interface for BMI protocols +*/ + +#pragma once + +#include "Bmi_Adapter.hpp" +#include "JSONProperty.hpp" +#include + +namespace models{ namespace bmi{ namespace protocols{ +using nonstd::expected; +using nonstd::make_unexpected; + +enum class Error{ + UNITIALIZED_MODEL, + UNSUPPORTED_PROTOCOL, + INTEGRATION_ERROR, + PROTOCOL_ERROR, + PROTOCOL_WARNING +}; + +class ProtocolError: public std::exception { + public: + ProtocolError () = delete; + ProtocolError(Error err, const std::string& message="") : err(std::move(err)), message(std::move(message)) {} + ProtocolError(const ProtocolError& other) = default; + ProtocolError(ProtocolError&& other) noexcept = default; + ProtocolError& operator=(const ProtocolError& other) = default; + ProtocolError& operator=(ProtocolError&& other) noexcept = default; + ~ProtocolError() = default; + + auto to_string() const -> std::string { + switch (err) { + case Error::UNITIALIZED_MODEL: return "Error(Uninitialized Model)::" + message; + case Error::UNSUPPORTED_PROTOCOL: return "Warning(Unsupported Protocol)::" + message; + case Error::INTEGRATION_ERROR: return "Error(Integration)::" + message; + case Error::PROTOCOL_ERROR: return "Error(Protocol)::" + message; + case Error::PROTOCOL_WARNING: return "Warning(Protocol)::" + message; + default: return "Unknown Error: " + message; + } + } + + auto error_code() const -> const Error& { return err; } + auto get_message() const -> const std::string& { return message; } + + char const *what() const noexcept override { + message = to_string(); + return message.c_str(); + } + + private: + Error err; + mutable std::string message; +}; + +struct Context{ + const int current_time_step; + const int total_steps; + const std::string& timestamp; + const std::string& id; +}; + +using ModelPtr = std::shared_ptr; +using Properties = geojson::PropertyMap; + +class NgenBmiProtocol{ + /** + * @brief Abstract interface for a generic BMI protocol + * + */ + + public: + + virtual ~NgenBmiProtocol() = default; + + protected: + /** + * @brief Handle a ProtocolError by either throwing it or logging it as a warning + * + * @param err The ProtocolError to handle + * @return expected Returns an empty expected if the error was logged as a warning, + * otherwise throws the ProtocolError. + * + * @throws ProtocolError if the error is of type PROTOCOL_ERROR + */ + static auto error_or_warning(const ProtocolError& err) -> expected { + // Log warnings, but throw errors + switch(err.error_code()){ + case Error::PROTOCOL_ERROR: + throw err; + break; + case Error::INTEGRATION_ERROR: + case Error::UNITIALIZED_MODEL: + case Error::UNSUPPORTED_PROTOCOL: + case Error::PROTOCOL_WARNING: + std::cerr << err.to_string() << std::endl; + return make_unexpected( ProtocolError(std::move(err) ) ); + default: + throw err; + } + assert (false && "Unreachable code reached in error_or_warning"); + } + + /** + * @brief Run the BMI protocol against the given model + * + * Execute the logic of the protocol with the provided context and model. + * It is the caller's responsibility to ensure that the model provided is + * consistent with the model provided to the object's initialize() and + * check_support() methods, hence the protected nature of this function. + * + * @param ctx Contextual information for the protocol run + * @param model A shared pointer to a Bmi_Adapter object which should be + * initialized before being passed to this method. + * + * @return expected May contain a ProtocolError if + * the protocol fails for any reason. Errors of ProtocolError::PROTOCOL_WARNING + * severity should be logged as warnings, but not cause the simulation to fail. + */ + nsel_NODISCARD virtual auto run(const ModelPtr& model, const Context& ctx) const -> expected = 0; + + /** + * @brief Check if the BMI protocol is supported by the model + * + * It is the caller's responsibility to ensure that the model provided is + * consistent with the model provided to the object's initialize() and + * run() methods, hence the protected nature of this function. + * + * @param model A shared pointer to a Bmi_Adapter object which should be + * initialized before being passed to this method. + * + * @return expected May contain a ProtocolError if + * the protocol is not supported by the model. + */ + nsel_NODISCARD virtual expected check_support(const ModelPtr& model) = 0; + + /** + * @brief Initialize the BMI protocol from a set of key/value properties + * + * It is the caller's responsibility to ensure that the model provided is + * consistent with the model provided to the object's run() and + * check_support() methods, hence the protected nature of this function. + * + * @param properties key/value pairs for initializing the protocol + * @param model A shared pointer to a Bmi_Adapter object which should be + * initialized before being passed to this method. + * + * @return expected May contain a ProtocolError if + * initialization fails for any reason, since the protocol must + * be effectively "optional", failed initialization results in + * the protocol being disabled for the duration of the simulation. + */ + virtual auto initialize(const ModelPtr& model, const Properties& properties) -> expected = 0; + + /** + * @brief Whether the protocol is supported by the model + * + */ + virtual bool is_supported() const = 0; + + /** + * @brief Friend class for managing one or more protocols + * + * This allows the NgenBmiProtocols container class to access the protected `run()` + * method. This allows the container to ensure consistent application of the + * protocol with a particular bmi model instance throughout the lifecycle of a given + * protocol. + * + */ + friend class NgenBmiProtocols; +}; + +}}} diff --git a/include/utilities/bmi/protocols.hpp b/include/utilities/bmi/protocols.hpp new file mode 100644 index 0000000000..723fcf1364 --- /dev/null +++ b/include/utilities/bmi/protocols.hpp @@ -0,0 +1,100 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ +Version 0.2.1 +Fix NgenBmiProtocols constructor to initialize protocols map when model is null +Fix run() to return the expected returned by the wrapped call + +Version 0.2 +Enumerate protocol types/names +The container now holds a single model pointer and passes it to each protocol +per the updated (v0.2) protocol interface +Keep protocols in a map for dynamic access by enumeration name +add operator<< for Protocol enum + +Version 0.1 +Container and management for abstract BMI protocols +*/ +#pragma once + +#include +#include +#include +#include +#include "Bmi_Adapter.hpp" +#include "JSONProperty.hpp" + +#include "mass_balance.hpp" + + +namespace models{ namespace bmi{ namespace protocols{ + +enum class Protocol { + MASS_BALANCE +}; + +auto operator<<(std::ostream& os, Protocol p) -> std::ostream&; + +class NgenBmiProtocols { + /** + * @brief Container and management interface for BMI protocols for use in ngen + * + */ + + public: + /** + * @brief Construct a new Ngen Bmi Protocols object with a null model + * + */ + NgenBmiProtocols(); + + /** + * @brief Construct a new Ngen Bmi Protocols object for use with a known model + * + * @param model An initialized BMI model + * @param properties Properties for each protocol being initialized + */ + NgenBmiProtocols(std::shared_ptr model, const geojson::PropertyMap& properties); + + /** + * @brief Run a specific BMI protocol by name with a given context + * + * @param protocol_name The name of the protocol to run + * @param ctx The context of the current protocol run + * + * @return expected May contain a ProtocolError if + * the protocol fails for any reason. + */ + auto run(const Protocol& protocol_name, const Context& ctx) const -> expected; + + private: + + /** + * @brief All protocols managed by this container will utilize the same model + * + * This reduces the amount of pointer copying and references across a large simulation + * and it ensures that all protocols see the same model state. + * + */ + std::shared_ptr model; + /** + * @brief Map of protocol name to protocol instance + * + */ + std::unordered_map> protocols; + }; + +}}} diff --git a/src/core/Layer.cpp b/src/core/Layer.cpp index 56d6506be6..aac9ced099 100644 --- a/src/core/Layer.cpp +++ b/src/core/Layer.cpp @@ -28,6 +28,8 @@ void ngen::Layer::update_models(boost::span catchment_outflows, double response(0.0); try { response = r_c->get_response(output_time_index, simulation_time.get_output_interval_seconds()); + // Check mass balance if able + r_c->check_mass_balance(output_time_index, simulation_time.get_total_output_times(), current_timestamp); } catch(models::external::State_Exception& e) { std::string msg = e.what(); @@ -36,6 +38,13 @@ void ngen::Layer::update_models(boost::span catchment_outflows, +" at feature id "+id; throw models::external::State_Exception(msg); } + catch(std::exception& e){ + std::string msg = e.what(); + msg = msg+" at timestep "+std::to_string(output_time_index) + +" ("+current_timestamp+")" + +" at feature id "+id; + throw std::runtime_error(msg); + } #if NGEN_WITH_ROUTING int results_index = catchment_indexes[id]; catchment_outflows[results_index] += response; diff --git a/src/core/nexus/HY_PointHydroNexusRemote.cpp b/src/core/nexus/HY_PointHydroNexusRemote.cpp index c0e3fa3c16..3d5c97a703 100644 --- a/src/core/nexus/HY_PointHydroNexusRemote.cpp +++ b/src/core/nexus/HY_PointHydroNexusRemote.cpp @@ -18,7 +18,7 @@ void MPI_Handle_Error(int status) } else { - MPI_Abort(MPI_COMM_WORLD,1); + MPI_Abort(MPI_COMM_WORLD, status); } } @@ -92,9 +92,9 @@ HY_PointHydroNexusRemote::HY_PointHydroNexusRemote(std::string nexus_id, Catchme HY_PointHydroNexusRemote::~HY_PointHydroNexusRemote() { - long wait_time = 0; - - // This destructore might be called after MPI_Finalize so do not attempt communication if + const unsigned int timeout = 120000; // timeout threshold in milliseconds + unsigned int wait_time = 0; + // This destructor might be called after MPI_Finalize so do not attempt communication if // this has occured int mpi_finalized; MPI_Finalized(&mpi_finalized); @@ -105,14 +105,46 @@ HY_PointHydroNexusRemote::~HY_PointHydroNexusRemote() process_communications(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - + if( wait_time < timeout && wait_time > 0 ){ // don't sleep if first call clears comms! + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + else + { + std::cerr << "HY_PointHydroNexusRemote: "<< id + << " destructor timed out after " << timeout/1000 + << " seconds waiting on pending MPI communications\n"; + // The return is is probably best, logging the error. + // There is no good way to recover from this. + // Throwing an exception from destructors is generally not a good idea + // as it can lead to undefined behavior. + // and using std::exit forces the program to terminate immediately, + // even if this situation is recoverable/acceptable in some cases. + return; + } wait_time += 1; + MPI_Finalized(&mpi_finalized); + } +} - if ( wait_time > 120000 ) +void HY_PointHydroNexusRemote::post_receives() +{ + // Post receives if not already posted (for pure receiver nexuses) + if (stored_receives.empty()) + { + for (int rank : upstream_ranks) { - // TODO log warning message that some comunications could not complete - + stored_receives.push_back({}); + stored_receives.back().buffer = std::make_shared(); + int tag = extract(id); + + MPI_Handle_Error(MPI_Irecv( + stored_receives.back().buffer.get(), + 1, + time_step_and_flow_type, + rank, + tag, + MPI_COMM_WORLD, + &stored_receives.back().mpi_request)); } } } @@ -130,31 +162,15 @@ double HY_PointHydroNexusRemote::get_downstream_flow(std::string catchment_id, t } else if ( type == receiver || type == sender_receiver ) { - for ( int rank : upstream_ranks ) - { - int status; - - stored_receives.resize(stored_receives.size() + 1); - stored_receives.back().buffer = std::make_shared(); - - int tag = extract(id); - - //Receive downstream_flow from Upstream Remote Nexus to this Downstream Remote Nexus - status = MPI_Irecv( - stored_receives.back().buffer.get(), - 1, - time_step_and_flow_type, - rank, - tag, - MPI_COMM_WORLD, - &stored_receives.back().mpi_request); - - MPI_Handle_Error(status); - - //std::cerr << "Creating receive with target_rank=" << rank << " on tag=" << tag << "\n"; - } - - //std::cerr << "Waiting on receives\n"; + post_receives(); + // Wait for receives to complete + // This ensures all upstream flows are received before returning + // and that we have matched all sends with receives for a given time step. + // As long as the functions are called appropriately, e.g. one call to + // `add_upstream_flow` per upstream catchment per time step, followed + // by a call to `get_downstream_flow` for each downstream catchment per time step, + // this loop will terminate and ensures the synchronization of flows between + // ranks. while ( stored_receives.size() > 0 ) { process_communications(); @@ -167,6 +183,28 @@ double HY_PointHydroNexusRemote::get_downstream_flow(std::string catchment_id, t void HY_PointHydroNexusRemote::add_upstream_flow(double val, std::string catchment_id, time_step_t t) { + // Process any completed communications to free resources + // If no communications are pending, this call will do nothing. + process_communications(); + // NOTE: It is possible for a partition to get "too far" ahead since the sends are now + // truely asynchronous. For pure receivers and sender_receivers, this isn't a problem + // because the get_downstream_flow function will block until all receives are processed. + // However, for pure senders, this could be a problem. + // We can use this spinlock here to limit how far ahead a partition can get. + // in this case, approximately 100 time steps per downstream catchment... + while( stored_sends.size() > downstream_ranks.size()*100 ) + { + process_communications(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + // Post receives before sending to prevent deadlock + // When stored_receives is empty, we need to post for incoming messages + if ((type == receiver || type == sender_receiver) && stored_receives.empty()) + { + post_receives(); + } + // first add flow to local copy HY_PointHydroNexus::add_upstream_flow(val, catchment_id, t); @@ -205,23 +243,25 @@ void HY_PointHydroNexusRemote::add_upstream_flow(double val, std::string catchme int tag = extract(id); //Send downstream_flow from this Upstream Remote Nexus to the Downstream Remote Nexus - MPI_Isend( + MPI_Handle_Error( + MPI_Isend( stored_sends.back().buffer.get(), 1, time_step_and_flow_type, *downstream_ranks.begin(), //TODO currently only support a SINGLE downstream message pairing tag, MPI_COMM_WORLD, - &stored_sends.back().mpi_request); - - //std::cerr << "Creating send with target_rank=" << *downstream_ranks.begin() << " on tag=" << tag << "\n"; + &stored_sends.back().mpi_request) + ); + //std::cerr << "Creating send with target_rank=" << *downstream_ranks.begin() << " on tag=" << tag << "\n"; - while ( stored_sends.size() > 0 ) - { - process_communications(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } + // Send is async, the next call to add_upstream_flow will test and ensure the send has completed + // and free the memory associated with the send. + // This prevents a potential deadlock situation where a send isn't able to complete + // because the remote receiver is also trying to send and the underlying mpi buffers/protocol + // are forced into a rendevous protocol. So we ensure that we always post receives before sends. + // and that we always test for completed sends before freeing the memory associated with the send. } } } diff --git a/src/geopackage/CMakeLists.txt b/src/geopackage/CMakeLists.txt index f0dee4aa70..0e544b800b 100644 --- a/src/geopackage/CMakeLists.txt +++ b/src/geopackage/CMakeLists.txt @@ -9,4 +9,4 @@ add_library(geopackage proj.cpp 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 sqlite3 NGen::logging) +target_link_libraries(geopackage PUBLIC NGen::geojson Boost::boost SQLite::SQLite3 NGen::logging) diff --git a/src/realizations/catchment/Bmi_Module_Formulation.cpp b/src/realizations/catchment/Bmi_Module_Formulation.cpp index 7c541e2d50..da478a294e 100644 --- a/src/realizations/catchment/Bmi_Module_Formulation.cpp +++ b/src/realizations/catchment/Bmi_Module_Formulation.cpp @@ -586,6 +586,9 @@ namespace realization { available_forcing_units[bmi_var_names_map[output_var_name]] = get_bmi_model()->GetVarUnits(output_var_name); //units come from the model output variable. } } + //Initialize all NgenBmiProtocols with the valid adapter pointer and any properties + //provided in the read configuration. + bmi_protocols = models::bmi::protocols::NgenBmiProtocols(get_bmi_model(), properties); } //check if units have not been specified. If not, default to native units. diff --git a/src/realizations/catchment/CMakeLists.txt b/src/realizations/catchment/CMakeLists.txt index db9bc0f04d..48771b18bf 100644 --- a/src/realizations/catchment/CMakeLists.txt +++ b/src/realizations/catchment/CMakeLists.txt @@ -21,5 +21,6 @@ target_link_libraries(realizations_catchment PUBLIC NGen::geojson NGen::logging NGen::ngen_bmi + NGen::bmi_protocols ) diff --git a/src/utilities/bmi/CMakeLists.txt b/src/utilities/bmi/CMakeLists.txt new file mode 100644 index 0000000000..5d86e87f70 --- /dev/null +++ b/src/utilities/bmi/CMakeLists.txt @@ -0,0 +1,37 @@ +# Author: Nels Frazier +# Copyright (C) 2025 Lynker +# ------------------------------------------------------------------------ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------ +# Library Version 0.1 +# BMI mass balance checking protocol + +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 + ${PROJECT_SOURCE_DIR}/include/bmi + ${PROJECT_SOURCE_DIR}/include/utilities/bmi + ${PROJECT_SOURCE_DIR}/include/geojson + ${NGEN_INC_DIR} +) + +target_link_libraries(ngen_bmi_protocols + PUBLIC + ${CMAKE_DL_LIBS} + Boost::boost # Headers-only Boost + NGen::logging +) + +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 new file mode 100644 index 0000000000..f542a682a0 --- /dev/null +++ b/src/utilities/bmi/mass_balance.cpp @@ -0,0 +1,195 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ +Version 0.3 (see mass_balance.hpp for details) + +Version 0.2 +Implement error handling via expected and error_or_warning + +Version 0.1 +Implementation the BMI mass balance checking protocol +*/ + +#include "mass_balance.hpp" + +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); +} + +NgenMassBalance::NgenMassBalance() : check(false) {} + +NgenMassBalance::~NgenMassBalance() = default; + +auto NgenMassBalance::run(const ModelPtr& model, const Context& ctx) const -> expected { + if( model == nullptr ) { + return make_unexpected( ProtocolError( + Error::UNITIALIZED_MODEL, + "Cannot run mass balance protocol with null model." + ) + ); + } else if( !check || !supported ) { + return {}; + } + bool check_step = false; + //if frequency was set to -1 (or any negative), only check at the end + //use <= to avoid a potential divide by zero should frequency be 0 + //(though frequency 0 should have been caught during initialization and check disabled) + if( frequency > 0 ){ + check_step = (ctx.current_time_step % frequency) == 0; + } + else if(ctx.current_time_step == ctx.total_steps){ + check_step = true; + } + + if(check_step) { + double mass_in, mass_out, mass_stored, mass_leaked, mass_balance; + model->GetValue(INPUT_MASS_NAME, &mass_in); + model->GetValue(OUTPUT_MASS_NAME, &mass_out); + model->GetValue(STORED_MASS_NAME, &mass_stored); + model->GetValue(LEAKED_MASS_NAME, &mass_leaked); + // TODO consider unit conversion if/when it becomes necessary + mass_balance = mass_in - mass_out - mass_stored - mass_leaked; + if ( std::abs(mass_balance) > tolerance || std::isnan(mass_balance)) { + std::stringstream ss; + ss << "mass_balance: " + << "at timestep " << std::to_string(ctx.current_time_step) + << " ("+ctx.timestamp+")" + << " at feature id " << ctx.id <GetComponentName() << "\n\t" << + INPUT_MASS_NAME << "(" << mass_in << ") - " << + OUTPUT_MASS_NAME << " (" << mass_out << ") - " << + STORED_MASS_NAME << " (" << mass_stored << ") - " << + LEAKED_MASS_NAME << " (" << mass_leaked << ") = " << + mass_balance << "\n\t" << "tolerance: " << tolerance << std::endl; + return make_unexpected( ProtocolError( + is_fatal ? Error::PROTOCOL_ERROR : Error::PROTOCOL_WARNING, + ss.str() + ) + ); + } + + } + return {}; +} + +auto NgenMassBalance::check_support(const ModelPtr& model) -> expected { + if (model != nullptr && model->is_model_initialized()) { + double mass_var; + std::vector units; + units.reserve(4); + try{ + for(const auto& name : + {INPUT_MASS_NAME, OUTPUT_MASS_NAME, STORED_MASS_NAME, LEAKED_MASS_NAME} + ){ + model->GetValue(name, &mass_var); + units.push_back( model->GetVarUnits(name) ); + } + //Compare all other units to the first one (+1) + if( std::equal( units.begin()+1, units.end(), units.begin() ) ) { + this->supported = true; + return {}; + } + else{ + // It may be possible to do unit conversion and still do meaninful mass balance + // this could be added as an extended feature, but for now, I don't think this is + // worth the complexity. It is, however, worth the sanity check performed here + // to ensure the units are consistent. + return make_unexpected( ProtocolError( + Error::INTEGRATION_ERROR, + "mass_balance: variables have incosistent units, cannot perform mass balance." + ) + ); + } + } catch (const std::exception &e) { + std::stringstream ss; + ss << "mass_balance: Error getting mass balance values for module '" << model->GetComponentName() << "': " << e.what() << std::endl; + return make_unexpected( ProtocolError( + Error::INTEGRATION_ERROR, + ss.str() + ) + ); + } + } else { + return make_unexpected( ProtocolError( + Error::UNITIALIZED_MODEL, + "Cannot check mass balance for uninitialized model. Disabling mass balance protocol." + ) + ); + } + return {}; +} + +auto NgenMassBalance::initialize(const ModelPtr& model, const Properties& properties) -> expected +{ + //now check if the user has requested to use mass balance + auto protocol_it = properties.find(CONFIGURATION_KEY); + if ( protocol_it != properties.end() ) { + geojson::PropertyMap mass_bal = protocol_it->second.get_values(); + + auto _it = mass_bal.find(TOLERANCE_KEY); + if( _it != mass_bal.end() ) tolerance = _it->second.as_real_number(); + //as_real_number() *should* return a floating point NaN representation + //if the input value were presented as "NaN" -- non numberic values + //non numberic values will throw an exception (not handled here) + //it is expected that the user/configuration is responsible for providing + //a valid numeric value for tolerance + if( std::isnan(tolerance) ) { + check = false; //disable mass balance checking + return error_or_warning( ProtocolError( + Error::PROTOCOL_WARNING, + "mass_balance: tolerance value 'NaN' provided, disabling mass balance check." + ) + ); + } + _it = mass_bal.find(FATAL_KEY); + if( _it != mass_bal.end() ) is_fatal = _it->second.as_boolean(); + + _it = mass_bal.find(CHECK_KEY); + if( _it != mass_bal.end() ) { + check = _it->second.as_boolean(); + } else { + //default to true if not specified + check = true; + } + + _it = mass_bal.find(FREQUENCY_KEY); + if( _it != mass_bal.end() ){ + frequency = _it->second.as_natural_number(); + } else { + frequency = 1; //default, check every timestep + } + if ( frequency == 0 ) { + check = false; // can't check at frequency 0, disable mass balance checking + } + } else{ + //no mass balance requested, or not supported, so don't check it + check = false; + } + if ( check ) { + //Ensure the model is capable of mass balance using the protocol + check_support(model).or_else( error_or_warning ); + } + return {}; // important to return for the expected to be properly created! +} + +bool NgenMassBalance::is_supported() const { + return this->supported; +} + +}}} // end namespace models::bmi::protocols diff --git a/src/utilities/bmi/protocols.cpp b/src/utilities/bmi/protocols.cpp new file mode 100644 index 0000000000..c78651a192 --- /dev/null +++ b/src/utilities/bmi/protocols.cpp @@ -0,0 +1,69 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ +Version 0.2.1 (See bmi/protocols.hpp for details) + +Version 0.2 +Implement error handling via expected and error_or_warning + +Version 0.1 +Container and management for abstract BMI protocols +*/ + +#include "protocols.hpp" + +namespace models{ namespace bmi{ namespace protocols{ + +auto operator<<(std::ostream& os, Protocol p) -> std::ostream& { + switch(p) { + case Protocol::MASS_BALANCE: os << "MASS_BALANCE"; break; + default: os << "UNKNOWN_PROTOCOL"; break; + } + return os; +} + +NgenBmiProtocols::NgenBmiProtocols() + : model(nullptr) { + protocols[Protocol::MASS_BALANCE] = std::make_unique(); +} + +NgenBmiProtocols::NgenBmiProtocols(ModelPtr model, const geojson::PropertyMap& properties) + : model(model) { + //Create and initialize mass balance configurable properties + protocols[Protocol::MASS_BALANCE] = std::make_unique(model, properties); +} + +auto NgenBmiProtocols::run(const Protocol& protocol_name, const Context& ctx) const -> expected { + // Consider using find() vs switch, especially if the number of protocols grows + expected result_or_err; + switch(protocol_name){ + case Protocol::MASS_BALANCE: + return protocols.at(Protocol::MASS_BALANCE)->run(model, ctx) + .or_else( NgenBmiProtocol::error_or_warning ); + break; + default: + std::stringstream ss; + ss << "Error: Request for unsupported protocol: '" << protocol_name << "'."; + return NgenBmiProtocol::error_or_warning( ProtocolError( + Error::UNSUPPORTED_PROTOCOL, + ss.str() + ) + ); + } + return {}; +} + +}}} // end namespace models::bmi::protocols diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 623dd7a481..a6b26ad200 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -207,6 +207,20 @@ if(TARGET test_forcings_engine) add_compile_definitions(NGEN_LUMPED_CONFIG_PATH="${CMAKE_CURRENT_BINARY_DIR}/config_aorc.yml") endif() +########################## BMI Protocol Unit Tests + +ngen_add_test( + test_bmi_protocols + OBJECTS + utils/bmi/mass_balance_Test.cpp + LIBRARIES + NGen::bmi_protocols + NGen::ngen_bmi + gmock + DEPENDS + testbmicppmodel +) + ########################## Series Unit Tests ngen_add_test( test_mdarray diff --git a/test/core/nexus/NexusRemoteTests.cpp b/test/core/nexus/NexusRemoteTests.cpp index 25964759b2..8904602b09 100644 --- a/test/core/nexus/NexusRemoteTests.cpp +++ b/test/core/nexus/NexusRemoteTests.cpp @@ -683,4 +683,438 @@ TEST_F(Nexus_Remote_Test, DISABLED_TestTree1) } + +/****************************************************************************** + * MPI DEADLOCK TESTS + * ================== + * As of commit 9ad6b7bf561fb2f96065511b382bc43a83167f10 + * A potential MPI deadlock scenario was discovered in the HY_PointHydroNexusRemote class. + * + * These tests demonstrate and verify a fix for MPI communication issues + * observed in production when running large domains with many partitions. + * + * ============================================================================= + * What can be shown: + * ============================================================================= + * + * 1. Deadlock prone code: + * The original HY_PointHydroNexusRemote::add_upstream_flow() does: + * a. MPI_Isend() - non-blocking, returns immediately + * b. while(stored_sends.size() > 0) { MPI_Test(); } - BLOCKS until + * the send is confirmed complete by MPI + * + * This pattern is problematic because it blocks progress until the send + * completes, preventing the code from posting receives. + * + * 2. WITH FORCED RENDEZVOUS, THIS PATTERN DEADLOCKS: + * When we force rendezvous protocol using --mca btl_tcp_eager_limit 80, + * the buggy pattern reliably deadlocks. Rendezvous requires a posted + * MPI_Irecv before the send can complete. + * + * 3. PRODUCTION RUNS WERE HANGING: + * Large-scale runs with 384+ partitions and 831K catchments __across multiple nodes__ + * were observed to be hanging (one node was making progress while others were not). + * + * 4. THE FIX IS CORRECT: + * Pre-posting receives (calling MPI_Irecv before MPI_Isend) is the standard + * MPI best practice and eliminates any rendezvous-related deadlock risk. + * + * ============================================================================= + * Assumptions about this fix that are hard to confirm: + * ============================================================================= + * + * We ASSUMED that production hangs occurred because: + * - High connection count (~38,000 connections) exhausted the eager buffer pool + * - This forced MPI to use rendezvous protocol even for small messages + * - Rendezvous + buggy pattern = deadlock + * + * HOWEVER: MPI documentation consistently states that rendezvous protocol is + * triggered by MESSAGE SIZE exceeding eager_limit, NOT by buffer pool exhaustion. + * We cannot find documentation supporting the "pool exhaustion triggers + * rendezvous" theory. + * + * Other possibilities for production hangs (unconfirmed) + * - TCP buffer exhaustion: if the sender's socket buffer fills up before + * the receiver can process messages, subsequent sends can block. + * And if two or more nodes get into this state, they can deadlock. + * - Network fabric issues at scale + * - Something else entirely that our fix happened to address + * + * ============================================================================= + * THE FIX IS STILL CORRECT: + * ============================================================================= + * + * Regardless of the exact production trigger, pre-posting receives is: + * 1. MPI best practice for avoiding deadlock + * 2. Required for correctness under rendezvous protocol + * 3. Harmless under eager protocol (just posts receives earlier) + * + * The fix eliminates a class of potential deadlocks even if we're uncertain + * about the exact mechanism that triggered the production hang. + * + * ============================================================================= + * TRIGGERING THE DEADLOCK IN TESTS: + * ============================================================================= + * + * We use --mca btl_tcp_eager_limit 80 to FORCE rendezvous protocol per-message. + * This is a TEST WORKAROUND that demonstrates the buggy pattern CAN deadlock. + * + * This is NOT necessarily the exact production failure mode - it's a way to + * reliably trigger the deadlock pattern in a controlled test environment. + * + * To reproduce in tests, force tcp communcation and set the eager limit to + * the minimum value (80 bytes -- openmpi_info may show different limits for + * different BTLs/environments). + * + * mpirun --mca btl tcp,self --mca btl_tcp_eager_limit 80 ... + * + * Parameters: + * --mca btl tcp,self : Disable shared memory, use TCP only + * --mca btl_tcp_eager_limit 80 : Force rendezvous per-message (minimum value) + * + * NOTE: Shared memory BTL uses copy-based communication that doesn't require + * a synchronous handshake, so --mca btl_sm_eager_limit 80 will NOT trigger + * the deadlock. + * + ******************************************************************************/ + + +/** + * ============================================================================= + * DISABLED_TestRawMpiDeadlockPattern + * ============================================================================= + * + * PURPOSE: Document and demonstrate the MPI communication pattern that causes + * deadlock when rendezvous protocol is triggered. + * This is the pattern that ngen used up to and including + * commit 9ad6b7bf561fb2f96065511b382bc43a83167f10 + * This test uses RAW MPI calls (not the HY_PointHydroNexusRemote class) to + * clearly illustrate the problematic pattern that existed in the original code. + * + * ============================================================================= + * THE PROBLEMATIC PATTERN (from original add_upstream_flow): + * ============================================================================= + * 1. MPI_Isend (non-blocking send) + * 2. Loop on MPI_Test waiting for send to complete <-- BLOCKS HERE + * 3. MPI_Irecv (never reached if step 2 blocks) + * + * ============================================================================= + * WHY NGEN PARTITIONS CREATE BIDIRECTIONAL COMMUNICATION: + * ============================================================================= + * + * Real hydrological networks are complex. When we partition the domain, the + * partition boundary cuts ACROSS the drainage network, not along it. + * This creates BIDIRECTIONAL communication between partitions. + * + * Analysis of CONUS (384 partitions, 831K catchments) confirms: + * - 182/384 partitions (47%) have BIDIRECTIONAL communication + * + * ============================================================================= + * TEST TOPOLOGY (simplified bidirectional chain): + * ============================================================================= + * + * Rank 0 <────> Rank 1 <────> Rank 2 <────> Rank 3 + * + * Each rank both SENDS to AND RECEIVES from its neighbors. + * + * ============================================================================= + * TO REPRODUCE DEADLOCK: + * ============================================================================= + * timeout 10 mpirun --mca btl tcp,self --mca btl_tcp_eager_limit 80 -n 4 \ + * ./test/test_remote_nexus --gtest_filter="*TestRawMpiDeadlockPattern*" \ + * --gtest_also_run_disabled_tests + * + * EXPECTED: Exit code 124 (timeout killed it) - confirms true deadlock + * + * ============================================================================= + */ +TEST_F(Nexus_Remote_Test, DISABLED_TestRawMpiDeadlockPattern) +{ + if (mpi_num_procs < 4) { + GTEST_SKIP() << "Requires at least 4 MPI ranks to demonstrate bidirectional deadlock"; + } + + // Bidirectional chain topology + std::vector downstream_ranks; + std::vector upstream_ranks; + + if (mpi_rank == 0) { + downstream_ranks.push_back(1); + upstream_ranks.push_back(1); + } else if (mpi_rank == 1) { + downstream_ranks.push_back(0); + downstream_ranks.push_back(2); + upstream_ranks.push_back(0); + upstream_ranks.push_back(2); + } else if (mpi_rank == 2) { + downstream_ranks.push_back(1); + downstream_ranks.push_back(3); + upstream_ranks.push_back(1); + upstream_ranks.push_back(3); + } else if (mpi_rank == 3) { + downstream_ranks.push_back(2); + upstream_ranks.push_back(2); + } + + bool is_sender = !downstream_ranks.empty(); + bool is_receiver = !upstream_ranks.empty(); + + // Create MPI datatype for our message + struct message_t { + long time_step; + long id; + double flow; + }; + + MPI_Datatype msg_type; + int counts[3] = {1, 1, 1}; + MPI_Aint displacements[3] = {0, sizeof(long), 2 * sizeof(long)}; + MPI_Datatype types[3] = {MPI_LONG, MPI_LONG, MPI_DOUBLE}; + MPI_Type_create_struct(3, counts, displacements, types, &msg_type); + MPI_Type_commit(&msg_type); + + const int NUM_MESSAGES = 3500; + const int TAG_BASE = 1000; + + std::cerr << "Rank " << mpi_rank << ": Starting bidirectional deadlock pattern\n"; + + MPI_Barrier(MPI_COMM_WORLD); + + // Storage for async operations + std::map> send_buffers; + std::map> recv_buffers; + std::map> send_requests; + std::map> recv_requests; + + for (int r : downstream_ranks) { + send_buffers[r].resize(NUM_MESSAGES); + send_requests[r].resize(NUM_MESSAGES); + } + for (int r : upstream_ranks) { + recv_buffers[r].resize(NUM_MESSAGES); + recv_requests[r].resize(NUM_MESSAGES); + } + + // Timing asymmetry emulates "work" that causes ranks to run at different "speeds" + if (mpi_rank == 1) { + volatile double dummy = 0.0; + for (int i = 0; i < 100000000; ++i) { + dummy += std::sin(i * 0.0001) * std::cos(i * 0.0002); + } + } + + // THE PROBLEMATIC PATTERN: All ranks try to complete sends BEFORE posting receives + if (is_sender) + { + for (int downstream : downstream_ranks) + { + for (int i = 0; i < NUM_MESSAGES; ++i) + { + send_buffers[downstream][i] = {i, mpi_rank, 100.0 + i}; + int tag = TAG_BASE + mpi_rank * 10000 + downstream * 100 + i; + + MPI_Isend(&send_buffers[downstream][i], 1, msg_type, downstream, tag, + MPI_COMM_WORLD, &send_requests[downstream][i]); + + // BLOCKING WAIT - THIS IS THE "BUG"! + // Under eager protocols, this test returns immediately + // Under rendezvous protocols, this test blocks until + // the receiver posts a matching Irecv. + // Similar logic applies to a full TCP buffer, MPI gets blocked waiting + // for TCP buffer to free up, which requires the receiver to + // read the data. + int flag = 0; + while (!flag) { + MPI_Test(&send_requests[downstream][i], &flag, MPI_STATUS_IGNORE); + } + } + } + } + + // Post receives - NEVER REACHED IN DEADLOCK + if (is_receiver) + { + for (int upstream : upstream_ranks) + { + for (int i = 0; i < NUM_MESSAGES; ++i) + { + int tag = TAG_BASE + upstream * 10000 + mpi_rank * 100 + i; + MPI_Irecv(&recv_buffers[upstream][i], 1, msg_type, upstream, tag, + MPI_COMM_WORLD, &recv_requests[upstream][i]); + } + MPI_Waitall(NUM_MESSAGES, recv_requests[upstream].data(), MPI_STATUSES_IGNORE); + } + } + + MPI_Type_free(&msg_type); + MPI_Barrier(MPI_COMM_WORLD); + std::cerr << "Rank " << mpi_rank << ": Test passed (eager buffer was sufficient)\n"; +} + + +/** + * ============================================================================= + * TestRemoteNexusDeadlockFree + * ============================================================================= + * + * PURPOSE: Verify that HY_PointHydroNexusRemote does NOT deadlock, even with + * extremely small MPI eager buffers that force rendezvous protocol. + * + * This test uses the SAME BIDIRECTIONAL topology as DISABLED_TestRawMpiDeadlockPattern + * but uses the HY_PointHydroNexusRemote class instead of raw MPI calls. + * + * ============================================================================= + * THE FIX: AUTO-POSTED RECEIVES + * ============================================================================= + * + * The HY_PointHydroNexusRemote class now auto-posts MPI_Irecv BEFORE sending. + * This breaks the deadlock cycle because peers can always complete their sends. + * + * ============================================================================= + * TO RUN (with small eager buffer to force rendezvous): + * ============================================================================= + * mpirun --mca btl tcp,self --mca btl_tcp_eager_limit 80 -n 4 \ + * ./test/test_remote_nexus --gtest_filter="*TestRemoteNexusDeadlockFree*" + * + * EXPECTED: Exit code 0 - test passes without deadlock + * + * ============================================================================= + */ +TEST_F(Nexus_Remote_Test, TestRemoteNexusDeadlockFree) +{ + if (mpi_num_procs < 4) { + GTEST_SKIP() << "Requires at least 4 MPI ranks for bidirectional topology"; + } + + // Same bidirectional topology as the deadlock test + std::vector downstream_ranks; + std::vector upstream_ranks; + + if (mpi_rank == 0) { + downstream_ranks.push_back(1); + upstream_ranks.push_back(1); + } else if (mpi_rank == 1) { + downstream_ranks.push_back(0); + downstream_ranks.push_back(2); + upstream_ranks.push_back(0); + upstream_ranks.push_back(2); + } else if (mpi_rank == 2) { + downstream_ranks.push_back(1); + downstream_ranks.push_back(3); + upstream_ranks.push_back(1); + upstream_ranks.push_back(3); + } else if (mpi_rank == 3) { + downstream_ranks.push_back(2); + upstream_ranks.push_back(2); + } + + bool is_sender = !downstream_ranks.empty(); + bool is_receiver = !upstream_ranks.empty(); + + const int NUM_CONNECTIONS = 50; + + std::cerr << "Rank " << mpi_rank << ": Setting up bidirectional topology\n"; + + // Create sender nexuses for each downstream rank + std::map>> senders; + for (int downstream : downstream_ranks) + { + for (int i = 0; i < NUM_CONNECTIONS; ++i) + { + int nex_num = mpi_rank * 100000 + downstream * 1000 + i; + std::string nex_id = "nex-" + std::to_string(nex_num); + std::string my_cat = "cat-send-" + std::to_string(mpi_rank) + "-" + std::to_string(downstream) + "-" + std::to_string(i); + std::string their_cat = "cat-recv-" + std::to_string(downstream) + "-" + std::to_string(mpi_rank) + "-" + std::to_string(i); + + HY_PointHydroNexusRemote::catcment_location_map_t loc_map; + loc_map[their_cat] = downstream; + + std::vector receiving = {their_cat}; + std::vector contributing = {my_cat}; + + senders[downstream].push_back(std::make_shared( + nex_id, receiving, contributing, loc_map)); + } + } + + // Create receiver nexuses for each upstream rank + std::map>> receivers; + for (int upstream : upstream_ranks) + { + for (int i = 0; i < NUM_CONNECTIONS; ++i) + { + int nex_num = upstream * 100000 + mpi_rank * 1000 + i; + std::string nex_id = "nex-" + std::to_string(nex_num); + std::string their_cat = "cat-send-" + std::to_string(upstream) + "-" + std::to_string(mpi_rank) + "-" + std::to_string(i); + std::string my_cat = "cat-recv-" + std::to_string(mpi_rank) + "-" + std::to_string(upstream) + "-" + std::to_string(i); + + HY_PointHydroNexusRemote::catcment_location_map_t loc_map; + loc_map[their_cat] = upstream; + + std::vector receiving = {my_cat}; + std::vector contributing = {their_cat}; + + receivers[upstream].push_back(std::make_shared( + nex_id, receiving, contributing, loc_map)); + } + } + + MPI_Barrier(MPI_COMM_WORLD); + + // Timing asymmetry emulates "work" that causes ranks to run at different "speeds" + if (mpi_rank == 1) { + volatile double dummy = 0.0; + for (int i = 0; i < 100000000; ++i) { + dummy += std::sin(i * 0.0001) * std::cos(i * 0.0002); + } + } + + long ts = 0; + double flow_value = 42.0; + + // Send all flows - with the fix, receives are auto-posted before sending + if (is_sender) + { + std::cerr << "Rank " << mpi_rank << ": Starting sends (receives auto-posted by fix)\n"; + + for (auto& kv : senders) + { + int downstream = kv.first; + auto& nexuses = kv.second; + for (int i = 0; i < NUM_CONNECTIONS; ++i) + { + std::string my_cat = "cat-send-" + std::to_string(mpi_rank) + "-" + std::to_string(downstream) + "-" + std::to_string(i); + nexuses[i]->add_upstream_flow(flow_value, my_cat, ts); + } + } + std::cerr << "Rank " << mpi_rank << ": All sends completed!\n"; + } + + // Receive all flows + if (is_receiver) + { + std::cerr << "Rank " << mpi_rank << ": Receiving from upstream\n"; + for (auto& kv : receivers) + { + int upstream = kv.first; + auto& nexuses = kv.second; + for (int i = 0; i < NUM_CONNECTIONS; ++i) + { + std::string my_cat = "cat-recv-" + std::to_string(mpi_rank) + "-" + std::to_string(upstream) + "-" + std::to_string(i); + double received = nexuses[i]->get_downstream_flow(my_cat, ts, 100.0); + ASSERT_DOUBLE_EQ(flow_value, received); + } + } + } + + senders.clear(); + receivers.clear(); + + MPI_Barrier(MPI_COMM_WORLD); + std::cerr << "Rank " << mpi_rank << ": Test PASSED - no deadlock with remote nexus\n"; +} + + +//#endif // NGEN_MPI_TESTS_ACTIVE + //#endif // NGEN_MPI_TESTS_ACTIVE diff --git a/test/data/bmi/test_bmi_python/test_bmi_python_config_2.yml b/test/data/bmi/test_bmi_python/test_bmi_python_config_2.yml new file mode 100644 index 0000000000..edd5b81e35 --- /dev/null +++ b/test/data/bmi/test_bmi_python/test_bmi_python_config_2.yml @@ -0,0 +1,2 @@ +time_step_seconds: 3600 +initial_time: 0 \ No newline at end of file diff --git a/test/realizations/catchments/Bmi_C_Formulation_Test.cpp b/test/realizations/catchments/Bmi_C_Formulation_Test.cpp index 3b46c648b4..e532f201e9 100644 --- a/test/realizations/catchments/Bmi_C_Formulation_Test.cpp +++ b/test/realizations/catchments/Bmi_C_Formulation_Test.cpp @@ -24,7 +24,7 @@ #include "FileChecker.h" #include "Formulation_Manager.hpp" #include - +#include "../../utils/bmi/MockConfig.hpp" using ::testing::MatchesRegex; using namespace realization; @@ -116,6 +116,7 @@ class Bmi_C_Formulation_Test : public ::testing::Test { std::vector main_output_variable; std::vector registration_functions; std::vector uses_forcing_file; + // std::vector tries_mass_balance; std::vector> forcing_params_examples; std::vector config_properties; std::vector config_prop_ptree; @@ -148,6 +149,7 @@ void Bmi_C_Formulation_Test::SetUp() { main_output_variable = std::vector(EX_COUNT); registration_functions = std::vector(EX_COUNT); uses_forcing_file = std::vector(EX_COUNT); + // tries_mass_balance = std::vector(EX_COUNT); forcing_params_examples = std::vector>(EX_COUNT); config_properties = std::vector(EX_COUNT); config_prop_ptree = std::vector(EX_COUNT); @@ -161,6 +163,7 @@ void Bmi_C_Formulation_Test::SetUp() { main_output_variable[0] = "OUTPUT_VAR_1"; registration_functions[0] = "register_bmi"; uses_forcing_file[0] = false; + // tries_mass_balance[0] = true; catchment_ids[1] = "cat-27"; model_type_name[1] = "test_bmi_c"; @@ -170,6 +173,7 @@ void Bmi_C_Formulation_Test::SetUp() { main_output_variable[1] = "OUTPUT_VAR_1"; registration_functions[1] = "register_bmi"; uses_forcing_file[1] = false; + // tries_mass_balance[1] = false; std::string variables_with_rain_rate = " \"output_variables\": [\"OUTPUT_VAR_2\",\n" " \"OUTPUT_VAR_1\"],\n"; @@ -198,6 +202,9 @@ void Bmi_C_Formulation_Test::SetUp() { + variables_line + " \"uses_forcing_file\": " + (uses_forcing_file[i] ? "true" : "false") + "," " \"model_params\": { \"PARAM_VAR_1\": 42, \"PARAM_VAR_2\": 4.2, \"PARAM_VAR_3\": [4, 2]}" + + // (tries_mass_balance[i] ? \ + // ", \"mass_balance\": {\"tolerance\": 1e-12, \"fatal\":true}" \ + // :"" )+ " }," " \"forcing\": { \"path\": \"" + forcing_file[i] + "\", \"provider\": \"CsvPerFeature\"}" " }" @@ -387,6 +394,235 @@ TEST_F(Bmi_C_Formulation_Test, determine_model_time_offset_0_c) { ASSERT_EQ(get_friend_bmi_model_start_time_forcing_offset_s(formulation), expected_offset); } +using models::bmi::protocols::INPUT_MASS_NAME; +using models::bmi::protocols::OUTPUT_MASS_NAME; +using models::bmi::protocols::STORED_MASS_NAME; +using models::bmi::protocols::LEAKED_MASS_NAME; +using models::bmi::protocols::ProtocolError; + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + // mass balance failure will throw an exception + ptree.put_child("mass_balance", MassBalanceMock(true).get()); + formulation.create_formulation(ptree); + formulation.check_mass_balance(0, 2, "t0"); + formulation.get_response(1, 3600); + formulation.check_mass_balance(1, 2, "t1"); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_warns) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(false).get()); + formulation.create_formulation(ptree); + //formulation.check_mass_balance(0, 1, "t0"); + formulation.get_response(1, 3600); + double mass_error = 100; + get_friend_bmi_model(formulation)->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + //ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), MassBalanceWarning); + testing::internal::CaptureStderr(); + formulation.check_mass_balance(0, 1, "t0"); + std::string output = testing::internal::GetCapturedStderr(); + std::cerr << output; + EXPECT_THAT(output, testing::HasSubstr("Warning(Protocol)::mass_balance:")); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_stored_fails) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true).get()); + formulation.create_formulation(ptree); + //formulation.check_mass_balance(0, 1, "t0"); + formulation.get_response(1, 3600); + double mass_error = 100; + get_friend_bmi_model(formulation)->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + //formulation.check_mass_balance(0, 1, "t0"); + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_in_fails_a) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5).get()); + formulation.create_formulation(ptree); + formulation.get_response(1, 3600); + double mass_error = 2; + get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_out_fails) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true).get()); + formulation.create_formulation(ptree); + formulation.get_response(1, 3600); + double mass_error = 2; + get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_leaked_fails) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true).get()); + formulation.create_formulation(ptree); + formulation.get_response(1, 3600); + double mass_error = 2; + get_friend_bmi_model(formulation)->SetValue(LEAKED_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_tolerance) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5).get()); + formulation.create_formulation(ptree); + //formulation.check_mass_balance(0, "t0"); + formulation.get_response(1, 3600); + double mass_error; + get_friend_bmi_model(formulation)->GetValue(INPUT_MASS_NAME, &mass_error); + mass_error += 1e-4; // Force a mass balance error not within tolerance + get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(formulation.check_mass_balance(0, 1, "t0"), ProtocolError); + try{ + formulation.check_mass_balance(0, 1, "t0"); + } + catch (ProtocolError& e) { + std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance:.*")); + } +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_tolerance_a) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5).get()); + formulation.create_formulation(ptree); + //formulation.check_mass_balance(0, 1, "t0"); + formulation.get_response(1, 3600); + double mass_error; + get_friend_bmi_model(formulation)->GetValue(INPUT_MASS_NAME, &mass_error); + mass_error += 1e-6; // Force a mass balance error within tolerance + get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + formulation.check_mass_balance(0, 1, "t0"); // Should not throw an error +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_off) { + int ex_index = 1; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + formulation.create_formulation(config_prop_ptree[ex_index]); + formulation.check_mass_balance(0, 2, "t0"); + formulation.get_response(1, 3600); + formulation.check_mass_balance(1, 2, "t1"); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_missing) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + + std::string catchment_path = "catchments." + catchment_ids[ex_index] + ".bmi_c"; + ptree.erase("mass_balance"); + formulation.create_formulation(ptree); + formulation.check_mass_balance(0, 2, "t0"); + formulation.get_response(1, 3600); + double mass_error = 100; + double more_mass_error = 99; + get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // Force a mass balance error + get_friend_bmi_model(formulation)->SetValue(INPUT_MASS_NAME, &more_mass_error); // Force a mass balance error + formulation.check_mass_balance(1, 2, "t1"); + +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_frequency) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5, 2).get()); + formulation.create_formulation(ptree); + double mass_error; + mass_error += 10; // Force a mass balance error above tolerance + get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // + //Check initial mass balance -- should error which indicates it was propoerly checked + //per frequency setting + ASSERT_THROW(formulation.check_mass_balance(0, 2, "t0"), ProtocolError); + // Call mass balance check again, this should NOT error, since the actual check + // should be skipped due to the frequency setting + formulation.check_mass_balance(1, 2, "t1"); + // Check mass balance again, this SHOULD error since the previous mass balance + // will propagate, and it should now be checked based on the frequency + ASSERT_THROW(formulation.check_mass_balance(2, 2, "t2"), ProtocolError); +} + +TEST_F(Bmi_C_Formulation_Test, check_mass_balance_frequency_1) { + int ex_index = 0; + + Bmi_C_Formulation formulation(catchment_ids[ex_index], std::make_shared(*forcing_params_examples[ex_index]), utils::StreamHandler()); + auto ptree = config_prop_ptree[ex_index]; + ptree.put_child("mass_balance", MassBalanceMock(true, 1e-5, -1).get()); + formulation.create_formulation(ptree); + double mass_error; + mass_error += 10; // Force a mass balance error above tolerance + get_friend_bmi_model(formulation)->SetValue(OUTPUT_MASS_NAME, &mass_error); // + //Check initial mass balance -- should NOT error + formulation.check_mass_balance(0, 2, "t0"); + // Call mass balance check again, this should NOT error, since the actual check + // should be skipped due to the frequency setting + formulation.check_mass_balance(1, 2, "t1"); + // Check mass balance again, this SHOULD error since the this is step 2/2 + // and it will now be checked based on the frequency (-1, check at end) + ASSERT_THROW(formulation.check_mass_balance(2, 2, "t2"), ProtocolError); +} + #endif // NGEN_BMI_C_LIB_TESTS_ACTIVE #endif // NGEN_BMI_C_FORMULATION_TEST_CPP diff --git a/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp b/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp index 05c244526e..e0773e6767 100644 --- a/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp +++ b/test/realizations/catchments/Bmi_Multi_Formulation_Test.cpp @@ -387,7 +387,7 @@ class Bmi_Multi_Formulation_Test : public ::testing::Test { return s; } - inline void buildExampleConfig(const int ex_index) { + inline void buildExampleConfig(const int ex_index, const int nested_count) { std::string outputVariablesSubConfig = (ex_index == 6) ? buildExampleOutputVariablesSubConfig(ex_index, true) : buildExampleOutputVariablesSubConfig(ex_index) + "\n"; std::string config = "{\n" @@ -403,10 +403,12 @@ class Bmi_Multi_Formulation_Test : public ::testing::Test { " \"init_config\": \"\",\n" " \"allow_exceed_end_time\": true,\n" " \"main_output_variable\": \"" + main_output_variables[ex_index] + "\",\n" - " \"modules\": [\n" - + buildExampleNestedModuleSubConfig(ex_index, 0) + ",\n" - + buildExampleNestedModuleSubConfig(ex_index, 1) + "\n" - " ],\n" + " \"modules\": [\n"; + for (int i = 0; i < nested_count - 1; ++i) { + config += buildExampleNestedModuleSubConfig(ex_index, i) + ",\n"; + } + config += buildExampleNestedModuleSubConfig(ex_index, nested_count - 1) + "\n"; + config += " ],\n" " \"uses_forcing_file\": false\n" + outputVariablesSubConfig + " }\n" @@ -462,7 +464,7 @@ class Bmi_Multi_Formulation_Test : public ::testing::Test { main_output_variables[ex_index] = nested_module_main_output_variables[ex_index][example_module_depth[ex_index] - 1]; specified_output_variables[ex_index] = output_variables; - buildExampleConfig(ex_index); + buildExampleConfig(ex_index, nested_module_lists[ex_index].size()); } @@ -490,7 +492,7 @@ void Bmi_Multi_Formulation_Test::SetUp() { // Define this manually to set how many nested modules per example, and implicitly how many examples. // This means example_module_depth.size() example scenarios with example_module_depth[i] nested modules in each scenario. - example_module_depth = {2, 2, 2, 2, 2, 2, 2}; + example_module_depth = {2, 2, 2, 2, 2, 2, 2, 3}; // Initialize the members for holding required input and result test data for individual example scenarios setupExampleDataCollections(); @@ -533,7 +535,12 @@ void Bmi_Multi_Formulation_Test::SetUp() { initializeTestExample(6, "cat-27", {std::string(BMI_CPP_TYPE), std::string(BMI_FORTRAN_TYPE)}, { "OUTPUT_VAR_3","OUTPUT_VAR_3","OUTPUT_VAR_3" }); - + #if NGEN_WITH_BMI_C + initializeTestExample(7, "cat-27", {std::string(BMI_C_TYPE), std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, {"OUTPUT_VAR_1__0"}); // Output var from C module... + #else + initializeTestExample(7, "cat-27", {std::string(BMI_FORTRAN_TYPE), std::string(BMI_PYTHON_TYPE)}, {"OUTPUT_VAR_1__0"}); // Output var from Fortran module... + + #endif // NGEN_WITH_PYTHON } /** Simple test to make sure the model config from example 0 initializes. */ @@ -940,6 +947,16 @@ TEST_F(Bmi_Multi_Formulation_Test, GetAvailableVariableNames) { ); } } + +TEST_F(Bmi_Multi_Formulation_Test, MassBalanceCheck) { + int ex_index = 6; + + Bmi_Multi_Formulation formulation(catchment_ids[ex_index], std::make_unique(*forcing_params_examples[ex_index]), utils::StreamHandler()); + formulation.create_formulation(config_prop_ptree[ex_index]); + + formulation.check_mass_balance(0, 1, "t0"); +} + #endif // NGEN_WITH_BMI_C || NGEN_WITH_BMI_FORTRAN || NGEN_WITH_PYTHON #endif // NGEN_BMI_MULTI_FORMULATION_TEST_CPP diff --git a/test/utils/bmi/MockConfig.hpp b/test/utils/bmi/MockConfig.hpp new file mode 100644 index 0000000000..094ce440df --- /dev/null +++ b/test/utils/bmi/MockConfig.hpp @@ -0,0 +1,70 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ +*/ +#pragma once +#include +#include "protocols.hpp" +#include "JSONProperty.hpp" + +static const auto noneConfig = std::map { + {"none", geojson::JSONProperty("none", true)} +}; + +static models::bmi::protocols::Context make_context(int current_time_step, int total_steps, const std::string& timestamp, const std::string& id) { + return models::bmi::protocols::Context{ + current_time_step, + total_steps, + timestamp, + id + }; +} + +class MassBalanceMock { + public: + + MassBalanceMock( bool fatal = false, double tolerance = 1e-12, int frequency = 1, bool check = true) + : properties() { + boost::property_tree::ptree config; + config.put("check", check); + config.put("tolerance", tolerance); + config.put("fatal", fatal); + config.put("frequency", frequency); + properties.add_child("mass_balance", config); + } + + MassBalanceMock( bool fatal, const char* tolerance){ + boost::property_tree::ptree config; + config.put("check", true); + config.put("tolerance", tolerance); + config.put("fatal", fatal); + config.put("frequency", 1); + properties.add_child("mass_balance", config); + } + + const boost::property_tree::ptree& get() const { + return properties.get_child("mass_balance"); + } + + const geojson::PropertyMap as_json_property() const { + auto props = geojson::JSONProperty("mass_balance", properties); + // geojson::JSONProperty::print_property(props, 1); + return props.get_values(); + } + + private: + boost::property_tree::ptree properties; +}; \ No newline at end of file diff --git a/test/utils/bmi/mass_balance_Test.cpp b/test/utils/bmi/mass_balance_Test.cpp new file mode 100644 index 0000000000..9d834c3ce3 --- /dev/null +++ b/test/utils/bmi/mass_balance_Test.cpp @@ -0,0 +1,435 @@ +/* +Author: Nels Frazier +Copyright (C) 2025 Lynker +------------------------------------------------------------------------ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +------------------------------------------------------------------------ +*/ +#ifdef NGEN_BMI_CPP_LIB_TESTS_ACTIVE +//Test utitilities +// #include "bmi.hpp" +#include "gtest/gtest.h" +#include "gmock/gmock.h" +//Mock configuration and context helpers +#include "MockConfig.hpp" +//BMI model/adapter +#include "FileChecker.h" +#include "Bmi_Cpp_Adapter.hpp" +#include +//Interface under test +#include "protocols.hpp" + +using ::testing::MatchesRegex; +//protocol symbols +using models::bmi::protocols::NgenBmiProtocols; +using models::bmi::protocols::INPUT_MASS_NAME; +using models::bmi::protocols::OUTPUT_MASS_NAME; +using models::bmi::protocols::STORED_MASS_NAME; +using models::bmi::protocols::LEAKED_MASS_NAME; +using models::bmi::protocols::ProtocolError; +using nonstd::expected_lite::expected; + +// Use the ngen bmi c++ test library +#ifndef BMI_TEST_CPP_LOCAL_LIB_NAME +#ifdef __APPLE__ +#define BMI_TEST_CPP_LOCAL_LIB_NAME "libtestbmicppmodel.dylib" +#else +#ifdef __GNUC__ + #define BMI_TEST_CPP_LOCAL_LIB_NAME "libtestbmicppmodel.so" + #endif // __GNUC__ +#endif // __APPLE__ +#endif // BMI_TEST_CPP_LOCAL_LIB_NAME + +#define CREATOR_FUNC "bmi_model_create" +#define DESTROYER_FUNC "bmi_model_destroy" + +using namespace models::bmi; + +// Copy of the struct def used within the test_bmi_c test library + +class Bmi_Cpp_Test_Adapter : public ::testing::Test { +protected: + + void SetUp() override; + + void TearDown() override; + + std::string file_search(const std::vector &parent_dir_options, const std::string& file_basename); + + std::string config_file_name_0; + std::string lib_file_name_0; + std::string bmi_module_type_name_0; + std::unique_ptr adapter; +}; + +void Bmi_Cpp_Test_Adapter::SetUp() { + /** + * @brief Set up the test environment for the protocol tests. + * + * The protocol requires a valid BMI model to operate on, so this setup + * function initializes a BMI C++ adapter using the test BMI C++ model + * + */ + // Uses the same config files as the C test model... + std::vector config_path_options = { + "test/data/bmi/test_bmi_c/", + "./test/data/bmi/test_bmi_c/", + "../test/data/bmi/test_bmi_c/", + "../../test/data/bmi/test_bmi_c/", + }; + std::string config_basename_0 = "test_bmi_c_config_0.txt"; + config_file_name_0 = file_search(config_path_options, config_basename_0); + + std::vector lib_dir_opts = { + "./extern/test_bmi_cpp/cmake_build/", + "../extern/test_bmi_cpp/cmake_build/", + "../../extern/test_bmi_cpp/cmake_build/" + }; + lib_file_name_0 = file_search(lib_dir_opts, BMI_TEST_CPP_LOCAL_LIB_NAME); + bmi_module_type_name_0 = "test_bmi_cpp"; + try { + adapter = std::make_unique(bmi_module_type_name_0, lib_file_name_0, config_file_name_0, + true, CREATOR_FUNC, DESTROYER_FUNC); + } + catch (const std::exception &e) { + std::clog << e.what() << std::endl; + throw e; + } + } + +void Bmi_Cpp_Test_Adapter::TearDown() { + +} + +std::string +Bmi_Cpp_Test_Adapter::file_search(const std::vector &parent_dir_options, const std::string& file_basename) { + // Build vector of names by building combinations of the path and basename options + std::vector name_combinations; + + // Build so that all path names are tried for given basename before trying a different basename option + for (auto & path_option : parent_dir_options) + name_combinations.push_back(path_option + file_basename); + + return utils::FileChecker::find_first_readable(name_combinations); +} + +class Bmi_Mass_Balance_Test : public Bmi_Cpp_Test_Adapter { +protected: + void SetUp() override { + Bmi_Cpp_Test_Adapter::SetUp(); + model = std::shared_ptr(adapter.release()); + model_name = model->GetComponentName(); + } + std::string time = "t0"; + std::string model_name; + std::shared_ptr model; +}; + +TEST_F(Bmi_Mass_Balance_Test, bad_model) { + model = nullptr; // simulate uninitialized model + auto properties = MassBalanceMock(true).as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_THAT(output, MatchesRegex("Error\\(Uninitialized Model\\).*Disabling mass balance protocol.\n")); + testing::internal::CaptureStderr(); + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( ! result.has_value() ); // should have an error!!! + EXPECT_EQ( result.error().error_code(), models::bmi::protocols::Error::UNITIALIZED_MODEL ); + output = testing::internal::GetCapturedStderr(); + EXPECT_THAT(output, MatchesRegex("Error\\(Uninitialized Model\\).*\n")); +} + +TEST_F(Bmi_Mass_Balance_Test, default_construct) { + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(); + testing::internal::CaptureStderr(); + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( ! result.has_value() ); // should have an error!!! + EXPECT_EQ( result.error().error_code(), models::bmi::protocols::Error::UNITIALIZED_MODEL ); + std::string output = testing::internal::GetCapturedStderr(); + EXPECT_THAT(output, MatchesRegex("Error\\(Uninitialized Model\\).*\n")); +} + +TEST_F(Bmi_Mass_Balance_Test, check) { + auto properties = MassBalanceMock(true).as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + model->Update(); + time = "t1"; + protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + std::string output = testing::internal::GetCapturedStderr(); + // Not warning/errors printed, and no exceptions thrown is success + EXPECT_EQ(output, ""); +} + +TEST_F(Bmi_Mass_Balance_Test, warns) { + auto properties = MassBalanceMock(false).as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + double mass_error = 100; + model->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + // The expected should be an error not a value (void in this case) + EXPECT_FALSE( result.has_value() ); // should have a warning!!! + EXPECT_THAT( result.error().to_string(), testing::HasSubstr("Warning(Protocol)::mass_balance:") ); + std::string output = testing::internal::GetCapturedStderr(); + // std::cerr << output; + //Warning was sent to stderr + EXPECT_THAT(output, testing::HasSubstr("Warning(Protocol)::mass_balance:")); +} + +TEST_F(Bmi_Mass_Balance_Test, storage_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + double mass_error = 2; + model->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + time = "t1"; + + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 1 \\(t1\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, in_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + time = "t1"; + double mass_error = 2; + model->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 1 \\(t1\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, out_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + time = "t1"; + double mass_error = 2; + model->SetValue(OUTPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 1 \\(t1\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, leaked_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + time = "t1"; + double mass_error = 2; + model->SetValue(LEAKED_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 1 \\(t1\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, tolerance_fails) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property();; + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + double mass_error; + model->GetValue(INPUT_MASS_NAME, &mass_error); + mass_error += 1e-4; // Force a mass balance error not within tolerance + model->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)), ProtocolError); + try{ + // This should throw, so result won't be defined... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)); + } + catch (ProtocolError& e) { + // std::cerr << e.to_string() << std::endl; + EXPECT_THAT(e.to_string(), MatchesRegex("Error\\(Protocol\\)::mass_balance: at timestep 0 \\(t0\\).*")); + } +} + +TEST_F(Bmi_Mass_Balance_Test, tolerance_passes) { + auto properties = MassBalanceMock(true, 1e-5).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + model->Update(); // advance model + double mass_error; + model->GetValue(INPUT_MASS_NAME, &mass_error); + mass_error += 1e-6; // Force a mass balance error within tolerance + model->SetValue(INPUT_MASS_NAME, &mass_error); // Force a mass balance error + // should not thow! + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass +} + +TEST_F(Bmi_Mass_Balance_Test, disabled) { + auto properties = MassBalanceMock(true, 1e-5, 1, false).as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( result.has_value() ); // should pass, as mass balance is disabled + model->Update(); // advance model + time = "t1"; + double mass_error = 100; + model->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + // should not throw, as mass balance is disabled + EXPECT_TRUE( result.has_value() ); // should pass, as mass balance is disabled + std::string output = testing::internal::GetCapturedStderr(); + // No warnings/errors printed, and no exceptions thrown is success + EXPECT_EQ(output, ""); +} + +TEST_F(Bmi_Mass_Balance_Test, unconfigured) { + auto properties = noneConfig; + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( result.has_value() ); // should pass, as mass balance is disabled + model->Update(); // advance model + time = "t1"; + double mass_error = 100; + model->SetValue(STORED_MASS_NAME, &mass_error); // Force a mass balance error + result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + // should not throw, as mass balance is disabled + EXPECT_TRUE( result.has_value() ); // should pass, as mass balance is disabled + std::string output = testing::internal::GetCapturedStderr(); + // No warnings/errors printed, and no exceptions thrown is success + EXPECT_EQ(output, ""); +} + +TEST_F(Bmi_Mass_Balance_Test, frequency) { + auto properties = MassBalanceMock(true, 1e-5, 2).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + double mass_error = 10; // Force a mass balance error above tolerance + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + //Check initial mass balance -- should error which indicates it was propoerly checked + //per frequency setting + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)), ProtocolError); + time = "t1"; + model->Update(); // advance model + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + // Call mass balance check again, this should NOT error, since the actual check + // should be skipped due to the frequency setting + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass + time = "t2"; + model->Update(); // advance model + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + // Check mass balance again, this SHOULD error since the previous mass balance + // will propagate, and it should now be checked based on the frequency + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(2, 2, time, model_name)), ProtocolError); +} + +TEST_F(Bmi_Mass_Balance_Test, frequency_zero) { + auto properties = MassBalanceMock(true, 1e-5, 0).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + double mass_error = 10; // Force a mass balance error above tolerance + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass, frequency 0 means never check +} + +TEST_F(Bmi_Mass_Balance_Test, frequency_end) { + auto properties = MassBalanceMock(true, 1e-5, -1).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + double mass_error = 10; // Force a mass balance error above tolerance + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + //Check initial mass balance -- should not error due to frequency setting + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass + time = "t1"; + model->Update(); // advance model + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + // Call mass balance check again, this should NOT error, since the actual check + // should be skipped due to the frequency setting + result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass + time = "t2"; + model->Update(); // advance model + model->SetValue(OUTPUT_MASS_NAME, &mass_error); + // Check mass balance again, this SHOULD error since its the last timestep and the previous mass balance + // will propagate, and it should now be checked based on the frequency + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(2, 2, time, model_name)), ProtocolError); +} + +TEST_F(Bmi_Mass_Balance_Test, nan) { + auto properties = MassBalanceMock(true, "NaN").as_json_property(); + auto context = make_context(0, 2, time, model_name); + testing::internal::CaptureStderr(); + auto protocols = NgenBmiProtocols(model, properties); + std::string output = testing::internal::GetCapturedStderr(); + // std::cerr << output; + EXPECT_THAT(output, testing::HasSubstr("Warning(Protocol)::mass_balance: tolerance value 'NaN'")); + double mass_error = 10; + model->SetValue(OUTPUT_MASS_NAME, &mass_error); // + // Would cause an error if tolerance were a number, should only see warning in the output above + // and this should pass without error... + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(0, 1, time, model_name)); + EXPECT_TRUE( result.has_value() ); // should pass +} + +TEST_F(Bmi_Mass_Balance_Test, model_nan) { + auto properties = MassBalanceMock(true).as_json_property(); + auto context = make_context(0, 2, time, model_name); + auto protocols = NgenBmiProtocols(model, properties); + auto result = protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, context); + EXPECT_TRUE( result.has_value() ); // should pass + double mass_error = std::numeric_limits::quiet_NaN(); + model->SetValue(OUTPUT_MASS_NAME, &mass_error); // Force a NaN into the mass balance computation + time = "t1"; + // should cause an error since mass balance will be NaN using this value in its computation + ASSERT_THROW(protocols.run(models::bmi::protocols::Protocol::MASS_BALANCE, make_context(1, 2, time, model_name)), ProtocolError); +} + +#endif // NGEN_BMI_CPP_LIB_TESTS_ACTIVE \ No newline at end of file