diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 8635cdd65..97e1d9170 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -63,7 +63,7 @@ jobs: repo-path: 'kokkos/kokkos' repo-ref: '4.6.01' cache: true - options: '-DCMAKE_CXX_STANDARD=17 + options: '-DCMAKE_CXX_STANDARD=20 -DBUILD_SHARED_LIBS=OFF -DKokkos_ENABLE_SERIAL=ON -DKokkos_ENABLE_OPENMP=OFF @@ -78,7 +78,7 @@ jobs: repo-path: 'kokkos/kokkos-kernels' repo-ref: '4.6.01' cache: true - options: '-DCMAKE_CXX_STANDARD=17 + options: '-DCMAKE_CXX_STANDARD=20 -DBUILD_SHARED_LIBS=OFF -DKokkos_DIR=${{ runner.temp }}/build-kokkos-openmpi/install/lib/cmake/Kokkos' @@ -189,10 +189,10 @@ jobs: -DCMAKE_CXX_COMPILER=mpicxx \ -DCMAKE_Fortran_COMPILER=mpifort \ -DPCMS_ENABLE_PETSC=ON \ + -DPETSC_LINK_STATIC=ON \ -DPETSC_DIR=${{ runner.temp }}/petsc-openmpi \ -DPETSC_ARCH=ubuntu-kokkos \ -DPCMS_TIMEOUT=10 \ - -DPCMS_ENABLE_SPDLOG=OFF \ -DCatch2_DIR=${{ runner.temp }}/build-Catch2-openmpi/install/lib/cmake/Catch2 \ -DOmega_h_DIR=${{ runner.temp }}/build-omega_h-openmpi/install/lib/cmake/Omega_h \ -Dmeshfields_DIR=${{ runner.temp }}/build-meshFields-openmpi/install/lib/cmake/meshfields \ diff --git a/.github/workflows/cmake-test.yml b/.github/workflows/cmake-test.yml index e927ad025..60c568941 100644 --- a/.github/workflows/cmake-test.yml +++ b/.github/workflows/cmake-test.yml @@ -25,6 +25,23 @@ jobs: compiler: [g++] language: ['cpp'] python_api: [OFF, ON] + meshfields: [ON] + petsc: [ON] + include: + - build_type: Release + memory_test: OFF + compiler: g++ + language: 'cpp' + python_api: OFF + meshfields: OFF + petsc: ON + - build_type: Release + memory_test: OFF + compiler: g++ + language: 'cpp' + python_api: OFF + meshfields: ON + petsc: OFF exclude: - build_type: Release memory_test: ON @@ -68,7 +85,7 @@ jobs: repo-ref: '4.6.01' cache: true cache-suffix: ${{ matrix.python_api == 'ON' && '-shared' || '' }} - options: '-DCMAKE_CXX_STANDARD=17 + options: '-DCMAKE_CXX_STANDARD=20 -DBUILD_SHARED_LIBS=${{ matrix.python_api }} -DKokkos_ENABLE_SERIAL=ON -DKokkos_ENABLE_OPENMP=OFF @@ -84,7 +101,7 @@ jobs: repo-ref: '4.6.01' cache: true cache-suffix: ${{ matrix.python_api == 'ON' && '-shared' || '' }} - options: '-DCMAKE_CXX_STANDARD=17 + options: '-DCMAKE_CXX_STANDARD=20 -DBUILD_SHARED_LIBS=${{ matrix.python_api }} -DKokkos_DIR=${{ runner.temp }}/build-kokkos/install/lib/cmake/Kokkos' @@ -106,6 +123,7 @@ jobs: -DKokkos_DIR=${{ runner.temp }}/build-kokkos/install/lib/cmake/Kokkos' - name: build meshFields + if: matrix.meshfields == 'ON' uses: ./.github/actions/install-repo with: repo-name: 'meshFields' @@ -153,7 +171,17 @@ jobs: -DADIOS2_DIR=${{ runner.temp }}/build-ADIOS2/install/lib/cmake/adios2 -Dperfstubs_DIR=${{ runner.temp }}/build-perfstubs/install/lib/cmake' + - name: Set LD_LIBRARY_PATH for shared libraries + if: matrix.python_api == 'ON' + run: | + meshfields_lib="" + if [ "${{ matrix.meshfields }}" = "ON" ]; then + meshfields_lib="${{ runner.temp }}/build-meshFields/install/lib:" + fi + echo "LD_LIBRARY_PATH=${{ runner.temp }}/build-kokkos/install/lib:${{ runner.temp }}/build-omega_h/install/lib:${meshfields_lib}${{ runner.temp }}/build-redev/install/lib:${{ runner.temp }}/build-ADIOS2/install/lib:${{ runner.temp }}/build-perfstubs/install/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV + - name: clone petsc + if: matrix.petsc == 'ON' id: clone-petsc run: | cd ${{ runner.temp }} @@ -162,14 +190,15 @@ jobs: echo "petsc-commit-hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - name: Cache PETSc + if: matrix.petsc == 'ON' id: cache-petsc uses: actions/cache@v3 with: path: ${{ runner.temp }}/petsc - key: build-petsc-${{ steps.clone-petsc.outputs.petsc-commit-hash }} + key: build-petsc-${{ steps.clone-petsc.outputs.petsc-commit-hash }}-${{ matrix.python_api }} - name: build petsc - if: steps.cache-petsc.outputs.cache-hit != 'true' + if: matrix.petsc == 'ON' && steps.cache-petsc.outputs.cache-hit != 'true' run: | cd ${{ runner.temp }}/petsc ./configure \ @@ -177,10 +206,19 @@ jobs: --with-kokkos-dir="${{ runner.temp }}/build-kokkos/install/" \ --with-kokkos-kernels-dir="${{ runner.temp }}/build-kokkos-kernels/install/" \ --with-cuda=0 \ - --with-shared-libraries=0 \ + --with-shared-libraries=${{ matrix.python_api == 'ON' && '1' || '0' }} \ --download-fblaslapack make all check + - name: Set PCMS PETSc options + run: | + echo "PCMS_ENABLE_PETSC=-DPCMS_ENABLE_PETSC=${{ matrix.petsc }}" >> $GITHUB_ENV + if [ "${{ matrix.petsc }}" = "ON" ]; then + echo "PCMS_PETSC_OPTIONS=-DPETSC_LINK_STATIC=ON -DPETSC_DIR=${{ runner.temp }}/petsc -DPETSC_ARCH=ubuntu-kokkos" >> $GITHUB_ENV + else + echo "PCMS_PETSC_OPTIONS=" >> $GITHUB_ENV + fi + - name: checkout pcms_testcases uses: actions/checkout@v3 with: @@ -190,10 +228,14 @@ jobs: - name: Install fftw3 run: sudo apt-get install -yq libfftw3-dev pkg-config - - name: Set LD_LIBRARY_PATH for shared libraries - if: matrix.python_api == 'ON' + - name: Set PCMS MeshFields options run: | - echo "LD_LIBRARY_PATH=${{ runner.temp }}/build-kokkos/install/lib:${{ runner.temp }}/build-omega_h/install/lib:${{ runner.temp }}/build-meshFields/install/lib:${{ runner.temp }}/build-redev/install/lib:${{ runner.temp }}/build-ADIOS2/install/lib:${{ runner.temp }}/build-perfstubs/install/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV + echo "PCMS_ENABLE_MESHFIELDS=-DPCMS_ENABLE_MESHFIELDS=${{ matrix.meshfields }}" >> $GITHUB_ENV + if [ "${{ matrix.meshfields }}" = "ON" ]; then + echo "PCMS_MESHFIELDS_DIR=-Dmeshfields_DIR=${{ runner.temp }}/build-meshFields/install/lib/cmake/meshfields" >> $GITHUB_ENV + else + echo "PCMS_MESHFIELDS_DIR=" >> $GITHUB_ENV + fi - name: build pcms uses: ./.github/actions/install-repo @@ -207,15 +249,14 @@ jobs: -DCMAKE_CXX_COMPILER=`which mpicxx` -DCMAKE_Fortran_COMPILER=`which mpifort` -DMPIEXEC_PREFLAGS="--oversubscribe" - -DPCMS_ENABLE_PETSC=ON - -DPETSC_DIR=${{ runner.temp }}/petsc - -DPETSC_ARCH=ubuntu-kokkos + ${{ env.PCMS_ENABLE_MESHFIELDS }} + ${{ env.PCMS_ENABLE_PETSC }} + ${{ env.PCMS_PETSC_OPTIONS }} -DPCMS_TIMEOUT=10 - -DPCMS_ENABLE_SPDLOG=OFF -DPCMS_ENABLE_Python=${{ matrix.python_api }} -DCatch2_DIR=${{ runner.temp }}/build-Catch2/install/lib/cmake/Catch2 -DOmega_h_DIR=${{ runner.temp }}/build-omega_h/install/lib/cmake/Omega_h - -Dmeshfields_DIR=${{ runner.temp }}/build-meshFields/install/lib/cmake/meshfields + ${{ env.PCMS_MESHFIELDS_DIR }} -Dredev_DIR=${{ runner.temp }}/build-redev/install/lib/cmake/redev -DMPIEXEC_EXECUTABLE=`which mpirun` -DADIOS2_DIR=${{ runner.temp }}/build-ADIOS2/install/lib/cmake/adios2 @@ -256,12 +297,13 @@ jobs: cat ${{ runner.temp }}/build-pcms/install/lib/cmake/pcms/pcms-config.cmake echo "-------- PCMS TARGETS -------------" cat ${{ runner.temp }}/build-pcms/install/lib/cmake/pcms/pcms-targets.cmake - echo "-------- PCMS INTERPOLATOR TARGETS -------------" - cat ${{ runner.temp }}/build-pcms/install/lib/cmake/pcms/pcms_interpolator-targets.cmake + echo "-------- PCMS TRANSFER TARGETS -------------" + cat ${{ runner.temp }}/build-pcms/install/lib/cmake/pcms/pcms_transfer-targets.cmake export VERBOSE=1 cmake \ -B ${{github.workspace}}/examples/external-usage-example/build \ -S ${{github.workspace}}/examples/external-usage-example/ \ -Dpcms_DIR=${{ runner.temp }}/build-pcms/install/lib/cmake/pcms \ + ${{ matrix.petsc == 'ON' && format('-DPETSC_LINK_STATIC=ON -DPETSC_DIR={0}/petsc -DPETSC_ARCH=ubuntu-kokkos', runner.temp) || '' }} \ --debug-output cmake --build ${{github.workspace}}/examples/external-usage-example/build diff --git a/.github/workflows/perlmutter/install.sh b/.github/workflows/perlmutter/install.sh index 3840592c6..0b9ceb623 100644 --- a/.github/workflows/perlmutter/install.sh +++ b/.github/workflows/perlmutter/install.sh @@ -45,7 +45,7 @@ build-pcms/install # cmake -S kokkos-kernels -B build-kokkos-kernels \ # -DCMAKE_INSTALL_PREFIX=$PWD/build-kokkos-kernels/install \ # -DCMAKE_CXX_COMPILER=CC \ -# -DCMAKE_CXX_STANDARD=17 +# -DCMAKE_CXX_STANDARD=20 # cmake --build build-kokkos-kernels -j24 --target install @@ -131,4 +131,4 @@ cmake -S pcms -B build-pcms \ -DPCMS_TIMEOUT=100 \ -DCMAKE_CXX_STANDARD=20 \ -DPCMS_TEST_DATA_DIR=$PWD/pcms_testcases -cmake --build build-pcms -j8 \ No newline at end of file +cmake --build build-pcms -j8 diff --git a/.github/workflows/self-hosted.yml b/.github/workflows/self-hosted.yml index 97628f714..30e273224 100644 --- a/.github/workflows/self-hosted.yml +++ b/.github/workflows/self-hosted.yml @@ -180,15 +180,37 @@ jobs: git clone https://github.com/jacobmerson/pcms_testcases.git ${workDir}/pcms_testcases + # pcms - petsc off + bdir=${workDir}/build-pcms-petsc-off + cmake -S ${{github.workspace}}/pcms_${{ github.event.id }} -B $bdir \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_COMPILER=mpicc \ + -DCMAKE_CXX_COMPILER=mpicxx \ + -DPCMS_TIMEOUT=20 \ + -DPCMS_ENABLE_PETSC=OFF \ + -Dredev_DIR=$rdbdir/install/lib64/cmake/redev/ \ + -DOmega_h_DIR=$ohbdir/install/lib64/cmake/Omega_h/ \ + -Dperfstubs_DIR=$psbdir/install/lib/cmake/ \ + -DADIOS2_DIR=$adiosbdir/install/lib64/cmake/adios2/ \ + -DCatch2_DIR=$c2bdir/install/lib64/cmake/Catch2/ \ + -DKokkos_DIR=$kkbdir/install/lib64/cmake/Kokkos/ \ + -DKokkosKernels_DIR=$kkkbdir/install/lib64/cmake/KokkosKernels/ \ + -Dmeshfields_DIR=$mfbdir/install/lib64/cmake/meshfields/ \ + -DPCMS_TEST_DATA_DIR=${workDir}/pcms_testcases/ \ + -DCMAKE_CXX_EXTENSIONS=Off + cmake --build $bdir + ctest --test-dir $bdir --output-on-failure + # pcms + export PETSC_OPTIONS="-use_gpu_aware_mpi 0" bdir=${workDir}/build-pcms cmake -S ${{github.workspace}}/pcms_${{ github.event.id }} -B $bdir \ -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_C_COMPILER=mpicc \ -DCMAKE_CXX_COMPILER=mpicxx \ -DPCMS_TIMEOUT=20 \ - -DPCMS_ENABLE_SPDLOG=OFF \ -DPCMS_ENABLE_PETSC=ON \ + -DPETSC_LINK_STATIC=ON \ -DPETSC_DIR=${workDir}/petsc \ -DPETSC_ARCH=cuda-kokkos \ -Dredev_DIR=$rdbdir/install/lib64/cmake/redev/ \ @@ -218,4 +240,4 @@ jobs: if: ${{ !cancelled() }} run: | echo "PCMS_WORK_DIR $PCMS_WORK_DIR" - rm -rf $PCMS_WORK_DIR \ No newline at end of file + rm -rf $PCMS_WORK_DIR diff --git a/CMakeLists.txt b/CMakeLists.txt index cb4e755e6..654177310 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,15 +44,14 @@ option(PCMS_ENABLE_CLIENT "enable the coupling client implementation" ON) option(PCMS_ENABLE_XGC "enable xgc field adapter" ON) option(PCMS_ENABLE_OMEGA_H "enable Omega_h field adapter" OFF) +option(PCMS_ENABLE_MFEM "enable MFEM field adapter" OFF) option(PCMS_ENABLE_C "Enable pcms C api" ON) option(PCMS_ENABLE_Python "Enable pcms Python api" OFF) option(PCMS_ENABLE_PRINT "PCMS print statements enabled" ON) -option(PCMS_ENABLE_SPDLOG "use spdlog for logging" ON) -if(PCMS_ENABLE_SPDLOG) - find_package(spdlog REQUIRED) -endif() +option(PETSC_LINK_STATIC "Use pkg-config --static results for PETSc" + ${_pcms_link_petsc_static_default}) # find package before fortran enabled, so we don't require the adios2 fortran # interfaces this is important because adios2 build with clang/gfortran is @@ -121,11 +120,29 @@ if(PCMS_ENABLE_OMEGA_H) message(FATAL_ERROR "Omega_h must be built with MPI enabled.") endif() endif() + +if(PCMS_ENABLE_MFEM) + find_package(MFEM REQUIRED) + message(STATUS "Found MFEM: ${MFEM_DIR} (found version ${MFEM_VERSION})") + if(NOT MFEM_USE_MPI) + message(FATAL_ERROR "MFEM must be built with MPI enabled.") + endif() +endif() # adios2 adds C and Fortran depending on how it was built find_package(ADIOS2 CONFIG 2.10.2 REQUIRED) find_package(Kokkos CONFIG 4.5 REQUIRED) -find_package(meshfields REQUIRED) +option(PCMS_ENABLE_MESHFIELDS "Enable MeshFields support" ON) +option(PCMS_ENABLE_PETSC "Enable PETSc support" OFF) + +if(PCMS_ENABLE_MESHFIELDS) + find_package(meshfields REQUIRED) + message(STATUS "Found MeshFields: ${meshfields_DIR} (found version ${meshfields_VERSION})") +endif() + +if(PCMS_ENABLE_PETSC) + find_package(PETSc REQUIRED) +endif() add_subdirectory(src) diff --git a/MAINTAINER.md b/MAINTAINER.md index bb7c15c34..2f1d994c4 100644 --- a/MAINTAINER.md +++ b/MAINTAINER.md @@ -43,6 +43,13 @@ see issue #<###> 6. create the tag `git tag -a v#.#.# -m "pcms version #.#.#"` 7. push the tag `git push origin v#.#.#` +## Branch Management + +The `develop` branch is the main development branch where all new features and bug fixes are merged. The `master` branch is the stable release branch that reflects the latest released version of pcms. All changes should be made in the `develop` branch and then merged into `master` when ready for release. + +It is generally recommended to use forks for development to keep the main repository clean and to facilitate code review. Contributors can create a fork of the repository, make their changes in a new branch, and then submit a pull request to the `develop` branch of the main repository. Ensure that the pull request includes a clear description of the changes made and references any relevant issues. + +When creating a new feature or fixing a bug in main repo, create a new branch from `develop` with a descriptive name (e.g., `feature/new-feature`, `bugfix/issue-123`). ## Maintain CI/CD diff --git a/README.md b/README.md index f14bc6b72..be15e9bb9 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ module load cuda/12.1.1-zxa4msk git clone --branch 4.6.01 --depth 1 git@github.com:kokkos/kokkos.git cmake -S kokkos -B build-kokkos \ -DCMAKE_INSTALL_PREFIX=build-kokkos/install \ - -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_CXX_STANDARD=20 \ -DKokkos_ENABLE_SERIAL=ON \ -DKokkos_ENABLE_OPENMP=OFF \ -DKokkos_ENABLE_CUDA=OFF \ @@ -44,7 +44,7 @@ cmake --build build-kokkos --target install git clone --branch 4.6.01 --depth 1 git@github.com:kokkos/kokkos-kernels.git cmake -S kokkos-kernels -B build-kokkos-kernels \ -DCMAKE_INSTALL_PREFIX=build-kokkos-kernels/install \ - -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_CXX_STANDARD=20 \ -DKokkos_ROOT=$PWD/build-kokkos/install/lib64/cmake \ -DBUILD_SHARED_LIBS=OFF cmake --build build-kokkos-kernels --target install @@ -97,7 +97,7 @@ cmake --build build-redev --target install git clone --branch 4.6.01 --depth 1 git@github.com:kokkos/kokkos.git cmake -S kokkos -B build-kokkos \ -DCMAKE_INSTALL_PREFIX=build-kokkos/install \ - -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_CXX_STANDARD=20 \ -DCMAKE_BUILD_TYPE="Release" \ -DCMAKE_CXX_COMPILER=$PWD/kokkos/bin/nvcc_wrapper \ -DKokkos_ARCH_AMPERE80=ON \ @@ -112,7 +112,7 @@ cmake --build build-kokkos --target install git clone --branch 4.6.01 --depth 1 git@github.com:kokkos/kokkos-kernels.git cmake -S kokkos-kernels -B build-kokkos-kernels \ -DCMAKE_INSTALL_PREFIX=build-kokkos-kernels/install \ - -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_CXX_STANDARD=20 \ -DKokkos_ROOT=$PWD/build-kokkos/install/lib64/cmake \ -DBUILD_SHARED_LIBS=off cmake --build build-kokkos-kernels --target install @@ -248,9 +248,19 @@ If work in SCOREC system, you can directly use the spack-scorec.yaml which conta Then add the binded python module to your `PYTHONPATH` environment variable. You can find the install prefix with `find $(spack location -i pcms) -name "*.so" | grep -i pcms`. ```console $ export PYTHONPATH=$:$PYTHONPATH - ``` + ``` Test the python API with `python -c "import pcms; print(pcms.__version__)"`. You should see the version of PCMS printed out without any errors. + + Point-evaluator construction in Python uses `EvaluationRequest`, for + example: + ```python + request = pcms.EvaluationRequest.from_coordinates(eval_coords) + evaluator = source_space.create_point_evaluator(request) + + optimized_request = pcms.EvaluationRequest.from_function_space(target_space) + evaluator = source_space.create_point_evaluator(optimized_request) + ``` PS: If you need certain config options for the python API, you can specify them in the spack spec. For example, to build the python API with Exodus support, one compatible config is `pcms+python ^omega-h+trilinos ^trilinos@15.0.0:+exodus ^netcdf-c@4.8.1+mpi`. At this moment, omega-h spack package does not have the exodus support, so you need to manually add `args.append("-Omega_h_USE_SEACASExodus:BOOL=ON")` to the `package.py` file of omega-h in spack before installing pcms with spack. @@ -262,3 +272,11 @@ Assign the full path of testdatas to test_dir in the test_init.cc file; Use "mpi ## Creating Archive for Release `git archive --format=tar.gz -o /tmp/pcms.tar.gz --prefix=pcms/ develop` + +## Running clang format on SCOREC +```console +module use /opt/scorec/spack/rhel9/v0222_2/lmod/linux-rhel9-x86_64/Core/ +module load clang-format +module load llvm +find . -regex '.*\.\(cpp\|cxx\|cc\|c\|hpp\|h\)' -exec clang-format -style=file -i {} \; +``` diff --git a/cmake/FindPETSc.cmake b/cmake/FindPETSc.cmake new file mode 100644 index 000000000..b556bb74e --- /dev/null +++ b/cmake/FindPETSc.cmake @@ -0,0 +1,50 @@ +find_package(PkgConfig REQUIRED QUIET) +if(DEFINED PETSC_DIR) + if(DEFINED PETSC_ARCH) + set(ENV{PKG_CONFIG_PATH} "${PETSC_DIR}/${PETSC_ARCH}/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}") + else() + set(ENV{PKG_CONFIG_PATH} "${PETSC_DIR}/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}") + endif() +endif() + +# we give an internal name _petsc +# so we can fill up the PETSC_VARIABLE based +# on static or not +pkg_check_modules(_petsc PETSc QUIET) + +if(_petsc_FOUND AND _petsc_VERSION) + set(PETSC_VERSION ${_petsc_VERSION}) +endif() + +# note, there are a number of additional properties that +# can be extracted / set on a target. We just do the basic +# set for now. See: https://cmake.org/cmake/help/latest/module/FindPkgConfig.html +if(PETSC_LINK_STATIC) + set(PETSC_LIBRARIES ${_petsc_STATIC_LIBRARIES}) + set(PETSC_INCLUDE_DIRS ${_petsc_STATIC_INCLUDE_DIRS}) + set(PETSC_LIBRARY_DIRS ${_petsc_STATIC_LIBRARY_DIRS}) + set(PETSC_LDFLAGS ${_petsc_STATIC_LDFLAGS}) + set(PETSC_LDFLAGS_OTHER ${_petsc_STATIC_LDFLAGS_OTHER}) +elseif(_petsc_FOUND) + set(PETSC_LIBRARIES ${_petsc_LIBRARIES}) + set(PETSC_INCLUDE_DIRS ${_petsc_INCLUDE_DIRS}) + set(PETSC_LIBRARY_DIRS ${_petsc_LIBRARY_DIRS}) + set(PETSC_LDFLAGS ${_petsc_LDFLAGS}) + set(PETSC_LDFLAGS_OTHER ${_petsc_LDFLAGS_OTHER}) +endif() + +if(NOT TARGET PETSc::PETSc) + add_library(PETSc::PETSc INTERFACE IMPORTED GLOBAL) + set_target_properties(PETSc::PETSc PROPERTIES INTERFACE_LINK_LIBRARIES "${PETSC_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${PETSC_INCLUDE_DIRS}" + INTERFACE_LINK_DIRECTORIES "${PETSC_LIBRARY_DIRS}" + INTERFACE_LINK_OPTIONS "${PETSC_LDFLAGS_OTHER}") +endif() + + + +include(FindPackageHandleStandardArgs) +# TODO consider adding version check logic +find_package_handle_standard_args(PETSc + REQUIRED_VARS PETSC_LIBRARIES PETSC_INCLUDE_DIRS + VERSION_VAR PETSC_VERSION) diff --git a/config.cmake.in b/config.cmake.in index 65c2dc64b..bf8b4882e 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -1,15 +1,28 @@ @PACKAGE_INIT@ include(CMakeFindDependencyMacro) + +# this will let us find any installed FindXXX.cmake e.g., petsc / gmsh +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") + find_dependency(redev CONFIG HINTS @redev_DIR@) find_dependency(Kokkos CONFIG HINTS @Kokkos_DIR@) find_dependency(KokkosKernels CONFIG HINTS @KokkosKernels_DIR@) -find_dependency(meshfields CONFIG HINTS @meshfields_DIR@) find_dependency(ADIOS2 CONFIG HINTS @ADIOS2_DIR@) find_dependency(MPI) -if(@spdlog_FOUND) - find_dependency(spdlog CONFIG HINTS @spdlog_DIR@) +if(@PCMS_ENABLE_MESHFIELDS@) + find_dependency(meshfields CONFIG HINTS @meshfields_DIR@) +endif() + +if(@PCMS_ENABLE_PETSC@) + if(NOT DEFINED PETSC_DIR) + set(PETSC_DIR "@PETSC_DIR@") + endif() + if(NOT DEFINED PETSC_ARCH) + set(PETSC_ARCH "@PETSC_ARCH@") + endif() + find_dependency(PETSc) endif() if(@PCMS_ENABLE_OMEGA_H@) @@ -17,18 +30,22 @@ if(@PCMS_ENABLE_OMEGA_H@) endif() include("${CMAKE_CURRENT_LIST_DIR}/pcms_utility-targets.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/pcms_discretization-targets.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/pcms_localization-targets.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/pcms_field-targets.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/pcms_coupler-targets.cmake") include("${CMAKE_CURRENT_LIST_DIR}/pcms_core-targets.cmake") -include("${CMAKE_CURRENT_LIST_DIR}/pcms_interpolator-targets.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/pcms_transfer-targets.cmake") if(@PCMS_ENABLE_C@) include("${CMAKE_CURRENT_LIST_DIR}/pcms_capi_core-targets.cmake") - include("${CMAKE_CURRENT_LIST_DIR}/pcms_capi_interpolator-targets.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/pcms_capi_transfer-targets.cmake") include("${CMAKE_CURRENT_LIST_DIR}/pcms_capi-targets.cmake") endif() if(@PCMS_ENABLE_Fortran@) include("${CMAKE_CURRENT_LIST_DIR}/pcms_fortranapi_core-targets.cmake") - include("${CMAKE_CURRENT_LIST_DIR}/pcms_fortranapi_interpolator-targets.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/pcms_fortranapi_transfer-targets.cmake") include("${CMAKE_CURRENT_LIST_DIR}/pcms_fortranapi-targets.cmake") endif() diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..1002a7985 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,87 @@ +# Build stage with Spack pre-installed and ready to be used +FROM spack/ubuntu-jammy:develop AS builder + + +# What we want to install and how we want to install it +# is specified in a manifest file (spack.yaml) +RUN mkdir -p /opt/spack-environment && \ +set -o noclobber \ +&& (echo spack: \ +&& echo ' # add package specs to the `specs` list' \ +&& echo ' specs:' \ +&& echo ' - fftw' \ +&& echo ' - pcms@develop+python~tests %gcc ^mpich ^omega-h+trilinos ^trilinos@16.1.0+exodus ^netcdf-c@4.8.1+mpi' \ +&& echo ' - py-numpy' \ +&& echo ' view: /opt/views/view' \ +&& echo ' concretizer:' \ +&& echo ' unify: true' \ +&& echo ' packages:' \ +&& echo ' llvm:' \ +&& echo ' externals:' \ +&& echo ' - spec: llvm@18.1.3+clang~flang~lld~lldb' \ +&& echo ' prefix: /usr' \ +&& echo ' extra_attributes:' \ +&& echo ' compilers:' \ +&& echo ' c: /usr/bin/clang-18' \ +&& echo ' cxx: /usr/bin/clang++-18' \ +&& echo ' gcc:' \ +&& echo ' externals:' \ +&& echo ' - spec: gcc@13.3.0 languages:='"'"'c,c++,fortran'"'"'' \ +&& echo ' prefix: /usr' \ +&& echo ' extra_attributes:' \ +&& echo ' compilers:' \ +&& echo ' c: /usr/bin/gcc' \ +&& echo ' cxx: /usr/bin/g++' \ +&& echo ' fortran: /usr/bin/gfortran' \ +&& echo ' config:' \ +&& echo ' install_tree:' \ +&& echo ' root: /opt/software' \ +&& echo ' build_jobs: 4') > /opt/spack-environment/spack.yaml + +RUN spack repo add https://github.com/spack/spack-packages.git + +# Clone and add the custom pcms spack repo +RUN git clone https://github.com/jacobmerson/pcms-spack.git /opt/pcms-spack && \ + spack repo add /opt/pcms-spack/spack_repo/pcms + +# Patch omega-h package.py to add the SEACASExodus flag +RUN sed -i 's/args.append("-DOmega_h_USE_Trilinos:BOOL=ON")/args.append("-DOmega_h_USE_Trilinos:BOOL=ON")\n args.append("-DOmega_h_USE_SEACASExodus:BOOL=ON")/' \ + $(spack location -p omega-h)/package.py + +# Install the software, remove unnecessary deps +RUN cd /opt/spack-environment && spack env activate . && spack install --fail-fast && spack gc -y + +# Strip all the binaries +RUN find -L /opt/views/view/* -type f -exec readlink -f '{}' \; | \ + xargs file -i | \ + grep 'charset=binary' | \ + grep 'x-executable\|x-archive\|x-sharedlib' | \ + awk -F: '{print $1}' | xargs strip + +# Modifications to the environment that are necessary to run +RUN cd /opt/spack-environment && \ + spack env activate --sh -d . > activate.sh + + +# Bare OS image to run the installed executables +FROM ubuntu:22.04 + +COPY --from=builder /opt/spack-environment /opt/spack-environment +COPY --from=builder /opt/software /opt/software + +# paths.view is a symlink, so copy the parent to avoid dereferencing and duplicating it +COPY --from=builder /opt/views /opt/views + +RUN { \ + echo '#!/bin/sh' \ + && echo '.' /opt/spack-environment/activate.sh \ + && echo 'exec "$@"'; \ + } > /entrypoint.sh \ +&& chmod a+x /entrypoint.sh \ +&& ln -s /opt/views/view /opt/view + +RUN echo "export PYTHONPATH=$(dirname $(ls $(spack location -i pcms)/lib/python*/site-packages/pcms*.so)):\$PYTHONPATH" >> /etc/bash.bashrc + +ENTRYPOINT [ "/entrypoint.sh" ] +CMD [ "/bin/bash" ] + diff --git a/examples/external-usage-example/CMakeLists.txt b/examples/external-usage-example/CMakeLists.txt index 4cb7b60d9..4b96f046d 100644 --- a/examples/external-usage-example/CMakeLists.txt +++ b/examples/external-usage-example/CMakeLists.txt @@ -6,6 +6,10 @@ cmake_minimum_required(VERSION 3.19) project(pcms_installation_test) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") + +message(STATUS "MODULE PATH: ${CMAKE_MODULE_PATH}") + # It searches in pcms_ROOT, pcms_DIR, or other standard paths find_package(pcms REQUIRED) diff --git a/examples/external-usage-example/main.cpp b/examples/external-usage-example/main.cpp index 8cc23e430..3af974b30 100644 --- a/examples/external-usage-example/main.cpp +++ b/examples/external-usage-example/main.cpp @@ -1,6 +1,5 @@ #include #include -#include #include int main() { diff --git a/examples/python-api-example/flow_example.py b/examples/python-api-example/flow_example.py index 36b6656eb..e1bd5cf3d 100644 --- a/examples/python-api-example/flow_example.py +++ b/examples/python-api-example/flow_example.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import pcms import numpy as np -import os # import matplotlib.pyplot as plt @@ -45,23 +44,31 @@ def demonstrate_face_field_transfer(mesh): mesh, face_field_values, face_dim ) print( - f"Vertex field (MLS): min={vertex_field_values.min():.6f}, " + f"Vertex field: min={vertex_field_values.min():.6f}, " f"max={vertex_field_values.max():.6f}, " f"mean={vertex_field_values.mean():.6f}" ) - vertex_layout = pcms.create_lagrange_layout(mesh, 1, 1, pcms.CoordinateSystem.Cartesian) - omega_h_field = vertex_layout.create_field() + omega_h_factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 1, pcms.CoordinateSystem.Cartesian + ) + omega_h_field = omega_h_factory.create_field() omega_h_field.set_dof_holder_data(vertex_field_values) divisions = [1000, 500] grid = pcms.create_uniform_grid_from_mesh(mesh, divisions) - ug_layout = pcms.UniformGridFieldLayout2D(grid, 1, pcms.CoordinateSystem.Cartesian) - ug_field = ug_layout.create_field() + ug_factory = pcms.LagrangeFunctionSpace.from_uniform_grid( + grid, 1, pcms.CoordinateSystem.Cartesian + ) + ug_field = ug_factory.create_field() - omega_h_field.set_out_of_bounds_mode(pcms.OutOfBoundsMode.FILL, 0.0) print(f"Interpolating field from OmegaH mesh to uniform grid...") - pcms.interpolate_field(omega_h_field, ug_field) + interp = pcms.Interpolator( + omega_h_factory, + ug_factory, + pcms.OutOfBoundsPolicy(pcms.OutOfBoundsMode.FILL, 0.0), + ) + interp.apply(omega_h_field, ug_field) transferred_data = ug_field.get_dof_holder_data() print(f"Transferred field: min={np.min(transferred_data):.6f}, max={np.max(transferred_data):.6f}, mean={np.mean(transferred_data):.6f}") diff --git a/pyproject.toml b/pyproject.toml index 3b85337fe..949bcbcc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "pcms" -version = "0.3.0" +version = "0.4.0" [tool.scikit-build.cmake.define] PCMS_ENABLE_Python = true -PCMS_ENABLE_SPDLOG = false diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 80dab0f54..143889fe1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,41 +2,11 @@ set( PCMS_HEADERS pcms.h - pcms/bounding_box.h - pcms/coordinate.h - pcms/coordinate_systems.h - pcms/coordinate_transform.h - pcms/field.h - pcms/create_field.h - pcms/field_communicator.h - pcms/field_communicator2.h - pcms/field_evaluation_methods.h - pcms/field_layout_communicator.h - pcms/partition.h - pcms/coupler.h - pcms/coupler2.h - pcms/coordinate_system.h - pcms/field_layout.h - pcms/field_layout_communicator.h ) set( PCMS_SOURCES pcms.cpp - pcms/create_field.cpp - pcms/coupler2.cpp - pcms/field_layout_communicator.cpp - pcms/adapter/point_cloud/point_cloud_layout.cpp - pcms/adapter/point_cloud/point_cloud.cpp - pcms/adapter/meshfields/mesh_fields_adapter_layout.cpp -) -set( - ADAPTER_HEADERS - pcms/adapter/point_cloud/point_cloud_layout.h - pcms/adapter/point_cloud/point_cloud.h - pcms/adapter/meshfields/mesh_fields_adapter_layout.h - pcms/adapter/meshfields/mesh_fields_adapter2.h - pcms/adapter/xgc/xgc_field_adapter.h ) configure_file(pcms/version.h.in pcms/version.h) @@ -44,31 +14,12 @@ configure_file(pcms/configuration.h.in pcms/configuration.h) list(APPEND PCMS_HEADERS ${CMAKE_CURRENT_BINARY_DIR}/pcms/version.h ${CMAKE_CURRENT_BINARY_DIR}/pcms/configuration.h) add_subdirectory(pcms/utility) +add_subdirectory(pcms/discretization) +add_subdirectory(pcms/localization) +add_subdirectory(pcms/field) +add_subdirectory(pcms/coupler) + -if(PCMS_ENABLE_XGC) - list(APPEND PCMS_SOURCES pcms/adapter/xgc/xgc_reverse_classification.cpp) - list(APPEND ADAPTER_HEADERS pcms/adapter/xgc/xgc_reverse_classification.h) -endif() -if(PCMS_ENABLE_OMEGA_H) - list(APPEND PCMS_SOURCES - pcms/point_search.cpp - pcms/adapter/uniform_grid/uniform_grid_field_layout.cpp - pcms/adapter/uniform_grid/uniform_grid_field.cpp - ) - list( - APPEND - PCMS_HEADERS - pcms/transfer_field.h - pcms/transfer_field2.h - pcms/uniform_grid.h - pcms/point_search.h - ) - list(APPEND ADAPTER_HEADERS - pcms/adapter/meshfields/mesh_fields_adapter.h - pcms/adapter/uniform_grid/uniform_grid_field_layout.h - pcms/adapter/uniform_grid/uniform_grid_field.h - ) -endif() find_package(Kokkos REQUIRED) find_package(perfstubs REQUIRED) @@ -79,14 +30,11 @@ set_target_properties( core ) add_library(pcms::core ALIAS pcms_core) -target_compile_features(pcms_core PUBLIC cxx_std_17) +target_compile_features(pcms_core PUBLIC cxx_std_20) target_link_libraries( - pcms_core PUBLIC meshfields::meshfields redev::redev - MPI::MPI_CXX Kokkos::kokkos perfstubs pcms::utility + pcms_core PUBLIC redev::redev + MPI::MPI_CXX Kokkos::kokkos perfstubs pcms::utility pcms::localization pcms::field pcms::coupler ) -if(PCMS_ENABLE_OMEGA_H) - target_link_libraries(pcms_core PUBLIC Omega_h::omega_h) -endif() if(PCMS_HAS_ASAN) target_compile_options( @@ -103,11 +51,6 @@ target_include_directories( "$" "$" ) -target_sources(pcms_core PUBLIC - FILE_SET adapters - TYPE HEADERS - BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/pcms - FILES ${ADAPTER_HEADERS}) install( TARGETS pcms_core EXPORT pcms_core-targets @@ -117,7 +60,6 @@ install( INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms - FILE_SET adapters DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms ) configure_package_config_file( @@ -135,6 +77,9 @@ install( DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms ) +install(FILES "${CMAKE_SOURCE_DIR}/cmake/FindPETSc.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) + install( EXPORT pcms_core-targets NAMESPACE pcms:: @@ -148,7 +93,6 @@ if (PCMS_ENABLE_Python) # Disable LTO/IPO before adding Python subdirectory to avoid fatbinData conflicts set(CMAKE_INTERPROCEDURAL_OPTIMIZATION OFF) add_subdirectory(pcms/pythonapi) - target_link_libraries(pcms_pcms INTERFACE pcms::pythonapi) endif() if(PCMS_ENABLE_C) add_subdirectory(pcms/capi) @@ -159,8 +103,8 @@ if(PCMS_ENABLE_Fortran) target_link_libraries(pcms_pcms INTERFACE pcms::fortranapi) endif() -add_subdirectory(pcms/interpolator) -target_link_libraries(pcms_pcms INTERFACE pcms::interpolator) +add_subdirectory(pcms/transfer) +target_link_libraries(pcms_pcms INTERFACE pcms::transfer) install( TARGETS pcms_pcms @@ -170,8 +114,6 @@ install( RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms - FILE_SET adapters DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms ) diff --git a/src/pcms.cpp b/src/pcms.cpp index 44d42911f..2d357381c 100644 --- a/src/pcms.cpp +++ b/src/pcms.cpp @@ -1,5 +1,7 @@ #include "pcms.h" #include "pcms/utility/types.h" +#include "redev.h" +#include "Omega_h_mesh.hpp" namespace pcms { diff --git a/src/pcms.h b/src/pcms.h index 978c280b4..a98dc025d 100644 --- a/src/pcms.h +++ b/src/pcms.h @@ -4,8 +4,5 @@ #include "pcms/configuration.h" #include "pcms/utility/common.h" #include "pcms/utility/profile.h" -#include "pcms/field_communicator.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include "pcms/coupler.h" #endif diff --git a/src/pcms/README.md b/src/pcms/README.md new file mode 100644 index 000000000..71e600cb0 --- /dev/null +++ b/src/pcms/README.md @@ -0,0 +1,11 @@ +# Discretization Components +The discretization library provides interfaces on top of discretization libraries. This library provides a baseline set of APIs over the discretizations as well as a set of basic coordinate systems. + +Current Support: +- Omega_h +- UniformGrid + +Planned Future Work: +- CartesianGrid (variable spacing) +- MFEM +- AMREX diff --git a/src/pcms/adapter/dummy_field_adapter.h b/src/pcms/adapter/dummy_field_adapter.h deleted file mode 100644 index 0238cb05e..000000000 --- a/src/pcms/adapter/dummy_field_adapter.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef PCMS_SRC_PCMS_DUMMY_FIELD_ADAPTER_H -#define PCMS_SRC_PCMS_DUMMY_FIELD_ADAPTER_H -#include "pcms/field.h" -#include "pcms/partition.h" -#include -namespace pcms -{ -class DummyFieldAdapter -{ -public: - using value_type = int; - [[nodiscard]] std::vector GetGids() const { return {}; } - [[nodiscard]] ReversePartitionMap GetReversePartitionMap( - const Partition& partition) const - { - return {}; - } - template - int Serialize(T1, T2) const noexcept - { - return 0; - } - // function so that call to Serialize({},{}) works. - int Serialize(int, int) const noexcept { return 0; } - template - void Deserialize(T1, T2) const noexcept - { - } -}; - -} // namespace pcms - -#endif // PCMS_SRC_PCMS_DUMMY_FIELD_ADAPTER_H diff --git a/src/pcms/adapter/meshfields/mesh_fields_adapter.h b/src/pcms/adapter/meshfields/mesh_fields_adapter.h deleted file mode 100644 index b91f06f9a..000000000 --- a/src/pcms/adapter/meshfields/mesh_fields_adapter.h +++ /dev/null @@ -1,596 +0,0 @@ -#ifndef PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_ADAPTER_H -#define PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_ADAPTER_H -#include "pcms/utility/types.h" -#include -#include "pcms/field.h" -#include "pcms/coordinate_systems.h" -#include -#include -#include -#include -#include "pcms/utility/arrays.h" -#include "pcms/utility/array_mask.h" -#include "pcms/point_search.h" -#include -#include -#include "pcms/transfer_field.h" -#include "pcms/utility/memory_spaces.h" -#include "pcms/utility/profile.h" -#include "pcms/partition.h" -#include - -// FIXME add executtion spaces (don't use kokkos exe spaces directly) - -namespace pcms -{ - -// TODO different types dependent on active OmegaHBackend -struct OmegaHMemorySpace -{ - using type = typename Kokkos::DefaultExecutionSpace::memory_space; -}; - -enum class mesh_entity_type : int -{ - VERTEX = 0, - EDGE = 1, - FACE = 2, - REGION = 3 -}; - -inline int mesh_entity_to_int(mesh_entity_type entity_type) -{ - static_assert(std::is_same_v, int>, - "mesh_entity_type must be an int"); - return static_cast>(entity_type); -} - -namespace detail -{ -template -struct memory_space_selector, void> -{ - using type = typename OmegaHMemorySpace::type; -}; -template -struct memory_space_selector, void> -{ - using type = typename OmegaHMemorySpace::type; -}; -template -struct memory_space_selector, void> -{ - using type = typename pcms::HostMemorySpace; -}; -template -struct memory_space_selector, void> -{ - using type = typename pcms::HostMemorySpace; -}; -template -Omega_h::Read filter_array(Omega_h::Read array, - const Omega_h::Read& mask, LO size) -{ - PCMS_FUNCTION_TIMER; - static_assert(dim > 0, "array dimension must be >0"); - Omega_h::Write filtered_field(size * dim); - PCMS_ALWAYS_ASSERT(array.size() == mask.size() * dim); - PCMS_ALWAYS_ASSERT(filtered_field.size() <= array.size()); - Omega_h::parallel_for( - mask.size(), OMEGA_H_LAMBDA(LO i) { - if (mask[i]) { - const auto idx = mask[i] - 1; - for (int j = 0; j < dim; ++j) { - filtered_field[idx * dim + j] = array[i * dim + j]; - } - } - }); - return filtered_field; -} -} // namespace detail - -template // CoordinateElement> -class MeshFieldsAdapter -{ -public: - using memory_space = OmegaHMemorySpace::type; - using value_type = T; - - MeshFieldsAdapter(std::string name, Omega_h::Mesh& mesh, - std::string global_id_name = "", int search_nx = 10, - int search_ny = 10, - mesh_entity_type entity_type = mesh_entity_type::VERTEX) - : name_(std::move(name)), - mesh_(mesh), - size_(mesh.nents(mesh_entity_to_int(entity_type))), - global_id_name_(std::move(global_id_name)), - entity_type_(entity_type) - { - PCMS_FUNCTION_TIMER; - } - MeshFieldsAdapter(std::string name, Omega_h::Mesh& mesh, - Omega_h::Read mask, - std::string global_id_name = "", int search_nx = 10, - int search_ny = 10, - mesh_entity_type entity_type = mesh_entity_type::VERTEX) - : name_(std::move(name)), - mesh_(mesh), - global_id_name_(std::move(global_id_name)), - entity_type_(entity_type) - { - PCMS_FUNCTION_TIMER; - if (mask.exists()) { - - using ExecutionSpace = typename memory_space::execution_space; - auto policy = Kokkos::RangePolicy(0, mask.size()); - // we use a parallel scan to construct the mask mapping so that filtering - // can happen in parallel. This method gives us the index to fill into the - // filtered array - PCMS_ALWAYS_ASSERT(mesh.nents(mesh_entity_to_int(entity_type_)) == - mask.size()); - Omega_h::Write index_mask(mask.size()); - auto index_mask_view = make_array_view(index_mask); - auto mask_view = make_const_array_view(mask); - Kokkos::parallel_scan( - policy, detail::ComputeMaskAV{index_mask_view, mask_view}, size_); - Kokkos::parallel_for(policy, detail::ScaleAV{index_mask_view, mask_view}); - mask_ = index_mask; - } else { - size_ = mesh.nents(mesh_entity_to_int(entity_type_)); - } - } - - [[nodiscard]] const std::string& GetName() const noexcept { return name_; } - [[nodiscard]] Omega_h::Mesh& GetMesh() const noexcept { return mesh_; } - [[nodiscard]] const Omega_h::Read& GetMask() const noexcept - { - return mask_; - }; - [[nodiscard]] bool HasMask() const noexcept { return mask_.exists(); }; - [[nodiscard]] mesh_entity_type GetEntityType() const noexcept - { - return entity_type_; - } - [[nodiscard]] LO Size() const noexcept { return size_; } - void ConstructSearch(int nx, int ny) - { - PCMS_FUNCTION_TIMER; - search_ = std::make_unique(mesh_, nx, ny); - } - // pass through to search function - [[nodiscard]] auto Search(Kokkos::View points) const - { - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(search_ != nullptr && - "search data structure must be constructed before use"); - return (*search_)(points); - } - - [[nodiscard]] Omega_h::Read GetClassIDs() const - { - PCMS_FUNCTION_TIMER; - if (HasMask()) - return detail::filter_array( - mesh_.get_array(mesh_entity_to_int(entity_type_), - "class_id"), - GetMask(), Size()); - return mesh_.get_array(mesh_entity_to_int(entity_type_), - "class_id"); - } - [[nodiscard]] Omega_h::Read GetClassDims() const - { - PCMS_FUNCTION_TIMER; - if (HasMask()) - return detail::filter_array( - mesh_.get_array(mesh_entity_to_int(entity_type_), - "class_dim"), - GetMask(), Size()); - return mesh_.get_array(mesh_entity_to_int(entity_type_), - "class_dim"); - } - [[nodiscard]] Omega_h::Read GetGids() const - { - PCMS_FUNCTION_TIMER; - Omega_h::Read gid_array; - if (global_id_name_.empty()) { - gid_array = mesh_.globals(mesh_entity_to_int(entity_type_)); - } else { - auto tag = - mesh_.get_tagbase(mesh_entity_to_int(entity_type_), global_id_name_); - if (Omega_h::is(tag)) { - gid_array = mesh_.get_array( - mesh_entity_to_int(entity_type_), global_id_name_); - } else if (Omega_h::is(tag)) { - auto array = mesh_.get_array( - mesh_entity_to_int(entity_type_), global_id_name_); - Omega_h::Write globals(array.size()); - Omega_h::parallel_for( - array.size(), OMEGA_H_LAMBDA(int i) { globals[i] = array[i]; }); - gid_array = Omega_h::Read(globals); - } else { - std::cerr << "Weird tag type for global arrays.\n"; - std::abort(); - } - } - if (HasMask()) { - return detail::filter_array(gid_array, GetMask(), Size()); - } - return gid_array; - } - -private: - std::string name_; - Omega_h::Mesh& mesh_; - // TODO: introduce base class to Search for alternative search methods - std::unique_ptr search_; - // bitmask array that specifies a filter on the field - Omega_h::Read mask_; - LO size_; - std::string global_id_name_; - mesh_entity_type entity_type_; -}; - -// internal field can only be one of the types supported by Omega_h -// The coordinate element for all internal fields is the same since -// all internal fields are on the same mesh -using InternalField = - std::variant, MeshFieldsAdapter, - MeshFieldsAdapter, - MeshFieldsAdapter>; - -template -auto get_nodal_data(const MeshFieldsAdapter& field) -> Omega_h::Read -{ - PCMS_FUNCTION_TIMER; - auto full_field = field.GetMesh().template get_array( - mesh_entity_to_int(field.GetEntityType()), field.GetName()); - if (field.HasMask()) { - return detail::filter_array(full_field, field.GetMask(), field.Size()); - } - return full_field; -} - -// TODO since Omega_h owns coordinate data, we could potentially -// return a view of the data without lifetime issues. -template -auto get_nodal_coordinates(const MeshFieldsAdapter& field) -{ - PCMS_FUNCTION_TIMER; - static constexpr auto coordinate_dimension = 2; - auto coords = get_ent_centroids(field.GetMesh(), - mesh_entity_to_int(field.GetEntityType())); - if (field.HasMask()) { - // FIXME dimension should be made runtime parameter - return detail::filter_array(coords, field.GetMask(), - field.Size()); - } - return coords; - // should never be here. Quash warning - return Omega_h::Reals{}; -} - -/** - * Sets the data on the entire mesh - */ -template -auto set_nodal_data(const MeshFieldsAdapter& field, - Rank1View data) -> void -{ - PCMS_FUNCTION_TIMER; - static_assert(std::is_convertible_v, - "must be able to convert nodal data into the field types data"); - auto& mesh = field.GetMesh(); - auto entity_type = field.GetEntityType(); - const auto has_tag = - mesh.has_tag(mesh_entity_to_int(entity_type), field.GetName()); - if (field.HasMask()) { - auto& mask = field.GetMask(); - PCMS_ALWAYS_ASSERT(mask.size() == - mesh.nents(mesh_entity_to_int(entity_type))); - Omega_h::Write array(mask.size()); - if (has_tag) { - auto original_data = mesh.template get_array( - mesh_entity_to_int(entity_type), field.GetName()); - PCMS_ALWAYS_ASSERT(original_data.size() == mask.size()); - Omega_h::parallel_for( - mask.size(), OMEGA_H_LAMBDA(size_t i) { - array[i] = mask[i] ? data(mask[i] - 1) : original_data[i]; - }); - mesh.set_tag(mesh_entity_to_int(entity_type), field.GetName(), - Omega_h::Read(array)); - } else { - Omega_h::parallel_for( - mask.size(), OMEGA_H_LAMBDA(size_t i) { - array[i] = mask[i] ? data(mask[i] - 1) : 0; - }); - mesh.add_tag(mesh_entity_to_int(entity_type), field.GetName(), 1, - Omega_h::Read(array)); - } - } else { - PCMS_ALWAYS_ASSERT(static_cast(data.size()) == - mesh.nents(mesh_entity_to_int(entity_type))); - Omega_h::Write array(data.size()); - Omega_h::parallel_for( - data.size(), OMEGA_H_LAMBDA(size_t i) { array[i] = data(i); }); - if (has_tag) { - mesh.set_tag(mesh_entity_to_int(entity_type), field.GetName(), - Omega_h::Read(array)); - } else { - mesh.add_tag(mesh_entity_to_int(entity_type), field.GetName(), 1, - Omega_h::Read(array)); - } - } - PCMS_ALWAYS_ASSERT( - mesh.has_tag(mesh_entity_to_int(entity_type), field.GetName())); -} - -// TODO abstract out repeat parts of lagrange/nearest neighbor evaluation -template -auto evaluate(const MeshFieldsAdapter& field, Lagrange<1> /* method */, - Rank1View coordinates) - -> Omega_h::Read -{ - PCMS_FUNCTION_TIMER; - Omega_h::Write values(coordinates.size() / 2); - auto tris2verts = field.GetMesh().ask_elem_verts(); - auto field_values = field.GetMesh().template get_array(0, field.GetName()); - - Kokkos::View coords("coords", coordinates.size() / 2); - Kokkos::parallel_for( - coordinates.size() / 2, KOKKOS_LAMBDA(LO i) { - coords(i, 0) = coordinates(2 * i); - coords(i, 1) = coordinates(2 * i + 1); - }); - auto results = field.Search(coords); - - Kokkos::parallel_for( - results.size(), KOKKOS_LAMBDA(LO i) { - auto [dim, elem_idx, coord] = results(i); - // TODO deal with case for elem_idx < 0 (point outside of mesh) - KOKKOS_ASSERT(elem_idx >= 0); - const auto elem_tri2verts = - Omega_h::gather_verts<3>(tris2verts, elem_idx); - Real val = 0; - for (int j = 0; j < 3; ++j) { - val += field_values[elem_tri2verts[j]] * coord[j]; - } - if constexpr (std::is_integral_v) { - val = std::round(val); - } - values[i] = val; - }); - - return values; -} - -template -auto evaluate(const MeshFieldsAdapter& field, NearestNeighbor /* method */, - Rank1View coordinates) - -> Omega_h::Read -{ - PCMS_FUNCTION_TIMER; - Omega_h::Write values(coordinates.size() / 2); - auto tris2verts = field.GetMesh().ask_elem_verts(); - auto field_values = field.GetMesh().template get_array(0, field.GetName()); - // TODO reuse coordinates_data if possible - Kokkos::View coords("coords", coordinates.size() / 2); - Kokkos::parallel_for( - coordinates.size() / 2, KOKKOS_LAMBDA(LO i) { - coords(i, 0) = coordinates(2 * i); - coords(i, 1) = coordinates(2 * i + 1); - }); - auto results = field.Search(coords); - - Kokkos::parallel_for( - results.size(), KOKKOS_LAMBDA(LO i) { - auto [dim, elem_idx, coord] = results(i); - // TODO deal with case for elem_idx < 0 (point outside of mesh) - KOKKOS_ASSERT(elem_idx >= 0); - const auto elem_tri2verts = - Omega_h::gather_verts<3>(tris2verts, elem_idx); - // value is closest to point has the largest coordinate - int vert = 0; - auto max_val = coord[0]; - for (int j = 1; j <= 2; ++j) { - auto next_val = coord[j]; - if (next_val > max_val) { - max_val = next_val; - vert = j; - } - } - values[i] = field_values[elem_tri2verts[vert]]; - }); - return values; -} - -template -auto evaluate(const MeshFieldsAdapter& field, Method&& m, - Rank1View coordinates) - -> std::enable_if_t< - !std::is_same_v, - Omega_h::HostRead> - -{ - PCMS_FUNCTION_TIMER; - auto coords_view = Kokkos::View>( - &coordinates[0], coordinates.size()); - using exe_space = typename OmegaHMemorySpace::type::execution_space; - auto coordinates_d = - Kokkos::create_mirror_view_and_copy(exe_space(), coords_view); - return Omega_h::HostRead(evaluate(field, std::forward(m), - make_const_array_view(coordinates_d))); -} - -} // namespace pcms -namespace Omega_h -{ -template -auto make_array_view(const Omega_h::Read& array) - -> pcms::Rank1View -{ - PCMS_FUNCTION_TIMER; - pcms::Rank1View view( - array.data(), array.size()); - return view; -} - -// ? how the above works without inline but this doesn't -inline Omega_h::Reals get_ent_centroids(Omega_h::Mesh& mesh, int entity_type) -{ - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(entity_type >= 0 && entity_type <= 3); - if (entity_type == 0) { - return mesh.coords(); - } else { - auto coords = mesh.coords(); - int dim = mesh.dim(); - auto ent2verts = mesh.ask_down(entity_type, Omega_h::VERT).ab2b; - auto nents = mesh.nents(entity_type); - Omega_h::Write ent_coords(nents * dim); - - auto calc_coords = OMEGA_H_LAMBDA(LO ent) - { - if (dim == 2) { - auto verts = Omega_h::gather_verts<3>(ent2verts, ent); - auto ent_vert_coords = Omega_h::gather_vectors<3, 2>(coords, verts); - auto ent_centroid = Omega_h::average(ent_vert_coords); - ent_coords[ent * dim] = ent_centroid[0]; - ent_coords[ent * dim + 1] = ent_centroid[1]; - } else if (dim == 3) { - auto verts = Omega_h::gather_verts<4>(ent2verts, ent); - auto ent_vert_coords = Omega_h::gather_vectors<4, 3>(coords, verts); - auto ent_centroid = Omega_h::average(ent_vert_coords); - ent_coords[ent * dim] = ent_centroid[0]; - ent_coords[ent * dim + 1] = ent_centroid[1]; - ent_coords[ent * dim + 2] = ent_centroid[2]; - } - }; - Omega_h::parallel_for(nents, calc_coords); - return Omega_h::Reals((ent_coords)); - } -} -} // namespace Omega_h - -namespace pcms -{ - -template -class OmegaHFieldAdapter -{ -public: - using memory_space = OmegaHMemorySpace::type; - using value_type = T; - OmegaHFieldAdapter(std::string name, Omega_h::Mesh& mesh, - std::string global_id_name = "", int search_nx = 10, - int search_ny = 10, - mesh_entity_type entity_type = mesh_entity_type::VERTEX) - : field_{std::move(name), mesh, std::move(global_id_name), - search_nx, search_ny, entity_type}, - entity_type_{entity_type} - { - PCMS_FUNCTION_TIMER; - } - - OmegaHFieldAdapter(std::string name, Omega_h::Mesh& mesh, - Omega_h::Read mask, - std::string global_id_name = "", int search_nx = 10, - int search_ny = 10, - mesh_entity_type entity_type = mesh_entity_type::VERTEX) - : field_{std::move(name), mesh, mask, std::move(global_id_name), - search_nx, search_ny, entity_type}, - entity_type_{entity_type} - { - PCMS_FUNCTION_TIMER; - } - [[nodiscard]] const std::string& GetName() const noexcept - { - return field_.GetName(); - } - // REQUIRED - int Serialize( - Rank1View buffer, - Rank1View permutation) const - { - PCMS_FUNCTION_TIMER; - // host copy of filtered field data array - const auto array_h = Omega_h::HostRead(get_nodal_data(field_)); - if (buffer.size() > 0) { - for (LO i = 0; i < array_h.size(); i++) { - buffer[i] = array_h[permutation[i]]; - } - } - return array_h.size(); - } - // REQUIRED - void Deserialize( - Rank1View buffer, - Rank1View permutation) const - { - PCMS_FUNCTION_TIMER; - REDEV_ALWAYS_ASSERT(buffer.size() == permutation.size()); - Omega_h::HostWrite sorted_buffer(buffer.size()); - for (size_t i = 0; i < buffer.size(); ++i) { - sorted_buffer[permutation[i]] = buffer[i]; - } - const auto sorted_buffer_d = Omega_h::Read(sorted_buffer); - set_nodal_data(field_, make_array_view(sorted_buffer_d)); - } - - [[nodiscard]] std::vector GetGids() const - { - PCMS_FUNCTION_TIMER; - auto gids = field_.GetGids(); - if (gids.size() > 0) { - auto gids_h = Omega_h::HostRead(gids); - return {&gids_h[0], &(gids_h[gids_h.size() - 1]) + 1}; - } - return {}; - } - // REQUIRED - [[nodiscard]] ReversePartitionMap GetReversePartitionMap( - const Partition& partition) const - { - PCMS_FUNCTION_TIMER; - auto classIds_h = Omega_h::HostRead(field_.GetClassIDs()); - auto classDims_h = Omega_h::HostRead(field_.GetClassDims()); - // const auto coords = Omega_h::HostRead(field_.GetMesh().coords()); - const auto coords = Omega_h::HostRead( - get_ent_centroids(field_.GetMesh(), mesh_entity_to_int(entity_type_))); - auto dim = field_.GetMesh().dim(); - - // local_index number of vertices going to each destination process by - // calling getRank - degree array - std::array coord; - pcms::ReversePartitionMap reverse_partition; - pcms::LO local_index = 0; - for (auto i = 0; i < classIds_h.size(); i++) { - coord[0] = coords[i * dim]; - coord[1] = coords[i * dim + 1]; - coord[2] = (dim == 3) ? coords[i * dim + 2] : 0.0; - auto dr = partition.GetDr(classIds_h[i], classDims_h[i], coord); - reverse_partition[dr].emplace_back(local_index++); - } - return reverse_partition; - } - // NOT REQUIRED PART OF FieldAdapter interface - [[nodiscard]] MeshFieldsAdapter& GetField() noexcept { return field_; } - // NOT REQUIRED PART OF FieldAdapter interface - [[nodiscard]] const MeshFieldsAdapter& GetField() const noexcept - { - return field_; - } - - [[nodiscard]] mesh_entity_type GetEntityType() const noexcept - { - return entity_type_; - } - -private: - MeshFieldsAdapter field_; - mesh_entity_type entity_type_; -}; -} // namespace pcms - -#endif // PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_ADAPTER_H diff --git a/src/pcms/adapter/meshfields/mesh_fields_adapter2.h b/src/pcms/adapter/meshfields/mesh_fields_adapter2.h deleted file mode 100644 index bc3c151df..000000000 --- a/src/pcms/adapter/meshfields/mesh_fields_adapter2.h +++ /dev/null @@ -1,540 +0,0 @@ -#ifndef PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_ADAPTER2_H -#define PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_ADAPTER2_H - -#include -#include -#include - -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter_layout.h" -#include "pcms/utility/types.h" -#include "pcms/utility/assert.h" -#include "pcms/utility/profile.h" -#include "pcms/field.h" -#include "pcms/coordinate_system.h" -#include "pcms/point_search.h" - -namespace pcms -{ -template -class MeshFieldBackend -{ -public: - virtual ~MeshFieldBackend() = default; - virtual Kokkos::View evaluate(Kokkos::View localCoords, - Kokkos::View offsets) const = 0; - virtual void SetData(Rank1View data, - size_t num_nodes, size_t num_components, int dim) = 0; - virtual void GetData(Rank1View data, size_t num_nodes, - size_t num_components, int dim) const = 0; -}; - -template -class MeshFieldBackendImpl : public MeshFieldBackend -{ -public: - MeshFieldBackendImpl(Omega_h::Mesh& mesh) - : mesh_(mesh), - mesh_field_(mesh), - shape_field_(mesh_field_.template CreateLagrangeField()) - { - } - - Kokkos::View evaluate(Kokkos::View localCoords, - Kokkos::View offsets) const override - { - auto self = const_cast*>(this); - return self->mesh_field_.triangleLocalPointEval(localCoords, offsets, - shape_field_); - } - - void SetData(Rank1View data, size_t num_nodes, - size_t num_components, int dim) override - { - size_t stride = num_nodes * num_components; - auto topo = static_cast(dim); - Kokkos::View data_d("data_d", - data.size()); - Kokkos::deep_copy(data_d, Kokkos::View( - data.data_handle(), data.size())); - Kokkos::parallel_for( - mesh_.nents(dim), KOKKOS_CLASS_LAMBDA(size_t ent) { - for (size_t n = 0; n < num_nodes; ++n) { - for (size_t c = 0; c < num_components; ++c) { - shape_field_(ent, n, c, topo) = - data_d[ent * stride + n * num_components + c]; - } - } - }); - } - - void GetData(Rank1View data, size_t num_nodes, - size_t num_components, int dim) const override - { - size_t stride = num_nodes * num_components; - auto topo = static_cast(dim); - Kokkos::View data_d("data_d", - data.size()); - Kokkos::parallel_for( - mesh_.nents(dim), KOKKOS_CLASS_LAMBDA(size_t ent) { - for (size_t n = 0; n < num_nodes; ++n) { - for (size_t c = 0; c < num_components; ++c) { - data_d[ent * stride + n * num_components + c] = - shape_field_(ent, n, c, topo); - } - } - }); - Kokkos::deep_copy( - Kokkos::View(data.data_handle(), data.size()), - data_d); - } - -private: - Omega_h::Mesh& mesh_; - MeshField::OmegahMeshField mesh_field_; - using ShapeField = - decltype(mesh_field_.template CreateLagrangeField()); - ShapeField shape_field_; -}; - -struct ComputeOffsetsFunctor -{ - Kokkos::View offsets_; - Kokkos::View elem_counts_; - - ComputeOffsetsFunctor(Kokkos::View offsets, - Kokkos::View elem_counts) - : offsets_(offsets), elem_counts_(elem_counts) - { - } - - KOKKOS_INLINE_FUNCTION - void operator()(LO i, LO& partial, bool is_final) const - { - if (is_final) { - offsets_(i) = partial; - } - partial += elem_counts_(i); - } -}; - -struct CountPointsPerElementFunctor -{ - Kokkos::View elem_counts_; - Kokkos::View search_results_; - - CountPointsPerElementFunctor( - Kokkos::View elem_counts, - Kokkos::View search_results) - : elem_counts_(elem_counts), search_results_(search_results) - { - } - - KOKKOS_INLINE_FUNCTION - void operator()(LO i) const - { - auto [dim, elem_idx, coord] = search_results_(i); - Kokkos::atomic_add(&elem_counts_(elem_idx), 1); - } -}; - -struct FillCoordinatesAndIndicesFunctor -{ - Omega_h::Mesh& mesh_; - Kokkos::View elem_counts_; - Kokkos::View offsets_; - Kokkos::View coordinates_; - Kokkos::View indices_; - Kokkos::View search_results_; - Omega_h::Int dim_; - - FillCoordinatesAndIndicesFunctor( - Omega_h::Mesh& mesh, Kokkos::View elem_counts, - Kokkos::View offsets, Kokkos::View coordinates, - Kokkos::View indices, - Kokkos::View search_results) - : mesh_(mesh), - elem_counts_(elem_counts), - offsets_(offsets), - coordinates_(coordinates), - indices_(indices), - search_results_(search_results), - dim_(mesh.dim()) - { - } - - KOKKOS_INLINE_FUNCTION - void operator()(LO i) const - { - auto [dim, elem_idx, coord] = search_results_(i); - // disable the host assertion macro for device code - // currently don't handle case where point is on a boundary - // PCMS_ALWAYS_ASSERT(static_cast(dim) == mesh_.dim()); - // element should be inside the domain (positive) - // PCMS_ALWAYS_ASSERT(elem_idx >= 0 && elem_idx < mesh_.nelems()); - LO count = Kokkos::atomic_sub_fetch(&elem_counts_(elem_idx), 1); - LO index = offsets_(elem_idx) + count - 1; - for (int j = 0; j < (dim_ + 1); ++j) { - coordinates_(index, j) = coord[j]; - } - indices_(index) = i; - } -}; - -struct MeshFieldsAdapter2LocalizationHint -{ - MeshFieldsAdapter2LocalizationHint( - Omega_h::Mesh& mesh, - Kokkos::View search_results, - OutOfBoundsMode mode) - : mode_(mode), num_valid_(0), num_missing_(0) - { - // First pass: count valid and invalid points - std::vector valid_point_indices; - std::vector missing_point_indices; - - if (mode_ == OutOfBoundsMode::ERROR) { - // Error mode - throw error immediately if any point is out of bounds - for (size_t i = 0; i < search_results.size(); ++i) { - auto [dim, elem_idx, coord] = search_results(i); - bool is_missing = - (static_cast(dim) != mesh.dim()) || (elem_idx < 0); - PCMS_ALWAYS_ASSERT(!is_missing && "Points found outside mesh domain"); - valid_point_indices.push_back(i); - } - } else { - // Other modes - collect valid and missing points separately - for (size_t i = 0; i < search_results.size(); ++i) { - auto [dim, elem_idx, coord] = search_results(i); - bool is_missing = - (static_cast(dim) != mesh.dim()) || (elem_idx < 0); - if (is_missing) { - missing_point_indices.push_back(i); - } else { - valid_point_indices.push_back(i); - } - } - } - - num_valid_ = valid_point_indices.size(); - num_missing_ = missing_point_indices.size(); - - // Handle missing points based on mode - if (num_missing_ > 0 && mode_ == OutOfBoundsMode::NEAREST_BOUNDARY) { - PCMS_ALWAYS_ASSERT(false && "NEAREST_BOUNDARY mode not implemented yet"); - } - - // Allocate arrays for valid points only - offsets_ = Kokkos::View("offsets", mesh.nelems() + 1); - coordinates_ = Kokkos::View( - "coordinates", num_valid_, mesh.dim() + 1); - indices_ = Kokkos::View("indices", num_valid_); - - // Store missing point indices - if (num_missing_ > 0) { - missing_indices_ = - Kokkos::View("missing_indices", num_missing_); - for (size_t i = 0; i < num_missing_; ++i) { - missing_indices_(i) = static_cast(missing_point_indices[i]); - } - } - - // Count points per element (valid points only) - Kokkos::View elem_counts("elem_counts", - mesh.nelems()); - for (size_t i = 0; i < num_valid_; ++i) { - auto [dim, elem_idx, coord] = search_results(valid_point_indices[i]); - elem_counts[elem_idx] += 1; - } - - // Compute offsets - LO total; - ComputeOffsetsFunctor functor(offsets_, elem_counts); - Kokkos::parallel_scan( - "ComputeOffsets", - Kokkos::RangePolicy(0, mesh.nelems()), - functor, total); - offsets_(mesh.nelems()) = total; - - // Fill coordinates and indices for valid points - for (size_t i = 0; i < num_valid_; ++i) { - size_t orig_idx = valid_point_indices[i]; - auto [dim, elem_idx, coord] = search_results(orig_idx); - elem_counts(elem_idx) -= 1; - LO index = offsets_(elem_idx) + elem_counts(elem_idx); - for (int j = 0; j < (mesh.dim() + 1); ++j) { - coordinates_(index, j) = coord[j]; - } - indices_(index) = static_cast(orig_idx); - } - } - - OutOfBoundsMode mode_; - size_t num_valid_; - size_t num_missing_; - - // offsets is the number of points in each element - Kokkos::View offsets_; - // coordinates are the parametric coordinates of each point - Kokkos::View coordinates_; - // indices are the index of the original point (for valid points) - Kokkos::View indices_; - // indices of points not found in mesh - Kokkos::View missing_indices_; -}; - -// TODO template over possible MeshFieldsAdapter2Types -template -class MeshFieldsAdapter2 : public FieldT -{ -public: - MeshFieldsAdapter2(const MeshFieldsAdapterLayout& layout); - - LocalizationHint GetLocalizationHint( - CoordinateView coordinate_view) const override; - - void Evaluate(LocalizationHint location, - FieldDataView results) const override; - - void EvaluateGradient(FieldDataView results) override; - - const FieldLayout& GetLayout() const override; - - bool CanEvaluateGradient() override; - - int Serialize(Rank1View buffer, - Rank1View permutation) - const override; - - void Deserialize( - Rank1View buffer, - Rank1View permutation) override; - - Rank1View GetDOFHolderData() const override; - void SetDOFHolderData(Rank1View data) override; - - ~MeshFieldsAdapter2() noexcept = default; - -private: - const MeshFieldsAdapterLayout& layout_; - Omega_h::Mesh& mesh_; - std::unique_ptr> mesh_field_; - GridPointSearch2D search_; - Kokkos::View dof_holder_data_; -}; - -/* - * MeshFieldsAdapter2 Implementation - */ -template -inline MeshFieldsAdapter2::MeshFieldsAdapter2( - const MeshFieldsAdapterLayout& layout) - : layout_(layout), - mesh_(layout.GetMesh()), - search_(mesh_, 10, 10), - dof_holder_data_("dof_holder_data", static_cast(layout.OwnedSize())) -{ - if (mesh_.dim() == 3) { - throw pcms_error("MeshFieldsAdapter2 does not support 3D meshes"); - } - auto nodes_per_dim = layout.GetNodesPerDim(); - if (nodes_per_dim[2] == 0 && nodes_per_dim[3] == 0) { - if (nodes_per_dim[0] == 1 && nodes_per_dim[1] == 0) { - switch (mesh_.dim()) { - case 1: - mesh_field_ = std::make_unique>(mesh_); - break; - case 2: - mesh_field_ = std::make_unique>(mesh_); - break; - default: break; // backend is null - } - } else if (nodes_per_dim[0] == 1 && nodes_per_dim[1] == 1) { - switch (mesh_.dim()) { - case 2: - mesh_field_ = std::make_unique>(mesh_); - break; - case 3: - mesh_field_ = std::make_unique>(mesh_); - break; - default: break; // backend is null - } - } - } -} - -template -inline Rank1View -MeshFieldsAdapter2::GetDOFHolderData() const -{ - PCMS_FUNCTION_TIMER; - auto nodes_per_dim = layout_.GetNodesPerDim(); - auto num_components = layout_.GetNumComponents(); - size_t offset = 0; - for (int i = 0; i <= mesh_.dim(); ++i) { - if (nodes_per_dim[i]) { - size_t len = static_cast(mesh_.nents(i)) * - static_cast(nodes_per_dim[i]) * - static_cast(num_components); - Rank1View subspan{ - std::data(dof_holder_data_) + offset, len}; - mesh_field_->GetData(subspan, nodes_per_dim[i], num_components, i); - offset += len; - } - } - - return make_const_array_view(dof_holder_data_); -} - -template -inline void MeshFieldsAdapter2::SetDOFHolderData( - Rank1View data) -{ - PCMS_FUNCTION_TIMER; - - auto nodes_per_dim = layout_.GetNodesPerDim(); - auto num_components = layout_.GetNumComponents(); - PCMS_ALWAYS_ASSERT(static_cast(data.size()) == - layout_.GetNumOwnedDofHolder() * num_components); - size_t offset = 0; - for (int i = 0; i <= mesh_.dim(); ++i) { - if (nodes_per_dim[i]) { - size_t len = static_cast(mesh_.nents(i)) * - static_cast(nodes_per_dim[i]) * - static_cast(num_components); - Rank1View subspan{data.data_handle() + offset, - len}; - mesh_field_->SetData(subspan, nodes_per_dim[i], num_components, i); - offset += len; - } - } -} - -template -inline LocalizationHint MeshFieldsAdapter2::GetLocalizationHint( - CoordinateView coordinate_view) const -{ - PCMS_FUNCTION_TIMER; - // TODO decide if we want to implicitly perform the coordinate transformations - // when possible - if (coordinate_view.GetCoordinateSystem() != - layout_.GetDOFHolderCoordinates().GetCoordinateSystem()) { - throw pcms_error("Coordinate system mismatch"); - } - - auto coordinates = coordinate_view.GetCoordinates(); - Kokkos::View coords("coords", coordinates.size() / 2); - auto coordinates_host = Kokkos::View( - coordinates.data_handle(), coordinates.extent(0), coordinates.extent(1)); - deep_copy_mismatch_layouts(coords, coordinates_host); - auto results = search_(coords); - Kokkos::View results_h( - "results_h", results.size()); - Kokkos::deep_copy(results_h, results); - auto hint = std::make_shared( - mesh_, results_h, this->out_of_bounds_mode_); - - return LocalizationHint{hint}; -} - -template -inline void MeshFieldsAdapter2::Evaluate( - LocalizationHint location, FieldDataView results) const -{ - PCMS_FUNCTION_TIMER; - // TODO decide if we want to implicitly perform the coordinate transformations - // when possible - if (results.GetCoordinateSystem() != - layout_.GetDOFHolderCoordinates().GetCoordinateSystem()) { - throw pcms_error("Coordinate system mismatch"); - } - - MeshFieldsAdapter2LocalizationHint hint = - *reinterpret_cast(location.data.get()); - - Kokkos::View coordinates_d( - "coordinates_d", hint.coordinates_.extent(0), hint.coordinates_.extent(1)); - deep_copy_mismatch_layouts(coordinates_d, hint.coordinates_); - Kokkos::View offsets_d("offsets_d", hint.offsets_.extent(0)); - Kokkos::deep_copy(offsets_d, hint.offsets_); - auto eval_results = mesh_field_->evaluate(coordinates_d, offsets_d); - Kokkos::View eval_results_h( - "eval_results_h", eval_results.extent(0), eval_results.extent(1)); - deep_copy_mismatch_layouts(eval_results_h, eval_results); - Rank1View values = results.GetValues(); - - // Copy results for valid points - Kokkos::parallel_for( - "CopyEvalResultsToValues", - Kokkos::RangePolicy( - 0, eval_results_h.extent(0)), - KOKKOS_LAMBDA(LO i) { values[hint.indices_(i)] = eval_results_h(i, 0); }); - - // Handle missing points based on mode - if (hint.num_missing_ > 0 && hint.mode_ == OutOfBoundsMode::FILL) { - auto fill_val = this->fill_value_; - Kokkos::parallel_for( - "FillMissingValues", - Kokkos::RangePolicy(0, - hint.num_missing_), - KOKKOS_LAMBDA(LO i) { values[hint.missing_indices_(i)] = fill_val; }); - } -} - -template -inline void MeshFieldsAdapter2::EvaluateGradient( - FieldDataView /* unused */) -{ - throw pcms_error("EvaluateGradient not implemented for MeshFieldsAdapter2"); -} - -template -inline const FieldLayout& MeshFieldsAdapter2::GetLayout() const -{ - return layout_; -} - -template -inline bool MeshFieldsAdapter2::CanEvaluateGradient() -{ - // TODO compute the gradient field using element shape functions - return false; -} - -template -inline int MeshFieldsAdapter2::Serialize( - Rank1View buffer, - Rank1View permutation) const -{ - PCMS_FUNCTION_TIMER; - // host copy of filtered field data array - const auto array_h = GetDOFHolderData(); - if (buffer.size() > 0) { - auto owned = layout_.GetOwned(); - for (size_t i = 0; i < array_h.size(); i++) { - if (owned[i]) - buffer[permutation[i]] = array_h[i]; - } - } - return array_h.size(); -} - -template -inline void MeshFieldsAdapter2::Deserialize( - Rank1View buffer, - Rank1View permutation) -{ - PCMS_FUNCTION_TIMER; - Omega_h::HostWrite sorted_buffer(permutation.size()); - auto owned = layout_.GetOwned(); - for (LO i = 0; i < sorted_buffer.size(); ++i) { - if (owned[i]) - sorted_buffer[i] = buffer[permutation[i]]; - } - - SetDOFHolderData(pcms::make_const_array_view(sorted_buffer)); -} - -} // namespace pcms - -#endif // PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_ADAPTER2_H diff --git a/src/pcms/adapter/point_cloud/point_cloud.cpp b/src/pcms/adapter/point_cloud/point_cloud.cpp deleted file mode 100644 index c0de91029..000000000 --- a/src/pcms/adapter/point_cloud/point_cloud.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include "point_cloud.h" -#include "pcms/utility/profile.h" -#include "pcms/utility/assert.h" - -namespace pcms -{ - -struct CopyCoordinatesFunctor -{ - Kokkos::View coordinates_; - Rank2View coords_; - - CopyCoordinatesFunctor(Kokkos::View coordinates, - Rank2View coords) - : coordinates_(coordinates), coords_(coords) - { - } - - KOKKOS_INLINE_FUNCTION - void operator()(int i) const - { - for (int j = 0; j < coords_.extent(1); ++j) { - coordinates_(i, j) = coords_(i, j); - } - } -}; - -struct PointCloudLocalizationHint -{ - PointCloudLocalizationHint(CoordinateView coordinate_view) - : coordinates_("", coordinate_view.GetCoordinates().extent(0), - coordinate_view.GetCoordinates().extent(1)) - { - auto coords = coordinate_view.GetCoordinates(); - int n = coords.extent(0); - Kokkos::parallel_for(n, CopyCoordinatesFunctor(coordinates_, coords)); - } - - Kokkos::View coordinates_; -}; - -PointCloud::PointCloud(const PointCloudLayout& layout) - : layout_(layout), - data_("", layout_.GetDOFHolderCoordinates().GetCoordinates().extent(0)), - data_host_("", layout_.GetDOFHolderCoordinates().GetCoordinates().extent(0)) -{ -} - -Rank1View PointCloud::GetDOFHolderData() const -{ - Kokkos::deep_copy(data_host_, data_); - return make_const_array_view(data_host_); -} - -void PointCloud::SetDOFHolderData(Rank1View data) -{ - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(data.size() == data_.size()); - Kokkos::parallel_for( - Kokkos::RangePolicy(0, data.size()), - KOKKOS_CLASS_LAMBDA(int i) { data_host_(i) = data[i]; }); - Kokkos::deep_copy(data_, data_host_); -} - -LocalizationHint PointCloud::GetLocalizationHint( - CoordinateView coordinate_view) const -{ - auto hint = std::make_shared(coordinate_view); - return LocalizationHint{hint}; -} - -void PointCloud::Evaluate( - LocalizationHint /* unused */, - FieldDataView /* unused */) const -{ - throw std::runtime_error("Not implemented"); -} - -void PointCloud::EvaluateGradient( - FieldDataView /* unused */) -{ - throw std::runtime_error("Not implemented"); -} - -const FieldLayout& PointCloud::GetLayout() const -{ - return layout_; -} - -bool PointCloud::CanEvaluateGradient() -{ - return false; -} - -int PointCloud::Serialize( - Rank1View buffer, - Rank1View permutation) const -{ - PCMS_FUNCTION_TIMER; - if (buffer.size() > 0) { - Kokkos::parallel_for( - data_.size(), - KOKKOS_CLASS_LAMBDA(int i) { buffer[permutation[i]] = data_(i); }); - } - return data_.size(); -} - -void PointCloud::Deserialize( - Rank1View buffer, - Rank1View permutation) -{ - PCMS_FUNCTION_TIMER; - Kokkos::parallel_for( - data_.size(), - KOKKOS_CLASS_LAMBDA(int i) { data_(i) = buffer[permutation[i]]; }); -} - -} // namespace pcms diff --git a/src/pcms/adapter/point_cloud/point_cloud.h b/src/pcms/adapter/point_cloud/point_cloud.h deleted file mode 100644 index f19298c43..000000000 --- a/src/pcms/adapter/point_cloud/point_cloud.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef POINT_CLOUD_H_ -#define POINT_CLOUD_H_ - -#include "pcms/field.h" -#include "pcms/utility/arrays.h" -#include "point_cloud_layout.h" - -namespace pcms -{ -class PointCloud : public FieldT -{ -public: - PointCloud(const PointCloudLayout& layout); - - LocalizationHint GetLocalizationHint( - CoordinateView coordinate_view) const override; - - void Evaluate(LocalizationHint location, - FieldDataView results) const override; - - void EvaluateGradient(FieldDataView results) override; - - const FieldLayout& GetLayout() const override; - - bool CanEvaluateGradient() override; - - int Serialize(Rank1View buffer, - Rank1View permutation) - const override; - - void Deserialize( - Rank1View buffer, - Rank1View permutation) override; - - Rank1View GetDOFHolderData() const override; - void SetDOFHolderData(Rank1View data) override; - -private: - const PointCloudLayout& layout_; - Kokkos::View data_; - Kokkos::View data_host_; -}; -} // namespace pcms - -#endif // POINT_CLOUD_H_ diff --git a/src/pcms/adapter/point_cloud/point_cloud_layout.cpp b/src/pcms/adapter/point_cloud/point_cloud_layout.cpp deleted file mode 100644 index 93c7754c1..000000000 --- a/src/pcms/adapter/point_cloud/point_cloud_layout.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#include "point_cloud_layout.h" -#include "point_cloud.h" -#include -#include - -namespace pcms -{ - -PointCloudLayout::PointCloudLayout(int dim, Kokkos::View coords, - CoordinateSystem coordinate_system) - : dim_(dim), - coordinate_system_(coordinate_system), - coords_(coords), - owned_("", coords.extent(0)), - gids_("", coords.extent(0)), - owned_host_("", coords.extent(0)), - gids_host_("", coords.extent(0)) -{ - components_ = 1; - - namespace KE = Kokkos::Experimental; - KE::fill(Kokkos::DefaultExecutionSpace(), owned_, true); - iota_view(gids_); -} - -std::unique_ptr> PointCloudLayout::CreateFieldReal() const -{ - return std::make_unique(*this); -} - -int PointCloudLayout::GetNumComponents() const -{ - return components_; -} - -LO PointCloudLayout::GetNumOwnedDofHolder() const -{ - return coords_.extent(0); -} - -GO PointCloudLayout::GetNumGlobalDofHolder() const -{ - return coords_.extent(0); -} - -Rank1View PointCloudLayout::GetOwned() const -{ - Kokkos::deep_copy(owned_host_, owned_); - return make_const_array_view(owned_host_); -} - -GlobalIDView PointCloudLayout::GetGids() const -{ - Kokkos::deep_copy(gids_host_, gids_); - return GlobalIDView(gids_host_.data(), gids_host_.size()); -} - -CoordinateView PointCloudLayout::GetDOFHolderCoordinates() - const -{ - Rank2View coords_view(coords_.data(), - coords_.extent(0), 2); - return CoordinateView{coordinate_system_, coords_view}; -} - -bool PointCloudLayout::IsDistributed() -{ - return false; -} - -size_t PointCloudLayout::GetNumEnts() const -{ - return coords_.extent(0); -} - -EntOffsetsArray PointCloudLayout::GetEntOffsets() const -{ - EntOffsetsArray offsets{}; - for (size_t i = 0; i < offsets.size(); ++i) - offsets[i] = coords_.extent(0); - offsets[0] = 0; - return offsets; -} - -std::array PointCloudLayout::GetNodesPerDim() const -{ - std::array nodes{}; - for (size_t i = 0; i < nodes.size(); ++i) - nodes[i] = 0; - nodes[0] = 1; - return nodes; -} - -ReversePartitionMap2 PointCloudLayout::GetReversePartitionMap( - const redev::Partition& /* unused */) const -{ - throw std::runtime_error("Unimplemented"); -} -} // namespace pcms diff --git a/src/pcms/adapter/point_cloud/point_cloud_layout.h b/src/pcms/adapter/point_cloud/point_cloud_layout.h deleted file mode 100644 index 13897e204..000000000 --- a/src/pcms/adapter/point_cloud/point_cloud_layout.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef POINT_CLOUD_LAYOUT_H_ -#define POINT_CLOUD_LAYOUT_H_ - -#include "pcms/field.h" - -namespace pcms -{ - -class PointCloudLayout : public FieldLayout -{ -public: - PointCloudLayout(int dim, Kokkos::View coords, - CoordinateSystem coordinate_system); - - std::unique_ptr> CreateFieldReal() const override; - - int GetNumComponents() const override; - // nodes for standard lagrange FEM - LO GetNumOwnedDofHolder() const override; - GO GetNumGlobalDofHolder() const override; - - Rank1View GetOwned() const override; - GlobalIDView GetGids() const override; - CoordinateView GetDOFHolderCoordinates() const override; - - bool IsDistributed() override; - size_t GetNumEnts() const; - EntOffsetsArray GetEntOffsets() const override; - - ReversePartitionMap2 GetReversePartitionMap( - const redev::Partition& partition) const override; - - std::array GetNodesPerDim() const; - -private: - int dim_; - int components_; - CoordinateSystem coordinate_system_; - Kokkos::View coords_; - Kokkos::View owned_; - Kokkos::View gids_; - Kokkos::View owned_host_; - Kokkos::View gids_host_; -}; -} // namespace pcms - -#endif // POINT_CLOUD_LAYOUT_H_ diff --git a/src/pcms/adapter/uniform_grid/uniform_grid_field.cpp b/src/pcms/adapter/uniform_grid/uniform_grid_field.cpp deleted file mode 100644 index b5468a5b3..000000000 --- a/src/pcms/adapter/uniform_grid/uniform_grid_field.cpp +++ /dev/null @@ -1,304 +0,0 @@ -#include "uniform_grid_field.h" -#include "pcms/utility/profile.h" -#include "pcms/interpolator/linear_interpolant.hpp" -#include "pcms/interpolator/multidimarray.hpp" -#include "pcms/utility/assert.h" -#include - -namespace pcms -{ - -// Localization hint structure for UniformGrid -template -struct UniformGridFieldLocalizationHint -{ - UniformGridFieldLocalizationHint( - Kokkos::View cell_indices, - Kokkos::View coordinates, OutOfBoundsMode mode, - Kokkos::View is_out_of_bounds, - size_t num_out_of_bounds) - : cell_indices_(cell_indices), - coordinates_(coordinates), - mode_(mode), - is_out_of_bounds_(is_out_of_bounds), - num_out_of_bounds_(num_out_of_bounds) - { - } - - // Cell indices for each point as 1d array - Kokkos::View cell_indices_; - // Coordinates of each point - Kokkos::View coordinates_; - // Out of bounds handling - OutOfBoundsMode mode_; - Kokkos::View is_out_of_bounds_; - size_t num_out_of_bounds_; -}; - -template -UniformGridField::UniformGridField( - const UniformGridFieldLayout& layout) - : layout_(layout), - grid_(layout.GetGrid()), - dof_holder_data_("dof_holder_data", static_cast(layout.OwnedSize())) -{ - PCMS_FUNCTION_TIMER; - // Default to NEAREST_BOUNDARY for uniform grid fields - out_of_bounds_mode_ = OutOfBoundsMode::NEAREST_BOUNDARY; -} - -template -Rank1View UniformGridField::GetDOFHolderData() - const -{ - PCMS_FUNCTION_TIMER; - return make_const_array_view(dof_holder_data_); -} - -template -void UniformGridField::SetDOFHolderData( - Rank1View data) -{ - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(data.size() == dof_holder_data_.size()); - - for (size_t i = 0; i < data.size(); ++i) { - dof_holder_data_[i] = data[i]; - } -} - -template -View UniformGridField::to_mdspan() -{ - PCMS_FUNCTION_TIMER; - - if constexpr (Dim == 2) { - return View( - dof_holder_data_.data(), grid_.divisions[0] + 1, grid_.divisions[1] + 1); - } else if constexpr (Dim == 3) { - return View( - dof_holder_data_.data(), grid_.divisions[0] + 1, grid_.divisions[1] + 1, - grid_.divisions[2] + 1); - } else { - static_assert(Dim == 2 || Dim == 3, - "to_mdspan only supports 2D or 3D uniform grids"); - } -} - -template -View UniformGridField::to_mdspan() const -{ - PCMS_FUNCTION_TIMER; - - if constexpr (Dim == 2) { - return View( - dof_holder_data_.data(), grid_.divisions[0] + 1, grid_.divisions[1] + 1); - } else if constexpr (Dim == 3) { - return View( - dof_holder_data_.data(), grid_.divisions[0] + 1, grid_.divisions[1] + 1, - grid_.divisions[2] + 1); - } else { - static_assert(Dim == 2 || Dim == 3, - "to_mdspan only supports 2D or 3D uniform grids"); - } -} - -template -LocalizationHint UniformGridField::GetLocalizationHint( - CoordinateView coordinate_view) const -{ - PCMS_FUNCTION_TIMER; - - if (coordinate_view.GetCoordinateSystem() != - layout_.GetDOFHolderCoordinates().GetCoordinateSystem()) { - throw std::runtime_error( - "Coordinate system mismatch in GetLocalizationHint"); - } - - auto coordinates = coordinate_view.GetCoordinates(); - LO num_points = coordinates.extent(0); - - Kokkos::View cell_indices("cell_indices", num_points); - Kokkos::View coords_copy("coords_copy", num_points, - Dim); - Kokkos::View is_out_of_bounds("is_out_of_bounds", - num_points); - size_t num_out_of_bounds = 0; - - // Find which cell each point belongs to and detect out-of-bounds - for (LO i = 0; i < num_points; ++i) { - Omega_h::Vector point; - for (unsigned d = 0; d < Dim; ++d) { - point[d] = coordinates(i, d); - coords_copy(i, d) = coordinates(i, d); - } - - // Check if point is within grid bounds - bool out_of_bounds = !grid_.IsPointInBounds(point); - - is_out_of_bounds[i] = out_of_bounds; - if (out_of_bounds) { - num_out_of_bounds++; - if (out_of_bounds_mode_ == OutOfBoundsMode::ERROR) { - PCMS_ALWAYS_ASSERT(false && "Point found outside uniform grid domain"); - } - } - - cell_indices[i] = grid_.ClosestCellID(point); - } - - auto hint = std::make_shared>( - cell_indices, coords_copy, out_of_bounds_mode_, is_out_of_bounds, - num_out_of_bounds); - return LocalizationHint{hint}; -} - -template -void UniformGridField::Evaluate( - LocalizationHint location, FieldDataView results) const -{ - PCMS_FUNCTION_TIMER; - - if (results.GetCoordinateSystem() != - layout_.GetDOFHolderCoordinates().GetCoordinateSystem()) { - throw std::runtime_error("Coordinate system mismatch in Evaluate"); - } - - UniformGridFieldLocalizationHint hint = - *reinterpret_cast*>( - location.data.get()); - - auto values = dof_holder_data_; - auto coordinates = hint.coordinates_; - auto cell_indices = hint.cell_indices_; - LO num_points = coordinates.extent(0); - - Kokkos::View cell_dimensioned_indices( - "cell_dimensioned_indices", num_points, Dim); - // Convert cell indices to multi-dimensional indices - for (LO i = 0; i < num_points; ++i) { - auto dim_indices = grid_.GetDimensionedIndex(cell_indices[i]); - for (unsigned d = 0; d < Dim; ++d) { - cell_dimensioned_indices(i, d) = dim_indices[d]; - } - } - - // Dimensions for vertex grid (m+1 vertices per dimension for m cells) - auto cell_divisions = layout_.GetGrid().divisions; - IntVecView dimensions_view("dimensions", Dim); - auto dimensions_view_host = Kokkos::create_mirror_view(dimensions_view); - for (unsigned d = 0; d < Dim; ++d) { - dimensions_view_host(d) = cell_divisions[d] + 1; - } - Kokkos::deep_copy(dimensions_view, dimensions_view_host); - - // Compute parametric coordinates for each point - RealMatView parametric_coords("parametric_coords", num_points, Dim); - auto parametric_coords_host = Kokkos::create_mirror_view(parametric_coords); - - for (LO i = 0; i < num_points; ++i) { - auto cell_bbox = grid_.GetCellBBOX(cell_indices[i]); - for (unsigned d = 0; d < Dim; ++d) { - Real coord = coordinates(i, d); - Real cell_min = cell_bbox.center[d] - cell_bbox.half_width[d]; - Real cell_max = cell_bbox.center[d] + cell_bbox.half_width[d]; - parametric_coords_host(i, d) = (coord - cell_min) / (cell_max - cell_min); - } - } - - Kokkos::deep_copy(parametric_coords, parametric_coords_host); - - // Convert cell indices and values to the required view types - IntMatView cell_indices_interp("cell_indices_interp", num_points, Dim); - auto cell_indices_interp_host = - Kokkos::create_mirror_view(cell_indices_interp); - for (LO i = 0; i < num_points; ++i) { - for (unsigned d = 0; d < Dim; ++d) { - cell_indices_interp_host(i, d) = cell_dimensioned_indices(i, d); - } - } - Kokkos::deep_copy(cell_indices_interp, cell_indices_interp_host); - - RealVecView values_interp("values_interp", values.extent(0)); - Kokkos::deep_copy(values_interp, values); - - auto interpolator = RegularGridInterpolator( - parametric_coords, values_interp, cell_indices_interp, dimensions_view); - auto evaluated_values = results.GetValues(); - auto interpolated_values = interpolator.linear_interpolation(); - auto interpolated_values_host = - Kokkos::create_mirror_view(interpolated_values); - Kokkos::deep_copy(interpolated_values_host, interpolated_values); - - // Copy interpolated values and handle out-of-bounds points - for (LO i = 0; i < evaluated_values.size(); ++i) { - if (hint.is_out_of_bounds_[i] && hint.mode_ == OutOfBoundsMode::FILL) { - // Fill out-of-bounds points with fill value - evaluated_values[i] = fill_value_; - } else { - // Use interpolated value (for in-bounds or NEAREST_BOUNDARY mode) - evaluated_values[i] = interpolated_values_host[i]; - } - } -} - -template -void UniformGridField::EvaluateGradient( - FieldDataView) -{ - throw std::runtime_error("Not implemented"); -} - -template -const FieldLayout& UniformGridField::GetLayout() const -{ - return layout_; -} - -template -bool UniformGridField::CanEvaluateGradient() -{ - return false; -} - -template -int UniformGridField::Serialize( - Rank1View buffer, - Rank1View permutation) const -{ - PCMS_FUNCTION_TIMER; - - const auto array_h = GetDOFHolderData(); - if (buffer.size() > 0) { - PCMS_ALWAYS_ASSERT(buffer.size() == array_h.size()); - for (LO i = 0; i < array_h.size(); ++i) { - buffer[permutation[i]] = array_h[i]; - } - } - return array_h.size(); -} - -template -void UniformGridField::Deserialize( - Rank1View buffer, - Rank1View permutation) -{ - PCMS_FUNCTION_TIMER; - - Kokkos::View sorted_buffer("sorted_buffer", - permutation.size()); - auto owned = layout_.GetOwned(); - - for (LO i = 0; i < sorted_buffer.size(); ++i) { - PCMS_ALWAYS_ASSERT(owned[i]); - sorted_buffer[i] = buffer[permutation[i]]; - } - - SetDOFHolderData(pcms::make_const_array_view(sorted_buffer)); -} - -// Explicit template instantiations -template class UniformGridField<2>; -template class UniformGridField<3>; - -} // namespace pcms diff --git a/src/pcms/adapter/uniform_grid/uniform_grid_field.h b/src/pcms/adapter/uniform_grid/uniform_grid_field.h deleted file mode 100644 index b72b29dee..000000000 --- a/src/pcms/adapter/uniform_grid/uniform_grid_field.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef PCMS_UNIFORM_GRID_FIELD_H -#define PCMS_UNIFORM_GRID_FIELD_H - -#include "pcms/adapter/uniform_grid/uniform_grid_field_layout.h" -#include "pcms/utility/types.h" -#include "pcms/field.h" -#include "pcms/coordinate_system.h" -#include "pcms/uniform_grid.h" -#include - -namespace pcms -{ -template -class UniformGridField : public FieldT -{ -public: - UniformGridField(const UniformGridFieldLayout& layout); - - LocalizationHint GetLocalizationHint( - CoordinateView coordinate_view) const override; - - void Evaluate(LocalizationHint location, - FieldDataView results) const override; - - void EvaluateGradient(FieldDataView results) override; - - const FieldLayout& GetLayout() const override; - - bool CanEvaluateGradient() override; - - int Serialize(Rank1View buffer, - Rank1View permutation) - const override; - - void Deserialize( - Rank1View buffer, - Rank1View permutation) override; - - Rank1View GetDOFHolderData() const override; - void SetDOFHolderData(Rank1View data) override; - - View to_mdspan(); - View to_mdspan() const; - - ~UniformGridField() noexcept = default; - -private: - const UniformGridFieldLayout& layout_; - UniformGrid& grid_; - Kokkos::View dof_holder_data_; -}; - -using UniformGridField2D = UniformGridField<2>; - -} // namespace pcms - -#endif // PCMS_UNIFORM_GRID_FIELD_H diff --git a/src/pcms/adapter/uniform_grid/uniform_grid_field_layout.cpp b/src/pcms/adapter/uniform_grid/uniform_grid_field_layout.cpp deleted file mode 100644 index 93b26e18f..000000000 --- a/src/pcms/adapter/uniform_grid/uniform_grid_field_layout.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include "uniform_grid_field_layout.h" -#include "uniform_grid_field.h" -#include "pcms/utility/profile.h" -#include - -namespace pcms -{ - -template -UniformGridFieldLayout::UniformGridFieldLayout( - UniformGrid& grid, int num_components, - CoordinateSystem coordinate_system) - : grid_(grid), - num_components_(num_components), - coordinate_system_(coordinate_system), - gids_("gids", GetNumVertices()), - dof_holder_coords_("dof_holder_coords", GetNumVertices(), Dim), - owned_("owned", GetNumVertices()) -{ - PCMS_FUNCTION_TIMER; - - LO num_vertices = GetNumVertices(); - - // Initialize global IDs and ownership - for (LO i = 0; i < num_vertices; ++i) { - gids_[i] = static_cast(i); - owned_[i] = true; - } - - // Initialize DOF holder coordinates at grid vertices - Real vertex_spacing[Dim]; - for (unsigned d = 0; d < Dim; ++d) { - vertex_spacing[d] = grid_.edge_length[d] / grid_.divisions[d]; - } - - if constexpr (Dim == 2) { - LO vertex_idx = 0; - for (LO j = 0; j <= grid_.divisions[1]; ++j) { - for (LO i = 0; i <= grid_.divisions[0]; ++i) { - dof_holder_coords_(vertex_idx, 0) = - grid_.bot_left[0] + i * vertex_spacing[0]; - dof_holder_coords_(vertex_idx, 1) = - grid_.bot_left[1] + j * vertex_spacing[1]; - ++vertex_idx; - } - } - } else if constexpr (Dim == 3) { - LO vertex_idx = 0; - for (LO k = 0; k <= grid_.divisions[2]; ++k) { - for (LO j = 0; j <= grid_.divisions[1]; ++j) { - for (LO i = 0; i <= grid_.divisions[0]; ++i) { - dof_holder_coords_(vertex_idx, 0) = - grid_.bot_left[0] + i * vertex_spacing[0]; - dof_holder_coords_(vertex_idx, 1) = - grid_.bot_left[1] + j * vertex_spacing[1]; - dof_holder_coords_(vertex_idx, 2) = - grid_.bot_left[2] + k * vertex_spacing[2]; - ++vertex_idx; - } - } - } - } -} - -template -std::unique_ptr> UniformGridFieldLayout::CreateFieldReal() - const -{ - return std::make_unique>(*this); -} - -template -int UniformGridFieldLayout::GetNumComponents() const -{ - return num_components_; -} - -template -LO UniformGridFieldLayout::GetNumOwnedDofHolder() const -{ - return GetNumVertices(); -} - -template -GO UniformGridFieldLayout::GetNumGlobalDofHolder() const -{ - return GetNumVertices(); -} - -template -Rank1View UniformGridFieldLayout::GetOwned() - const -{ - return make_const_array_view(owned_); -} - -template -GlobalIDView UniformGridFieldLayout::GetGids() const -{ - return GlobalIDView(gids_.data(), gids_.size()); -} - -template -CoordinateView -UniformGridFieldLayout::GetDOFHolderCoordinates() const -{ - Rank2View coords_view( - dof_holder_coords_.data(), dof_holder_coords_.extent(0), Dim); - return CoordinateView{coordinate_system_, coords_view}; -} - -template -bool UniformGridFieldLayout::IsDistributed() -{ - return false; -} - -template -UniformGrid& UniformGridFieldLayout::GetGrid() const -{ - return grid_; -} - -template -LO UniformGridFieldLayout::GetNumCells() const -{ - return grid_.GetNumCells(); -} - -template -LO UniformGridFieldLayout::GetNumVertices() const -{ - LO num_vertices = 1; - for (unsigned d = 0; d < Dim; ++d) { - num_vertices *= (grid_.divisions[d] + 1); - } - return num_vertices; -} - -template -EntOffsetsArray UniformGridFieldLayout::GetEntOffsets() const -{ - EntOffsetsArray offsets{}; - offsets[0] = 0; - offsets[1] = grid_.GetNumCells(); - offsets[2] = grid_.GetNumCells(); - offsets[3] = grid_.GetNumCells(); - offsets[4] = grid_.GetNumCells(); - return offsets; -} - -template -ReversePartitionMap2 UniformGridFieldLayout::GetReversePartitionMap( - const redev::Partition& partition) const -{ - throw std::runtime_error("Unimplemented"); -} - -// Explicit template instantiations -template class UniformGridFieldLayout<2>; -template class UniformGridFieldLayout<3>; - -} // namespace pcms diff --git a/src/pcms/adapter/uniform_grid/uniform_grid_field_layout.h b/src/pcms/adapter/uniform_grid/uniform_grid_field_layout.h deleted file mode 100644 index 3e95b81fb..000000000 --- a/src/pcms/adapter/uniform_grid/uniform_grid_field_layout.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef PCMS_UNIFORM_GRID_FIELD_LAYOUT_H -#define PCMS_UNIFORM_GRID_FIELD_LAYOUT_H - -#include "pcms/utility/arrays.h" -#include "pcms/field_layout.h" -#include "pcms/coordinate_system.h" -#include "pcms/field.h" -#include "pcms/uniform_grid.h" - -#include - -namespace pcms -{ -template -class UniformGridFieldLayout : public FieldLayout -{ -public: - UniformGridFieldLayout(UniformGrid& grid, int num_components, - CoordinateSystem coordinate_system); - - std::unique_ptr> CreateFieldReal() const override; - - int GetNumComponents() const override; - LO GetNumOwnedDofHolder() const override; - GO GetNumGlobalDofHolder() const override; - - Rank1View GetOwned() const override; - GlobalIDView GetGids() const override; - CoordinateView GetDOFHolderCoordinates() const override; - - bool IsDistributed() override; - - EntOffsetsArray GetEntOffsets() const override; - - ReversePartitionMap2 GetReversePartitionMap( - const redev::Partition& partition) const override; - - UniformGrid& GetGrid() const; - LO GetNumCells() const; - LO GetNumVertices() const; - -private: - UniformGrid& grid_; - int num_components_; - CoordinateSystem coordinate_system_; - Kokkos::View gids_; - Kokkos::View dof_holder_coords_; - Kokkos::View owned_; -}; - -using UniformGridFieldLayout2D = UniformGridFieldLayout<2>; - -} // namespace pcms -#endif // PCMS_UNIFORM_GRID_FIELD_LAYOUT_H diff --git a/src/pcms/adapter/xgc/xgc_field_adapter.h b/src/pcms/adapter/xgc/xgc_field_adapter.h deleted file mode 100644 index 96921d5f4..000000000 --- a/src/pcms/adapter/xgc/xgc_field_adapter.h +++ /dev/null @@ -1,238 +0,0 @@ -#ifndef PCMS_COUPLING_XGC_FIELD_ADAPTER_H -#define PCMS_COUPLING_XGC_FIELD_ADAPTER_H -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include "pcms/utility/types.h" -#include "pcms/utility/memory_spaces.h" -#include "pcms/field.h" -#include -#include -#include "xgc_reverse_classification.h" -#include "pcms/utility/assert.h" -#include "pcms/utility/array_mask.h" -#include "pcms/utility/profile.h" - -namespace pcms -{ -template -class XGCFieldAdapter -{ -public: - using memory_space = HostMemorySpace; - using value_type = T; - using coordinate_element_type = CoordinateElementType; - /** - * - * @param name name of the field - * @param plane_communicator the communicator of all ranks corresponding to a - * given XGC plane. This corresponds to sml_plane_comm - * @param data a view of the data to be used as the field definition - * @param reverse_classification the reverse classification data for the XGC - * field - * @param in_overlap a function describing if an entity defined by the - * geometric dimension and ID - */ - XGCFieldAdapter(std::string name, MPI_Comm plane_communicator, - Rank1View data, - const ReverseClassificationVertex& reverse_classification, - std::function in_overlap) - : name_(std::move(name)), - plane_comm_(plane_communicator), - data_(data), - gids_(data.size()), - reverse_classification_(reverse_classification), - in_overlap_(in_overlap) - { - PCMS_FUNCTION_TIMER; - // PCMS_ALWAYS_ASSERT(reverse_classification.nverts() == data.size()); - MPI_Comm_rank(plane_comm_, &plane_rank_); - if (RankParticipatesCouplingCommunication()) { - Kokkos::View mask("mask", data.size()); - PCMS_ALWAYS_ASSERT((bool)in_overlap); - for (auto& geom : reverse_classification_) { - if (in_overlap(geom.first.dim, geom.first.id)) { - for (auto vert : geom.second) { - PCMS_ALWAYS_ASSERT(vert < data.size()); - mask(vert) = 1; - } - } - } - mask_ = ArrayMask{make_const_array_view(mask)}; - PCMS_ALWAYS_ASSERT(!mask_.empty()); - //// XGC meshes are naively ordered in iteration order (full mesh on every - //// cpu) First ID in XGC is 1! - std::iota(gids_.begin(), gids_.end(), static_cast(1)); - } - } - - int Serialize(Rank1View buffer, - Rank1View permutation) const - { - PCMS_FUNCTION_TIMER; - static_assert(std::is_same_v, - "gpu space unhandled\n"); - if (RankParticipatesCouplingCommunication()) { - auto const_data = - Rank1View{data_.data_handle(), data_.size()}; - if (buffer.size() > 0) { - mask_.Apply(const_data, buffer, permutation); - } - return mask_.Size(); - } - return 0; - } - void Deserialize(Rank1View buffer, - Rank1View permutation) const - { - PCMS_FUNCTION_TIMER; - static_assert(std::is_same_v, - "gpu space unhandled\n"); - if (RankParticipatesCouplingCommunication()) { - mask_.ToFullArray(buffer, data_, permutation); - } - // duplicate the data on the root rank of the plane to all other ranks - MPI_Bcast(data_.data_handle(), data_.size(), - redev::getMpiType(value_type{}), plane_root_, plane_comm_); - } - - // REQUIRED - [[nodiscard]] std::vector GetGids() const - { - PCMS_FUNCTION_TIMER; - if (RankParticipatesCouplingCommunication()) { - std::vector gids(mask_.Size()); - auto v1 = make_array_view(gids_); - auto v2 = make_array_view(gids); - mask_.Apply(v1, v2); - return gids; - } - return {}; - } - - // REQUIRED - [[nodiscard]] ReversePartitionMap GetReversePartitionMap( - const Partition& partition) const - { - PCMS_FUNCTION_TIMER; - if (RankParticipatesCouplingCommunication()) { - - pcms::ReversePartitionMap reverse_partition; - // in_overlap_ must contain a function! - PCMS_ALWAYS_ASSERT(static_cast(in_overlap_)); - for (const auto& geom : reverse_classification_) { - // if the geometry is in specified overlap region - if (in_overlap_(geom.first.dim, geom.first.id)) { - - auto dr = partition.GetDr(geom.first.id, geom.first.dim); - auto [it, inserted] = reverse_partition.try_emplace(dr); - // the map gives the local iteration order of the global ids - auto map = mask_.GetMap(); - std::transform(geom.second.begin(), geom.second.end(), - std::back_inserter(it->second), [&map](auto v) { - auto idx = map[v]; - PCMS_ALWAYS_ASSERT(idx > 0); - return idx - 1; - }); - } - } - - // Rather than convert an explicit forward classification, - // we can construct the reverse partitionbased on the geometry - // and sort the node ids after to get the iteration order correct - // in XGC the local iteration order maps directly to the global ids - for (auto& [rank, idxs] : reverse_partition) { - std::sort(idxs.begin(), idxs.end()); - } - return reverse_partition; - } - return {}; - } - [[nodiscard]] bool RankParticipatesCouplingCommunication() const noexcept - { - PCMS_FUNCTION_TIMER; - // only do adios communications on 0 rank of the XGC fields - return (plane_rank_ == plane_root_); - } - - [[nodiscard]] pcms::mesh_entity_type GetEntityType() const noexcept - { - return pcms::mesh_entity_type::VERTEX; - } - -private: - std::string name_; - MPI_Comm plane_comm_; - int plane_rank_; - Rank1View data_; - std::vector gids_; - const ReverseClassificationVertex& reverse_classification_; - std::function in_overlap_; - ArrayMask mask_; - static constexpr int plane_root_{0}; -}; - -struct ReadXGCNodeClassificationResult -{ - std::vector dimension; - std::vector geometric_id; -}; - -/** - * - * - * @param in istream input. The input should be in XGC node iteration order - * (implicit numbering). Each line of the input should have have a dimension and - * geometric id that the node is classified on. - * - * @return the classification for each node in the XGC mesh - */ -[[nodiscard]] ReadXGCNodeClassificationResult ReadXGCNodeClassification( - std::istream& in); - -template -auto get_nodal_coordinates( - const XGCFieldAdapter& field) -{ - PCMS_FUNCTION_TIMER; - Kokkos::View::memory_space> - coordinates; - return coordinates; -} -template -auto evaluate(const XGCFieldAdapter& field, - Lagrange<1> /* method */, - Rank1View coordinates) - -> Kokkos::View -{ - PCMS_FUNCTION_TIMER; - Kokkos::View values("data", coordinates.size() / 2); - std::cerr << "Evaluation of XGC Field not implemented yet!\n"; - std::abort(); - return values; -} -template -auto evaluate(const XGCFieldAdapter& field, - NearestNeighbor /* method */, - Rank1View coordinates) - -> Kokkos::View -{ - PCMS_FUNCTION_TIMER; - Kokkos::View values("data", coordinates.size() / 2); - std::cerr << "Evaluation of XGC Field not implemented yet!\n"; - std::abort(); - return values; -} - -template -auto set_nodal_data( - const XGCFieldAdapter& field, - Rank1View::memory_space> - data) -> void -{ - PCMS_FUNCTION_TIMER; -} - -} // namespace pcms - -#endif // PCMS_COUPLING_XGC_FIELD_ADAPTER_H diff --git a/src/pcms/capi/CMakeLists.txt b/src/pcms/capi/CMakeLists.txt index fc6cd2393..81819d7a5 100644 --- a/src/pcms/capi/CMakeLists.txt +++ b/src/pcms/capi/CMakeLists.txt @@ -23,32 +23,32 @@ target_sources(pcms_capi_core PUBLIC TYPE HEADERS BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. FILES ${CAPI_CORE_HEADERS}) -add_library(pcms_capi_interpolator mesh.cpp interpolator.cpp kokkos.cpp) -add_library(pcms::capi::interpolator ALIAS pcms_capi_interpolator) -target_link_libraries(pcms_capi_interpolator PUBLIC MPI::MPI_C PRIVATE pcms::interpolator) -target_include_directories(pcms_capi_interpolator +add_library(pcms_capi_transfer mesh.cpp interpolator.cpp kokkos.cpp) +add_library(pcms::capi::transfer ALIAS pcms_capi_transfer) +target_link_libraries(pcms_capi_transfer PUBLIC MPI::MPI_C PRIVATE pcms::transfer) +target_include_directories(pcms_capi_transfer PUBLIC "$" # this makes the module path pcms/capi "$") -set(CAPI_INTERPOLATOR_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/interpolator.h +set(CAPI_TRANSFER_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/interpolator.h ${CMAKE_CURRENT_SOURCE_DIR}/mesh.h) -set_target_properties(pcms_capi_interpolator - PROPERTIES OUTPUT_NAME pcmscapiinterpolator - EXPORT_NAME capi::interpolator) -target_sources(pcms_capi_interpolator PUBLIC - FILE_SET interpolator +set_target_properties(pcms_capi_transfer + PROPERTIES OUTPUT_NAME pcmscapitransfer + EXPORT_NAME capi::transfer) +target_sources(pcms_capi_transfer PUBLIC + FILE_SET transfer TYPE HEADERS BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. - FILES ${CAPI_INTERPOLATOR_HEADERS}) + FILES ${CAPI_TRANSFER_HEADERS}) # high level interface target add_library(pcms_capi INTERFACE) add_library(pcms::capi ALIAS pcms_capi) set_target_properties(pcms_capi PROPERTIES EXPORT_NAME capi) # link capi libraries to a high level interface library target_link_libraries(pcms_capi INTERFACE pcms::capi::core) -target_link_libraries(pcms_capi INTERFACE pcms::capi::interpolator) +target_link_libraries(pcms_capi INTERFACE pcms::capi::transfer) install( TARGETS pcms_capi EXPORT pcms_capi-targets @@ -79,15 +79,15 @@ install( DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) install( - TARGETS pcms_capi_interpolator - EXPORT pcms_capi_interpolator-targets + TARGETS pcms_capi_transfer + EXPORT pcms_capi_transfer-targets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} - FILE_SET interpolator DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms) + FILE_SET transfer DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms) install( - EXPORT pcms_capi_interpolator-targets + EXPORT pcms_capi_transfer-targets NAMESPACE pcms:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) diff --git a/src/pcms/capi/client.cpp b/src/pcms/capi/client.cpp index 124a995d8..4faada7bb 100644 --- a/src/pcms/capi/client.cpp +++ b/src/pcms/capi/client.cpp @@ -1,133 +1,264 @@ #include "client.h" #include "pcms.h" -#include "pcms/adapter/xgc/xgc_field_adapter.h" -#include -#include -#include -#include "pcms/adapter/xgc/xgc_reverse_classification.h" -#include "pcms/adapter/dummy_field_adapter.h" +#include "pcms/field/function_space.h" +#include "pcms/field/function_space/xgc.h" +#include "pcms/field/data/xgc.h" +#include "pcms/field/layout/xgc.h" +#include "pcms/coupler/serializer/xgc.h" +#include "pcms/discretization/discretization/xgc_reverse_classification.h" +#include "pcms/coupler/coupler.hpp" +#include "pcms/field/layout/empty.h" +#include "pcms/field/data/simple.h" #include "pcms/utility/assert.h" +#include +#include +#include +#include + namespace pcms { -// Note that we have a closed set of types that can be used in the C interface -using FieldAdapterVariant = - std::variant, - pcms::XGCFieldAdapter, pcms::XGCFieldAdapter, - pcms::XGCFieldAdapter, pcms::DummyFieldAdapter>; + +namespace detail +{ + +template +struct XGCFieldRegistration +{ + XGCFieldFactory function_space; + MPI_Comm plane_comm; + Rank1View data; + + XGCFieldRegistration(XGCFieldFactory fs, MPI_Comm comm, + Rank1View d) + : function_space(std::move(fs)), plane_comm(comm), data(d) + { + } +}; + +struct DummyFieldRegistration +{}; + +class EmptyFunctionSpace : public FunctionSpace +{ +public: + EmptyFunctionSpace() : layout_(std::make_shared()) {} + + [[nodiscard]] std::shared_ptr GetLayout() + const noexcept override + { + return layout_; + } + + [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept override + { + return CoordinateSystem::Cartesian; + } + +protected: + [[nodiscard]] FieldVariant CreateFieldImpl( + Type value_type, FieldMetadata metadata) const override + { + return apply_to_type( + value_type, [this, metadata](auto tag) -> FieldVariant { + using T = typename decltype(tag)::type; + return WrapField( + layout_, std::make_unique>(layout_, metadata)); + }); + } + + [[nodiscard]] FieldVariant CreateFieldImpl( + FieldDataVariant data) const override + { + return std::visit( + [this](auto&& fd) -> FieldVariant { + using FD = std::decay_t; + using T = typename FD::element_type::value_type; + PCMS_ALWAYS_ASSERT(fd != nullptr); + if (dynamic_cast*>(fd.get()) == nullptr) { + throw pcms_error( + "EmptyFunctionSpace::CreateField: requires SimpleFieldData"); + } + if (fd->GetDOFHolderDataHost().size() != + detail::ExpectedFlatFieldDataSize(*layout_)) { + throw pcms_error( + "EmptyFunctionSpace::CreateField: field data size does not match " + "layout"); + } + return WrapField(layout_, std::forward(fd)); + }, + std::move(data)); + } + + [[nodiscard]] PointEvaluatorVariant CreatePointEvaluatorImpl( + Type /*value_type*/, const EvaluationRequest& /*request*/) const override + { + throw pcms_error("EmptyFunctionSpace does not support point evaluation"); + } + +private: + std::shared_ptr layout_; +}; + +} // namespace detail + +struct ClientState +{ + std::unique_ptr coupler; + Application* app = nullptr; + using HandleVariant = std::variant, FieldHandle, + FieldHandle, FieldHandle>; + std::map field_handles; +}; + +using FieldAdapterVariant = std::variant< + std::monostate, detail::XGCFieldRegistration, + detail::XGCFieldRegistration, detail::XGCFieldRegistration, + detail::XGCFieldRegistration, detail::DummyFieldRegistration>; + +template +using ClientFieldHandle = FieldHandle; + +inline ClientState::HandleVariant RegisterField(Application& /*app*/, + std::string name, + const std::monostate&, bool) +{ + throw pcms_error("pcms_add_field: field adapter for field '" + name + + "' was never initialized"); +} + +template +ClientState::HandleVariant RegisterField( + Application& app, std::string name, + const detail::XGCFieldRegistration& registration, bool participates) +{ + auto field = registration.function_space.template CreateField( + std::make_unique>( + registration.function_space.GetXGCLayout(), FieldMetadata{}, + registration.data)); + app.AddLayout(name, registration.function_space.GetLayout(), participates); + std::unique_ptr> serializer = + std::make_unique>(registration.plane_comm, + participates); + return ClientState::HandleVariant{app.AddField( + std::move(name), std::move(field), std::move(serializer), participates)}; +} + +inline ClientState::HandleVariant RegisterField( + Application& app, std::string name, const detail::DummyFieldRegistration&, + bool participates) +{ + auto function_space = detail::EmptyFunctionSpace{}; + app.AddLayout(name, function_space.GetLayout(), participates); + auto field = function_space.CreateField(FieldMetadata{}); + std::unique_ptr> serializer = + std::make_unique>(); + return ClientState::HandleVariant{app.AddField( + std::move(name), std::move(field), std::move(serializer), participates)}; +} } // namespace pcms [[nodiscard]] PcmsClientHandle pcms_create_client(const char* name, MPI_Comm comm) { - auto* coupler = new pcms::Coupler(name, comm, false, {}); - auto* app = coupler->AddApplication(name); - PcmsClientHandle handle; - handle.couplerPointer = reinterpret_cast(coupler); - handle.appPointer = reinterpret_cast(app); - return handle; + auto* client = new pcms::ClientState{}; + client->coupler = + std::make_unique(name, comm, false, redev::Partition{}); + client->app = client->coupler->AddApplication(name); + return {reinterpret_cast(client), + reinterpret_cast(client->app)}; } + void pcms_destroy_client(PcmsClientHandle client) { - if (client.couplerPointer != nullptr) - delete reinterpret_cast(client.couplerPointer); + delete reinterpret_cast(client.couplerPointer); } + PcmsReverseClassificationHandle pcms_load_reverse_classification( const char* file, MPI_Comm comm) { - // std::filesystem::path filepath{file}; auto* rc = new pcms::ReverseClassificationVertex{ pcms::ReadReverseClassificationVertex(file, comm)}; return {reinterpret_cast(rc)}; } + void pcms_destroy_reverse_classification(PcmsReverseClassificationHandle rc) { - if (rc.pointer != nullptr) - delete reinterpret_cast(rc.pointer); + delete reinterpret_cast(rc.pointer); } -struct AddFieldVariantOperators -{ - AddFieldVariantOperators(const char* name, pcms::Application* app, - int participates) - : name_(name), app_(app), participates_(participates) - { - } - - [[nodiscard]] - pcms::CoupledField* operator()(const std::monostate&) const noexcept - { - return nullptr; - } - template - [[nodiscard]] - pcms::CoupledField* operator()( - const FieldAdapter& field_adapter) const noexcept - { - return app_->AddField(name_, field_adapter, participates_); - } - - const char* name_; - pcms::Application* app_; - bool participates_; -}; PcmsFieldHandle pcms_add_field(PcmsClientHandle client_handle, const char* name, PcmsFieldAdapterHandle adapter_handle, int participates) { - + auto* client = + reinterpret_cast(client_handle.couplerPointer); + auto* app = reinterpret_cast(client_handle.appPointer); auto* adapter = reinterpret_cast(adapter_handle.pointer); - auto* app = reinterpret_cast(client_handle.appPointer); + PCMS_ALWAYS_ASSERT(client != nullptr); PCMS_ALWAYS_ASSERT(app != nullptr); PCMS_ALWAYS_ASSERT(adapter != nullptr); - // pcms::CoupledField* field = std::visit( - // redev::overloaded{ - // [](const std::monostate&) -> pcms::CoupledField* { return nullptr; }, - // [&name, &client, participates](const auto& field_adapter) { - // return client->AddField(name, field_adapter, participates); - // }}, - // *adapter); - pcms::CoupledField* field = - std::visit(AddFieldVariantOperators{name, app, participates}, *adapter); - return {reinterpret_cast(field)}; + + auto handle = std::visit( + [&](const auto& registration) { + return pcms::RegisterField(*app, name, registration, participates); + }, + *adapter); + + auto [it, inserted] = + client->field_handles.try_emplace(name, std::move(handle)); + if (!inserted) { + throw pcms::pcms_error("Field with this name already exists"); + } + return {reinterpret_cast(&it->second)}; } + void pcms_send_field_name(PcmsClientHandle client_handle, const char* name) { auto* app = reinterpret_cast(client_handle.appPointer); PCMS_ALWAYS_ASSERT(app != nullptr); app->SendField(name); } + void pcms_receive_field_name(PcmsClientHandle client_handle, const char* name) { auto* app = reinterpret_cast(client_handle.appPointer); PCMS_ALWAYS_ASSERT(app != nullptr); app->ReceiveField(name); } + void pcms_send_field(PcmsFieldHandle field_handle) { - auto* field = reinterpret_cast(field_handle.pointer); + auto* field = + reinterpret_cast(field_handle.pointer); PCMS_ALWAYS_ASSERT(field != nullptr); - field->Send(); + std::visit([](auto& typed_handle) { typed_handle.Send(); }, *field); } + void pcms_receive_field(PcmsFieldHandle field_handle) { - auto* field = reinterpret_cast(field_handle.pointer); + auto* field = + reinterpret_cast(field_handle.pointer); PCMS_ALWAYS_ASSERT(field != nullptr); - field->Receive(); + std::visit([](auto& typed_handle) { typed_handle.Receive(); }, *field); } + template void pcms_create_xgc_field_adapter_t( - const char* name, MPI_Comm comm, void* data, int size, + const char* /* name */, MPI_Comm comm, void* data, int size, const pcms::ReverseClassificationVertex& reverse_classification, in_overlap_function in_overlap, pcms::FieldAdapterVariant& field_adapter) { PCMS_ALWAYS_ASSERT((size > 0) ? (data != nullptr) : true); + auto function_space = + pcms::XGCFieldFactory(reverse_classification, in_overlap, size); pcms::Rank1View data_view( reinterpret_cast(data), size); - field_adapter.emplace>( - name, comm, data_view, reverse_classification, in_overlap); + field_adapter.emplace>( + std::move(function_space), comm, data_view); } + PcmsFieldAdapterHandle pcms_create_xgc_field_adapter( const char* name, MPI_Comm comm, void* data, int size, PcmsType data_type, const PcmsReverseClassificationHandle rc, in_overlap_function in_overlap) @@ -154,7 +285,7 @@ PcmsFieldAdapterHandle pcms_create_xgc_field_adapter( *field_adapter); break; case PCMS_LONG_INT: - pcms_create_xgc_field_adapter_t(name, comm, data, size, + pcms_create_xgc_field_adapter_t(name, comm, data, size, *reverse_classification, in_overlap, *field_adapter); break; @@ -164,22 +295,19 @@ PcmsFieldAdapterHandle pcms_create_xgc_field_adapter( } return {reinterpret_cast(field_adapter)}; } + PcmsFieldAdapterHandle pcms_create_dummy_field_adapter() { auto* field_adapter = - new pcms::FieldAdapterVariant{pcms::DummyFieldAdapter{}}; + new pcms::FieldAdapterVariant{pcms::detail::DummyFieldRegistration{}}; return {reinterpret_cast(field_adapter)}; } void pcms_destroy_field_adapter(PcmsFieldAdapterHandle adapter_handle) { - auto* adapter = - reinterpret_cast(adapter_handle.pointer); - if (adapter != nullptr) { - delete adapter; - adapter = nullptr; - } + delete reinterpret_cast(adapter_handle.pointer); } + int pcms_reverse_classification_count_verts(PcmsReverseClassificationHandle rc) { auto* reverse_classification = @@ -191,24 +319,28 @@ int pcms_reverse_classification_count_verts(PcmsReverseClassificationHandle rc) return current + verts.second.size(); }); } + void pcms_begin_send_phase(PcmsClientHandle h) { auto* app = reinterpret_cast(h.appPointer); PCMS_ALWAYS_ASSERT(app != nullptr); app->BeginSendPhase(); } + void pcms_end_send_phase(PcmsClientHandle h) { auto* app = reinterpret_cast(h.appPointer); PCMS_ALWAYS_ASSERT(app != nullptr); app->EndSendPhase(); } + void pcms_begin_receive_phase(PcmsClientHandle h) { auto* app = reinterpret_cast(h.appPointer); PCMS_ALWAYS_ASSERT(app != nullptr); app->BeginReceivePhase(); } + void pcms_end_receive_phase(PcmsClientHandle h) { auto* app = reinterpret_cast(h.appPointer); diff --git a/src/pcms/capi/interpolator.cpp b/src/pcms/capi/interpolator.cpp index d3cbb4c76..9b5703d17 100644 --- a/src/pcms/capi/interpolator.cpp +++ b/src/pcms/capi/interpolator.cpp @@ -3,10 +3,11 @@ // #include #include -#include +#include #include #include #include +#include #include //[[nodiscard]] @@ -14,7 +15,7 @@ PcmsInterpolatorHandle pcms_create_interpolator(PcmsOmegaHMeshHandle oh_mesh, double radius) { auto* source_mesh = reinterpret_cast(oh_mesh.mesh_handle); - auto* interpolator = new MLSMeshInterpolation(*source_mesh, radius); + auto* interpolator = new pcms::MLSMeshInterpolation(*source_mesh, radius); return {reinterpret_cast(interpolator)}; } @@ -28,7 +29,7 @@ PcmsInterpolatorHandle pcms_create_point_based_interpolator( reinterpret_cast(source_points), source_points_size); auto target_points_view = pcms::Rank1View( reinterpret_cast(target_points), target_points_size); - auto* interpolator = new MLSPointCloudInterpolation( + auto* interpolator = new pcms::MLSPointCloudInterpolation( source_points_view, target_points_view, 2, radius, min_req_supports, degree, true, lambda, decay_factor); return {reinterpret_cast(interpolator)}; @@ -42,7 +43,9 @@ Omega_h::HostRead read_mesh_centroids(const char* mesh_filename, pcms::printInfo("The interpolator got dg2 mesh file: %s\n", fname.c_str()); auto mesh_lib = Omega_h::Library(nullptr, nullptr, MPI_COMM_SELF); auto mesh = Omega_h::binary::read(fname, mesh_lib.world()); - auto elem_centroids = getCentroids(mesh); + OMEGA_H_CHECK_PRINTF(mesh.dim() == 2, "Mesh dimension is not 2D %d\n", + mesh.dim()); + auto elem_centroids = pcms::get_entity_centroids(mesh, Omega_h::FACE); num_elements = mesh.nelems(); OMEGA_H_CHECK_PRINTF(num_elements * 2 == elem_centroids.size(), "Mesh element centroids size does not match the number " @@ -51,8 +54,6 @@ Omega_h::HostRead read_mesh_centroids(const char* mesh_filename, pcms::printInfo("Number of element centroids: %d\n", elem_centroids.size() / 2); - OMEGA_H_CHECK_PRINTF(mesh.dim() == 2, "Mesh dimension is not 2D %d\n", - mesh.dim()); return {elem_centroids}; } @@ -106,7 +107,7 @@ PcmsInterpolatorHandle pcms_create_xgcnodedegas2_interpolator( void pcms_destroy_interpolator(PcmsInterpolatorHandle interpolator) { if (interpolator.pointer != nullptr) { - delete reinterpret_cast(interpolator.pointer); + delete reinterpret_cast(interpolator.pointer); } } @@ -114,7 +115,7 @@ void pcms_interpolate(PcmsInterpolatorHandle interpolator, void* input, int input_size, void* output, int output_size) { auto* mls_interpolator = - reinterpret_cast(interpolator.pointer); + reinterpret_cast(interpolator.pointer); OMEGA_H_CHECK_PRINTF( input_size == mls_interpolator->getSourceSize(), diff --git a/src/pcms/configuration.h.in b/src/pcms/configuration.h.in index f52825b62..51859ec0a 100644 --- a/src/pcms/configuration.h.in +++ b/src/pcms/configuration.h.in @@ -4,5 +4,6 @@ #cmakedefine PCMS_ENABLE_OMEGA_H #cmakedefine PCMS_ENABLE_C #cmakedefine PCMS_ENABLE_PRINT -#cmakedefine PCMS_ENABLE_SPDLOG #cmakedefine PCMS_ENABLE_Fortran +#cmakedefine PCMS_ENABLE_MESHFIELDS +#cmakedefine PCMS_ENABLE_PETSC diff --git a/src/pcms/coupler.h b/src/pcms/coupler.h deleted file mode 100644 index 494bc0b6d..000000000 --- a/src/pcms/coupler.h +++ /dev/null @@ -1,266 +0,0 @@ -#ifndef PCMS_COUPLER_H -#define PCMS_COUPLER_H -#include "pcms/utility/common.h" -#include "pcms/field_communicator.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include "pcms/utility/profile.h" - -namespace pcms -{ - -// to avoid having any redev:: types in the user interface -using ProcessType = redev::ProcessType; - -class CoupledField -{ -public: - template - CoupledField(const std::string& name, FieldAdapterT field_adapter, - MPI_Comm mpi_comm, redev::Redev& redev, redev::Channel& channel, - bool participates = true) - { - PCMS_FUNCTION_TIMER; - MPI_Comm mpi_comm_subset = MPI_COMM_NULL; - PCMS_ALWAYS_ASSERT((mpi_comm == MPI_COMM_NULL) ? (participates == false) - : true); - if (mpi_comm != MPI_COMM_NULL) { - int rank = -1; - MPI_Comm_rank(mpi_comm, &rank); - MPI_Comm_split(mpi_comm, participates ? 0 : MPI_UNDEFINED, rank, - &mpi_comm_subset); - } - coupled_field_ = - std::make_unique>( - name, std::move(field_adapter), mpi_comm_subset, redev, channel, - participates); - } - - void Send(Mode mode = Mode::Synchronous) - { - PCMS_FUNCTION_TIMER; - coupled_field_->Send(mode); - } - void Receive(Mode mode = Mode::Synchronous) - { - PCMS_FUNCTION_TIMER; - coupled_field_->Receive(mode); - } - template - [[nodiscard]] T* GetFieldAdapter() const - { - PCMS_FUNCTION_TIMER; - if (typeid(T) == coupled_field_->GetFieldAdapterType()) { - auto* adapter = coupled_field_->GetFieldAdapter(); - return reinterpret_cast(adapter); - } - std::cerr << "Requested type does not match field adapter type\n"; - std::abort(); - } - struct CoupledFieldConcept - { - virtual void Send(Mode) = 0; - virtual void Receive(Mode) = 0; - [[nodiscard]] virtual const std::type_info& GetFieldAdapterType() - const noexcept = 0; - [[nodiscard]] virtual void* GetFieldAdapter() noexcept = 0; - virtual ~CoupledFieldConcept() = default; - }; - template - struct CoupledFieldModel final : CoupledFieldConcept - { - using value_type = typename FieldAdapterT::value_type; - - CoupledFieldModel(const std::string& name, FieldAdapterT&& field_adapter, - MPI_Comm mpi_comm_subset, redev::Redev& redev, - redev::Channel& channel, bool participates) - : mpi_comm_subset_(mpi_comm_subset), - field_adapter_(std::move(field_adapter)), - comm_(FieldCommunicator(name, mpi_comm_subset_, redev, channel, - field_adapter_)), - type_info_(typeid(FieldAdapterT)) - { - PCMS_FUNCTION_TIMER; - } - void Send(Mode mode) final - { - PCMS_FUNCTION_TIMER; - comm_.Send(mode); - }; - void Receive(Mode mode) final - { - PCMS_FUNCTION_TIMER; - comm_.Receive(mode); - }; - virtual const std::type_info& GetFieldAdapterType() const noexcept - { - return type_info_; - } - virtual void* GetFieldAdapter() noexcept - { - return reinterpret_cast(&field_adapter_); - }; - ~CoupledFieldModel() - { - PCMS_FUNCTION_TIMER; - if (mpi_comm_subset_ != MPI_COMM_NULL) - MPI_Comm_free(&mpi_comm_subset_); - } - - MPI_Comm mpi_comm_subset_; - FieldAdapterT field_adapter_; - FieldCommunicator comm_; - const std::type_info& type_info_; - }; - -private: - std::unique_ptr coupled_field_; -}; - -class Application -{ -public: - Application(std::string name, MPI_Comm comm, redev::Redev& redev, - adios2::Params params, redev::TransportType transport_type, - std::string path) - : mpi_comm_(comm), - redev_(redev), - channel_{redev_.CreateAdiosChannel(std::move(name), std::move(params), - transport_type, std::move(path))} - { - PCMS_FUNCTION_TIMER; - } - // FIXME should take a file path for the parameters, not take adios2 params. - // These fields are supposed to be agnostic to adios2... - template - CoupledField* AddField(std::string name, FieldAdapterT&& field_adapter, - bool participates = true) - { - PCMS_FUNCTION_TIMER; - auto [it, inserted] = fields_.try_emplace( - name, name, std::forward(field_adapter), mpi_comm_, redev_, - channel_, participates); - if (!inserted) { - std::cerr << "Field with this name" << name << "already exists!\n"; - std::terminate(); - } - return &(it->second); - } - void SendField(const std::string& name, Mode mode = Mode::Synchronous) - { - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(InSendPhase()); - detail::find_or_error(name, fields_).Send(mode); - }; - void ReceiveField(const std::string& name, Mode mode = Mode::Synchronous) - { - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(InReceivePhase()); - detail::find_or_error(name, fields_).Receive(mode); - }; - [[nodiscard]] bool InSendPhase() const noexcept - { - PCMS_FUNCTION_TIMER; - return channel_.InSendCommunicationPhase(); - } - [[nodiscard]] bool InReceivePhase() const noexcept - { - PCMS_FUNCTION_TIMER; - return channel_.InReceiveCommunicationPhase(); - } - void BeginSendPhase() - { - PCMS_FUNCTION_TIMER; - channel_.BeginSendCommunicationPhase(); - } - void EndSendPhase() - { - PCMS_FUNCTION_TIMER; - channel_.EndSendCommunicationPhase(); - } - void BeginReceivePhase() - { - PCMS_FUNCTION_TIMER; - channel_.BeginReceiveCommunicationPhase(); - } - void EndReceivePhase() - { - PCMS_FUNCTION_TIMER; - channel_.EndReceiveCommunicationPhase(); - } - - template - auto SendPhase(const Func& func, Args&&... args) - { - PCMS_FUNCTION_TIMER; - return channel_.SendPhase(func, std::forward(args)...); - } - template - auto ReceivePhase(const Func& func, Args&&... args) - { - PCMS_FUNCTION_TIMER; - return channel_.ReceivePhase(func, std::forward(args)...); - } - -private: - MPI_Comm mpi_comm_; - redev::Redev& redev_; - redev::Channel channel_; - // map is used rather than unordered_map because we give pointers to the - // internal data and rehash of unordered_map can cause pointer invalidation. - // map is less cache friendly, but pointers are not invalidated. - std::map fields_; -}; - -class Coupler -{ -private: - redev::Redev SetUpRedev(bool isServer, redev::Partition partition) - { - if (isServer) - return redev::Redev(mpi_comm_, std::move(partition), ProcessType::Server); - else - return redev::Redev(mpi_comm_); - } - -public: - Coupler(std::string name, MPI_Comm comm, bool isServer, - redev::Partition partition) - : name_(std::move(name)), - mpi_comm_(comm), - redev_(SetUpRedev(isServer, std::move(partition))) - { - PCMS_FUNCTION_TIMER; - } - Application* AddApplication( - std::string name, std::string path = "", - redev::TransportType transport_type = redev::TransportType::BP4, - adios2::Params params = {{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}) - { - PCMS_FUNCTION_TIMER; - auto key = path + name; - auto [it, inserted] = applications_.try_emplace( - key, std::move(name), mpi_comm_, redev_, std::move(params), - transport_type, std::move(path)); - if (!inserted) { - std::cerr << "Application with name " << name << "already exists!\n"; - std::terminate(); - } - return &(it->second); - } - - [[nodiscard]] const redev::Partition& GetPartition() const noexcept - { - return redev_.GetPartition(); - } - -private: - std::string name_; - MPI_Comm mpi_comm_; - redev::Redev redev_; - // gather and scatter operations have reference to internal fields - std::map applications_; -}; - -} // namespace pcms - -#endif // PCMS_COUPLER_H diff --git a/src/pcms/coupler/CMakeLists.txt b/src/pcms/coupler/CMakeLists.txt new file mode 100644 index 000000000..82afceb32 --- /dev/null +++ b/src/pcms/coupler/CMakeLists.txt @@ -0,0 +1,69 @@ +set(PCMS_COUPLER_HEADERS + coupler_types.h + field_communicator.hpp + coupler.hpp + field_serializer.h + field_layout_communicator.h + field_exchange_planner.h + partition.h + overlap_mask.h +) + + +set(PCMS_COUPLER_SOURCES + coupler.cpp + field_layout_communicator.cpp + field_exchange_planner.cpp +) + +if(PCMS_ENABLE_XGC) + list(APPEND PCMS_COUPLER_HEADERS + serializer/xgc.h) +endif() + +add_library(pcms_coupler ${PCMS_COUPLER_SOURCES}) +set_target_properties(pcms_coupler PROPERTIES + OUTPUT_NAME pcmscoupler + EXPORT_NAME coupler) +target_sources(pcms_coupler PUBLIC + FILE_SET coupler + TYPE HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. + FILES ${PCMS_COUPLER_HEADERS}) +add_library(pcms::coupler ALIAS pcms_coupler) +target_compile_features(pcms_coupler PUBLIC cxx_std_20) + +target_link_libraries(pcms_coupler PUBLIC + redev::redev + MPI::MPI_CXX + Kokkos::kokkos + perfstubs + pcms::utility + pcms::field) + +if(PCMS_ENABLE_OMEGA_H) + target_link_libraries(pcms_coupler PUBLIC Omega_h::omega_h) +endif() + +if (PCMS_ENABLE_MESHFIELDS) + target_link_libraries(pcms_coupler PUBLIC meshfields::meshfields) +endif () + +target_include_directories(pcms_coupler INTERFACE + $ + $ + $) + +install( + TARGETS pcms_coupler + EXPORT pcms_coupler-targets + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms/coupler + FILE_SET coupler DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + +install( + EXPORT pcms_coupler-targets + NAMESPACE pcms:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) diff --git a/src/pcms/coupler/coupler.cpp b/src/pcms/coupler/coupler.cpp new file mode 100644 index 000000000..a0c83e7db --- /dev/null +++ b/src/pcms/coupler/coupler.cpp @@ -0,0 +1,65 @@ +#include "pcms/coupler/coupler.hpp" + +namespace pcms +{ + +FieldLayoutCommunicator& Application::GetLayoutCommunicator( + const FieldLayout& layout) +{ + PCMS_FUNCTION_TIMER; + auto it = field_layout_communicators_.find(&layout); + if (it != field_layout_communicators_.end()) { + return *it->second; + } else { + throw pcms_error("Field added with unregistered layout. Call AddLayout() " + "before AddField()."); + } +} + +const FieldLayout& Application::AddLayout( + std::string name, std::shared_ptr layout, + bool participates) +{ + return AddLayout(std::move(name), std::move(layout), + std::make_unique(), + participates); +} + +const FieldLayout& Application::AddLayout( + std::string name, std::shared_ptr layout, + std::unique_ptr planner, bool participates) +{ + MPI_Comm mpi_comm_subset = MPI_COMM_NULL; + bool own_mpi_comm = false; + PCMS_ALWAYS_ASSERT((mpi_comm_ == MPI_COMM_NULL) ? (!participates) : true); + if (mpi_comm_ != MPI_COMM_NULL) { + int rank = -1; + MPI_Comm_rank(mpi_comm_, &rank); + MPI_Comm_split(mpi_comm_, participates ? 0 : MPI_UNDEFINED, rank, + &mpi_comm_subset); + own_mpi_comm = true; + } + layouts_.push_back(std::move(layout)); + const FieldLayout& layout_ref = *layouts_.back(); + + // Check if there's an overlap mask for this layout + const OverlapMask* overlap_mask = nullptr; + auto mask_it = layout_overlap_masks_.find(name); + if (mask_it != layout_overlap_masks_.end()) { + overlap_mask = mask_it->second.get(); + } + + field_layout_communicators_.emplace( + &layout_ref, std::make_unique( + name, mpi_comm_subset, redev_, channel_, layout_ref, + std::move(planner), own_mpi_comm, overlap_mask)); + return layout_ref; +} + +void Application::SetLayoutOverlapMask( + const std::string& layout_name, std::unique_ptr overlap_mask) +{ + layout_overlap_masks_[layout_name] = std::move(overlap_mask); +} + +} // namespace pcms diff --git a/src/pcms/coupler/coupler.hpp b/src/pcms/coupler/coupler.hpp new file mode 100644 index 000000000..d2d12cb1a --- /dev/null +++ b/src/pcms/coupler/coupler.hpp @@ -0,0 +1,284 @@ +#ifndef COUPLER2_H_ +#define COUPLER2_H_ + +#include "pcms/coupler/coupler_types.h" +#include "pcms/field/field.h" +#include "pcms/field/field_layout.h" +#include "pcms/coupler/field_layout_communicator.h" +#include "pcms/coupler/field_communicator.hpp" +#include "pcms/coupler/field_exchange_planner.h" +#include "pcms/coupler/overlap_mask.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/common.h" +#include "pcms/utility/profile.h" +#include + +namespace pcms +{ + +class Application; + +template +class FieldHandle +{ +public: + FieldHandle(Application* app, std::string name) + : app_(app), name_(std::move(name)) + { + } + + void Send(redev::Mode mode = redev::Mode::Synchronous) const; + void Receive(redev::Mode mode = redev::Mode::Synchronous) const; + [[nodiscard]] Field& GetField() const; + +private: + Application* app_; + std::string name_; +}; + +class Application +{ +public: + Application(std::string name, MPI_Comm comm, redev::Redev& redev, + adios2::Params params, redev::TransportType transport_type, + std::string path) + : mpi_comm_(comm), + redev_(redev), + channel_{redev_.CreateAdiosChannel(std::move(name), std::move(params), + transport_type, std::move(path))} + { + PCMS_FUNCTION_TIMER; + } + + const FieldLayout& AddLayout(std::string name, + std::shared_ptr layout, + bool participates = true); + const FieldLayout& AddLayout(std::string name, + std::shared_ptr layout, + std::unique_ptr planner, + bool participates = true); + + // Set the overlap mask for a specific layout by name + void SetLayoutOverlapMask(const std::string& layout_name, + std::unique_ptr overlap_mask); + + template + FieldHandle AddField(std::string name, Field&& field, + bool participates = true); + + template + FieldHandle AddField(std::string name, Field&& field, + std::unique_ptr> serializer, + bool participates = true); + + void SendField(const std::string& name, + redev::Mode mode = redev::Mode::Synchronous) + { + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(InSendPhase()); + FieldCommunicator2Ptr& communicator = + detail::find_or_error(name, field_communicators_); + std::visit( + [mode](auto& field_communicator) { field_communicator->Send(mode); }, + communicator); + }; + void ReceiveField(const std::string& name, + redev::Mode mode = redev::Mode::Synchronous) + { + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(InReceivePhase()); + std::visit( + [mode](auto& field_communicator) { field_communicator->Receive(); }, + detail::find_or_error(name, field_communicators_)); + }; + [[nodiscard]] bool InSendPhase() const noexcept + { + PCMS_FUNCTION_TIMER; + return channel_.InSendCommunicationPhase(); + } + [[nodiscard]] bool InReceivePhase() const noexcept + { + PCMS_FUNCTION_TIMER; + return channel_.InReceiveCommunicationPhase(); + } + void BeginSendPhase() + { + PCMS_FUNCTION_TIMER; + channel_.BeginSendCommunicationPhase(); + } + void EndSendPhase() + { + PCMS_FUNCTION_TIMER; + channel_.EndSendCommunicationPhase(); + } + void BeginReceivePhase() + { + PCMS_FUNCTION_TIMER; + channel_.BeginReceiveCommunicationPhase(); + } + void EndReceivePhase() + { + PCMS_FUNCTION_TIMER; + channel_.EndReceiveCommunicationPhase(); + } + + template + auto SendPhase(const Func& func, Args&&... args) + { + PCMS_FUNCTION_TIMER; + return channel_.SendPhase(func, std::forward(args)...); + } + template + auto ReceivePhase(const Func& func, Args&&... args) + { + PCMS_FUNCTION_TIMER; + return channel_.ReceivePhase(func, std::forward(args)...); + } + + [[nodiscard]] std::size_t GetLayoutCommunicatorCount() const noexcept + { + return field_layout_communicators_.size(); + } + + template + [[nodiscard]] Field& GetField(const std::string& name); + +private: + FieldLayoutCommunicator& GetLayoutCommunicator(const FieldLayout& layout); + + MPI_Comm mpi_comm_; + redev::Redev& redev_; + redev::Channel channel_; + std::vector> layouts_; + std::map fields_; + std::vector> + owned_field_layout_communicators_; + // map is used rather than unordered_map because we give pointers to the + // internal data and rehash of unordered_map can cause pointer invalidation. + // map is less cache friendly, but pointers are not invalidated. + std::map field_communicators_; + std::map> + field_layout_communicators_; + std::map> layout_overlap_masks_; +}; + +class Coupler +{ +private: + redev::Redev SetUpRedev(bool isServer, redev::Partition partition) + { + if (isServer) + return redev::Redev(mpi_comm_, std::move(partition), ProcessType::Server); + else + return redev::Redev(mpi_comm_); + } + +public: + Coupler(std::string name, MPI_Comm comm, bool isServer, + redev::Partition partition) + : name_(std::move(name)), + mpi_comm_(comm), + redev_(SetUpRedev(isServer, std::move(partition))) + { + PCMS_FUNCTION_TIMER; + } + Application* AddApplication( + std::string name, std::string path = "", + redev::TransportType transport_type = redev::TransportType::BP4, + adios2::Params params = {{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}) + { + PCMS_FUNCTION_TIMER; + auto key = path + name; + auto [it, inserted] = applications_.try_emplace( + key, std::move(name), mpi_comm_, redev_, std::move(params), + transport_type, std::move(path)); + if (!inserted) { + std::cerr << "Application with name " << name << "already exists!\n"; + std::terminate(); + } + return &(it->second); + } + + [[nodiscard]] const redev::Partition& GetPartition() const noexcept + { + return redev_.GetPartition(); + } + +private: + std::string name_; + MPI_Comm mpi_comm_; + redev::Redev redev_; + // gather and scatter operations have reference to internal fields + std::map applications_; +}; + +} // namespace pcms + +template +void pcms::FieldHandle::Send(redev::Mode mode) const +{ + PCMS_ALWAYS_ASSERT(app_ != nullptr); + app_->SendField(name_, mode); +} + +template +void pcms::FieldHandle::Receive(redev::Mode mode) const +{ + PCMS_ALWAYS_ASSERT(app_ != nullptr); + app_->ReceiveField(name_, mode); +} + +template +pcms::Field& pcms::FieldHandle::GetField() const +{ + PCMS_ALWAYS_ASSERT(app_ != nullptr); + return app_->GetField(name_); +} + +template +pcms::Field& pcms::Application::GetField(const std::string& name) +{ + auto* field = std::get_if>(&detail::find_or_error(name, fields_)); + if (field == nullptr) { + throw pcms_error("Field stored with different type than requested"); + } + return *field; +} + +template +pcms::FieldHandle pcms::Application::AddField(std::string name, + Field&& field, + bool participates) +{ + return AddField(std::move(name), std::move(field), + std::make_unique>(), participates); +} + +template +pcms::FieldHandle pcms::Application::AddField( + std::string name, Field&& field, + std::unique_ptr> serializer, bool participates) +{ + PCMS_FUNCTION_TIMER; + (void)participates; + auto [field_it, field_inserted] = fields_.emplace(name, std::move(field)); + if (!field_inserted) { + throw pcms_error("Field with this name already exists"); + } + auto& field_obj = std::get>(field_it->second); + const FieldLayout& layout = field_obj.GetLayout(); + FieldLayoutCommunicator& layout_communicator = GetLayoutCommunicator(layout); + FieldCommunicator2Ptr field_communicator = + std::make_unique>(name, layout_communicator, field_obj, + std::move(serializer)); + + auto [it, inserted] = + field_communicators_.emplace(name, std::move(field_communicator)); + if (!inserted) { + fields_.erase(field_it); + throw pcms_error("Field with this name already exists"); + } + return FieldHandle{this, std::move(name)}; +} + +#endif // COUPLER2_H_ diff --git a/src/pcms/coupler/coupler_types.h b/src/pcms/coupler/coupler_types.h new file mode 100644 index 000000000..46deb86b1 --- /dev/null +++ b/src/pcms/coupler/coupler_types.h @@ -0,0 +1,14 @@ +#ifndef PCMS_COUPLER_TYPES_H_ +#define PCMS_COUPLER_TYPES_H_ + +#include + +namespace pcms +{ + +using ProcessType = redev::ProcessType; +using Mode = redev::Mode; + +} // namespace pcms + +#endif // PCMS_COUPLER_TYPES_H_ diff --git a/src/pcms/field_communicator2.h b/src/pcms/coupler/field_communicator.hpp similarity index 56% rename from src/pcms/field_communicator2.h rename to src/pcms/coupler/field_communicator.hpp index af16af733..3d1014a0e 100644 --- a/src/pcms/field_communicator2.h +++ b/src/pcms/coupler/field_communicator.hpp @@ -1,9 +1,10 @@ #ifndef FIELD_COMMUNICATOR2_H_ #define FIELD_COMMUNICATOR2_H_ -#include "field_layout_communicator.h" -#include "pcms/field_layout.h" -#include "pcms/field.h" +#include "pcms/coupler/field_layout_communicator.h" +#include "pcms/field/field.h" +#include "pcms/field/field_layout.h" +#include "pcms/coupler/field_serializer.h" #include "pcms/utility/profile.h" #include "pcms/utility/assert.h" #include "pcms/utility/inclusive_scan.h" @@ -15,16 +16,28 @@ namespace pcms { template -class FieldCommunicator2 +class FieldCommunicator { public: - FieldCommunicator2(FieldLayoutCommunicator& layout_comm, FieldT& field) - : comm_buffer_{}, layout_comm_(layout_comm), field_(field) + FieldCommunicator(const std::string& name, + FieldLayoutCommunicator& layout_comm, Field& field) + : FieldCommunicator(name, layout_comm, field, + std::make_unique>()) + { + } + + FieldCommunicator(const std::string& name, + FieldLayoutCommunicator& layout_comm, Field& field, + std::unique_ptr> serializer) + : comm_buffer_{}, + layout_comm_(layout_comm), + field_(field), + serializer_(std::move(serializer)) { PCMS_ALWAYS_ASSERT(&layout_comm.GetLayout() == &field.GetLayout()); comm_buffer_.resize(layout_comm.GetMsgSize()); - comm_ = layout_comm_.GetChannel().CreateComm(layout_comm.GetName(), - layout_comm.GetMPIComm()); + comm_ = + layout_comm_.GetChannel().CreateComm(name, layout_comm.GetMPIComm()); layout_comm_.SetOutMessageLayout(comm_); } @@ -32,9 +45,9 @@ class FieldCommunicator2 { PCMS_FUNCTION_TIMER; PCMS_ALWAYS_ASSERT(layout_comm_.GetChannel().InSendCommunicationPhase()); - auto n = field_.Serialize({}, {}); auto buffer = make_array_view(comm_buffer_); - field_.Serialize(buffer, layout_comm_.GetPermutationArray()); + serializer_->Serialize(field_.GetData(), field_.GetLayout(), buffer, + layout_comm_.GetPermutationArray()); comm_.Send(buffer.data_handle(), mode); } @@ -46,19 +59,21 @@ class FieldCommunicator2 // mode because we make an immediate call to deserialize after a call to // receive. auto data = comm_.Recv(redev::Mode::Synchronous); - field_.Deserialize(make_const_array_view(data), - layout_comm_.GetPermutationArray()); + serializer_->Deserialize(field_.GetData(), field_.GetLayout(), + make_const_array_view(data), + layout_comm_.GetPermutationArray()); } private: std::vector comm_buffer_; redev::BidirectionalComm comm_; FieldLayoutCommunicator& layout_comm_; - FieldT& field_; + Field& field_; + std::unique_ptr> serializer_; }; template -using FieldCommunicator2PtrT = std::unique_ptr>; +using FieldCommunicator2PtrT = std::unique_ptr>; using FieldCommunicator2Ptr = std::variant, FieldCommunicator2PtrT, diff --git a/src/pcms/coupler/field_exchange_planner.cpp b/src/pcms/coupler/field_exchange_planner.cpp new file mode 100644 index 000000000..08e1d6ca1 --- /dev/null +++ b/src/pcms/coupler/field_exchange_planner.cpp @@ -0,0 +1,337 @@ +#include "pcms/coupler/field_exchange_planner.h" +#include "partition.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/inclusive_scan.h" +#include "pcms/utility/profile.h" +#include +#include + +namespace pcms +{ + +namespace +{ + +// Sentinel in a receive permutation: this local DOF's GID was not present in +// the received message. The deserializer skips it (negative => not received). +constexpr redev::LO kUnreceivedDof = -1; + +struct PartitionMapping +{ + std::vector indices; + EntOffsetsArray ent_offsets; + + PartitionMapping() { ent_offsets.fill(0); } +}; + +using ReversePartitionMap2 = std::map; + +struct OutMsg +{ + redev::LOs dest; + redev::LOs offset; +}; + +// Returns the mesh entity dimension for DOF local_index based on the +// entity offsets array. ent_offsets[d]..ent_offsets[d+1] is the range of +// DOF indices belonging to mesh entity dimension d. +static int GetMeshEntityDim(LO local_index, const EntOffsetsArray& ent_offsets) +{ + for (int d = 0; d < ent_offsets_len - 1; ++d) { + if (local_index >= static_cast(ent_offsets[d]) && + local_index < static_cast(ent_offsets[d + 1])) { + return d; + } + } + return ent_offsets_len - 1; +} + +static size_t GetMessageBlockIndex( + LO permutation_entry, Rank1View offsets) +{ + auto begin = offsets.data_handle(); + auto end = begin + offsets.size(); + auto it = std::upper_bound(begin, end, permutation_entry); + PCMS_ALWAYS_ASSERT(it != begin); + return static_cast(std::distance(begin, it - 1)); +} + +static OutMsg ConstructOutMessage(const ReversePartitionMap2& reverse_partition) +{ + PCMS_FUNCTION_TIMER; + OutMsg out; + redev::LOs counts; + counts.reserve(reverse_partition.size()); + out.dest.reserve(reverse_partition.size()); + for (const auto& rank : reverse_partition) { + out.dest.push_back(rank.first); + counts.push_back(rank.second.indices.size() + + rank.second.ent_offsets.size()); + } + out.offset.resize(counts.size() + 1); + out.offset[0] = 0; + pcms::inclusive_scan(counts.begin(), counts.end(), + std::next(out.offset.begin(), 1)); + return out; +} + +static redev::LOs ConstructPermutation( + const ReversePartitionMap2& reverse_partition, size_t num_entries, + int* length) +{ + PCMS_FUNCTION_TIMER; + redev::LOs permutation(num_entries); + LO entry = 0; + for (const auto& rank : reverse_partition) { + entry += ent_offsets_len; + + for (unsigned e = 0; e < rank.second.ent_offsets.size() - 1; ++e) { + const int start = rank.second.ent_offsets[e]; + const int end = rank.second.ent_offsets[e + 1]; + + for (int i = start; i < end; ++i) { + auto index = rank.second.indices[i]; + PCMS_ALWAYS_ASSERT(static_cast(index) < permutation.size()); + permutation[index] = entry++; + } + } + } + *length = entry; + return permutation; +} + +static redev::LOs ConstructPermutation( + GlobalIDView local_gids, + GlobalIDView received_msg, + const EntOffsetsArray& ent_offsets) +{ + PCMS_FUNCTION_TIMER; + std::array, 4> gid_to_buffer_index; + size_t offset = 0; + while (true) { + GlobalIDView received_offsets( + received_msg.data_handle() + offset, ent_offsets_len); + int length = received_offsets[received_offsets.size() - 1]; + GlobalIDView received_gids( + received_msg.data_handle() + offset + ent_offsets_len, length); + + PCMS_ALWAYS_ASSERT(offset + ent_offsets_len + length - 1 < + received_msg.size()); + + for (size_t e = 0; e < received_offsets.size() - 1; ++e) { + size_t start = received_offsets[e]; + size_t end = received_offsets[e + 1]; + + for (size_t i = start; i < end; ++i) { + gid_to_buffer_index[e][received_gids[i]] = offset + ent_offsets_len + i; + } + } + + offset += length + ent_offsets_len; + if (offset >= received_msg.size()) + break; + } + redev::LOs permutation(local_gids.size()); + for (size_t e = 0; e < ent_offsets.size() - 1; ++e) { + const auto start = ent_offsets[e]; + const auto end = ent_offsets[e + 1]; + + for (size_t i = start; i < end; ++i) { + // A local DOF whose GID was not in the received message (e.g. it lies + // outside the sender's overlap mask) gets the sentinel kUnreceivedDof. + // The deserializer must skip these and preserve the field's existing + // value, rather than reading buffer[0]. Use find() rather than + // operator[] so missing keys are not silently inserted as 0. + const auto it = gid_to_buffer_index[e].find(local_gids[i]); + if (it!=gid_to_buffer_index[e].end()) { + permutation[i] = it->second; + } + } + } + + REDEV_ALWAYS_ASSERT(permutation.size() == local_gids.size()); + return permutation; +} + +static OutMsg ConstructOutMessage(int rank, int nproc, + const redev::InMessageLayout& in) +{ + PCMS_FUNCTION_TIMER; + REDEV_ALWAYS_ASSERT(!in.srcRanks.empty()); + auto nAppProcs = in.srcRanks.size() / static_cast(nproc); + redev::LOs senderDeg(nAppProcs); + for (size_t i = 0; i < nAppProcs - 1; ++i) { + senderDeg[i] = + in.srcRanks[(i + 1) * nproc + rank] - in.srcRanks[i * nproc + rank]; + } + const auto totInMsgs = in.offset[rank + 1] - in.offset[rank]; + senderDeg[nAppProcs - 1] = + totInMsgs - in.srcRanks[(nAppProcs - 1) * nproc + rank]; + OutMsg out; + for (size_t i = 0; i < nAppProcs; ++i) { + if (senderDeg[i] > 0) + out.dest.push_back(i); + } + redev::GO sum = 0; + for (auto deg : senderDeg) { + if (deg > 0) { + out.offset.push_back(sum); + sum += deg; + } + } + out.offset.push_back(sum); + return out; +} + +static ReversePartitionMap2 BuildReversePartitionMap( + const FieldLayout& layout, const redev::Partition& partition, + const OverlapMask& overlap_mask) +{ + PCMS_FUNCTION_TIMER; + auto owned = layout.GetOwnedHost(); + auto class_dims = layout.GetDOFHolderClassificationDimensionsHost(); + auto class_ids = layout.GetDOFHolderClassificationIdsHost(); + auto coords = layout.GetDOFHolderCoordinates().GetCoordinates(); + auto ent_offsets = layout.GetEntOffsets(); + int mesh_dim = static_cast(coords.extent(1)); + + // move coords to host + Kokkos::View coords_device( + "coords_device", coords.extent(0), coords.extent(1)); + Kokkos::parallel_for( + "copy_coords", Kokkos::RangePolicy<>(0, coords.extent(0)), + KOKKOS_LAMBDA(int i) { + for (unsigned d = 0; d < coords.extent(1); ++d) { + coords_device(i, d) = coords(i, d); + } + }); + auto coords_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), coords_device); + + ReversePartitionMap2 reverse_partition; + LO n = static_cast(owned.extent(0)); + std::array coord{}; + auto overlap_mask_view = overlap_mask.GetMask(layout); + + for (LO local_index = 0; local_index < n; ++local_index) { + if (!owned[local_index]) + continue; + + if (!overlap_mask_view[local_index]) + continue; + + for (int d = 0; d < mesh_dim; ++d) + coord[d] = coords_host(local_index, d); + for (int d = mesh_dim; d < 3; ++d) + coord[d] = 0.0; + + int mesh_ent_dim = GetMeshEntityDim(local_index, ent_offsets); + LO class_dim = class_dims[local_index]; + LO class_id = class_ids[local_index]; + + auto dr = std::visit(GetRank{class_id, class_dim, coord}, partition); + reverse_partition[dr].indices.emplace_back(local_index); + + for (size_t e = static_cast(mesh_ent_dim) + 1; e < ent_offsets_len; + ++e) { + reverse_partition[dr].ent_offsets[e] += 1; + } + } + return reverse_partition; +} + +} // namespace + +ExchangePlan GenericFieldExchangePlanner::BuildExchangePlan( + const FieldLayout& layout, const redev::Partition& partition, + const OverlapMask* overlap_mask) const +{ + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(overlap_mask != nullptr); + auto gids = layout.GetGidsHost(); + + const ReversePartitionMap2 reverse_partition = + BuildReversePartitionMap(layout, partition, *overlap_mask); + + ExchangePlan plan; + + auto out_msg = ConstructOutMessage(reverse_partition); + plan.dest_ranks = std::move(out_msg.dest); + plan.offsets = std::move(out_msg.offset); + + int length = 0; + plan.permutation = + ConstructPermutation(reverse_partition, gids.size(), &length); + plan.msg_size = static_cast(length); + + return plan; +} + +ExchangePlan GenericFieldExchangePlanner::BuildReceivePlan( + const FieldLayout& layout, GlobalIDView received_gids, + int rank, int nproc, const redev::InMessageLayout& in_message_layout) const +{ + PCMS_FUNCTION_TIMER; + auto gids = layout.GetGidsHost(); + auto ent_offsets = layout.GetEntOffsets(); + + ExchangePlan plan; + auto out_msg = ConstructOutMessage(rank, nproc, in_message_layout); + plan.dest_ranks = std::move(out_msg.dest); + plan.offsets = std::move(out_msg.offset); + plan.permutation = ConstructPermutation(gids, received_gids, ent_offsets); + plan.msg_size = received_gids.size(); + return plan; +} + +void GenericFieldExchangePlanner::FillGidMessage( + const FieldLayout& layout, const ExchangePlan& plan, + Rank1View gid_message, + const OverlapMask* overlap_mask) const +{ + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(static_cast(gid_message.size()) == plan.msg_size); + + auto gids = layout.GetGidsHost(); + auto owned = layout.GetOwnedHost(); + auto ent_offsets = layout.GetEntOffsets(); + auto offsets = Rank1View( + plan.offsets.data(), plan.offsets.size()); + + // Participation must match BuildReversePartitionMap (owned AND in overlap); + // otherwise owned-but-non-overlap DOFs inflate the per-rank counts written + // here and corrupt the message. A default all-true mask is used when none is + // provided so behavior is unchanged for callers without an overlap mask. + OverlapMask default_mask(static_cast(gids.size())); + auto overlap = + (overlap_mask ? *overlap_mask : default_mask).GetMask(layout); + + std::vector per_rank_offsets(plan.dest_ranks.size()); + + for (LO local_index = 0; local_index < static_cast(gids.size()); + ++local_index) { + if (!owned[local_index] || !overlap[local_index]) + continue; + + LO perm_index = plan.permutation[local_index]; + gid_message[perm_index] = gids[local_index]; + + auto block_index = GetMessageBlockIndex(perm_index, offsets); + int mesh_ent_dim = GetMeshEntityDim(local_index, ent_offsets); + for (size_t e = static_cast(mesh_ent_dim) + 1; e < ent_offsets_len; + ++e) { + per_rank_offsets[block_index][e] += 1; + } + } + + for (size_t block_index = 0; block_index < per_rank_offsets.size(); + ++block_index) { + auto header_offset = static_cast(plan.offsets[block_index]); + for (size_t e = 0; e < ent_offsets_len; ++e) { + gid_message[header_offset + e] = + static_cast(per_rank_offsets[block_index][e]); + } + } +} + +} // namespace pcms diff --git a/src/pcms/coupler/field_exchange_planner.h b/src/pcms/coupler/field_exchange_planner.h new file mode 100644 index 000000000..d6278c932 --- /dev/null +++ b/src/pcms/coupler/field_exchange_planner.h @@ -0,0 +1,60 @@ +#ifndef PCMS_FIELD_EXCHANGE_PLANNER_H +#define PCMS_FIELD_EXCHANGE_PLANNER_H + +#include "pcms/field/field_layout.h" +#include "pcms/utility/arrays.h" +#include "overlap_mask.h" +#include +#include + +namespace pcms +{ + +struct ExchangePlan +{ + redev::LOs dest_ranks; + redev::LOs offsets; + std::vector permutation; + size_t msg_size = 0; +}; + +class FieldExchangePlanner +{ +public: + virtual ExchangePlan BuildExchangePlan( + const FieldLayout& layout, const redev::Partition& partition, + const OverlapMask* overlap_mask = nullptr) const = 0; + + virtual ExchangePlan BuildReceivePlan( + const FieldLayout& layout, GlobalIDView received_gids, + int rank, int nproc, + const redev::InMessageLayout& in_message_layout) const = 0; + + virtual void FillGidMessage( + const FieldLayout& layout, const ExchangePlan& plan, + Rank1View gid_message, + const OverlapMask* overlap_mask = nullptr) const = 0; + + virtual ~FieldExchangePlanner() noexcept = default; +}; + +class GenericFieldExchangePlanner : public FieldExchangePlanner +{ +public: + ExchangePlan BuildExchangePlan( + const FieldLayout& layout, const redev::Partition& partition, + const OverlapMask* overlap_mask = nullptr) const override; + + ExchangePlan BuildReceivePlan( + const FieldLayout& layout, GlobalIDView received_gids, + int rank, int nproc, + const redev::InMessageLayout& in_message_layout) const override; + + void FillGidMessage( + const FieldLayout& layout, const ExchangePlan& plan, + Rank1View gid_message, + const OverlapMask* overlap_mask = nullptr) const override; +}; + +} // namespace pcms +#endif // PCMS_FIELD_EXCHANGE_PLANNER_H diff --git a/src/pcms/coupler/field_layout_communicator.cpp b/src/pcms/coupler/field_layout_communicator.cpp new file mode 100644 index 000000000..3a0d50c71 --- /dev/null +++ b/src/pcms/coupler/field_layout_communicator.cpp @@ -0,0 +1,128 @@ +#include "pcms/coupler/field_layout_communicator.h" + +namespace pcms +{ + +FieldLayoutCommunicator::FieldLayoutCommunicator( + const std::string& name, MPI_Comm mpi_comm, redev::Redev& redev, + redev::Channel& channel, const FieldLayout& layout, bool own_mpi_comm) + : FieldLayoutCommunicator(name, mpi_comm, redev, channel, layout, + std::make_unique(), + own_mpi_comm) +{ +} + +FieldLayoutCommunicator::FieldLayoutCommunicator( + const std::string& name, MPI_Comm mpi_comm, redev::Redev& redev, + redev::Channel& channel, const FieldLayout& layout, + std::unique_ptr planner, bool own_mpi_comm, + const OverlapMask* overlap_mask) + : mpi_comm_(mpi_comm), + channel_(channel), + layout_(layout), + name_(name), + redev_(redev), + planner_(std::move(planner)), + overlap_mask_(overlap_mask ? std::make_unique(*overlap_mask) + : std::make_unique( + layout.GetGidsHost().size())), + own_mpi_comm_(own_mpi_comm) +{ + gid_comm_ = channel.CreateComm(name_ + "_gids", mpi_comm_); + if (mpi_comm != MPI_COMM_NULL) { + UpdateLayout(); + } else { + UpdateLayoutNull(); + } +} + +Rank1View +FieldLayoutCommunicator::GetPermutationArray() const +{ + return make_const_array_view(plan_.permutation); +} + +const std::string& FieldLayoutCommunicator::GetName() const +{ + return name_; +} + +const FieldLayout& FieldLayoutCommunicator::GetLayout() const +{ + return layout_; +} + +size_t FieldLayoutCommunicator::GetMsgSize() const +{ + return plan_.msg_size; +} + +redev::Channel& FieldLayoutCommunicator::GetChannel() const +{ + return channel_; +} + +MPI_Comm& FieldLayoutCommunicator::GetMPIComm() +{ + return mpi_comm_; +} + +void FieldLayoutCommunicator::UpdateLayout() +{ + PCMS_FUNCTION_TIMER; + if (redev_.GetProcessType() == redev::ProcessType::Client) { + // overlap_mask_ is always non-null (created in constructor if not provided) + plan_ = planner_->BuildExchangePlan( + layout_, redev::Partition{redev_.GetPartition()}, overlap_mask_.get()); + gid_comm_.SetOutMessageLayout(plan_.dest_ranks, plan_.offsets); + std::vector gid_message(plan_.msg_size); + planner_->FillGidMessage( + layout_, plan_, + Rank1View(gid_message.data(), gid_message.size()), + overlap_mask_.get()); + + channel_.BeginSendCommunicationPhase(); + gid_comm_.Send(gid_message.data()); + channel_.EndSendCommunicationPhase(); + } else { + channel_.BeginReceiveCommunicationPhase(); + auto recv_gids = gid_comm_.Recv(); + channel_.EndReceiveCommunicationPhase(); + + int rank, nproc; + MPI_Comm_rank(mpi_comm_, &rank); + MPI_Comm_size(mpi_comm_, &nproc); + + const auto in_message_layout = gid_comm_.GetInMessageLayout(); + GlobalIDView recv_gids_view(recv_gids.data(), + recv_gids.size()); + plan_ = planner_->BuildReceivePlan(layout_, recv_gids_view, rank, nproc, + in_message_layout); + } +} + +void FieldLayoutCommunicator::UpdateLayoutNull() +{ + PCMS_FUNCTION_TIMER; + if (redev_.GetProcessType() == redev::ProcessType::Client) { + channel_.BeginSendCommunicationPhase(); + channel_.EndSendCommunicationPhase(); + } else { + channel_.BeginReceiveCommunicationPhase(); + channel_.EndReceiveCommunicationPhase(); + } +} + +void FieldLayoutCommunicator::SetOverlapMask(std::unique_ptr mask) +{ + overlap_mask_ = std::move(mask); +} + +FieldLayoutCommunicator::~FieldLayoutCommunicator() +{ + if (own_mpi_comm_ && mpi_comm_ != MPI_COMM_NULL) { + MPI_Comm_free(&mpi_comm_); + } +} + +} // namespace pcms diff --git a/src/pcms/coupler/field_layout_communicator.h b/src/pcms/coupler/field_layout_communicator.h new file mode 100644 index 000000000..bc0179f35 --- /dev/null +++ b/src/pcms/coupler/field_layout_communicator.h @@ -0,0 +1,69 @@ +#ifndef FIELD_LAYOUT_COMMUNICATOR_H_ +#define FIELD_LAYOUT_COMMUNICATOR_H_ + +#include "pcms/field/field_layout.h" +#include "field_exchange_planner.h" +#include "overlap_mask.h" +#include "pcms/utility/profile.h" +#include "pcms/utility/arrays.h" +#include +#include + +namespace pcms +{ + +class FieldLayoutCommunicator +{ +public: + FieldLayoutCommunicator(const std::string& name, MPI_Comm mpi_comm, + redev::Redev& redev, redev::Channel& channel, + const FieldLayout& layout, bool own_mpi_comm = false); + + FieldLayoutCommunicator(const std::string& name, MPI_Comm mpi_comm, + redev::Redev& redev, redev::Channel& channel, + const FieldLayout& layout, + std::unique_ptr planner, + bool own_mpi_comm = false, + const OverlapMask* overlap_mask = nullptr); + + Rank1View GetPermutationArray() const; + + const std::string& GetName() const; + + const FieldLayout& GetLayout() const; + + size_t GetMsgSize() const; + + redev::Channel& GetChannel() const; + + MPI_Comm& GetMPIComm(); + + template + void SetOutMessageLayout(redev::BidirectionalComm& comm) + { + comm.SetOutMessageLayout(plan_.dest_ranks, plan_.offsets); + } + + void UpdateLayout(); + + void UpdateLayoutNull(); + + void SetOverlapMask(std::unique_ptr mask); + + ~FieldLayoutCommunicator(); + +private: + MPI_Comm mpi_comm_; + redev::Channel& channel_; + redev::BidirectionalComm gid_comm_; + ExchangePlan plan_; + const FieldLayout& layout_; + std::string name_; + redev::Redev& redev_; + std::unique_ptr planner_; + std::unique_ptr overlap_mask_; + bool own_mpi_comm_ = false; +}; +} // namespace pcms + +#endif // FIELD_LAYOUT_COMMUNICATOR_H_ diff --git a/src/pcms/coupler/field_serializer.h b/src/pcms/coupler/field_serializer.h new file mode 100644 index 000000000..a5905b424 --- /dev/null +++ b/src/pcms/coupler/field_serializer.h @@ -0,0 +1,65 @@ +#ifndef PCMS_FIELD_SERIALIZER_H +#define PCMS_FIELD_SERIALIZER_H + +#include "pcms/field/field.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/types.h" +#include + +namespace pcms +{ + +template +class FieldSerializer +{ +public: + virtual int Serialize(const FieldData& field, const FieldLayout& layout, + Rank1View buffer, + Rank1View permutation) const + { + auto data = field.GetDOFHolderDataHost(); + auto owned = layout.GetOwnedHost(); + LO counter = 0; + for (LO i = 0; i < static_cast(data.size()); ++i) { + if (!owned[i] || permutation[i] < 0) { + continue; + } + ++counter; + if (!buffer.empty()) { + PCMS_ALWAYS_ASSERT(static_cast(permutation[i]) < buffer.size()); + buffer[permutation[i]] = data[i]; + } + } + return counter; + } + + virtual void Deserialize( + FieldData& field, const FieldLayout& layout, + Rank1View buffer, + Rank1View permutation) const + { + // Seed from the field's current values so DOFs that were not received are + // preserved rather than overwritten. This matters for masked coupling: a + // permutation entry < 0 (kUnreceivedDof) means the sender did not include + // that DOF's GID, so its existing value must be left untouched instead of + // reading buffer[0]. + auto current = field.GetDOFHolderDataHost(); + Kokkos::View sorted("sorted", permutation.size()); + for (LO i = 0; i < static_cast(sorted.size()); ++i) { + sorted[i] = current[i]; + } + auto owned = layout.GetOwnedHost(); + for (LO i = 0; i < static_cast(sorted.size()); ++i) { + if (owned[i] && permutation[i] >= 0) + sorted[i] = buffer[permutation[i]]; + } + field.SetDOFHolderDataHost(make_const_array_view(sorted)); + } + + virtual ~FieldSerializer() noexcept = default; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_SERIALIZER_H diff --git a/src/pcms/coupler/overlap_mask.h b/src/pcms/coupler/overlap_mask.h new file mode 100644 index 000000000..fef2d7537 --- /dev/null +++ b/src/pcms/coupler/overlap_mask.h @@ -0,0 +1,77 @@ +#ifndef PCMS_OVERLAP_MASK_H +#define PCMS_OVERLAP_MASK_H + +#include "pcms/field/field_layout.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/assert.h" +#include +#include + +namespace pcms +{ + +struct OverlapMask +{ + Kokkos::View is_overlap_; + std::function in_overlap_func_; + + // Default constructor: all DOFs are in overlap + OverlapMask(size_t size) : is_overlap_("overlap_info", size) + { + Kokkos::deep_copy(is_overlap_, true); + } + + // Construct from a function that determines overlap based on classification + OverlapMask(size_t size, std::function in_overlap) + : is_overlap_("overlap_info", size), in_overlap_func_(std::move(in_overlap)) + { + Kokkos::deep_copy(is_overlap_, true); + } + + // Construct from a precomputed per-DOF-holder host mask (e.g. an MFEM + // subdomain selected by element attribute). Indexed by local DOF holder. + OverlapMask(size_t size, Kokkos::View is_overlap_host) + : is_overlap_("overlap_info", size) + { + PCMS_ALWAYS_ASSERT(is_overlap_host.extent(0) == size); + Kokkos::deep_copy(is_overlap_, is_overlap_host); + } + + // Construct from Omega_h host array + OverlapMask(size_t size, Omega_h::HostRead is_overlap_host) + : is_overlap_("overlap_info", size) + { + for (size_t i = 0; i < size; ++i) { + is_overlap_[i] = static_cast(is_overlap_host[i]); + } + } + + // Construct from Omega_h device array + OverlapMask(size_t size, Omega_h::Read is_overlap_device) + : is_overlap_("overlap_info", size) + { + auto is_overlap_host = Omega_h::HostRead(is_overlap_device); + for (size_t i = 0; i < size; ++i) { + is_overlap_[i] = static_cast(is_overlap_host[i]); + } + } + + // Get the mask, evaluating the function if needed + Rank1View GetMask( + const FieldLayout& layout) const + { + if (in_overlap_func_) { + auto class_dims = layout.GetDOFHolderClassificationDimensionsHost(); + auto class_ids = layout.GetDOFHolderClassificationIdsHost(); + for (size_t i = 0; i < is_overlap_.extent(0); ++i) { + is_overlap_[i] = + static_cast(in_overlap_func_(class_dims[i], class_ids[i])); + } + } + return make_const_array_view(is_overlap_); + } +}; + +} // namespace pcms + +#endif // PCMS_OVERLAP_MASK_H diff --git a/src/pcms/partition.h b/src/pcms/coupler/partition.h similarity index 98% rename from src/pcms/partition.h rename to src/pcms/coupler/partition.h index d18b68f30..baba1751b 100644 --- a/src/pcms/partition.h +++ b/src/pcms/coupler/partition.h @@ -3,6 +3,7 @@ #include "pcms/utility/common.h" #include "pcms/utility/profile.h" #include "pcms/utility/types.h" +#include #include namespace pcms diff --git a/src/pcms/coupler/serializer/xgc.h b/src/pcms/coupler/serializer/xgc.h new file mode 100644 index 000000000..d31b9b46c --- /dev/null +++ b/src/pcms/coupler/serializer/xgc.h @@ -0,0 +1,89 @@ +#ifndef PCMS_XGC_FIELD_SERIALIZER_H +#define PCMS_XGC_FIELD_SERIALIZER_H + +#include "pcms/field/data/xgc.h" +#include "pcms/coupler/field_serializer.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/mpi_type.h" +#include + +namespace pcms +{ + +template +class XGCFieldSerializer : public FieldSerializer +{ +public: + explicit XGCFieldSerializer(MPI_Comm plane_comm, + bool rank_participates = true) + : plane_comm_(plane_comm), rank_participates_(rank_participates) + { + } + + int Serialize(const FieldData& field, const FieldLayout& layout, + Rank1View buffer, + Rank1View permutation) const override + { + if (!rank_participates_) { + return 0; + } + + auto const* xgc_field = dynamic_cast*>(&field); + if (!xgc_field) { + throw pcms_error("XGCFieldSerializer::Serialize: incompatible FieldData"); + } + + auto data = xgc_field->GetDOFHolderDataHost(); + auto owned = layout.GetOwnedHost(); + if (buffer.size() > 0) { + for (LO i = 0; i < static_cast(data.size()); ++i) { + if (owned[i]) { + buffer[permutation[i]] = data[i]; + } + } + } + return static_cast(buffer.size()); + } + + void Deserialize( + FieldData& field, const FieldLayout& layout, + Rank1View buffer, + Rank1View permutation) const override + { + auto* xgc_field = dynamic_cast*>(&field); + if (!xgc_field) { + throw pcms_error( + "XGCFieldSerializer::Deserialize: incompatible FieldData"); + } + + auto current = xgc_field->GetDOFHolderDataHost(); + auto owned = layout.GetOwnedHost(); + std::vector full_data(current.size()); + for (size_t i = 0; i < current.size(); ++i) { + full_data[i] = current[i]; + } + if (rank_participates_) { + for (LO i = 0; i < static_cast(current.size()); ++i) { + // permutation[i] < 0 (kUnreceivedDof) => this DOF's GID was not in the + // received message; preserve its current value (and avoid buffer[-1]). + if (owned[i] && permutation[i] >= 0) { + full_data[i] = buffer[permutation[i]]; + } + } + } + + MPI_Bcast(full_data.data(), static_cast(full_data.size()), + pcms::GetMPIType(T{}), 0, plane_comm_); + + xgc_field->SetDOFHolderDataHost( + Rank1View(full_data.data(), full_data.size())); + } + +private: + MPI_Comm plane_comm_; + bool rank_participates_; +}; + +} // namespace pcms + +#endif // PCMS_XGC_FIELD_SERIALIZER_H diff --git a/src/pcms/coupler2.cpp b/src/pcms/coupler2.cpp deleted file mode 100644 index cc25ff97b..000000000 --- a/src/pcms/coupler2.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "coupler2.h" - -namespace pcms -{ - -FieldLayoutCommunicator& Application2::GetLayoutCommunicator( - const FieldLayout& layout) -{ - PCMS_FUNCTION_TIMER; - auto it = field_layout_communicators_.find(&layout); - if (it != field_layout_communicators_.end()) { - return *it->second; - } else { - std::cerr << "Field added with external layout\n"; - std::terminate(); - } -} - -const FieldLayout& Application2::AddLayout(std::string name, - std::unique_ptr layout) -{ - layouts_.push_back(std::move(layout)); - const FieldLayout& layout_ref = *layouts_.back(); - field_layout_communicators_.emplace( - &layout_ref, std::make_unique( - name, mpi_comm_, redev_, channel_, layout_ref)); - return layout_ref; -} - -void Application2::AddField(std::string name, OwnedFieldPtr field, - bool participates) -{ - PCMS_FUNCTION_TIMER; - - fields_.push_back(std::move(field)); - - FieldPtr field_ptr = GetRawPointer(fields_.back()); - - FieldCommunicator2Ptr field_communicator = std::visit( - [this, name](auto* field_ptr) -> FieldCommunicator2Ptr { - using T = std::remove_pointer_t::value_type; - FieldLayoutCommunicator& layout_communicator = - GetLayoutCommunicator(field_ptr->GetLayout()); - return std::make_unique>(layout_communicator, - *field_ptr); - }, - field_ptr); - - auto [it, inserted] = - field_communicators_.insert_or_assign(name, std::move(field_communicator)); - - if (!inserted) { - throw pcms_error("Field with this name already exists"); - } -} - -} // namespace pcms \ No newline at end of file diff --git a/src/pcms/coupler2.h b/src/pcms/coupler2.h deleted file mode 100644 index 1ee0e7633..000000000 --- a/src/pcms/coupler2.h +++ /dev/null @@ -1,176 +0,0 @@ -#ifndef COUPLER2_H_ -#define COUPLER2_H_ - -#include "field.h" -#include "field_communicator2.h" -#include "field_layout.h" -#include "field_layout_communicator.h" -#include "pcms/field_layout.h" -#include "pcms/field_layout_communicator.h" -#include "pcms/field_communicator2.h" -#include "pcms/utility/assert.h" -#include "pcms/utility/common.h" -#include "pcms/utility/profile.h" -#include - -namespace pcms -{ - -// to avoid having any redev:: types in the user interface -using ProcessType = redev::ProcessType; - -class Application2 -{ -public: - Application2(std::string name, MPI_Comm comm, redev::Redev& redev, - adios2::Params params, redev::TransportType transport_type, - std::string path) - : mpi_comm_(comm), - redev_(redev), - channel_{redev_.CreateAdiosChannel(std::move(name), std::move(params), - transport_type, std::move(path))} - { - PCMS_FUNCTION_TIMER; - } - - const FieldLayout& AddLayout(std::string name, - std::unique_ptr layout); - - // FIXME should take a file path for the parameters, not take adios2 params. - // These fields are supposed to be agnostic to adios2... - void AddField(std::string name, OwnedFieldPtr field, - bool participates = true); - - void SendField(const std::string& name, - redev::Mode mode = redev::Mode::Synchronous) - { - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(InSendPhase()); - FieldCommunicator2Ptr& communicator = - detail::find_or_error(name, field_communicators_); - std::visit( - [mode](auto& field_communicator) { field_communicator->Send(mode); }, - communicator); - }; - void ReceiveField(const std::string& name, - redev::Mode mode = redev::Mode::Synchronous) - { - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(InReceivePhase()); - std::visit( - [mode](auto& field_communicator) { field_communicator->Receive(); }, - detail::find_or_error(name, field_communicators_)); - }; - [[nodiscard]] bool InSendPhase() const noexcept - { - PCMS_FUNCTION_TIMER; - return channel_.InSendCommunicationPhase(); - } - [[nodiscard]] bool InReceivePhase() const noexcept - { - PCMS_FUNCTION_TIMER; - return channel_.InReceiveCommunicationPhase(); - } - void BeginSendPhase() - { - PCMS_FUNCTION_TIMER; - channel_.BeginSendCommunicationPhase(); - } - void EndSendPhase() - { - PCMS_FUNCTION_TIMER; - channel_.EndSendCommunicationPhase(); - } - void BeginReceivePhase() - { - PCMS_FUNCTION_TIMER; - channel_.BeginReceiveCommunicationPhase(); - } - void EndReceivePhase() - { - PCMS_FUNCTION_TIMER; - channel_.EndReceiveCommunicationPhase(); - } - - template - auto SendPhase(const Func& func, Args&&... args) - { - PCMS_FUNCTION_TIMER; - return channel_.SendPhase(func, std::forward(args)...); - } - template - auto ReceivePhase(const Func& func, Args&&... args) - { - PCMS_FUNCTION_TIMER; - return channel_.ReceivePhase(func, std::forward(args)...); - } - -private: - FieldLayoutCommunicator& GetLayoutCommunicator(const FieldLayout& layout); - - MPI_Comm mpi_comm_; - redev::Redev& redev_; - redev::Channel channel_; - std::vector> layouts_; - std::vector fields_; - // map is used rather than unordered_map because we give pointers to the - // internal data and rehash of unordered_map can cause pointer invalidation. - // map is less cache friendly, but pointers are not invalidated. - std::map field_communicators_; - std::map> - field_layout_communicators_; -}; - -class Coupler2 -{ -private: - redev::Redev SetUpRedev(bool isServer, redev::Partition partition) - { - if (isServer) - return redev::Redev(mpi_comm_, std::move(partition), ProcessType::Server); - else - return redev::Redev(mpi_comm_); - } - -public: - Coupler2(std::string name, MPI_Comm comm, bool isServer, - redev::Partition partition) - : name_(std::move(name)), - mpi_comm_(comm), - redev_(SetUpRedev(isServer, std::move(partition))) - { - PCMS_FUNCTION_TIMER; - } - Application2* AddApplication( - std::string name, std::string path = "", - redev::TransportType transport_type = redev::TransportType::BP4, - adios2::Params params = {{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}) - { - PCMS_FUNCTION_TIMER; - auto key = path + name; - auto [it, inserted] = applications_.try_emplace( - key, std::move(name), mpi_comm_, redev_, std::move(params), - transport_type, std::move(path)); - if (!inserted) { - std::cerr << "Application with name " << name << "already exists!\n"; - std::terminate(); - } - return &(it->second); - } - - [[nodiscard]] const redev::Partition& GetPartition() const noexcept - { - return redev_.GetPartition(); - } - -private: - std::string name_; - MPI_Comm mpi_comm_; - redev::Redev redev_; - // gather and scatter operations have reference to internal fields - std::map applications_; -}; - -} // namespace pcms - -#endif // COUPLER2_H_ \ No newline at end of file diff --git a/src/pcms/create_field.cpp b/src/pcms/create_field.cpp deleted file mode 100644 index 96b44ea73..000000000 --- a/src/pcms/create_field.cpp +++ /dev/null @@ -1,121 +0,0 @@ -#include "create_field.h" -#include "adapter/meshfields/mesh_fields_adapter2.h" -#include "adapter/meshfields/mesh_fields_adapter_layout.h" -#include "adapter/uniform_grid/uniform_grid_field.h" -#include "adapter/uniform_grid/uniform_grid_field_layout.h" -#include "uniform_grid.h" -#include "point_search.h" - -#include -#include - -namespace pcms -{ - -std::unique_ptr CreateLagrangeLayout( - Omega_h::Mesh& mesh, int order, int num_components, - CoordinateSystem coordinate_system, std::string global_id_name) -{ - - std::array nodes_per_dim; - - switch (order) { - case 1: nodes_per_dim = {1, 0, 0, 0}; break; - case 2: nodes_per_dim = {1, 1, 0, 0}; break; - default: throw std::runtime_error("Unimplemented order"); - } - - return std::make_unique( - mesh, nodes_per_dim, num_components, coordinate_system, global_id_name); -} - -template <> -std::pair>, - std::unique_ptr>> -CreateUniformGridBinaryFieldFromGrid<2>(Omega_h::Mesh& mesh, - UniformGrid<2>& grid) -{ - constexpr unsigned dim = 2; - - // Get total number of vertices - const LO num_vertices = (grid.divisions[0] + 1) * (grid.divisions[1] + 1); - - // Create GridPointSearch for point-in-mesh queries - GridPointSearch2D point_search(mesh, grid.divisions[0], grid.divisions[1]); - - // Create array of grid vertex points - Kokkos::View vertices("vertices", num_vertices); - auto vertices_h = Kokkos::create_mirror_view(vertices); - - // Fill vertex coordinates - const Real dx = grid.edge_length[0] / grid.divisions[0]; - const Real dy = grid.edge_length[1] / grid.divisions[1]; - - for (LO j = 0; j <= grid.divisions[1]; ++j) { - for (LO i = 0; i <= grid.divisions[0]; ++i) { - const LO vertex_id = j * (grid.divisions[0] + 1) + i; - vertices_h(vertex_id, 0) = grid.bot_left[0] + i * dx; - vertices_h(vertex_id, 1) = grid.bot_left[1] + j * dy; - } - } - - // Copy to device - Kokkos::deep_copy(vertices, vertices_h); - - // Perform point search - auto results = point_search(vertices); - fprintf(stderr, "Completed point-in-mesh search for %d vertices.\n", - num_vertices); - - // Copy results back to host - auto results_h = Kokkos::create_mirror_view(results); - Kokkos::deep_copy(results_h, results); - fprintf(stderr, "Copied point search results back to host.\n"); - - // Create UniformGridFieldLayout and field - auto layout = std::make_unique>( - grid, 1, CoordinateSystem::Cartesian); - auto field = std::make_unique>(*layout); - - // Create binary data as Real values (0.0 or 1.0) - Kokkos::View binary_data("binary_data", num_vertices); - for (LO i = 0; i < num_vertices; ++i) { - binary_data(i) = (results_h(i).element_id >= 0) ? 1.0 : 0.0; - } - fprintf(stderr, "Generated binary inside/outside data for grid vertices.\n"); - - // Set the DOF holder data - Rank1View data_view(binary_data.data(), - binary_data.extent(0)); - field->SetDOFHolderData(data_view); - fprintf(stderr, "Set binary data on UniformGridField.\n"); - - return {std::move(layout), std::move(field)}; -} - -template <> -std::pair>, - std::unique_ptr>> -CreateUniformGridBinaryField<2>(Omega_h::Mesh& mesh, - const std::array& divisions) -{ - constexpr unsigned dim = 2; - - // Create the uniform grid from the mesh - auto grid = CreateUniformGridFromMesh(mesh, divisions); - - // Delegate to the grid-based implementation - return CreateUniformGridBinaryFieldFromGrid<2>(mesh, grid); -} - -template <> -std::pair>, - std::unique_ptr>> -CreateUniformGridBinaryField<2>(Omega_h::Mesh& mesh, LO cells_per_dim) -{ - std::array divisions; - divisions.fill(cells_per_dim); - return CreateUniformGridBinaryField<2>(mesh, divisions); -} - -} // namespace pcms diff --git a/src/pcms/create_field.h b/src/pcms/create_field.h deleted file mode 100644 index fe2c58f40..000000000 --- a/src/pcms/create_field.h +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef CREATE_FIELD_H_ -#define CREATE_FIELD_H_ - -#include "adapter/meshfields/mesh_fields_adapter_layout.h" -#include "adapter/uniform_grid/uniform_grid_field.h" -#include "adapter/uniform_grid/uniform_grid_field_layout.h" -#include "field_layout.h" -#include "field.h" -#include "coordinate_system.h" -#include "utility/types.h" -#include "uniform_grid.h" - -#include -#include -#include -#include - -namespace pcms -{ -std::unique_ptr CreateLagrangeLayout( - Omega_h::Mesh& mesh, int order, int num_components, - CoordinateSystem coordinate_system, std::string global_id_name = "global"); - -/** - * \brief Create a binary field on a uniform grid indicating inside/outside mesh - * - * This function creates a uniform grid from the mesh and generates a binary - * field where each grid vertex is assigned: - * - 1 if the vertex is inside the original mesh - * - 0 if the vertex is outside the original mesh - * - * Uses GridPointSearch to determine if a point lies within the mesh domain. - * Currently only supports 2D meshes. - * - * \tparam dim Spatial dimension of the mesh (currently only dim=2 is supported) - * \param mesh The Omega_h mesh to create the grid from - * \param divisions Array specifying the number of cells in each dimension - * \return std::pair containing the layout and field (layout must outlive field) - * - * \note The returned field has vertex-centered data with values 0.0 or 1.0. - * Vertex values are ordered according to the grid's internal indexing. - * The layout must be kept alive as long as the field is used. - */ -template -std::pair>, - std::unique_ptr>> -CreateUniformGridBinaryField(Omega_h::Mesh& mesh, - const std::array& divisions); - -/** - * \brief Create a binary field with equal divisions in all dimensions - * - * Convenience function that creates a uniform grid with the same number of - * cells in each dimension and generates the binary inside/outside field. - * - * \tparam dim Spatial dimension of the mesh (currently only dim=2 is supported) - * \param mesh The Omega_h mesh to create the grid from - * \param cells_per_dim Number of cells per dimension (same for all dimensions) - * \return std::pair containing the layout and field (layout must outlive field) - */ -template -std::pair>, - std::unique_ptr>> -CreateUniformGridBinaryField(Omega_h::Mesh& mesh, LO cells_per_dim); - -/** - * \brief Create a binary field on a given uniform grid - * - * This function takes a pre-defined uniform grid and generates a binary field - * where each grid vertex is assigned: - * - 1 if the vertex is inside the mesh - * - 0 if the vertex is outside the mesh - * - * This allows testing with custom grids that may extend beyond the mesh - * boundaries. - * - * \tparam dim Spatial dimension (currently only dim=2 is supported) - * \param mesh The Omega_h mesh to test against - * \param grid The uniform grid to evaluate - * \return std::pair containing the layout and field (layout must outlive field) - */ -template -std::pair>, - std::unique_ptr>> -CreateUniformGridBinaryFieldFromGrid(Omega_h::Mesh& mesh, - UniformGrid& grid); - -} // namespace pcms - -#endif // CREATE_FIELD_H_ diff --git a/src/pcms/discretization/CMakeLists.txt b/src/pcms/discretization/CMakeLists.txt new file mode 100644 index 000000000..1afa1f201 --- /dev/null +++ b/src/pcms/discretization/CMakeLists.txt @@ -0,0 +1,69 @@ +set( + PCMS_DISCRETIZATION_HEADERS + discretization.h + discretization/xgc_reverse_classification.h + discretization/empty.hpp + discretization/point_cloud.hpp + discretization/uniform_grid.hpp + discretization/xgc.hpp +) +if (PCMS_ENABLE_OMEGA_H) + list(APPEND PCMS_DISCRETIZATION_HEADERS discretization/omega_h.hpp) +endif() + +set( + PCMS_DISCRETIZATION_SOURCES + discretization/empty.cpp + discretization/point_cloud.cpp + discretization/xgc_reverse_classification.cpp + discretization/xgc.cpp +) +if (PCMS_ENABLE_OMEGA_H) + list(APPEND PCMS_DISCRETIZATION_SOURCES discretization/omega_h.cpp) +endif() + +add_library(pcms_discretization ${PCMS_DISCRETIZATION_SOURCES}) +add_library(pcms::discretization ALIAS pcms_discretization) +target_sources(pcms_discretization PUBLIC + FILE_SET field_discretization + TYPE HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. + FILES ${PCMS_DISCRETIZATION_HEADERS}) +target_include_directories( + pcms_discretization + PUBLIC + # include path should be pcms/discretization/ + "$" + # include path to search for pcms/config.h + "$" + "$") +target_link_libraries(pcms_discretization PUBLIC Kokkos::kokkos pcms::utility) + +if (PCMS_ENABLE_OMEGA_H) + target_link_libraries(pcms_discretization PRIVATE Omega_h::omega_h) +endif() + +target_compile_features(pcms_discretization PUBLIC cxx_std_20) + +set_target_properties( + pcms_discretization PROPERTIES OUTPUT_NAME pcmsdiscretization EXPORT_NAME + discretization +) + +## export the library +install( + TARGETS pcms_discretization + EXPORT pcms_discretization-targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + FILE_SET field_discretization DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms +) + +install( + EXPORT pcms_discretization-targets + NAMESPACE pcms:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms +) diff --git a/src/pcms/discretization/discretization.h b/src/pcms/discretization/discretization.h new file mode 100644 index 000000000..2383ff5b2 --- /dev/null +++ b/src/pcms/discretization/discretization.h @@ -0,0 +1,33 @@ +#ifndef PCMS_DISCRETIZATION_DISCRETIZATION_H +#define PCMS_DISCRETIZATION_DISCRETIZATION_H + +#include "pcms/utility/arrays.h" +#include "pcms/utility/entity_types.h" + +namespace pcms +{ + +using ClassificationDimension = int8_t; +using ClassificationId = LO; + +class Discretization +{ +public: + virtual bool SameEntities(const Discretization& other) const noexcept = 0; + + virtual int GetDimension() const = 0; + + virtual LO GetNumEntities(int entity_dim) const = 0; + + virtual Rank1View + GetEntityClassificationDimensions(int entity_dim) const = 0; + + virtual Rank1View + GetEntityClassificationIds(int entity_dim) const = 0; + + virtual ~Discretization() noexcept = default; +}; + +} // namespace pcms + +#endif // PCMS_DISCRETIZATION_DISCRETIZATION_H diff --git a/src/pcms/discretization/discretization/empty.cpp b/src/pcms/discretization/discretization/empty.cpp new file mode 100644 index 000000000..6f082141f --- /dev/null +++ b/src/pcms/discretization/discretization/empty.cpp @@ -0,0 +1,36 @@ +#include "pcms/discretization/discretization/empty.hpp" + +namespace pcms +{ + +EmptyDiscretization::EmptyDiscretization() {} + +bool EmptyDiscretization::SameEntities( + const Discretization& other) const noexcept +{ + return dynamic_cast(&other) != nullptr; +} + +int EmptyDiscretization::GetDimension() const +{ + return 0; +} + +LO EmptyDiscretization::GetNumEntities(int) const +{ + return 0; +} + +Rank1View +EmptyDiscretization::GetEntityClassificationDimensions(int) const +{ + return {}; +} + +Rank1View +EmptyDiscretization::GetEntityClassificationIds(int) const +{ + return {}; +} + +} // namespace pcms diff --git a/src/pcms/discretization/discretization/empty.hpp b/src/pcms/discretization/discretization/empty.hpp new file mode 100644 index 000000000..9cfb18991 --- /dev/null +++ b/src/pcms/discretization/discretization/empty.hpp @@ -0,0 +1,31 @@ +#ifndef PCMS_DISCRETIZATION_DISCRETIZATION_EMPTY_H +#define PCMS_DISCRETIZATION_DISCRETIZATION_EMPTY_H + +#include "pcms/discretization/discretization.h" + +#include + +namespace pcms +{ + +class EmptyDiscretization : public Discretization +{ +public: + EmptyDiscretization(); + + bool SameEntities(const Discretization& other) const noexcept override; + + int GetDimension() const override; + + LO GetNumEntities(int entity_dim) const override; + + Rank1View + GetEntityClassificationDimensions(int entity_dim) const override; + + Rank1View + GetEntityClassificationIds(int entity_dim) const override; +}; + +} // namespace pcms + +#endif // PCMS_DISCRETIZATION_DISCRETIZATION_EMPTY_H diff --git a/src/pcms/discretization/discretization/omega_h.cpp b/src/pcms/discretization/discretization/omega_h.cpp new file mode 100644 index 000000000..fcc6f5caa --- /dev/null +++ b/src/pcms/discretization/discretization/omega_h.cpp @@ -0,0 +1,69 @@ +#include "pcms/discretization/discretization/omega_h.hpp" +#include "pcms/utility/assert.h" +#include "pcms/utility/mesh_geometry.h" + +namespace pcms +{ + +static_assert(std::is_same_v, + "ClassificationId must match Omega_h::ClassId for direct views"); +static_assert( + std::is_same_v, + "ClassificationDimension must match Omega_h::I8 for direct views"); + +OmegaHDiscretization::OmegaHDiscretization(Omega_h::Mesh& mesh) : mesh_(mesh) +{ + for (int entity_dim = 0; entity_dim <= max_entity_dim_; ++entity_dim) { + class_dims_by_dim_[entity_dim] = Omega_h::Read(); + class_ids_by_dim_[entity_dim] = Omega_h::Read(); + } + + for (int entity_dim = 0; entity_dim <= mesh_.dim(); ++entity_dim) { + class_dims_by_dim_[entity_dim] = Omega_h::Read( + mesh_.get_array(entity_dim, "class_dim")); + class_ids_by_dim_[entity_dim] = Omega_h::Read( + mesh_.get_array(entity_dim, "class_id")); + } +} + +bool OmegaHDiscretization::SameEntities( + const Discretization& other) const noexcept +{ + auto p = dynamic_cast(&other); + return p != nullptr && &p->mesh_ == &mesh_; +} + +int OmegaHDiscretization::GetDimension() const +{ + return mesh_.dim(); +} + +LO OmegaHDiscretization::GetNumEntities(int entity_dim) const +{ + if (entity_dim < 0 || entity_dim > mesh_.dim()) + return 0; + return mesh_.nents(entity_dim); +} + +Rank1View +OmegaHDiscretization::GetEntityClassificationDimensions(int entity_dim) const +{ + if (entity_dim < 0 || entity_dim > max_entity_dim_) + return Rank1View(nullptr, + 0); + const auto& dims = class_dims_by_dim_[entity_dim]; + return Rank1View( + dims.data(), dims.size()); +} + +Rank1View +OmegaHDiscretization::GetEntityClassificationIds(int entity_dim) const +{ + if (entity_dim < 0 || entity_dim > max_entity_dim_) + return Rank1View(nullptr, 0); + const auto& ids = class_ids_by_dim_[entity_dim]; + return Rank1View(ids.data(), + ids.size()); +} + +} // namespace pcms diff --git a/src/pcms/discretization/discretization/omega_h.hpp b/src/pcms/discretization/discretization/omega_h.hpp new file mode 100644 index 000000000..deeeab900 --- /dev/null +++ b/src/pcms/discretization/discretization/omega_h.hpp @@ -0,0 +1,43 @@ +#ifndef PCMS_DISCRETIZATION_DISCRETIZATION_OMEGA_H_H +#define PCMS_DISCRETIZATION_DISCRETIZATION_OMEGA_H_H + +#include "pcms/discretization/discretization.h" + +#include +#include + +#include + +namespace pcms +{ + +class OmegaHDiscretization : public Discretization +{ +public: + explicit OmegaHDiscretization(Omega_h::Mesh& mesh); + + bool SameEntities(const Discretization& other) const noexcept override; + + int GetDimension() const override; + + LO GetNumEntities(int entity_dim) const override; + + Rank1View + GetEntityClassificationDimensions(int entity_dim) const override; + + Rank1View + GetEntityClassificationIds(int entity_dim) const override; + +private: + static constexpr int max_entity_dim_ = Region; + + Omega_h::Mesh& mesh_; + std::array, max_entity_dim_ + 1> + class_dims_by_dim_; + std::array, max_entity_dim_ + 1> + class_ids_by_dim_; +}; + +} // namespace pcms + +#endif // PCMS_DISCRETIZATION_DISCRETIZATION_OMEGA_H_H diff --git a/src/pcms/discretization/discretization/point_cloud.cpp b/src/pcms/discretization/discretization/point_cloud.cpp new file mode 100644 index 000000000..94e851cc1 --- /dev/null +++ b/src/pcms/discretization/discretization/point_cloud.cpp @@ -0,0 +1,55 @@ +#include "pcms/discretization/discretization/point_cloud.hpp" + +namespace pcms +{ + +PointCloudDiscretization::PointCloudDiscretization( + int dim, Kokkos::View coords, + const void* entity_identity) + : dim_(dim), + entity_identity_(entity_identity != nullptr + ? entity_identity + : static_cast(coords.data())), + class_dims_("point_cloud_discretization_class_dims", coords.extent(0)), + class_ids_("point_cloud_discretization_class_ids", coords.extent(0)) +{ + Kokkos::deep_copy(class_dims_, static_cast(dim_)); + Kokkos::deep_copy(class_ids_, static_cast(0)); +} + +bool PointCloudDiscretization::SameEntities( + const Discretization& other) const noexcept +{ + auto p = dynamic_cast(&other); + return p != nullptr && p->entity_identity_ == entity_identity_; +} + +int PointCloudDiscretization::GetDimension() const +{ + return dim_; +} + +LO PointCloudDiscretization::GetNumEntities(int entity_dim) const +{ + return entity_dim == Vertex ? static_cast(class_dims_.extent(0)) : 0; +} + +Rank1View +PointCloudDiscretization::GetEntityClassificationDimensions( + int entity_dim) const +{ + return entity_dim == Vertex + ? make_const_array_view(class_dims_) + : Rank1View( + nullptr, 0); +} + +Rank1View +PointCloudDiscretization::GetEntityClassificationIds(int entity_dim) const +{ + return entity_dim == Vertex + ? make_const_array_view(class_ids_) + : Rank1View(nullptr, 0); +} + +} // namespace pcms diff --git a/src/pcms/discretization/discretization/point_cloud.hpp b/src/pcms/discretization/discretization/point_cloud.hpp new file mode 100644 index 000000000..271e378c2 --- /dev/null +++ b/src/pcms/discretization/discretization/point_cloud.hpp @@ -0,0 +1,39 @@ +#ifndef PCMS_DISCRETIZATION_DISCRETIZATION_POINT_CLOUD_H +#define PCMS_DISCRETIZATION_DISCRETIZATION_POINT_CLOUD_H + +#include "pcms/discretization/discretization.h" + +#include + +namespace pcms +{ + +class PointCloudDiscretization : public Discretization +{ +public: + PointCloudDiscretization(int dim, + Kokkos::View coords, + const void* entity_identity = nullptr); + + bool SameEntities(const Discretization& other) const noexcept override; + + int GetDimension() const override; + + LO GetNumEntities(int entity_dim) const override; + + Rank1View + GetEntityClassificationDimensions(int entity_dim) const override; + + Rank1View + GetEntityClassificationIds(int entity_dim) const override; + +private: + int dim_; + const void* entity_identity_; + Kokkos::View class_dims_; + Kokkos::View class_ids_; +}; + +} // namespace pcms + +#endif // PCMS_DISCRETIZATION_DISCRETIZATION_POINT_CLOUD_H diff --git a/src/pcms/discretization/discretization/uniform_grid.hpp b/src/pcms/discretization/discretization/uniform_grid.hpp new file mode 100644 index 000000000..ee34763c5 --- /dev/null +++ b/src/pcms/discretization/discretization/uniform_grid.hpp @@ -0,0 +1,122 @@ +#ifndef PCMS_DISCRETIZATION_DISCRETIZATION_UNIFORM_GRID_H +#define PCMS_DISCRETIZATION_DISCRETIZATION_UNIFORM_GRID_H + +#include "pcms/discretization/discretization.h" +#include "pcms/utility/uniform_grid.h" + +#include + +namespace pcms +{ + +template +class UniformGridDiscretization : public Discretization +{ +public: + explicit UniformGridDiscretization(UniformGrid grid); + + bool SameEntities(const Discretization& other) const noexcept override; + + int GetDimension() const override; + + LO GetNumEntities(int entity_dim) const override; + + Rank1View + GetEntityClassificationDimensions(int entity_dim) const override; + + Rank1View + GetEntityClassificationIds(int entity_dim) const override; + +private: + static constexpr int CellEntityDim = static_cast(Dim); + + UniformGrid grid_; + Kokkos::View vertex_class_dims_; + Kokkos::View vertex_class_ids_; + Kokkos::View cell_class_dims_; + Kokkos::View cell_class_ids_; +}; + +template +UniformGridDiscretization::UniformGridDiscretization(UniformGrid grid) + : grid_(std::move(grid)), + vertex_class_dims_("uniform_grid_vertex_class_dims", + [&]() { + LO n = 1; + for (unsigned d = 0; d < Dim; ++d) + n *= (grid_.divisions[d] + 1); + return n; + }()), + vertex_class_ids_("uniform_grid_vertex_class_ids", + vertex_class_dims_.extent(0)), + cell_class_dims_("uniform_grid_cell_class_dims", grid_.GetNumCells()), + cell_class_ids_("uniform_grid_cell_class_ids", cell_class_dims_.extent(0)) +{ + Kokkos::deep_copy(vertex_class_dims_, + static_cast(Vertex)); + Kokkos::deep_copy(vertex_class_ids_, static_cast(0)); + Kokkos::deep_copy(cell_class_dims_, + static_cast(CellEntityDim)); + Kokkos::deep_copy(cell_class_ids_, static_cast(0)); +} + +template +bool UniformGridDiscretization::SameEntities( + const Discretization& other) const noexcept +{ + auto p = dynamic_cast(&other); + if (p == nullptr) + return false; + for (unsigned d = 0; d < Dim; ++d) { + if (grid_.bot_left[d] != p->grid_.bot_left[d] || + grid_.edge_length[d] != p->grid_.edge_length[d] || + grid_.divisions[d] != p->grid_.divisions[d]) { + return false; + } + } + return true; +} + +template +int UniformGridDiscretization::GetDimension() const +{ + return static_cast(Dim); +} + +template +LO UniformGridDiscretization::GetNumEntities(int entity_dim) const +{ + if (entity_dim == Vertex) + return static_cast(vertex_class_dims_.extent(0)); + if (entity_dim == CellEntityDim) + return static_cast(cell_class_dims_.extent(0)); + return 0; +} + +template +Rank1View +UniformGridDiscretization::GetEntityClassificationDimensions( + int entity_dim) const +{ + if (entity_dim == Vertex) + return make_const_array_view(vertex_class_dims_); + if (entity_dim == CellEntityDim) + return make_const_array_view(cell_class_dims_); + return Rank1View(nullptr, + 0); +} + +template +Rank1View +UniformGridDiscretization::GetEntityClassificationIds(int entity_dim) const +{ + if (entity_dim == Vertex) + return make_const_array_view(vertex_class_ids_); + if (entity_dim == CellEntityDim) + return make_const_array_view(cell_class_ids_); + return Rank1View(nullptr, 0); +} + +} // namespace pcms + +#endif // PCMS_DISCRETIZATION_DISCRETIZATION_UNIFORM_GRID_H diff --git a/src/pcms/discretization/discretization/xgc.cpp b/src/pcms/discretization/discretization/xgc.cpp new file mode 100644 index 000000000..cacf41c54 --- /dev/null +++ b/src/pcms/discretization/discretization/xgc.cpp @@ -0,0 +1,95 @@ +#include "pcms/discretization/discretization/xgc.hpp" + +namespace pcms +{ + +struct ClassifyVertsFunctor +{ + pcms::DimID geom; + Kokkos::View verts; + Kokkos::View class_dims_; + Kokkos::View class_ids_; + + ClassifyVertsFunctor( + DimID geom, Kokkos::View verts, + Kokkos::View class_dims, + Kokkos::View class_ids) + : geom(geom), verts(verts), class_dims_(class_dims), class_ids_(class_ids) + { + } + + KOKKOS_INLINE_FUNCTION void operator()(const int i) const + { + LO vert = verts(i); + if (vert >= 0 && vert < class_dims_.extent(0)) { + class_dims_(vert) = geom.dim; + class_ids_(vert) = geom.id; + } + }; +}; + +XGCDiscretization::XGCDiscretization( + const ReverseClassificationVertex& reverse_classification, LO num_plane_nodes) + : reverse_classification_(&reverse_classification), + num_plane_nodes_(num_plane_nodes), + class_dims_("xgc_discretization_class_dims", num_plane_nodes), + class_ids_("xgc_discretization_class_ids", num_plane_nodes) +{ + Kokkos::deep_copy(class_dims_, static_cast(-1)); + Kokkos::deep_copy(class_ids_, static_cast(-1)); + + for (const auto& [geom, verts] : reverse_classification) { + auto verts_host = + Kokkos::View("verts_host", verts.size()); + int idx = 0; + for (LO vert : verts) + verts_host(idx++) = vert; + auto verts_device = + Kokkos::View("verts_device", verts.size()); + Kokkos::deep_copy(verts_device, verts_host); + Kokkos::parallel_for( + "ClassifyVerts", Kokkos::RangePolicy<>(0, verts.size()), + ClassifyVertsFunctor(geom, verts_device, class_dims_, class_ids_)); + Kokkos::fence(); // Wait for kernel to complete before verts_device is + // destroyed, better would be to optimize the + // reverse_classification data structure to avoid this copy + // and synchronization, but this is simpler for now + } +} + +bool XGCDiscretization::SameEntities(const Discretization& other) const noexcept +{ + auto p = dynamic_cast(&other); + return p != nullptr && + p->reverse_classification_ == reverse_classification_ && + p->num_plane_nodes_ == num_plane_nodes_; +} + +int XGCDiscretization::GetDimension() const +{ + return 2; +} + +LO XGCDiscretization::GetNumEntities(int entity_dim) const +{ + return entity_dim == Vertex ? num_plane_nodes_ : 0; +} + +Rank1View +XGCDiscretization::GetEntityClassificationDimensions(int entity_dim) const +{ + return entity_dim == Vertex + ? make_const_array_view(class_dims_) + : Rank1View( + nullptr, 0); +} + +Rank1View +XGCDiscretization::GetEntityClassificationIds(int entity_dim) const +{ + return entity_dim == Vertex + ? make_const_array_view(class_ids_) + : Rank1View(nullptr, 0); +} + +} // namespace pcms diff --git a/src/pcms/discretization/discretization/xgc.hpp b/src/pcms/discretization/discretization/xgc.hpp new file mode 100644 index 000000000..fb1d06d31 --- /dev/null +++ b/src/pcms/discretization/discretization/xgc.hpp @@ -0,0 +1,39 @@ +#ifndef PCMS_DISCRETIZATION_DISCRETIZATION_XGC_H +#define PCMS_DISCRETIZATION_DISCRETIZATION_XGC_H + +#include "pcms/discretization/discretization.h" +#include "pcms/discretization/discretization/xgc_reverse_classification.h" + +#include + +namespace pcms +{ + +class XGCDiscretization : public Discretization +{ +public: + XGCDiscretization(const ReverseClassificationVertex& reverse_classification, + LO num_plane_nodes); + + bool SameEntities(const Discretization& other) const noexcept override; + + int GetDimension() const override; + + LO GetNumEntities(int entity_dim) const override; + + Rank1View + GetEntityClassificationDimensions(int entity_dim) const override; + + Rank1View + GetEntityClassificationIds(int entity_dim) const override; + +private: + const ReverseClassificationVertex* reverse_classification_; + LO num_plane_nodes_; + Kokkos::View class_dims_; + Kokkos::View class_ids_; +}; + +} // namespace pcms + +#endif // PCMS_DISCRETIZATION_DISCRETIZATION_XGC_H diff --git a/src/pcms/adapter/xgc/xgc_reverse_classification.cpp b/src/pcms/discretization/discretization/xgc_reverse_classification.cpp similarity index 84% rename from src/pcms/adapter/xgc/xgc_reverse_classification.cpp rename to src/pcms/discretization/discretization/xgc_reverse_classification.cpp index 930568d57..8c1f9e73d 100644 --- a/src/pcms/adapter/xgc/xgc_reverse_classification.cpp +++ b/src/pcms/discretization/discretization/xgc_reverse_classification.cpp @@ -1,8 +1,12 @@ -#include "xgc_reverse_classification.h" +#include "pcms/discretization/discretization/xgc_reverse_classification.h" + #include "mpi.h" -#include #include "pcms/utility/assert.h" + +#include +#include #include + namespace pcms { @@ -18,8 +22,9 @@ std::vector ReverseClassificationVertex::Serialize() const } return serialized_data; } + void ReverseClassificationVertex::Deserialize( - Rank1View serialized_data) + Rank1View serialized_data) { // expect to deserialize into an empty reverse classification class PCMS_ALWAYS_ASSERT(data_.empty()); @@ -35,6 +40,7 @@ void ReverseClassificationVertex::Deserialize( i += nverts; } } + ReverseClassificationVertex ReadReverseClassificationVertex(std::istream& in) { LO total_nverts; @@ -64,6 +70,7 @@ ReverseClassificationVertex ReadReverseClassificationVertex(std::istream& in) } return rc; } + ReverseClassificationVertex ReadReverseClassificationVertex(std::istream& instr, MPI_Comm comm, int root) @@ -80,23 +87,22 @@ ReverseClassificationVertex ReadReverseClassificationVertex(std::istream& instr, MPI_Bcast(serialized_rc.data(), serialized_rc.size(), MPI_INT32_T, root, comm); return rc; - } else { - size_t sz = 0; - MPI_Bcast(&sz, 1, MPI_INT64_T, root, comm); - std::vector serialized_rc(sz); - PCMS_ALWAYS_ASSERT(serialized_rc.size() == sz); - MPI_Bcast(serialized_rc.data(), sz, MPI_INT32_T, root, comm); - pcms::Rank1View av{serialized_rc.data(), - serialized_rc.size()}; - ReverseClassificationVertex rc; - rc.Deserialize(av); - return rc; } + + size_t sz = 0; + MPI_Bcast(&sz, 1, MPI_INT64_T, root, comm); + std::vector serialized_rc(sz); + PCMS_ALWAYS_ASSERT(serialized_rc.size() == sz); + MPI_Bcast(serialized_rc.data(), sz, MPI_INT32_T, root, comm); + Rank1View av{serialized_rc.data(), serialized_rc.size()}; + ReverseClassificationVertex rc; + rc.Deserialize(av); + return rc; } + ReverseClassificationVertex ReadReverseClassificationVertex( std::string classification_file) { - // PCMS_ALWAYS_ASSERT(classification_file.has_filename()); std::ifstream infile(classification_file); PCMS_ALWAYS_ASSERT(infile.is_open() && infile.good()); return ReadReverseClassificationVertex(infile); @@ -105,7 +111,6 @@ ReverseClassificationVertex ReadReverseClassificationVertex( ReverseClassificationVertex ReadReverseClassificationVertex( std::string classification_file, MPI_Comm comm, int root) { - // PCMS_ALWAYS_ASSERT(classification_file.has_filename()); std::ifstream infile(classification_file); if (!infile.is_open()) { std::cerr << "Cannot open reverse classification file " @@ -115,8 +120,8 @@ ReverseClassificationVertex ReadReverseClassificationVertex( return ReadReverseClassificationVertex(infile, comm, root); } -void ReverseClassificationVertex::Insert( - const DimID& key, Rank1View data) +void ReverseClassificationVertex::Insert(const DimID& key, + Rank1View data) { // mdspan doesn't have begin currently. This should be switched // to range based for-loop @@ -124,6 +129,7 @@ void ReverseClassificationVertex::Insert( Insert(key, data(i)); } } + void ReverseClassificationVertex::Insert(const DimID& key, LO data) { data_[key].insert(data); @@ -145,6 +151,7 @@ const std::set* ReverseClassificationVertex::Query( } return nullptr; } + std::ostream& operator<<(std::ostream& os, const ReverseClassificationVertex& v) { os << v.GetTotalVerts() << "\n"; diff --git a/src/pcms/adapter/xgc/xgc_reverse_classification.h b/src/pcms/discretization/discretization/xgc_reverse_classification.h similarity index 78% rename from src/pcms/adapter/xgc/xgc_reverse_classification.h rename to src/pcms/discretization/discretization/xgc_reverse_classification.h index fc865ac8a..b451782ac 100644 --- a/src/pcms/adapter/xgc/xgc_reverse_classification.h +++ b/src/pcms/discretization/discretization/xgc_reverse_classification.h @@ -1,15 +1,16 @@ -#ifndef PCMS_COUPLING_XGC_REVERSE_CLASSIFICATION_H -#define PCMS_COUPLING_XGC_REVERSE_CLASSIFICATION_H +#ifndef PCMS_DISCRETIZATION_XGC_REVERSE_CLASSIFICATION_H +#define PCMS_DISCRETIZATION_XGC_REVERSE_CLASSIFICATION_H + +#include "pcms/configuration.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/types.h" + #include #include -#include "pcms/utility/types.h" -#include #include -#include "mdspan/mdspan.hpp" -#include "pcms/utility/arrays.h" -#include "pcms/utility/memory_spaces.h" -#include "pcms/configuration.h" -// #include +#include + #ifdef PCMS_ENABLE_OMEGA_H #include #include "pcms/utility/assert.h" @@ -17,6 +18,7 @@ namespace pcms { + struct DimID { LO dim; @@ -26,9 +28,12 @@ struct DimID return (dim == other.dim) && (id == other.id); } }; + } // namespace pcms + namespace std { + template <> struct hash { @@ -40,7 +45,9 @@ struct hash // https://en.cppreference.com/w/cpp/utility/hash } }; + } // namespace std + namespace pcms { /// @@ -53,12 +60,16 @@ class ReverseClassificationVertex // each geometric entity in iteration order (for xgc) where the gids are in // ascending order using DataMapType = std::unordered_map>; - void Insert(const DimID& key, Rank1View data); + + void Insert(const DimID& key, Rank1View data); void Insert(const DimID& key, LO data); + [[nodiscard]] std::vector Serialize() const; - void Deserialize(Rank1View serialized_data); + void Deserialize(Rank1View serialized_data); + [[nodiscard]] bool operator==(const ReverseClassificationVertex& other) const; [[nodiscard]] const std::set* Query(const DimID& geometry) const noexcept; + [[nodiscard]] DataMapType::iterator begin() noexcept { return data_.begin(); } [[nodiscard]] DataMapType::iterator end() noexcept { return data_.end(); } [[nodiscard]] DataMapType::const_iterator begin() const noexcept @@ -69,7 +80,9 @@ class ReverseClassificationVertex { return data_.end(); } + [[nodiscard]] LO GetTotalVerts() const noexcept { return total_verts_; } + friend std::ostream& operator<<(std::ostream& os, const ReverseClassificationVertex& v); @@ -88,37 +101,39 @@ ReverseClassificationVertex ReadReverseClassificationVertex(std::string, int root = 0); #ifdef PCMS_ENABLE_OMEGA_H + enum class IndexBase { Zero = 0, One = 1 }; + template [[nodiscard]] ReverseClassificationVertex ConstructRCFromOmegaHMesh( Omega_h::Mesh& mesh, std::string numbering = "simNumbering", IndexBase index_base = IndexBase::One) { - // transfer vtx classification to host - auto classIds_h = Omega_h::HostRead( + auto class_ids_h = Omega_h::HostRead( mesh.get_array(0, "class_id")); - auto classDims_h = + auto class_dims_h = Omega_h::HostRead(mesh.get_array(0, "class_dim")); - auto vertid = Omega_h::HostRead(mesh.get_array(0, numbering)); - pcms::ReverseClassificationVertex rc; - PCMS_ALWAYS_ASSERT(classDims_h.size() == classIds_h.size()); - for (int i = 0; i < classDims_h.size(); ++i) { - pcms::DimID geom{classDims_h[i], classIds_h[i]}; + auto vert_id = Omega_h::HostRead(mesh.get_array(0, numbering)); + ReverseClassificationVertex rc; + PCMS_ALWAYS_ASSERT(class_dims_h.size() == class_ids_h.size()); + for (int i = 0; i < class_dims_h.size(); ++i) { + DimID geom{class_dims_h[i], class_ids_h[i]}; if (index_base == IndexBase::Zero) { - rc.Insert(geom, vertid[i]); + rc.Insert(geom, vert_id[i]); } else { - PCMS_ALWAYS_ASSERT(vertid[i] > 0); - rc.Insert(geom, vertid[i] - 1); + PCMS_ALWAYS_ASSERT(vert_id[i] > 0); + rc.Insert(geom, vert_id[i] - 1); } } return rc; } #endif + } // namespace pcms -#endif // PCMS_COUPLING_XGC_REVERSE_CLASSIFICATION_H +#endif // PCMS_DISCRETIZATION_XGC_REVERSE_CLASSIFICATION_H diff --git a/src/pcms/field.h b/src/pcms/field.h deleted file mode 100644 index 7689c2a53..000000000 --- a/src/pcms/field.h +++ /dev/null @@ -1,177 +0,0 @@ -#ifndef PCMS_COUPLING_FIELD_H -#define PCMS_COUPLING_FIELD_H -#include "field_layout.h" -#include "pcms/utility/types.h" -#include "pcms/utility/arrays.h" -#include "pcms/utility/memory_spaces.h" -#include -#include -#include -#include -#include // TODO remove this include -#include "pcms/field_evaluation_methods.h" // TODO remove this include -#include "pcms/coordinate_system.h" -#include "pcms/field_layout.h" - -namespace pcms -{ - -namespace detail -{ -template -using VoidT = void; - -template -struct HasCoordinateSystem : public std::false_type -{}; - -template -struct HasCoordinateSystem> - : public std::true_type -{}; - -} // namespace detail - -// TODO should the view store the layout and data, not just coordinate system -// and data? -template -class FieldDataView -{ -public: - FieldDataView(Rank1View values, - CoordinateSystem coordinate_system) - : values_(values), coordinate_system_(coordinate_system) - { - } - LO Size() const { return values_.size(); } - CoordinateSystem GetCoordinateSystem() const { return coordinate_system_; } - - [[nodiscard]] Rank1View GetValues() const noexcept - { - return values_; - } - [[nodiscard]] Rank1View GetValues() noexcept - { - return values_; - } - - // Note: currently don't believe we should allow changing the coordinate - // system - -private: - Rank1View values_; - CoordinateSystem coordinate_system_; -}; - -/* - * The LocalizationHint can hold any data that the underlying field finds - * useful. This is essentially a method to store an external cache of - * localization information. The API of the Field does allow for internal - * cacheing as well, however some wrapped Fields may use a C interface. This - * avoids the need for additional wrapping of the C interface. May re-evaluate - * the need - */ -struct LocalizationHint -{ - std::shared_ptr data = nullptr; -}; - -enum class OutOfBoundsMode -{ - ERROR, // Throw error when points are out of bounds - FILL, // Fill out-of-bounds points with a fill value - NEAREST_BOUNDARY // Map to nearest boundary cell (extrapolate) -}; - -class FieldLayout; - -/* - * A field expresses the highest level view of operations - * that a user can perform on a field - * Note results must be tagged by the coordinate system type - * Shape functions can be thought of as a particular field type. - */ -template -class FieldT -{ -public: - using value_type = T; - - CoordinateSystem GetCoordinateSystem() const - { - return GetLayout().GetDOFHolderCoordinates().GetCoordinateSystem(); - } - - // returns a hint that can be given to the Evaluate method - // this can be useful to cache data if you have multiple sets of coordinates - // you may evaluate - virtual LocalizationHint GetLocalizationHint( - CoordinateView coordinates) const = 0; - - // always takes 3D view, dof holder #, dimension, component - // underlying allocated buffer needs to be #dof holder * # components - // We return a FieldDataView to make sure we get both the data, and the - // coordinate system that the data is in - virtual void Evaluate(LocalizationHint location, - FieldDataView results) const = 0; - - // should offer component wise version? - // if data is scalar results are vector, if data is - // Results should use same coordinate frame as Coordinates passed in - virtual void EvaluateGradient(FieldDataView results) = 0; - - virtual Rank1View GetDOFHolderData() const = 0; - virtual void SetDOFHolderData(Rank1View data) = 0; - - virtual const FieldLayout& GetLayout() const = 0; - // number of physical dimensions (typically 1-6) - // int GetDimension(); - virtual bool CanEvaluateGradient() = 0; - - virtual int Serialize( - Rank1View buffer, - Rank1View permutation) const = 0; - - virtual void Deserialize( - Rank1View buffer, - Rank1View permutation) = 0; - - // Out-of-bounds handling - void SetOutOfBoundsMode(OutOfBoundsMode mode, Real fill_value = 0.0) - { - out_of_bounds_mode_ = mode; - fill_value_ = fill_value; - } - - OutOfBoundsMode GetOutOfBoundsMode() const { return out_of_bounds_mode_; } - Real GetFillValue() const { return fill_value_; } - - virtual ~FieldT() noexcept = default; - -protected: - OutOfBoundsMode out_of_bounds_mode_ = OutOfBoundsMode::ERROR; - Real fill_value_ = 0.0; -}; -// Should statically instantiate types -using FieldPtr = - std::variant*, FieldT*, FieldT*, - FieldT*, FieldT*>; - -template -using OwnedFieldPtrT = std::unique_ptr>; - -using OwnedFieldPtr = - std::variant, OwnedFieldPtrT, - OwnedFieldPtrT, OwnedFieldPtrT, - OwnedFieldPtrT>; - -// Helper function to extract raw pointer from OwnedFieldPtr variant -inline FieldPtr GetRawPointer(const OwnedFieldPtr& owned_ptr) -{ - return std::visit([](auto& field_ptr) -> FieldPtr { return field_ptr.get(); }, - owned_ptr); -} - -} // namespace pcms - -#endif // PCMS_COUPLING_FIELD_H diff --git a/src/pcms/field/CMakeLists.txt b/src/pcms/field/CMakeLists.txt new file mode 100644 index 000000000..3c4f02865 --- /dev/null +++ b/src/pcms/field/CMakeLists.txt @@ -0,0 +1,138 @@ +find_package(KokkosKernels REQUIRED) + +set(PCMS_FIELD_HEADERS + field.h + coordinate_system.h + evaluation_request.h + field_layout.h + field_metadata.h + layout/empty.h + out_of_bounds_policy.h + field_data.h + field_factory.h + function_space.h + point_evaluator.h + data/simple.h + function_space/polynomial_reconstruction.hpp + layout/point_cloud.h + data/point_cloud.h +) +set(PCMS_FIELD_SOURCES + function_space/polynomial_reconstruction.cpp + layout/empty.cpp + layout/point_cloud.cpp + data/point_cloud.cpp +) + +if (PCMS_ENABLE_OMEGA_H) + list(APPEND PCMS_FIELD_SOURCES + function_space/lagrange.cpp + function_space/spline.cpp + layout/omega_h_entity.cpp + layout/omega_h_lagrange.cpp + layout/uniform_grid.cpp + evaluator/mls_interpolation.cpp + ) + list(APPEND PCMS_FIELD_HEADERS + function_space/lagrange.h + function_space/spline.h + field_evaluator_factory.h + layout/omega_h_entity.h + layout/omega_h_lagrange.h + evaluator/omega_h_lagrange.h + layout/uniform_grid.h + uniform_grid_binary_field.h + evaluator/uniform_grid.h + evaluator/uniform_grid_spline.h + evaluator/mls_options.h + evaluator/mls_interpolation.hpp + evaluator/mls_interpolation_impl.hpp + evaluator/pcms_interpolator_aliases.hpp + evaluator/pcms_interpolator_view_utils.hpp + evaluator/pcms_interpolator_logger.hpp + evaluator/mls_point_cloud.h + evaluator/point_cloud.h + ) +endif () + +if (PCMS_ENABLE_MFEM) + list(APPEND PCMS_FIELD_SOURCES + layout/mfem.cpp + data/mfem.cpp + ) + list(APPEND PCMS_FIELD_HEADERS + layout/mfem.h + data/mfem.h + function_space/mfem.h + ) +endif () + +if (PCMS_ENABLE_MESHFIELDS) + list(APPEND PCMS_FIELD_SOURCES + layout/mesh_fields.cpp + ) + list(APPEND PCMS_FIELD_HEADERS + layout/mesh_fields.h + evaluator/mesh_fields_backend.h + evaluator/mesh_fields.h + data/mesh_fields.h + ) +endif () + +if(PCMS_ENABLE_XGC) + list(APPEND PCMS_FIELD_HEADERS + data/xgc.h + function_space/xgc.h + layout/xgc.h) + list(APPEND PCMS_FIELD_SOURCES + layout/xgc.cpp) +endif() + +add_library(pcms_field ${PCMS_FIELD_SOURCES}) +set_target_properties(pcms_field PROPERTIES + OUTPUT_NAME pcmsfield + EXPORT_NAME field) +target_sources(pcms_field PUBLIC + FILE_SET field + TYPE HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. + FILES ${PCMS_FIELD_HEADERS}) +add_library(pcms::field ALIAS pcms_field) +target_compile_features(pcms_field PUBLIC cxx_std_20) + +target_link_libraries(pcms_field PUBLIC + pcms::utility + pcms::discretization + pcms::localization) + +if(PCMS_ENABLE_OMEGA_H) + target_link_libraries(pcms_field PUBLIC Omega_h::omega_h) + target_link_libraries(pcms_field PRIVATE Kokkos::kokkoskernels) +endif() + +if(PCMS_ENABLE_MFEM) + target_link_libraries(pcms_field PUBLIC mfem) +endif() + +if (PCMS_ENABLE_MESHFIELDS) + target_link_libraries(pcms_field PUBLIC meshfields::meshfields) +endif () + +target_include_directories(pcms_field INTERFACE + $ + $ + $) + +install( + TARGETS pcms_field + EXPORT pcms_field-targets + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms/field + FILE_SET field DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + +install( + EXPORT pcms_field-targets + NAMESPACE pcms:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) diff --git a/src/pcms/coordinate.h b/src/pcms/field/coordinate.h similarity index 100% rename from src/pcms/coordinate.h rename to src/pcms/field/coordinate.h diff --git a/src/pcms/coordinate_system.h b/src/pcms/field/coordinate_system.h similarity index 80% rename from src/pcms/coordinate_system.h rename to src/pcms/field/coordinate_system.h index 68e6f6198..2ff6f5a18 100644 --- a/src/pcms/coordinate_system.h +++ b/src/pcms/field/coordinate_system.h @@ -14,12 +14,15 @@ enum class CoordinateSystem BEAMS3D }; -template +template > class CoordinateView { public: - CoordinateView(CoordinateSystem cs, - Rank2View coords) noexcept + CoordinateView( + CoordinateSystem cs, + Rank2View coords) noexcept : coordinate_system_(cs), coordinates_(coords) { } @@ -29,8 +32,8 @@ class CoordinateView return coordinate_system_; } - [[nodiscard]] Rank2View GetCoordinates() - const noexcept + [[nodiscard]] Rank2View + GetCoordinates() const noexcept { return coordinates_; } @@ -38,7 +41,8 @@ class CoordinateView // would prefer if these operations were limited to use by // CoordinateTransformation as they are unsafe (i.e., you can break class // invariant) passkey pattern? - [[nodiscard]] Rank2View GetCoordinates() noexcept + [[nodiscard]] Rank2View + GetCoordinates() noexcept { return coordinates_; } @@ -49,7 +53,7 @@ class CoordinateView private: CoordinateSystem coordinate_system_; - Rank2View coordinates_; + Rank2View coordinates_; }; class CoordinateTransformation diff --git a/src/pcms/coordinate_systems.h b/src/pcms/field/coordinate_systems.h similarity index 100% rename from src/pcms/coordinate_systems.h rename to src/pcms/field/coordinate_systems.h diff --git a/src/pcms/coordinate_transform.h b/src/pcms/field/coordinate_transform.h similarity index 100% rename from src/pcms/coordinate_transform.h rename to src/pcms/field/coordinate_transform.h diff --git a/src/pcms/field/data/mesh_fields.h b/src/pcms/field/data/mesh_fields.h new file mode 100644 index 000000000..5d243c1fa --- /dev/null +++ b/src/pcms/field/data/mesh_fields.h @@ -0,0 +1,100 @@ +#ifndef PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_FIELD_DATA_H +#define PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_FIELD_DATA_H + +#include "pcms/field/layout/mesh_fields.h" +#include "pcms/field/evaluator/mesh_fields_backend.h" +#include "pcms/field/field_data.h" +#include "pcms/field/field_metadata.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/arrays.h" + +#include +#include + +namespace pcms +{ + +template +class MeshFieldsFieldData : public FieldData +{ +public: + MeshFieldsFieldData(std::shared_ptr layout, + FieldMetadata metadata) + : layout_(std::move(layout)), + metadata_(metadata), + mesh_field_(MakeMeshFieldBackend(*layout_)), + host_data_("meshfields_field_data", + static_cast(layout_->OwnedSize())), + device_data_("meshfields_field_data_device", + static_cast(layout_->OwnedSize())) + { + if (!mesh_field_) { + throw pcms_error( + "MeshFieldsFieldData does not support this layout/order"); + } + } + + const FieldMetadata& GetMetadata() const override { return metadata_; } + + Rank1View GetDOFHolderDataHost() const override + { + Kokkos::deep_copy(host_data_, device_data_); + return make_const_array_view(host_data_); + } + + void SetDOFHolderDataHost(Rank1View values) override + { + PCMS_ALWAYS_ASSERT(values.size() == + static_cast(layout_->OwnedSize())); + CopyHostRank1ViewToDeviceView(device_data_, values); + SyncBackend(make_const_array_view(device_data_)); + } + + Rank1View GetDOFHolderData() const override + { + return make_const_array_view(device_data_); + } + + void SetDOFHolderData(Rank1View values) override + { + PCMS_ALWAYS_ASSERT(values.size() == + static_cast(layout_->OwnedSize())); + CopyDeviceRank1ViewToDeviceView(device_data_, values); + SyncBackend(make_const_array_view(device_data_)); + } + + std::shared_ptr> GetMeshFieldBackend() const + { + return mesh_field_; + } + +private: + void SyncBackend(Rank1View flat) + { + auto nodes_per_dim = layout_->GetNodesPerDim(); + auto num_components = layout_->GetNumComponents(); + auto& mesh = layout_->GetMesh(); + size_t offset = 0; + for (int i = 0; i <= mesh.dim(); ++i) { + if (nodes_per_dim[i]) { + size_t len = static_cast(mesh.nents(i)) * + static_cast(nodes_per_dim[i]) * + static_cast(num_components); + Rank1View subspan{ + flat.data_handle() + offset, len}; + mesh_field_->SetData(subspan, nodes_per_dim[i], num_components, i); + offset += len; + } + } + } + + std::shared_ptr layout_; + FieldMetadata metadata_; + std::shared_ptr> mesh_field_; + mutable Kokkos::View host_data_; + Kokkos::View device_data_; +}; + +} // namespace pcms + +#endif // PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_FIELD_DATA_H diff --git a/src/pcms/field/data/mfem.cpp b/src/pcms/field/data/mfem.cpp new file mode 100644 index 000000000..f1f667e39 --- /dev/null +++ b/src/pcms/field/data/mfem.cpp @@ -0,0 +1,89 @@ +#include "pcms/field/data/mfem.h" + +#include "pcms/utility/arrays.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/profile.h" + +namespace pcms +{ + +MFEMVertexFieldData::MFEMVertexFieldData(mfem::ParFiniteElementSpace& pfes, + mfem::ParGridFunction& gf, + FieldMetadata metadata) + : pfes_(pfes), + gf_(gf), + metadata_(metadata), + host_data_("mfem_field_data_host", pfes.GetNDofs()), + device_data_("mfem_field_data_device", pfes.GetNDofs()) +{ + PCMS_ALWAYS_ASSERT(pfes_.GetVDim() == 1); +} + +const FieldMetadata& MFEMVertexFieldData::GetMetadata() const +{ + return metadata_; +} + +Rank1View +MFEMVertexFieldData::GetDOFHolderDataHost() const +{ + PCMS_FUNCTION_TIMER; + + // Pull owner values into a true-DOF vector via the parallel restriction so + // every shared vertex reports its owner's value. + mfem::Vector true_values(pfes_.GetTrueVSize()); + gf_.GetTrueDofs(true_values); + + mfem::Array vdofs; + const int nv = pfes_.GetNDofs(); + for (int v = 0; v < nv; ++v) { + pfes_.GetVertexDofs(v, vdofs); + PCMS_ALWAYS_ASSERT(vdofs.Size() == 1); + const int lt = pfes_.GetLocalTDofNumber(vdofs[0]); + host_data_(v) = (lt >= 0) ? static_cast(true_values[lt]) : Real{0}; + } + + return make_const_array_view(host_data_); +} + +void MFEMVertexFieldData::SetDOFHolderDataHost( + Rank1View data) +{ + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(static_cast(data.size()) == pfes_.GetNDofs()); + + // Scatter received owner values into a true-DOF vector, then distribute to + // all local DOFs (including shared, non-owned vertices) via the parallel + // prolongation. + mfem::Vector true_values(pfes_.GetTrueVSize()); + mfem::Array vdofs; + const int nv = pfes_.GetNDofs(); + for (int v = 0; v < nv; ++v) { + pfes_.GetVertexDofs(v, vdofs); + PCMS_ALWAYS_ASSERT(vdofs.Size() == 1); + const int lt = pfes_.GetLocalTDofNumber(vdofs[0]); + if (lt >= 0) { + true_values[lt] = static_cast(data[v]); + } + } + + gf_.SetFromTrueDofs(true_values); +} + +Rank1View +MFEMVertexFieldData::GetDOFHolderData() const +{ + GetDOFHolderDataHost(); + Kokkos::deep_copy(device_data_, host_data_); + return make_const_array_view(device_data_); +} + +void MFEMVertexFieldData::SetDOFHolderData( + Rank1View data) +{ + PCMS_ALWAYS_ASSERT(static_cast(data.size()) == pfes_.GetNDofs()); + CopyDeviceRank1ViewToHostView(host_data_, data); + SetDOFHolderDataHost(make_const_array_view(host_data_)); +} + +} // namespace pcms diff --git a/src/pcms/field/data/mfem.h b/src/pcms/field/data/mfem.h new file mode 100644 index 000000000..d5ca1c52e --- /dev/null +++ b/src/pcms/field/data/mfem.h @@ -0,0 +1,47 @@ +#ifndef PCMS_FIELD_DATA_MFEM_H +#define PCMS_FIELD_DATA_MFEM_H + +#include "pcms/field/field_data.h" +#include "pcms/field/field_metadata.h" +#include "pcms/utility/arrays.h" + +#include + +namespace pcms +{ + +// FieldData backend for an MFEM order-1 H1 (vertex) scalar field. The +// coefficient store is the live mfem::ParGridFunction: Get/Set operate on it +// directly so the coupler and the MFEM solver share state. +// +// DOF-holder ordering is the local vertex ordering, matching MFEMLayout. To +// stay consistent across process boundaries, Get/Set round-trip through the +// FE space true DOFs: Get reads owner values via the parallel restriction and +// Set distributes received owner values via the parallel prolongation, filling +// shared (non-owned) vertices on this rank. +class MFEMVertexFieldData : public FieldData +{ +public: + MFEMVertexFieldData(mfem::ParFiniteElementSpace& pfes, + mfem::ParGridFunction& gf, FieldMetadata metadata = {}); + + const FieldMetadata& GetMetadata() const override; + + Rank1View GetDOFHolderDataHost() const override; + void SetDOFHolderDataHost( + Rank1View data) override; + + Rank1View GetDOFHolderData() const override; + void SetDOFHolderData(Rank1View data) override; + +private: + mfem::ParFiniteElementSpace& pfes_; + mfem::ParGridFunction& gf_; + FieldMetadata metadata_; + mutable Kokkos::View host_data_; + mutable Kokkos::View device_data_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_DATA_MFEM_H diff --git a/src/pcms/field/data/point_cloud.cpp b/src/pcms/field/data/point_cloud.cpp new file mode 100644 index 000000000..3e61355fc --- /dev/null +++ b/src/pcms/field/data/point_cloud.cpp @@ -0,0 +1,51 @@ +#include "pcms/field/data/point_cloud.h" +#include "pcms/field/layout/point_cloud.h" +#include "pcms/utility/profile.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/arrays.h" + +namespace pcms +{ + +PointCloud::PointCloud(std::shared_ptr layout) + : layout_(std::move(layout)), + metadata_{}, + device_data_("", + layout_->GetDOFHolderCoordinates().GetCoordinates().extent(0)), + data_host_("", + layout_->GetDOFHolderCoordinates().GetCoordinates().extent(0)) +{ +} + +const FieldMetadata& PointCloud::GetMetadata() const +{ + return metadata_; +} + +Rank1View PointCloud::GetDOFHolderDataHost() const +{ + Kokkos::deep_copy(data_host_, device_data_); + return make_const_array_view(data_host_); +} + +void PointCloud::SetDOFHolderDataHost( + Rank1View data) +{ + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(data.size() == device_data_.size()); + CopyHostRank1ViewToDeviceView(device_data_, data); +} + +Rank1View PointCloud::GetDOFHolderData() const +{ + return make_const_array_view(device_data_); +} + +void PointCloud::SetDOFHolderData(Rank1View data) +{ + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(data.size() == device_data_.size()); + CopyDeviceRank1ViewToDeviceView(device_data_, data); +} + +} // namespace pcms diff --git a/src/pcms/field/data/point_cloud.h b/src/pcms/field/data/point_cloud.h new file mode 100644 index 000000000..dd0bc6923 --- /dev/null +++ b/src/pcms/field/data/point_cloud.h @@ -0,0 +1,34 @@ +#ifndef POINT_CLOUD_H_ +#define POINT_CLOUD_H_ + +#include "pcms/field/field_data.h" +#include "pcms/field/field_metadata.h" +#include "pcms/utility/arrays.h" +#include "pcms/field/layout/point_cloud.h" +#include + +namespace pcms +{ +class PointCloud : public FieldData +{ +public: + PointCloud(std::shared_ptr layout); + + const FieldMetadata& GetMetadata() const override; + + Rank1View GetDOFHolderDataHost() const override; + void SetDOFHolderDataHost( + Rank1View data) override; + + Rank1View GetDOFHolderData() const override; + void SetDOFHolderData(Rank1View data) override; + +private: + std::shared_ptr layout_; + FieldMetadata metadata_; + Kokkos::View device_data_; + mutable Kokkos::View data_host_; +}; +} // namespace pcms + +#endif // POINT_CLOUD_H_ diff --git a/src/pcms/field/data/simple.h b/src/pcms/field/data/simple.h new file mode 100644 index 000000000..cfefbd05b --- /dev/null +++ b/src/pcms/field/data/simple.h @@ -0,0 +1,74 @@ +#ifndef PCMS_SIMPLE_FIELD_DATA_H +#define PCMS_SIMPLE_FIELD_DATA_H + +#include "../field_data.h" +#include "../field_layout.h" +#include "../field_metadata.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/memory_spaces.h" +#include +#include +#include + +namespace pcms +{ + +// SimpleFieldData is a generic concrete FieldData backed by a flat +// Kokkos::View. It works for any backend whose DOF data +// is a flat coefficient array (OmegaH, UniformGrid, PointCloud, etc.). +// +// Ownership of the layout is shared — the layout is typically held by the +// factory that created this field data object. +template +class SimpleFieldData : public FieldData +{ +public: + SimpleFieldData(std::shared_ptr layout, + FieldMetadata metadata) + : layout_(std::move(layout)), + metadata_(metadata), + host_data_("simple_field_data", + static_cast(layout_->OwnedSize())), + device_data_("simple_field_data_device", + static_cast(layout_->OwnedSize())) + { + } + + const FieldMetadata& GetMetadata() const override { return metadata_; } + + Rank1View GetDOFHolderDataHost() const override + { + Kokkos::deep_copy(host_data_, device_data_); + return make_const_array_view(host_data_); + } + + void SetDOFHolderDataHost(Rank1View values) override + { + PCMS_ALWAYS_ASSERT(values.size() == + static_cast(layout_->OwnedSize())); + CopyHostRank1ViewToDeviceView(device_data_, values); + } + + Rank1View GetDOFHolderData() const override + { + return make_const_array_view(device_data_); + } + + void SetDOFHolderData(Rank1View values) override + { + PCMS_ALWAYS_ASSERT(values.size() == + static_cast(layout_->OwnedSize())); + CopyDeviceRank1ViewToDeviceView(device_data_, values); + } + +private: + std::shared_ptr layout_; + FieldMetadata metadata_; + mutable Kokkos::View host_data_; + Kokkos::View device_data_; +}; + +} // namespace pcms + +#endif // PCMS_SIMPLE_FIELD_DATA_H diff --git a/src/pcms/field/data/xgc.h b/src/pcms/field/data/xgc.h new file mode 100644 index 000000000..2f9c8871a --- /dev/null +++ b/src/pcms/field/data/xgc.h @@ -0,0 +1,87 @@ +#ifndef PCMS_XGC_FIELD_DATA_H +#define PCMS_XGC_FIELD_DATA_H + +#include "pcms/field/layout/xgc.h" +#include "pcms/field/field_data.h" +#include "pcms/utility/assert.h" +#include +#include + +namespace pcms +{ + +template +class XGCFieldData : public FieldData +{ +public: + // Externally-managed storage: the caller owns the underlying data buffer. + // The view must remain valid for the lifetime of this object. + XGCFieldData(std::shared_ptr layout, + FieldMetadata metadata, Rank1View data) + : layout_(std::move(layout)), metadata_(metadata), data_(data) + { + PCMS_ALWAYS_ASSERT(layout_ != nullptr); + PCMS_ALWAYS_ASSERT(static_cast(data_.size()) == + layout_->GetFullDataSize()); + } + + // Self-allocating constructor: XGCFieldFactory::CreateFieldImpl uses this + // to produce a field with internally-managed storage. + XGCFieldData(std::shared_ptr layout, + FieldMetadata metadata) + : layout_(std::move(layout)), + metadata_(metadata), + owned_data_("xgc_field_data", + static_cast(layout_->GetFullDataSize())), + data_(owned_data_.data(), owned_data_.extent(0)) + { + PCMS_ALWAYS_ASSERT(layout_ != nullptr); + } + + const FieldMetadata& GetMetadata() const override { return metadata_; } + + Rank1View GetDOFHolderDataHost() const override + { + return Rank1View(data_.data_handle(), + data_.size()); + } + + void SetDOFHolderDataHost(Rank1View values) override + { + if (values.size() != data_.size()) { + throw pcms_error("XGCFieldData::SetDOFHolderDataHost: size mismatch"); + } + for (size_t i = 0; i < values.size(); ++i) { + data_(i) = values[i]; + } + } + + Rank1View GetDOFHolderData() const override + { + Kokkos::View host_view("GetDOFHolderData_host_view", + data_.size()); + for (size_t i = 0; i < data_.size(); ++i) { + host_view(i) = data_(i); + } + Kokkos::deep_copy(device_data_, host_view); + return make_const_array_view(device_data_); + } + + void SetDOFHolderData(Rank1View values) override + { + CopyDeviceRank1ViewToDeviceView(device_data_, values); + CopyRank1ViewToHost(data_, values); + } + +private: + std::shared_ptr layout_; + FieldMetadata metadata_; + // owned_data_ is non-empty only when the self-allocating constructor is used. + Kokkos::View owned_data_; + Rank1View data_; + mutable Kokkos::View device_data_; +}; + +} // namespace pcms + +#endif // PCMS_XGC_FIELD_DATA_H diff --git a/src/pcms/field/evaluation_request.h b/src/pcms/field/evaluation_request.h new file mode 100644 index 000000000..943985a8e --- /dev/null +++ b/src/pcms/field/evaluation_request.h @@ -0,0 +1,67 @@ +#ifndef PCMS_EVALUATION_REQUEST_H +#define PCMS_EVALUATION_REQUEST_H + +#include "coordinate_system.h" +#include "field_layout.h" +#include "out_of_bounds_policy.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" + +#include + +namespace pcms +{ + +class FunctionSpace; + +struct EvaluationRequest +{ + // Query coordinates to evaluate at. These are always consumed at evaluator + // construction time. + CoordinateView coords; + // Optional provenance for the query sites. When present, construction-time + // logic may use the layout and its discretization to select optimized + // localization paths. Concrete PointEvaluator implementations are not + // required to retain this layout after construction. + std::shared_ptr query_layout; + // Construction-time policy that is baked into the created PointEvaluator. + OutOfBoundsPolicy policy = {}; + + static EvaluationRequest FromCoordinates( + CoordinateView coords, OutOfBoundsPolicy policy = {}); + + static EvaluationRequest FromLayout(std::shared_ptr layout, + OutOfBoundsPolicy policy = {}); + + static EvaluationRequest FromFunctionSpace( + const FunctionSpace& function_space, OutOfBoundsPolicy policy = {}); + + [[nodiscard]] const FieldLayout* GetQueryLayout() const noexcept + { + return query_layout.get(); + } + + [[nodiscard]] const Discretization* GetQueryDiscretization() const noexcept + { + auto* layout = GetQueryLayout(); + if (layout == nullptr) { + return nullptr; + } + auto disc = layout->GetDiscretization(); + return disc.get(); + } + +private: + explicit EvaluationRequest(CoordinateView coords_in, + std::shared_ptr query_layout_in, + OutOfBoundsPolicy policy_in = {}) noexcept + : coords(coords_in), + query_layout(std::move(query_layout_in)), + policy(policy_in) + { + } +}; + +} // namespace pcms + +#endif // PCMS_EVALUATION_REQUEST_H diff --git a/src/pcms/field/evaluator/mesh_fields.h b/src/pcms/field/evaluator/mesh_fields.h new file mode 100644 index 000000000..cbe69f5b2 --- /dev/null +++ b/src/pcms/field/evaluator/mesh_fields.h @@ -0,0 +1,163 @@ +#ifndef PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_EVALUATOR_FACTORY_H +#define PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_EVALUATOR_FACTORY_H + +#include "pcms/field/evaluator/mesh_fields_backend.h" +#include "pcms/field/data/mesh_fields.h" +#include "pcms/field/field_evaluator_factory.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/field/field_data.h" +#include "pcms/utility/profile.h" +#include "pcms/utility/arrays.h" +#include + +namespace pcms +{ + +// MeshFieldsPointEvaluator implements PointEvaluator for +// MeshFields-backed simplex meshes. Each Evaluate call loads DOF data from the +// FieldData argument into the MeshFieldBackend's shape_field_ via SetData, then +// calls evaluate(). +template > +class MeshFieldsPointEvaluator : public PointEvaluator +{ +public: + MeshFieldsPointEvaluator( + std::shared_ptr layout, + MeshFieldsAdapter2LocalizationHint hint, Real fill_value) + : layout_(std::move(layout)), + hint_(std::move(hint)), + fill_value_(fill_value) + { + } + + void Evaluate( + const Field& field, + Rank2View values) const override + { + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(values.extent(0) == + hint_.coordinates_.extent(0) + hint_.num_missing_); + PCMS_ALWAYS_ASSERT(values.extent(1) == + static_cast(layout_->GetNumComponents())); + auto const* mesh_field_data = + dynamic_cast*>(&field.GetData()); + if (!mesh_field_data) { + throw pcms_error( + "MeshFieldsPointEvaluator::Evaluate: incompatible FieldData type"); + } + + // Use device views directly from hint (no copy needed) + auto eval_results = mesh_field_data->GetMeshFieldBackend()->evaluate( + hint_.coordinates_d_, hint_.offsets_d_); + + // Scatter results directly on device (no host copy) + Kokkos::parallel_for( + "CopyEvalResultsToValues", + Kokkos::RangePolicy( + 0, eval_results.extent(0)), + KOKKOS_CLASS_LAMBDA(LO i) { + values(hint_.indices_d_(i), 0) = eval_results(i, 0); + }); + + if (hint_.num_missing_ > 0 && hint_.mode_ == OutOfBoundsMode::FILL) { + T fill_val = static_cast(fill_value_); + Kokkos::parallel_for( + "FillMissingValues", + Kokkos::RangePolicy( + 0, hint_.num_missing_), + KOKKOS_CLASS_LAMBDA(LO i) { + values(hint_.missing_indices_d_(i), 0) = fill_val; + }); + } + } + +private: + std::shared_ptr layout_; + MeshFieldsAdapter2LocalizationHint hint_; + Real fill_value_; +}; + +// MeshFieldsEvaluatorFactory implements FieldEvaluatorFactory for +// MeshFields-backed simplex meshes. It owns the spatial search structure and +// the MeshFieldBackend (which holds the internal shape field). +template +class MeshFieldsEvaluatorFactory : public FieldEvaluatorFactory +{ +public: + explicit MeshFieldsEvaluatorFactory( + std::shared_ptr layout) + : layout_(std::move(layout)), + mesh_(layout_->GetMesh()), + search_(mesh_, 10, 10) + { + if (mesh_.dim() == 3) { + throw pcms_error("MeshFieldsEvaluatorFactory does not support 3D meshes"); + } + if (layout_->GetNumComponents() != 1) { + throw pcms_error( + "MeshFieldsEvaluatorFactory only supports single-component fields"); + } + } + + const FieldLayout& GetLayout() const override { return *layout_; } + + CoordinateSystem GetCoordinateSystem() const override + { + return layout_->GetDOFHolderCoordinates().GetCoordinateSystem(); + } + + bool HasDOFHolderCoordinates() const override { return true; } + + bool SupportsNearestBoundary() const override { return false; } + + std::unique_ptr> CreatePointEvaluator( + const EvaluationRequest& request) const override + { + PCMS_FUNCTION_TIMER; + const auto coords = request.coords; + const auto policy = request.policy; + if (coords.GetCoordinateSystem() != GetCoordinateSystem()) { + throw pcms_error( + "MeshFieldsEvaluatorFactory: coordinate system mismatch"); + } + if (policy.mode == OutOfBoundsMode::NEAREST_BOUNDARY) { + throw pcms_error( + "MeshFieldsEvaluatorFactory: NearestBoundary is not supported"); + } + + auto coordinates = coords.GetCoordinates(); + Kokkos::View coords_search( + "coords_search", coordinates.extent(0)); + Kokkos::parallel_for( + "copy_coords", + Kokkos::RangePolicy( + 0, coordinates.extent(0)), + KOKKOS_LAMBDA(const int i) { + coords_search(i, 0) = coordinates(i, 0); + coords_search(i, 1) = coordinates(i, 1); + }); + auto results = search_(coords_search); + + MeshFieldsAdapter2LocalizationHint hint(mesh_, results, policy.mode); + + return std::make_unique>( + layout_, std::move(hint), policy.fill_value); + } + + CoordinateView GetDOFHolderCoordinates() const override + { + return layout_->GetDOFHolderCoordinates(); + } + +private: + std::shared_ptr layout_; + Omega_h::Mesh& mesh_; + mutable GridPointSearch2D search_; +}; + +} // namespace pcms + +#endif // PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_EVALUATOR_FACTORY_H diff --git a/src/pcms/field/evaluator/mesh_fields_backend.h b/src/pcms/field/evaluator/mesh_fields_backend.h new file mode 100644 index 000000000..fdce6d6c0 --- /dev/null +++ b/src/pcms/field/evaluator/mesh_fields_backend.h @@ -0,0 +1,627 @@ +#ifndef PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_BACKEND_H +#define PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_BACKEND_H + +// Backend types shared between MeshFieldsAdapter2 and +// MeshFieldsEvaluatorFactory. Extracted here to avoid a circular include. + +#include +#include +#include + +#include "pcms/field/layout/mesh_fields.h" +#include "pcms/utility/types.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/arrays.h" +#include "pcms/localization/point_search.h" +#include "pcms/field/field.h" // OutOfBoundsMode + +namespace pcms +{ + +// --------------------------------------------------------------------------- +// Abstract backend interface +// --------------------------------------------------------------------------- +template +class MeshFieldBackend +{ +public: + virtual ~MeshFieldBackend() = default; + virtual Kokkos::View evaluate(Kokkos::View localCoords, + Kokkos::View offsets) const = 0; + virtual void SetData(Rank1View data, + size_t num_nodes, size_t num_components, int dim) = 0; + virtual void GetData(Rank1View data, size_t num_nodes, + size_t num_components, int dim) const = 0; +}; + +// --------------------------------------------------------------------------- +// Concrete backend implementation +// --------------------------------------------------------------------------- +template +class MeshFieldBackendImpl : public MeshFieldBackend +{ +public: + MeshFieldBackendImpl(Omega_h::Mesh& mesh) + : mesh_(mesh), + mesh_field_(mesh), + shape_field_(mesh_field_.template CreateLagrangeField()) + { + } + + Kokkos::View evaluate(Kokkos::View localCoords, + Kokkos::View offsets) const override + { + auto self = const_cast*>(this); + return self->mesh_field_.triangleLocalPointEval(localCoords, offsets, + shape_field_); + } + + void SetData(Rank1View data, size_t num_nodes, + size_t num_components, int dim) override + { + size_t stride = num_nodes * num_components; + auto topo = static_cast(dim); + Kokkos::parallel_for( + mesh_.nents(dim), KOKKOS_CLASS_LAMBDA(size_t ent) { + for (size_t n = 0; n < num_nodes; ++n) { + for (size_t c = 0; c < num_components; ++c) { + shape_field_(ent, n, c, topo) = + data[ent * stride + n * num_components + c]; + } + } + }); + } + + void GetData(Rank1View data, size_t num_nodes, + size_t num_components, int dim) const override + { + size_t stride = num_nodes * num_components; + auto topo = static_cast(dim); + Kokkos::parallel_for( + mesh_.nents(dim), KOKKOS_CLASS_LAMBDA(size_t ent) { + for (size_t n = 0; n < num_nodes; ++n) { + for (size_t c = 0; c < num_components; ++c) { + data[ent * stride + n * num_components + c] = + shape_field_(ent, n, c, topo); + } + } + }); + } + +private: + Omega_h::Mesh& mesh_; + MeshField::OmegahMeshField mesh_field_; + using ShapeField = + decltype(mesh_field_.template CreateLagrangeField()); + ShapeField shape_field_; +}; + +// --------------------------------------------------------------------------- +// Factory function: create a MeshFieldBackend from a layout +// --------------------------------------------------------------------------- +template +std::shared_ptr> MakeMeshFieldBackend( + const MeshFieldsAdapterLayout& layout) +{ + if constexpr (!std::is_same_v && + !std::is_same_v) { + throw pcms_error( + "MeshFieldBackend only supports the MeshFields scalar types enabled in " + "this build"); + } + + Omega_h::Mesh& mesh = layout.GetMesh(); + if (mesh.dim() == 3) { + throw pcms_error("MeshFieldBackend does not support 3D meshes"); + } + auto nodes_per_dim = layout.GetNodesPerDim(); + if (nodes_per_dim[0] == 1 && nodes_per_dim[1] == 0 && nodes_per_dim[2] == 0 && + nodes_per_dim[3] == 0) { + switch (mesh.dim()) { + case 1: return std::make_shared>(mesh); + case 2: return std::make_shared>(mesh); + default: break; + } + } else if (nodes_per_dim[0] == 1 && nodes_per_dim[1] == 1 && + nodes_per_dim[2] == 0 && nodes_per_dim[3] == 0) { + switch (mesh.dim()) { + case 2: return std::make_shared>(mesh); + case 3: return std::make_shared>(mesh); + default: break; + } + } + return nullptr; +} + +// --------------------------------------------------------------------------- +// Helper functors used in MeshFieldsAdapter2LocalizationHint +// --------------------------------------------------------------------------- +struct ComputeOffsetsFunctor +{ + Kokkos::View offsets_; + Kokkos::View elem_counts_; + + ComputeOffsetsFunctor(Kokkos::View offsets, + Kokkos::View elem_counts) + : offsets_(offsets), elem_counts_(elem_counts) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i, LO& partial, bool is_final) const + { + if (is_final) { + offsets_(i) = partial; + } + partial += elem_counts_(i); + } +}; + +// Device-side version +struct ComputeOffsetsDeviceFunctor +{ + Kokkos::View offsets_; + Kokkos::View elem_counts_; + + ComputeOffsetsDeviceFunctor(Kokkos::View offsets, + Kokkos::View elem_counts) + : offsets_(offsets), elem_counts_(elem_counts) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i, LO& partial, bool is_final) const + { + if (is_final) { + offsets_(i) = partial; + } + partial += elem_counts_(i); + } +}; + +struct CountPointsPerElementFunctor +{ + Kokkos::View elem_counts_; + Kokkos::View search_results_; + + CountPointsPerElementFunctor( + Kokkos::View elem_counts, + Kokkos::View search_results) + : elem_counts_(elem_counts), search_results_(search_results) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i) const + { + auto [dim, elem_idx, coord] = search_results_(i); + Kokkos::atomic_add(&elem_counts_(elem_idx), 1); + } +}; + +struct FillCoordinatesAndIndicesFunctor +{ + Omega_h::Mesh& mesh_; + Kokkos::View elem_counts_; + Kokkos::View offsets_; + Kokkos::View coordinates_; + Kokkos::View indices_; + Kokkos::View search_results_; + Omega_h::Int dim_; + + FillCoordinatesAndIndicesFunctor( + Omega_h::Mesh& mesh, Kokkos::View elem_counts, + Kokkos::View offsets, Kokkos::View coordinates, + Kokkos::View indices, + Kokkos::View search_results) + : mesh_(mesh), + elem_counts_(elem_counts), + offsets_(offsets), + coordinates_(coordinates), + indices_(indices), + search_results_(search_results), + dim_(mesh.dim()) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i) const + { + auto [dim, elem_idx, coord] = search_results_(i); + LO count = Kokkos::atomic_sub_fetch(&elem_counts_(elem_idx), 1); + LO index = offsets_(elem_idx) + count - 1; + for (int j = 0; j < (dim_ + 1); ++j) { + coordinates_(index, j) = coord[j]; + } + indices_(index) = i; + } +}; + +// Device-side functors for processing search results +struct FilterValidPointsFunctor +{ + Kokkos::View search_results_; + Kokkos::View valid_flags_; + Omega_h::Int mesh_dim_; + OutOfBoundsMode mode_; + + FilterValidPointsFunctor(Kokkos::View results, + Kokkos::View flags, Omega_h::Int mesh_dim, + OutOfBoundsMode mode) + : search_results_(results), + valid_flags_(flags), + mesh_dim_(mesh_dim), + mode_(mode) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i, LO& num_valid, LO& num_missing) const + { + auto [dim, elem_idx, coord] = search_results_(i); + bool is_valid = (static_cast(dim) == mesh_dim_) && (elem_idx >= 0); + + if (mode_ == OutOfBoundsMode::ERROR && !is_valid) { + Kokkos::abort("Points found outside mesh domain"); + } + + valid_flags_(i) = is_valid ? 1 : 0; + if (is_valid) { + num_valid++; + } else { + num_missing++; + } + } +}; + +struct CompactIndicesFunctor +{ + Kokkos::View flags_; + Kokkos::View scan_; + Kokkos::View valid_indices_; + Kokkos::View missing_indices_; + LO num_valid_; + + CompactIndicesFunctor(Kokkos::View flags, Kokkos::View scan, + Kokkos::View valid_indices, + Kokkos::View missing_indices, LO num_valid) + : flags_(flags), + scan_(scan), + valid_indices_(valid_indices), + missing_indices_(missing_indices), + num_valid_(num_valid) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i) const + { + if (flags_(i) == 1) { + // Valid point + valid_indices_(scan_(i)) = i; + } else { + // Missing point - use offset from end of valid region + LO missing_offset = i - scan_(i); + missing_indices_(missing_offset) = i; + } + } +}; + +struct CountPerElementFunctor +{ + Kokkos::View search_results_; + Kokkos::View valid_indices_; + Kokkos::View elem_counts_; + + CountPerElementFunctor(Kokkos::View results, + Kokkos::View valid_indices, + Kokkos::View elem_counts) + : search_results_(results), + valid_indices_(valid_indices), + elem_counts_(elem_counts) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i) const + { + LO orig_idx = valid_indices_(i); + auto [dim, elem_idx, coord] = search_results_(orig_idx); + Kokkos::atomic_add(&elem_counts_(elem_idx), 1); + } +}; + +struct FillCoordinatesDeviceFunctor +{ + Kokkos::View search_results_; + Kokkos::View valid_indices_; + Kokkos::View elem_counts_; + Kokkos::View offsets_; + Kokkos::View coordinates_; + Kokkos::View indices_; + Omega_h::Int dim_; + + FillCoordinatesDeviceFunctor(Kokkos::View results, + Kokkos::View valid_indices, + Kokkos::View elem_counts, + Kokkos::View offsets, + Kokkos::View coordinates, + Kokkos::View indices, Omega_h::Int dim) + : search_results_(results), + valid_indices_(valid_indices), + elem_counts_(elem_counts), + offsets_(offsets), + coordinates_(coordinates), + indices_(indices), + dim_(dim) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i) const + { + LO orig_idx = valid_indices_(i); + auto [dim, elem_idx, coord] = search_results_(orig_idx); + LO count = Kokkos::atomic_fetch_sub(&elem_counts_(elem_idx), 1); + LO index = offsets_(elem_idx) + count - 1; + + for (int j = 0; j < (dim_ + 1); ++j) { + coordinates_(index, j) = coord[j]; + } + indices_(index) = orig_idx; + } +}; + +struct ExclusiveScanFunctor +{ + Kokkos::View valid_flags_; + Kokkos::View scan_result_; + + ExclusiveScanFunctor(Kokkos::View flags, Kokkos::View scan) + : valid_flags_(flags), scan_result_(scan) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(const LO i, LO& update, const bool final) const + { + if (final) { + scan_result_(i) = update; + } + update += valid_flags_(i); + } +}; + +struct SetFinalOffsetFunctor +{ + Kokkos::View offsets_; + LO nelems_; + LO total_; + + SetFinalOffsetFunctor(Kokkos::View offsets, LO nelems, LO total) + : offsets_(offsets), nelems_(nelems), total_(total) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO) const { offsets_(nelems_) = total_; } +}; + +// --------------------------------------------------------------------------- +// Localization hint for MeshFields +// --------------------------------------------------------------------------- +struct MeshFieldsAdapter2LocalizationHint +{ + // Host-side constructor (legacy, for compatibility) + MeshFieldsAdapter2LocalizationHint( + Omega_h::Mesh& mesh, + Kokkos::View search_results, + OutOfBoundsMode mode) + : mode_(mode), num_valid_(0), num_missing_(0) + { + std::vector valid_point_indices; + std::vector missing_point_indices; + + if (mode_ == OutOfBoundsMode::ERROR) { + for (size_t i = 0; i < search_results.size(); ++i) { + auto [dim, elem_idx, coord] = search_results(i); + bool is_missing = + (static_cast(dim) != mesh.dim()) || (elem_idx < 0); + PCMS_ALWAYS_ASSERT(!is_missing && "Points found outside mesh domain"); + valid_point_indices.push_back(i); + } + } else { + for (size_t i = 0; i < search_results.size(); ++i) { + auto [dim, elem_idx, coord] = search_results(i); + bool is_missing = + (static_cast(dim) != mesh.dim()) || (elem_idx < 0); + if (is_missing) { + missing_point_indices.push_back(i); + } else { + valid_point_indices.push_back(i); + } + } + } + + num_valid_ = valid_point_indices.size(); + num_missing_ = missing_point_indices.size(); + + if (num_missing_ > 0 && mode_ == OutOfBoundsMode::NEAREST_BOUNDARY) { + PCMS_ALWAYS_ASSERT(false && "NEAREST_BOUNDARY mode not implemented yet"); + } + + offsets_ = Kokkos::View("offsets", mesh.nelems() + 1); + coordinates_ = Kokkos::View( + "coordinates", num_valid_, mesh.dim() + 1); + indices_ = Kokkos::View("indices", num_valid_); + + if (num_missing_ > 0) { + missing_indices_ = + Kokkos::View("missing_indices", num_missing_); + for (size_t i = 0; i < num_missing_; ++i) { + missing_indices_(i) = static_cast(missing_point_indices[i]); + } + } + + Kokkos::View elem_counts("elem_counts", + mesh.nelems()); + for (size_t i = 0; i < num_valid_; ++i) { + auto [dim, elem_idx, coord] = search_results(valid_point_indices[i]); + elem_counts[elem_idx] += 1; + } + + LO total; + ComputeOffsetsFunctor functor(offsets_, elem_counts); + Kokkos::parallel_scan( + "ComputeOffsets", + Kokkos::RangePolicy(0, mesh.nelems()), + functor, total); + offsets_(mesh.nelems()) = total; + + for (size_t i = 0; i < num_valid_; ++i) { + size_t orig_idx = valid_point_indices[i]; + auto [dim, elem_idx, coord] = search_results(orig_idx); + elem_counts(elem_idx) -= 1; + LO index = offsets_(elem_idx) + elem_counts(elem_idx); + for (int j = 0; j < (mesh.dim() + 1); ++j) { + coordinates_(index, j) = coord[j]; + } + indices_(index) = static_cast(orig_idx); + } + + // Copy all data to device once during construction + offsets_d_ = Kokkos::View("offsets_d", offsets_.extent(0)); + Kokkos::deep_copy(offsets_d_, offsets_); + + coordinates_d_ = Kokkos::View( + "coordinates_d", coordinates_.extent(0), coordinates_.extent(1)); + DeepCopyMismatchLayouts(coordinates_d_, coordinates_); + + indices_d_ = Kokkos::View("indices_d", indices_.extent(0)); + Kokkos::deep_copy(indices_d_, indices_); + + if (num_missing_ > 0) { + missing_indices_d_ = + Kokkos::View("missing_indices_d", missing_indices_.extent(0)); + Kokkos::deep_copy(missing_indices_d_, missing_indices_); + } + } + + // Device-side constructor + MeshFieldsAdapter2LocalizationHint( + Omega_h::Mesh& mesh, + Kokkos::View search_results_d, + OutOfBoundsMode mode) + : mode_(mode), num_valid_(0), num_missing_(0) + { + const LO n_points = search_results_d.size(); + + if (mode_ == OutOfBoundsMode::NEAREST_BOUNDARY) { + PCMS_ALWAYS_ASSERT(false && "NEAREST_BOUNDARY mode not implemented yet"); + } + + // Step 1: Filter valid/missing points on device + Kokkos::View valid_flags("valid_flags", n_points); + FilterValidPointsFunctor filter_functor(search_results_d, valid_flags, + mesh.dim(), mode_); + LO num_valid = 0, num_missing = 0; + Kokkos::parallel_reduce( + "FilterValidPoints", + Kokkos::RangePolicy(0, n_points), + filter_functor, num_valid, num_missing); + + num_valid_ = num_valid; + num_missing_ = num_missing; + + Kokkos::View scan_result("scan_result", n_points); + ExclusiveScanFunctor scan_functor(valid_flags, scan_result); + Kokkos::parallel_scan( + "ExclusiveScanFlags", + Kokkos::RangePolicy(0, n_points), + scan_functor); + + Kokkos::View valid_indices_d("valid_indices_d", num_valid_); + Kokkos::View missing_indices_tmp("missing_indices_tmp", num_missing_); + + CompactIndicesFunctor compact_functor(valid_flags, scan_result, + valid_indices_d, missing_indices_tmp, + num_valid_); + Kokkos::parallel_for( + "CompactIndices", + Kokkos::RangePolicy(0, n_points), + compact_functor); + + Kokkos::View elem_counts_d("elem_counts_d", mesh.nelems()); + Kokkos::deep_copy(elem_counts_d, 0); + + CountPerElementFunctor count_functor(search_results_d, valid_indices_d, + elem_counts_d); + Kokkos::parallel_for( + "CountPerElement", + Kokkos::RangePolicy(0, num_valid_), + count_functor); + + offsets_d_ = Kokkos::View("offsets_d", mesh.nelems() + 1); + ComputeOffsetsDeviceFunctor offsets_functor(offsets_d_, elem_counts_d); + LO total; + Kokkos::parallel_scan( + "ComputeOffsets", + Kokkos::RangePolicy(0, mesh.nelems()), + offsets_functor, total); + + // Set final offset + SetFinalOffsetFunctor set_final_functor(offsets_d_, mesh.nelems(), total); + Kokkos::parallel_for( + "SetFinalOffset", + Kokkos::RangePolicy(0, 1), + set_final_functor); + + // Step 6: Fill coordinates and indices on device + coordinates_d_ = + Kokkos::View("coordinates_d", num_valid_, mesh.dim() + 1); + indices_d_ = Kokkos::View("indices_d", num_valid_); + + FillCoordinatesDeviceFunctor fill_functor( + search_results_d, valid_indices_d, elem_counts_d, offsets_d_, + coordinates_d_, indices_d_, mesh.dim()); + Kokkos::parallel_for( + "FillCoordinatesAndIndices", + Kokkos::RangePolicy(0, num_valid_), + fill_functor); + + // Step 7: Handle missing indices + if (num_missing_ > 0) { + missing_indices_d_ = missing_indices_tmp; + } + + // Create host mirrors for compatibility (lazy copy - only if needed) + offsets_ = Kokkos::create_mirror_view(offsets_d_); + coordinates_ = Kokkos::View( + "coordinates_", num_valid_, mesh.dim() + 1); + DeepCopyMismatchLayouts(coordinates_, coordinates_d_); + indices_ = Kokkos::create_mirror_view(indices_d_); + if (num_missing_ > 0) { + missing_indices_ = Kokkos::create_mirror_view(missing_indices_d_); + } + } + + OutOfBoundsMode mode_; + size_t num_valid_; + size_t num_missing_; + + // Host views (for construction and debugging) + Kokkos::View offsets_; + Kokkos::View coordinates_; + Kokkos::View indices_; + Kokkos::View missing_indices_; + + // Device views (primary data for evaluation - avoid copies) + Kokkos::View offsets_d_; + Kokkos::View coordinates_d_; + Kokkos::View indices_d_; + Kokkos::View missing_indices_d_; +}; + +} // namespace pcms + +#endif // PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_BACKEND_H diff --git a/src/pcms/interpolator/mls_interpolation.cpp b/src/pcms/field/evaluator/mls_interpolation.cpp similarity index 63% rename from src/pcms/interpolator/mls_interpolation.cpp rename to src/pcms/field/evaluator/mls_interpolation.cpp index 16b948c6b..47fbd97ac 100644 --- a/src/pcms/interpolator/mls_interpolation.cpp +++ b/src/pcms/field/evaluator/mls_interpolation.cpp @@ -1,76 +1,61 @@ -#include -#include +#include +#include #include #include namespace pcms { -// RBF_GAUSSIAN Functor +// 'a' is a shape parameter +// the value of 'a' is higher if the data is localized +// the value of 'a' is smaller if the data is farther + +// gaussian struct RBF_GAUSSIAN { - // 'a' is a spreading factor/decay factor - // the value of 'a' is higher if the data is localized - // the value of 'a' is smaller if the data is farther - double a; - RBF_GAUSSIAN(double a_val) : a(a_val) {} OMEGA_H_INLINE double operator()(double r_sq, double rho_sq) const { - double phi; OMEGA_H_CHECK_PRINTF(rho_sq >= 0, "ERROR: square of cutoff distance should always be " "positive but the value is %.16f\n", rho_sq); - OMEGA_H_CHECK_PRINTF(r_sq >= 0, "ERROR: square of distance should always be positive " "but the value is %.16f\n", r_sq); - double r = Kokkos::sqrt(r_sq); - double rho = Kokkos::sqrt(rho_sq); - double ratio = r / rho; - double limit = 1 - ratio; - - if (limit < 0) { - phi = 0; - + if (rho_sq < r_sq) { + return 0; } else { - phi = Kokkos::exp(-a * a * r * r); + return Kokkos::exp(-a * a * r * r); } - - return phi; } }; -// RBF_C4 Functor struct RBF_C4 { - OMEGA_H_INLINE double operator()(double r_sq, double rho_sq) const { - double phi; - double r = Kokkos::sqrt(r_sq); OMEGA_H_CHECK_PRINTF( rho_sq > 0, "ERROR: rho_sq in rbf has to be positive, but got %.16f\n", rho_sq); + double r = Kokkos::sqrt(r_sq); double rho = Kokkos::sqrt(rho_sq); double ratio = r / rho; double limit = 1 - ratio; - if (limit < 0) { + double phi; + if (rho_sq < r_sq) { phi = 0; - } else { phi = 5 * pow(ratio, 5) + 30 * pow(ratio, 4) + 72 * pow(ratio, 3) + 82 * pow(ratio, 2) + 36 * ratio + 6; phi = phi * pow(limit, 6); } - OMEGA_H_CHECK_PRINTF(!std::isnan(phi), "ERROR: phi in rbf is NaN. r_sq, rho_sq = (%f, %f)\n", r_sq, rho_sq); @@ -78,29 +63,62 @@ struct RBF_C4 } }; -// RBF_const Functor -// struct RBF_CONST { - OMEGA_H_INLINE double operator()(double r_sq, double rho_sq) const { - double phi; - double r = Kokkos::sqrt(r_sq); OMEGA_H_CHECK_PRINTF( rho_sq > 0, "ERROR: rho_sq in rbf has to be positive, but got %.16f\n", rho_sq); + double phi = (rho_sq < r_sq) ? 0.0 : 1.0; + OMEGA_H_CHECK_PRINTF(!std::isnan(phi), + "ERROR: phi in rbf is NaN. r_sq, rho_sq = (%f, %f)\n", + r_sq, rho_sq); + return phi; + } +}; + +struct NoOp +{ + OMEGA_H_INLINE + double operator()(double, double) const { return 1.0; } +}; + +struct RBF_MULTIQUADRIC +{ + double a; + RBF_MULTIQUADRIC(double a_val) : a(a_val) {} + + OMEGA_H_INLINE + double operator()(double r_sq, double rho_sq) const + { + double r = Kokkos::sqrt(r_sq); double rho = Kokkos::sqrt(rho_sq); double ratio = r / rho; - double limit = 1 - ratio; - if (limit < 0) { - phi = 0; + double phi = + (rho_sq < r_sq) ? 0.0 : Kokkos::sqrt(1.0 + a * a * ratio * ratio); + OMEGA_H_CHECK_PRINTF(!std::isnan(phi), + "ERROR: phi in rbf is NaN. r_sq, rho_sq = (%f, %f)\n", + r_sq, rho_sq); + return phi; + } +}; - } else { - phi = 1.0; - } +// inverse multiquadric +struct RBF_INVMULTIQUADRIC +{ + double a; + RBF_INVMULTIQUADRIC(double a_val) : a(a_val) {} + OMEGA_H_INLINE + double operator()(double r_sq, double rho_sq) const + { + double r = Kokkos::sqrt(r_sq); + double rho = Kokkos::sqrt(rho_sq); + double ratio = r / rho; + double phi = + (rho_sq < r_sq) ? 0.0 : 1.0 / Kokkos::sqrt(1.0 + a * a * ratio * ratio); OMEGA_H_CHECK_PRINTF(!std::isnan(phi), "ERROR: phi in rbf is NaN. r_sq, rho_sq = (%f, %f)\n", r_sq, rho_sq); @@ -108,10 +126,43 @@ struct RBF_CONST } }; -struct NoOp +struct RBF_THINPLATESPLINE { + double a; + RBF_THINPLATESPLINE(double a_val) : a(a_val) {} + OMEGA_H_INLINE - double operator()(double, double) const { return 1.0; } + double operator()(double r_sq, double rho_sq) const + { + double r = Kokkos::sqrt(r_sq); + double rho = Kokkos::sqrt(rho_sq); + double ratio = r / rho; + double phi = + (rho_sq < r_sq) ? 0.0 : a * a * ratio * ratio * Kokkos::log(a * ratio); + OMEGA_H_CHECK_PRINTF(!std::isnan(phi), + "ERROR: phi in rbf is NaN. r_sq, rho_sq = (%f, %f)\n", + r_sq, rho_sq); + return phi; + } +}; + +struct RBF_CUBIC +{ + double a; + RBF_CUBIC(double a_val) : a(a_val) {} + + OMEGA_H_INLINE + double operator()(double r_sq, double rho_sq) const + { + double r = Kokkos::sqrt(r_sq); + double rho = Kokkos::sqrt(rho_sq); + double ratio = r / rho; + double phi = (rho_sq < r_sq) ? 0.0 : a * a * a * ratio * ratio * ratio; + OMEGA_H_CHECK_PRINTF(!std::isnan(phi), + "ERROR: phi in rbf is NaN. r_sq, rho_sq = (%f, %f)\n", + r_sq, rho_sq); + return phi; + } }; Omega_h::Write mls_interpolation( @@ -120,35 +171,51 @@ Omega_h::Write mls_interpolation( const Omega_h::LO& dim, const Omega_h::LO& degree, RadialBasisFunction bf, double lambda, double tol, double decay_factor) { - const auto nvertices_target = target_coordinates.size() / dim; - Omega_h::Write interpolated_values( nvertices_target, 0, "approximated target values"); + switch (bf) { case RadialBasisFunction::RBF_GAUSSIAN: interpolated_values = detail::mls_interpolation( source_values, source_coordinates, target_coordinates, support, dim, degree, RBF_GAUSSIAN{decay_factor}, lambda, tol); break; - case RadialBasisFunction::RBF_C4: interpolated_values = detail::mls_interpolation( source_values, source_coordinates, target_coordinates, support, dim, degree, RBF_C4{}, lambda, tol); break; - case RadialBasisFunction::RBF_CONST: interpolated_values = detail::mls_interpolation( source_values, source_coordinates, target_coordinates, support, dim, degree, RBF_CONST{}, lambda, tol); break; - case RadialBasisFunction::NO_OP: interpolated_values = detail::mls_interpolation( source_values, source_coordinates, target_coordinates, support, dim, degree, NoOp{}, lambda, tol); break; + case RadialBasisFunction::RBF_MULTIQUADRIC: + interpolated_values = detail::mls_interpolation( + source_values, source_coordinates, target_coordinates, support, dim, + degree, RBF_MULTIQUADRIC{decay_factor}, lambda, tol); + break; + case RadialBasisFunction::RBF_INVMULTIQUADRIC: + interpolated_values = detail::mls_interpolation( + source_values, source_coordinates, target_coordinates, support, dim, + degree, RBF_INVMULTIQUADRIC{decay_factor}, lambda, tol); + break; + case RadialBasisFunction::RBF_THINPLATESPLINE: + interpolated_values = detail::mls_interpolation( + source_values, source_coordinates, target_coordinates, support, dim, + degree, RBF_THINPLATESPLINE{decay_factor}, lambda, tol); + break; + case RadialBasisFunction::RBF_CUBIC: + interpolated_values = detail::mls_interpolation( + source_values, source_coordinates, target_coordinates, support, dim, + degree, RBF_CUBIC{decay_factor}, lambda, tol); + break; } return interpolated_values; @@ -162,14 +229,15 @@ void calculate_basis_slice_lengths(IntHostMatView& array) int degree = array.extent(0); int dim = array.extent(1); + if (degree == 0) + return; + for (int j = 0; j < dim; ++j) { array(0, j) = 1; } - for (int i = 0; i < degree; ++i) { array(i, 0) = 1; } - for (int i = 1; i < degree; ++i) { for (int j = 1; j < dim; ++j) { array(i, j) = array(i, j - 1) + array(i - 1, j); @@ -188,7 +256,6 @@ int calculate_basis_vector_size(const IntHostMatView& array) sum += array(i, j); } } - return sum; } @@ -196,7 +263,6 @@ int calculate_scratch_shared_size(const SupportResults& support, const int nvertices_target, int basis_size, int dim) { - IntDeviceVecView shmem_each_team("stores the size required for each team", nvertices_target); Kokkos::parallel_for( diff --git a/src/pcms/field/evaluator/mls_interpolation.hpp b/src/pcms/field/evaluator/mls_interpolation.hpp new file mode 100644 index 000000000..fe32eae0a --- /dev/null +++ b/src/pcms/field/evaluator/mls_interpolation.hpp @@ -0,0 +1,141 @@ +#ifndef PCMS_FIELD_EVALUATOR_MLS_INTERPOLATION_HPP +#define PCMS_FIELD_EVALUATOR_MLS_INTERPOLATION_HPP + +#include + +namespace pcms +{ +// Forward declaration — full definition is in pcms/localization/adj_search.hpp +struct SupportResults; + +/** + * @brief Enumeration of supported radial basis functions (RBFs) for MLS + * interpolation. + * + * Specifies the kernel type used for computing weights in Moving Least Squares + * (MLS) interpolation. Each RBF defines a different spatial weighting behavior, + * influencing locality, smoothness, and conditioning of the interpolant. + * + * ### Values: + * + * - **RBF_GAUSSIAN (0)** + * \f$ \phi(r) = \exp\left(-\left(\frac{a r}{r_c}\right)^2\right) \f$ + * Infinitely smooth and compactly supported. + * `a` is the shape (decay) parameter — smaller values give wider influence. + * Good for smooth interpolants; may cause ill-conditioning for large `a`. + * + * - **RBF_C4 (1)** + * Compactly supported, \f$ C^4\f$-continuous polynomial kernel. + * Does not require a shape parameter. + * Efficient, stable, and good for local approximations. + * + * - **RBF_CONST (2)** + * Uniform weights within support: + * \f$ \phi(r) = 1 \text{ for } r < r_c \f$, otherwise 0. + * Equivalent to averaging. + * + * - **NO_OP (3)** + * Disables internal RBF weighting logic. + * Always returns \f$ \phi = 1 \f$ regardless of distance. + * Useful for methods like SPR patching + * + * - **RBF_MULTIQUADRIC (4)** + * \f$ \phi(r) = \sqrt{1 + \left(\frac{a r}{r_c}\right)^2} \f$ + * Smooth and locally supported. + * `a` controls the width; higher values increase conditioning issues. + * + * - **RBF_INVMULTIQUADRIC (5)** + * \f$ \phi(r) = \frac{1}{\sqrt{1 + \left(\frac{a r}{r_c}\right)^2}} \f$ + * Compactly supported and generally more stable than MQ. + * `a` adjusts the decay; larger `a` leads to faster fall-off. + * + * - **RBF_THINPLATESPLINE (6)** + * \f$ \phi(r) = \left(\frac{a r}{r_c}\right)^2 \log\left(\frac{a + * r}{r_c}\right) \f$ Classic 2D surface fitting kernel. No explicit shape + * parameter is required, but `a` can scale smoothness. + * + * - **RBF_CUBIC (7)** + * \f$ \phi(r) = \left(\frac{a r}{r_c}\right)^3 \f$ + * Smooth with compact support. + * Simple and effective for local interpolation. + * + * --- + * + * @note + * - The shape parameter `a` (also called decay or spread factor) is required + * for Gaussian, MQ, IMQ, ThinPlateSpline, and Cubic kernels. + * - All kernels are compactly supported within the cutoff radius \f$ r_c \f$. + * - A good starting value for `a` is 3–5. Avoid `a = 0` (leads to constant + * behavior). + * - In cases of ill-conditioning, use regularization (`lambda > 0`). + * + * @see mls_interpolation() + */ +enum class RadialBasisFunction +{ + RBF_GAUSSIAN = 0, + RBF_C4, + RBF_CONST, + NO_OP, + RBF_MULTIQUADRIC, + RBF_INVMULTIQUADRIC, + RBF_THINPLATESPLINE, + RBF_CUBIC +}; + +/** + * @brief Performs Moving Least Squares (MLS) interpolation at target points. + * + * This function computes interpolated values at a set of target coordinates + * using Moving Least Squares (MLS) based on the provided source values and + * coordinates. It supports different radial basis functions (RBFs), polynomial + * degrees, spatial dimensions, optional regularization, and solver tolerance. + * + * @param source_values A flat array of source data values. Length should + * match the number of source points + * (`num_sources`). + * @param source_coordinates A flat array of source point coordinates of size + * `num_sources * dim`. + * @param target_coordinates A flat array of target point coordinates of size + * `num_targets * dim`. + * @param support A structure containing neighbor information for + * each target point in Compressed Sparse Row (CSR) + * format. + * @param dim Spatial dimension of the coordinate data (e.g., 2 + * or 3). + * @param degree Degree of the polynomial basis used in MLS. + * @param bf The radial basis function (RBF) used for + * weighting (e.g., Gaussian, C4). + * @param lambda Tikhonov regularization parameter (default: 0.0). + * A small positive value improves numerical + * stability for ill-conditioned local systems. + * - Well-conditioned systems: 1e-8 to 1e-6 + * - Mildly ill-conditioned: 1e-5 to 1e-3 + * - Highly ill-conditioned: 1e-2 to 1e-1 + * @param tol Optional solver tolerance (default: 1e-6). + * Singular values below this threshold are ignored. + * @param decay_factor Controls the spread of the RBF influence width + * (default: 5.0). + * - 1–3 → wide influence (smooth interpolation) + * - 3–8 → moderate locality (balanced, + * recommended) + * - 8–20 → highly local behavior (sharp detail, + * possible conditioning issues) + * + * @return A Write array containing interpolated values at each target + * point. Length matches the number of target points. + * + * @note + * - All input arrays must reside in device memory (e.g., Kokkos device views). + * - Coordinate arrays must be correctly sized as `num_points * dim`. + * - Choose `lambda` and `decay_factor` carefully to balance accuracy and + * stability: a larger decay factor often requires a slightly higher `lambda`. + */ +Omega_h::Write mls_interpolation( + const Omega_h::Reals source_values, const Omega_h::Reals source_coordinates, + const Omega_h::Reals target_coordinates, const SupportResults& support, + const Omega_h::LO& dim, const Omega_h::LO& degree, RadialBasisFunction bf, + double lambda = 0, double tol = 1e-6, double decay_factor = 5.0); + +} // namespace pcms +#endif // PCMS_FIELD_EVALUATOR_MLS_INTERPOLATION_HPP diff --git a/src/pcms/interpolator/mls_interpolation_impl.hpp b/src/pcms/field/evaluator/mls_interpolation_impl.hpp similarity index 93% rename from src/pcms/interpolator/mls_interpolation_impl.hpp rename to src/pcms/field/evaluator/mls_interpolation_impl.hpp index ab3b9751b..edc8f3bee 100644 --- a/src/pcms/interpolator/mls_interpolation_impl.hpp +++ b/src/pcms/field/evaluator/mls_interpolation_impl.hpp @@ -1,5 +1,5 @@ -#ifndef PCMS_INTERPOLATOR_MLS_INTERPOLATION_IMP_HPP -#define PCMS_INTERPOLATOR_MLS_INTERPOLATION_IMP_HPP +#ifndef PCMS_FIELD_EVALUATOR_MLS_INTERPOLATION_IMPL_HPP +#define PCMS_FIELD_EVALUATOR_MLS_INTERPOLATION_IMPL_HPP #include #include @@ -12,15 +12,15 @@ #include #include #include -#include -#include -#include +#include +#include +#include #include #include -#include -#include +#include +#include -#include //KokkosBlas::gemv +#include #include #include #include @@ -33,7 +33,9 @@ static constexpr int MAX_DIM = 6; * eval_basis_vector are needed to evaluate the polynomial basis for any degree * and dimension For instance, polynomial basis vector for dim = 2 and degree = * 3 at the point (x,y) looks like {1, x, y, xx, xy, yy, xxx, xxy, xyy,yyy}. The - * slices can be written as [1] degree 0 [x] & [y] degree 1 + * slices can be written as + * [1] degree 0 + * [x] & [y] degree 1 * [xx] & [xy, yy] degree 2 * [xxx] & [xxy,xyy, yyy] degree 3 * @@ -56,6 +58,7 @@ namespace pcms namespace detail { + /** * @brief Computes the slice lengths of the polynomial basis * @@ -130,7 +133,6 @@ void eval_basis_vector(const IntDeviceMatView& slice_length, const double* p, int curr_col = 1; double point[MAX_DIM]; - for (int i = 0; i < dim; ++i) { point[i] = p[i]; } @@ -141,10 +143,8 @@ void eval_basis_vector(const IntDeviceMatView& slice_length, const double* p, for (int k = 0; k < slice_length(i, j); ++k) { basis_vector(offset + k) = basis_vector(prev_col + k) * point[j]; } - offset += slice_length(i, j); } - prev_col = curr_col; curr_col = offset; } @@ -160,7 +160,7 @@ void eval_basis_vector(const IntDeviceMatView& slice_length, const double* p, * @param[in, out] pivot The Coord object that stores the coordinate of the *target pivot * @param[in,out] support_coordinates The orginal coordinates when in, when out - * normalized coordinates + * normalized coordinates ** */ KOKKOS_INLINE_FUNCTION @@ -230,11 +230,9 @@ KOKKOS_INLINE_FUNCTION void compute_phi_vector( const double* target_point, const ScratchMatView& local_source_points, int j, double cuttoff_dis_sq, Func rbf_func, ScratchVecView phi) { - int N = local_source_points.extent(0); int dim = local_source_points.extent(1); double ds_sq = 0; - for (int i = 0; i < dim; ++i) { double temp = target_point[i] - local_source_points(j, i); ds_sq += temp * temp; @@ -265,19 +263,15 @@ void scale_column_trans_matrix(const ScratchMatView& matrix, member_type /*unused*/, int j, ScratchMatView result_matrix) { - int N = matrix.extent(1); ScratchVecView matrix_row = Kokkos::subview(matrix, j, Kokkos::ALL()); for (int k = 0; k < N; k++) { OMEGA_H_CHECK_PRINTF(!std::isnan(matrix_row(k)), "ERROR: given matrix is NaN for k = %d\n", k); - OMEGA_H_CHECK_PRINTF(!std::isnan(vector(j)), "ERROR: given vector is NaN for j = %d\n", j); - result_matrix(k, j) = matrix_row(k) * vector(j); - OMEGA_H_CHECK_PRINTF(!std::isnan(result_matrix(k, j)), "ERROR: result_matrix is NaN for k = %d, j = %d\n", k, j); @@ -322,11 +316,8 @@ void solve_matrix_svd(member_type team, const ScratchVecView& weight, ScratchVecView rhs_values, ScratchMatView matrix, ScratchVecView solution_vector, double lambda, double tol) { - int row = matrix.extent(0); - int column = matrix.extent(1); - int weight_size = weight.size(); OMEGA_H_CHECK_PRINTF( @@ -336,9 +327,7 @@ void solve_matrix_svd(member_type team, const ScratchVecView& weight, weight_size, row); eval_row_scaling(team, weight, matrix); - team.team_barrier(); - eval_rhs_scaling(team, weight, rhs_values); team.team_barrier(); @@ -373,7 +362,6 @@ void solve_matrix_svd(member_type team, const ScratchVecView& weight, team.team_barrier(); calculate_shrinkage_factor(team, lambda, sigma); - auto Ut = find_transpose(team, U); scale_and_adjust(team, sigma, Ut, temp_matrix); // S^-1 U^T @@ -443,16 +431,12 @@ void mls_interpolation(RealConstDefaultRank1View source_values, IntHostMatView host_slice_length( "stores slice length of polynomial basis in host", degree, dim); - Kokkos::deep_copy(host_slice_length, 0); - calculate_basis_slice_lengths(host_slice_length); - auto basis_size = calculate_basis_vector_size(host_slice_length); IntDeviceMatView slice_length( "stores slice length of polynomial basis in device", degree, dim); - auto slice_length_hd = Kokkos::create_mirror_view(slice_length); Kokkos::deep_copy(slice_length_hd, host_slice_length); Kokkos::deep_copy(slice_length, slice_length_hd); @@ -461,21 +445,19 @@ void mls_interpolation(RealConstDefaultRank1View source_values, calculate_scratch_shared_size(support, ntargets, basis_size, dim); team_policy tp(ntargets, Kokkos::AUTO); - int scratch_size = tp.scratch_size_max(1); - // printf("Scratch Size = %d\n", scratch_size); - // printf("Shared Size = %d\n", shared_size); PCMS_ALWAYS_ASSERT(scratch_size > shared_size); // calculates the interpolated values Kokkos::parallel_for( - "MLS coefficients", tp.set_scratch_size(1, Kokkos::PerTeam(scratch_size)), + "MLS coefficients", tp.set_scratch_size(1, Kokkos::PerTeam(shared_size)), KOKKOS_LAMBDA(const member_type& team) { int league_rank = team.league_rank(); int start_ptr = support.supports_ptr[league_rank]; int end_ptr = support.supports_ptr[league_rank + 1]; int nsupports = end_ptr - start_ptr; + // Logger logger(0); // local_source_point stores the coordinates of source supports of a // given target ScratchMatView local_source_points(team.team_scratch(1), nsupports, dim); @@ -483,6 +465,7 @@ void mls_interpolation(RealConstDefaultRank1View source_values, // rbf function values of source supports Phi(n,n) ScratchVecView phi_vector(team.team_scratch(1), nsupports); + // rbf function values of source supports Phi(n,n) // vondermonde matrix P from the vectors of basis vector of supports ScratchMatView vandermonde_matrix(team.team_scratch(1), nsupports, basis_size); @@ -503,8 +486,6 @@ void mls_interpolation(RealConstDefaultRank1View source_values, fill(0.0, team, target_basis_vector); fill(0.0, team, solution_coefficients); - // Logger logger(15); - /** * * the local_source_points is of the type ScratchMatView with @@ -516,23 +497,16 @@ void mls_interpolation(RealConstDefaultRank1View source_values, for (int j = start_ptr; j < end_ptr; ++j) { count++; auto index = support.supports_idx[j]; - for (int i = 0; i < dim; ++i) { local_source_points(count, i) = source_coordinates[index * dim + i]; } } - // logger.logMatrix(team, LogLevel::DEBUG, local_source_points, - // "Support Coordinates"); double target_point[MAX_DIM] = {}; - for (int i = 0; i < dim; ++i) { target_point[i] = target_coordinates[league_rank * dim + i]; } - // logger.logArray(team, LogLevel::DEBUG, target_point, dim, - // "Target points"); - /** phi(nsupports) is the array of rbf functions evaluated at the * source supports In the actual implementation, Phi(nsupports, * nsupports) is the diagonal matrix & each diagonal element is the phi @@ -546,7 +520,6 @@ void mls_interpolation(RealConstDefaultRank1View source_values, compute_phi_vector(target_point, local_source_points, j, support.radii2[league_rank], rbf_func, phi_vector); }); - team.team_barrier(); /** support_values(nsupports) (or known rhs vector b) is the vector of @@ -562,13 +535,8 @@ void mls_interpolation(RealConstDefaultRank1View source_values, OMEGA_H_CHECK_PRINTF(!std::isnan(support_values(j)), "ERROR: NaN found: at support %d\n", j); }); - team.team_barrier(); - // logger.log(team, LogLevel::DEBUG, "The search starts"); - // logger.logVector(team, LogLevel::DEBUG, support_values, "Support - // values"); - /** * step 3: normalize local source supports and target point */ @@ -595,14 +563,9 @@ void mls_interpolation(RealConstDefaultRank1View source_values, create_vandermonde_matrix(local_source_points, j, slice_length, vandermonde_matrix); }); - team.team_barrier(); - // logger.logMatrix(team, LogLevel::DEBUG, vandermonde_matrix, - // "vandermonde matrix"); - OMEGA_H_CHECK_PRINTF( - support.radii2[league_rank] > 0, "ERROR: radius2 has to be positive but found to be %.16f\n", support.radii2[league_rank]); @@ -618,10 +581,6 @@ void mls_interpolation(RealConstDefaultRank1View source_values, double target_value = KokkosBlas::Experimental::dot( team, solution_coefficients, target_basis_vector); - // printf("Target Point : %d \t\t Value: %5.6f\n", league_rank, - // target_value); - // logger.logScalar(team, LogLevel::DEBUG, target_value, - // "interpolated value"); if (team.team_rank() == 0) { OMEGA_H_CHECK_PRINTF(!std::isnan(target_value), "Nan at %d\n", league_rank); @@ -654,25 +613,19 @@ Omega_h::Write mls_interpolation( const Omega_h::LO& dim, const Omega_h::LO& degree, Func rbf_func, double lambda, double tol) { - const auto nsources = source_coordinates.size() / dim; - const auto ntargets = target_coordinates.size() / dim; RealConstDefaultRank1View source_values_array_view(source_values.data(), source_values.size()); - RealConstDefaultRank1View source_coordinates_array_view( source_coordinates.data(), source_coordinates.size()); - RealConstDefaultRank1View target_coordinates_array_view( target_coordinates.data(), target_coordinates.size()); - RealDefaultRank1View radii2_array_view(support.radii2.data(), support.radii2.size()); Omega_h::Write interpolated_values( ntargets, 0, "approximated target values"); - RealDefaultRank1View interpolated_values_array_view( interpolated_values.data(), interpolated_values.size()); @@ -686,4 +639,4 @@ Omega_h::Write mls_interpolation( } // namespace detail } // namespace pcms -#endif +#endif // PCMS_FIELD_EVALUATOR_MLS_INTERPOLATION_IMPL_HPP diff --git a/src/pcms/field/evaluator/mls_options.h b/src/pcms/field/evaluator/mls_options.h new file mode 100644 index 000000000..b0db52f0a --- /dev/null +++ b/src/pcms/field/evaluator/mls_options.h @@ -0,0 +1,26 @@ +#ifndef PCMS_FIELD_EVALUATOR_MLS_OPTIONS_H +#define PCMS_FIELD_EVALUATOR_MLS_OPTIONS_H + +#include "pcms/field/evaluator/mls_interpolation.hpp" + +namespace pcms +{ + +/// Configuration for Moving Least Squares evaluation. +/// Passed to PolynomialReconstructionFunctionSpace::Create and +/// PointCloudEvaluatorFactory. +struct MLSOptions +{ + double radius = 0.5; + unsigned min_req_supports = 10; + unsigned degree = 3; + bool adapt_radius = true; + double lambda = 0.0; + double tol = 1e-6; + double decay_factor = 5.0; + RadialBasisFunction basis = RadialBasisFunction::RBF_GAUSSIAN; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_EVALUATOR_MLS_OPTIONS_H diff --git a/src/pcms/field/evaluator/mls_point_cloud.h b/src/pcms/field/evaluator/mls_point_cloud.h new file mode 100644 index 000000000..3feeee9eb --- /dev/null +++ b/src/pcms/field/evaluator/mls_point_cloud.h @@ -0,0 +1,83 @@ +#ifndef PCMS_FIELD_EVALUATOR_MLS_POINT_CLOUD_H +#define PCMS_FIELD_EVALUATOR_MLS_POINT_CLOUD_H + +#include "pcms/field/point_evaluator.h" +#include "pcms/field/field_data.h" +#include "pcms/field/evaluator/mls_options.h" +#include "pcms/field/evaluator/mls_interpolation.hpp" +#include "pcms/localization/adj_search.hpp" +#include "pcms/utility/assert.h" + +#include +#include + +namespace pcms +{ + +// MLSPointEvaluator is a concrete PointEvaluator that evaluates a +// point-cloud-backed scalar field at a fixed set of query points using MLS. +// +// Supports only scalar fields (num_components == 1). Throws for any other +// component count. +// +// The support structure is built once at construction and reused across +// repeated Evaluate calls at zero additional localization cost. +template > +class MLSPointEvaluator : public PointEvaluator +{ +public: + MLSPointEvaluator(Omega_h::Reals source_coords, Omega_h::Reals target_coords, + SupportResults supports, int dim, MLSOptions options) + : source_coords_(std::move(source_coords)), + target_coords_(std::move(target_coords)), + supports_(std::move(supports)), + dim_(dim), + options_(options) + { + } + + void Evaluate( + const Field& field, + Rank2View values) const override + { + if (values.extent(1) != 1) { + throw pcms_error( + "MLSPointEvaluator: only scalar (num_components==1) evaluation is " + "supported in this phase"); + } + + auto device_data = field.GetDOFHolderData(); + const int n_sources = static_cast(device_data.size()); + + Omega_h::Write src_w(n_sources, "mls_source_values"); + Kokkos::parallel_for( + "CopyDeviceDataToOmegaHWrite", + Kokkos::RangePolicy(0, n_sources), + KOKKOS_LAMBDA(int i) { src_w[i] = device_data(i); }); + Omega_h::Reals source_values(src_w); + + auto result = mls_interpolation( + source_values, source_coords_, target_coords_, supports_, dim_, + static_cast(options_.degree), options_.basis, + options_.lambda, options_.tol, options_.decay_factor); + + const int n_targets = result.size(); + PCMS_ALWAYS_ASSERT(static_cast(n_targets) == values.extent(0)); + Kokkos::parallel_for( + "CopyMLSResultToValues", + Kokkos::RangePolicy(0, n_targets), + KOKKOS_LAMBDA(int i) { values(i, 0) = result[i]; }); + } + +private: + Omega_h::Reals source_coords_; + Omega_h::Reals target_coords_; + SupportResults supports_; + int dim_; + MLSOptions options_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_EVALUATOR_MLS_POINT_CLOUD_H diff --git a/src/pcms/field/evaluator/omega_h_lagrange.h b/src/pcms/field/evaluator/omega_h_lagrange.h new file mode 100644 index 000000000..827a99d39 --- /dev/null +++ b/src/pcms/field/evaluator/omega_h_lagrange.h @@ -0,0 +1,327 @@ +#ifndef PCMS_OMEGA_H_LAGRANGE_EVALUATOR_FACTORY_H +#define PCMS_OMEGA_H_LAGRANGE_EVALUATOR_FACTORY_H + +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/field/field_evaluator_factory.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/field/field_data.h" +#include "pcms/localization/point_search.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/profile.h" +#include "pcms/utility/types.h" + +#include +#include +#include +#include +#include + +namespace pcms +{ + +// --------------------------------------------------------------------------- +// Localization hint — computed once per query point set, reused across Evaluate +// --------------------------------------------------------------------------- +struct OmegaHLagrangeLocHint +{ + Kokkos::View + elem_ids; // containing element (valid pts) + Kokkos::View bary; // [n_valid x (dim+1)] + Kokkos::View orig_indices; // original query index + Kokkos::View missing_indices; + OutOfBoundsMode mode; +}; + +namespace detail +{ + +template +struct CopyCoordsFunctor +{ + using DefaultLayout = + detail::default_layout_for_memory_space_t; + + Kokkos::View coords_d; + Rank2View raw_coords; + + CopyCoordsFunctor( + Kokkos::View coords_d_, + Rank2View raw_coords_) + : coords_d(coords_d_), raw_coords(raw_coords_) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(const LO i) const + { + for (int d = 0; d < Dim; ++d) { + coords_d(i, d) = raw_coords(i, d); + } + } +}; + +template +OmegaHLagrangeLocHint BuildLagrangeLocHint( + int mesh_dim, + Kokkos::View::Result*, + DeviceMemorySpace> + results, + OutOfBoundsMode mode) +{ + LO n = static_cast(results.size()); + + // First pass: count valid and missing + Kokkos::View is_valid("is_valid", n); + Kokkos::parallel_for( + "CheckValidity", + Kokkos::RangePolicy(0, n), + KOKKOS_LAMBDA(LO i) { + bool out = (static_cast(results(i).dimensionality) != mesh_dim) || + (results(i).element_id < 0); + is_valid(i) = out ? 0 : 1; + }); + + // Exclusive scan to get compaction indices + Kokkos::View valid_offsets("valid_offsets", n); + LO nv; + Kokkos::parallel_scan( + "ScanValid", + Kokkos::RangePolicy(0, n), + KOKKOS_LAMBDA(const LO i, LO& update, const bool final) { + const LO val = is_valid(i); + if (final && val) + valid_offsets(i) = update; + update += val; + }, + nv); + + LO nm = n - nv; + + // Allocate output arrays + Kokkos::View elem_ids("elem_ids", nv); + Kokkos::View bary("bary", nv, mesh_dim + 1); + Kokkos::View orig_indices("orig_indices", nv); + Kokkos::View missing_indices("missing_indices", nm); + + // Compact valid and missing separately + Kokkos::View missing_offsets("missing_offsets", n); + Kokkos::parallel_scan( + "ScanMissing", + Kokkos::RangePolicy(0, n), + KOKKOS_LAMBDA(const LO i, LO& update, const bool final) { + const LO val = 1 - is_valid(i); + if (final && val) + missing_offsets(i) = update; + update += val; + }); + + Kokkos::parallel_for( + "CompactData", + Kokkos::RangePolicy(0, n), + KOKKOS_LAMBDA(LO i) { + if (is_valid(i)) { + LO k = valid_offsets(i); + elem_ids(k) = results(i).element_id; + orig_indices(k) = i; + for (int d = 0; d <= mesh_dim; ++d) + bary(k, d) = results(i).parametric_coords[d]; + } else { + LO k = missing_offsets(i); + missing_indices(k) = i; + } + }); + + return OmegaHLagrangeLocHint{elem_ids, bary, orig_indices, missing_indices, + mode}; +} + +inline std::variant MakeSearch( + Omega_h::Mesh& mesh) +{ + if (mesh.dim() == 2) + return GridPointSearch2D(mesh, 10, 10); + else if (mesh.dim() == 3) + return GridPointSearch3D(mesh, 10, 10, 10); + throw std::invalid_argument( + "OmegaHLagrangeEvaluatorFactory: only 2D and 3D meshes are supported"); +} + +} // namespace detail + +// --------------------------------------------------------------------------- +// PointEvaluator +// --------------------------------------------------------------------------- + +// OmegaHLagrangePointEvaluator implements PointEvaluator for simplex +// meshes backed by Omega_h. Localization results (element IDs, barycentric +// coordinates) are computed once at construction and cached for repeated +// Evaluate calls. +// +// Output shape: [num_query_points][num_components]. +template > +class OmegaHLagrangePointEvaluator : public PointEvaluator +{ +public: + OmegaHLagrangePointEvaluator( + std::shared_ptr layout, + OmegaHLagrangeLocHint hint, Real fill_value) + : layout_(std::move(layout)), + hint_(std::move(hint)), + fill_value_(fill_value) + { + } + + void Evaluate( + const Field& field, + Rank2View values) const override + { + PCMS_FUNCTION_TIMER; + auto dof_data = field.GetDOFHolderData(); + LO n_valid = static_cast(hint_.elem_ids.size()); + int n_comp = layout_->GetNumComponents(); + + PCMS_ALWAYS_ASSERT(values.extent(1) == static_cast(n_comp)); + + if (layout_->GetOrder() == 0) { + Kokkos::parallel_for( + "OmegaHLagrangePointEvaluator::EvaluateOrder0", + Kokkos::RangePolicy(0, n_valid), + KOKKOS_CLASS_LAMBDA(LO k) { + LO orig = hint_.orig_indices(k); + LO elem = hint_.elem_ids(k); + for (int c = 0; c < n_comp; ++c) { + values(orig, c) = dof_data[elem * n_comp + c]; + } + }); + + } else { + // Order-1: barycentric interpolation over element vertices + Omega_h::Mesh& mesh = const_cast(layout_->GetMesh()); + int mesh_dim = mesh.dim(); + int nvpe = mesh_dim + 1; + auto elem_verts = mesh.ask_elem_verts(); + + Kokkos::parallel_for( + "OmegaHLagrangePointEvaluator::EvaluateOrder1", + Kokkos::RangePolicy(0, n_valid), + KOKKOS_CLASS_LAMBDA(LO k) { + LO orig = hint_.orig_indices(k); + LO elem = hint_.elem_ids(k); + for (int c = 0; c < n_comp; ++c) { + T val = T{}; + for (int v = 0; v < nvpe; ++v) { + LO vert = elem_verts[elem * nvpe + v]; + val += + static_cast(hint_.bary(k, v)) * dof_data[vert * n_comp + c]; + } + values(orig, c) = val; + } + }); + } + + if (hint_.mode == OutOfBoundsMode::FILL) { + T fill_val = static_cast(fill_value_); + Kokkos::parallel_for( + "OmegaHLagrangePointEvaluator::FillOutOfBounds", + Kokkos::RangePolicy( + 0, static_cast(hint_.missing_indices.size())), + KOKKOS_CLASS_LAMBDA(LO k) { + LO orig = hint_.missing_indices(k); + for (int c = 0; c < n_comp; ++c) { + values(orig, c) = fill_val; + } + }); + } + } + +private: + std::shared_ptr layout_; + OmegaHLagrangeLocHint hint_; + Real fill_value_; +}; + +// --------------------------------------------------------------------------- +// EvaluatorFactory +// --------------------------------------------------------------------------- + +// OmegaHLagrangeEvaluatorFactory implements FieldEvaluatorFactory for +// simplex meshes backed by Omega_h. It owns the spatial search structure and +// creates OmegaHLagrangePointEvaluator instances on demand. +template +class OmegaHLagrangeEvaluatorFactory : public FieldEvaluatorFactory +{ +public: + explicit OmegaHLagrangeEvaluatorFactory( + std::shared_ptr layout) + : layout_(std::move(layout)), + search_(detail::MakeSearch(layout_->GetMesh())) + { + } + + const FieldLayout& GetLayout() const override { return *layout_; } + + CoordinateSystem GetCoordinateSystem() const override + { + return layout_->GetDOFHolderCoordinates().GetCoordinateSystem(); + } + + bool HasDOFHolderCoordinates() const override { return true; } + + bool SupportsNearestBoundary() const override { return false; } + + std::unique_ptr> CreatePointEvaluator( + const EvaluationRequest& request) const override + { + PCMS_FUNCTION_TIMER; + const auto coords = request.coords; + const auto policy = request.policy; + if (coords.GetCoordinateSystem() != GetCoordinateSystem()) { + throw pcms_error( + "OmegaHLagrangeEvaluatorFactory: coordinate system mismatch"); + } + if (policy.mode == OutOfBoundsMode::NEAREST_BOUNDARY) { + throw pcms_error( + "OmegaHLagrangeEvaluatorFactory: NearestBoundary is not supported"); + } + + auto raw_coords = coords.GetCoordinates(); + LO n_pts = static_cast(raw_coords.extent(0)); + int mesh_dim = layout_->GetMesh().dim(); + + OmegaHLagrangeLocHint hint = std::visit( + [&](auto& search) { + using SearchT = std::decay_t; + constexpr int Dim = SearchT::DIM; + Kokkos::View coords_d( + "coords_d", raw_coords.extent(0), raw_coords.extent(1)); + + detail::CopyCoordsFunctor copy_functor(coords_d, raw_coords); + Kokkos::parallel_for("copy_coords", n_pts, copy_functor); + + auto results_d = search(coords_d); + return detail::BuildLagrangeLocHint(mesh_dim, results_d, + policy.mode); + }, + search_); + + return std::make_unique>( + layout_, std::move(hint), policy.fill_value); + } + + CoordinateView GetDOFHolderCoordinates() const override + { + return layout_->GetDOFHolderCoordinates(); + } + +private: + std::shared_ptr layout_; + mutable std::variant search_; +}; + +} // namespace pcms + +#endif // PCMS_OMEGA_H_LAGRANGE_EVALUATOR_FACTORY_H diff --git a/src/pcms/interpolator/pcms_interpolator_aliases.hpp b/src/pcms/field/evaluator/pcms_interpolator_aliases.hpp similarity index 85% rename from src/pcms/interpolator/pcms_interpolator_aliases.hpp rename to src/pcms/field/evaluator/pcms_interpolator_aliases.hpp index 5aef1d632..6450f44a5 100644 --- a/src/pcms/interpolator/pcms_interpolator_aliases.hpp +++ b/src/pcms/field/evaluator/pcms_interpolator_aliases.hpp @@ -1,5 +1,5 @@ -#ifndef PCMS_INTERPOLATOR_ALIASES_HPP -#define PCMS_INTERPOLATOR_ALIASES_HPP +#ifndef PCMS_FIELD_EVALUATOR_PCMS_INTERPOLATOR_ALIASES_HPP +#define PCMS_FIELD_EVALUATOR_PCMS_INTERPOLATOR_ALIASES_HPP #include #include "pcms/utility/arrays.h" @@ -16,8 +16,6 @@ using range_policy = typename Kokkos::RangePolicy<>; using team_policy = typename Kokkos::TeamPolicy<>; using member_type = typename Kokkos::TeamPolicy<>::member_type; -// alias for scratch view - using ScratchSpace = typename Kokkos::DefaultExecutionSpace::scratch_memory_space; @@ -44,4 +42,4 @@ using RealConstDefaultRank1View = Rank1View; } // namespace pcms -#endif +#endif // PCMS_FIELD_EVALUATOR_PCMS_INTERPOLATOR_ALIASES_HPP diff --git a/src/pcms/interpolator/pcms_interpolator_logger.hpp b/src/pcms/field/evaluator/pcms_interpolator_logger.hpp similarity index 93% rename from src/pcms/interpolator/pcms_interpolator_logger.hpp rename to src/pcms/field/evaluator/pcms_interpolator_logger.hpp index 60c7310f1..9e12439c4 100644 --- a/src/pcms/interpolator/pcms_interpolator_logger.hpp +++ b/src/pcms/field/evaluator/pcms_interpolator_logger.hpp @@ -1,7 +1,7 @@ -#ifndef PCMS_INTERPOLATOR_LOGGER_HPP -#define PCMS_INTERPOLATOR_LOGGER_HPP +#ifndef PCMS_FIELD_EVALUATOR_PCMS_INTERPOLATOR_LOGGER_HPP +#define PCMS_FIELD_EVALUATOR_PCMS_INTERPOLATOR_LOGGER_HPP -#include +#include #include namespace pcms @@ -26,7 +26,6 @@ class Logger { } - // log level info template KOKKOS_INLINE_FUNCTION void log(const member_type& team, const LogLevel level, const char* fmt, Args... args) @@ -45,7 +44,6 @@ class Logger void logStruct(const member_type& team, const LogLevel level, const Coord& p, const char* name) { - if (team.league_rank() == selected_league_rank_) { Kokkos::single(Kokkos::PerTeam(team), [&]() { Kokkos::printf("[%s] (League %d) %s: \n", logLevelToString(level), @@ -56,17 +54,14 @@ class Logger } } - // log array KOKKOS_INLINE_FUNCTION void logArray(const member_type& team, const LogLevel level, const double* array, const int size, const char* name) { - if (team.league_rank() == selected_league_rank_) { Kokkos::single(Kokkos::PerTeam(team), [&]() { Kokkos::printf("[%s] (League %d) %s: \n", logLevelToString(level), team.league_rank(), name); - for (int i = 0; i < size; ++i) { Kokkos::printf("%12.6f\n", array[i]); } @@ -74,7 +69,7 @@ class Logger }); } } - // log scratch vector + KOKKOS_INLINE_FUNCTION void logVector(const member_type& team, const LogLevel level, const ScratchVecView& vector, const char* name) const @@ -91,7 +86,6 @@ class Logger } } - // log scratch matrix KOKKOS_INLINE_FUNCTION void logMatrix(const member_type& team, const LogLevel level, const ScratchMatView& matrix, const char* name) const @@ -106,13 +100,11 @@ class Logger } Kokkos::printf("\n"); } - Kokkos::printf("\n"); }); } } - // log scalar KOKKOS_INLINE_FUNCTION void logScalar(const member_type& team, const LogLevel level, const double value, const char* name) const @@ -133,15 +125,12 @@ class Logger { switch (loglevel) { case LogLevel::INFO: return "INFO"; - case LogLevel::WARNING: return "WARNING"; - case LogLevel::ERROR: return "ERROR"; - case LogLevel::DEBUG: return "DEBUG"; default: return "UNKNOWN"; } } }; -} // end namespace pcms -#endif +} // namespace pcms +#endif // PCMS_FIELD_EVALUATOR_PCMS_INTERPOLATOR_LOGGER_HPP diff --git a/src/pcms/interpolator/pcms_interpolator_view_utils.hpp b/src/pcms/field/evaluator/pcms_interpolator_view_utils.hpp similarity index 95% rename from src/pcms/interpolator/pcms_interpolator_view_utils.hpp rename to src/pcms/field/evaluator/pcms_interpolator_view_utils.hpp index eae2be152..8762e334a 100644 --- a/src/pcms/interpolator/pcms_interpolator_view_utils.hpp +++ b/src/pcms/field/evaluator/pcms_interpolator_view_utils.hpp @@ -1,7 +1,7 @@ -#ifndef PCMS_INTERPOLATOR_ARRAY_OPS_HPP -#define PCMS_INTERPOLATOR_ARRAY_OPS_HPP +#ifndef PCMS_FIELD_EVALUATOR_PCMS_INTERPOLATOR_VIEW_UTILS_HPP +#define PCMS_FIELD_EVALUATOR_PCMS_INTERPOLATOR_VIEW_UTILS_HPP -#include +#include #include #include #include @@ -22,7 +22,6 @@ namespace detail KOKKOS_INLINE_FUNCTION void fill(double value, member_type team, ScratchMatView matrix) { - int row = matrix.extent(0); int col = matrix.extent(1); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, row), [=](int j) { @@ -43,17 +42,22 @@ void fill(double value, member_type team, ScratchMatView matrix) KOKKOS_INLINE_FUNCTION void fill(double value, member_type team, ScratchVecView vector) { - int size = vector.extent(0); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, size), [=](int j) { vector(j) = value; }); } +/** + * @brief Evaluates the square root of each element in scratch view + * + * @param team The team member + * @param array The scratch vector + * + */ KOKKOS_INLINE_FUNCTION void find_sq_root_each(member_type team, ScratchVecView& array) { int size = array.size(); - Kokkos::parallel_for(Kokkos::TeamThreadRange(team, size), [=](int i) { OMEGA_H_CHECK_PRINTF( array(i) >= 0, @@ -117,7 +121,6 @@ ScratchMatView find_transpose(member_type team, const ScratchMatView& matrix) { int row = matrix.extent(0); int column = matrix.extent(1); - ScratchMatView transMatrix(team.team_scratch(1), column, row); fill(0.0, team, transMatrix); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, row), [=](int i) { @@ -170,11 +173,9 @@ KOKKOS_INLINE_FUNCTION void eval_row_scaling(member_type team, ScratchVecView diagonal_entries, ScratchMatView matrix) { - int row = matrix.extent(0); int column = matrix.extent(1); int vector_size = diagonal_entries.size(); - OMEGA_H_CHECK_PRINTF( vector_size <= row, "[ERROR]: for row scaling the size of diagonal entries vector should be " @@ -208,10 +209,8 @@ KOKKOS_INLINE_FUNCTION void eval_rhs_scaling(member_type team, ScratchVecView diagonal_entries, ScratchVecView rhs_values) { - int weight_size = diagonal_entries.size(); int rhs_size = rhs_values.size(); - OMEGA_H_CHECK_PRINTF( weight_size == rhs_size, "[ERROR]: for row scaling the size of diagonal entries vector should be " @@ -252,7 +251,6 @@ void scale_and_adjust(member_type team, ScratchVecView& diagonal_entries, ScratchMatView& adjustedMatrix) { size_t rowA = matrixToScale.extent(0); - size_t rowB = adjustedMatrix.extent(0); size_t colB = adjustedMatrix.extent(1); OMEGA_H_CHECK(colB == rowA); @@ -267,4 +265,4 @@ void scale_and_adjust(member_type team, ScratchVecView& diagonal_entries, } // namespace detail } // namespace pcms -#endif +#endif // PCMS_FIELD_EVALUATOR_PCMS_INTERPOLATOR_VIEW_UTILS_HPP diff --git a/src/pcms/field/evaluator/point_cloud.h b/src/pcms/field/evaluator/point_cloud.h new file mode 100644 index 000000000..aafb88811 --- /dev/null +++ b/src/pcms/field/evaluator/point_cloud.h @@ -0,0 +1,111 @@ +#ifndef PCMS_POINT_CLOUD_EVALUATOR_FACTORY_H +#define PCMS_POINT_CLOUD_EVALUATOR_FACTORY_H + +#include "pcms/field/field_evaluator_factory.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/field/field_data.h" +#include "pcms/field/evaluation_request.h" +#include "pcms/field/evaluator/mls_options.h" +#include "pcms/field/evaluator/mls_point_cloud.h" +#include "pcms/localization/localization_path_selection.h" +#include "pcms/localization/mesh_localization.h" +#include "pcms/localization/localization_factory.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/omega_h_array_utils.h" + +#include +#include + +namespace pcms +{ + +// PointCloudEvaluatorFactory implements FieldEvaluatorFactory for +// reconstructed fields that provide source coordinates through FieldLayout. +// +// Support localization is delegated to a LocalizationFactory, allowing +// different search backends (N² point-cloud or mesh-adjacency BFS) to be +// plugged in without changing this class. CreatePointEvaluator calls +// LocalizationFactory::Build once for the supplied target coordinates and +// returns an MLSPointEvaluator that can be reused at zero additional +// localization cost. +class PointCloudEvaluatorFactory : public FieldEvaluatorFactory +{ +public: + PointCloudEvaluatorFactory(std::shared_ptr layout, + std::shared_ptr localization, + MLSOptions options = {}) + : layout_(std::move(layout)), + localization_(std::move(localization)), + options_(options) + { + } + + const FieldLayout& GetLayout() const override { return *layout_; } + + CoordinateSystem GetCoordinateSystem() const override + { + return layout_->GetDOFHolderCoordinates().GetCoordinateSystem(); + } + + bool HasDOFHolderCoordinates() const override { return true; } + + CoordinateView GetDOFHolderCoordinates() const override + { + return layout_->GetDOFHolderCoordinates(); + } + + bool SupportsNearestBoundary() const override { return false; } + + std::unique_ptr> CreatePointEvaluator( + const EvaluationRequest& request) const override + { + const auto coords = request.coords; + if (coords.GetCoordinateSystem() != GetCoordinateSystem()) { + throw pcms_error( + "PointCloudEvaluatorFactory: coordinate system mismatch"); + } + if (GetCoordinateSystem() != CoordinateSystem::Cartesian) { + throw pcms_error( + "PointCloudEvaluatorFactory: only Cartesian coordinates are " + "supported for MLS point-cloud evaluation"); + } + + // Extract source coordinates for the MLS solve. + const auto src_view = layout_->GetDOFHolderCoordinates().GetCoordinates(); + const int dim = layout_->GetDimension(); + Omega_h::Reals source_coords = + flatten_to_omega_h_reals(src_view, "src_coords"); + + // Extract target coordinates for the MLS solve. + const auto tgt_view = coords.GetCoordinates(); + PCMS_ALWAYS_ASSERT(static_cast(tgt_view.extent(1)) == dim); + Omega_h::Reals target_coords_oh = + flatten_to_omega_h_reals(tgt_view, "tgt_coords"); + + SupportResults supports; + auto path = + detail::SelectLocalizationPath(*layout_, request.GetQueryLayout()); + if (path == detail::LocalizationPath::CentroidToVertexAdjacencySearch) { + auto* adjacency = + dynamic_cast(localization_.get()); + PCMS_ALWAYS_ASSERT(adjacency != nullptr); + supports = adjacency->BuildSameMeshCentroidToVertex(); + } else { + supports = localization_->Build(coords); + } + + return std::make_unique>( + std::move(source_coords), std::move(target_coords_oh), + std::move(supports), dim, options_); + } + +private: + std::shared_ptr layout_; + std::shared_ptr localization_; + MLSOptions options_; +}; + +} // namespace pcms + +#endif // PCMS_POINT_CLOUD_EVALUATOR_FACTORY_H diff --git a/src/pcms/interpolator/spline_interpolator.hpp b/src/pcms/field/evaluator/spline_interpolator.hpp similarity index 99% rename from src/pcms/interpolator/spline_interpolator.hpp rename to src/pcms/field/evaluator/spline_interpolator.hpp index 503351c38..c59973829 100644 --- a/src/pcms/interpolator/spline_interpolator.hpp +++ b/src/pcms/field/evaluator/spline_interpolator.hpp @@ -1,5 +1,5 @@ -#ifndef MLS_RBF_OPTIONS_HPP -#define MLS_RBF_OPTIONS_HPP +#ifndef PCMS_TRANSFER_SPLINE_INTERPOLATOR_HPP +#define PCMS_TRANSFER_SPLINE_INTERPOLATOR_HPP #include "mdspan/mdspan.hpp" #include "pcms/utility/arrays.h" @@ -2620,8 +2620,8 @@ ExplicitCubicSplineInterpolator:: InitCubicCoeffFunctor init_functor(fspl, values); Kokkos::parallel_for("initialize_coefficients", nx, init_functor); - Kokkos::View wk_view("working space", 10); - auto wk = Rank1View(wk_view.data(), 10); + Kokkos::View wk_view("working space", nx); + auto wk = Rank1View(wk_view.data(), nx); // TODO: pass member type to solve_spline SolveExplicitCubicSplineFunctor functor( x, nx, fspl, static_cast(ibcxmin), bcxmin, static_cast(ibcxmax), @@ -2658,8 +2658,8 @@ CompactCubicSplineInterpolator::CompactCubicSplineInterpolator( Kokkos::View fspl4_view("explicit coefficients", 4 * nx); auto fspl4 = Rank2View(fspl4_view.data(), 4, nx); - Kokkos::View wk_view("working space", 10); - auto wk = Rank1View(wk_view.data(), 10); + Kokkos::View wk_view("working space", nx); + auto wk = Rank1View(wk_view.data(), nx); SolveCompactCubicSplineFunctor functor( x, nx, fspl, fspl4, static_cast(ibcxmin), bcxmin, @@ -2949,4 +2949,4 @@ CompactBiCubicSplineInterpolator:: } } // namespace pcms -#endif +#endif // PCMS_TRANSFER_SPLINE_INTERPOLATOR_HPP diff --git a/src/pcms/field/evaluator/uniform_grid.h b/src/pcms/field/evaluator/uniform_grid.h new file mode 100644 index 000000000..47cfa15f8 --- /dev/null +++ b/src/pcms/field/evaluator/uniform_grid.h @@ -0,0 +1,308 @@ +#ifndef PCMS_UNIFORM_GRID_EVALUATOR_FACTORY_H +#define PCMS_UNIFORM_GRID_EVALUATOR_FACTORY_H + +#include "pcms/field/layout/uniform_grid.h" +#include "pcms/utility/types.h" +#include +#include "pcms/field/field_evaluator_factory.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/field/field_data.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/profile.h" +#include "pcms/transfer/linear_interpolant.hpp" +#include "pcms/transfer/multidimarray.hpp" + +#include + +namespace pcms +{ + +// --------------------------------------------------------------------------- +// Localization hint — computed once per query point set, reused across Evaluate +// --------------------------------------------------------------------------- +template +struct UniformGridFieldLocalizationHint +{ + UniformGridFieldLocalizationHint( + Kokkos::View cell_indices, + Kokkos::View coordinates, OutOfBoundsMode mode, + Kokkos::View is_out_of_bounds, + size_t num_out_of_bounds) + : cell_indices_(cell_indices), + coordinates_(coordinates), + mode_(mode), + is_out_of_bounds_(is_out_of_bounds), + num_out_of_bounds_(num_out_of_bounds) + { + } + + Kokkos::View cell_indices_; + Kokkos::View coordinates_; + OutOfBoundsMode mode_; + Kokkos::View is_out_of_bounds_; + size_t num_out_of_bounds_; +}; + +// UniformGridPointEvaluator implements PointEvaluator for structured +// uniform grids. Localization results (cell indices, parametric coordinates) +// are computed once at construction and cached for repeated Evaluate calls. +// +// Evaluation logic is taken directly from UniformGridField::Evaluate. +// Output shape: [num_query_points][num_components]. +// Preconditions: +// - field is compatible with layout_. +// - values has extents [num_query_points][num_components]. +template > +class UniformGridPointEvaluator : public PointEvaluator +{ +public: + UniformGridPointEvaluator( + std::shared_ptr> layout, + UniformGridFieldLocalizationHint hint, Real fill_value) + : layout_(std::move(layout)), + grid_(layout_->GetGrid()), + hint_(std::move(hint)), + fill_value_(fill_value) + { + } + + void Evaluate( + const Field& field, + Rank2View values) const override + { + PCMS_FUNCTION_TIMER; + auto dof_data = field.GetDOFHolderData(); + LO num_points = static_cast(hint_.coordinates_.extent(0)); + int n_comp = layout_->GetNumComponents(); + + PCMS_ALWAYS_ASSERT(values.extent(0) == static_cast(num_points)); + PCMS_ALWAYS_ASSERT(values.extent(1) == static_cast(n_comp)); + PCMS_ALWAYS_ASSERT(dof_data.size() == + static_cast(layout_->GetNumOwnedDofHolder() * + layout_->GetNumComponents())); + + auto cell_indices = hint_.cell_indices_; + auto coordinates = hint_.coordinates_; + auto is_out_of_bounds = hint_.is_out_of_bounds_; + + if (layout_->GetOrder() == 0) { + OutOfBoundsMode mode = hint_.mode_; + Real fv = fill_value_; + Kokkos::parallel_for( + "evaluate_order0_device", + Kokkos::MDRangePolicy>({0, 0}, {num_points, n_comp}), + KOKKOS_LAMBDA(const LO i, const int c) { + if (is_out_of_bounds(i) && mode == OutOfBoundsMode::FILL) { + values(i, c) = fv; + } else { + LO dof_idx = cell_indices(i); + values(i, c) = dof_data[dof_idx * n_comp + c]; + } + }); + return; + } + + // Order-1: multilinear interpolation + Kokkos::View cell_dim_indices("cell_dim_indices", + num_points, Dim); + Kokkos::parallel_for( + "compute_cell_dim_indices", + Kokkos::RangePolicy(0, num_points), + KOKKOS_CLASS_LAMBDA(const LO i) { + auto dim_idx = grid_.GetDimensionedIndex(cell_indices(i)); + for (unsigned d = 0; d < Dim; ++d) { + cell_dim_indices(i, d) = dim_idx[d]; + } + }); + + auto cell_divisions = grid_.divisions; + IntVecView dimensions_view("dimensions", Dim); + Kokkos::parallel_for( + "set_dimensions_view", + Kokkos::RangePolicy(0, Dim), + KOKKOS_LAMBDA(const unsigned d) { + dimensions_view(d) = cell_divisions[d] + 1; + }); + + RealMatView parametric_coords("parametric_coords", num_points, Dim); + Kokkos::parallel_for( + "compute_parametric_coords", + Kokkos::RangePolicy(0, num_points), + KOKKOS_CLASS_LAMBDA(const LO i) { + auto cell_bbox = grid_.GetCellBBOX(cell_indices(i)); + for (unsigned d = 0; d < Dim; ++d) { + Real coord = coordinates(i, d); + Real cell_min = cell_bbox.center[d] - cell_bbox.half_width[d]; + Real cell_max = cell_bbox.center[d] + cell_bbox.half_width[d]; + parametric_coords(i, d) = (coord - cell_min) / (cell_max - cell_min); + } + }); + + IntMatView cell_indices_interp("cell_indices_interp", num_points, Dim); + Kokkos::parallel_for( + "copy_cell_dim_indices_to_interp", + Kokkos::RangePolicy(0, num_points), + KOKKOS_LAMBDA(const LO i) { + for (unsigned d = 0; d < Dim; ++d) + cell_indices_interp(i, d) = cell_dim_indices(i, d); + }); + + // n_comp == 1 for now (multi-component path would need per-component calls) + PCMS_ALWAYS_ASSERT( + n_comp == 1 && + "UniformGridPointEvaluator: multi-component order-1 not yet supported"); + + RealVecView values_interp("values_interp", + static_cast(dof_data.size())); + Kokkos::parallel_for( + "copy_dof_data_to_values_interp", + Kokkos::RangePolicy( + 0, static_cast(dof_data.size())), + KOKKOS_LAMBDA(const LO i) { values_interp(i) = dof_data[i]; }); + + auto interpolator = RegularGridInterpolator( + parametric_coords, values_interp, cell_indices_interp, dimensions_view); + auto interpolated = interpolator.linear_interpolation(); + + OutOfBoundsMode mode = hint_.mode_; + Real fv = fill_value_; + Kokkos::parallel_for( + "evaluate_order1_device", + Kokkos::RangePolicy(0, num_points), + KOKKOS_LAMBDA(const LO i) { + if (is_out_of_bounds(i) && mode == OutOfBoundsMode::FILL) { + values(i, 0) = fv; + } else { + values(i, 0) = interpolated(i); + } + }); + } + +private: + std::shared_ptr> layout_; + const UniformGrid& grid_; + UniformGridFieldLocalizationHint hint_; + Real fill_value_; +}; + +// UniformGridEvaluatorFactory implements FieldEvaluatorFactory for +// structured uniform grids. It owns the layout and creates +// UniformGridPointEvaluator instances on demand. +// +// Localization logic is taken directly from +// UniformGridField::GetLocalizationHint. +template +class UniformGridEvaluatorFactory : public FieldEvaluatorFactory +{ +public: + explicit UniformGridEvaluatorFactory( + std::shared_ptr> layout) + : layout_(std::move(layout)), grid_(layout_->GetGrid()) + { + } + + const FieldLayout& GetLayout() const override { return *layout_; } + + CoordinateSystem GetCoordinateSystem() const override + { + return layout_->GetDOFHolderCoordinates().GetCoordinateSystem(); + } + + bool HasDOFHolderCoordinates() const override { return true; } + + bool SupportsNearestBoundary() const override { return true; } + + std::unique_ptr> CreatePointEvaluator( + const EvaluationRequest& request) const override + { + PCMS_FUNCTION_TIMER; + const auto coords = request.coords; + const auto policy = request.policy; + PCMS_FUNCTION_TIMER; + if (coords.GetCoordinateSystem() != GetCoordinateSystem()) { + throw pcms_error( + "UniformGridEvaluatorFactory: coordinate system mismatch"); + } + if (policy.mode == OutOfBoundsMode::NEAREST_BOUNDARY && + !SupportsNearestBoundary()) { + throw pcms_error( + "UniformGridEvaluatorFactory: nearest-boundary evaluation is not " + "supported"); + } + if (layout_->GetOrder() == 1 && layout_->GetNumComponents() != 1) { + throw pcms_error( + "UniformGridEvaluatorFactory: order-1 multi-component evaluation is " + "not implemented"); + } + + auto coordinates = coords.GetCoordinates(); + LO num_points = static_cast(coordinates.extent(0)); + + Kokkos::View cell_indices_device("cell_indices", + num_points); + Kokkos::View coordinates_device( + "coordinates_device", num_points, Dim); + Kokkos::parallel_for( + "copy_coordinates_to_device", num_points, KOKKOS_LAMBDA(const LO i) { + for (unsigned d = 0; d < Dim; ++d) { + coordinates_device(i, d) = coordinates(i, d); + } + }); + + Kokkos::View is_out_of_bounds_device( + "is_out_of_bounds", num_points); + + // Parallel reduction to compute cell indices and count out-of-bounds points + size_t num_out_of_bounds = 0; + Kokkos::parallel_reduce( + "localize_points_on_device", + Kokkos::RangePolicy(0, num_points), + KOKKOS_CLASS_LAMBDA(const LO i, size_t& local_count) { + Omega_h::Vector point; + for (unsigned d = 0; d < Dim; ++d) { + point[d] = coordinates_device(i, d); + } + + bool out_of_bounds = !grid_.IsPointInBounds(point); + is_out_of_bounds_device(i) = out_of_bounds; + if (out_of_bounds) { + local_count += 1; + } + + cell_indices_device(i) = grid_.ClosestCellID(point); + }, + num_out_of_bounds); + + // Check for errors after kernel execution + if (num_out_of_bounds > 0 && policy.mode == OutOfBoundsMode::ERROR) { + throw pcms_error( + "UniformGridEvaluatorFactory: " + std::to_string(num_out_of_bounds) + + " point(s) found outside uniform grid domain"); + } + + UniformGridFieldLocalizationHint hint( + cell_indices_device, coordinates_device, policy.mode, + is_out_of_bounds_device, num_out_of_bounds); + + return std::make_unique>( + layout_, std::move(hint), policy.fill_value); + } + + CoordinateView GetDOFHolderCoordinates() const override + { + return layout_->GetDOFHolderCoordinates(); + } + +private: + std::shared_ptr> layout_; + const UniformGrid& grid_; +}; + +using UniformGridEvaluatorFactory2D = UniformGridEvaluatorFactory<2>; + +} // namespace pcms + +#endif // PCMS_UNIFORM_GRID_EVALUATOR_FACTORY_H diff --git a/src/pcms/field/evaluator/uniform_grid_spline.h b/src/pcms/field/evaluator/uniform_grid_spline.h new file mode 100644 index 000000000..a25a22a84 --- /dev/null +++ b/src/pcms/field/evaluator/uniform_grid_spline.h @@ -0,0 +1,268 @@ +#ifndef PCMS_UNIFORM_GRID_SPLINE_EVALUATOR_FACTORY_H +#define PCMS_UNIFORM_GRID_SPLINE_EVALUATOR_FACTORY_H + +#include "pcms/field/evaluator/spline_interpolator.hpp" +#include "pcms/field/evaluator/uniform_grid.h" +#include "pcms/field/field_data.h" +#include "pcms/field/field_evaluator_factory.h" +#include "pcms/field/layout/uniform_grid.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/types.h" + +#include + +namespace pcms +{ + +template > +class UniformGridSplinePointEvaluator2D + : public PointEvaluator +{ +public: + UniformGridSplinePointEvaluator2D( + std::shared_ptr> layout, + UniformGridFieldLocalizationHint<2> hint, Real fill_value) + : layout_(std::move(layout)), + grid_(layout_->GetGrid()), + hint_(std::move(hint)), + fill_value_(fill_value), + x_coords_("uniform_grid_spline_x", grid_.divisions[0] + 1), + y_coords_("uniform_grid_spline_y", grid_.divisions[1] + 1) + { + Real dx = grid_.edge_length[0] / grid_.divisions[0]; + Real dy = grid_.edge_length[1] / grid_.divisions[1]; + for (LO ix = 0; ix <= grid_.divisions[0]; ++ix) { + x_coords_(ix) = grid_.bot_left[0] + dx * ix; + } + for (LO iy = 0; iy <= grid_.divisions[1]; ++iy) { + y_coords_(iy) = grid_.bot_left[1] + dy * iy; + } + } + + void Evaluate( + const Field& field, + Rank2View values) const override + { + LO num_points = static_cast(hint_.coordinates_.extent(0)); + PCMS_ALWAYS_ASSERT(values.extent(0) == static_cast(num_points)); + PCMS_ALWAYS_ASSERT(values.extent(1) == 1); + + auto dof_data = field.GetDOFHolderData(); + LO nx = grid_.divisions[0] + 1; + LO ny = grid_.divisions[1] + 1; + PCMS_ALWAYS_ASSERT(static_cast(dof_data.size()) == nx * ny); + + Kokkos::View spline_values( + "uniform_grid_spline_values", static_cast(nx * ny)); + Kokkos::parallel_for( + "copy_spline_values_device", + Kokkos::MDRangePolicy>({0, 0}, {ny, nx}), + KOKKOS_LAMBDA(const LO iy, const LO ix) { + spline_values(ix * ny + iy) = dof_data[iy * nx + ix]; + }); + + if (hint_.num_out_of_bounds_ == static_cast(num_points) && + hint_.mode_ == OutOfBoundsMode::FILL) { + Kokkos::parallel_for( + "fill_out_of_bounds_values", + Kokkos::RangePolicy(0, num_points), + KOKKOS_CLASS_LAMBDA(LO i) { values(i, 0) = fill_value_; }); + return; + } + + LO num_in_bounds = static_cast(num_points - hint_.num_out_of_bounds_); + Kokkos::View eval_x("uniform_grid_spline_eval_x", + num_in_bounds); + Kokkos::View eval_y("uniform_grid_spline_eval_y", + num_in_bounds); + Kokkos::View in_bounds_indices( + "uniform_grid_spline_in_bounds_indices", num_in_bounds); + + auto coordinates_device = hint_.coordinates_; + + auto is_out_of_bounds_device = hint_.is_out_of_bounds_; + + // Stream compaction: filter in-bounds points using parallel_scan + Kokkos::parallel_scan( + "compact_in_bounds_points", + Kokkos::RangePolicy(0, num_points), + KOKKOS_LAMBDA(const LO i, LO& update, const bool final) { + const bool is_in_bounds = !is_out_of_bounds_device(i); + if (is_in_bounds && final) { + eval_x(update) = coordinates_device(i, 0); + eval_y(update) = coordinates_device(i, 1); + in_bounds_indices(update) = i; + } + if (is_in_bounds) { + update += 1; + } + }); + + Kokkos::View spline_output( + "uniform_grid_spline_output", num_in_bounds, 1); + Kokkos::View empty_bc( + "uniform_grid_spline_empty_bc", 0); + + CompactBiCubicSplineInterpolator interpolator( + Rank1View(x_coords_.data(), x_coords_.extent(0)), + Rank1View(y_coords_.data(), y_coords_.extent(0)), + Rank1View(spline_values.data(), + spline_values.extent(0)), + BoundaryCondition::NOT_A_KNOT, + Rank1View(empty_bc.data(), empty_bc.extent(0)), + BoundaryCondition::NOT_A_KNOT, + Rank1View(empty_bc.data(), empty_bc.extent(0)), + BoundaryCondition::NOT_A_KNOT, + Rank1View(empty_bc.data(), empty_bc.extent(0)), + BoundaryCondition::NOT_A_KNOT, + Rank1View(empty_bc.data(), empty_bc.extent(0))); + interpolator.evaluate( + Rank1View(eval_x.data(), eval_x.extent(0)), + Rank1View(eval_y.data(), eval_y.extent(0)), + Rank2View(spline_output.data(), + spline_output.extent(0), 1)); + + if (hint_.mode_ == OutOfBoundsMode::FILL) { + Kokkos::parallel_for( + "fill_out_of_bounds", + Kokkos::RangePolicy(0, num_points), + KOKKOS_CLASS_LAMBDA(LO i) { + if (hint_.is_out_of_bounds_(i)) { + values(i, 0) = fill_value_; + } + }); + } + + Kokkos::parallel_for( + "copy_in_bounds_values", + Kokkos::RangePolicy(0, num_in_bounds), + KOKKOS_LAMBDA(LO i) { + values(in_bounds_indices(i), 0) = spline_output(i, 0); + }); + } + +private: + std::shared_ptr> layout_; + const UniformGrid<2>& grid_; + UniformGridFieldLocalizationHint<2> hint_; + Real fill_value_; + Kokkos::View x_coords_; + Kokkos::View y_coords_; +}; + +class UniformGridSplineEvaluatorFactory2D : public FieldEvaluatorFactory +{ +public: + explicit UniformGridSplineEvaluatorFactory2D( + std::shared_ptr> layout) + : layout_(std::move(layout)), grid_(layout_->GetGrid()) + { + } + + const FieldLayout& GetLayout() const override { return *layout_; } + + CoordinateSystem GetCoordinateSystem() const override + { + return layout_->GetDOFHolderCoordinates().GetCoordinateSystem(); + } + + bool HasDOFHolderCoordinates() const override { return true; } + + bool SupportsNearestBoundary() const override { return false; } + + std::unique_ptr> CreatePointEvaluator( + const EvaluationRequest& request) const override + { + const auto coords = request.coords; + const auto policy = request.policy; + if (coords.GetCoordinateSystem() != GetCoordinateSystem()) { + throw pcms_error( + "UniformGridSplineEvaluatorFactory2D: coordinate system mismatch"); + } + if (policy.mode == OutOfBoundsMode::NEAREST_BOUNDARY) { + throw pcms_error( + "UniformGridSplineEvaluatorFactory2D: nearest-boundary evaluation is " + "not supported"); + } + if (layout_->GetOrder() != 1) { + throw pcms_error( + "UniformGridSplineEvaluatorFactory2D: spline evaluation requires an " + "order-1 uniform-grid layout"); + } + if (layout_->GetNumComponents() != 1) { + throw pcms_error( + "UniformGridSplineEvaluatorFactory2D: spline evaluation only supports " + "single-component fields"); + } + + auto coordinates = coords.GetCoordinates(); + LO num_points = static_cast(coordinates.extent(0)); + + Kokkos::View coordinates_d("coordinates_d", + num_points, 2); + Kokkos::parallel_for( + "copy_coordinates", + Kokkos::RangePolicy(0, num_points), + KOKKOS_LAMBDA(LO i) { + for (LO j = 0; j < 2; ++j) { + coordinates_d(i, j) = coordinates(i, j); + } + }); + + Kokkos::View cell_indices_device("cell_indices", + num_points); + Kokkos::View is_out_of_bounds_device( + "is_out_of_bounds", num_points); + + // Parallel reduction to compute cell indices and count out-of-bounds points + size_t num_out_of_bounds = 0; + Kokkos::parallel_reduce( + "localize_points_on_device_spline", + Kokkos::RangePolicy(0, num_points), + KOKKOS_CLASS_LAMBDA(const LO i, size_t& local_count) { + Omega_h::Vector<2> point; + for (unsigned d = 0; d < 2; ++d) { + point[d] = coordinates_d(i, d); + } + + bool out_of_bounds = !grid_.IsPointInBounds(point); + is_out_of_bounds_device(i) = out_of_bounds; + if (out_of_bounds) { + local_count += 1; + } + + cell_indices_device(i) = grid_.ClosestCellID(point); + }, + num_out_of_bounds); + + // Check for errors after kernel execution + if (num_out_of_bounds > 0 && policy.mode == OutOfBoundsMode::ERROR) { + throw pcms_error("UniformGridSplineEvaluatorFactory2D: " + + std::to_string(num_out_of_bounds) + + " point(s) found outside uniform grid domain"); + } + + UniformGridFieldLocalizationHint<2> hint( + cell_indices_device, coordinates_d, policy.mode, is_out_of_bounds_device, + num_out_of_bounds); + return std::make_unique>( + layout_, std::move(hint), policy.fill_value); + } + + CoordinateView GetDOFHolderCoordinates() const override + { + throw pcms_error("UniformGridSplineEvaluatorFactory2D: " + "GetDOFHolderCoordinates not yet implemented"); + } + +private: + std::shared_ptr> layout_; + const UniformGrid<2>& grid_; +}; + +} // namespace pcms + +#endif // PCMS_UNIFORM_GRID_SPLINE_EVALUATOR_FACTORY_H diff --git a/src/pcms/field/field.h b/src/pcms/field/field.h new file mode 100644 index 000000000..54fb67cf5 --- /dev/null +++ b/src/pcms/field/field.h @@ -0,0 +1,76 @@ +#ifndef PCMS_COUPLING_FIELD_H +#define PCMS_COUPLING_FIELD_H + +#include "field_data.h" +#include "field_layout.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/types.h" +#include +#include + +namespace pcms +{ + +class FieldFactory; + +// Field is FieldLayout (topology / coupling identity) plus owned FieldData +template +class Field +{ +public: + Field(Field&&) = default; + Field& operator=(Field&&) = default; + Field(const Field&) = delete; + Field& operator=(const Field&) = delete; + + FieldData& GetData() noexcept { return *data_; } + const FieldData& GetData() const noexcept { return *data_; } + + const FieldLayout& GetLayout() const { return *layout_; } + + Rank1View GetDOFHolderDataHost() const + { + return data_->GetDOFHolderDataHost(); + } + + void SetDOFHolderDataHost(Rank1View v) + { + data_->SetDOFHolderDataHost(v); + } + + Rank1View GetDOFHolderData() const + { + return data_->GetDOFHolderData(); + } + + void SetDOFHolderData(Rank1View v) + { + data_->SetDOFHolderData(v); + } + +private: + class CtorKey + { + CtorKey() = default; + friend class FieldFactory; + }; + + Field(CtorKey, std::shared_ptr layout, + std::unique_ptr> data) + : layout_(std::move(layout)), data_(std::move(data)) + { + } + + friend class FieldFactory; + + std::shared_ptr layout_; + std::unique_ptr> data_; +}; + +using FieldVariant = std::variant, Field, Field, + Field, Field>; + +} // namespace pcms + +#endif // PCMS_COUPLING_FIELD_H diff --git a/src/pcms/field/field_data.h b/src/pcms/field/field_data.h new file mode 100644 index 000000000..eee02a00d --- /dev/null +++ b/src/pcms/field/field_data.h @@ -0,0 +1,76 @@ +#ifndef PCMS_FIELD_DATA_H +#define PCMS_FIELD_DATA_H + +#include "field_metadata.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include +#include + +namespace pcms +{ + +// FieldData stores the data that parameterizes a field function, plus +// metadata. The interpretation of the stored data is defined entirely by the +// FieldEvaluatorFactory that consumes it: +// +// - Mesh-backed fields: DOF holder coefficients (nodal values, modal +// coefficients, etc.) at locations defined by the +// layout. +// - Analytic functions: function parameters (e.g. wavenumber, amplitude). +// - ML surrogates: model weights or a handle to model state. +// +// This generality means FieldData is not restricted to fields with spatial DOF +// locations. HasDOFHolderCoordinates() on the corresponding +// FieldEvaluatorFactory distinguishes spatially-located fields from those +// whose parameter data has no physical-space interpretation. +// +// Concrete backends manage the ownership and storage policy for the underlying +// data. +// +// Contract: +// - GetMetadata() describes the stored coefficient data. +// - The flattened DOF-holder data ordering must match the layout ordering. +// - Views returned by GetDOFHolderDataHost()/GetDOFHolderData() remain +// valid until this FieldData object is mutated or destroyed. +// - SetDOFHolderDataHost()/SetDOFHolderData() replace the entire stored +// coefficient array; partial updates are not part of this interface. +// - Basic coefficient operations should be expressible from FieldData and +// its associated FieldLayout; callers should not need the originating +// factory for routine get/set workflows. +// +// NOTE: FieldData is being introduced incrementally alongside the existing +// FieldT. Concrete backends will migrate from FieldT to FieldData +// over time. +template +class FieldData +{ +public: + using value_type = T; + + virtual const FieldMetadata& GetMetadata() const = 0; + + // Direct access to flattened DOFHolder-ordered coefficient data. + // The returned view remains valid until the FieldData is mutated or + // destroyed. + virtual Rank1View GetDOFHolderDataHost() const = 0; + virtual void SetDOFHolderDataHost( + Rank1View values) = 0; + + // The returned view remains valid until the FieldData is mutated or + // destroyed. + virtual Rank1View GetDOFHolderData() const = 0; + virtual void SetDOFHolderData( + Rank1View values) = 0; + + virtual ~FieldData() noexcept = default; +}; + +using FieldDataVariant = std::variant< + std::unique_ptr>, std::unique_ptr>, + std::unique_ptr>, std::unique_ptr>, + std::unique_ptr>>; + +} // namespace pcms + +#endif // PCMS_FIELD_DATA_H diff --git a/src/pcms/field/field_evaluator_factory.h b/src/pcms/field/field_evaluator_factory.h new file mode 100644 index 000000000..a6226d979 --- /dev/null +++ b/src/pcms/field/field_evaluator_factory.h @@ -0,0 +1,101 @@ +#ifndef PCMS_FIELD_EVALUATOR_FACTORY_H +#define PCMS_FIELD_EVALUATOR_FACTORY_H + +#include "field_layout.h" +#include "coordinate_system.h" +#include "evaluation_request.h" +#include "out_of_bounds_policy.h" +#include "point_evaluator.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/discretization/discretization.h" +#include + +namespace pcms +{ + +// FieldEvaluatorFactory owns backend-specific knowledge needed to evaluate a +// field (mesh geometry, model architecture, analytic parameters, etc.). It is +// reusable across any number of query point sets and FieldData objects and is +// created once per field configuration. +// +// Contract: +// - A factory instance is tied to one backend/layout configuration. +// - CreatePointEvaluator(...) either returns a fully usable evaluator or +// throws pcms_error immediately; unsupported operations must not be +// deferred to PointEvaluator::Evaluate(). +// - A PointEvaluator created by this factory may be reused with multiple +// FieldData objects only when they are compatible with this factory's +// layout/backend contract. +// - OutOfBoundsPolicy is fixed at evaluator creation time. +// +// FieldEvaluatorFactory is a standalone abstract type rather than being +// merged into concrete field factories (LagrangeFunctionSpace, etc.). This is +// intentional: field families such as analytic functions and ML surrogates can +// be evaluated at arbitrary points but have no meaningful field construction +// operation. Concrete field factories implement both this interface and their +// own field creation interface. +// +// Concrete field factories store a FieldEvaluatorFactory by composition and +// expose CreatePointEvaluator(request) as a convenience that delegates to the +// internal factory. +// +// Example (sketch of a concrete field factory): +// +// class LagrangeFunctionSpace { +// public: +// std::unique_ptr> CreateFieldReal() const; +// std::shared_ptr GetLayout() const; +// CoordinateSystem GetCoordinateSystem() const; +// +// // convenience — delegates to the internal FieldEvaluatorFactory: +// std::unique_ptr> CreatePointEvaluator( +// const EvaluationRequest& request) const; +// +// private: +// std::shared_ptr> evaluator_factory_; +// }; +template +class FieldEvaluatorFactory +{ +public: + // Returns the layout this factory was constructed for. Used by callers to + // verify FieldData compatibility before calling Evaluate. + virtual const FieldLayout& GetLayout() const = 0; + + // The coordinate system that query coordinates must be expressed in when + // calling CreatePointEvaluator. A CoordinateView with a mismatched system + // will produce a descriptive error at CreatePointEvaluator time. + // (Automatic coordinate transform insertion is deferred.) + virtual CoordinateSystem GetCoordinateSystem() const = 0; + + // Whether this factory can supply DOF holder coordinates. True for all + // mesh-backed evaluators (unstructured, structured). False for evaluators + // whose DOF holders have no meaningful physical-space coordinates (e.g. + // Fourier mode indices). BuildInterpolationOperator requires this to be true + // on the target factory. + virtual bool HasDOFHolderCoordinates() const = 0; + + virtual CoordinateView GetDOFHolderCoordinates() const = 0; + + // Whether this factory supports OutOfBoundsMode::NearestBoundary. + // Backends that return false will throw a descriptive error if + // NearestBoundary is requested via OutOfBoundsPolicy. + virtual bool SupportsNearestBoundary() const = 0; + + // Performs all localization work for the given evaluation request and + // returns a PointEvaluator that caches the resolved evaluation state. The + // PointEvaluator may be reused across Evaluate calls as long as the query + // points and underlying geometry/layout remain unchanged. Implementations may + // discard construction-time request metadata once the cached evaluator state + // has been built. On misuse or unsupported capability, implementations + // should throw pcms_error. + virtual std::unique_ptr> CreatePointEvaluator( + const EvaluationRequest& request) const = 0; + + virtual ~FieldEvaluatorFactory() noexcept = default; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_EVALUATOR_FACTORY_H diff --git a/src/pcms/field/field_factory.h b/src/pcms/field/field_factory.h new file mode 100644 index 000000000..8ea5bc8da --- /dev/null +++ b/src/pcms/field/field_factory.h @@ -0,0 +1,95 @@ +#ifndef PCMS_FIELD_FACTORY_H +#define PCMS_FIELD_FACTORY_H + +#include "field.h" +#include "field_data.h" +#include "field_layout.h" +#include "field_metadata.h" +#include "pcms/utility/common.h" +#include "pcms/utility/types.h" +#include + +namespace pcms +{ + +namespace detail +{ + +inline size_t ExpectedFlatFieldDataSize(const FieldLayout& layout) +{ + return static_cast(layout.GetNumOwnedDofHolder()) * + static_cast(layout.GetNumComponents()); +} + +} // namespace detail + +// Compile-time gate: true only for the five supported field value types. +template +inline constexpr bool is_supported_field_type_v = + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v; + +// FieldFactory is an abstract type that can produce +// Fields over a known layout. This is the entire surface the coupler needs +// (GetLayout + CreateField). A FieldFactory makes discrete fields (values on +// DOF holders); it makes no claim about evaluating them as functions. +// +// Communication-only adapters (MFEM, XGC) are FieldFactories. Evaluatable +// spaces are FunctionSpaces, which derive from FieldFactory +class FieldFactory +{ +public: + virtual std::shared_ptr GetLayout() const noexcept = 0; + + virtual ~FieldFactory() noexcept = default; + + // Create a new field with freshly allocated data for this factory. + // Compile-time error for unsupported T; runtime error for T unsupported by + // the concrete backend. + template + [[nodiscard]] Field CreateField(FieldMetadata metadata = {}) const; + + // Expert API: wrap externally constructed field data into a Field for this + // factory. The concrete factory validates backend-specific field-data type + // and storage size compatibility. + template + [[nodiscard]] Field CreateField(std::unique_ptr> data) const; + +protected: + template + static Field WrapField(std::shared_ptr layout, + std::unique_ptr> data) + { + return Field(typename Field::CtorKey{}, std::move(layout), + std::move(data)); + } + + virtual FieldVariant CreateFieldImpl(Type value_type, + FieldMetadata metadata) const = 0; + + virtual FieldVariant CreateFieldImpl(FieldDataVariant data) const = 0; +}; + +template +Field FieldFactory::CreateField(FieldMetadata metadata) const +{ + static_assert(is_supported_field_type_v, + "T is not a supported field type"); + return std::get>(CreateFieldImpl(TypeEnumFromType(), metadata)); +} + +template +Field FieldFactory::CreateField(std::unique_ptr> data) const +{ + static_assert(is_supported_field_type_v, + "T is not a supported field type"); + if (!data) { + throw pcms_error("FieldFactory::CreateField: data must not be null"); + } + return std::get>(CreateFieldImpl(FieldDataVariant{std::move(data)})); +} + +} // namespace pcms + +#endif // PCMS_FIELD_FACTORY_H diff --git a/src/pcms/field_layout.h b/src/pcms/field/field_layout.h similarity index 51% rename from src/pcms/field_layout.h rename to src/pcms/field/field_layout.h index 7614d1ebc..03aef6580 100644 --- a/src/pcms/field_layout.h +++ b/src/pcms/field/field_layout.h @@ -1,10 +1,12 @@ #ifndef PCMS_FIELD_LAYOUT_H #define PCMS_FIELD_LAYOUT_H +#include #include -#include -#include "pcms/field.h" +#include +#include "pcms/discretization/discretization.h" +#include "pcms/utility/types.h" #include "pcms/utility/arrays.h" -#include "pcms/coordinate_system.h" +#include "coordinate_system.h" namespace pcms { @@ -12,24 +14,13 @@ namespace pcms constexpr int ent_offsets_len = 5; using EntOffsetsArray = std::array; -struct PartitionMapping -{ - std::vector indices; - EntOffsetsArray ent_offsets; - - PartitionMapping() { ent_offsets.fill(0); } -}; - using ReversePartitionMap = std::map>; -using ReversePartitionMap2 = std::map; - -template -class FieldT; class FieldLayout { public: - virtual std::unique_ptr> CreateFieldReal() const = 0; + virtual std::shared_ptr GetDiscretization() + const noexcept = 0; // number of components int virtual GetNumComponents() const = 0; @@ -46,12 +37,12 @@ class FieldLayout return GetNumComponents() * GetNumGlobalDofHolder(); }; - virtual Rank1View GetOwned() const = 0; - virtual GlobalIDView GetGids() const = 0; + virtual Rank1View GetOwnedHost() const = 0; + virtual GlobalIDView GetGidsHost() const = 0; // returns true if the field layout is distributed // if the field layout is distributed, the owned and global dofs are the same - virtual bool IsDistributed() = 0; + [[nodiscard]] virtual bool IsDistributed() const = 0; // This class should construct the permutation arrays that are needed // for serialization / deserialization @@ -59,21 +50,15 @@ class FieldLayout virtual EntOffsetsArray GetEntOffsets() const = 0; - virtual ReversePartitionMap2 GetReversePartitionMap( - const redev::Partition& partition) const = 0; - - virtual CoordinateView GetDOFHolderCoordinates() const = 0; + virtual CoordinateView GetDOFHolderCoordinates() const = 0; - // Serialize, Derserialize, ReversePartitionMap? - // GetOwnedDofHolderCoordinates(CoordinateSystem); + virtual int GetDimension() const = 0; - // Serialize(FieldDataView, SerializationBuffer); - // Deserialize(SerializationBuffer, FieldDataView); + virtual Rank1View + GetDOFHolderClassificationDimensionsHost() const = 0; - // Adjacency information - // TODO: Need a GraphView class (simply two rank1 arrays CSR matrix w/o - // values) virtual bool HasAdjacency() = 0; virtual GraphView GetAdjacency(LO - // dim) = 0; + virtual Rank1View + GetDOFHolderClassificationIdsHost() const = 0; virtual ~FieldLayout() noexcept = default; }; diff --git a/src/pcms/field/field_metadata.h b/src/pcms/field/field_metadata.h new file mode 100644 index 000000000..8dfca3c43 --- /dev/null +++ b/src/pcms/field/field_metadata.h @@ -0,0 +1,30 @@ +#ifndef PCMS_FIELD_METADATA_H +#define PCMS_FIELD_METADATA_H + +#include "coordinate_system.h" + +namespace pcms +{ + +enum class FieldValueType +{ + Scalar, + Vector, + Tensor + // Tensor variance and transformation rules are intentionally deferred to a + // future richer metadata model. +}; + +struct FieldMetadata +{ + FieldValueType value_type = FieldValueType::Scalar; + // The coordinate system that field values are expressed in. Evaluate always + // returns values in this system and does not transform them. Callers are + // responsible for any value transformation after Evaluate (coordinate + // transform wiring is deferred). + CoordinateSystem value_coordinate_system = CoordinateSystem::Cartesian; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_METADATA_H diff --git a/src/pcms/field/function_space.h b/src/pcms/field/function_space.h new file mode 100644 index 000000000..3badf34e4 --- /dev/null +++ b/src/pcms/field/function_space.h @@ -0,0 +1,88 @@ +#ifndef PCMS_FUNCTION_SPACE_H +#define PCMS_FUNCTION_SPACE_H + +#include "coordinate_system.h" +#include "evaluation_request.h" +#include "field.h" +#include "field_data.h" +#include "field_factory.h" +#include "field_layout.h" +#include "field_metadata.h" +#include "out_of_bounds_policy.h" +#include "point_evaluator.h" +#include "pcms/discretization/discretization.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/types.h" +#include +#include + +namespace pcms +{ + +// A FunctionSpace allows you to construct fields and evaluators for those fields +class FunctionSpace : public FieldFactory +{ +public: + virtual std::shared_ptr GetDiscretization() + const noexcept + { + return GetLayout()->GetDiscretization(); + } + + virtual CoordinateSystem GetCoordinateSystem() const noexcept = 0; + + // Create a point evaluator for the given evaluation request. + // Compile-time error for unsupported T; runtime error for T or capability + // unsupported by the concrete backend. + // + // EvaluationRequest is a construction-time object: it supplies the query + // coordinates, out-of-bounds policy, and any optional provenance that may + // help the backend choose an optimized localization path. The resulting + // PointEvaluator caches only the resolved state needed for repeated + // Evaluate(...) calls; it is not required to retain the original request. + template + [[nodiscard]] std::unique_ptr> CreatePointEvaluator( + const EvaluationRequest& request) const; + +protected: + virtual PointEvaluatorVariant CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const = 0; +}; + +template +std::unique_ptr> FunctionSpace::CreatePointEvaluator( + const EvaluationRequest& request) const +{ + static_assert(is_supported_field_type_v, + "T is not a supported field type"); + return std::get>>( + CreatePointEvaluatorImpl(TypeEnumFromType(), request)); +} + +inline EvaluationRequest EvaluationRequest::FromCoordinates( + CoordinateView coords, OutOfBoundsPolicy policy) +{ + return EvaluationRequest(coords, nullptr, policy); +} + +inline EvaluationRequest EvaluationRequest::FromLayout( + std::shared_ptr layout, OutOfBoundsPolicy policy) +{ + if (layout == nullptr) { + throw pcms_error("EvaluationRequest::FromLayout: layout must not be null"); + } + // Must evaluate GetDOFHolderCoordinates() before std::move(layout) + auto coords = layout->GetDOFHolderCoordinates(); + return EvaluationRequest(coords, std::move(layout), policy); +} + +inline EvaluationRequest EvaluationRequest::FromFunctionSpace( + const FunctionSpace& function_space, OutOfBoundsPolicy policy) +{ + return FromLayout(function_space.GetLayout(), policy); +} + +} // namespace pcms + +#endif // PCMS_FUNCTION_SPACE_H diff --git a/src/pcms/field/function_space/lagrange.cpp b/src/pcms/field/function_space/lagrange.cpp new file mode 100644 index 000000000..99c8f2864 --- /dev/null +++ b/src/pcms/field/function_space/lagrange.cpp @@ -0,0 +1,292 @@ +#include "pcms/field/function_space/lagrange.h" +#include "pcms/configuration.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/common.h" +#ifdef PCMS_ENABLE_MESHFIELDS +#include "pcms/field/layout/mesh_fields.h" +#include "pcms/field/evaluator/mesh_fields.h" +#include "pcms/field/data/mesh_fields.h" +#endif +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/field/evaluator/omega_h_lagrange.h" +#include "pcms/field/layout/uniform_grid.h" +#include "pcms/field/evaluator/uniform_grid.h" +#include "pcms/field/data/simple.h" +#include "pcms/utility/uniform_grid.h" + +#include + +namespace pcms +{ + +namespace +{ + +template +void ValidateLagrangeWrappedFieldData(const FieldLayout& layout, + const FieldData& data) +{ +#ifdef PCMS_ENABLE_MESHFIELDS + if (dynamic_cast(&layout) != nullptr) { + if (dynamic_cast*>(&data) == nullptr) { + throw pcms_error( + "LagrangeFunctionSpace::CreateField: MeshFields layout requires " + "MeshFieldsFieldData"); + } + } else +#endif + { + if (dynamic_cast*>(&data) == nullptr) { + throw pcms_error( + "LagrangeFunctionSpace::CreateField: this backend requires " + "SimpleFieldData"); + } + } + + if (data.GetDOFHolderDataHost().size() != + detail::ExpectedFlatFieldDataSize(layout)) { + throw pcms_error( + "LagrangeFunctionSpace::CreateField: field data size does not match " + "layout"); + } +} + +} // namespace + +LagrangeFunctionSpace::LagrangeFunctionSpace( + std::shared_ptr layout, + std::function create_field_data_fn, + std::shared_ptr> evaluator_factory) noexcept + : layout_(std::move(layout)), + create_field_data_fn_(std::move(create_field_data_fn)), + evaluator_factory_(std::move(evaluator_factory)) +{ +} + +LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( + Omega_h::Mesh& mesh, int order, int num_components, + CoordinateSystem coordinate_system, std::string global_id_name, + Backend backend) +{ + // https://github.com/SCOREC/meshFields/issues/88 + if (backend == Backend::MeshFields && order == 0) { + // MeshFields does not support order-0 fields; fall back to OmegaH. + backend = Backend::OmegaH; + } + if (backend == Backend::MeshFields) { +#ifdef PCMS_ENABLE_MESHFIELDS + if (num_components != 1) { + throw pcms_error( + "LagrangeFunctionSpace::FromMesh: MeshFields backend only supports " + "single-component fields"); + } + std::array nodes_per_dim{}; + switch (order) { + case 1: nodes_per_dim = {1, 0, 0, 0}; break; + case 2: nodes_per_dim = {1, 1, 0, 0}; break; + default: throw pcms_error("Unimplemented Lagrange order"); + } + auto mesh_layout = std::make_shared( + mesh, nodes_per_dim, num_components, coordinate_system, + std::move(global_id_name)); + auto eval_factory = + std::make_shared>(mesh_layout); + return LagrangeFunctionSpace( + mesh_layout, + [mesh_layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + if (t == Type::Float) { + if constexpr (std::is_same_v || + std::is_same_v) { + return std::make_unique>(mesh_layout, + metadata); + } + throw pcms_error( + "LagrangeFunctionSpace: MeshFields backend does not support " + "float in this build"); + } + if (t == Type::Real) { + return std::make_unique>(mesh_layout, + metadata); + } + throw pcms_error( + "LagrangeFunctionSpace: MeshFields backend only supports the " + "MeshFields scalar types enabled in this build"); + }, + std::move(eval_factory)); +#else + throw pcms_error( + "LagrangeFunctionSpace::FromMesh: MeshFields backend requested but " + "PCMS_ENABLE_MESHFIELDS is not set"); +#endif + } + if (order > 1) { + throw pcms_error( + "LagrangeFunctionSpace::FromMesh: OmegaH backend only supports " + "Lagrange orders 0 and 1"); + } + auto layout = std::make_shared( + mesh, order, num_components, coordinate_system, std::move(global_id_name)); + auto eval_factory = + std::make_shared>(layout); + return LagrangeFunctionSpace( + layout, + [layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + return apply_to_type(t, [&](auto tag) -> FieldDataVariant { + using T = typename decltype(tag)::type; + if constexpr (std::is_same_v) { + throw pcms_error("LagrangeFunctionSpace: int8_t is not supported"); + } else { + return std::make_unique>(layout, metadata); + } + }); + }, + std::move(eval_factory)); +} + +LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( + Omega_h::Mesh& mesh, int order, int num_components, + CoordinateSystem coordinate_system, Omega_h::Read owned_mask, + std::string global_id_name, Backend backend) +{ + if (backend == Backend::MeshFields) { + throw pcms_error( + "LagrangeFunctionSpace::FromMesh: owned-mask construction is only " + "supported by the OmegaH backend"); + } + if (order > 1) { + throw pcms_error( + "LagrangeFunctionSpace::FromMesh: OmegaH backend only supports " + "Lagrange orders 0 and 1"); + } + auto layout = std::make_shared( + mesh, order, num_components, coordinate_system, std::move(owned_mask), + std::move(global_id_name)); + auto eval_factory = + std::make_shared>(layout); + return LagrangeFunctionSpace( + layout, + [layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + return apply_to_type(t, [&](auto tag) -> FieldDataVariant { + using T = typename decltype(tag)::type; + if constexpr (std::is_same_v) { + throw pcms_error("LagrangeFunctionSpace: int8_t is not supported"); + } else { + return std::make_unique>(layout, metadata); + } + }); + }, + std::move(eval_factory)); +} + +LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( + const UniformGrid<2>& grid, int num_components, + CoordinateSystem coordinate_system, int order) +{ + if (order != 0 && order != 1) { + throw std::invalid_argument("LagrangeFunctionSpace::FromUniformGrid: only " + "orders 0 and 1 are supported"); + } + auto ug_layout = std::make_shared>( + grid, num_components, coordinate_system, order); + auto eval_factory = + std::make_shared>(ug_layout); + return LagrangeFunctionSpace( + ug_layout, + [ug_layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + return apply_to_type(t, [&](auto tag) -> FieldDataVariant { + using T = typename decltype(tag)::type; + if constexpr (std::is_same_v) { + throw pcms_error("LagrangeFunctionSpace: int8_t is not supported"); + } else { + return std::make_unique>(ug_layout, metadata); + } + }); + }, + std::move(eval_factory)); +} + +LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( + const UniformGrid<3>& grid, int num_components, + CoordinateSystem coordinate_system, int order) +{ + if (order != 0 && order != 1) { + throw std::invalid_argument("LagrangeFunctionSpace::FromUniformGrid: only " + "orders 0 and 1 are supported"); + } + auto ug_layout = std::make_shared>( + grid, num_components, coordinate_system, order); + auto eval_factory = + std::make_shared>(ug_layout); + return LagrangeFunctionSpace( + ug_layout, + [ug_layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + return apply_to_type(t, [&](auto tag) -> FieldDataVariant { + using T = typename decltype(tag)::type; + if constexpr (std::is_same_v) { + throw pcms_error("LagrangeFunctionSpace: int8_t is not supported"); + } else { + return std::make_unique>(ug_layout, metadata); + } + }); + }, + std::move(eval_factory)); +} + +std::shared_ptr LagrangeFunctionSpace::GetLayout() + const noexcept +{ + return layout_; +} + +CoordinateSystem LagrangeFunctionSpace::GetCoordinateSystem() const noexcept +{ + return evaluator_factory_->GetCoordinateSystem(); +} + +FieldVariant LagrangeFunctionSpace::CreateFieldImpl( + Type value_type, FieldMetadata metadata) const +{ + auto field_data = create_field_data_fn_(value_type, metadata); + return std::visit( + [this](auto&& fd) -> FieldVariant { + using FD = std::decay_t; + using T = typename FD::element_type::value_type; + return WrapField(layout_, std::forward(fd)); + }, + std::move(field_data)); +} + +FieldVariant LagrangeFunctionSpace::CreateFieldImpl(FieldDataVariant data) const +{ + return std::visit( + [this](auto&& fd) -> FieldVariant { + using FD = std::decay_t; + using T = typename FD::element_type::value_type; + PCMS_ALWAYS_ASSERT(fd != nullptr); + if constexpr (std::is_same_v) { + throw pcms_error( + "LagrangeFunctionSpace: int8_t is not a supported field type"); + } else { + ValidateLagrangeWrappedFieldData(*layout_, *fd); + return WrapField(layout_, std::forward(fd)); + } + }, + std::move(data)); +} + +PointEvaluatorVariant LagrangeFunctionSpace::CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const +{ + if (value_type != Type::Real) { + throw pcms_error( + "LagrangeFunctionSpace: point evaluation only supports double (Real)"); + } + if (!evaluator_factory_) { + throw pcms_error( + "LagrangeFunctionSpace::CreatePointEvaluatorImpl: evaluator construction " + "is not available for this backend"); + } + return evaluator_factory_->CreatePointEvaluator(request); +} + +} // namespace pcms diff --git a/src/pcms/field/function_space/lagrange.h b/src/pcms/field/function_space/lagrange.h new file mode 100644 index 000000000..54b2f695e --- /dev/null +++ b/src/pcms/field/function_space/lagrange.h @@ -0,0 +1,92 @@ +#ifndef PCMS_LAGRANGE_FIELD_FACTORY_H +#define PCMS_LAGRANGE_FIELD_FACTORY_H + +#include +#include "pcms/configuration.h" +#include "pcms/field/field.h" +#include "pcms/field/field_layout.h" +#include "pcms/field/function_space.h" +#include "pcms/field/field_data.h" +#include "pcms/field/field_metadata.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/field_evaluator_factory.h" +#include "../coordinate_system.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/types.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/uniform_grid.h" + +#include +#include +#include +#include + +namespace pcms +{ + +class LagrangeFunctionSpace : public FunctionSpace +{ +public: + enum class Backend + { + MeshFields, + OmegaH + }; + +#ifdef PCMS_ENABLE_MESHFIELDS + static constexpr Backend DefaultBackend = Backend::MeshFields; +#else + static constexpr Backend DefaultBackend = Backend::OmegaH; +#endif + + // Unstructured mesh — dispatches to MeshFields or native Omega_h backend + [[nodiscard]] static LagrangeFunctionSpace FromMesh( + Omega_h::Mesh& mesh, int order, int num_components, + CoordinateSystem coordinate_system, std::string global_id_name = "global", + Backend backend = DefaultBackend); + + [[nodiscard]] static LagrangeFunctionSpace FromMesh( + Omega_h::Mesh& mesh, int order, int num_components, + CoordinateSystem coordinate_system, Omega_h::Read owned_mask, + std::string global_id_name = "global", Backend backend = DefaultBackend); + + // Structured uniform grid — order-1 H1-conforming nodal field on a regular + // grid + [[nodiscard]] static LagrangeFunctionSpace FromUniformGrid( + const UniformGrid<2>& grid, int num_components, + CoordinateSystem coordinate_system, int order = 1); + + [[nodiscard]] static LagrangeFunctionSpace FromUniformGrid( + const UniformGrid<3>& grid, int num_components, + CoordinateSystem coordinate_system, int order = 1); + + [[nodiscard]] std::shared_ptr GetLayout() + const noexcept override; + + [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept override; + +protected: + [[nodiscard]] FieldVariant CreateFieldImpl( + Type value_type, FieldMetadata metadata) const override; + + [[nodiscard]] FieldVariant CreateFieldImpl( + FieldDataVariant data) const override; + + [[nodiscard]] PointEvaluatorVariant CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const override; + +private: + explicit LagrangeFunctionSpace( + std::shared_ptr layout, + std::function create_field_data_fn, + std::shared_ptr> evaluator_factory) noexcept; + + std::shared_ptr layout_; + std::function create_field_data_fn_; + std::shared_ptr> evaluator_factory_; +}; + +} // namespace pcms + +#endif // PCMS_LAGRANGE_FIELD_FACTORY_H diff --git a/src/pcms/field/function_space/mfem.h b/src/pcms/field/function_space/mfem.h new file mode 100644 index 000000000..d1beafb36 --- /dev/null +++ b/src/pcms/field/function_space/mfem.h @@ -0,0 +1,79 @@ +#ifndef PCMS_FUNCTION_SPACE_MFEM_H +#define PCMS_FUNCTION_SPACE_MFEM_H + +#include "pcms/field/coordinate_system.h" +#include "pcms/field/data/mfem.h" +#include "pcms/field/field_factory.h" +#include "pcms/field/layout/mfem.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/common.h" + +#include + +#include +#include + +namespace pcms +{ + +// Field factory for an MFEM order-1 H1 (vertex) scalar +// field. The produced field's data is bound to the live mfem::ParGridFunction, +// so coupled get/set operate on the solver's state. +// Lifetime: the factory and any Field it produces must not outlive +// pmesh / pfes / gf. +class MFEMFieldFactory : public FieldFactory +{ +public: + MFEMFieldFactory(mfem::ParMesh& pmesh, mfem::ParFiniteElementSpace& pfes, + mfem::ParGridFunction& gf, + CoordinateSystem coordinate_system) + : layout_( + std::make_shared(pmesh, pfes, coordinate_system)), + pfes_(pfes), + gf_(gf) + { + } + + [[nodiscard]] std::shared_ptr GetLayout() + const noexcept override + { + return layout_; + } + +protected: + [[nodiscard]] FieldVariant CreateFieldImpl( + Type value_type, FieldMetadata metadata) const override + { + if (value_type != Type::Real) { + throw pcms_error("MFEM adapter only supports Real (double) fields"); + } + return WrapField( + layout_, std::make_unique(pfes_, gf_, metadata)); + } + + [[nodiscard]] FieldVariant CreateFieldImpl( + FieldDataVariant data) const override + { + auto* real = std::get_if>>(&data); + if (real == nullptr || *real == nullptr || + dynamic_cast(real->get()) == nullptr) { + throw pcms_error( + "MFEMFieldFactory::CreateField: requires MFEMVertexFieldData"); + } + if ((*real)->GetDOFHolderDataHost().size() != + detail::ExpectedFlatFieldDataSize(*layout_)) { + throw pcms_error("MFEMFieldFactory::CreateField: field data size does " + "not match layout"); + } + return WrapField(layout_, std::move(*real)); + } + +private: + std::shared_ptr layout_; + mfem::ParFiniteElementSpace& pfes_; + mfem::ParGridFunction& gf_; +}; + +} // namespace pcms + +#endif // PCMS_FUNCTION_SPACE_MFEM_H diff --git a/src/pcms/field/function_space/polynomial_reconstruction.cpp b/src/pcms/field/function_space/polynomial_reconstruction.cpp new file mode 100644 index 000000000..d26beec14 --- /dev/null +++ b/src/pcms/field/function_space/polynomial_reconstruction.cpp @@ -0,0 +1,138 @@ +#include "pcms/field/function_space/polynomial_reconstruction.hpp" +#include "pcms/field/layout/omega_h_entity.h" +#include "pcms/field/layout/point_cloud.h" +#include "pcms/field/evaluator/point_cloud.h" +#include "pcms/field/data/simple.h" +#include "pcms/discretization/discretization/omega_h.hpp" +#include "pcms/utility/assert.h" +#include "pcms/utility/common.h" +#include "pcms/utility/mesh_geometry.h" +#include "pcms/localization/point_cloud_localization.h" +#include "pcms/localization/mesh_localization.h" + +#include +#include + +namespace pcms +{ + +PolynomialReconstructionFunctionSpace::PolynomialReconstructionFunctionSpace( + std::shared_ptr layout, + std::shared_ptr> evaluator_factory) noexcept + : layout_(std::move(layout)), evaluator_factory_(std::move(evaluator_factory)) +{ +} + +PolynomialReconstructionFunctionSpace +PolynomialReconstructionFunctionSpace::Create( + Rank2View coords, CoordinateSystem coordinate_system, + MLSOptions options) +{ + int dim = static_cast(coords.extent(1)); + Kokkos::View host_view( + coords.data_handle(), coords.extent(0), coords.extent(1)); + auto device_view = Kokkos::View("device_view", host_view.extent(0), + host_view.extent(1)); + DeepCopyMismatchLayouts(device_view, host_view); + auto pc_layout = + std::make_shared(dim, device_view, coordinate_system); + auto localization = + std::make_shared(pc_layout, options); + auto eval_factory = std::make_shared( + pc_layout, localization, options); + return PolynomialReconstructionFunctionSpace(pc_layout, + std::move(eval_factory)); +} + +PolynomialReconstructionFunctionSpace +PolynomialReconstructionFunctionSpace::FromMesh( + Omega_h::Mesh& mesh, int source_entity_dim, + CoordinateSystem coordinate_system, MLSOptions options) +{ + if (coordinate_system != CoordinateSystem::Cartesian) { + throw pcms_error( + "PolynomialReconstructionFunctionSpace::FromMesh: only Cartesian " + "coordinates are currently supported for MLS"); + } + if (source_entity_dim < 0 || source_entity_dim > mesh.dim()) { + throw pcms_error( + "PolynomialReconstructionFunctionSpace::FromMesh: source_entity_dim is " + "out of range"); + } + + auto mesh_layout = std::make_shared( + mesh, source_entity_dim, 1, coordinate_system); + auto localization = std::make_shared( + mesh, source_entity_dim, options); + auto eval_factory = std::make_shared( + mesh_layout, localization, options); + return PolynomialReconstructionFunctionSpace(mesh_layout, + std::move(eval_factory)); +} + +std::shared_ptr +PolynomialReconstructionFunctionSpace::GetLayout() const noexcept +{ + return layout_; +} + +CoordinateSystem PolynomialReconstructionFunctionSpace::GetCoordinateSystem() + const noexcept +{ + return evaluator_factory_->GetCoordinateSystem(); +} + +FieldVariant PolynomialReconstructionFunctionSpace::CreateFieldImpl( + Type value_type, FieldMetadata metadata) const +{ + return apply_to_type(value_type, [&](auto tag) -> FieldVariant { + using T = typename decltype(tag)::type; + if constexpr (!std::is_same_v) { + throw pcms_error( + "PolynomialReconstructionFunctionSpace: only double (Real) is " + "supported"); + } else { + return WrapField( + layout_, std::make_unique>(layout_, metadata)); + } + }); +} + +FieldVariant PolynomialReconstructionFunctionSpace::CreateFieldImpl( + FieldDataVariant data) const +{ + if (!std::holds_alternative>>(data)) { + throw pcms_error( + "PolynomialReconstructionFunctionSpace: only double (Real) is " + "supported"); + } + auto fd = std::move(std::get>>(data)); + PCMS_ALWAYS_ASSERT(fd != nullptr); + if (dynamic_cast*>(fd.get()) == nullptr) { + throw pcms_error( + "PolynomialReconstructionFunctionSpace::CreateField: requires " + "SimpleFieldData"); + } + if (fd->GetDOFHolderDataHost().size() != + detail::ExpectedFlatFieldDataSize(*layout_)) { + throw pcms_error( + "PolynomialReconstructionFunctionSpace::CreateField: field data size " + "does not match layout"); + } + return WrapField(layout_, std::move(fd)); +} + +PointEvaluatorVariant +PolynomialReconstructionFunctionSpace::CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const +{ + if (value_type != Type::Real) { + throw pcms_error( + "PolynomialReconstructionFunctionSpace: point evaluation only supports " + "double (Real)"); + } + PCMS_ALWAYS_ASSERT(evaluator_factory_ != nullptr); + return evaluator_factory_->CreatePointEvaluator(request); +} + +} // namespace pcms diff --git a/src/pcms/field/function_space/polynomial_reconstruction.hpp b/src/pcms/field/function_space/polynomial_reconstruction.hpp new file mode 100644 index 000000000..0222c282f --- /dev/null +++ b/src/pcms/field/function_space/polynomial_reconstruction.hpp @@ -0,0 +1,61 @@ +#ifndef PCMS_POLYNOMIAL_RECONSTRUCTION_FUNCTION_SPACE_H +#define PCMS_POLYNOMIAL_RECONSTRUCTION_FUNCTION_SPACE_H + +#include "pcms/field/field.h" +#include "pcms/field/field_layout.h" +#include "pcms/field/field_data.h" +#include "pcms/field/field_metadata.h" +#include "pcms/field/function_space.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/field/evaluator/mls_options.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" + +#include +#include + +namespace pcms +{ + +template +class FieldEvaluatorFactory; + +class PolynomialReconstructionFunctionSpace : public FunctionSpace +{ +public: + [[nodiscard]] static PolynomialReconstructionFunctionSpace Create( + Rank2View coords, CoordinateSystem coordinate_system, + MLSOptions options = {}); + + [[nodiscard]] static PolynomialReconstructionFunctionSpace FromMesh( + Omega_h::Mesh& mesh, int source_entity_dim, + CoordinateSystem coordinate_system, MLSOptions options = {}); + + [[nodiscard]] std::shared_ptr GetLayout() + const noexcept override; + + [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept override; + +protected: + [[nodiscard]] FieldVariant CreateFieldImpl( + Type value_type, FieldMetadata metadata) const override; + + [[nodiscard]] FieldVariant CreateFieldImpl( + FieldDataVariant data) const override; + + [[nodiscard]] PointEvaluatorVariant CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const override; + +private: + explicit PolynomialReconstructionFunctionSpace( + std::shared_ptr layout, + std::shared_ptr> evaluator_factory) noexcept; + + std::shared_ptr layout_; + std::shared_ptr> evaluator_factory_; +}; + +} // namespace pcms + +#endif // PCMS_POLYNOMIAL_RECONSTRUCTION_FUNCTION_SPACE_H diff --git a/src/pcms/field/function_space/spline.cpp b/src/pcms/field/function_space/spline.cpp new file mode 100644 index 000000000..167048b8e --- /dev/null +++ b/src/pcms/field/function_space/spline.cpp @@ -0,0 +1,90 @@ +#include "pcms/field/function_space/spline.h" + +#include "pcms/field/data/simple.h" +#include "pcms/field/evaluator/uniform_grid_spline.h" +#include "pcms/field/layout/uniform_grid.h" +#include "pcms/utility/common.h" + +#include + +namespace pcms +{ + +SplineFunctionSpace::SplineFunctionSpace( + std::shared_ptr layout, + std::shared_ptr> evaluator_factory) noexcept + : layout_(std::move(layout)), evaluator_factory_(std::move(evaluator_factory)) +{ +} + +SplineFunctionSpace SplineFunctionSpace::FromUniformGrid( + const UniformGrid<2>& grid, CoordinateSystem coordinate_system) +{ + auto layout = + std::make_shared>(grid, 1, coordinate_system, 1); + auto evaluator_factory = + std::make_shared(layout); + return SplineFunctionSpace(layout, std::move(evaluator_factory)); +} + +std::shared_ptr SplineFunctionSpace::GetLayout() + const noexcept +{ + return layout_; +} + +CoordinateSystem SplineFunctionSpace::GetCoordinateSystem() const noexcept +{ + return evaluator_factory_->GetCoordinateSystem(); +} + +FieldVariant SplineFunctionSpace::CreateFieldImpl(Type value_type, + FieldMetadata metadata) const +{ + return apply_to_type(value_type, [&](auto tag) -> FieldVariant { + using T = typename decltype(tag)::type; + if constexpr (!std::is_same_v) { + throw pcms_error("SplineFunctionSpace: only double (Real) is supported"); + } else { + return WrapField( + layout_, std::make_unique>(layout_, metadata)); + } + }); +} + +FieldVariant SplineFunctionSpace::CreateFieldImpl(FieldDataVariant data) const +{ + if (!std::holds_alternative>>(data)) { + throw pcms_error("SplineFunctionSpace: only double (Real) is supported"); + } + auto fd = std::move(std::get>>(data)); + PCMS_ALWAYS_ASSERT(fd != nullptr); + if (dynamic_cast*>(fd.get()) == nullptr) { + throw pcms_error( + "SplineFunctionSpace::CreateField: requires SimpleFieldData"); + } + if (fd->GetDOFHolderDataHost().size() != + detail::ExpectedFlatFieldDataSize(*layout_)) { + throw pcms_error( + "SplineFunctionSpace::CreateField: field data size does not match " + "layout"); + } + return WrapField(layout_, std::move(fd)); +} + +PointEvaluatorVariant SplineFunctionSpace::CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const +{ + if (value_type != Type::Real) { + throw pcms_error( + "SplineFunctionSpace: point evaluation only supports double (Real)"); + } + if (!evaluator_factory_) { + throw pcms_error( + "SplineFunctionSpace::CreatePointEvaluatorImpl: evaluator construction " + "is not available for this backend"); + } + return evaluator_factory_->CreatePointEvaluator(request); +} + +} // namespace pcms diff --git a/src/pcms/field/function_space/spline.h b/src/pcms/field/function_space/spline.h new file mode 100644 index 000000000..a744784d9 --- /dev/null +++ b/src/pcms/field/function_space/spline.h @@ -0,0 +1,53 @@ +#ifndef PCMS_SPLINE_FUNCTION_SPACE_H +#define PCMS_SPLINE_FUNCTION_SPACE_H + +#include "pcms/field/field.h" +#include "pcms/field/field_data.h" +#include "pcms/field/field_evaluator_factory.h" +#include "pcms/field/field_layout.h" +#include "pcms/field/field_metadata.h" +#include "pcms/field/function_space.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/uniform_grid.h" + +#include + +namespace pcms +{ + +class SplineFunctionSpace : public FunctionSpace +{ +public: + [[nodiscard]] static SplineFunctionSpace FromUniformGrid( + const UniformGrid<2>& grid, CoordinateSystem coordinate_system); + + [[nodiscard]] std::shared_ptr GetLayout() + const noexcept override; + + [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept override; + +protected: + [[nodiscard]] FieldVariant CreateFieldImpl( + Type value_type, FieldMetadata metadata) const override; + + [[nodiscard]] FieldVariant CreateFieldImpl( + FieldDataVariant data) const override; + + [[nodiscard]] PointEvaluatorVariant CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const override; + +private: + explicit SplineFunctionSpace( + std::shared_ptr layout, + std::shared_ptr> evaluator_factory) noexcept; + + std::shared_ptr layout_; + std::shared_ptr> evaluator_factory_; +}; + +} // namespace pcms + +#endif // PCMS_SPLINE_FUNCTION_SPACE_H diff --git a/src/pcms/field/function_space/xgc.h b/src/pcms/field/function_space/xgc.h new file mode 100644 index 000000000..c1a3564a0 --- /dev/null +++ b/src/pcms/field/function_space/xgc.h @@ -0,0 +1,86 @@ +#ifndef PCMS_XGC_FIELD_FACTORY_H +#define PCMS_XGC_FIELD_FACTORY_H + +#include "pcms/field/coordinate_system.h" +#include "pcms/field/data/xgc.h" +#include "pcms/field/field.h" +#include "pcms/field/field_factory.h" +#include "pcms/field/field_metadata.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/common.h" +#include +#include + +namespace pcms +{ + +// Field factory for XGC fields. +class XGCFieldFactory : public FieldFactory +{ +public: + XGCFieldFactory(const ReverseClassificationVertex& reverse_classification, + std::function in_overlap, + LO num_plane_nodes) + : layout_(std::make_shared( + reverse_classification, std::move(in_overlap), num_plane_nodes)) + { + } + + [[nodiscard]] std::shared_ptr GetLayout() + const noexcept override + { + return layout_; + } + + [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept + { + return CoordinateSystem::XGC; + } + + [[nodiscard]] std::shared_ptr GetXGCLayout() + const noexcept + { + return layout_; + } + +protected: + [[nodiscard]] FieldVariant CreateFieldImpl( + Type value_type, FieldMetadata metadata) const override + { + return apply_to_type(value_type, [&](auto tag) -> FieldVariant { + using T = typename decltype(tag)::type; + return WrapField(layout_, + std::make_unique>(layout_, metadata)); + }); + } + + [[nodiscard]] FieldVariant CreateFieldImpl( + FieldDataVariant data) const override + { + return std::visit( + [this](auto&& fd) -> FieldVariant { + using FD = std::decay_t; + using T = typename FD::element_type::value_type; + PCMS_ALWAYS_ASSERT(fd != nullptr); + if (dynamic_cast*>(fd.get()) == nullptr) { + throw pcms_error( + "XGCFieldFactory::CreateField: requires XGCFieldData"); + } + if (fd->GetDOFHolderDataHost().size() != + static_cast(layout_->GetFullDataSize())) { + throw pcms_error( + "XGCFieldFactory::CreateField: field data size does not match " + "layout"); + } + return WrapField(layout_, std::move(fd)); + }, + std::move(data)); + } + +private: + std::shared_ptr layout_; +}; + +} // namespace pcms + +#endif // PCMS_XGC_FIELD_FACTORY_H diff --git a/src/pcms/field/layout/empty.cpp b/src/pcms/field/layout/empty.cpp new file mode 100644 index 000000000..83fef4858 --- /dev/null +++ b/src/pcms/field/layout/empty.cpp @@ -0,0 +1,85 @@ +#include "pcms/field/layout/empty.h" + +namespace pcms +{ + +EmptyFieldLayout::EmptyFieldLayout() + : owned_("null_owned", 0), + gids_("null_gids", 0), + class_dims_("null_class_dims", 0), + class_ids_("null_class_ids", 0), + owned_host_("null_owned_host", 0), + gids_host_("null_gids_host", 0), + classification_dims_host_("null_classification_dims_host", 0), + classification_ids_host_("null_classification_ids_host", 0), + coords_("null_coords", 0, 2) +{ + discretization_ = std::make_shared(); +} + +std::shared_ptr EmptyFieldLayout::GetDiscretization() + const noexcept +{ + return discretization_; +} + +int EmptyFieldLayout::GetNumComponents() const +{ + return 1; +} + +LO EmptyFieldLayout::GetNumOwnedDofHolder() const +{ + return 0; +} + +GO EmptyFieldLayout::GetNumGlobalDofHolder() const +{ + return 0; +} + +Rank1View EmptyFieldLayout::GetOwnedHost() const +{ + return make_const_array_view(owned_host_); +} + +GlobalIDView EmptyFieldLayout::GetGidsHost() const +{ + return make_const_array_view(gids_host_); +} + +bool EmptyFieldLayout::IsDistributed() const +{ + return false; +} + +EntOffsetsArray EmptyFieldLayout::GetEntOffsets() const +{ + return {0, 0, 0, 0, 0}; +} + +CoordinateView EmptyFieldLayout::GetDOFHolderCoordinates() + const +{ + auto coords_view = MakeConstRank2View(coords_); + return CoordinateView{CoordinateSystem::XGC, coords_view}; +} + +int EmptyFieldLayout::GetDimension() const +{ + return 2; +} + +Rank1View +EmptyFieldLayout::GetDOFHolderClassificationDimensionsHost() const +{ + return make_const_array_view(classification_dims_host_); +} + +Rank1View +EmptyFieldLayout::GetDOFHolderClassificationIdsHost() const +{ + return make_const_array_view(classification_ids_host_); +} + +} // namespace pcms diff --git a/src/pcms/field/layout/empty.h b/src/pcms/field/layout/empty.h new file mode 100644 index 000000000..991d58ff8 --- /dev/null +++ b/src/pcms/field/layout/empty.h @@ -0,0 +1,48 @@ +#ifndef PCMS_EMPTY_FIELD_LAYOUT_H +#define PCMS_EMPTY_FIELD_LAYOUT_H + +#include "pcms/discretization/discretization/empty.hpp" +#include "pcms/field/field_layout.h" +#include + +namespace pcms +{ + +class EmptyFieldLayout : public FieldLayout +{ +public: + EmptyFieldLayout(); + + std::shared_ptr GetDiscretization() + const noexcept override; + + int GetNumComponents() const override; + LO GetNumOwnedDofHolder() const override; + GO GetNumGlobalDofHolder() const override; + Rank1View GetOwnedHost() const override; + GlobalIDView GetGidsHost() const override; + bool IsDistributed() const override; + EntOffsetsArray GetEntOffsets() const override; + CoordinateView GetDOFHolderCoordinates() const override; + int GetDimension() const override; + Rank1View + GetDOFHolderClassificationDimensionsHost() const override; + Rank1View GetDOFHolderClassificationIdsHost() + const override; + +private: + Kokkos::View owned_; + Kokkos::View gids_; + Kokkos::View class_dims_; + Kokkos::View class_ids_; + Kokkos::View owned_host_; + Kokkos::View gids_host_; + Kokkos::View classification_dims_host_; + Kokkos::View classification_ids_host_; + Kokkos::View coords_; + std::shared_ptr discretization_; +}; + +} // namespace pcms + +#endif // PCMS_EMPTY_FIELD_LAYOUT_H diff --git a/src/pcms/adapter/meshfields/mesh_fields_adapter_layout.cpp b/src/pcms/field/layout/mesh_fields.cpp similarity index 76% rename from src/pcms/adapter/meshfields/mesh_fields_adapter_layout.cpp rename to src/pcms/field/layout/mesh_fields.cpp index 39c0cb922..fa0866623 100644 --- a/src/pcms/adapter/meshfields/mesh_fields_adapter_layout.cpp +++ b/src/pcms/field/layout/mesh_fields.cpp @@ -1,9 +1,8 @@ -#include "mesh_fields_adapter.h" -#include "mesh_fields_adapter2.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter_layout.h" -#include "mesh_fields_adapter_layout.h" +#include "pcms/field/layout/mesh_fields.h" +#include "pcms/utility/assert.h" #include "pcms/utility/inclusive_scan.h" #include "pcms/utility/profile.h" +#include #include namespace pcms @@ -135,8 +134,6 @@ MeshFieldsAdapterLayout::MeshFieldsAdapterLayout( coordinate_system_(coordinate_system), nodes_per_dim_(nodes_per_dim), dof_holder_coords_("", GetNumOwnedDofHolder(), mesh_.dim()), - dof_holder_coords_host_("dof_holder_coords_host", GetNumOwnedDofHolder(), - mesh_.dim()), class_ids_(GetNumEnts()), class_dims_(class_ids_.size()), owned_("", class_dims_.size()), @@ -196,11 +193,26 @@ MeshFieldsAdapterLayout::MeshFieldsAdapterLayout( } } gids_host_ = Omega_h::HostWrite(gids_); + + int n = class_ids_.size(); + classification_dims_host_ = + Kokkos::View("classification_dims", n); + classification_ids_host_ = + Kokkos::View("classification_ids", n); + auto class_dims_h = Omega_h::HostRead(class_dims_); + auto class_ids_h = Omega_h::HostRead(class_ids_); + for (int i = 0; i < n; ++i) { + classification_dims_host_(i) = + static_cast(static_cast(class_dims_h[i])); + classification_ids_host_(i) = static_cast(class_ids_h[i]); + } + discretization_ = std::make_shared(mesh_); } -std::unique_ptr> MeshFieldsAdapterLayout::CreateFieldReal() const +std::shared_ptr +MeshFieldsAdapterLayout::GetDiscretization() const noexcept { - return std::make_unique>(*this); + return discretization_; } int MeshFieldsAdapterLayout::GetNumComponents() const @@ -231,27 +243,26 @@ std::array MeshFieldsAdapterLayout::GetNodesPerDim() const return nodes_per_dim_; } -Rank1View MeshFieldsAdapterLayout::GetOwned() const +Rank1View MeshFieldsAdapterLayout::GetOwnedHost() + const { Kokkos::deep_copy(owned_host_, owned_); return make_const_array_view(owned_host_); } -GlobalIDView MeshFieldsAdapterLayout::GetGids() const +GlobalIDView MeshFieldsAdapterLayout::GetGidsHost() const { return GlobalIDView(gids_host_.data(), gids_host_.size()); } -CoordinateView +CoordinateView MeshFieldsAdapterLayout::GetDOFHolderCoordinates() const { - deep_copy_mismatch_layouts(dof_holder_coords_host_, dof_holder_coords_); - Rank2View coords_view( - dof_holder_coords_host_.data(), dof_holder_coords_host_.extent(0), 2); - return CoordinateView{coordinate_system_, coords_view}; + auto coords_view = MakeConstRank2View(dof_holder_coords_); + return CoordinateView{coordinate_system_, coords_view}; } -bool MeshFieldsAdapterLayout::IsDistributed() +bool MeshFieldsAdapterLayout::IsDistributed() const { return true; } @@ -296,49 +307,21 @@ EntOffsetsArray MeshFieldsAdapterLayout::GetEntOffsets() const return offsets; } -ReversePartitionMap2 MeshFieldsAdapterLayout::GetReversePartitionMap( - const redev::Partition& partition) const +int MeshFieldsAdapterLayout::GetDimension() const { - PCMS_FUNCTION_TIMER; - auto classIds_h = Omega_h::HostRead(GetClassIDs()); - auto classDims_h = Omega_h::HostRead(GetClassDims()); - auto owned = GetOwned(); - const auto coords = GetDOFHolderCoordinates().GetCoordinates(); - auto dim = mesh_.dim(); - - PCMS_ALWAYS_ASSERT(classDims_h.size() == classIds_h.size() && - classIds_h.size() == coords.extent(0)); - - // local_index number of vertices going to each destination process by - // calling getRank - degree array - std::array coord; - ReversePartitionMap2 reverse_partition; - LO local_index = 0; - for (int ent_dim = 0; ent_dim <= mesh_.dim(); ++ent_dim) { - if (nodes_per_dim_[ent_dim] == 0) - continue; - - for (LO i = 0; i < mesh_.nents(ent_dim); ++i, ++local_index) { - if (!owned[local_index]) - continue; - - coord[0] = coords(local_index, 0); - coord[1] = coords(local_index, 1); - coord[2] = (dim > 2) ? coords(local_index, 2) : 0.0; - - auto dr = std::visit( - GetRank{classIds_h[local_index], classDims_h[local_index], coord}, - partition); - reverse_partition[dr].indices.emplace_back(local_index); - - const auto n = reverse_partition[dr].ent_offsets.size(); - for (size_t e = ent_dim + 1; e < n; ++e) { - reverse_partition[dr].ent_offsets[e] += 1; - } - } - } + return mesh_.dim(); +} - return reverse_partition; +Rank1View +MeshFieldsAdapterLayout::GetDOFHolderClassificationDimensionsHost() const +{ + return make_const_array_view(classification_dims_host_); +} + +Rank1View +MeshFieldsAdapterLayout::GetDOFHolderClassificationIdsHost() const +{ + return make_const_array_view(classification_ids_host_); } } // namespace pcms diff --git a/src/pcms/adapter/meshfields/mesh_fields_adapter_layout.h b/src/pcms/field/layout/mesh_fields.h similarity index 59% rename from src/pcms/adapter/meshfields/mesh_fields_adapter_layout.h rename to src/pcms/field/layout/mesh_fields.h index 79ad3cee5..1c44760c6 100644 --- a/src/pcms/adapter/meshfields/mesh_fields_adapter_layout.h +++ b/src/pcms/field/layout/mesh_fields.h @@ -4,9 +4,10 @@ #include #include "pcms/utility/arrays.h" -#include "pcms/field_layout.h" -#include "pcms/coordinate_system.h" -#include "pcms/field.h" +#include "pcms/discretization/discretization/omega_h.hpp" +#include "pcms/field/field_layout.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/field/field.h" #include @@ -20,25 +21,31 @@ class MeshFieldsAdapterLayout : public FieldLayout CoordinateSystem coordinate_system, std::string global_id_name = "global"); - std::unique_ptr> CreateFieldReal() const override; + std::shared_ptr GetDiscretization() + const noexcept override; int GetNumComponents() const override; // nodes for standard lagrange FEM LO GetNumOwnedDofHolder() const override; GO GetNumGlobalDofHolder() const override; - Rank1View GetOwned() const override; - GlobalIDView GetGids() const override; - CoordinateView GetDOFHolderCoordinates() const override; + Rank1View GetOwnedHost() const override; + GlobalIDView GetGidsHost() const override; + CoordinateView GetDOFHolderCoordinates() const override; // returns true if the field layout is distributed // if the field layout is distributed, the owned and global dofs are the same - bool IsDistributed() override; + [[nodiscard]] bool IsDistributed() const override; EntOffsetsArray GetEntOffsets() const override; - ReversePartitionMap2 GetReversePartitionMap( - const redev::Partition& partition) const override; + int GetDimension() const override; + + Rank1View + GetDOFHolderClassificationDimensionsHost() const override; + + Rank1View GetDOFHolderClassificationIdsHost() + const override; std::array GetNodesPerDim() const; size_t GetNumEnts() const; @@ -56,11 +63,17 @@ class MeshFieldsAdapterLayout : public FieldLayout CoordinateSystem coordinate_system_; std::array nodes_per_dim_; Kokkos::View dof_holder_coords_; - Kokkos::View dof_holder_coords_host_; + Kokkos::View + dof_holder_coords_device_right_; Omega_h::Write class_ids_; Omega_h::Write class_dims_; + Omega_h::HostWrite class_ids_host_; + Omega_h::HostWrite class_dims_host_; Kokkos::View owned_; Kokkos::View owned_host_; + Kokkos::View classification_dims_host_; + Kokkos::View classification_ids_host_; + std::shared_ptr discretization_; }; } // namespace pcms diff --git a/src/pcms/field/layout/mfem.cpp b/src/pcms/field/layout/mfem.cpp new file mode 100644 index 000000000..245bc8015 --- /dev/null +++ b/src/pcms/field/layout/mfem.cpp @@ -0,0 +1,189 @@ +#include "pcms/field/layout/mfem.h" + +#include "pcms/discretization/discretization/point_cloud.hpp" +#include "pcms/utility/arrays.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/profile.h" + +#include + +namespace pcms +{ + +namespace +{ + +std::shared_ptr MakeVertexDiscretization( + int dim, Kokkos::View coords_host) +{ + return std::make_shared(dim, coords_host); +} + +} // namespace + +MFEMLayout::MFEMLayout(mfem::ParMesh& pmesh, + mfem::ParFiniteElementSpace& pfes, + CoordinateSystem coordinate_system) + : pmesh_(pmesh), + pfes_(pfes), + dim_(pmesh.SpaceDimension()), + coordinate_system_(coordinate_system) +{ + PCMS_FUNCTION_TIMER; + + AssertVertexScalarSpace(); + + const int nv = pmesh_.GetNV(); + + // Host coordinates, one row per vertex. GetVertex returns a pointer to the + // contiguous spatial coordinates of the vertex, independent of MFEM's + // internal storage ordering. + Kokkos::View coords_host("mfem_coords_host", nv, + dim_); + for (int v = 0; v < nv; ++v) { + const double* x = pmesh_.GetVertex(v); + for (int d = 0; d < dim_; ++d) { + coords_host(v, d) = static_cast(x[d]); + } + } + coords_ = Kokkos::View("mfem_coords", nv, dim_); + Kokkos::deep_copy(coords_, coords_host); + + // Global vertex ids. + mfem::Array vertex_gids; + pmesh_.GetGlobalVertexIndices(vertex_gids); + gids_host_ = Kokkos::View("mfem_gids", nv); + + // Ownership: a vertex DOF holder is owned by this rank iff it maps to a + // local true DOF. Shared vertices are owned by exactly one rank, so every + // global DOF holder has a single owner across the communicator. + owned_host_ = Kokkos::View("mfem_owned", nv); + + // Placeholder classification: vertex entity dim (0) and local-index ids. + // RCB partitioning ignores these; they exist to satisfy the FieldLayout and + // OverlapMask contracts. + classification_dims_host_ = + Kokkos::View("mfem_class_dims", nv); + classification_ids_host_ = + Kokkos::View("mfem_class_ids", nv); + + mfem::Array vdofs; + for (int v = 0; v < nv; ++v) { + pfes_.GetVertexDofs(v, vdofs); + PCMS_ALWAYS_ASSERT(vdofs.Size() == 1); + const int dof = vdofs[0]; + + gids_host_(v) = static_cast(vertex_gids[v]); + owned_host_(v) = (pfes_.GetLocalTDofNumber(dof) >= 0); + classification_dims_host_(v) = 0; + classification_ids_host_(v) = v; + } + + discretization_ = MakeVertexDiscretization(dim_, coords_host); +} + +void MFEMLayout::AssertVertexScalarSpace() const +{ + // Order-1 H1, single scalar component: exactly one DOF per vertex. + PCMS_ALWAYS_ASSERT(pfes_.GetVDim() == 1); + PCMS_ALWAYS_ASSERT(pfes_.GetNDofs() == pmesh_.GetNV()); +} + +std::shared_ptr MFEMLayout::GetDiscretization() + const noexcept +{ + return discretization_; +} + +int MFEMLayout::GetNumComponents() const +{ + return 1; +} + +LO MFEMLayout::GetNumOwnedDofHolder() const +{ + // Number of local DOF holders (all vertices on this rank). The owned mask + // distinguishes the single-owner subset used for communication. + return static_cast(pmesh_.GetNV()); +} + +GO MFEMLayout::GetNumGlobalDofHolder() const +{ + // Each globally unique vertex corresponds to one true DOF. + return static_cast(pfes_.GlobalTrueVSize()); +} + +Rank1View MFEMLayout::GetOwnedHost() const +{ + return make_const_array_view(owned_host_); +} + +GlobalIDView MFEMLayout::GetGidsHost() const +{ + return GlobalIDView(gids_host_.data(), gids_host_.size()); +} + +CoordinateView MFEMLayout::GetDOFHolderCoordinates() const +{ + return CoordinateView{coordinate_system_, + MakeConstRank2View(coords_)}; +} + +bool MFEMLayout::IsDistributed() const +{ + return true; +} + +EntOffsetsArray MFEMLayout::GetEntOffsets() const +{ + EntOffsetsArray offsets{}; + offsets.fill(0); + const auto n = static_cast(GetNumOwnedDofHolder()); + // Vertex DOF holders occupy entity dimension 0. + for (int i = 1; i < ent_offsets_len; ++i) { + offsets[i] = n; + } + return offsets; +} + +int MFEMLayout::GetDimension() const +{ + return dim_; +} + +Rank1View +MFEMLayout::GetDOFHolderClassificationDimensionsHost() const +{ + return make_const_array_view(classification_dims_host_); +} + +Rank1View +MFEMLayout::GetDOFHolderClassificationIdsHost() const +{ + return make_const_array_view(classification_ids_host_); +} + +Kokkos::View MFEMLayout::OverlapMaskFromAttribute( + mfem::ParMesh& pmesh, int attribute) +{ + PCMS_FUNCTION_TIMER; + + const int nv = pmesh.GetNV(); + Kokkos::View overlap("mfem_overlap_mask", nv); + Kokkos::deep_copy(overlap, false); + + mfem::Array verts; + for (int e = 0; e < pmesh.GetNE(); ++e) { + if (pmesh.GetAttribute(e) != attribute) { + continue; + } + pmesh.GetElementVertices(e, verts); + for (int j = 0; j < verts.Size(); ++j) { + overlap(verts[j]) = true; + } + } + + return overlap; +} + +} // namespace pcms diff --git a/src/pcms/field/layout/mfem.h b/src/pcms/field/layout/mfem.h new file mode 100644 index 000000000..f780e64e4 --- /dev/null +++ b/src/pcms/field/layout/mfem.h @@ -0,0 +1,80 @@ +#ifndef PCMS_FIELD_LAYOUT_MFEM_H +#define PCMS_FIELD_LAYOUT_MFEM_H + +#include "pcms/field/coordinate_system.h" +#include "pcms/field/field_layout.h" + +#include + +#include + +namespace pcms +{ + +// Field layout for an MFEM order-1 H1 (vertex) scalar field. +// +// Each DOF holder is a mesh vertex; DOF-holder ordering follows the MFEM local +// vertex ordering. This is the minimal layout matching the original MFEM +// adapter scope: vertex-only, order-1, single component. +// +// Partitioning relies on DOF-holder coordinates (redev::RCBPtn). The +// classification arrays required by FieldLayout are filled with placeholders +// (entity dim 0, id = local index) because MFEM has no Omega_h-style geometric +// classification; an RCB partition ignores them. +class MFEMLayout : public FieldLayout +{ +public: + MFEMLayout(mfem::ParMesh& pmesh, mfem::ParFiniteElementSpace& pfes, + CoordinateSystem coordinate_system); + + std::shared_ptr GetDiscretization() + const noexcept override; + + int GetNumComponents() const override; + LO GetNumOwnedDofHolder() const override; + GO GetNumGlobalDofHolder() const override; + + Rank1View GetOwnedHost() const override; + GlobalIDView GetGidsHost() const override; + CoordinateView GetDOFHolderCoordinates() const override; + + [[nodiscard]] bool IsDistributed() const override; + EntOffsetsArray GetEntOffsets() const override; + int GetDimension() const override; + + Rank1View + GetDOFHolderClassificationDimensionsHost() const override; + + Rank1View GetDOFHolderClassificationIdsHost() + const override; + + mfem::ParMesh& GetMesh() const noexcept { return pmesh_; } + mfem::ParFiniteElementSpace& GetFESpace() const noexcept { return pfes_; } + + // Build a per-vertex overlap mask from MFEM element attributes: a vertex is + // in the overlap if it is incident to at least one element with the given + // attribute. Mirrors the create_mask strategy used in the mfem-pcms-example. + // The returned host array is indexed by local vertex (DOF holder) and can be + // passed to pcms::OverlapMask. + static Kokkos::View OverlapMaskFromAttribute( + mfem::ParMesh& pmesh, int attribute); + +private: + void AssertVertexScalarSpace() const; + + mfem::ParMesh& pmesh_; + mfem::ParFiniteElementSpace& pfes_; + int dim_; + CoordinateSystem coordinate_system_; + + Kokkos::View coords_; + Kokkos::View owned_host_; + Kokkos::View gids_host_; + Kokkos::View classification_dims_host_; + Kokkos::View classification_ids_host_; + std::shared_ptr discretization_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_LAYOUT_MFEM_H diff --git a/src/pcms/field/layout/omega_h_entity.cpp b/src/pcms/field/layout/omega_h_entity.cpp new file mode 100644 index 000000000..32f390437 --- /dev/null +++ b/src/pcms/field/layout/omega_h_entity.cpp @@ -0,0 +1,163 @@ +#include "pcms/field/layout/omega_h_entity.h" + +#include "pcms/utility/assert.h" +#include "pcms/utility/mesh_geometry.h" +#include "pcms/utility/omega_h_array_utils.h" + +namespace pcms +{ + +namespace +{ + +template +Omega_h::Write GetGidsHelper(Omega_h::Mesh& mesh, int entity_dim, + const std::string& global_id_name) +{ + auto dim_gids = mesh.get_array(entity_dim, global_id_name); + Omega_h::Write gids(dim_gids.size()); + Omega_h::parallel_for( + dim_gids.size(), OMEGA_H_LAMBDA(int i) { gids[i] = dim_gids[i]; }); + return gids; +} + +Omega_h::Write BuildGids(Omega_h::Mesh& mesh, int entity_dim, + const std::string& global_id_name) +{ + auto tag = mesh.get_tagbase(entity_dim, global_id_name); + Omega_h::Write gids; + if (Omega_h::is(tag)) { + gids = GetGidsHelper(mesh, entity_dim, global_id_name); + } else if (Omega_h::is(tag)) { + gids = GetGidsHelper(mesh, entity_dim, global_id_name); + } else { + std::cerr << "Weird tag type for global arrays.\n"; + std::abort(); + } + return gids; +} + +Kokkos::View BuildOwned(Omega_h::Mesh& mesh, + int entity_dim) +{ + Kokkos::View owned("owned", mesh.nents(entity_dim)); + auto owned_h = Omega_h::Read(mesh.owned(entity_dim)); + Kokkos::parallel_for( + mesh.nents(entity_dim), + OMEGA_H_LAMBDA(int i) { owned(i) = static_cast(owned_h[i]); }); + return owned; +} + +} // namespace + +OmegaHEntityLayout::OmegaHEntityLayout(Omega_h::Mesh& mesh, int entity_dim, + int num_components, + CoordinateSystem coordinate_system, + std::string global_id_name) + : dimension_(mesh.dim()), + entity_dim_(entity_dim), + num_components_(num_components), + num_global_dof_holder_(mesh.nglobal_ents(entity_dim)), + coordinate_system_(coordinate_system), + gids_(BuildGids(mesh, entity_dim, global_id_name)), + coords_(get_entity_centroids(mesh, entity_dim)), + coords_2d_(ConvertCoordsTo2D(coords_, mesh.nents(entity_dim), mesh.dim())), + class_ids_(mesh.get_array(entity_dim, "class_id")), + class_dims_(mesh.get_array(entity_dim, "class_dim")), + owned_(BuildOwned(mesh, entity_dim)), + owned_host_("owned_host", owned_.size()), + classification_dims_host_("classification_dims", mesh.nents(entity_dim)), + classification_ids_host_("classification_ids", mesh.nents(entity_dim)), + discretization_(std::make_shared(mesh)) +{ + PCMS_ALWAYS_ASSERT(entity_dim_ >= 0 && entity_dim_ <= dimension_); + + gids_host_ = Omega_h::HostWrite(gids_); + Kokkos::deep_copy(owned_host_, owned_); + + class_dims_ = Omega_h::Read(class_dims_); + class_ids_ = Omega_h::Read(class_ids_); + auto class_dims_host = Omega_h::HostRead(class_dims_); + auto class_ids_host = Omega_h::HostRead(class_ids_); + for (int i = 0; i < mesh.nents(entity_dim_); ++i) { + classification_dims_host_(i) = + static_cast(static_cast(class_dims_host[i])); + classification_ids_host_(i) = static_cast(class_ids_host[i]); + } +} + +std::shared_ptr OmegaHEntityLayout::GetDiscretization() + const noexcept +{ + return discretization_; +} + +int OmegaHEntityLayout::GetNumComponents() const +{ + return num_components_; +} + +LO OmegaHEntityLayout::GetNumOwnedDofHolder() const +{ + return static_cast(coords_.size() / dimension_); +} + +GO OmegaHEntityLayout::GetNumGlobalDofHolder() const +{ + return num_global_dof_holder_; +} + +Rank1View OmegaHEntityLayout::GetOwnedHost() const +{ + return make_const_array_view(owned_host_); +} + +GlobalIDView OmegaHEntityLayout::GetGidsHost() const +{ + return GlobalIDView(gids_host_.data(), gids_host_.size()); +} + +CoordinateView OmegaHEntityLayout::GetDOFHolderCoordinates() + const +{ + using LayoutPolicy = + detail::default_layout_for_memory_space_t; + Rank2View coords_view( + coords_2d_.data(), GetNumOwnedDofHolder(), dimension_); + return CoordinateView{coordinate_system_, + coords_view}; +} + +bool OmegaHEntityLayout::IsDistributed() const +{ + return true; +} + +EntOffsetsArray OmegaHEntityLayout::GetEntOffsets() const +{ + EntOffsetsArray offsets{}; + offsets.fill(0); + const auto n = static_cast(GetNumOwnedDofHolder()); + for (int i = entity_dim_ + 1; i < ent_offsets_len; ++i) + offsets[i] = n; + return offsets; +} + +int OmegaHEntityLayout::GetDimension() const +{ + return dimension_; +} + +Rank1View +OmegaHEntityLayout::GetDOFHolderClassificationDimensionsHost() const +{ + return make_const_array_view(classification_dims_host_); +} + +Rank1View +OmegaHEntityLayout::GetDOFHolderClassificationIdsHost() const +{ + return make_const_array_view(classification_ids_host_); +} + +} // namespace pcms diff --git a/src/pcms/field/layout/omega_h_entity.h b/src/pcms/field/layout/omega_h_entity.h new file mode 100644 index 000000000..4c60545ae --- /dev/null +++ b/src/pcms/field/layout/omega_h_entity.h @@ -0,0 +1,67 @@ +#ifndef PCMS_FIELD_LAYOUT_OMEGA_H_ENTITY_H +#define PCMS_FIELD_LAYOUT_OMEGA_H_ENTITY_H + +#include + +#include "pcms/discretization/discretization/omega_h.hpp" +#include "pcms/field/coordinate_system.h" +#include "pcms/field/field_layout.h" + +namespace pcms +{ + +// Layout for fields with one DOF holder on each entity of a single Omega_h +// mesh dimension. Unlike OmegaHLagrangeLayout, this is not limited to +// Lagrange orders and directly represents "one value per chosen entity" +// layouts such as face-centroid reconstruction sites. +class OmegaHEntityLayout : public FieldLayout +{ +public: + OmegaHEntityLayout(Omega_h::Mesh& mesh, int entity_dim, int num_components, + CoordinateSystem coordinate_system, + std::string global_id_name = "global"); + + std::shared_ptr GetDiscretization() + const noexcept override; + + int GetNumComponents() const override; + LO GetNumOwnedDofHolder() const override; + GO GetNumGlobalDofHolder() const override; + + Rank1View GetOwnedHost() const override; + GlobalIDView GetGidsHost() const override; + CoordinateView GetDOFHolderCoordinates() const override; + + [[nodiscard]] bool IsDistributed() const override; + EntOffsetsArray GetEntOffsets() const override; + int GetDimension() const override; + + Rank1View + GetDOFHolderClassificationDimensionsHost() const override; + + Rank1View GetDOFHolderClassificationIdsHost() + const override; + +private: + int dimension_; + int entity_dim_; + int num_components_; + GO num_global_dof_holder_; + CoordinateSystem coordinate_system_; + + Omega_h::Write gids_; + Omega_h::HostWrite gids_host_; + Omega_h::Read coords_; // device coordinates (1D flattened) + Kokkos::View coords_2d_; // device coordinates (2D) + Omega_h::Read class_ids_; + Omega_h::Read class_dims_; + Kokkos::View owned_; + Kokkos::View owned_host_; + Kokkos::View classification_dims_host_; + Kokkos::View classification_ids_host_; + std::shared_ptr discretization_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_LAYOUT_OMEGA_H_ENTITY_H diff --git a/src/pcms/field/layout/omega_h_lagrange.cpp b/src/pcms/field/layout/omega_h_lagrange.cpp new file mode 100644 index 000000000..93ebd0bda --- /dev/null +++ b/src/pcms/field/layout/omega_h_lagrange.cpp @@ -0,0 +1,254 @@ +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/mesh_geometry.h" +#include "pcms/utility/omega_h_array_utils.h" +#include "pcms/utility/profile.h" +#include + +namespace pcms +{ + +namespace +{ +int EntityDimForOrder(int order, int mesh_dim) +{ + switch (order) { + case 0: return mesh_dim; // one DOF per element + case 1: return 0; // one DOF per vertex + default: + throw std::invalid_argument( + "OmegaHLagrangeLayout: only order 0 and 1 are supported"); + } +} + +template +Omega_h::Write GetGidsHelper(Omega_h::Mesh& mesh, int entity_dim, + const std::string& global_id_name) +{ + auto dim_gids = mesh.get_array(entity_dim, global_id_name); + Omega_h::Write gids(dim_gids.size()); + Omega_h::parallel_for( + dim_gids.size(), OMEGA_H_LAMBDA(int i) { gids[i] = dim_gids[i]; }); + return gids; +} + +Omega_h::Write BuildGids(Omega_h::Mesh& mesh, int entity_dim, + const std::string& global_id_name) +{ + auto tag = mesh.get_tagbase(entity_dim, global_id_name); + Omega_h::Write gids; + if (Omega_h::is(tag)) { + gids = GetGidsHelper(mesh, entity_dim, global_id_name); + } else if (Omega_h::is(tag)) { + gids = GetGidsHelper(mesh, entity_dim, global_id_name); + } else { + std::cerr << "Weird tag type for global arrays.\n"; + std::abort(); + } + return gids; +} + +Kokkos::View BuildOwned(Omega_h::Mesh& mesh, + int entity_dim) +{ + int n = mesh.nents(entity_dim); + Kokkos::View owned("owned", n); + auto src = Omega_h::Read(mesh.owned(entity_dim)); + Kokkos::parallel_for( + n, OMEGA_H_LAMBDA(int i) { owned(i) = static_cast(src[i]); }); + return owned; +} + +Kokkos::View BuildOwned( + Omega_h::Mesh& mesh, int entity_dim, Omega_h::Read mask) +{ + auto owned = BuildOwned(mesh, entity_dim); + auto mask_h = Omega_h::Read(mask); + PCMS_ALWAYS_ASSERT(static_cast(mask_h.size()) == mesh.nents(entity_dim)); + Kokkos::parallel_for( + mesh.nents(entity_dim), OMEGA_H_LAMBDA(int i) { + owned(i) = owned(i) && static_cast(mask_h[i]); + }); + return owned; +} + +} // namespace + +OmegaHLagrangeLayout::OmegaHLagrangeLayout(Omega_h::Mesh& mesh, int order, + int num_components, + CoordinateSystem coordinate_system, + std::string global_id_name) + : mesh_(mesh), + order_(order), + num_components_(num_components), + coordinate_system_(coordinate_system), + global_id_name_(std::move(global_id_name)) +{ + PCMS_FUNCTION_TIMER; + int entity_dim = EntityDimForOrder(order_, mesh_.dim()); + + gids_ = BuildGids(mesh_, entity_dim, global_id_name_); + gids_host_ = Omega_h::HostWrite(gids_); + coords_ = get_entity_centroids(mesh_, entity_dim); + coords_2d_ = ConvertCoordsTo2D(coords_, mesh_.nents(entity_dim), mesh_.dim()); + owned_ = BuildOwned(mesh_, entity_dim); + owned_host_ = + Kokkos::View("owned_host", owned_.size()); + Kokkos::deep_copy(owned_host_, owned_); + + class_ids_ = Omega_h::Read( + mesh_.get_array(entity_dim, "class_id")); + class_dims_ = Omega_h::Read( + mesh_.get_array(entity_dim, "class_dim")); + + auto class_ids_host = Omega_h::HostRead(class_ids_); + auto class_dims_host = Omega_h::HostRead(class_dims_); + + int n = mesh_.nents(entity_dim); + classification_dims_host_ = + Kokkos::View("classification_dims", n); + classification_ids_host_ = + Kokkos::View("classification_ids", n); + for (int i = 0; i < n; ++i) { + classification_dims_host_(i) = + static_cast(static_cast(class_dims_host[i])); + classification_ids_host_(i) = static_cast(class_ids_host[i]); + } + discretization_ = std::make_shared(mesh_); +} + +OmegaHLagrangeLayout::OmegaHLagrangeLayout( + Omega_h::Mesh& mesh, int order, int num_components, + CoordinateSystem coordinate_system, Omega_h::Read owned_mask, + std::string global_id_name) + : mesh_(mesh), + order_(order), + num_components_(num_components), + coordinate_system_(coordinate_system), + global_id_name_(std::move(global_id_name)) +{ + PCMS_FUNCTION_TIMER; + int entity_dim = EntityDimForOrder(order_, mesh_.dim()); + + gids_ = BuildGids(mesh_, entity_dim, global_id_name_); + gids_host_ = Omega_h::HostWrite(gids_); + coords_ = get_entity_centroids(mesh_, entity_dim); + coords_2d_ = ConvertCoordsTo2D(coords_, mesh_.nents(entity_dim), mesh_.dim()); + owned_ = BuildOwned(mesh_, entity_dim, owned_mask); + owned_host_ = + Kokkos::View("owned_host", owned_.size()); + + class_ids_ = Omega_h::Read( + mesh_.get_array(entity_dim, "class_id")); + class_dims_ = Omega_h::Read( + mesh_.get_array(entity_dim, "class_dim")); + + auto class_ids_host = Omega_h::HostRead(class_ids_); + auto class_dims_host = Omega_h::HostRead(class_dims_); + + int n = mesh_.nents(entity_dim); + classification_dims_host_ = + Kokkos::View("classification_dims", n); + classification_ids_host_ = + Kokkos::View("classification_ids", n); + for (int i = 0; i < n; ++i) { + classification_dims_host_(i) = + static_cast(static_cast(class_dims_host[i])); + classification_ids_host_(i) = static_cast(class_ids_host[i]); + } + discretization_ = std::make_shared(mesh_); +} + +std::shared_ptr OmegaHLagrangeLayout::GetDiscretization() + const noexcept +{ + return discretization_; +} + +int OmegaHLagrangeLayout::GetNumComponents() const +{ + return num_components_; +} + +LO OmegaHLagrangeLayout::GetNumOwnedDofHolder() const +{ + return mesh_.nents(EntityDimForOrder(order_, mesh_.dim())); +} + +GO OmegaHLagrangeLayout::GetNumGlobalDofHolder() const +{ + return mesh_.nglobal_ents(EntityDimForOrder(order_, mesh_.dim())); +} + +Rank1View OmegaHLagrangeLayout::GetOwnedHost() + const +{ + return make_const_array_view(owned_host_); +} + +GlobalIDView OmegaHLagrangeLayout::GetGidsHost() const +{ + return GlobalIDView(gids_host_.data(), gids_host_.size()); +} + +CoordinateView +OmegaHLagrangeLayout::GetDOFHolderCoordinates() const +{ + int n = mesh_.nents(EntityDimForOrder(order_, mesh_.dim())); + int dim = mesh_.dim(); + using LayoutPolicy = + detail::default_layout_for_memory_space_t; + Rank2View coords_view( + coords_2d_.data(), n, dim); + return CoordinateView{coordinate_system_, + coords_view}; +} + +bool OmegaHLagrangeLayout::IsDistributed() const +{ + return true; +} + +EntOffsetsArray OmegaHLagrangeLayout::GetEntOffsets() const +{ + // Slot i holds the starting DOF index for entity dimension i. + // Slot 4 holds the total DOF count. + // For order-1 (entity_dim=0): offsets = {0, n, n, n, n} + // For order-0 (entity_dim=mesh.dim()): offsets = {0,..,0, n, n} + EntOffsetsArray offsets{}; + offsets.fill(0); + int entity_dim = EntityDimForOrder(order_, mesh_.dim()); + LO n = mesh_.nents(entity_dim); + for (int i = entity_dim + 1; i < ent_offsets_len; ++i) + offsets[i] = n; + return offsets; +} + +int OmegaHLagrangeLayout::GetDimension() const +{ + return mesh_.dim(); +} + +Rank1View +OmegaHLagrangeLayout::GetDOFHolderClassificationDimensionsHost() const +{ + return make_const_array_view(classification_dims_host_); +} + +Rank1View +OmegaHLagrangeLayout::GetDOFHolderClassificationIdsHost() const +{ + return make_const_array_view(classification_ids_host_); +} + +int OmegaHLagrangeLayout::GetOrder() const +{ + return order_; +} + +Omega_h::Mesh& OmegaHLagrangeLayout::GetMesh() const +{ + return mesh_; +} + +} // namespace pcms diff --git a/src/pcms/field/layout/omega_h_lagrange.h b/src/pcms/field/layout/omega_h_lagrange.h new file mode 100644 index 000000000..f750b47ad --- /dev/null +++ b/src/pcms/field/layout/omega_h_lagrange.h @@ -0,0 +1,76 @@ +#ifndef PCMS_ADAPTER_OMEGA_H_LAGRANGE_LAYOUT_H +#define PCMS_ADAPTER_OMEGA_H_LAGRANGE_LAYOUT_H + +#include +#include "pcms/utility/arrays.h" +#include "pcms/discretization/discretization/omega_h.hpp" +#include "pcms/field/field_layout.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/field/field.h" + +namespace pcms +{ + +// Layout for native Omega_h Lagrange fields. +// order 0: one DOF holder per element (centroid coordinates) +// order 1: one DOF holder per vertex (barycentric interpolation) +// Throws std::invalid_argument for any other order. +class OmegaHLagrangeLayout : public FieldLayout +{ +public: + OmegaHLagrangeLayout(Omega_h::Mesh& mesh, int order, int num_components, + CoordinateSystem coordinate_system, + std::string global_id_name = "global"); + OmegaHLagrangeLayout(Omega_h::Mesh& mesh, int order, int num_components, + CoordinateSystem coordinate_system, + Omega_h::Read owned_mask, + std::string global_id_name = "global"); + + std::shared_ptr GetDiscretization() + const noexcept override; + + int GetNumComponents() const override; + LO GetNumOwnedDofHolder() const override; + GO GetNumGlobalDofHolder() const override; + + Rank1View GetOwnedHost() const override; + GlobalIDView GetGidsHost() const override; + CoordinateView GetDOFHolderCoordinates() const override; + + [[nodiscard]] bool IsDistributed() const override; + + EntOffsetsArray GetEntOffsets() const override; + + int GetDimension() const override; + + Rank1View + GetDOFHolderClassificationDimensionsHost() const override; + + Rank1View GetDOFHolderClassificationIdsHost() + const override; + + int GetOrder() const; + Omega_h::Mesh& GetMesh() const; + +private: + Omega_h::Mesh& mesh_; + int order_; + int num_components_; + CoordinateSystem coordinate_system_; + std::string global_id_name_; + + Omega_h::Write gids_; + Omega_h::HostWrite gids_host_; + Kokkos::View coords_2d_; + Omega_h::Read coords_; // device coordinates + Kokkos::View owned_; + Kokkos::View owned_host_; + Omega_h::Read class_ids_; + Omega_h::Read class_dims_; + Kokkos::View classification_dims_host_; + Kokkos::View classification_ids_host_; + std::shared_ptr discretization_; +}; + +} // namespace pcms +#endif // PCMS_ADAPTER_OMEGA_H_LAGRANGE_LAYOUT_H diff --git a/src/pcms/field/layout/point_cloud.cpp b/src/pcms/field/layout/point_cloud.cpp new file mode 100644 index 000000000..ffa9ca4d7 --- /dev/null +++ b/src/pcms/field/layout/point_cloud.cpp @@ -0,0 +1,173 @@ +#include "pcms/field/layout/point_cloud.h" +#include "pcms/utility/arrays.h" +#include +#include + +namespace pcms +{ + +namespace +{ + +std::shared_ptr MakePointCloudDiscretization( + int dim, Kokkos::View coords) +{ + auto coords_mirror = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), coords); + // Create a view with the default layout for HostMemorySpace to avoid layout + // incompatibility + Kokkos::View coords_host( + "coords_host", coords_mirror.extent(0), coords_mirror.extent(1)); + Kokkos::deep_copy(coords_host, coords_mirror); + return std::make_shared( + dim, coords_host, static_cast(coords.data())); +} + +void InitializePointCloudClassification( + Kokkos::View classification_dims, + Kokkos::View classification_ids, LO n, + LO classification_entity_dim) +{ + Kokkos::deep_copy(classification_dims, classification_entity_dim); + // for (LO i = 0; i < n; ++i) { + // classification_ids_host(i) = i; + // } + Kokkos::parallel_for(n, OMEGA_H_LAMBDA(LO i) { classification_ids(i) = i; }); +} + +} // namespace + +PointCloudLayout::PointCloudLayout(int dim, Kokkos::View coords, + CoordinateSystem coordinate_system) + : PointCloudLayout(dim, coords, coordinate_system, + MakePointCloudDiscretization(dim, coords), Vertex) +{ +} + +PointCloudLayout::PointCloudLayout( + int dim, Kokkos::View coords, CoordinateSystem coordinate_system, + std::shared_ptr discretization, + int classification_entity_dim) + : dim_(dim), + coordinate_system_(coordinate_system), + coords_(coords), + owned_("", coords.extent(0)), + gids_("", coords.extent(0)), + owned_host_("", coords.extent(0)), + gids_host_("", coords.extent(0)) +{ + components_ = 1; + + namespace KE = Kokkos::Experimental; + KE::fill(Kokkos::DefaultExecutionSpace(), owned_, true); + iota_view(gids_); + + LO n = static_cast(coords.extent(0)); + classification_dims_ = + Kokkos::View("classification_dims", n); + classification_ids_ = + Kokkos::View("classification_ids", n); + InitializePointCloudClassification(classification_dims_, classification_ids_, + n, classification_entity_dim); + classification_dims_host_ = + Kokkos::View("classification_dims", n); + classification_ids_host_ = + Kokkos::View("classification_ids", n); + discretization_ = std::move(discretization); + + Kokkos::deep_copy(owned_host_, owned_); + Kokkos::deep_copy(gids_host_, gids_); + Kokkos::deep_copy(classification_dims_host_, classification_dims_); + Kokkos::deep_copy(classification_ids_host_, classification_ids_); +} + +std::shared_ptr PointCloudLayout::GetDiscretization() + const noexcept +{ + return discretization_; +} + +int PointCloudLayout::GetNumComponents() const +{ + return components_; +} + +LO PointCloudLayout::GetNumOwnedDofHolder() const +{ + return coords_.extent(0); +} + +GO PointCloudLayout::GetNumGlobalDofHolder() const +{ + return coords_.extent(0); +} + +Rank1View PointCloudLayout::GetOwnedHost() const +{ + return make_const_array_view(owned_host_); +} + +GlobalIDView PointCloudLayout::GetGidsHost() const +{ + return GlobalIDView(gids_host_.data(), gids_host_.size()); +} + +CoordinateView PointCloudLayout::GetDOFHolderCoordinates() + const +{ + auto coords_view = MakeConstRank2View(coords_); + return CoordinateView{coordinate_system_, coords_view}; +} + +bool PointCloudLayout::IsDistributed() const +{ + return false; +} + +size_t PointCloudLayout::GetNumEnts() const +{ + return coords_.extent(0); +} + +EntOffsetsArray PointCloudLayout::GetEntOffsets() const +{ + EntOffsetsArray offsets{}; + for (size_t i = 0; i < offsets.size(); ++i) + offsets[i] = coords_.extent(0); + offsets[0] = 0; + return offsets; +} + +std::array PointCloudLayout::GetNodesPerDim() const +{ + std::array nodes{}; + for (size_t i = 0; i < nodes.size(); ++i) + nodes[i] = 0; + nodes[0] = 1; + return nodes; +} + +Kokkos::View PointCloudLayout::GetCoordinates() + const +{ + return coords_; +} + +int PointCloudLayout::GetDimension() const +{ + return dim_; +} + +Rank1View +PointCloudLayout::GetDOFHolderClassificationDimensionsHost() const +{ + return make_const_array_view(classification_dims_host_); +} + +Rank1View +PointCloudLayout::GetDOFHolderClassificationIdsHost() const +{ + return make_const_array_view(classification_ids_host_); +} + +} // namespace pcms diff --git a/src/pcms/field/layout/point_cloud.h b/src/pcms/field/layout/point_cloud.h new file mode 100644 index 000000000..50e6e9874 --- /dev/null +++ b/src/pcms/field/layout/point_cloud.h @@ -0,0 +1,65 @@ +#ifndef POINT_CLOUD_LAYOUT_H_ +#define POINT_CLOUD_LAYOUT_H_ + +#include "pcms/field/field.h" +#include "pcms/discretization/discretization/point_cloud.hpp" + +namespace pcms +{ + +class PointCloudLayout : public FieldLayout +{ +public: + PointCloudLayout(int dim, Kokkos::View coords, + CoordinateSystem coordinate_system); + PointCloudLayout(int dim, Kokkos::View coords, + CoordinateSystem coordinate_system, + std::shared_ptr discretization, + int classification_entity_dim); + + std::shared_ptr GetDiscretization() + const noexcept override; + + int GetNumComponents() const override; + // nodes for standard lagrange FEM + LO GetNumOwnedDofHolder() const override; + GO GetNumGlobalDofHolder() const override; + + Rank1View GetOwnedHost() const override; + GlobalIDView GetGidsHost() const override; + CoordinateView GetDOFHolderCoordinates() const override; + + [[nodiscard]] bool IsDistributed() const override; + size_t GetNumEnts() const; + EntOffsetsArray GetEntOffsets() const override; + + int GetDimension() const override; + + Rank1View + GetDOFHolderClassificationDimensionsHost() const override; + + Rank1View GetDOFHolderClassificationIdsHost() + const override; + + std::array GetNodesPerDim() const; + + Kokkos::View GetCoordinates() const; + +private: + int dim_; + int components_; + CoordinateSystem coordinate_system_; + Kokkos::View coords_; + Kokkos::View owned_; + Kokkos::View gids_; + Kokkos::View owned_host_; + Kokkos::View gids_host_; + Kokkos::View classification_dims_; + Kokkos::View classification_ids_; + Kokkos::View classification_dims_host_; + Kokkos::View classification_ids_host_; + std::shared_ptr discretization_; +}; +} // namespace pcms + +#endif // POINT_CLOUD_LAYOUT_H_ diff --git a/src/pcms/field/layout/uniform_grid.cpp b/src/pcms/field/layout/uniform_grid.cpp new file mode 100644 index 000000000..08f4823d1 --- /dev/null +++ b/src/pcms/field/layout/uniform_grid.cpp @@ -0,0 +1,317 @@ +#include "pcms/field/layout/uniform_grid.h" +#include "pcms/utility/profile.h" +#include "pcms/utility/assert.h" +#include + +namespace pcms +{ + +namespace +{ + +// Functor for initializing 2D grid coordinates on device +template +struct InitGridCoordsFunctor2D +{ + Kokkos::View coords_; + UniformGrid grid_; + int order_; + Real vertex_spacing_0_; + Real vertex_spacing_1_; + LO nx_; + + InitGridCoordsFunctor2D(Kokkos::View coords, + const UniformGrid& grid, int order, Real vs0, + Real vs1, LO nx) + : coords_(coords), + grid_(grid), + order_(order), + vertex_spacing_0_(vs0), + vertex_spacing_1_(vs1), + nx_(nx) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO dof_idx) const + { + if (order_ == 1) { + LO i = dof_idx % nx_; + LO j = dof_idx / nx_; + coords_(dof_idx, 0) = grid_.bot_left[0] + i * vertex_spacing_0_; + coords_(dof_idx, 1) = grid_.bot_left[1] + j * vertex_spacing_1_; + } else { + LO i = dof_idx % grid_.divisions[0]; + LO j = dof_idx / grid_.divisions[0]; + coords_(dof_idx, 0) = grid_.bot_left[0] + (i + 0.5) * vertex_spacing_0_; + coords_(dof_idx, 1) = grid_.bot_left[1] + (j + 0.5) * vertex_spacing_1_; + } + } +}; + +// Functor for initializing 3D grid coordinates on device +template +struct InitGridCoordsFunctor3D +{ + Kokkos::View coords_; + UniformGrid grid_; + int order_; + Real vertex_spacing_0_; + Real vertex_spacing_1_; + Real vertex_spacing_2_; + LO nx_; + LO ny_; + + InitGridCoordsFunctor3D(Kokkos::View coords, + const UniformGrid& grid, int order, Real vs0, + Real vs1, Real vs2, LO nx, LO ny) + : coords_(coords), + grid_(grid), + order_(order), + vertex_spacing_0_(vs0), + vertex_spacing_1_(vs1), + vertex_spacing_2_(vs2), + nx_(nx), + ny_(ny) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO dof_idx) const + { + if (order_ == 1) { + LO i = dof_idx % nx_; + LO j = (dof_idx / nx_) % ny_; + LO k = dof_idx / (nx_ * ny_); + coords_(dof_idx, 0) = grid_.bot_left[0] + i * vertex_spacing_0_; + coords_(dof_idx, 1) = grid_.bot_left[1] + j * vertex_spacing_1_; + coords_(dof_idx, 2) = grid_.bot_left[2] + k * vertex_spacing_2_; + } else { + LO i = dof_idx % grid_.divisions[0]; + LO j = (dof_idx / grid_.divisions[0]) % grid_.divisions[1]; + LO k = dof_idx / (grid_.divisions[0] * grid_.divisions[1]); + coords_(dof_idx, 0) = grid_.bot_left[0] + (i + 0.5) * vertex_spacing_0_; + coords_(dof_idx, 1) = grid_.bot_left[1] + (j + 0.5) * vertex_spacing_1_; + coords_(dof_idx, 2) = grid_.bot_left[2] + (k + 0.5) * vertex_spacing_2_; + } + } +}; + +struct InitilizeGidsAndOwnedFunctor +{ + Kokkos::View gids_; + Kokkos::View owned_; + + InitilizeGidsAndOwnedFunctor(Kokkos::View gids, + Kokkos::View owned) + : gids_(gids), owned_(owned) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i) const + { + gids_[i] = static_cast(i); + owned_[i] = true; + } +}; + +} // anonymous namespace + +template +UniformGridFieldLayout::UniformGridFieldLayout( + UniformGrid grid, int num_components, CoordinateSystem coordinate_system, + int order) + : grid_(std::move(grid)), + num_components_(num_components), + coordinate_system_(coordinate_system), + order_(order), + gids_("gids", GetNumDofHolders()), + gids_host_("gids_host", GetNumDofHolders()), + dof_holder_coords_("dof_holder_coords", GetNumDofHolders(), Dim), + owned_("owned", GetNumDofHolders()), + owned_host_("owned_host", GetNumDofHolders()) +{ + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(order_ == 0 || order_ == 1); + + LO num_dofs = GetNumDofHolders(); + + Kokkos::parallel_for( + "InitUniformGridGidsAndOwned", + Kokkos::RangePolicy(0, num_dofs), + InitilizeGidsAndOwnedFunctor(gids_, owned_)); + + Kokkos::deep_copy(gids_host_, gids_); + Kokkos::deep_copy(owned_host_, owned_); + + // Initialize DOF holder coordinates directly on device using parallel + // dispatch + Real vertex_spacing[Dim]; + for (unsigned d = 0; d < Dim; ++d) { + vertex_spacing[d] = grid_.edge_length[d] / grid_.divisions[d]; + } + + if constexpr (Dim == 2) { + LO nx = (order_ == 1) ? (grid_.divisions[0] + 1) : grid_.divisions[0]; + InitGridCoordsFunctor2D functor(dof_holder_coords_, grid_, order_, + vertex_spacing[0], vertex_spacing[1], + nx); + Kokkos::parallel_for( + "InitUniformGrid2DCoords", + Kokkos::RangePolicy(0, num_dofs), + functor); + } else if constexpr (Dim == 3) { + LO nx = (order_ == 1) ? (grid_.divisions[0] + 1) : grid_.divisions[0]; + LO ny = (order_ == 1) ? (grid_.divisions[1] + 1) : grid_.divisions[1]; + InitGridCoordsFunctor3D functor(dof_holder_coords_, grid_, order_, + vertex_spacing[0], vertex_spacing[1], + vertex_spacing[2], nx, ny); + Kokkos::parallel_for( + "InitUniformGrid3DCoords", + Kokkos::RangePolicy(0, num_dofs), + functor); + } + + int entity_dim = (order_ == 0) ? static_cast(Dim) : 0; + LO n = GetNumDofHolders(); + classification_dims_ = + Kokkos::View("classification_dims", n); + classification_ids_ = + Kokkos::View("classification_ids", n); + Kokkos::deep_copy(classification_dims_host_, static_cast(entity_dim)); + Kokkos::deep_copy(classification_ids_host_, LO{0}); + + classification_dims_host_ = + Kokkos::View("classification_dims", n); + classification_ids_host_ = + Kokkos::View("classification_ids", n); + Kokkos::deep_copy(classification_dims_host_, classification_dims_); + Kokkos::deep_copy(classification_ids_host_, classification_ids_); + discretization_ = std::make_shared>(grid_); +} + +template +std::shared_ptr +UniformGridFieldLayout::GetDiscretization() const noexcept +{ + return discretization_; +} + +template +int UniformGridFieldLayout::GetNumComponents() const +{ + return num_components_; +} + +template +LO UniformGridFieldLayout::GetNumOwnedDofHolder() const +{ + return GetNumDofHolders(); +} + +template +GO UniformGridFieldLayout::GetNumGlobalDofHolder() const +{ + return GetNumDofHolders(); +} + +template +Rank1View +UniformGridFieldLayout::GetOwnedHost() const +{ + return make_const_array_view(owned_host_); +} + +template +GlobalIDView UniformGridFieldLayout::GetGidsHost() const +{ + return GlobalIDView(gids_host_.data(), gids_host_.size()); +} + +template +CoordinateView +UniformGridFieldLayout::GetDOFHolderCoordinates() const +{ + auto coords_view = MakeConstRank2View(dof_holder_coords_); + return CoordinateView{coordinate_system_, coords_view}; +} + +template +bool UniformGridFieldLayout::IsDistributed() const +{ + return false; +} + +template +const UniformGrid& UniformGridFieldLayout::GetGrid() const +{ + return grid_; +} + +template +LO UniformGridFieldLayout::GetNumCells() const +{ + return grid_.GetNumCells(); +} + +template +LO UniformGridFieldLayout::GetNumVertices() const +{ + LO num_vertices = 1; + for (unsigned d = 0; d < Dim; ++d) { + num_vertices *= (grid_.divisions[d] + 1); + } + return num_vertices; +} + +template +LO UniformGridFieldLayout::GetNumDofHolders() const +{ + return order_ == 0 ? GetNumCells() : GetNumVertices(); +} + +template +int UniformGridFieldLayout::GetDimension() const +{ + return static_cast(Dim); +} + +template +Rank1View +UniformGridFieldLayout::GetDOFHolderClassificationDimensionsHost() const +{ + return make_const_array_view(classification_dims_host_); +} + +template +Rank1View +UniformGridFieldLayout::GetDOFHolderClassificationIdsHost() const +{ + return make_const_array_view(classification_ids_host_); +} + +template +EntOffsetsArray UniformGridFieldLayout::GetEntOffsets() const +{ + EntOffsetsArray offsets{}; + offsets.fill(0); + LO n = GetNumDofHolders(); + int entity_dim = (order_ == 0) ? static_cast(Dim) : 0; + for (int i = entity_dim + 1; i < ent_offsets_len; ++i) { + offsets[i] = n; + } + return offsets; +} + +template +int UniformGridFieldLayout::GetOrder() const +{ + return order_; +} + +// Explicit template instantiations +template class UniformGridFieldLayout<2>; +template class UniformGridFieldLayout<3>; + +} // namespace pcms diff --git a/src/pcms/field/layout/uniform_grid.h b/src/pcms/field/layout/uniform_grid.h new file mode 100644 index 000000000..f4a78a854 --- /dev/null +++ b/src/pcms/field/layout/uniform_grid.h @@ -0,0 +1,72 @@ +#ifndef PCMS_UNIFORM_GRID_FIELD_LAYOUT_H +#define PCMS_UNIFORM_GRID_FIELD_LAYOUT_H + +#include "pcms/utility/arrays.h" +#include "pcms/discretization/discretization/uniform_grid.hpp" +#include "pcms/field/field_layout.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/field/field.h" +#include "pcms/utility/uniform_grid.h" + +#include + +namespace pcms +{ +template +class UniformGridFieldLayout : public FieldLayout +{ +public: + UniformGridFieldLayout(UniformGrid grid, int num_components, + CoordinateSystem coordinate_system, int order = 1); + + std::shared_ptr GetDiscretization() + const noexcept override; + + int GetNumComponents() const override; + LO GetNumOwnedDofHolder() const override; + GO GetNumGlobalDofHolder() const override; + + Rank1View GetOwnedHost() const override; + GlobalIDView GetGidsHost() const override; + CoordinateView GetDOFHolderCoordinates() const override; + + [[nodiscard]] bool IsDistributed() const override; + + EntOffsetsArray GetEntOffsets() const override; + + int GetDimension() const override; + + Rank1View + GetDOFHolderClassificationDimensionsHost() const override; + + Rank1View GetDOFHolderClassificationIdsHost() + const override; + + const UniformGrid& GetGrid() const; + LO GetNumCells() const; + LO GetNumVertices() const; + int GetOrder() const; + +private: + LO GetNumDofHolders() const; + + UniformGrid grid_; + int num_components_; + CoordinateSystem coordinate_system_; + int order_; + Kokkos::View gids_; + Kokkos::View owned_; + Kokkos::View dof_holder_coords_; + Kokkos::View gids_host_; + Kokkos::View owned_host_; + Kokkos::View classification_dims_; + Kokkos::View classification_ids_; + Kokkos::View classification_dims_host_; + Kokkos::View classification_ids_host_; + std::shared_ptr discretization_; +}; + +using UniformGridFieldLayout2D = UniformGridFieldLayout<2>; + +} // namespace pcms +#endif // PCMS_UNIFORM_GRID_FIELD_LAYOUT_H diff --git a/src/pcms/field/layout/xgc.cpp b/src/pcms/field/layout/xgc.cpp new file mode 100644 index 000000000..18729189f --- /dev/null +++ b/src/pcms/field/layout/xgc.cpp @@ -0,0 +1,193 @@ +#include "pcms/field/layout/xgc.h" +#include "pcms/utility/assert.h" + +namespace pcms +{ + +struct InitilizeXGCMembersFunctor +{ + Kokkos::View owned_; + Kokkos::View gids_; + Kokkos::View class_dims_; + Kokkos::View class_ids_; + + InitilizeXGCMembersFunctor(Kokkos::View owned, + Kokkos::View gids, + Kokkos::View class_dims, + Kokkos::View class_ids) + : owned_(owned), gids_(gids), class_dims_(class_dims), class_ids_(class_ids) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(LO i) const + { + owned_(i) = false; + gids_(i) = static_cast(i) + 1; + class_dims_(i) = -1; + class_ids_(i) = -1; + } +}; + +struct ClassifyVertsAndOwnedFunctor +{ + pcms::DimID geom; + Kokkos::View verts; + Kokkos::View owned_; + Kokkos::View class_dims_; + Kokkos::View class_ids_; + + ClassifyVertsAndOwnedFunctor(pcms::DimID geom, + Kokkos::View verts, + Kokkos::View owned, + Kokkos::View class_dims, + Kokkos::View class_ids) + : geom(geom), + verts(verts), + owned_(owned), + class_dims_(class_dims), + class_ids_(class_ids) + { + } + + KOKKOS_INLINE_FUNCTION void operator()(const int i) const + { + LO vert = verts(i); + if (vert >= 0 && vert < owned_.extent(0)) { + owned_(vert) = true; + class_dims_(vert) = geom.dim; + class_ids_(vert) = geom.id; + } + }; +}; + +XGCFieldLayout::XGCFieldLayout( + const ReverseClassificationVertex& reverse_classification, + std::function in_overlap, LO num_plane_nodes) + : owned_("xgc_owned", num_plane_nodes), + gids_("xgc_gids", num_plane_nodes), + class_dims_("xgc_class_dims", num_plane_nodes), + class_ids_("xgc_class_ids", num_plane_nodes), + owned_host_("xgc_owned_host", num_plane_nodes), + gids_host_("xgc_gids_host", num_plane_nodes), + classification_dims_host_("xgc_classification_dims_host", num_plane_nodes), + classification_ids_host_("xgc_classification_ids_host", num_plane_nodes), + coords_("xgc_coords", num_plane_nodes, 2), + num_plane_nodes_(num_plane_nodes) +{ + PCMS_ALWAYS_ASSERT(static_cast(in_overlap)); + Kokkos::parallel_for( + "InitXGCMembers", + Kokkos::RangePolicy(0, + num_plane_nodes_), + InitilizeXGCMembersFunctor(owned_, gids_, class_dims_, class_ids_)); + + for (const auto& [geom, verts] : reverse_classification) { + if (!in_overlap(geom.dim, geom.id)) + continue; + auto verts_host = + Kokkos::View("verts_host", verts.size()); + int idx = 0; + for (LO vert : verts) + verts_host(idx++) = vert; + auto verts_device = + Kokkos::View("verts_device", verts.size()); + Kokkos::deep_copy(verts_device, verts_host); + Kokkos::parallel_for( + "ClassifyVerts", Kokkos::RangePolicy<>(0, verts.size()), + ClassifyVertsAndOwnedFunctor(geom, verts_device, owned_, class_dims_, + class_ids_)); + Kokkos::fence(); // Wait for kernel to complete before verts_device is + // destroyed, better would be to optimize the + // reverse_classification data structure to avoid this copy + // and synchronization, but this is simpler for now + } + + Kokkos::deep_copy(owned_host_, owned_); + Kokkos::deep_copy(gids_host_, gids_); + Kokkos::deep_copy(classification_dims_host_, class_dims_); + Kokkos::deep_copy(classification_ids_host_, class_ids_); + // Copy coordinates to device + Kokkos::deep_copy(coords_, 0.0); + discretization_ = std::make_shared(reverse_classification, + num_plane_nodes_); +} + +std::shared_ptr XGCFieldLayout::GetDiscretization() + const noexcept +{ + return discretization_; +} + +int XGCFieldLayout::GetNumComponents() const +{ + return 1; +} + +LO XGCFieldLayout::GetNumOwnedDofHolder() const +{ + return num_plane_nodes_; +} + +GO XGCFieldLayout::GetNumGlobalDofHolder() const +{ + return num_plane_nodes_; +} + +Rank1View XGCFieldLayout::GetOwnedHost() const +{ + return make_const_array_view(owned_host_); +} + +GlobalIDView XGCFieldLayout::GetGidsHost() const +{ + return make_const_array_view(gids_host_); +} + +bool XGCFieldLayout::IsDistributed() const +{ + return false; +} + +EntOffsetsArray XGCFieldLayout::GetEntOffsets() const +{ + return {0, static_cast(num_plane_nodes_), + static_cast(num_plane_nodes_), + static_cast(num_plane_nodes_), + static_cast(num_plane_nodes_)}; +} + +CoordinateView XGCFieldLayout::GetDOFHolderCoordinates() + const +{ + using LayoutPolicy = + detail::default_layout_for_memory_space_t; + return CoordinateView{ + CoordinateSystem::XGC, + Rank2View( + coords_.data(), num_plane_nodes_, 2)}; +} + +int XGCFieldLayout::GetDimension() const +{ + return 2; +} + +Rank1View +XGCFieldLayout::GetDOFHolderClassificationDimensionsHost() const +{ + return make_const_array_view(classification_dims_host_); +} + +Rank1View +XGCFieldLayout::GetDOFHolderClassificationIdsHost() const +{ + return make_const_array_view(classification_ids_host_); +} + +LO XGCFieldLayout::GetFullDataSize() const noexcept +{ + return num_plane_nodes_; +} + +} // namespace pcms diff --git a/src/pcms/field/layout/xgc.h b/src/pcms/field/layout/xgc.h new file mode 100644 index 000000000..a755dcb0b --- /dev/null +++ b/src/pcms/field/layout/xgc.h @@ -0,0 +1,55 @@ +#ifndef PCMS_XGC_FIELD_LAYOUT_H +#define PCMS_XGC_FIELD_LAYOUT_H + +#include "pcms/discretization/discretization/xgc.hpp" +#include "pcms/discretization/discretization/xgc_reverse_classification.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/field/field_layout.h" +#include + +namespace pcms +{ + +class XGCFieldLayout : public FieldLayout +{ +public: + XGCFieldLayout(const ReverseClassificationVertex& reverse_classification, + std::function in_overlap, + LO num_plane_nodes); + + std::shared_ptr GetDiscretization() + const noexcept override; + + int GetNumComponents() const override; + LO GetNumOwnedDofHolder() const override; + GO GetNumGlobalDofHolder() const override; + Rank1View GetOwnedHost() const override; + GlobalIDView GetGidsHost() const override; + bool IsDistributed() const override; + EntOffsetsArray GetEntOffsets() const override; + CoordinateView GetDOFHolderCoordinates() const override; + int GetDimension() const override; + Rank1View + GetDOFHolderClassificationDimensionsHost() const override; + Rank1View GetDOFHolderClassificationIdsHost() + const override; + + LO GetFullDataSize() const noexcept; + +private: + Kokkos::View owned_; + Kokkos::View gids_; + Kokkos::View class_dims_; + Kokkos::View class_ids_; + Kokkos::View owned_host_; + Kokkos::View gids_host_; + Kokkos::View classification_dims_host_; + Kokkos::View classification_ids_host_; + Kokkos::View coords_; + LO num_plane_nodes_; + std::shared_ptr discretization_; +}; + +} // namespace pcms + +#endif // PCMS_XGC_FIELD_LAYOUT_H diff --git a/src/pcms/field/out_of_bounds_policy.h b/src/pcms/field/out_of_bounds_policy.h new file mode 100644 index 000000000..7e3cdc1f6 --- /dev/null +++ b/src/pcms/field/out_of_bounds_policy.h @@ -0,0 +1,34 @@ +#ifndef PCMS_OUT_OF_BOUNDS_POLICY_H +#define PCMS_OUT_OF_BOUNDS_POLICY_H + +#include "pcms/utility/types.h" + +namespace pcms +{ + +enum class OutOfBoundsMode +{ + ERROR, // Throw error when points are out of bounds + FILL, // Fill out-of-bounds points with a fill value + NEAREST_BOUNDARY // Map to nearest boundary cell (extrapolate) +}; + +// OutOfBoundsPolicy wraps OutOfBoundsMode with an optional fill value into a +// single construction-time policy object. Out-of-bounds behavior is a policy +// passed when creating a PointEvaluator rather than mutable state on the field. +// +// NearestBoundary is an optional backend capability. Call +// FieldEvaluatorFactory::SupportsNearestBoundary() before requesting it. +// Backends that do not support it throw a descriptive error if it is requested. +// The policy is fixed when the PointEvaluator is created; it is not mutable +// after that evaluator has been constructed. + +struct OutOfBoundsPolicy +{ + OutOfBoundsMode mode = OutOfBoundsMode::ERROR; + Real fill_value = 0.0; // used only when mode == FILL +}; + +} // namespace pcms + +#endif // PCMS_OUT_OF_BOUNDS_POLICY_H diff --git a/src/pcms/field/point_evaluator.h b/src/pcms/field/point_evaluator.h new file mode 100644 index 000000000..8250c4ebc --- /dev/null +++ b/src/pcms/field/point_evaluator.h @@ -0,0 +1,56 @@ +#ifndef PCMS_POINT_EVALUATOR_H +#define PCMS_POINT_EVALUATOR_H + +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" + +namespace pcms +{ + +template +class Field; + +// PointEvaluator is bound to a specific set of query points. Created by +// calling CreatePointEvaluator(coords) on either a concrete field factory or a +// FieldEvaluatorFactory directly. Performs and caches all backend-specific +// localization work (spatial search, barycentric coordinate computation, etc.) +// at construction time. Evaluate may then be called repeatedly for different +// FieldData objects at zero additional localization cost. +// +// Replaces the previous design's FieldEvaluator + EvaluationCache pair. +// LocalizationHint from older code maps to the internal state of +// PointEvaluator. +// +// Field compatibility with a PointEvaluator is a precondition of Evaluate. +// Implementations should check this with a cheap backend-specific identity +// check rather than deep structural comparison and should throw pcms_error on +// mismatch. +// +// Evaluate writes results into a rank-2 output view with shape: +// [num_query_points][num_components] +// The caller must provide the full output buffer. Successful Evaluate calls +// fill the entire buffer. +template > +class PointEvaluator +{ +public: + virtual void Evaluate( + const Field& field, + Rank2View values) const = 0; + + virtual ~PointEvaluator() noexcept = default; +}; + +// Variant types using default layout for device memory space +using PointEvaluatorVariant = + std::variant>, + std::unique_ptr>, + std::unique_ptr>, + std::unique_ptr>, + std::unique_ptr>>; + +} // namespace pcms + +#endif // PCMS_POINT_EVALUATOR_H diff --git a/src/pcms/field/uniform_grid_binary_field.h b/src/pcms/field/uniform_grid_binary_field.h new file mode 100644 index 000000000..4bdd7d278 --- /dev/null +++ b/src/pcms/field/uniform_grid_binary_field.h @@ -0,0 +1,95 @@ +#ifndef PCMS_UNIFORM_GRID_BINARY_FIELD_H +#define PCMS_UNIFORM_GRID_BINARY_FIELD_H + +#include "pcms/field/layout/uniform_grid.h" +#include "pcms/field/data/simple.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/field_metadata.h" +#include "pcms/localization/point_search.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/types.h" +#include "pcms/utility/uniform_grid.h" + +#include +#include +#include +#include +#include + +namespace pcms +{ + +/** + * \brief Create a binary (inside/outside) mask field on a uniform grid. + * + * Each DOF in the returned order-1 (vertex-centered) field is set to 1.0 + * if the vertex lies inside the mesh and 0.0 otherwise. A single point + * search pass over the grid vertices is performed; no intermediate source + * field is required. + * + * \tparam Dim Spatial dimension (2 or 3). + * \param mesh The Omega_h mesh defining the domain. + * \param grid Uniform grid whose vertices are tested. + * \return Pair of (layout, field) with binary mask values. + */ +template +std::pair>, Field> +CreateUniformGridBinaryField(Omega_h::Mesh& mesh, const UniformGrid& grid) +{ + auto function_space = LagrangeFunctionSpace::FromUniformGrid( + grid, 1, CoordinateSystem::Cartesian); + auto layout = std::dynamic_pointer_cast>( + function_space.GetLayout()); + PCMS_ALWAYS_ASSERT(layout != nullptr); + auto field = function_space.template CreateField(FieldMetadata{}); + + auto coord_view = layout->GetDOFHolderCoordinates(); + auto coords = coord_view.GetCoordinates(); + LO n = layout->GetNumOwnedDofHolder(); + + Kokkos::View coords_d("coords_d", n); + Kokkos::parallel_for( + "CopyCoords", Kokkos::RangePolicy<>(0, n), KOKKOS_LAMBDA(int i) { + for (int d = 0; d < Dim; ++d) { + coords_d(i, d) = coords(i, d); + } + }); + + // Run point-in-mesh search + Kokkos::View::Result*> results_d; + if constexpr (Dim == 2) { + GridPointSearch2D search(mesh, grid.divisions[0], grid.divisions[1]); + results_d = search(coords_d); + } else { + GridPointSearch3D search(mesh, grid.divisions[0], grid.divisions[1], + grid.divisions[2]); + results_d = search(coords_d); + } + auto results_h = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, results_d); + + Kokkos::View data("binary_mask", n); + for (LO i = 0; i < n; ++i) + data(i) = (results_h(i).element_id >= 0) ? 1.0 : 0.0; + + field.SetDOFHolderDataHost( + Rank1View(data.data(), n)); + + return {std::move(layout), std::move(field)}; +} + +/** + * \brief Convenience overload: derives the grid from the mesh bounding box. + */ +template +std::pair>, Field> +CreateUniformGridBinaryField(Omega_h::Mesh& mesh, + const std::array& divisions) +{ + return CreateUniformGridBinaryField( + mesh, CreateUniformGridFromMesh(mesh, divisions)); +} + +} // namespace pcms + +#endif // PCMS_UNIFORM_GRID_BINARY_FIELD_H diff --git a/src/pcms/field_communicator.h b/src/pcms/field_communicator.h deleted file mode 100644 index 4a3c84bec..000000000 --- a/src/pcms/field_communicator.h +++ /dev/null @@ -1,287 +0,0 @@ -#ifndef PCMS_COUPLING_FIELD_COMMUNICATOR_H -#define PCMS_COUPLING_FIELD_COMMUNICATOR_H -#include -#include "pcms/field.h" -#include -#include "pcms/utility/inclusive_scan.h" -#include "pcms/utility/profile.h" -#include "pcms/partition.h" - -namespace pcms -{ - -namespace -{ -struct OutMsg -{ - redev::LOs dest; - redev::LOs offset; -}; - -// reverse partition is a map that has the partition rank as a key -// and the values are an vector where each entry is the index into -// the array of data to send -OutMsg ConstructOutMessage(const ReversePartitionMap& reverse_partition) -{ - PCMS_FUNCTION_TIMER; - OutMsg out; - redev::LOs counts; - counts.reserve(reverse_partition.size()); - out.dest.clear(); - out.dest.reserve(reverse_partition.size()); - // number of entries for each rank - for (auto& rank : reverse_partition) { - out.dest.push_back(rank.first); - counts.push_back(rank.second.size()); - } - out.offset.resize(counts.size() + 1); - out.offset[0] = 0; - pcms::inclusive_scan(counts.begin(), counts.end(), - std::next(out.offset.begin(), 1)); - return out; -} -size_t count_entries(const ReversePartitionMap& reverse_partition) -{ - PCMS_FUNCTION_TIMER; - size_t num_entries = 0; - for (const auto& v : reverse_partition) { - num_entries += v.second.size(); - } - return num_entries; -} -// note this function can be parallelized by making use of the offsets -redev::LOs ConstructPermutation(const ReversePartitionMap& reverse_partition) -{ - PCMS_FUNCTION_TIMER; - auto num_entries = count_entries(reverse_partition); - redev::LOs permutation(num_entries); - LO entry = 0; - for (auto& rank : reverse_partition) { - for (auto& idx : rank.second) { - PCMS_ALWAYS_ASSERT(static_cast(idx) < num_entries); - permutation[idx] = entry++; - } - } - return permutation; -} -/** - * - * @param local_gids local gids are the mesh GIDs in local mesh iteration order - * @param received_gids received GIDs are the GIDS in the order of the incomming - * message1 - * @return permutation array such that GIDS(Permutation[i]) = msgs - */ -redev::LOs ConstructPermutation(const std::vector& local_gids, - const std::vector& received_gids) -{ - PCMS_FUNCTION_TIMER; - - if (local_gids.size() != received_gids.size()) { - std::stringstream ss; - ss << " :local_gids.size() [" << local_gids.size() - << "] does not match received_gids.size() [" << received_gids.size() - << "]\n"; - std::cerr << ss.str(); - std::abort(); - } - - REDEV_ALWAYS_ASSERT(local_gids.size() == received_gids.size()); - REDEV_ALWAYS_ASSERT(std::is_permutation(local_gids.begin(), local_gids.end(), - received_gids.begin())); - std::map global_to_local_ids; - for (size_t i = 0; i < local_gids.size(); ++i) { - global_to_local_ids[local_gids[i]] = i; - } - redev::LOs permutation; - permutation.reserve(local_gids.size()); - for (auto gid : received_gids) { - permutation.push_back(global_to_local_ids[gid]); - } - return permutation; -} -OutMsg ConstructOutMessage(int rank, int nproc, - const redev::InMessageLayout& in) -{ - PCMS_FUNCTION_TIMER; - REDEV_ALWAYS_ASSERT(!in.srcRanks.empty()); - // auto nAppProcs = - // Omega_h::divide_no_remainder(in.srcRanks.size(),static_cast(nproc)); - auto nAppProcs = in.srcRanks.size() / static_cast(nproc); - // build dest and offsets arrays from incoming message metadata - redev::LOs senderDeg(nAppProcs); - for (size_t i = 0; i < nAppProcs - 1; i++) { - senderDeg[i] = - in.srcRanks[(i + 1) * nproc + rank] - in.srcRanks[i * nproc + rank]; - } - const auto totInMsgs = in.offset[rank + 1] - in.offset[rank]; - senderDeg[nAppProcs - 1] = - totInMsgs - in.srcRanks[(nAppProcs - 1) * nproc + rank]; - OutMsg out; - for (size_t i = 0; i < nAppProcs; i++) { - if (senderDeg[i] > 0) { - out.dest.push_back(i); - } - } - redev::GO sum = 0; - for (auto deg : senderDeg) { // exscan over values > 0 - if (deg > 0) { - out.offset.push_back(sum); - sum += deg; - } - } - out.offset.push_back(sum); - return out; -} - -template -bool HasDuplicates(std::vector v) -{ - PCMS_FUNCTION_TIMER; - std::sort(v.begin(), v.end()); - auto it = std::adjacent_find(v.begin(), v.end()); - return it != v.end(); -} -} // namespace - -using redev::Mode; - -// TODO refactor to take application rather than channel -template -struct FieldCommunicator -{ - using T = typename FieldAdapterT::value_type; - -public: - FieldCommunicator(std::string name, MPI_Comm mpi_comm, redev::Redev& redev, - redev::Channel& channel, FieldAdapterT& field_adapter) - : mpi_comm_(mpi_comm), - channel_(channel), - comm_buffer_{}, - message_permutation_{}, - buffer_size_needs_update_{true}, - field_adapter_(field_adapter), - name_{std::move(name)}, - redev_(redev) - { - PCMS_FUNCTION_TIMER; - comm_ = channel.CreateComm(name_, mpi_comm_); - gid_comm_ = channel.CreateComm(name_ + "_gids", mpi_comm_); - if (mpi_comm != MPI_COMM_NULL) { - UpdateLayout(); - } else { - UpdateLayoutNull(); - } - } - - FieldCommunicator(const FieldCommunicator&) = delete; - FieldCommunicator(FieldCommunicator&&) = default; - FieldCommunicator& operator=(const FieldCommunicator&) = delete; - FieldCommunicator& operator=(FieldCommunicator&&) = default; - - void Send(Mode mode = Mode::Synchronous) - { - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(channel_.InSendCommunicationPhase()); - auto n = field_adapter_.Serialize({}, {}); - REDEV_ALWAYS_ASSERT(comm_buffer_.size() == static_cast(n)); - auto buffer = make_array_view(comm_buffer_); - field_adapter_.Serialize(buffer, - make_const_array_view(message_permutation_)); - comm_.Send(buffer.data_handle(), mode); - } - void Receive(Mode mode = Mode::Synchronous) - { - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(channel_.InReceiveCommunicationPhase()); - // Current implementation requires that Receive is always called in Sync - // mode because we make an immediate call to deserialize after a call to - // receive. - auto data = comm_.Recv(mode); - field_adapter_.Deserialize(make_const_array_view(data), - make_const_array_view(message_permutation_)); - } - /** update the permutation array and buffer sizes upon mesh change - * @WARNING this function mut be called on *both* the client and server - * after any modifications on the client - */ -private: - // note channel_ operations are collective on full channel comm - // comm_ operations should only be called on ranks with - void UpdateLayout() - { - PCMS_FUNCTION_TIMER; - // if (mpi_comm_ != MPI_COMM_NULL) { - auto gids = field_adapter_.GetGids(); - if (redev_.GetProcessType() == redev::ProcessType::Client) { - const ReversePartitionMap reverse_partition = - field_adapter_.GetReversePartitionMap(Partition{redev_.GetPartition()}); - auto out_message = ConstructOutMessage(reverse_partition); - comm_.SetOutMessageLayout(out_message.dest, out_message.offset); - gid_comm_.SetOutMessageLayout(out_message.dest, out_message.offset); - message_permutation_ = ConstructPermutation(reverse_partition); - // use permutation array to send the gids - std::vector gid_msgs(gids.size()); - REDEV_ALWAYS_ASSERT(gids.size() == message_permutation_.size()); - for (size_t i = 0; i < gids.size(); ++i) { - gid_msgs[message_permutation_[i]] = gids[i]; - } - channel_.BeginSendCommunicationPhase(); - gid_comm_.Send(gid_msgs.data()); - channel_.EndSendCommunicationPhase(); - } else { - channel_.BeginReceiveCommunicationPhase(); - auto recv_gids = gid_comm_.Recv(); - channel_.EndReceiveCommunicationPhase(); - int rank, nproc; - MPI_Comm_rank(mpi_comm_, &rank); - MPI_Comm_size(mpi_comm_, &nproc); - // we require that the layout for the gids and the message are the same - const auto in_message_layout = gid_comm_.GetInMessageLayout(); - auto out_message = ConstructOutMessage(rank, nproc, in_message_layout); - comm_.SetOutMessageLayout(out_message.dest, out_message.offset); - // construct server permutation array - // Verify that there are no duplicate entries in the received - // data. Duplicate data indicates that sender is not sending data from - // only the owned rank - REDEV_ALWAYS_ASSERT(!HasDuplicates(recv_gids)); - message_permutation_ = ConstructPermutation(gids, recv_gids); - } - comm_buffer_.resize(message_permutation_.size()); - //} - } - void UpdateLayoutNull() - { - PCMS_FUNCTION_TIMER; - // if (mpi_comm_ != MPI_COMM_NULL) { - if (redev_.GetProcessType() == redev::ProcessType::Client) { - channel_.BeginSendCommunicationPhase(); - channel_.EndSendCommunicationPhase(); - } else { - channel_.BeginReceiveCommunicationPhase(); - channel_.EndReceiveCommunicationPhase(); - } - } - -private: - MPI_Comm mpi_comm_; - redev::Channel& channel_; - std::vector comm_buffer_; - std::vector message_permutation_; - redev::BidirectionalComm comm_; - redev::BidirectionalComm gid_comm_; - bool buffer_size_needs_update_; - // Stored functions used for updated field - // info/serialization/deserialization - FieldAdapterT& field_adapter_; - redev::Redev& redev_; - std::string name_; -}; -template <> -struct FieldCommunicator -{ - void Send(Mode = {}) {} - void Receive(Mode = {}) {} -}; -} // namespace pcms - -#endif // PCMS_COUPLING_FIELD_COMMUNICATOR_H diff --git a/src/pcms/field_evaluation_methods.h b/src/pcms/field_evaluation_methods.h deleted file mode 100644 index b503f9f92..000000000 --- a/src/pcms/field_evaluation_methods.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef PCMS_COUPLING_FIELD_EVALUATION_METHODS_H -#define PCMS_COUPLING_FIELD_EVALUATION_METHODS_H -namespace pcms -{ - -template -struct Lagrange -{ - static constexpr int order{o}; -}; -// variable order lagrange interpolation -template <> -struct Lagrange<0> -{ - explicit Lagrange(int o) : order{o} {}; - int order; -}; - -struct NearestNeighbor -{}; - -struct Copy -{}; - -} // namespace pcms - -#endif // PCMS_COUPLING_FIELD_EVALUATION_METHODS_H diff --git a/src/pcms/field_layout_communicator.cpp b/src/pcms/field_layout_communicator.cpp deleted file mode 100644 index 00f52543e..000000000 --- a/src/pcms/field_layout_communicator.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include "field_layout_communicator.h" - -namespace pcms -{ - -namespace field_layout_communicator -{ -// reverse partition is a map that has the partition rank as a key -// and the values are an vector where each entry is the index into -// the array of data to send -OutMsg ConstructOutMessage(const ReversePartitionMap2& reverse_partition) -{ - PCMS_FUNCTION_TIMER; - OutMsg out; - redev::LOs counts; - counts.reserve(reverse_partition.size()); - out.dest.clear(); - out.dest.reserve(reverse_partition.size()); - // number of entries for each rank - for (auto& rank : reverse_partition) { - out.dest.push_back(rank.first); - counts.push_back(rank.second.indices.size() + - rank.second.ent_offsets.size()); - } - out.offset.resize(counts.size() + 1); - out.offset[0] = 0; - pcms::inclusive_scan(counts.begin(), counts.end(), - std::next(out.offset.begin(), 1)); - return out; -} - -size_t count_entries(const ReversePartitionMap2& reverse_partition) -{ - PCMS_FUNCTION_TIMER; - size_t num_entries = 0; - for (const auto& v : reverse_partition) { - num_entries += v.second.indices.size(); - } - return num_entries; -} - -// note this function can be parallelized by making use of the offsets -redev::LOs ConstructPermutation(const ReversePartitionMap2& reverse_partition, - size_t num_entries, int* length) -{ - PCMS_FUNCTION_TIMER; - redev::LOs permutation(num_entries); - LO entry = 0; - for (auto& rank : reverse_partition) { - entry += ent_offsets_len; - - for (int e = 0; e < rank.second.ent_offsets.size() - 1; ++e) { - int start = rank.second.ent_offsets[e]; - int end = rank.second.ent_offsets[e + 1]; - - for (int i = start; i < end; ++i) { - LO index = rank.second.indices[i]; - PCMS_ALWAYS_ASSERT(index < permutation.size()); - permutation[index] = entry++; - } - } - } - *length = entry; - return permutation; -} - -/** - * - * @param local_gids local gids are the mesh GIDs in local mesh iteration order - * @param received_gids received GIDs are the GIDS in the order of the incomming - * message1 - * @return permutation array such that GIDS(Permutation[i]) = msgs - */ -redev::LOs ConstructPermutation(GlobalIDView local_gids, - GlobalIDView received_msg, - EntOffsetsArray ent_offsets) -{ - PCMS_FUNCTION_TIMER; - std::array, 4> gid_to_buffer_index; - size_t offset = 0; - while (true) { - GlobalIDView received_offsets( - received_msg.data_handle() + offset, ent_offsets_len); - int length = received_offsets[received_offsets.size() - 1]; - GlobalIDView received_gids( - received_msg.data_handle() + offset + ent_offsets_len, length); - - PCMS_ALWAYS_ASSERT(offset + ent_offsets_len + length - 1 < - received_msg.size()); - - for (int e = 0; e < received_offsets.size() - 1; ++e) { - size_t start = received_offsets[e]; - size_t end = received_offsets[e + 1]; - - for (int i = start; i < end; ++i) { - gid_to_buffer_index[e][received_gids[i]] = offset + ent_offsets_len + i; - } - } - - offset += length + ent_offsets_len; - if (offset >= received_msg.size()) - break; - } - - redev::LOs permutation; - permutation.reserve(local_gids.size()); - for (int e = 0; e < ent_offsets.size() - 1; ++e) { - size_t start = ent_offsets[e]; - size_t end = ent_offsets[e + 1]; - - for (int i = start; i < end; ++i) { - permutation.push_back(gid_to_buffer_index[e][local_gids[i]]); - } - } - - REDEV_ALWAYS_ASSERT(permutation.size() == local_gids.size()); - return permutation; -} - -OutMsg ConstructOutMessage(int rank, int nproc, - const redev::InMessageLayout& in) -{ - PCMS_FUNCTION_TIMER; - REDEV_ALWAYS_ASSERT(!in.srcRanks.empty()); - // auto nAppProcs = - // Omega_h::divide_no_remainder(in.srcRanks.size(),static_cast(nproc)); - auto nAppProcs = in.srcRanks.size() / static_cast(nproc); - // build dest and offsets arrays from incoming message metadata - redev::LOs senderDeg(nAppProcs); - for (size_t i = 0; i < nAppProcs - 1; i++) { - senderDeg[i] = - in.srcRanks[(i + 1) * nproc + rank] - in.srcRanks[i * nproc + rank]; - } - const auto totInMsgs = in.offset[rank + 1] - in.offset[rank]; - senderDeg[nAppProcs - 1] = - totInMsgs - in.srcRanks[(nAppProcs - 1) * nproc + rank]; - OutMsg out; - for (size_t i = 0; i < nAppProcs; i++) { - if (senderDeg[i] > 0) { - out.dest.push_back(i); - } - } - redev::GO sum = 0; - for (auto deg : senderDeg) { // exscan over values > 0 - if (deg > 0) { - out.offset.push_back(sum); - sum += deg; - } - } - out.offset.push_back(sum); - return out; -} - -} // namespace field_layout_communicator -} // namespace pcms \ No newline at end of file diff --git a/src/pcms/field_layout_communicator.h b/src/pcms/field_layout_communicator.h deleted file mode 100644 index f2723bad3..000000000 --- a/src/pcms/field_layout_communicator.h +++ /dev/null @@ -1,202 +0,0 @@ -#ifndef FIELD_LAYOUT_COMMUNICATOR_H_ -#define FIELD_LAYOUT_COMMUNICATOR_H_ - -#include "field_layout.h" -#include "pcms/field_layout.h" -#include "pcms/field.h" -#include "pcms/utility/profile.h" -#include "pcms/utility/assert.h" -#include "pcms/utility/inclusive_scan.h" -#include "pcms/utility/arrays.h" -#include -#include - -namespace pcms -{ - -namespace field_layout_communicator -{ -struct OutMsg -{ - redev::LOs dest; - redev::LOs offset; -}; - -// reverse partition is a map that has the partition rank as a key -// and the values are an vector where each entry is the index into -// the array of data to send -OutMsg ConstructOutMessage(const ReversePartitionMap2& reverse_partition); - -size_t count_entries(const ReversePartitionMap2& reverse_partition); - -// note this function can be parallelized by making use of the offsets -redev::LOs ConstructPermutation(const ReversePartitionMap2& reverse_partition, - size_t num_entries, int* length); - -/** - * - * @param local_gids local gids are the mesh GIDs in local mesh iteration order - * @param received_gids received GIDs are the GIDS in the order of the incomming - * message1 - * @return permutation array such that GIDS(Permutation[i]) = msgs - */ -redev::LOs ConstructPermutation(GlobalIDView local_gids, - GlobalIDView received_msg, - EntOffsetsArray ent_offsets); - -OutMsg ConstructOutMessage(int rank, int nproc, - const redev::InMessageLayout& in); - -template -bool HasDuplicates(ItBegin begin, ItEnd end) -{ - PCMS_FUNCTION_TIMER; - std::sort(begin, end); - auto it = std::adjacent_find(begin, end); - return it != end; -} - -template -bool IsValid(std::vector recv_msg) -{ - auto gids = recv_msg.begin() + 4; - for (int i = 0; i < 4; ++i) { - auto begin = gids + recv_msg[i]; - auto end = i + 1 < 4 ? gids + recv_msg[i + 1] : recv_msg.end(); - if (HasDuplicates(begin, end)) { - return false; - } - } - - return true; -} -} // namespace field_layout_communicator - -class FieldLayoutCommunicator -{ -public: - FieldLayoutCommunicator(std::string name, MPI_Comm mpi_comm, - redev::Redev& redev, redev::Channel& channel, - const FieldLayout& layout) - : mpi_comm_(mpi_comm), - channel_(channel), - message_permutation_{}, - buffer_size_needs_update_{true}, - layout_(layout), - name_{std::move(name)}, - redev_(redev) - { - gid_comm_ = channel.CreateComm(name_ + "_gids", mpi_comm_); - if (mpi_comm != MPI_COMM_NULL) { - UpdateLayout(); - } else { - UpdateLayoutNull(); - } - } - - Rank1View GetPermutationArray() const - { - return make_const_array_view(message_permutation_); - } - - const std::string& GetName() const { return name_; } - - const FieldLayout& GetLayout() const { return layout_; } - - size_t GetMsgSize() const { return msg_size_; } - - redev::Channel& GetChannel() { return channel_; } - - MPI_Comm& GetMPIComm() { return mpi_comm_; } - - template - void SetOutMessageLayout(redev::BidirectionalComm& comm) - { - comm.SetOutMessageLayout(out_msg_.dest, out_msg_.offset); - } - - void UpdateLayout() - { - namespace flc = field_layout_communicator; - - PCMS_FUNCTION_TIMER; - auto gids = layout_.GetGids(); - auto owned = layout_.GetOwned(); - auto ent_offsets = layout_.GetEntOffsets(); - if (redev_.GetProcessType() == redev::ProcessType::Client) { - const ReversePartitionMap2 reverse_partition = - layout_.GetReversePartitionMap(redev::Partition{redev_.GetPartition()}); - out_msg_ = flc::ConstructOutMessage(reverse_partition); - gid_comm_.SetOutMessageLayout(out_msg_.dest, out_msg_.offset); - int length; - message_permutation_ = - flc::ConstructPermutation(reverse_partition, gids.size(), &length); - // use permutation array to send the gids - msg_size_ = length; - std::vector msg(msg_size_); - for (size_t i = 0; i < gids.size(); ++i) { - if (owned[i]) - msg[message_permutation_[i]] = gids[i]; - } - for (auto& rank : reverse_partition) { - size_t i_offsets = - message_permutation_[rank.second.indices[0]] - ent_offsets_len; - for (int i = 0; i < rank.second.ent_offsets.size(); ++i) { - msg[i_offsets + i] = rank.second.ent_offsets[i]; - } - } - - channel_.BeginSendCommunicationPhase(); - gid_comm_.Send(msg.data()); - channel_.EndSendCommunicationPhase(); - } else { - channel_.BeginReceiveCommunicationPhase(); - auto recv_gids = gid_comm_.Recv(); - channel_.EndReceiveCommunicationPhase(); - int rank, nproc; - MPI_Comm_rank(mpi_comm_, &rank); - MPI_Comm_size(mpi_comm_, &nproc); - // we require that the layout for the gids and the message are the same - const auto in_message_layout = gid_comm_.GetInMessageLayout(); - out_msg_ = flc::ConstructOutMessage(rank, nproc, in_message_layout); - // construct server permutation array - // Verify that there are no duplicate entries in the received - // data. Duplicate data indicates that sender is not sending data from - // only the owned rank - // REDEV_ALWAYS_ASSERT(IsValid(recv_gids)); - GlobalIDView recv_gids_view(recv_gids.data(), - recv_gids.size()); - - message_permutation_ = - flc::ConstructPermutation(gids, recv_gids_view, ent_offsets); - msg_size_ = recv_gids.size(); - } - } - - void UpdateLayoutNull() - { - PCMS_FUNCTION_TIMER; - if (redev_.GetProcessType() == redev::ProcessType::Client) { - channel_.BeginSendCommunicationPhase(); - channel_.EndSendCommunicationPhase(); - } else { - channel_.BeginReceiveCommunicationPhase(); - channel_.EndReceiveCommunicationPhase(); - } - } - -private: - MPI_Comm mpi_comm_; - redev::Channel& channel_; - std::vector message_permutation_; - redev::BidirectionalComm gid_comm_; - bool buffer_size_needs_update_; - field_layout_communicator::OutMsg out_msg_; - const FieldLayout& layout_; - redev::Redev& redev_; - std::string name_; - size_t msg_size_; -}; -} // namespace pcms - -#endif // FIELD_LAYOUT_COMMUNICATOR_H_ diff --git a/src/pcms/fortranapi/CMakeLists.txt b/src/pcms/fortranapi/CMakeLists.txt index aa1bcfaca..cb2f578ed 100644 --- a/src/pcms/fortranapi/CMakeLists.txt +++ b/src/pcms/fortranapi/CMakeLists.txt @@ -15,19 +15,19 @@ target_link_libraries( PRIVATE pcms::capi::core PUBLIC Kokkos::kokkos) -add_library(pcms_fortranapi_interpolator pcms_interpolator.f90 interpolator_wrap.c pcms_mesh.f90 mesh_wrap.c) -add_library(pcms::fortranapi::interpolator ALIAS pcms_fortranapi_interpolator) -set_target_properties(pcms_fortranapi_interpolator PROPERTIES Fortran_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/fortran - OUTPUT_NAME pcmsfortranapiinterpolator - EXPORT_NAME fortranapi::interpolator) -target_include_directories(pcms_fortranapi_interpolator PUBLIC $> +add_library(pcms_fortranapi_transfer pcms_interpolator.f90 interpolator_wrap.c pcms_mesh.f90 mesh_wrap.c) +add_library(pcms::fortranapi::transfer ALIAS pcms_fortranapi_transfer) +set_target_properties(pcms_fortranapi_transfer PROPERTIES Fortran_MODULE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/fortran + OUTPUT_NAME pcmsfortranapi_transfer + EXPORT_NAME fortranapi::transfer) +target_include_directories(pcms_fortranapi_transfer PUBLIC $> $) -target_link_libraries(pcms_fortranapi_interpolator PRIVATE pcms::capi::interpolator PUBLIC Kokkos::kokkos) +target_link_libraries(pcms_fortranapi_transfer PRIVATE pcms::capi::transfer PUBLIC Kokkos::kokkos) INSTALL(DIRECTORY $ DESTINATION ${CMAKE_INSTALL_LIBDIR}) -INSTALL(DIRECTORY $ +INSTALL(DIRECTORY $ DESTINATION ${CMAKE_INSTALL_LIBDIR}) # high level interface target @@ -36,7 +36,7 @@ add_library(pcms::fortranapi ALIAS pcms_fortranapi) set_target_properties(pcms_fortranapi PROPERTIES EXPORT_NAME fortranapi) # link capi libraries to a high level interface library target_link_libraries(pcms_fortranapi INTERFACE pcms::fortranapi::core) -target_link_libraries(pcms_fortranapi INTERFACE pcms::fortranapi::interpolator) +target_link_libraries(pcms_fortranapi INTERFACE pcms::fortranapi::transfer) install( @@ -70,8 +70,8 @@ install( DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) install( - TARGETS pcms_fortranapi_interpolator - EXPORT pcms_fortranapi_interpolator-targets + TARGETS pcms_fortranapi_transfer + EXPORT pcms_fortranapi_transfer-targets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} @@ -80,6 +80,6 @@ install( PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms/fortranapi/) install( - EXPORT pcms_fortranapi_interpolator-targets + EXPORT pcms_fortranapi_transfer-targets NAMESPACE pcms:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) diff --git a/src/pcms/interpolator/CMakeLists.txt b/src/pcms/interpolator/CMakeLists.txt deleted file mode 100644 index 472ebe33f..000000000 --- a/src/pcms/interpolator/CMakeLists.txt +++ /dev/null @@ -1,53 +0,0 @@ -find_package(KokkosKernels REQUIRED) - -set(PCMS_FIELD_TRANSFER_HEADERS - pcms_interpolator_aliases.hpp - adj_search.hpp - mls_interpolation_impl.hpp - queue_visited.hpp - linear_interpolant.hpp - multidimarray.hpp - mls_interpolation.hpp - pcms_interpolator_view_utils.hpp - pcms_interpolator_logger.hpp - interpolation_base.h - interpolation_helpers.h - spline_interpolator.hpp) - -set(PCMS_FIELD_TRANSFER_SOURCES - mls_interpolation.cpp - interpolation_base.cpp -) - -add_library(pcms_interpolator ${PCMS_FIELD_TRANSFER_SOURCES}) -set_target_properties(pcms_interpolator PROPERTIES - OUTPUT_NAME pcmsinterpolator - EXPORT_NAME interpolator) -target_sources(pcms_interpolator PUBLIC - FILE_SET transformer - TYPE HEADERS - BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. - FILES ${PCMS_FIELD_TRANSFER_HEADERS}) -add_library(pcms::interpolator ALIAS pcms_interpolator) -target_compile_features(pcms_interpolator PUBLIC cxx_std_17) - -target_link_libraries(pcms_interpolator PUBLIC pcms::core PRIVATE Kokkos::kokkoskernels) - -target_include_directories(pcms_interpolator INTERFACE - $ - $ - $) - - -install( - TARGETS pcms_interpolator - EXPORT pcms_interpolator-targets - INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms/interpolator - FILE_SET transformer DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) - - -install( - EXPORT pcms_interpolator-targets - NAMESPACE pcms:: - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) diff --git a/src/pcms/interpolator/interpolation_helpers.h b/src/pcms/interpolator/interpolation_helpers.h deleted file mode 100644 index bc76b1a83..000000000 --- a/src/pcms/interpolator/interpolation_helpers.h +++ /dev/null @@ -1,37 +0,0 @@ -// -// Created by hasanm4 on 10/10/25. -// - -#ifndef PCMS_INTERPOLATION_HELPERS_H -#define PCMS_INTERPOLATION_HELPERS_H -#include "pcms/utility/arrays.h" -#include "pcms/utility/memory_spaces.h" -#include -#include - -void copyHostScalarArrayView2HostWrite( - pcms::Rank1View source, - Omega_h::HostWrite& target); - -void copyHostWrite2ScalarArrayView( - const Omega_h::HostWrite& source, - pcms::Rank1View target); - -Omega_h::Reals getCentroids(Omega_h::Mesh& mesh); - -inline bool within_number_of_support_range(unsigned min_supports_found, - unsigned max_supports_found, - unsigned min_req_supports, - unsigned max_allowed_supports) -{ - return (min_supports_found >= min_req_supports) && - (max_supports_found <= max_allowed_supports); -} - -void minmax(Omega_h::Read num_supports, - unsigned& min_supports_found, unsigned& max_supports_found); -void adapt_radii(unsigned min_req_supports, unsigned max_allowed_supports, - Omega_h::LO n_targets, Omega_h::Write radii2_l, - Omega_h::Write num_supports); - -#endif // PCMS_INTERPOLATION_HELPERS_H diff --git a/src/pcms/interpolator/mls_interpolation.hpp b/src/pcms/interpolator/mls_interpolation.hpp deleted file mode 100644 index a941dbb40..000000000 --- a/src/pcms/interpolator/mls_interpolation.hpp +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef MLS_RBF_OPTIONS_HPP -#define MLS_RBF_OPTIONS_HPP - -#include - -// struct holds results neighbor search -// defined in adj_search -struct SupportResults; - -namespace pcms -{ -/** - * @brief Enumeration of supported radial basis functions (RBF) for MLS - *interpolation. - * - * This enum specifies the type of radial basis function used to weight source - *points in the Moving Least Squares (MLS) interpolation process. - * - * Members: - * - RBF_GAUSSIAN: - * A Gaussian RBF: `exp(- a^2 * r^2)`, smooth and commonly used. Good for - *localized support. `a` is a spreading/decay factor - * - * - RBF_C4: - * A compactly supported C4-continuous function. Useful for bounded support - *and efficiency. - * - * - RBF_CONST: - * Constant basis function. Effectively uses uniform weights. - * - * - NO_OP: - * No operation. Disables RBF weighting — typically used when weights - * are externally defined or not needed. - * - * @note These are intended to be passed into function `mls_interpolation` to - *control weighting behavior. - */ -enum class RadialBasisFunction -{ - RBF_GAUSSIAN = 0, - RBF_C4, - RBF_CONST, - NO_OP - -}; - -/** - * @brief Performs Moving Least Squares (MLS) interpolation at target points. - * - * This function computes interpolated values at a set of target coordinates - * using Moving Least Squares (MLS) based on the provided source values and - * coordinates. It supports different radial basis functions (RBFs), polynomial - * degrees, dimension, optional regularization and tolerance - * - * @param source_values A flat array of source data values. Length should - * be `num_sources`. - * @param source_coordinates A flat array of source point coordinates. Length - * should be `num_sources * dim`. - * @param target_coordinates A flat array of target point coordinates. Length - * should be `num_targets * dim`. - * @param support A data structure holding neighbor information for - * each target (in - * CSR format). - * @param dim Dimension - * @param degree Polynomial degree - * @param bf The radial basis function used for weighting - * (e.g., Gaussian, C4). - * @param lambda Optional regularization parameter (default is - * 0.0). Helps with stability in ill-conditioned systems. - * @param tol Optional solver tolerance (default is 1e-6). - * Small singular values below this are discarded. - * - * @return A Write array containing the interpolated values at each target - * point. - * - * @note - * - All input arrays are expected to reside in device memory (e.g., Kokkos - * device views). - * - Ensure consistency in dimensions: coordinate arrays must be sized as - * `num_points * dim`. - * - The result array length will match the number of target points. - */ -Omega_h::Write mls_interpolation( - const Omega_h::Reals source_values, const Omega_h::Reals source_coordinates, - const Omega_h::Reals target_coordinates, const SupportResults& support, - const Omega_h::LO& dim, const Omega_h::LO& degree, RadialBasisFunction bf, - double lambda = 0, double tol = 1e-6, double decay_factor = 5.0); -} // namespace pcms -#endif diff --git a/src/pcms/localization/CMakeLists.txt b/src/pcms/localization/CMakeLists.txt new file mode 100644 index 000000000..269b1b776 --- /dev/null +++ b/src/pcms/localization/CMakeLists.txt @@ -0,0 +1,61 @@ +set( + PCMS_LOCALIZATION_HEADERS + point_search.h + queue_visited.hpp + adj_search.hpp + mls_support_helpers.h + localization_factory.h + point_cloud_localization.h + mesh_localization.h +) + +set( + PCMS_LOCALIZATION_SOURCES + point_search.cpp + adj_search.cpp + mls_support_helpers.cpp + mesh_localization.cpp +) +add_library(pcms_localization ${PCMS_LOCALIZATION_SOURCES}) +add_library(pcms::localization ALIAS pcms_localization) +target_include_directories( + pcms_localization + PUBLIC + # include path should be pcms/localization/ + "$" + # include path to search for pcms/config.h + "$" + "$") +target_link_libraries( + pcms_localization PUBLIC + Kokkos::kokkos + pcms::utility + pcms::discretization + Omega_h::omega_h +) +target_compile_features(pcms_localization PUBLIC cxx_std_20) + +set_target_properties( + pcms_localization PROPERTIES OUTPUT_NAME pcmslocalization EXPORT_NAME + localization +) + +## export the library +set_target_properties(pcms_localization PROPERTIES PUBLIC_HEADER "${PCMS_LOCALIZATION_HEADERS}") + +install( + TARGETS pcms_localization + EXPORT pcms_localization-targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms/localization +) + +install( + EXPORT pcms_localization-targets + NAMESPACE pcms:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms +) diff --git a/src/pcms/interpolator/adj_search.hpp b/src/pcms/localization/adj_search.cpp similarity index 50% rename from src/pcms/interpolator/adj_search.hpp rename to src/pcms/localization/adj_search.cpp index 85baa236b..e1b937d7d 100644 --- a/src/pcms/interpolator/adj_search.hpp +++ b/src/pcms/localization/adj_search.cpp @@ -1,31 +1,12 @@ -#ifndef ADJ_SEARCH_HPP -#define ADJ_SEARCH_HPP +#include "pcms/localization/adj_search.hpp" +#include -#include -#include -#include "interpolation_helpers.h" // for helper functions - -#include "queue_visited.hpp" - -static constexpr int max_dim = 3; - -// TODO change this into span/mdspan -OMEGA_H_INLINE -Omega_h::Real calculateDistance(const Omega_h::Real* p1, - const Omega_h::Real* p2, const int dim) +namespace pcms { - Omega_h::Real dx, dy, dz; - dx = p1[0] - p2[0]; - dy = p1[1] - p2[1]; - if (dim != 3) { - dz = 0.0; - } else { - dz = p1[2] - p2[2]; - } - return dx * dx + dy * dy + dz * dz; -} -inline void checkTargetPoints( +// Debug helper retained intentionally: useful for diagnosing point-localization +// failures while developing support-search logic. +[[maybe_unused]] static void checkTargetPoints( const Kokkos::View& results) { Kokkos::fence(); @@ -45,7 +26,9 @@ inline void checkTargetPoints( pcms::printInfo("\n"); } -inline void printSupportsForTarget( +// Debug helper retained intentionally: useful for tracing CSR support contents +// for a specific target id during local debugging. +[[maybe_unused]] static void printSupportsForTarget( const Omega_h::LO target_id, const Omega_h::Write& supports_ptr, const Omega_h::Write& nSupports, const Omega_h::Write& support_idx) @@ -66,43 +49,147 @@ inline void printSupportsForTarget( }); } -class FindSupports +static Omega_h::Write build_support_offsets( + const Omega_h::LO nvertices_target, + const Omega_h::Write& nSupports, Omega_h::LO& total_supports) +{ + auto supports_ptr = Omega_h::Write( + nvertices_target + 1, 0, "number of support source vertices in CSR format"); + + total_supports = 0; + Kokkos::parallel_scan( + nvertices_target, + OMEGA_H_LAMBDA(int j, int& update, bool final) { + update += nSupports[j]; + if (final) { + supports_ptr[j + 1] = update; + } + }, + total_supports); + + return supports_ptr; +} + +static Omega_h::Write locate_target_cells( + Omega_h::Mesh& source_mesh, const Omega_h::Reals& target_coords, + Omega_h::LO nvertices_target) +{ + const auto dim = source_mesh.dim(); + auto source_cell_ids = + Omega_h::Write(nvertices_target, -1, "source cell ids"); + + if (dim == 2) { + Kokkos::View target_points("target_points", + nvertices_target); + Omega_h::parallel_for( + nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO i) { + target_points(i, 0) = target_coords[i * dim]; + target_points(i, 1) = target_coords[i * dim + 1]; + }); + Kokkos::fence(); + + pcms::GridPointSearch2D search_cell(source_mesh, 10, 10); + auto results = search_cell(target_points); + Omega_h::parallel_for( + nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO i) { + auto source_cell_id = results(i).element_id; + if (source_cell_id < 0) + source_cell_id = Kokkos::abs(source_cell_id); + OMEGA_H_CHECK_PRINTF( + source_cell_id >= 0, + "ERROR: Source cell id not found for target %d (%f,%f)\n", i, + target_points(i, 0), target_points(i, 1)); + source_cell_ids[i] = source_cell_id; + }); + } else if (dim == 3) { + Kokkos::View target_points("target_points", + nvertices_target); + Omega_h::parallel_for( + nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO i) { + target_points(i, 0) = target_coords[i * dim]; + target_points(i, 1) = target_coords[i * dim + 1]; + target_points(i, 2) = target_coords[i * dim + 2]; + }); + Kokkos::fence(); + + pcms::GridPointSearch3D search_cell(source_mesh, 10, 10, 10); + auto results = search_cell(target_points); + Omega_h::parallel_for( + nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO i) { + auto source_cell_id = results(i).element_id; + if (source_cell_id < 0) + source_cell_id = Kokkos::abs(source_cell_id); + OMEGA_H_CHECK_PRINTF(source_cell_id >= 0, + "ERROR: Source cell id not found for target %d\n", + i); + source_cell_ids[i] = source_cell_id; + }); + } else { + throw pcms_error("Unsupported dimension in locate_target_cells"); + } + + return source_cell_ids; +} + +OMEGA_H_INLINE void copy_from_rank1_view(const Omega_h::Reals& view, + const Omega_h::LO id, + const Omega_h::LO dim, + Omega_h::Real* out) +{ + for (Omega_h::LO k = 0; k < dim; ++k) { + out[k] = view[id * dim + k]; + } +} + +OMEGA_H_INLINE void copy_from_rank2_view( + const Kokkos::View& view, const Omega_h::LO id, + const Omega_h::LO dim, Omega_h::Real* out) +{ + for (Omega_h::LO k = 0; k < dim; ++k) { + out[k] = view(id, k); + } +} + +OMEGA_H_INLINE void add_support_if_unvisited_and_within_cutoff( + const Omega_h::LO candidate_id, const Omega_h::Real cutoff_distance, + const Omega_h::Real* target_coords, const Omega_h::Reals& support_coords_view, + const Omega_h::LO dim, Track& visited, Queue& queue, int& count, + const bool is_build_csr_call, const Omega_h::LO start_counter, + Omega_h::Write support_idx) { -private: - Omega_h::Mesh& source_mesh; - Omega_h::Mesh& target_mesh; // TODO it's null when one mesh is used - -public: - FindSupports(Omega_h::Mesh& source_mesh_, Omega_h::Mesh& target_mesh_) - : source_mesh(source_mesh_), target_mesh(target_mesh_){}; - - FindSupports(Omega_h::Mesh& mesh_) : source_mesh(mesh_), target_mesh(mesh_){}; - - void adjBasedSearch(Omega_h::Write& supports_ptr, - Omega_h::Write& nSupports, - Omega_h::Write& support_idx, - Omega_h::Write& radii2, - bool is_build_csr_call); - - void adjBasedSearchCentroidNodes(Omega_h::Write& supports_ptr, - Omega_h::Write& nSupports, - Omega_h::Write& support_idx, - Omega_h::Write& radii2, - bool is_build_csr_call); -}; - -inline void FindSupports::adjBasedSearch( + if (!visited.notVisited(candidate_id)) + return; + + visited.push_back(candidate_id); + + Omega_h::Real candidate_coords[max_dim]; + for (Omega_h::LO k = 0; k < dim; ++k) { + candidate_coords[k] = support_coords_view[candidate_id * dim + k]; + } + + const Omega_h::Real dist = + pcms::distance_squared(target_coords, candidate_coords, dim); + if (dist <= cutoff_distance) { + count++; + queue.push_back(candidate_id); + if (!is_build_csr_call) { + const Omega_h::LO idx_count = count - 1; + support_idx[start_counter + idx_count] = candidate_id; + } + } +} + +static void adjBasedSearchFromPoints( + Omega_h::Mesh& source_mesh, const Omega_h::Reals& target_coords, + const Omega_h::Write& source_cell_ids, Omega_h::Write& supports_ptr, Omega_h::Write& nSupports, Omega_h::Write& support_idx, Omega_h::Write& radii2, bool is_build_csr_call) { - const auto& sourcePoints_coords = source_mesh.coords(); const auto dim = source_mesh.dim(); - - const auto& targetPoints_coords = target_mesh.coords(); - const auto nvertices_target = target_mesh.nverts(); + const auto nvertices_target = nSupports.size(); OMEGA_H_CHECK(radii2.size() == nvertices_target); const auto& vert2vert = source_mesh.ask_star(Omega_h::VERT); @@ -110,84 +197,39 @@ inline void FindSupports::adjBasedSearch( const auto& v2v_data = vert2vert.ab2b; const auto& cells2verts = source_mesh.ask_verts_of(dim); - Kokkos::View target_points("test_points", - nvertices_target); - Omega_h::parallel_for( - nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO i) { - target_points(i, 0) = targetPoints_coords[i * dim]; - target_points(i, 1) = targetPoints_coords[i * dim + 1]; - }); - Kokkos::fence(); - - pcms::GridPointSearch2D search_cell(source_mesh, 10, 10); - auto results = search_cell(target_points); - checkTargetPoints(results); - Omega_h::parallel_for( nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO id) { Queue queue; Track visited; Omega_h::Real cutoffDistance = radii2[id]; + Omega_h::LO source_cell_id = source_cell_ids[id]; - Omega_h::LO source_cell_id = results(id).element_id; - OMEGA_H_CHECK_PRINTF( - source_cell_id >= 0, - "ERROR: Source cell id not found for target %d (%f,%f)\n", id, - target_points(id, 0), target_points(id, 1)); + OMEGA_H_CHECK_PRINTF(source_cell_id >= 0, + "ERROR: Source cell id not found for target %d\n", + id); const Omega_h::LO num_verts_in_dim = dim + 1; Omega_h::LO start_ptr = source_cell_id * num_verts_in_dim; Omega_h::LO end_ptr = start_ptr + num_verts_in_dim; - Omega_h::Real target_coords[max_dim]; - Omega_h::Real support_coords[max_dim]; + Omega_h::Real target_coords_i[max_dim]; + copy_from_rank1_view(target_coords, id, dim, target_coords_i); - for (Omega_h::LO k = 0; k < dim; ++k) { - target_coords[k] = target_points(id, k); - } - - Omega_h::LO start_counter; + Omega_h::LO start_counter = 0; if (!is_build_csr_call) { start_counter = supports_ptr[id]; } - // * Method: - // 1. Get the vertices of the source cell (source cell is the cell in the - // source mesh in which the target point lies): done above - // 2. Using those 3 vertices, get the adjacent vertices of those 3 - // vertices and go on until the queue is empty - // 3. Already visited vertices are stored in visited and the vertices to - // be checked (dist < cutoff) are stored in the queue - // 4. If not CSR building call, store the support vertices in support_idx - // * Method - int count = 0; for (Omega_h::LO i = start_ptr; i < end_ptr; ++i) { Omega_h::LO vert_id = cells2verts[i]; - visited.push_back(vert_id); - - for (Omega_h::LO k = 0; k < dim; ++k) { - support_coords[k] = sourcePoints_coords[vert_id * dim + k]; - } - - Omega_h::Real dist = - calculateDistance(target_coords, support_coords, dim); - if (dist <= cutoffDistance) { - count++; - if (count >= 500) { - printf( - "Warning: count exceeds 500 for target %d with %d supports\n", id, - end_ptr - start_ptr); - printf("Warning: Target %d: coors: (%f, %f) and support %d: " - "coords: (%f, %f)\n", - id, target_coords[0], target_coords[1], vert_id, - support_coords[0], support_coords[1]); - } - queue.push_back(vert_id); - if (!is_build_csr_call) { - Omega_h::LO idx_count = count - 1; - support_idx[start_counter + idx_count] = vert_id; - } + const int count_before = count; + add_support_if_unvisited_and_within_cutoff( + vert_id, cutoffDistance, target_coords_i, sourcePoints_coords, dim, + visited, queue, count, is_build_csr_call, start_counter, support_idx); + if (count > count_before && count >= 500) { + printf("Warning: count exceeds 500 for target %d with %d supports\n", + id, end_ptr - start_ptr); } } @@ -200,50 +242,45 @@ inline void FindSupports::adjBasedSearch( for (Omega_h::LO i = start; i < end; ++i) { auto neighborIndex = v2v_data[i]; - // check if neighbor index is already in the queue to be checked - // TODO refactor this into a function - - if (visited.notVisited(neighborIndex)) { - visited.push_back(neighborIndex); - for (int k = 0; k < dim; ++k) { - support_coords[k] = sourcePoints_coords[neighborIndex * dim + k]; - } - - Omega_h::Real dist = - calculateDistance(target_coords, support_coords, dim); - - if (dist <= cutoffDistance) { - count++; - if (count >= 500) { - printf("Warning: count exceeds 500 for target %d with start %d " - "and end %d radius2 %f adding neighbor %d\n", - id, start, end, cutoffDistance, neighborIndex); - } - queue.push_back(neighborIndex); - if (!is_build_csr_call) { - Omega_h::LO idx_count = count - 1; - support_idx[start_counter + idx_count] = neighborIndex; - } - } + const int count_before = count; + add_support_if_unvisited_and_within_cutoff( + neighborIndex, cutoffDistance, target_coords_i, sourcePoints_coords, + dim, visited, queue, count, is_build_csr_call, start_counter, + support_idx); + if (count > count_before && count >= 500) { + printf("Warning: count exceeds 500 for target %d with start %d " + "and end %d radius2 %f adding neighbor %d\n", + id, start, end, cutoffDistance, neighborIndex); } } - } // end of while loop + } nSupports[id] = count; - }, // lambda + }, "count the number of supports in each target point"); - if (is_build_csr_call == false) { - // printSupportsForTarget(2057, supports_ptr, nSupports, support_idx); - } } -inline void FindSupports::adjBasedSearchCentroidNodes( +void FindSupports::adjBasedSearch(Omega_h::Write& supports_ptr, + Omega_h::Write& nSupports, + Omega_h::Write& support_idx, + Omega_h::Write& radii2, + bool is_build_csr_call) +{ + const auto& targetPoints_coords = target_mesh.coords(); + const auto nvertices_target = target_mesh.nverts(); + auto source_cell_ids = + locate_target_cells(source_mesh, targetPoints_coords, nvertices_target); + adjBasedSearchFromPoints(source_mesh, targetPoints_coords, source_cell_ids, + supports_ptr, nSupports, support_idx, radii2, + is_build_csr_call); +} + +void FindSupports::adjBasedSearchCentroidNodes( Omega_h::Write& supports_ptr, Omega_h::Write& nSupports, Omega_h::Write& support_idx, Omega_h::Write& radii2, bool is_build_csr_call) { - // Mesh Info const auto& mesh_coords = source_mesh.coords(); const auto& nvertices = source_mesh.nverts(); const auto& dim = source_mesh.dim(); @@ -254,8 +291,10 @@ inline void FindSupports::adjBasedSearchCentroidNodes( const auto& faces2nodes = source_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; - auto cell_centroids = getCentroids(source_mesh); - // * Got the adj data and cell centroids + OMEGA_H_CHECK_PRINTF(source_mesh.dim() == 2, + "Only 2D meshes are supported but found %d\n", + source_mesh.dim()); + auto cell_centroids = pcms::get_entity_centroids(source_mesh, Omega_h::FACE); Omega_h::parallel_for( nvertices, @@ -264,15 +303,11 @@ inline void FindSupports::adjBasedSearchCentroidNodes( Track visited; const Omega_h::LO num_verts_in_dim = dim + 1; Omega_h::Real target_coords[max_dim]; - Omega_h::Real support_coords[max_dim]; Omega_h::Real cutoffDistance = radii2[id]; - //? copying the target vertex coordinates - for (Omega_h::LO k = 0; k < dim; ++k) { - target_coords[k] = mesh_coords[id * dim + k]; - } + copy_from_rank1_view(mesh_coords, id, dim, target_coords); - Omega_h::LO start_counter; + Omega_h::LO start_counter = 0; if (!is_build_csr_call) { start_counter = supports_ptr[id]; } @@ -282,25 +317,12 @@ inline void FindSupports::adjBasedSearchCentroidNodes( int count = 0; for (Omega_h::LO i = start_ptr; i < end_ptr; ++i) { Omega_h::LO cell_id = n2f_data[i]; - visited.push_back(cell_id); - - for (Omega_h::LO k = 0; k < dim; ++k) { - support_coords[k] = cell_centroids[cell_id * dim + k]; - } - - Omega_h::Real dist = - calculateDistance(target_coords, support_coords, dim); - if (dist <= cutoffDistance) { - count++; - queue.push_back(cell_id); - if (!is_build_csr_call) { - Omega_h::LO idx_count = count - 1; - support_idx[start_counter + idx_count] = cell_id; - } - } + add_support_if_unvisited_and_within_cutoff( + cell_id, cutoffDistance, target_coords, cell_centroids, dim, visited, + queue, count, is_build_csr_call, start_counter, support_idx); } - while (!queue.isEmpty()) { // ? can queue be empty? + while (!queue.isEmpty()) { Omega_h::LO currentCell = queue.front(); queue.pop_front(); Omega_h::LO start = currentCell * num_verts_in_dim; @@ -317,56 +339,27 @@ inline void FindSupports::adjBasedSearchCentroidNodes( // check if neighbor index is already in the queue to be checked // TODO refactor this into a function - if (visited.notVisited(neighbor_cell_index)) { - visited.push_back(neighbor_cell_index); - for (int k = 0; k < dim; ++k) { - support_coords[k] = - cell_centroids[neighbor_cell_index * dim + k]; - } - - Omega_h::Real dist = - calculateDistance(target_coords, support_coords, dim); - - if (dist <= cutoffDistance) { - count++; - queue.push_back(neighbor_cell_index); - if (!is_build_csr_call) { - Omega_h::LO idx_count = count - 1; - support_idx[start_counter + idx_count] = neighbor_cell_index; - } // end of support_idx check - } // end of distance check - } // end of not visited check - } // end of loop over adj cells to the current vertex - } // end of loop over nodes - - } // end of while loop + add_support_if_unvisited_and_within_cutoff( + neighbor_cell_index, cutoffDistance, target_coords, + cell_centroids, dim, visited, queue, count, is_build_csr_call, + start_counter, support_idx); + } + } + } nSupports[id] = count; - }, // end of lambda + }, "count the number of supports in each target point"); - - if (is_build_csr_call == false) { - // printSupportsForTarget(2057, supports_ptr, nSupports, support_idx); - } } -struct SupportResults +SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, + const Omega_h::Reals& target_coords, + Omega_h::LO nvertices_target, + Omega_h::Real& cutoffDistance, + Omega_h::LO min_req_support, + Omega_h::LO max_allowed_support, + bool adapt_radius) { - Omega_h::LOs supports_ptr; - Omega_h::LOs supports_idx; - Omega_h::Write radii2; -}; - -inline SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, - Omega_h::Mesh& target_mesh, - Omega_h::Real& cutoffDistance, - Omega_h::LO min_req_support = 12, - Omega_h::LO max_allowed_support = 36, - bool adapt_radius = true) -{ - FindSupports search(source_mesh, target_mesh); - Omega_h::LO nvertices_target = target_mesh.nverts(); - Omega_h::Write nSupports( nvertices_target, 0, "number of supports in each target vertex"); pcms::printInfo("INFO: Cut off distance: %f\n", cutoffDistance); @@ -375,11 +368,15 @@ inline SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, Omega_h::Write supports_ptr; Omega_h::Write supports_idx; + auto source_cell_ids = + locate_target_cells(source_mesh, target_coords, nvertices_target); if (!adapt_radius) { pcms::printInfo("INFO: Fixed radius search *(disregarding required minimum " "support)*... \n"); - search.adjBasedSearch(supports_ptr, nSupports, supports_idx, radii2, true); + adjBasedSearchFromPoints(source_mesh, target_coords, source_cell_ids, + supports_ptr, nSupports, supports_idx, radii2, + true); } else { pcms::printInfo("INFO: Adaptive radius search... \n"); int r_adjust_loop = 0; @@ -387,23 +384,13 @@ inline SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, nSupports = Omega_h::Write( nvertices_target, 0, "number of supports in each target vertex"); - Omega_h::Real max_radius = 0.0; - Kokkos::parallel_reduce( - "find max radius", nvertices_target, - OMEGA_H_LAMBDA(const Omega_h::LO i, Omega_h::Real& local_max) { - local_max = (radii2[i] > local_max) ? radii2[i] : local_max; - }, - Kokkos::Max(max_radius)); + const auto max_radius = Omega_h::get_max(Omega_h::read(radii2)); pcms::printInfo("INFO: Loop %d: max_radius: %f\n", r_adjust_loop, max_radius); - // create storage every time to avoid complexity - // FIXME avoid repeated dynamic allocation - Omega_h::Write temp_supports_ptr; - Omega_h::Write temp_supports_idx; - Kokkos::fence(); - search.adjBasedSearch(temp_supports_ptr, nSupports, temp_supports_idx, - radii2, true); + adjBasedSearchFromPoints(source_mesh, target_coords, source_cell_ids, + supports_ptr, nSupports, supports_idx, radii2, + true); Kokkos::fence(); unsigned min_supports_found = 0; @@ -428,36 +415,40 @@ inline SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, r_adjust_loop); } - supports_ptr = Omega_h::Write( - nvertices_target + 1, 0, "number of support source vertices in CSR format"); - Omega_h::LO total_supports = 0; - - Kokkos::parallel_scan( - nvertices_target, - OMEGA_H_LAMBDA(int j, int& update, bool final) { - update += nSupports[j]; - if (final) { - supports_ptr[j + 1] = update; - } - }, - total_supports); + supports_ptr = + build_support_offsets(nvertices_target, nSupports, total_supports); Kokkos::fence(); supports_idx = Omega_h::Write( total_supports, 0, "index of source supports of each target node"); - search.adjBasedSearch(supports_ptr, nSupports, supports_idx, radii2, false); + adjBasedSearchFromPoints(source_mesh, target_coords, source_cell_ids, + supports_ptr, nSupports, supports_idx, radii2, + false); - target_mesh.add_tag(Omega_h::VERT, "radii2", 1, radii2); return SupportResults{read(supports_ptr), read(supports_idx), radii2}; } -inline SupportResults searchNeighbors(Omega_h::Mesh& mesh, - Omega_h::Real cutoffDistance, - Omega_h::LO min_support = 12, - bool adapt_radius = true) +SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, + Omega_h::Mesh& target_mesh, + Omega_h::Real& cutoffDistance, + Omega_h::LO min_req_support, + Omega_h::LO max_allowed_support, + bool adapt_radius) +{ + auto target_coords = target_mesh.coords(); + auto result = searchNeighbors( + source_mesh, target_coords, target_mesh.nverts(), cutoffDistance, + min_req_support, max_allowed_support, adapt_radius); + target_mesh.add_tag(Omega_h::VERT, "radii2", 1, result.radii2); + return result; +} + +SupportResults searchNeighbors(Omega_h::Mesh& mesh, + Omega_h::Real cutoffDistance, + Omega_h::LO min_support, bool adapt_radius) { Omega_h::Write supports_ptr; Omega_h::Write supports_idx; @@ -479,19 +470,12 @@ inline SupportResults searchNeighbors(Omega_h::Mesh& mesh, pcms::printInfo("INFO: Adaptive radius search... \n"); int r_adjust_loop = 0; while (true) { // until the number of minimum support is met - Omega_h::Real max_radius = 0.0; - Kokkos::parallel_reduce( - "find max radius", nvertices_target, - OMEGA_H_LAMBDA(const Omega_h::LO i, Omega_h::Real& local_max) { - local_max = (radii2[i] > local_max) ? radii2[i] : local_max; - }, - Kokkos::Max(max_radius)); + const auto max_radius = Omega_h::get_max(Omega_h::read(radii2)); pcms::printInfo("INFO: Loop %d: max_radius: %f\n", r_adjust_loop, max_radius); nSupports = Omega_h::Write( nvertices_target, 0, "number of supports in each target vertex"); - SupportResults support; // create support every time to avoid complexity search.adjBasedSearchCentroidNodes(supports_ptr, nSupports, supports_idx, radii2, true); @@ -513,25 +497,15 @@ inline SupportResults searchNeighbors(Omega_h::Mesh& mesh, adapt_radii(min_support, 3 * min_support, radii2.size(), radii2, nSupports); - } // while loop + } pcms::printInfo("INFO: Took %d loops to adjust the radius\n", r_adjust_loop); - } // adaptive radius search + } // offset array for the supports of each target vertex - supports_ptr = Omega_h::Write( - nvertices_target + 1, 0, "number of support source vertices in CSR format"); - Omega_h::LO total_supports = 0; - Kokkos::parallel_scan( - nvertices_target, - OMEGA_H_LAMBDA(int j, int& update, bool final) { - update += nSupports[j]; - if (final) { - supports_ptr[j + 1] = update; - } - }, - total_supports); + supports_ptr = + build_support_offsets(nvertices_target, nSupports, total_supports); pcms::printInfo("INFO: Inside searchNeighbors 3\n"); Kokkos::fence(); @@ -547,4 +521,4 @@ inline SupportResults searchNeighbors(Omega_h::Mesh& mesh, return SupportResults{read(supports_ptr), read(supports_idx), radii2}; } -#endif +} // namespace pcms diff --git a/src/pcms/localization/adj_search.hpp b/src/pcms/localization/adj_search.hpp new file mode 100644 index 000000000..c27b11be5 --- /dev/null +++ b/src/pcms/localization/adj_search.hpp @@ -0,0 +1,70 @@ +#ifndef PCMS_LOCALIZATION_ADJ_SEARCH_HPP +#define PCMS_LOCALIZATION_ADJ_SEARCH_HPP + +#include +#include +#include +#include +#include + +#include + +namespace pcms +{ + +static constexpr int max_dim = 3; + +class FindSupports +{ +private: + Omega_h::Mesh& source_mesh; + Omega_h::Mesh& target_mesh; + +public: + FindSupports(Omega_h::Mesh& source_mesh_, Omega_h::Mesh& target_mesh_) + : source_mesh(source_mesh_), target_mesh(target_mesh_){}; + + FindSupports(Omega_h::Mesh& mesh_) : source_mesh(mesh_), target_mesh(mesh_){}; + + void adjBasedSearch(Omega_h::Write& supports_ptr, + Omega_h::Write& nSupports, + Omega_h::Write& support_idx, + Omega_h::Write& radii2, + bool is_build_csr_call); + + void adjBasedSearchCentroidNodes(Omega_h::Write& supports_ptr, + Omega_h::Write& nSupports, + Omega_h::Write& support_idx, + Omega_h::Write& radii2, + bool is_build_csr_call); +}; + +struct SupportResults +{ + Omega_h::LOs supports_ptr; + Omega_h::LOs supports_idx; + Omega_h::Write radii2; +}; + +SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, + const Omega_h::Reals& target_coords, + Omega_h::LO n_targets, + Omega_h::Real& cutoffDistance, + Omega_h::LO min_req_support = 12, + Omega_h::LO max_allowed_support = 36, + bool adapt_radius = true); + +SupportResults searchNeighbors(Omega_h::Mesh& source_mesh, + Omega_h::Mesh& target_mesh, + Omega_h::Real& cutoffDistance, + Omega_h::LO min_req_support = 12, + Omega_h::LO max_allowed_support = 36, + bool adapt_radius = true); + +SupportResults searchNeighbors(Omega_h::Mesh& mesh, + Omega_h::Real cutoffDistance, + Omega_h::LO min_support = 12, + bool adapt_radius = true); +} // namespace pcms + +#endif // PCMS_LOCALIZATION_ADJ_SEARCH_HPP diff --git a/src/pcms/localization/localization_factory.h b/src/pcms/localization/localization_factory.h new file mode 100644 index 000000000..f5671fa69 --- /dev/null +++ b/src/pcms/localization/localization_factory.h @@ -0,0 +1,28 @@ +#ifndef PCMS_LOCALIZATION_LOCALIZATION_FACTORY_H +#define PCMS_LOCALIZATION_LOCALIZATION_FACTORY_H + +#include "pcms/localization/adj_search.hpp" +#include "pcms/field/coordinate_system.h" +#include "pcms/utility/memory_spaces.h" + +namespace pcms +{ + +// Abstract interface for building MLS support structures. +// +// A LocalizationFactory encapsulates the source geometry and search strategy +// (N^2 point-cloud search or mesh-adjacency BFS). Concrete implementations +// are created at FunctionSpace construction time and reused across repeated +// CreatePointEvaluator calls with different target coordinate sets. +class LocalizationFactory +{ +public: + virtual ~LocalizationFactory() = default; + + virtual SupportResults Build( + CoordinateView target_coords) const = 0; +}; + +} // namespace pcms + +#endif // PCMS_LOCALIZATION_LOCALIZATION_FACTORY_H diff --git a/src/pcms/localization/localization_path_selection.h b/src/pcms/localization/localization_path_selection.h new file mode 100644 index 000000000..40bb98940 --- /dev/null +++ b/src/pcms/localization/localization_path_selection.h @@ -0,0 +1,86 @@ +#ifndef PCMS_LOCALIZATION_PATH_SELECTION_H +#define PCMS_LOCALIZATION_PATH_SELECTION_H + +#include "pcms/field/field_layout.h" +#include "pcms/utility/entity_types.h" + +#include + +namespace pcms::detail +{ + +enum class LocalizationPath +{ + PointCloudSupports, + VertexAdjacencySearch, + CentroidToVertexAdjacencySearch +}; + +// Returns the unique entity dimension represented by all DOF holders in the +// layout when its offsets contain exactly one populated entity block and the +// full DOF-holder coordinate set matches that discretization entity count. +// Otherwise, returns std::nullopt. +inline std::optional GetUniformDofHolderEntityDim( + const FieldLayout& layout) +{ + auto disc = layout.GetDiscretization(); + if (disc == nullptr) { + return std::nullopt; + } + + const auto offsets = layout.GetEntOffsets(); + std::optional entity_dim; + for (int dim = 0; dim < ent_offsets_len - 1; ++dim) { + const auto block_size = offsets[dim + 1] - offsets[dim]; + if (block_size > 0) { + if (entity_dim.has_value()) { + return std::nullopt; + } + entity_dim = dim; + } + } + + if (!entity_dim.has_value()) { + return std::nullopt; + } + + if (static_cast(layout.GetDOFHolderCoordinates().GetCoordinates().extent( + 0)) != disc->GetNumEntities(*entity_dim)) { + return std::nullopt; + } + + return entity_dim; +} + +inline LocalizationPath SelectLocalizationPath( + const FieldLayout& source_layout, const FieldLayout* target_layout = nullptr) +{ + auto source_disc = source_layout.GetDiscretization(); + if (source_disc == nullptr) { + return LocalizationPath::PointCloudSupports; + } + + auto source_entity_dim = GetUniformDofHolderEntityDim(source_layout); + if (source_entity_dim.has_value() && *source_entity_dim == Vertex) { + return LocalizationPath::VertexAdjacencySearch; + } + + if (target_layout == nullptr) { + return LocalizationPath::PointCloudSupports; + } + + auto target_disc = target_layout->GetDiscretization(); + auto target_entity_dim = GetUniformDofHolderEntityDim(*target_layout); + if (source_entity_dim.has_value() && *source_entity_dim == Face && + target_entity_dim.has_value() && *target_entity_dim == Vertex && + target_disc != nullptr && source_disc->GetDimension() == 2 && + source_disc->SameEntities(*target_disc)) { + return LocalizationPath::CentroidToVertexAdjacencySearch; + } + + return LocalizationPath::PointCloudSupports; +} + +} // namespace pcms::detail + +#endif // PCMS_LOCALIZATION_PATH_SELECTION_H diff --git a/src/pcms/localization/mesh_localization.cpp b/src/pcms/localization/mesh_localization.cpp new file mode 100644 index 000000000..a73d21bd8 --- /dev/null +++ b/src/pcms/localization/mesh_localization.cpp @@ -0,0 +1,64 @@ +#include "pcms/localization/mesh_localization.h" +#include "pcms/localization/adj_search.hpp" +#include "pcms/localization/mls_support_helpers.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/entity_types.h" +#include "pcms/utility/mesh_geometry.h" +#include "pcms/utility/omega_h_array_utils.h" + +#include + +namespace pcms +{ + +SupportResults AdjacencyLocalizationFactory::Build( + CoordinateView target_coords) const +{ + if (target_coords.GetCoordinateSystem() != CoordinateSystem::Cartesian) { + throw pcms_error( + "AdjacencyLocalizationFactory: only Cartesian coordinates are supported"); + } + + const auto tgt_view = target_coords.GetCoordinates(); + const int dim = source_mesh_.dim(); + PCMS_ALWAYS_ASSERT(static_cast(tgt_view.extent(1)) == dim); + const int n_tgt = static_cast(tgt_view.extent(0)); + + // Pass radius^2 as the cutoff. searchNeighbors stores it in a "radii2" array + // and compares it against squared distances, so the squared value is correct. + Omega_h::Real radius_sq = options_.radius * options_.radius; + + if (source_entity_dim_ == Vertex) { + Omega_h::Reals target_coords_oh = + flatten_to_omega_h_reals(tgt_view, "tgt_coords"); + + return searchNeighbors( + source_mesh_, target_coords_oh, n_tgt, radius_sq, + static_cast(options_.min_req_supports), + static_cast(3 * options_.min_req_supports), + options_.adapt_radius); + } + const Omega_h::Reals src_oh = + get_entity_centroids(source_mesh_, source_entity_dim_); + + Omega_h::Reals target_coords_oh = + flatten_to_omega_h_reals(tgt_view, "tgt_coords"); + + return BuildPointCloudSupports(src_oh, target_coords_oh, dim, options_.radius, + options_.min_req_supports, + options_.adapt_radius); +} + +SupportResults AdjacencyLocalizationFactory::BuildSameMeshCentroidToVertex() + const +{ + PCMS_ALWAYS_ASSERT(source_entity_dim_ == Face); + PCMS_ALWAYS_ASSERT(source_mesh_.dim() == 2); + Omega_h::Real radius_sq = options_.radius * options_.radius; + return searchNeighbors(source_mesh_, radius_sq, + static_cast(options_.min_req_supports), + options_.adapt_radius); +} + +} // namespace pcms diff --git a/src/pcms/localization/mesh_localization.h b/src/pcms/localization/mesh_localization.h new file mode 100644 index 000000000..f685b98dc --- /dev/null +++ b/src/pcms/localization/mesh_localization.h @@ -0,0 +1,44 @@ +#ifndef PCMS_LOCALIZATION_ADJACENCY_LOCALIZATION_H +#define PCMS_LOCALIZATION_ADJACENCY_LOCALIZATION_H + +#include "pcms/localization/localization_factory.h" +#include "pcms/field/evaluator/mls_options.h" + +#include + +namespace pcms +{ + +// LocalizationFactory implementation using mesh-adjacency BFS search. +// +// Stores a non-owning reference to the source mesh. The source mesh must +// outlive this factory. +// +// Build(CoordinateView) dispatch: +// - source_entity_dim == VERT: uses two-mesh searchNeighbors (adjacency BFS). +// - source_entity_dim != VERT: falls back to N^2 BuildPointCloudSupports. +// +class AdjacencyLocalizationFactory : public LocalizationFactory +{ +public: + AdjacencyLocalizationFactory(Omega_h::Mesh& source_mesh, + int source_entity_dim, MLSOptions options) + : source_mesh_(source_mesh), + source_entity_dim_(source_entity_dim), + options_(options) + { + } + + SupportResults Build( + CoordinateView target_coords) const override; + SupportResults BuildSameMeshCentroidToVertex() const; + +private: + Omega_h::Mesh& source_mesh_; + int source_entity_dim_; + MLSOptions options_; +}; + +} // namespace pcms + +#endif // PCMS_LOCALIZATION_ADJACENCY_LOCALIZATION_H diff --git a/src/pcms/localization/mls_support_helpers.cpp b/src/pcms/localization/mls_support_helpers.cpp new file mode 100644 index 000000000..e94d7a01e --- /dev/null +++ b/src/pcms/localization/mls_support_helpers.cpp @@ -0,0 +1,229 @@ +#include "pcms/localization/mls_support_helpers.h" +#include "pcms/localization/adj_search.hpp" +#include "pcms/utility/assert.h" +#include "pcms/utility/print.h" + +#include + +namespace pcms +{ + +// replace with Kokkos::minmax_element when out of experimental +// https://kokkos.org/kokkos-core-wiki/API/algorithms/std-algorithms/all/StdMinMaxElement.html +void minmax(Omega_h::Read num_supports, + unsigned& min_supports_found, unsigned& max_supports_found) +{ + using minMaxReducerType = Kokkos::MinMax; + using minMaxValueType = minMaxReducerType::value_type; + minMaxValueType minmax_val; + Kokkos::parallel_reduce( + num_supports.size(), + KOKKOS_LAMBDA(int i, minMaxValueType& update) { + if (static_cast(num_supports[i]) < update.min_val) + update.min_val = static_cast(num_supports[i]); + if (static_cast(num_supports[i]) > update.max_val) + update.max_val = static_cast(num_supports[i]); + }, + minMaxReducerType(minmax_val)); + Kokkos::fence(); + min_supports_found = minmax_val.min_val; + max_supports_found = minmax_val.max_val; +} + +void adapt_radii(unsigned min_req_supports, unsigned max_allowed_supports, + Omega_h::LO n_targets, Omega_h::Write radii2_l, + Omega_h::Write num_supports) +{ + Omega_h::parallel_for( + "increase radius", n_targets, OMEGA_H_LAMBDA(const int& i) { + Omega_h::LO nsupports = num_supports[i]; + if (nsupports < static_cast(min_req_supports)) { + double factor = + Omega_h::Real(min_req_supports) / Omega_h::Real(nsupports); + OMEGA_H_CHECK_PRINTF(factor > 1.0, + "Factor should be more than 1.0: %f\n", factor); + factor = (nsupports == 0 || factor > 1.5) ? 1.5 : factor; + radii2_l[i] *= factor; + } else if (nsupports > static_cast(max_allowed_supports)) { + double factor = + Omega_h::Real(min_req_supports) / Omega_h::Real(nsupports); + OMEGA_H_CHECK_PRINTF(factor < 1.0, + "Factor should be less than 1.0: %f\n", factor); + factor = (factor < 0.1) ? 0.33 : factor; + radii2_l[i] *= factor; + } + num_supports[i] = 0; + }); + Kokkos::fence(); +} + +// ---- point-cloud N² support search helpers -------------------------------- + +namespace +{ + +KOKKOS_INLINE_FUNCTION +Omega_h::Vector<3> load_point(const Omega_h::Reals& coords, int id, int dim) +{ + Omega_h::Vector<3> p{0, 0, 0}; + for (int d = 0; d < dim; ++d) + p[d] = coords[id * dim + d]; + return p; +} + +struct NSquareCountFunctor +{ + const int dim; + const Omega_h::LO n_sources; + const Omega_h::Reals target_coords; + const Omega_h::Reals source_coords; + const Omega_h::Write radii2; + const Omega_h::Write num_supports; + + KOKKOS_INLINE_FUNCTION + void operator()(const int target_id) const + { + auto tgt = load_point(target_coords, target_id, dim); + auto r2 = radii2[target_id]; + int count = 0; + for (int s = 0; s < n_sources; ++s) { + auto src = load_point(source_coords, s, dim); + double dist2 = 0; + for (int d = 0; d < dim; ++d) { + double diff = tgt[d] - src[d]; + dist2 += diff * diff; + } + if (dist2 <= r2) + ++count; + } + num_supports[target_id] = count; + } +}; + +struct ScanSupportPtrFunctor +{ + Omega_h::Write support_ptr; + Omega_h::Write num_supports; + + KOKKOS_INLINE_FUNCTION + void operator()(const int i, unsigned& update, const bool final) const + { + update += num_supports[i]; + if (final) + support_ptr[i + 1] = update; + } +}; + +struct FillSupportIdxFunctor +{ + const int dim; + const Omega_h::LO n_sources; + const Omega_h::Write support_ptr; + const Omega_h::Write supports_idx; + const Omega_h::Reals target_coords; + const Omega_h::Reals source_coords; + const Omega_h::Write radii2; + + KOKKOS_INLINE_FUNCTION + void operator()(const int target_id) const + { + auto tgt = load_point(target_coords, target_id, dim); + auto r2 = radii2[target_id]; + auto pos = support_ptr[target_id]; + const auto end = support_ptr[target_id + 1]; + for (int s = 0; s < n_sources; ++s) { + auto src = load_point(source_coords, s, dim); + double dist2 = 0; + for (int d = 0; d < dim; ++d) { + double diff = tgt[d] - src[d]; + dist2 += diff * diff; + } + if (dist2 <= r2) { + OMEGA_H_CHECK_PRINTF(pos < end, + "Support index out of bounds: pos %d end %d " + "target_id %d\n", + pos, end, target_id); + supports_idx[pos++] = s; + } + } + } +}; + +} // anonymous namespace + +SupportResults BuildPointCloudSupports(const Omega_h::Reals& source_coords, + const Omega_h::Reals& target_coords, + int dim, double radius, + unsigned min_req_supports, + bool adapt_radius_flag, + unsigned max_iterations) +{ + PCMS_ALWAYS_ASSERT(radius >= 0.0); + const Omega_h::LO n_targets = target_coords.size() / dim; + const Omega_h::LO n_sources = source_coords.size() / dim; + const unsigned max_allowed = 3 * min_req_supports; + const auto radius_sq = static_cast(radius * radius); + + auto radii2 = Omega_h::Write(n_targets, radius_sq); + auto num_supports = Omega_h::Write(n_targets, 0); + + unsigned min_found = 0, max_found = 0; + unsigned loop_count = 0; + + while (!within_number_of_support_range(min_found, max_found, min_req_supports, + max_allowed)) { + Kokkos::parallel_for("n2_count", n_targets, + NSquareCountFunctor{dim, n_sources, target_coords, + source_coords, radii2, + num_supports}); + Kokkos::fence(); + + ++loop_count; + if (!adapt_radius_flag) + break; + if (loop_count > max_iterations) { + pcms::printError( + "BuildPointCloudSupports: radius adjustment did not converge after " + "%d iterations.\n", + max_iterations); + break; + } + + minmax(Omega_h::read(num_supports), min_found, max_found); + + if (!within_number_of_support_range(min_found, max_found, min_req_supports, + max_allowed)) { + pcms::printInfo("BuildPointCloudSupports: adjusting radius iter %d " + "(min=%d max=%d req=%d allowed=%d)\n", + loop_count, min_found, max_found, min_req_supports, + max_allowed); + adapt_radii(min_req_supports, max_allowed, n_targets, radii2, + num_supports); + } + } + + pcms::printInfo( + "BuildPointCloudSupports: support search done after %d iterations " + "(min=%d max=%d)\n", + loop_count, min_found, max_found); + + // Build CSR offset array + auto support_ptr = Omega_h::Write(n_targets + 1, 0); + unsigned total_supports = 0; + Kokkos::parallel_scan("scan_support_ptr", n_targets, + ScanSupportPtrFunctor{support_ptr, num_supports}, + total_supports); + Kokkos::fence(); + + auto supports_idx = Omega_h::Write(total_supports, 0); + Kokkos::parallel_for("fill_support_idx", n_targets, + FillSupportIdxFunctor{dim, n_sources, support_ptr, + supports_idx, target_coords, + source_coords, radii2}); + Kokkos::fence(); + + return SupportResults{Omega_h::LOs(support_ptr), Omega_h::LOs(supports_idx), + radii2}; +} + +} // namespace pcms diff --git a/src/pcms/localization/mls_support_helpers.h b/src/pcms/localization/mls_support_helpers.h new file mode 100644 index 000000000..1410f3df6 --- /dev/null +++ b/src/pcms/localization/mls_support_helpers.h @@ -0,0 +1,43 @@ +#ifndef PCMS_LOCALIZATION_MLS_SUPPORT_HELPERS_H +#define PCMS_LOCALIZATION_MLS_SUPPORT_HELPERS_H + +#include + +namespace pcms +{ + +struct SupportResults; + +// Returns true when both the minimum and maximum support counts fall within +// the requested range. +inline bool within_number_of_support_range(unsigned min_supports_found, + unsigned max_supports_found, + unsigned min_req_supports, + unsigned max_allowed_supports) +{ + return (min_supports_found >= min_req_supports) && + (max_supports_found <= max_allowed_supports); +} + +// Compute the element-wise min and max of a support-count array. +void minmax(Omega_h::Read num_supports, + unsigned& min_supports_found, unsigned& max_supports_found); + +// Scale per-target squared radii to drive support counts toward [min, max]. +void adapt_radii(unsigned min_req_supports, unsigned max_allowed_supports, + Omega_h::LO n_targets, Omega_h::Write radii2_l, + Omega_h::Write num_supports); + +// Build SupportResults for a point-cloud source/target pair using a +// distance-based N² search with optional adaptive radius adjustment. +// source_coords and target_coords are flat arrays in [x0,y0,..., x1,y1,...] +// order. +SupportResults BuildPointCloudSupports(const Omega_h::Reals& source_coords, + const Omega_h::Reals& target_coords, + int dim, double radius, + unsigned min_req_supports, + bool adapt_radius, + unsigned max_iterations = 100); + +} // namespace pcms +#endif // PCMS_LOCALIZATION_MLS_SUPPORT_HELPERS_H diff --git a/src/pcms/localization/point_cloud_localization.h b/src/pcms/localization/point_cloud_localization.h new file mode 100644 index 000000000..5a1d6e811 --- /dev/null +++ b/src/pcms/localization/point_cloud_localization.h @@ -0,0 +1,61 @@ +#ifndef PCMS_LOCALIZATION_POINT_CLOUD_LOCALIZATION_H +#define PCMS_LOCALIZATION_POINT_CLOUD_LOCALIZATION_H + +#include "pcms/localization/localization_factory.h" +#include "pcms/localization/mls_support_helpers.h" +#include "pcms/field/layout/point_cloud.h" +#include "pcms/field/evaluator/mls_options.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/omega_h_array_utils.h" + +#include +#include + +namespace pcms +{ + +// LocalizationFactory implementation using N^2 point-cloud search. +// +// Holds a reference to the PointCloudLayout so source coordinates are not +// copied at construction time; they are extracted from the layout in Build(). +class PointCloudLocalizationFactory : public LocalizationFactory +{ +public: + PointCloudLocalizationFactory(std::shared_ptr layout, + MLSOptions options) + : layout_(std::move(layout)), options_(options) + { + } + + SupportResults Build( + CoordinateView target_coords) const override + { + if (target_coords.GetCoordinateSystem() != CoordinateSystem::Cartesian) { + throw pcms_error( + "PointCloudLocalizationFactory: only Cartesian coordinates are " + "supported"); + } + + const auto src_view = layout_->GetCoordinates(); + const int dim = layout_->GetDimension(); + Omega_h::Reals source_coords = + flatten_to_omega_h_reals(src_view, "src_coords"); + + const auto tgt_view = target_coords.GetCoordinates(); + PCMS_ALWAYS_ASSERT(static_cast(tgt_view.extent(1)) == dim); + Omega_h::Reals target_coords_oh = + flatten_to_omega_h_reals(tgt_view, "tgt_coords"); + + return BuildPointCloudSupports(source_coords, target_coords_oh, dim, + options_.radius, options_.min_req_supports, + options_.adapt_radius); + } + +private: + std::shared_ptr layout_; + MLSOptions options_; +}; + +} // namespace pcms + +#endif // PCMS_LOCALIZATION_POINT_CLOUD_LOCALIZATION_H diff --git a/src/pcms/point_search.cpp b/src/pcms/localization/point_search.cpp similarity index 97% rename from src/pcms/point_search.cpp rename to src/pcms/localization/point_search.cpp index eb1359162..1b2bd45e4 100644 --- a/src/pcms/point_search.cpp +++ b/src/pcms/localization/point_search.cpp @@ -44,10 +44,11 @@ AABBox<2> triangle_bbox(const Omega_h::Matrix<2, 3>& coords) } template -AABBox simplex_bbox(const Omega_h::Matrix& coords) +KOKKOS_INLINE_FUNCTION AABBox simplex_bbox( + const Omega_h::Matrix& coords) { - std::array max; - std::array min; + Kokkos::Array max; + Kokkos::Array min; for (int j = 0; j < dim; ++j) { max[j] = coords(j, 0); min[j] = coords(j, 0); @@ -59,8 +60,8 @@ AABBox simplex_bbox(const Omega_h::Matrix& coords) } } - std::array center; - std::array half_width; + Kokkos::Array center; + Kokkos::Array half_width; for (int j = 0; j < dim; ++j) { center[j] = (max[j] + min[j]) / 2.0; @@ -208,8 +209,7 @@ template * Check if a triangle element represented by 3 coordinates in two dimensions * intersects with a bounding box */ -[[nodiscard]] -KOKKOS_FUNCTION bool triangle_intersects_bbox( +[[nodiscard]] KOKKOS_FUNCTION bool triangle_intersects_bbox( const Omega_h::Matrix<2, 3>& coords, const AABBox<2>& bbox, Real fuzz) { // triangle and grid cell bounding box intersect @@ -234,8 +234,7 @@ KOKKOS_FUNCTION bool triangle_intersects_bbox( } template -[[nodiscard]] -KOKKOS_FUNCTION bool simplex_intersects_bbox( +[[nodiscard]] KOKKOS_FUNCTION bool simplex_intersects_bbox( const Omega_h::Matrix& coords, const AABBox& bbox) { return intersects(simplex_bbox(coords), bbox); @@ -393,7 +392,7 @@ OMEGA_H_INLINE double myreduce(const Omega_h::Vector& x, } Kokkos::View GridPointSearch2D::operator()( - Kokkos::View points) const + Kokkos::View points) const { Kokkos::View results("point search result", points.extent(0)); @@ -543,7 +542,7 @@ GridPointSearch2D::GridPointSearch2D(Omega_h::Mesh& mesh, LO Nx, LO Ny, } Kokkos::View GridPointSearch3D::operator()( - Kokkos::View points) const + Kokkos::View points) const { Kokkos::View results("point search result", points.extent(0)); @@ -572,7 +571,6 @@ Kokkos::View GridPointSearch3D::operator()( auto nearest_triangle = candidates_begin; auto dimensionality = GridPointSearch3D::Result::Dimensionality::EDGE; - Omega_h::Real distance_to_nearest{INFINITY}; Omega_h::Vector parametric_coords_to_nearest; // create array that's size of number of candidates x num coords to store // parametric inversion diff --git a/src/pcms/point_search.h b/src/pcms/localization/point_search.h similarity index 94% rename from src/pcms/point_search.h rename to src/pcms/localization/point_search.h index d68f4432e..cfcc2012d 100644 --- a/src/pcms/point_search.h +++ b/src/pcms/localization/point_search.h @@ -8,8 +8,8 @@ #include #include "pcms/utility/types.h" -#include "pcms/uniform_grid.h" -#include "pcms/bounding_box.h" +#include "pcms/utility/uniform_grid.h" +#include "pcms/utility/bounding_box.h" namespace pcms { @@ -59,7 +59,7 @@ class PointLocalizationSearch } virtual Kokkos::View operator()( - Kokkos::View point) const = 0; + Kokkos::View point) const = 0; virtual ~PointLocalizationSearch() = default; protected: @@ -89,7 +89,7 @@ class GridPointSearch2D : public PointLocalizationSearch2D * closest element */ Kokkos::View operator()( - Kokkos::View point) const override; + Kokkos::View point) const override; private: Omega_h::Mesh mesh_; @@ -124,7 +124,7 @@ class GridPointSearch3D : public PointLocalizationSearch3D * return a negative id of the closest element. */ Kokkos::View operator()( - Kokkos::View point) const override; + Kokkos::View point) const override; private: Omega_h::Mesh mesh_; diff --git a/src/pcms/interpolator/queue_visited.hpp b/src/pcms/localization/queue_visited.hpp similarity index 92% rename from src/pcms/interpolator/queue_visited.hpp rename to src/pcms/localization/queue_visited.hpp index 5a366c8f5..421281c57 100644 --- a/src/pcms/interpolator/queue_visited.hpp +++ b/src/pcms/localization/queue_visited.hpp @@ -1,5 +1,5 @@ -#ifndef QUEUE_VISITED_HPP -#define QUEUE_VISITED_HPP +#ifndef PCMS_LOCALIZATION_QUEUE_VISITED_HPP +#define PCMS_LOCALIZATION_QUEUE_VISITED_HPP #include #include @@ -10,6 +10,9 @@ #define MAX_SIZE_QUEUE 500 #define MAX_SIZE_TRACK 800 +namespace pcms +{ + class Queue { private: @@ -134,4 +137,5 @@ int Track::size() return count; } -#endif +} // namespace pcms +#endif // PCMS_LOCALIZATION_QUEUE_VISITED_HPP diff --git a/src/pcms/pythonapi/CMakeLists.txt b/src/pcms/pythonapi/CMakeLists.txt index 388783a88..3c697bb2f 100644 --- a/src/pcms/pythonapi/CMakeLists.txt +++ b/src/pcms/pythonapi/CMakeLists.txt @@ -6,17 +6,17 @@ pybind11_add_module(pcms pythonapi.cpp bind_field_base.cpp bind_field_layout.cpp - bind_omega_h_field2.cpp + bind_omega_h_field.cpp bind_omega_h_field_layout.cpp bind_uniform_grid_field.cpp bind_uniform_grid_field_layout.cpp bind_omega_h.cpp - bind_transfer_field2.cpp + bind_transfer_field.cpp bind_mls_interpolation.cpp bind_mesh_utilities.cpp ) -set_target_properties(pcms PROPERTIES CXX_STANDARD 17) -target_link_libraries(pcms PRIVATE Kokkos::kokkos pybind11::module MPI::MPI_C pcms::core pcms::interpolator) +target_compile_features(pcms PUBLIC cxx_std_20) +target_link_libraries(pcms PRIVATE Kokkos::kokkos pybind11::module MPI::MPI_C pcms::core pcms::transfer) # if we are building as a python package from the pyproject.toml # scikit-build-core will set this variable @@ -35,12 +35,5 @@ endif() install( TARGETS pcms - EXPORT pcms_python-targets RUNTIME DESTINATION ${PCMS_PYTHON_INSTALL_LOCATION} - LIBRARY DESTINATION ${PCMS_PYTHON_INSTALL_LOCATION} - PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms/pythonapi/) - -install( - EXPORT pcms_python-targets - NAMESPACE pcms:: - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) + LIBRARY DESTINATION ${PCMS_PYTHON_INSTALL_LOCATION}) diff --git a/src/pcms/pythonapi/bind_field_base.cpp b/src/pcms/pythonapi/bind_field_base.cpp index edf1571d9..4854ca808 100644 --- a/src/pcms/pythonapi/bind_field_base.cpp +++ b/src/pcms/pythonapi/bind_field_base.cpp @@ -1,10 +1,21 @@ #include #include #include -#include "pcms/coordinate_system.h" -#include "pcms/coordinate.h" -#include "pcms/create_field.h" -#include "pcms/uniform_grid.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/field/coordinate.h" +#include "pcms/field/field.h" +#include "pcms/field/field_evaluator_factory.h" +#include "pcms/field/field_layout.h" +#include "pcms/field/field_metadata.h" +#include "pcms/field/function_space.h" +#include "pcms/field/data/simple.h" +#include "pcms/field/layout/uniform_grid.h" +#include "pcms/field/uniform_grid_binary_field.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/function_space/polynomial_reconstruction.hpp" +#include "pcms/field/evaluator/mls_options.h" +#include "pcms/utility/uniform_grid.h" +#include "pcms/utility/arrays.h" #include "numpy_array_transform.h" namespace py = pybind11; @@ -12,6 +23,19 @@ namespace py = pybind11; namespace pcms { +namespace +{ + +struct PythonEvaluationRequest +{ + EvaluationRequest request; + py::object owner; + // Keep the device view alive to prevent dangling pointer in mdspan + Kokkos::View device_coords; +}; + +} // namespace + void bind_coordinate_system_module(py::module& m) { // Bind CoordinateSystem enum @@ -32,7 +56,9 @@ void bind_coordinate_system_module(py::module& m) throw std::runtime_error("Coordinates must be a 2D array"); } // Create a view from the numpy array - Rank2View coords_view( + using LayoutPolicy = + detail::default_layout_for_memory_space_t; + Rank2View coords_view( static_cast(buf.ptr), buf.shape[0], buf.shape[1]); return CoordinateView(cs, coords_view); }), @@ -131,17 +157,215 @@ void bind_coordinate_module(py::module& m) void bind_create_field_module(py::module& m) { - // Bind CreateLagrangeLayout function with shared_ptr wrapper - // pybind11 handles shared_ptr better than unique_ptr for Python ownership - m.def( - "create_lagrange_layout", - [](Omega_h::Mesh& mesh, int order, int num_components, - CoordinateSystem coordinate_system) { - return std::shared_ptr( - CreateLagrangeLayout(mesh, order, num_components, coordinate_system)); - }, - py::arg("mesh"), py::arg("order"), py::arg("num_components") = 1, - py::arg("coordinate_system") = CoordinateSystem::Cartesian); + py::class_(m, "EvaluationRequest") + .def_static( + "from_coordinates", + [](py::array_t coords, CoordinateSystem coordinate_system, + OutOfBoundsPolicy policy) { + auto coords_view = numpy_to_view_2d(coords); + // Create a Kokkos::View from the host data and deep copy to device + auto coords_host = Kokkos::View( + "coords_host", coords_view.extent(0), coords_view.extent(1)); + for (size_t i = 0; i < coords_view.extent(0); ++i) { + for (size_t j = 0; j < coords_view.extent(1); ++j) { + coords_host(i, j) = coords_view(i, j); + } + } + auto coords_device = Kokkos::View( + "coords_device", coords_view.extent(0), coords_view.extent(1)); + DeepCopyMismatchLayouts(coords_device, coords_host); + auto coords_device_view = MakeRank2View(coords_device); + return PythonEvaluationRequest{ + EvaluationRequest::FromCoordinates( + CoordinateView(coordinate_system, + coords_device_view), + policy), + py::reinterpret_borrow(coords), + coords_device}; // Keep the View alive! + }, + py::arg("coordinates"), + py::arg("coordinate_system") = CoordinateSystem::Cartesian, + py::arg("policy") = OutOfBoundsPolicy{}, + "Create an EvaluationRequest from an explicit coordinate array.") + .def_static( + "from_function_space", + [](const FunctionSpace& function_space, OutOfBoundsPolicy policy) { + return PythonEvaluationRequest{ + EvaluationRequest::FromFunctionSpace(function_space, policy), + py::none(), + Kokkos::View()}; // Empty view for + // function_space case + }, + py::arg("function_space"), py::arg("policy") = OutOfBoundsPolicy{}, + "Create an EvaluationRequest from a FunctionSpace's DOF-holder sites."); + + // Bind Field: composed per-field object returned by + // FunctionSpace-backed factories' create_field(). Move-only in C++; Python + // holds it by value in a heap-allocated wrapper. + py::class_>(m, "Field") + .def( + "get_dof_holder_data", + [](const Field& self) { + auto data = self.GetDOFHolderDataHost(); + py::array_t result(static_cast(data.size())); + auto buf = result.request(); + Real* ptr = static_cast(buf.ptr); + for (size_t i = 0; i < data.size(); ++i) + ptr[i] = data[i]; + return result; + }, + "Get the DOF holder data as a 1D numpy array") + + .def( + "set_dof_holder_data", + [](Field& self, py::array_t arr) { + auto buf = arr.request(); + if (buf.ndim != 1) { + throw std::runtime_error("DOF holder data must be a 1D array"); + } + Rank1View view( + static_cast(buf.ptr), static_cast(buf.shape[0])); + self.SetDOFHolderDataHost(view); + }, + py::arg("data"), "Set the DOF holder data from a 1D numpy array") + + .def( + "get_num_dof_holders", + [](const Field& self) { + return self.GetLayout().GetNumOwnedDofHolder(); + }, + "Number of owned DOF holders (nodes/elements)") + + .def( + "get_num_components", + [](const Field& self) { + return self.GetLayout().GetNumComponents(); + }, + "Number of field components per DOF holder") + + .def( + "get_dof_holder_coordinates", + [](const Field& self) { + auto cv = self.GetLayout().GetDOFHolderCoordinates(); + auto coords = cv.GetCoordinates(); + Kokkos::View coords_device( + "coords_device", coords.extent(0), coords.extent(1)); + Kokkos::parallel_for( + Kokkos::RangePolicy( + 0, coords.extent(0)), + KOKKOS_LAMBDA(size_t i) { + for (size_t j = 0; j < coords.extent(1); ++j) { + coords_device(i, j) = coords(i, j); + } + }); + Kokkos::View coords_host( + "coords_host", coords.extent(0), coords.extent(1)); + DeepCopyMismatchLayouts(coords_host, coords_device); + py::array_t result( + {static_cast(coords_host.extent(0)), + static_cast(coords_host.extent(1))}); + auto buf = result.request(); + Real* ptr = static_cast(buf.ptr); + for (size_t i = 0; i < coords_host.extent(0); ++i) + for (size_t j = 0; j < coords_host.extent(1); ++j) + ptr[i * coords_host.extent(1) + j] = coords_host(i, j); + return result; + }, + "DOF holder coordinates as a 2D numpy array (num_dof_holders × dim)"); + + py::class_(m, "FunctionSpace") + .def( + "create_field", + [](const FunctionSpace& self) { return self.CreateField(); }, + "Create a Field for this function space.") + .def( + "create_point_evaluator", + [](const FunctionSpace& self, const PythonEvaluationRequest& request) { + return self.CreatePointEvaluator(request.request); + }, + py::arg("request"), + "Create a reusable point evaluator from an EvaluationRequest.") + .def("get_coordinate_system", &FunctionSpace::GetCoordinateSystem, + "Get the coordinate system for this function space"); + + // Bind LagrangeFunctionSpace as a concrete FunctionSpace subtype. + py::class_(m, "LagrangeFunctionSpace") + .def_static( + "from_mesh", + [](Omega_h::Mesh& mesh, int order, int num_components, + CoordinateSystem coordinate_system) { + return LagrangeFunctionSpace::FromMesh(mesh, order, num_components, + coordinate_system); + }, + py::arg("mesh"), py::arg("order"), py::arg("num_components") = 1, + py::arg("coordinate_system") = CoordinateSystem::Cartesian, + "Create a LagrangeFunctionSpace from an Omega_h mesh") + + .def_static( + "from_uniform_grid", + [](const UniformGrid<2>& grid, int num_components, CoordinateSystem cs, + int order) { + return LagrangeFunctionSpace::FromUniformGrid(grid, num_components, cs, + order); + }, + py::arg("grid"), py::arg("num_components") = 1, + py::arg("coordinate_system") = CoordinateSystem::Cartesian, + py::arg("order") = 1, + "Create a LagrangeFunctionSpace from a 2D uniform grid") + + .def_static( + "from_uniform_grid", + [](const UniformGrid<3>& grid, int num_components, CoordinateSystem cs, + int order) { + return LagrangeFunctionSpace::FromUniformGrid(grid, num_components, cs, + order); + }, + py::arg("grid"), py::arg("num_components") = 1, + py::arg("coordinate_system") = CoordinateSystem::Cartesian, + py::arg("order") = 1, + "Create a LagrangeFunctionSpace from a 3D uniform grid"); + + // Bind MLSOptions: configuration struct for + // PolynomialReconstructionFunctionSpace MLS + // evaluation. + py::class_(m, "MLSOptions") + .def(py::init<>(), "Default MLSOptions") + .def_readwrite("radius", &MLSOptions::radius) + .def_readwrite("min_req_supports", &MLSOptions::min_req_supports) + .def_readwrite("degree", &MLSOptions::degree) + .def_readwrite("adapt_radius", &MLSOptions::adapt_radius) + .def_readwrite("lambda_reg", &MLSOptions::lambda) + .def_readwrite("tol", &MLSOptions::tol) + .def_readwrite("decay_factor", &MLSOptions::decay_factor) + .def_readwrite("basis", &MLSOptions::basis); + + // Bind PolynomialReconstructionFunctionSpace as a concrete FunctionSpace + // subtype. + py::class_( + m, "PolynomialReconstructionFunctionSpace") + .def_static( + "from_coords", + [](py::array_t coords, CoordinateSystem cs, MLSOptions opts) { + auto view = numpy_to_view_2d(coords); + return PolynomialReconstructionFunctionSpace::Create(view, cs, opts); + }, + py::arg("coords"), + py::arg("coordinate_system") = CoordinateSystem::Cartesian, + py::arg("options") = MLSOptions{}, + "Create a PolynomialReconstructionFunctionSpace from a 2D array of " + "source coordinates (shape: num_points × dim).") + .def_static( + "from_mesh", + [](Omega_h::Mesh& mesh, int source_entity_dim, CoordinateSystem cs, + MLSOptions opts) { + return PolynomialReconstructionFunctionSpace::FromMesh( + mesh, source_entity_dim, cs, opts); + }, + py::arg("mesh"), py::arg("source_entity_dim"), + py::arg("coordinate_system") = CoordinateSystem::Cartesian, + py::arg("options") = MLSOptions{}, + "Create a PolynomialReconstructionFunctionSpace from mesh entity " + "coordinates."); // Bind CreateUniformGridFromMesh for 2D m.def( @@ -161,121 +385,19 @@ void bind_create_field_module(py::module& m) py::arg("mesh"), py::arg("divisions"), "Create a 3D uniform grid from an Omega_h mesh"); - // Bind CreateUniformGridBinaryField for 2D m.def( "create_uniform_grid_binary_field", [](Omega_h::Mesh& mesh, const std::array& divisions) { auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, divisions); - // Wrap in shared_ptr for proper Python ownership and lifetime management - return py::make_tuple( - std::shared_ptr>(std::move(layout)), - std::shared_ptr>(std::move(field))); + static_cast(layout); + return field; }, py::arg("mesh"), py::arg("divisions"), - "Create a 2D binary field on a uniform grid indicating inside/outside " - "mesh. " - "Returns tuple of (layout, field). Layout lifetime is properly managed via " - "shared_ptr."); + "Create a 2D vertex mask field indicating inside/outside mesh"); } -template -void bind_field_t(py::module& m, const std::string& type_suffix) -{ - std::string class_name = "FieldT_" + type_suffix; - - py::class_, std::shared_ptr>>(m, class_name.c_str()) - .def("get_coordinate_system", &FieldT::GetCoordinateSystem, - "Get the coordinate system of the field") - - .def("get_localization_hint", &FieldT::GetLocalizationHint, - py::arg("coordinates"), - "Get a localization hint for a set of coordinates") - - .def( - "evaluate", - [](const FieldT& self, LocalizationHint hint, - py::array_t results_array, CoordinateSystem coord_sys) { - auto results_view = numpy_to_view(results_array); - FieldDataView results(results_view, coord_sys); - self.Evaluate(hint, results); - }, - py::arg("hint"), py::arg("results"), py::arg("coordinate_system"), - "Evaluate the field at given locations") - - .def( - "evaluate_gradient", - [](FieldT& self, py::array_t results_array, - CoordinateSystem coord_sys) { - auto results_view = numpy_to_view(results_array); - FieldDataView results(results_view, coord_sys); - self.EvaluateGradient(results); - }, - py::arg("results"), py::arg("coordinate_system"), - "Evaluate the gradient of the field") - - .def( - "get_dof_holder_data", - [](const FieldT& self) { - auto data = self.GetDOFHolderData(); - return view_to_numpy(data); - }, - "Get the DOF holder data") - - .def( - "set_dof_holder_data", - [](FieldT& self, py::array_t data) { - auto data_view = numpy_to_view(data); - self.SetDOFHolderData(data_view); - }, - py::arg("data"), "Set the DOF holder data") - - .def("get_layout", &FieldT::GetLayout, - py::return_value_policy::reference, "Get the field layout") - - .def("can_evaluate_gradient", &FieldT::CanEvaluateGradient, - "Check if the field can evaluate gradients") - - .def( - "serialize", - [](const FieldT& self, py::array_t buffer, - py::array_t permutation) { - auto buffer_view = numpy_to_view(buffer); - auto perm_view = numpy_to_view(permutation); - return self.Serialize(buffer_view, perm_view); - }, - py::arg("buffer"), py::arg("permutation"), "Serialize the field data") - - .def( - "deserialize", - [](FieldT& self, py::array_t buffer, - py::array_t permutation) { - auto buffer_view = numpy_to_view(buffer); - auto perm_view = numpy_to_view(permutation); - self.Deserialize(buffer_view, perm_view); - }, - py::arg("buffer"), py::arg("permutation"), - "Deserialize field data from buffer"); -} - -void bind_field_module(py::module& m) -{ - // Bind LocalizationHint (opaque type - data member is internal only) - py::class_(m, "LocalizationHint").def(py::init<>()); - - // Bind FieldDataView for common types - py::class_>(m, "FieldDataView_Real") - .def(py::init, CoordinateSystem>(), - py::arg("values"), py::arg("coordinate_system")) - .def("size", &FieldDataView::Size) - .def("get_coordinate_system", - &FieldDataView::GetCoordinateSystem) - // TODO: const GetValues? - .def("get_values", [](FieldDataView& self) { - return view_to_numpy(self.GetValues()); - }); - - // Bind FieldT only for double (Real is defined as double in types.h) - bind_field_t(m, "Double"); -} +// bind_field_module is kept for compatibility but now registers nothing that +// refers to the deleted FieldT / LocalizationHint / FieldDataView types. +void bind_field_module(py::module& /*m*/) {} -} // namespace pcms \ No newline at end of file +} // namespace pcms diff --git a/src/pcms/pythonapi/bind_field_layout.cpp b/src/pcms/pythonapi/bind_field_layout.cpp index a2277c126..4625a09d5 100644 --- a/src/pcms/pythonapi/bind_field_layout.cpp +++ b/src/pcms/pythonapi/bind_field_layout.cpp @@ -1,7 +1,7 @@ #include #include #include -#include "pcms/field_layout.h" +#include "pcms/field/field_layout.h" #include "numpy_array_transform.h" namespace py = pybind11; @@ -11,88 +11,7 @@ namespace pcms void bind_field_layout_module(py::module& m) { - // Bind the base FieldLayout class as an abstract base - py::class_>(m, "FieldLayout") - .def("create_field", &FieldLayout::CreateFieldReal, - "Create a field with this layout") - - .def("get_num_components", &FieldLayout::GetNumComponents, - "Get the number of components in the field") - - .def("get_num_owned_dof_holder", &FieldLayout::GetNumOwnedDofHolder, - "Get the number of owned DOF holders") - - .def("get_num_global_dof_holder", &FieldLayout::GetNumGlobalDofHolder, - "Get the number of global DOF holders") - - .def("owned_size", &FieldLayout::OwnedSize, - "Get the owned size (num_components * num_owned_dof_holder)") - - .def("global_size", &FieldLayout::GlobalSize, - "Get the global size (num_components * num_global_dof_holder)") - - .def( - "get_owned", - [](const FieldLayout& self) { - auto owned = self.GetOwned(); - // Convert to numpy array - py::array_t result(owned.extent(0)); - auto buf = result.request(); - bool* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < owned.extent(0); ++i) { - ptr[i] = owned[i]; - } - return result; - }, - "Get the owned mask array") - - .def( - "get_gids", - [](const FieldLayout& self) { - auto gids = self.GetGids(); - // Convert to numpy array - py::array_t result(gids.extent(0)); - auto buf = result.request(); - GO* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < gids.extent(0); ++i) { - ptr[i] = gids[i]; - } - return result; - }, - "Get the global IDs array") - - .def("is_distributed", &FieldLayout::IsDistributed, - "Check if the field layout is distributed") - - .def( - "get_ent_offsets", - [](const FieldLayout& self) { - auto offsets = self.GetEntOffsets(); - // Convert std::array to list - py::list result; - for (size_t i = 0; i < offsets.size(); ++i) { - result.append(offsets[i]); - } - return result; - }, - "Get the entity offsets array") - - .def( - "get_dof_holder_coordinates", - [](const FieldLayout& self) { - auto coords = self.GetDOFHolderCoordinates().GetCoordinates(); - // Convert to numpy array (2D) - py::array_t result({coords.extent(0), coords.extent(1)}); - auto buf = result.request(); - Real* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < coords.extent(0); ++i) { - for (size_t j = 0; j < coords.extent(1); ++j) { - ptr[i * coords.extent(1) + j] = coords(i, j); - } - } - return result; - }, - "Get the DOF holder coordinates"); + static_cast(m); } -} // namespace pcms \ No newline at end of file +} // namespace pcms diff --git a/src/pcms/pythonapi/bind_mesh_utilities.cpp b/src/pcms/pythonapi/bind_mesh_utilities.cpp index 6c87895cb..f83af08cc 100644 --- a/src/pcms/pythonapi/bind_mesh_utilities.cpp +++ b/src/pcms/pythonapi/bind_mesh_utilities.cpp @@ -2,8 +2,9 @@ #include #include #include -#include -#include +#include +#include +#include #include "numpy_array_transform.h" namespace py = pybind11; @@ -15,29 +16,7 @@ namespace pcms py::array_t compute_entity_centroids(Omega_h::Mesh& mesh, Omega_h::Int entity_dim) { - const auto dim = mesh.dim(); - const auto nents = mesh.nents(entity_dim); - const auto ent2verts = mesh.ask_down(entity_dim, Omega_h::VERT).ab2b; - const auto coords = mesh.coords(); - Omega_h::Write centroids(dim * nents, 0.0, "entity centroids"); - - if (dim == 2 && entity_dim == 2) { - Kokkos::parallel_for( - "entity_centroids_tri2d", nents, OMEGA_H_LAMBDA(const Omega_h::LO id) { - const auto current_el_verts = Omega_h::gather_verts<3>(ent2verts, id); - const Omega_h::Few, 3> current_el_vert_coords = - Omega_h::gather_vectors<3, 2>(coords, current_el_verts); - auto centroid = Omega_h::average(current_el_vert_coords); - int index = 2 * id; - centroids[index] = centroid[0]; - centroids[index + 1] = centroid[1]; - }); - } else { - throw std::runtime_error( - "Centroid computation not implemented for this dimension combination"); - } - - return omega_h_read_to_numpy(Omega_h::read(centroids)); + return omega_h_read_to_numpy(pcms::get_entity_centroids(mesh, entity_dim)); } // Helper function to determine patch size based on interpolation degree diff --git a/src/pcms/pythonapi/bind_mls_interpolation.cpp b/src/pcms/pythonapi/bind_mls_interpolation.cpp index 11e1ec353..0a99ef3f9 100644 --- a/src/pcms/pythonapi/bind_mls_interpolation.cpp +++ b/src/pcms/pythonapi/bind_mls_interpolation.cpp @@ -1,9 +1,5 @@ #include -#include -#include -#include -#include -#include "numpy_array_transform.h" +#include namespace py = pybind11; @@ -12,79 +8,14 @@ namespace pcms void bind_mls_interpolation_module(py::module& m) { - py::class_(m, "SupportResults") - .def(py::init<>()) - .def(py::init([](py::array_t supports_ptr, - py::array_t supports_idx, - py::array_t radii2) { - auto supports_ptr_read = - numpy_to_omega_h_read(supports_ptr); - auto supports_idx_read = - numpy_to_omega_h_read(supports_idx); - auto radii2_write = numpy_to_omega_h_write(radii2); - return SupportResults{supports_ptr_read, supports_idx_read, - radii2_write}; - }), - py::arg("supports_ptr"), py::arg("supports_idx"), py::arg("radii2")) - .def_property( - "supports_ptr", - [](const SupportResults& self) { - return omega_h_read_to_numpy(self.supports_ptr); - }, - [](SupportResults& self, py::array_t value) { - self.supports_ptr = numpy_to_omega_h_read(value); - }) - .def_property( - "supports_idx", - [](const SupportResults& self) { - return omega_h_read_to_numpy(self.supports_idx); - }, - [](SupportResults& self, py::array_t value) { - self.supports_idx = numpy_to_omega_h_read(value); - }) - .def_property( - "radii2", - [](const SupportResults& self) { - return omega_h_read_to_numpy(Omega_h::read(self.radii2)); - }, - [](SupportResults& self, py::array_t value) { - self.radii2 = numpy_to_omega_h_write(value); - }); - + // RadialBasisFunction is part of the MLSOptions API used by + // PolynomialReconstructionFunctionSpace. py::enum_(m, "RadialBasisFunction") .value("RBF_GAUSSIAN", pcms::RadialBasisFunction::RBF_GAUSSIAN) .value("RBF_C4", pcms::RadialBasisFunction::RBF_C4) .value("RBF_CONST", pcms::RadialBasisFunction::RBF_CONST) .value("NO_OP", pcms::RadialBasisFunction::NO_OP) .export_values(); - - m.def( - "mls_interpolation", - [](py::array_t source_values, - py::array_t source_coordinates, - py::array_t target_coordinates, - const SupportResults& support, Omega_h::LO dim, Omega_h::LO degree, - pcms::RadialBasisFunction bf, double lambda_reg, double tol, - double decay_factor) { - auto source_values_read = - numpy_to_omega_h_read(source_values); - auto source_coordinates_read = - numpy_to_omega_h_read(source_coordinates); - auto target_coordinates_read = - numpy_to_omega_h_read(target_coordinates); - - auto interpolated_values = pcms::mls_interpolation( - source_values_read, source_coordinates_read, target_coordinates_read, - support, dim, degree, bf, lambda_reg, tol, decay_factor); - - return omega_h_read_to_numpy(Omega_h::Reals(interpolated_values)); - }, - py::arg("source_values"), py::arg("source_coordinates"), - py::arg("target_coordinates"), py::arg("support"), py::arg("dim"), - py::arg("degree"), - py::arg("radial_basis_function") = pcms::RadialBasisFunction::NO_OP, - py::arg("lambda_reg") = 0.0, py::arg("tol") = 1e-6, - py::arg("decay_factor") = 5.0); } } // namespace pcms diff --git a/src/pcms/pythonapi/bind_omega_h_field.cpp b/src/pcms/pythonapi/bind_omega_h_field.cpp new file mode 100644 index 000000000..33c0ab633 --- /dev/null +++ b/src/pcms/pythonapi/bind_omega_h_field.cpp @@ -0,0 +1,43 @@ +#include +#include "pcms/utility/arrays.h" +#include "pcms/field/out_of_bounds_policy.h" + +namespace py = pybind11; + +namespace pcms +{ + +void bind_omega_h_field(py::module& m) +{ + // Bind OutOfBoundsMode enum + py::enum_(m, "OutOfBoundsMode") + .value("ERROR", OutOfBoundsMode::ERROR, + "Raise error when points are out of bounds") + .value("FILL", OutOfBoundsMode::FILL, + "Fill with a specified value when points are out of bounds") + .value("NEAREST_BOUNDARY", OutOfBoundsMode::NEAREST_BOUNDARY, + "Clamp to nearest boundary cell (extrapolate)") + .export_values(); + + // Bind OutOfBoundsPolicy struct + py::class_(m, "OutOfBoundsPolicy") + .def(py::init<>(), "Default constructor (mode=ERROR, fill_value=0)") + .def(py::init([](OutOfBoundsMode mode, Real fill_value) { + OutOfBoundsPolicy p; + p.mode = mode; + p.fill_value = fill_value; + return p; + }), + py::arg("mode"), py::arg("fill_value") = Real(0), + "Construct with explicit mode and optional fill value") + .def_readwrite("mode", &OutOfBoundsPolicy::mode, + "Out-of-bounds handling mode") + .def_readwrite("fill_value", &OutOfBoundsPolicy::fill_value, + "Fill value (used only when mode == FILL)"); + + // NOTE: MeshFieldsAdapter2 and FieldT have been removed from + // the C++ API. Python now works with Field objects created from + // FunctionSpace-derived factories such as LagrangeFunctionSpace. +} + +} // namespace pcms diff --git a/src/pcms/pythonapi/bind_omega_h_field2.cpp b/src/pcms/pythonapi/bind_omega_h_field2.cpp deleted file mode 100644 index 1a6a54286..000000000 --- a/src/pcms/pythonapi/bind_omega_h_field2.cpp +++ /dev/null @@ -1,154 +0,0 @@ -#include -#include -#include -#include -#include "pcms/utility/arrays.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter2.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter_layout.h" -#include "numpy_array_transform.h" - -namespace py = pybind11; - -namespace pcms -{ - -void bind_omega_h_field2(py::module& m) -{ - - // Bind OutOfBoundsMode enum - py::enum_(m, "OutOfBoundsMode") - .value("ERROR", OutOfBoundsMode::ERROR, - "Raise error when points are out of bounds") - .value("FILL", OutOfBoundsMode::FILL, - "Fill with a specified value when points are out of bounds") - .value("NEAREST_BOUNDARY", OutOfBoundsMode::NEAREST_BOUNDARY, - "Clamp to nearest boundary cell (extrapolate)") - .export_values(); - - // Bind MeshFieldsAdapter2 class template instantiation for Real type - py::class_, FieldT, - std::shared_ptr>>(m, "MeshFieldsAdapter2") - .def(py::init(), py::arg("layout"), - "Constructor for MeshFieldsAdapter2") - - .def( - "get_localization_hint", - [](const MeshFieldsAdapter2& self, py::array_t coordinates, - const CoordinateSystem& coord_system) { - // Create CoordinateView from numpy array - auto coords_view = numpy_to_view_2d(coordinates); - CoordinateView coord_view(coord_system, coords_view); - return self.GetLocalizationHint(coord_view); - }, - py::arg("coordinates"), py::arg("coordinate_system"), - "Get localization hint for given coordinates") - - .def( - "evaluate", - [](const MeshFieldsAdapter2& self, const LocalizationHint& location, - py::array_t values, const CoordinateSystem& coord_system) { - // Create FieldDataView - auto values_view = numpy_to_view(values); - FieldDataView field_data_view(values_view, - coord_system); - self.Evaluate(location, field_data_view); - }, - py::arg("location"), py::arg("values"), py::arg("coordinate_system"), - "Evaluate field at locations specified by localization hint") - - .def( - "evaluate_gradient", - [](MeshFieldsAdapter2& self, py::array_t gradients, - const CoordinateSystem& coord_system) { - auto gradients_view = numpy_to_view(gradients); - FieldDataView field_data_view(gradients_view, - coord_system); - self.EvaluateGradient(field_data_view); - }, - py::arg("gradients"), py::arg("coordinate_system"), - "Evaluate gradient of the field") - - .def("get_layout", &MeshFieldsAdapter2::GetLayout, - py::return_value_policy::reference, "Get the field layout") - - .def("can_evaluate_gradient", - &MeshFieldsAdapter2::CanEvaluateGradient, - "Check if gradient evaluation is supported") - - .def( - "serialize", - [](const MeshFieldsAdapter2& self, py::array_t buffer, - py::array_t permutation) { - auto buffer_view = numpy_to_view(buffer); - auto perm_view = numpy_to_view(permutation); - return self.Serialize(buffer_view, perm_view); - }, - py::arg("buffer"), py::arg("permutation"), - "Serialize field data into buffer") - - .def( - "deserialize", - [](MeshFieldsAdapter2& self, py::array_t buffer, - py::array_t permutation) { - auto buffer_view = numpy_to_view(buffer); - auto perm_view = numpy_to_view(permutation); - self.Deserialize(buffer_view, perm_view); - }, - py::arg("buffer"), py::arg("permutation"), - "Deserialize field data from buffer") - - .def( - "get_dof_holder_data", - [](const MeshFieldsAdapter2& self) { - auto const_data = self.GetDOFHolderData(); - // Create a numpy array that owns its own data - return view_to_numpy(const_data); - }, - "Get the DOF holder data") - - .def( - "set_dof_holder_data", - [](MeshFieldsAdapter2& self, py::array_t data) { - // Ensure array is contiguous - auto contiguous_data = py::array_t(data); - auto data_view = numpy_to_view(contiguous_data); - // Create const view wrapper - Rank1View const_view( - data_view.data_handle(), data_view.size()); - self.SetDOFHolderData(const_view); - }, - py::arg("data"), "Set the DOF holder data") - - .def("set_out_of_bounds_mode", - &MeshFieldsAdapter2::SetOutOfBoundsMode, py::arg("mode"), - py::arg("fill_value") = 0.0, - "Set the out-of-bounds mode and fill value") - - .def("get_out_of_bounds_mode", - &MeshFieldsAdapter2::GetOutOfBoundsMode, - "Get the current out-of-bounds mode") - - .def("get_fill_value", &MeshFieldsAdapter2::GetFillValue, - "Get the current fill value for out-of-bounds points"); - - // Helper functions for creating views (if needed for testing) - m.def( - "create_coordinate_view", - [](py::array_t coordinates, const CoordinateSystem& coord_system) { - auto coords_view = numpy_to_view_2d(coordinates); - return CoordinateView(coord_system, coords_view); - }, - py::arg("coordinates"), py::arg("coordinate_system"), - "Create a CoordinateView from numpy array"); - - m.def( - "create_field_data_view", - [](py::array_t values, const CoordinateSystem& coord_system) { - auto values_view = numpy_to_view(values); - return FieldDataView(values_view, coord_system); - }, - py::arg("values"), py::arg("coordinate_system"), - "Create a FieldDataView from numpy array"); -} - -} // namespace pcms \ No newline at end of file diff --git a/src/pcms/pythonapi/bind_omega_h_field_layout.cpp b/src/pcms/pythonapi/bind_omega_h_field_layout.cpp index b50fb7e87..9574b4238 100644 --- a/src/pcms/pythonapi/bind_omega_h_field_layout.cpp +++ b/src/pcms/pythonapi/bind_omega_h_field_layout.cpp @@ -1,9 +1,9 @@ #include #include #include -#include "pcms/adapter/meshfields/mesh_fields_adapter_layout.h" -#include "pcms/field.h" -#include "pcms/field_layout.h" +#include "pcms/field/layout/mesh_fields.h" +#include "pcms/field/field.h" +#include "pcms/field/field_layout.h" #include "numpy_array_transform.h" namespace py = pybind11; @@ -13,124 +13,7 @@ namespace pcms void bind_omega_h_field_layout_module(py::module& m) { - // Bind the MeshFieldsAdapterLayout class - py::class_>( - m, "MeshFieldsAdapterLayout") - .def(py::init, int, CoordinateSystem, - std::string>(), - py::arg("mesh"), py::arg("nodes_per_dim"), py::arg("num_components"), - py::arg("coordinate_system"), py::arg("global_id_name") = "global", - "Constructor for MeshFieldsAdapterLayout") - - .def( - "create_field", - [](MeshFieldsAdapterLayout& self) { - return std::shared_ptr>(self.CreateFieldReal()); - }, - "Create a field with this layout") - - .def("get_num_components", &MeshFieldsAdapterLayout::GetNumComponents, - "Get the number of components in the field") - - .def("get_num_owned_dof_holder", - &MeshFieldsAdapterLayout::GetNumOwnedDofHolder, - "Get the number of owned DOF holders") - - .def("get_num_global_dof_holder", - &MeshFieldsAdapterLayout::GetNumGlobalDofHolder, - "Get the number of global DOF holders") - - .def( - "get_owned", - [](const MeshFieldsAdapterLayout& self) { - auto owned = self.GetOwned(); - // Convert to numpy array - py::array_t result(owned.extent(0)); - auto buf = result.request(); - bool* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < owned.extent(0); ++i) { - ptr[i] = owned[i]; - } - return result; - }, - "Get the owned mask array") - - .def( - "get_gids", - [](const MeshFieldsAdapterLayout& self) { - auto gids = self.GetGids(); - // Convert to numpy array - py::array_t result(gids.extent(0)); - auto buf = result.request(); - GO* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < gids.extent(0); ++i) { - ptr[i] = gids[i]; - } - return result; - }, - "Get the global IDs array") - - .def( - "get_dof_holder_coordinates", - [](const MeshFieldsAdapterLayout& self) { - auto coords = self.GetDOFHolderCoordinates().GetCoordinates(); - // Convert to numpy array (2D) - py::array_t result({coords.extent(0), coords.extent(1)}); - auto buf = result.request(); - Real* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < coords.extent(0); ++i) { - for (size_t j = 0; j < coords.extent(1); ++j) { - ptr[i * coords.extent(1) + j] = coords(i, j); - } - } - return result; - }, - "Get the DOF holder coordinates") - - .def("is_distributed", &MeshFieldsAdapterLayout::IsDistributed, - "Check if the field layout is distributed") - - .def( - "get_ent_offsets", - [](const MeshFieldsAdapterLayout& self) { - auto offsets = self.GetEntOffsets(); - // Convert std::array to list/tuple - py::list result; - for (size_t i = 0; i < offsets.size(); ++i) { - result.append(offsets[i]); - } - return result; - }, - "Get the entity offsets array") - - .def( - "get_nodes_per_dim", - [](const MeshFieldsAdapterLayout& self) { - auto nodes = self.GetNodesPerDim(); - py::list result; - for (size_t i = 0; i < nodes.size(); ++i) { - result.append(nodes[i]); - } - return result; - }, - "Get the nodes per dimension array") - - .def("get_num_ents", &MeshFieldsAdapterLayout::GetNumEnts, - "Get the total number of entities") - - .def( - "get_mesh", - [](MeshFieldsAdapterLayout& self) -> Omega_h::Mesh& { - return self.GetMesh(); - }, - py::return_value_policy::reference, "Get the underlying Omega_h mesh") - - .def("owned_size", &MeshFieldsAdapterLayout::OwnedSize, - "Get the owned size (num_components * num_owned_dof_holder)") - - .def("global_size", &MeshFieldsAdapterLayout::GlobalSize, - "Get the global size (num_components * num_global_dof_holder)"); + static_cast(m); } -} // namespace pcms \ No newline at end of file +} // namespace pcms diff --git a/src/pcms/pythonapi/bind_transfer_field.cpp b/src/pcms/pythonapi/bind_transfer_field.cpp new file mode 100644 index 000000000..599b31f3b --- /dev/null +++ b/src/pcms/pythonapi/bind_transfer_field.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include "pcms/transfer/copy.h" +#include "pcms/field/field.h" +#include "pcms/field/function_space.h" +#include "pcms/field/point_evaluator.h" +#include "../transfer/interpolator.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "numpy_array_transform.h" +#include "pcms/utility/types.h" + +namespace py = pybind11; + +namespace pcms +{ + +void bind_transfer_field_module(py::module& m) +{ + py::class_, std::unique_ptr>>( + m, "PointEvaluator") + .def( + "evaluate", + [](const PointEvaluator& self, const Field& field, + py::array_t output) { + auto output_view = numpy_to_kokkos_view_2d(output); + auto output_device = Kokkos::View( + "output_device", output_view.extent(0), output_view.extent(1)); + DeepCopyMismatchLayouts(output_device, output_view); + auto output_rank2 = MakeRank2View(output_device); + self.Evaluate(field, output_rank2); + DeepCopyMismatchLayouts(output_view, output_device); + }, + py::arg("field"), py::arg("output"), + "Evaluate the given field at the cached query coordinates into a " + "preallocated 2D numpy array of shape " + "(num_query_points, num_components)."); + + // Bind Interpolator: construct once per source×target function-space + // pair (localization happens at construction), then call apply() repeatedly + // for different field states at zero additional localization cost. + py::class_>(m, "Interpolator") + .def( + py::init([](const FunctionSpace& source_space, + const FunctionSpace& target_space, OutOfBoundsPolicy policy) { + return Interpolator(source_space, target_space, policy); + }), + py::arg("source_space"), py::arg("target_space"), + py::arg("policy") = OutOfBoundsPolicy{}, + "Construct an interpolator. Localization is performed here and cached. " + "Call apply() repeatedly without re-localizing.") + .def( + "apply", + [](const Interpolator& self, const Field& source, + Field& target) { self.Apply(source, target); }, + py::arg("source"), py::arg("target"), + "Interpolate source field to target DOF locations (cheap; reuses cached " + "localization)."); + + py::class_>(m, "Copy") + .def(py::init([](const FunctionSpace& source_space, + const FunctionSpace& target_space) { + return Copy(source_space, target_space); + }), + py::arg("source_space"), py::arg("target_space"), + "Construct a copy operator for compatible function spaces.") + .def( + "apply", + [](const Copy& self, const Field& source, + Field& target) { self.Apply(source, target); }, + py::arg("source"), py::arg("target"), + "Copy source field data to target field (same layout required)."); +} + +} // namespace pcms diff --git a/src/pcms/pythonapi/bind_transfer_field2.cpp b/src/pcms/pythonapi/bind_transfer_field2.cpp deleted file mode 100644 index 243c96276..000000000 --- a/src/pcms/pythonapi/bind_transfer_field2.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include -#include -#include "pcms/transfer_field2.h" -#include "pcms/field.h" - -namespace py = pybind11; - -namespace pcms -{ - -void bind_transfer_field2_module(py::module& m) -{ - // Only bind for double since that's the only FieldT type exposed to Python - // (FieldT and FieldT are not bound in bind_field_base.cpp) - - m.def( - "copy_field", - [](const FieldT& source, FieldT& target) { - copy_field2(source, target); - }, - py::arg("source"), py::arg("target"), - "Copy field data from source to target. Source and target must be of the " - "same type."); - - m.def( - "interpolate_field", - [](const FieldT& source, FieldT& target) { - interpolate_field2(source, target); - }, - py::arg("source"), py::arg("target"), - "Interpolate field from source to target. Coordinate systems must match."); -} - -} // namespace pcms \ No newline at end of file diff --git a/src/pcms/pythonapi/bind_uniform_grid_field.cpp b/src/pcms/pythonapi/bind_uniform_grid_field.cpp index c69409859..bc211edb0 100644 --- a/src/pcms/pythonapi/bind_uniform_grid_field.cpp +++ b/src/pcms/pythonapi/bind_uniform_grid_field.cpp @@ -1,258 +1,19 @@ #include -#include -#include -#include -#include "pcms/utility/arrays.h" -#include "pcms/adapter/uniform_grid/uniform_grid_field.h" -#include "pcms/adapter/uniform_grid/uniform_grid_field_layout.h" -#include "numpy_array_transform.h" namespace py = pybind11; namespace pcms { -void bind_uniform_grid_field_module(py::module& m) -{ - - // Bind UniformGridField class for 2D - py::class_, FieldT, - std::shared_ptr>>(m, "UniformGridField2D") - .def(py::init&>(), py::arg("layout"), - "Constructor for UniformGridField2D") - - .def( - "get_localization_hint", - [](const UniformGridField<2>& self, py::array_t coordinates, - const CoordinateSystem& coord_system) { - // Create CoordinateView from numpy array - auto coords_view = numpy_to_view_2d(coordinates); - CoordinateView coord_view(coord_system, coords_view); - return self.GetLocalizationHint(coord_view); - }, - py::arg("coordinates"), py::arg("coordinate_system"), - "Get localization hint for given coordinates") - - .def( - "evaluate", - [](const UniformGridField<2>& self, const LocalizationHint& location, - py::array_t values, const CoordinateSystem& coord_system) { - // Create FieldDataView - auto values_view = numpy_to_view(values); - FieldDataView field_data_view(values_view, - coord_system); - self.Evaluate(location, field_data_view); - }, - py::arg("location"), py::arg("values"), py::arg("coordinate_system"), - "Evaluate field at locations specified by localization hint") - - .def( - "evaluate_gradient", - [](UniformGridField<2>& self, py::array_t gradients, - const CoordinateSystem& coord_system) { - auto gradients_view = numpy_to_view(gradients); - FieldDataView field_data_view(gradients_view, - coord_system); - self.EvaluateGradient(field_data_view); - }, - py::arg("gradients"), py::arg("coordinate_system"), - "Evaluate gradient of the field") - - .def("get_layout", &UniformGridField<2>::GetLayout, - py::return_value_policy::reference, "Get the field layout") - - .def("can_evaluate_gradient", &UniformGridField<2>::CanEvaluateGradient, - "Check if gradient evaluation is supported") - - .def( - "serialize", - [](const UniformGridField<2>& self, py::array_t buffer, - py::array_t permutation) { - auto buffer_view = numpy_to_view(buffer); - auto perm_view = numpy_to_view(permutation); - return self.Serialize(buffer_view, perm_view); - }, - py::arg("buffer"), py::arg("permutation"), - "Serialize field data into buffer") - - .def( - "deserialize", - [](UniformGridField<2>& self, py::array_t buffer, - py::array_t permutation) { - auto buffer_view = numpy_to_view(buffer); - auto perm_view = numpy_to_view(permutation); - self.Deserialize(buffer_view, perm_view); - }, - py::arg("buffer"), py::arg("permutation"), - "Deserialize field data from buffer") - - .def( - "get_dof_holder_data", - [](const UniformGridField<2>& self) { - auto const_data = self.GetDOFHolderData(); - // Create a numpy array that owns its own data - return view_to_numpy(const_data); - }, - "Get the DOF holder data") - - .def( - "set_dof_holder_data", - [](UniformGridField<2>& self, py::array_t data) { - // Ensure array is contiguous - auto contiguous_data = py::array_t(data); - auto data_view = numpy_to_view(contiguous_data); - // Create const view wrapper - Rank1View const_view( - data_view.data_handle(), data_view.size()); - self.SetDOFHolderData(const_view); - }, - py::arg("data"), "Set the DOF holder data") - - .def( - "to_numpy", - [](const UniformGridField<2>& self) { - return mdspan_view_to_numpy(self.to_mdspan()); - }, - "Get the DOF holder data as a 2D numpy array (copy)") - - .def("set_out_of_bounds_mode", &UniformGridField<2>::SetOutOfBoundsMode, - py::arg("mode"), py::arg("fill_value") = 0.0, - "Set the out-of-bounds mode and fill value") - - .def("get_out_of_bounds_mode", &UniformGridField<2>::GetOutOfBoundsMode, - "Get the current out-of-bounds mode") - - .def("get_fill_value", &UniformGridField<2>::GetFillValue, - "Get the current fill value for out-of-bounds points"); - - // Bind UniformGridField class for 3D - py::class_, FieldT, - std::shared_ptr>>(m, "UniformGridField3D") - .def(py::init&>(), py::arg("layout"), - "Constructor for UniformGridField3D") - - .def( - "get_localization_hint", - [](const UniformGridField<3>& self, py::array_t coordinates, - const CoordinateSystem& coord_system) { - // Create CoordinateView from numpy array - auto coords_view = numpy_to_view_2d(coordinates); - CoordinateView coord_view(coord_system, coords_view); - return self.GetLocalizationHint(coord_view); - }, - py::arg("coordinates"), py::arg("coordinate_system"), - "Get localization hint for given coordinates") - - .def( - "evaluate", - [](const UniformGridField<3>& self, const LocalizationHint& location, - py::array_t values, const CoordinateSystem& coord_system) { - // Create FieldDataView - auto values_view = numpy_to_view(values); - FieldDataView field_data_view(values_view, - coord_system); - self.Evaluate(location, field_data_view); - }, - py::arg("location"), py::arg("values"), py::arg("coordinate_system"), - "Evaluate field at locations specified by localization hint") - - .def( - "evaluate_gradient", - [](UniformGridField<3>& self, py::array_t gradients, - const CoordinateSystem& coord_system) { - auto gradients_view = numpy_to_view(gradients); - FieldDataView field_data_view(gradients_view, - coord_system); - self.EvaluateGradient(field_data_view); - }, - py::arg("gradients"), py::arg("coordinate_system"), - "Evaluate gradient of the field") - - .def("get_layout", &UniformGridField<3>::GetLayout, - py::return_value_policy::reference, "Get the field layout") - - .def("can_evaluate_gradient", &UniformGridField<3>::CanEvaluateGradient, - "Check if gradient evaluation is supported") - - .def( - "serialize", - [](const UniformGridField<3>& self, py::array_t buffer, - py::array_t permutation) { - auto buffer_view = numpy_to_view(buffer); - auto perm_view = numpy_to_view(permutation); - return self.Serialize(buffer_view, perm_view); - }, - py::arg("buffer"), py::arg("permutation"), - "Serialize field data into buffer") - - .def( - "deserialize", - [](UniformGridField<3>& self, py::array_t buffer, - py::array_t permutation) { - auto buffer_view = numpy_to_view(buffer); - auto perm_view = numpy_to_view(permutation); - self.Deserialize(buffer_view, perm_view); - }, - py::arg("buffer"), py::arg("permutation"), - "Deserialize field data from buffer") - - .def( - "get_dof_holder_data", - [](const UniformGridField<3>& self) { - auto const_data = self.GetDOFHolderData(); - // Create a numpy array that owns its own data - return view_to_numpy(const_data); - }, - "Get the DOF holder data") - - .def( - "set_dof_holder_data", - [](UniformGridField<3>& self, py::array_t data) { - // Ensure array is contiguous - auto contiguous_data = py::array_t(data); - auto data_view = numpy_to_view(contiguous_data); - // Create const view wrapper - Rank1View const_view( - data_view.data_handle(), data_view.size()); - self.SetDOFHolderData(const_view); - }, - py::arg("data"), "Set the DOF holder data") - - .def( - "to_numpy", - [](const UniformGridField<3>& self) { - return mdspan_view_to_numpy(self.to_mdspan()); - }, - "Get the DOF holder data as a 3D numpy array (copy)") - - .def("set_out_of_bounds_mode", &UniformGridField<3>::SetOutOfBoundsMode, - py::arg("mode"), py::arg("fill_value") = 0.0, - "Set the out-of-bounds mode and fill value") - - .def("get_out_of_bounds_mode", &UniformGridField<3>::GetOutOfBoundsMode, - "Get the current out-of-bounds mode") - - .def("get_fill_value", &UniformGridField<3>::GetFillValue, - "Get the current fill value for out-of-bounds points"); - - // Helper functions for creating views (if needed for testing) - m.def( - "create_coordinate_view", - [](py::array_t coordinates, const CoordinateSystem& coord_system) { - auto coords_view = numpy_to_view_2d(coordinates); - return CoordinateView(coord_system, coords_view); - }, - py::arg("coordinates"), py::arg("coordinate_system"), - "Create a CoordinateView from numpy array"); - - m.def( - "create_field_data_view", - [](py::array_t values, const CoordinateSystem& coord_system) { - auto values_view = numpy_to_view(values); - return FieldDataView(values_view, coord_system); - }, - py::arg("values"), py::arg("coordinate_system"), - "Create a FieldDataView from numpy array"); -} +// UniformGridField (the old monolithic field type that combined layout, +// data storage, and evaluation) has been removed. Uniform-grid functionality +// is now available via: +// - UniformGridFieldLayout (layout, bound in +// bind_uniform_grid_field_layout.cpp) +// - LagrangeFunctionSpace::from_uniform_grid (a concrete FunctionSpace, +// bound in bind_field_base.cpp) +// - Field (bound in bind_field_base.cpp) +// This stub is kept so that the build system does not need to be modified. +void bind_uniform_grid_field_module(py::module& /*m*/) {} } // namespace pcms diff --git a/src/pcms/pythonapi/bind_uniform_grid_field_layout.cpp b/src/pcms/pythonapi/bind_uniform_grid_field_layout.cpp index f98f75112..96d0286ab 100644 --- a/src/pcms/pythonapi/bind_uniform_grid_field_layout.cpp +++ b/src/pcms/pythonapi/bind_uniform_grid_field_layout.cpp @@ -1,11 +1,8 @@ #include #include #include -#include "pcms/adapter/uniform_grid/uniform_grid_field_layout.h" -#include "pcms/field.h" -#include "pcms/field_layout.h" -#include "pcms/uniform_grid.h" -#include "numpy_array_transform.h" +#include "pcms/field/layout/uniform_grid.h" +#include "pcms/utility/uniform_grid.h" namespace py = pybind11; @@ -14,7 +11,9 @@ namespace pcms void bind_uniform_grid_field_layout_module(py::module& m) { - // Bind UniformGrid structure for 2D + // Bind UniformGrid setup types. The corresponding layout types remain + // internal to the Python API; users construct function spaces from grids + // and work with Field objects. py::class_>(m, "UniformGrid2D") .def(py::init<>()) .def_readwrite("edge_length", &UniformGrid<2>::edge_length, @@ -67,176 +66,6 @@ void bind_uniform_grid_field_layout_module(py::module& m) "Get the cell ID that contains or is closest to the given point") .def("get_cell_bbox", &UniformGrid<3>::GetCellBBOX, py::arg("cell_index"), "Get the bounding box of a cell"); - - // Bind the UniformGridFieldLayout class for 2D - py::class_, FieldLayout, - std::shared_ptr>>( - m, "UniformGridFieldLayout2D") - .def(py::init&, int, CoordinateSystem>(), py::arg("grid"), - py::arg("num_components"), py::arg("coordinate_system"), - "Constructor for UniformGridFieldLayout2D") - - .def( - "create_field", - [](UniformGridFieldLayout<2>& self) { - return std::shared_ptr>(self.CreateFieldReal()); - }, - "Create a field with this layout") - - .def("get_num_components", &UniformGridFieldLayout<2>::GetNumComponents, - "Get the number of components in the field") - - .def("get_num_owned_dof_holder", - &UniformGridFieldLayout<2>::GetNumOwnedDofHolder, - "Get the number of owned DOF holders") - - .def("get_num_global_dof_holder", - &UniformGridFieldLayout<2>::GetNumGlobalDofHolder, - "Get the number of global DOF holders") - - .def( - "get_owned", - [](const UniformGridFieldLayout<2>& self) { - auto owned = self.GetOwned(); - // Convert to numpy array - py::array_t result(owned.extent(0)); - auto buf = result.request(); - bool* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < owned.extent(0); ++i) { - ptr[i] = owned[i]; - } - return result; - }, - "Get the owned mask array") - - .def( - "get_gids", - [](const UniformGridFieldLayout<2>& self) { - auto gids = self.GetGids(); - // Convert to numpy array - py::array_t result(gids.extent(0)); - auto buf = result.request(); - GO* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < gids.extent(0); ++i) { - ptr[i] = gids[i]; - } - return result; - }, - "Get the global IDs array") - - .def( - "get_dof_holder_coordinates", - [](const UniformGridFieldLayout<2>& self) { - auto coords = self.GetDOFHolderCoordinates().GetCoordinates(); - // Convert to numpy array (2D) - py::array_t result({coords.extent(0), coords.extent(1)}); - auto buf = result.request(); - Real* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < coords.extent(0); ++i) { - for (size_t j = 0; j < coords.extent(1); ++j) { - ptr[i * coords.extent(1) + j] = coords(i, j); - } - } - return result; - }, - "Get the DOF holder coordinates") - - .def("is_distributed", &UniformGridFieldLayout<2>::IsDistributed, - "Check if the field layout is distributed") - - .def("get_grid", &UniformGridFieldLayout<2>::GetGrid, - py::return_value_policy::reference, "Get the underlying uniform grid") - - .def("get_num_cells", &UniformGridFieldLayout<2>::GetNumCells, - "Get the number of cells in the grid") - - .def("get_num_vertices", &UniformGridFieldLayout<2>::GetNumVertices, - "Get the number of vertices in the grid"); - - // Bind the UniformGridFieldLayout class for 3D - py::class_, FieldLayout, - std::shared_ptr>>( - m, "UniformGridFieldLayout3D") - .def(py::init&, int, CoordinateSystem>(), py::arg("grid"), - py::arg("num_components"), py::arg("coordinate_system"), - "Constructor for UniformGridFieldLayout3D") - - .def( - "create_field", - [](UniformGridFieldLayout<3>& self) { - return std::shared_ptr>(self.CreateFieldReal()); - }, - "Create a field with this layout") - - .def("get_num_components", &UniformGridFieldLayout<3>::GetNumComponents, - "Get the number of components in the field") - - .def("get_num_owned_dof_holder", - &UniformGridFieldLayout<3>::GetNumOwnedDofHolder, - "Get the number of owned DOF holders") - - .def("get_num_global_dof_holder", - &UniformGridFieldLayout<3>::GetNumGlobalDofHolder, - "Get the number of global DOF holders") - - .def( - "get_owned", - [](const UniformGridFieldLayout<3>& self) { - auto owned = self.GetOwned(); - // Convert to numpy array - py::array_t result(owned.extent(0)); - auto buf = result.request(); - bool* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < owned.extent(0); ++i) { - ptr[i] = owned[i]; - } - return result; - }, - "Get the owned mask array") - - .def( - "get_gids", - [](const UniformGridFieldLayout<3>& self) { - auto gids = self.GetGids(); - // Convert to numpy array - py::array_t result(gids.extent(0)); - auto buf = result.request(); - GO* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < gids.extent(0); ++i) { - ptr[i] = gids[i]; - } - return result; - }, - "Get the global IDs array") - - .def( - "get_dof_holder_coordinates", - [](const UniformGridFieldLayout<3>& self) { - auto coords = self.GetDOFHolderCoordinates().GetCoordinates(); - // Convert to numpy array (2D) - py::array_t result({coords.extent(0), coords.extent(1)}); - auto buf = result.request(); - Real* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < coords.extent(0); ++i) { - for (size_t j = 0; j < coords.extent(1); ++j) { - ptr[i * coords.extent(1) + j] = coords(i, j); - } - } - return result; - }, - "Get the DOF holder coordinates") - - .def("is_distributed", &UniformGridFieldLayout<3>::IsDistributed, - "Check if the field layout is distributed") - - .def("get_grid", &UniformGridFieldLayout<3>::GetGrid, - py::return_value_policy::reference, "Get the underlying uniform grid") - - .def("get_num_cells", &UniformGridFieldLayout<3>::GetNumCells, - "Get the number of cells in the grid") - - .def("get_num_vertices", &UniformGridFieldLayout<3>::GetNumVertices, - "Get the number of vertices in the grid"); } } // namespace pcms diff --git a/src/pcms/pythonapi/pythonapi.cpp b/src/pcms/pythonapi/pythonapi.cpp index bd9e0b906..5f943518b 100644 --- a/src/pcms/pythonapi/pythonapi.cpp +++ b/src/pcms/pythonapi/pythonapi.cpp @@ -6,9 +6,9 @@ namespace py = pybind11; namespace pcms { -void bind_omega_h_field2(py::module& m); +void bind_omega_h_field(py::module& m); -void bind_transfer_field2_module(py::module& m); +void bind_transfer_field_module(py::module& m); void bind_omega_h_mesh_module(py::module& m); @@ -41,22 +41,27 @@ PYBIND11_MODULE(pcms, m) // Bind mesh and field infrastructure pcms::bind_omega_h_mesh_module(m); - pcms::bind_field_layout_module(m); + // bind_field_module is a no-op stub — + // FieldT/LocalizationHint/FieldDataView have been removed from the C++ + // API. pcms::bind_field_module(m); - pcms::bind_omega_h_field_layout_module(m); pcms::bind_uniform_grid_field_layout_module(m); + // Bind OutOfBoundsPolicy before FunctionSpace so the default argument in + // create_point_evaluator is a registered Python type at binding time. + pcms::bind_omega_h_field(m); + // bind_create_field_module registers FunctionSpace, + // LagrangeFunctionSpace, and Field. pcms::bind_create_field_module(m); - - // Bind field types - pcms::bind_omega_h_field2(m); + // bind_uniform_grid_field_module is a no-op stub — UniformGridField has + // been removed; use LagrangeFunctionSpace::from_uniform_grid instead. pcms::bind_uniform_grid_field_module(m); - // Bind field operations - pcms::bind_transfer_field2_module(m); + // Bind field operations such as Interpolator and Copy. + pcms::bind_transfer_field_module(m); // Bind interpolator operations pcms::bind_mls_interpolation_module(m); // Bind mesh utility functions pcms::bind_mesh_utilities_module(m); -} \ No newline at end of file +} diff --git a/src/pcms/pythonapi/test_field_copy.py b/src/pcms/pythonapi/test_field_copy.py index 3d569e9b2..077140691 100644 --- a/src/pcms/pythonapi/test_field_copy.py +++ b/src/pcms/pythonapi/test_field_copy.py @@ -2,7 +2,7 @@ import numpy as np def test_copy(world, dim, order, num_components): - """Test copying omega_h_field2 data""" + """Test copying omega_h field data.""" nx = 100 ny = 100 if dim > 1 else 0 nz = 100 if dim > 2 else 0 @@ -20,34 +20,29 @@ def test_copy(world, dim, order, num_components): print(f" Mesh type: {type(mesh)}") print(f" Mesh object: {mesh}") - # Create layout - print(" About to create layout...") - layout = pcms.create_lagrange_layout( - mesh, - order, - num_components, - pcms.CoordinateSystem.Cartesian + # Create factory and layout + print(" About to create factory...") + factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components, pcms.CoordinateSystem.Cartesian ) - print(f" Layout created successfully") print(f"Testing dim={dim}, order={order}, num_components={num_components}...") - # Get number of data points - ndata = layout.get_num_owned_dof_holder() * num_components + # Create original field and set data + original = factory.create_field() + print(" Created original field") + ndata = original.get_num_dof_holders() * original.get_num_components() print(f" Number of data points: {ndata}") # Create sequential array of IDs ids = np.arange(ndata, dtype=np.float64) print(f" Created array of IDs from 0 to {ndata-1}") - - # Create original field and set data - original = layout.create_field() - print(" Created original field") original.set_dof_holder_data(ids) print(" Set data in original field") # Create copied field and copy data - copied = layout.create_field() - pcms.copy_field(original, copied) + copied = factory.create_field() + copier = pcms.Copy(factory, factory) + copier.apply(original, copied) print(" Copied data to new field") # Get copied data @@ -66,7 +61,7 @@ def test_copy(world, dim, order, num_components): def main(): """Run all test cases""" - print("Testing copy omega_h_field2 data...") + print("Testing copy omega_h field data...") # Initialize Omega_h library lib = pcms.OmegaHLibrary() @@ -84,4 +79,4 @@ def main(): del lib if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/pcms/pythonapi/test_file_io.py b/src/pcms/pythonapi/test_file_io.py index 6aa8c8366..7fdde684d 100644 --- a/src/pcms/pythonapi/test_file_io.py +++ b/src/pcms/pythonapi/test_file_io.py @@ -7,6 +7,8 @@ import numpy as np import os import shutil +import gc +import tempfile def test_binary_io(lib, world): """Test binary file format I/O""" @@ -28,9 +30,9 @@ def test_binary_io(lib, world): dim_orig = mesh.dim() print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - # Create temporary directory for test files - test_dir = "test_data" - os.makedirs(test_dir) + # Create unique temporary directory for test files + test_dir = tempfile.mkdtemp(prefix="pcms_test_binary_") + print(f"Using temporary directory: {test_dir}") try: # Test binary write/read binary_file = os.path.join(test_dir, "test_mesh.osh") @@ -47,17 +49,15 @@ def test_binary_io(lib, world): print("✓ Binary I/O test passed") + # Explicitly delete mesh objects before cleanup + del mesh_read + del mesh + gc.collect() # Force garbage collection + finally: # Clean up temporary files - if os.path.exists(test_dir): - for item in os.listdir(test_dir): - item_path = os.path.join(test_dir, item) - if os.path.isfile(item_path): - os.remove(item_path) - elif os.path.isdir(item_path): - shutil.rmtree(item_path) - os.rmdir(test_dir) - print(f"Cleaned up test directory: {test_dir}") + shutil.rmtree(test_dir, ignore_errors=True) + print(f"Cleaned up test directory: {test_dir}") def test_gmsh_io(lib, world): @@ -80,9 +80,9 @@ def test_gmsh_io(lib, world): dim_orig = mesh.dim() print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - # Create temporary directory for test files - test_dir = "test_data" - os.makedirs(test_dir) + # Create unique temporary directory for test files + test_dir = tempfile.mkdtemp(prefix="pcms_test_gmsh_") + print(f"Using temporary directory: {test_dir}") try: # Test Gmsh write/read gmsh_file = os.path.join(test_dir, "test_mesh.msh") @@ -99,17 +99,15 @@ def test_gmsh_io(lib, world): print("✓ Gmsh I/O test passed") + # Explicitly delete mesh objects before cleanup + del mesh_read + del mesh + gc.collect() # Force garbage collection + finally: # Clean up temporary files - if os.path.exists(test_dir): - for item in os.listdir(test_dir): - item_path = os.path.join(test_dir, item) - if os.path.isfile(item_path): - os.remove(item_path) - elif os.path.isdir(item_path): - shutil.rmtree(item_path) - os.rmdir(test_dir) - print(f"Cleaned up test directory: {test_dir}") + shutil.rmtree(test_dir, ignore_errors=True) + print(f"Cleaned up test directory: {test_dir}") def test_vtk_io(lib, world): @@ -134,9 +132,9 @@ def test_vtk_io(lib, world): print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") print(f"Coordinates shape: {coords_orig.shape}") - # Create temporary directory for test files - test_dir = "test_data" - os.makedirs(test_dir) + # Create unique temporary directory for test files + test_dir = tempfile.mkdtemp(prefix="pcms_test_vtk_") + print(f"Using temporary directory: {test_dir}") try: # Test VTU write (VTU is write-only in the API, typically for visualization) vtu_file = os.path.join(test_dir, "test_mesh.vtu") @@ -159,17 +157,14 @@ def test_vtk_io(lib, world): print("✓ VTK I/O test passed") + # Explicitly delete mesh objects before cleanup + del mesh + gc.collect() # Force garbage collection + finally: # Clean up temporary files - if os.path.exists(test_dir): - for item in os.listdir(test_dir): - item_path = os.path.join(test_dir, item) - if os.path.isfile(item_path): - os.remove(item_path) - elif os.path.isdir(item_path): - shutil.rmtree(item_path) - os.rmdir(test_dir) - print(f"Cleaned up test directory: {test_dir}") + shutil.rmtree(test_dir, ignore_errors=True) + print(f"Cleaned up test directory: {test_dir}") def test_meshb_io(lib, world): @@ -197,9 +192,9 @@ def test_meshb_io(lib, world): dim_orig = mesh.dim() print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - # Create temporary directory for test files - test_dir = "test_data" - os.makedirs(test_dir) + # Create unique temporary directory for test files + test_dir = tempfile.mkdtemp(prefix="pcms_test_meshb_") + print(f"Using temporary directory: {test_dir}") try: # Test MESHB write/read meshb_file = os.path.join(test_dir, "test_mesh.mesh") @@ -219,17 +214,15 @@ def test_meshb_io(lib, world): print("✓ MESHB I/O test passed") + # Explicitly delete mesh objects before cleanup + del mesh_read + del mesh + gc.collect() # Force garbage collection + finally: # Clean up temporary files - if os.path.exists(test_dir): - for item in os.listdir(test_dir): - item_path = os.path.join(test_dir, item) - if os.path.isfile(item_path): - os.remove(item_path) - elif os.path.isdir(item_path): - shutil.rmtree(item_path) - os.rmdir(test_dir) - print(f"Cleaned up test directory: {test_dir}") + shutil.rmtree(test_dir, ignore_errors=True) + print(f"Cleaned up test directory: {test_dir}") def test_exodus_io(lib, world): @@ -257,9 +250,9 @@ def test_exodus_io(lib, world): dim_orig = mesh.dim() print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - # Create temporary directory for test files - test_dir = "test_data" - os.makedirs(test_dir) + # Create unique temporary directory for test files + test_dir = tempfile.mkdtemp(prefix="pcms_test_exodus_") + print(f"Using temporary directory: {test_dir}") try: # Test Exodus write exodus_file = os.path.join(test_dir, "test_mesh.exo") @@ -290,20 +283,19 @@ def test_exodus_io(lib, world): # Close the file pcms.exodus_close(exo_handle) + # Explicitly delete mesh object from handle + del mesh_from_handle print("✓ Exodus I/O test passed") + # Explicitly delete mesh objects before cleanup + del mesh + gc.collect() # Force garbage collection + finally: # Clean up temporary files - if os.path.exists(test_dir): - for item in os.listdir(test_dir): - item_path = os.path.join(test_dir, item) - if os.path.isfile(item_path): - os.remove(item_path) - elif os.path.isdir(item_path): - shutil.rmtree(item_path) - os.rmdir(test_dir) - print(f"Cleaned up test directory: {test_dir}") + shutil.rmtree(test_dir, ignore_errors=True) + print(f"Cleaned up test directory: {test_dir}") def test_adios2_io(lib, world): @@ -331,9 +323,9 @@ def test_adios2_io(lib, world): dim_orig = mesh.dim() print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - # Create temporary directory for test files - test_dir = "test_data" - os.makedirs(test_dir) + # Create unique temporary directory for test files + test_dir = tempfile.mkdtemp(prefix="pcms_test_adios2_") + print(f"Using temporary directory: {test_dir}") try: # Test ADIOS2 write/read adios2_file = os.path.join(test_dir, "test_mesh.bp") @@ -350,17 +342,15 @@ def test_adios2_io(lib, world): print("✓ ADIOS2 I/O test passed") + # Explicitly delete mesh objects before cleanup + del mesh_read + del mesh + gc.collect() # Force garbage collection + finally: # Clean up temporary files - if os.path.exists(test_dir): - for item in os.listdir(test_dir): - item_path = os.path.join(test_dir, item) - if os.path.isfile(item_path): - os.remove(item_path) - elif os.path.isdir(item_path): - shutil.rmtree(item_path) - os.rmdir(test_dir) - print(f"Cleaned up test directory: {test_dir}") + shutil.rmtree(test_dir, ignore_errors=True) + print(f"Cleaned up test directory: {test_dir}") def test_read_mesh_file_auto_detect(lib, world): @@ -380,9 +370,9 @@ def test_read_mesh_file_auto_detect(lib, world): nverts_orig = mesh.nverts() nelems_orig = mesh.nelems() - # Create temporary directory for test files - test_dir = "test_data" - os.makedirs(test_dir) + # Create unique temporary directory for test files + test_dir = tempfile.mkdtemp(prefix="pcms_test_autodetect_") + print(f"Using temporary directory: {test_dir}") try: # Write in different formats binary_file = os.path.join(test_dir, "mesh.osh") @@ -405,17 +395,16 @@ def test_read_mesh_file_auto_detect(lib, world): assert mesh_gmsh.nelems() == nelems_orig print("✓ Gmsh auto-detection passed") + # Explicitly delete mesh objects before cleanup + del mesh_binary + del mesh_gmsh + del mesh + gc.collect() # Force garbage collection + finally: # Clean up temporary files - if os.path.exists(test_dir): - for item in os.listdir(test_dir): - item_path = os.path.join(test_dir, item) - if os.path.isfile(item_path): - os.remove(item_path) - elif os.path.isdir(item_path): - shutil.rmtree(item_path) - os.rmdir(test_dir) - print(f"Cleaned up test directory: {test_dir}") + shutil.rmtree(test_dir, ignore_errors=True) + print(f"Cleaned up test directory: {test_dir}") if __name__ == "__main__": print("=" * 60) diff --git a/src/pcms/pythonapi/test_mls_interpolation.py b/src/pcms/pythonapi/test_mls_interpolation.py index bcc9cbc36..cb9f8ca26 100644 --- a/src/pcms/pythonapi/test_mls_interpolation.py +++ b/src/pcms/pythonapi/test_mls_interpolation.py @@ -1,4 +1,8 @@ #!/usr/bin/env python3 +""" +Tests for MLS-backed PolynomialReconstructionFunctionSpace using the +Field-based API. +""" import numpy as np import pcms @@ -16,15 +20,15 @@ def poly_value(x, y, degree): raise ValueError(f"Unsupported polynomial degree: {degree}") -def build_full_support(num_targets, num_sources): - supports_ptr = np.arange(0, (num_targets + 1) * num_sources, - num_sources, dtype=np.int32) - supports_idx = np.tile(np.arange(num_sources, dtype=np.int32), num_targets) - radii2 = np.full(num_targets, 10.0, dtype=np.float64) - return pcms.SupportResults(supports_ptr, supports_idx, radii2) - - def test_mls_interpolation_polynomial_reproduction(): + """ + Verify that MLS reproduces polynomials up to the configured degree. + + Source points are a 4x4 grid on [0,1]^2. Target points are six + arbitrary interior locations. For each interpolation degree d, MLS + must exactly reproduce any polynomial of degree <= d (up to the + configured tolerance). + """ tolerance = 5e-4 grid_vals = np.linspace(0.0, 1.0, 4) @@ -41,17 +45,28 @@ def test_mls_interpolation_polynomial_reproduction(): ], dtype=np.float64, ) - - num_sources = source_xy.shape[0] num_targets = target_xy.shape[0] - source_coordinates = source_xy.reshape(-1) - target_coordinates = target_xy.reshape(-1) - support = build_full_support(num_targets, num_sources) - - print("Testing MLS interpolation for polynomial reproduction...") + print("Testing MLS polynomial reproduction via PolynomialReconstructionFunctionSpace...") for interp_degree in range(1, 4): + # Use a radius that covers the entire unit square from any target so + # all 16 source points are always in support (equivalent to the + # full-support test in the old low-level API). + opts = pcms.MLSOptions() + opts.degree = interp_degree + opts.radius = 1.5 + opts.adapt_radius = False + opts.basis = pcms.RadialBasisFunction.NO_OP + + factory = pcms.PolynomialReconstructionFunctionSpace.from_coords( + source_xy, pcms.CoordinateSystem.Cartesian, opts + ) + field = factory.create_field() + request = pcms.EvaluationRequest.from_coordinates(target_xy) + evaluator = factory.create_point_evaluator(request) + results = np.zeros((num_targets, 1), dtype=np.float64) + for func_degree in range(interp_degree, -1, -1): source_values = np.array( [poly_value(x, y, func_degree) for (x, y) in source_xy], @@ -62,18 +77,9 @@ def test_mls_interpolation_polynomial_reproduction(): dtype=np.float64, ) - approx_target_values = pcms.mls_interpolation( - source_values, - source_coordinates, - target_coordinates, - support, - 2, - interp_degree, - pcms.RadialBasisFunction.NO_OP, - 1e-5, - 1e-6, - 5.0, - ) + field.set_dof_holder_data(source_values) + evaluator.evaluate(field, results) + approx_target_values = results[:, 0] max_abs_err = np.max(np.abs(exact_target_values - approx_target_values)) assert max_abs_err < tolerance, ( @@ -81,10 +87,13 @@ def test_mls_interpolation_polynomial_reproduction(): f"func_degree={func_degree}: max_abs_err={max_abs_err}" ) + print("MLS polynomial reproduction test passed.") + + if __name__ == "__main__": lib = pcms.OmegaHLibrary() world = lib.world() - + try: test_mls_interpolation_polynomial_reproduction() print("MLS interpolation test passed") @@ -94,6 +103,5 @@ def test_mls_interpolation_polynomial_reproduction(): traceback.print_exc() exit(1) finally: - # Explicitly delete objects to avoid an MPI finalizing issue del world del lib diff --git a/src/pcms/pythonapi/test_omega_h_field.py b/src/pcms/pythonapi/test_omega_h_field.py index 8e741b627..0b6ad0503 100644 --- a/src/pcms/pythonapi/test_omega_h_field.py +++ b/src/pcms/pythonapi/test_omega_h_field.py @@ -1,161 +1,120 @@ #!/usr/bin/env python3 """ -Test OmegaHFieldLayout Python bindings +Field-centric Python examples for Omega_h-backed function spaces. """ -import pcms import numpy as np +import pcms -def test_layout_methods(world, dim, order, num_components): - """Test OmegaHFieldLayout methods""" + +def test_field_methods(world, dim, order, num_components): + """Create an Omega_h-backed Field and exercise the public Field API.""" nx = 10 ny = 10 if dim > 1 else 0 nz = 10 if dim > 2 else 0 - print(f"\nTesting layout: dim={dim}, order={order}, num_components={num_components}") - - # Build mesh mesh = pcms.build_box( - world, - pcms.Family.SIMPLEX, - 1.0, 1.0, 1.0, - nx, ny, nz, + world, + pcms.Family.SIMPLEX, + 1.0, 1.0, 1.0, + nx, ny, nz, False ) - # Create layout - layout = pcms.create_lagrange_layout( - mesh, - order, - num_components, - pcms.CoordinateSystem.Cartesian + factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components, pcms.CoordinateSystem.Cartesian ) + field = factory.create_field() - # Test layout methods - num_comp = layout.get_num_components() - assert num_comp == num_components, f"Expected {num_components} components, got {num_comp}" - - num_owned = layout.get_num_owned_dof_holder() - print(f" Number of owned DOF holders: {num_owned}") - assert num_owned > 0, "Expected at least one owned DOF holder" - - num_global = layout.get_num_global_dof_holder() - print(f" Number of global DOF holders: {num_global}") - assert num_global >= num_owned, "Global DOF holders should be >= owned" - - # Get ownership mask - owned = layout.get_owned() - print(f" Ownership mask length: {len(owned)}") - assert len(owned) > 0, "Ownership mask should not be empty" - - # Get global IDs - gids = layout.get_gids() - print(f" Global IDs length: {len(gids)}") - assert len(gids) > 0, "Global IDs should not be empty" - - # Get coordinates of DOF holders (2D parametric coordinates) - coords = layout.get_dof_holder_coordinates() - print(f" Coordinates shape: {coords.shape}") - assert coords.shape[0] > 0, "Should have coordinates" - assert coords.shape[1] == 2, "Coordinates should be 2D (parametric)" - - # Check if distributed - is_distributed = layout.is_distributed() - print(f" Is distributed: {is_distributed}") - - # Get entity offsets - ent_offsets = layout.get_ent_offsets() - print(f" Entity offsets: {ent_offsets}") - assert len(ent_offsets) == 5, f"Expected 5 entity offsets" - - # Get nodes per dimension - nodes = layout.get_nodes_per_dim() - print(f" Nodes per dim: {nodes}") - assert len(nodes) == 4, "Should have 4 entries (verts, edges, faces, regions)" - - # Get number of entities - num_ents = layout.get_num_ents() - print(f" Number of entities: {num_ents}") - assert num_ents > 0, "Should have at least one entity" - - # Get sizes - owned_size = layout.owned_size() - global_size = layout.global_size() - print(f" Owned size: {owned_size}") - print(f" Global size: {global_size}") - assert owned_size == num_owned * num_components - assert global_size == num_global * num_components - - # Create a field from the layout - field = layout.create_field() - print(f" Created field: {type(field)}") - - # Test setting and getting data - ndata = num_owned * num_components + assert field.get_num_components() == num_components + assert field.get_num_dof_holders() > 0 + + coords = field.get_dof_holder_coordinates() + assert coords.shape[0] == field.get_num_dof_holders() + assert coords.shape[1] == dim + + ndata = field.get_num_dof_holders() * num_components test_data = np.arange(ndata, dtype=np.float64) field.set_dof_holder_data(test_data) + np.testing.assert_allclose(field.get_dof_holder_data(), test_data) + + +def test_field_transfer(world, dim, order, num_components): + """Transfer data between identical Omega_h-backed function spaces.""" + if num_components != 1: + return + + nx = 10 + ny = 10 if dim > 1 else 0 + nz = 10 if dim > 2 else 0 + + mesh = pcms.build_box( + world, + pcms.Family.SIMPLEX, + 1.0, 1.0, 1.0, + nx, ny, nz, + False + ) + + source_space = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components, pcms.CoordinateSystem.Cartesian + ) + target_space = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components, pcms.CoordinateSystem.Cartesian + ) + + source = source_space.create_field() + target = target_space.create_field() - retrieved_data = field.get_dof_holder_data() - assert len(retrieved_data) == ndata, f"Expected {ndata} elements, got {len(retrieved_data)}" + coords = source.get_dof_holder_coordinates() + source_data = np.zeros(source.get_num_dof_holders(), dtype=np.float64) + for i in range(source.get_num_dof_holders()): + source_data[i] = np.sum(coords[i, :dim]) + source.set_dof_holder_data(source_data) - # Verify data matches - # debug print - for i in range(min(10, ndata)): - print(f" Data[{i}]: set={test_data[i]}, got={retrieved_data[i]}") - differences = np.abs(test_data - retrieved_data) - assert np.all(differences < 1e-12), "Data mismatch after set/get" + interp = pcms.Interpolator(source_space, target_space) + interp.apply(source, target) + + np.testing.assert_allclose(target.get_dof_holder_data(), source_data, atol=1e-14) - print(f"✓ Test passed: dim={dim}, order={order}, num_components={num_components}") def test_field_evaluation(world, dim, order, num_components): - """Test OmegaHField evaluation at points""" + """Evaluate an Omega_h-backed field at explicit query points.""" nx = 10 ny = 10 if dim > 1 else 0 nz = 10 if dim > 2 else 0 - print(f"\nTesting field evaluation: dim={dim}, order={order}, num_components={num_components}") + print( + f"\nTesting field evaluation: dim={dim}, order={order}, " + f"num_components={num_components}" + ) - # Build mesh mesh = pcms.build_box( - world, - pcms.Family.SIMPLEX, - 1.0, 1.0, 1.0, - nx, ny, nz, + world, + pcms.Family.SIMPLEX, + 1.0, 1.0, 1.0, + nx, ny, nz, False ) - # Create layout - layout = pcms.create_lagrange_layout( - mesh, - order, - num_components, - pcms.CoordinateSystem.Cartesian + factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components, pcms.CoordinateSystem.Cartesian ) + field = factory.create_field() - # Create field - field = layout.create_field() - - # Set up test data - use a simple function: f(x,y,z) = sin(x*y) for component 0, etc. - print(f" Setting up field data...") - # Get mesh vertex coordinates to set field values + print(" Setting up field data...") mesh_coords = mesh.coords() num_verts = mesh.nverts() print(f" Mesh has {num_verts} vertices") - # For order 1, we only need vertex values - # For order 2, we also need edge midpoint values - num_owned = layout.get_num_owned_dof_holder() + num_owned = field.get_num_dof_holders() ndata = num_owned * num_components print(f" Setting up field data for {ndata} DOFs") - # Define test function def test_func(x, y, z, component): return np.sin(5.0 * x * y) + float(component) - # Create test data array test_data = np.zeros(ndata, dtype=np.float64) - - # Set vertex values for i in range(min(num_verts, num_owned)): x = mesh_coords[i * dim + 0] if dim >= 1 else 0.0 y = mesh_coords[i * dim + 1] if dim >= 2 else 0.0 @@ -164,365 +123,209 @@ def test_func(x, y, z, component): idx = i * num_components + c test_data[idx] = test_func(x, y, z, c) - print(f" Set vertex values for field data") - - # For order 2, set edge midpoint values (simplified - would need proper edge coordinates) if order == 2 and num_owned > num_verts: for i in range(num_verts, num_owned): for c in range(num_components): idx = i * num_components + c - # Use simplified values for edges test_data[idx] = float(c) + 0.5 field.set_dof_holder_data(test_data) - print(f" Set field data with test function") + print(" Set field data with test function") - # Define evaluation points (physical coordinates) if dim == 2: eval_coords = np.array([ [0.5, 0.5], [0.25, 0.25], [0.75, 0.75], [0.1, 0.9], - [0.9, 0.1] + [0.9, 0.1], ], dtype=np.float64) - else: # dim == 3 + else: eval_coords = np.array([ [0.5, 0.5, 0.5], [0.25, 0.25, 0.25], [0.75, 0.75, 0.75], [0.1, 0.9, 0.1], - [0.9, 0.1, 0.9] + [0.9, 0.1, 0.9], ], dtype=np.float64) - num_eval_points = eval_coords.shape[0] - print(f" Evaluating at {num_eval_points} points") - - # Get localization hint for the coordinates - coord_system = pcms.CoordinateSystem.Cartesian - location_hint = field.get_localization_hint(eval_coords, coord_system) - print(f" Got localization hint") + print(f" Evaluating at {eval_coords.shape[0]} points") + request = pcms.EvaluationRequest.from_coordinates(eval_coords) + evaluator = factory.create_point_evaluator(request) + print(" Created point evaluator") - # Create output buffer for evaluation results - eval_values = np.zeros(num_eval_points * num_components, dtype=np.float64) + eval_values = np.zeros((eval_coords.shape[0], num_components), + dtype=np.float64) + evaluator.evaluate(field, eval_values) + print(" Field evaluated successfully") - # Evaluate the field - field.evaluate(location_hint, eval_values, coord_system) - print(f" Field evaluated successfully") - - # Print and verify results - for i in range(num_eval_points): + for i in range(eval_coords.shape[0]): coords_str = ", ".join([f"{eval_coords[i, j]:.3f}" for j in range(dim)]) - values_str = ", ".join([f"{eval_values[i * num_components + c]:.4f}" - for c in range(num_components)]) + values_str = ", ".join( + [f"{eval_values[i, c]:.4f}" for c in range(num_components)] + ) print(f" Point {i} ({coords_str}): values = [{values_str}]") - - # Verify we got valid values (not NaN or infinity) for c in range(num_components): - val = eval_values[i * num_components + c] - assert not np.isnan(val), f"Got NaN value at point {i}, component {c}" - assert not np.isinf(val), f"Got inf value at point {i}, component {c}" - - # Test gradient evaluation if available - if field.can_evaluate_gradient(): - print(f" Testing gradient evaluation...") - gradient_values = np.zeros(num_eval_points * num_components * dim, dtype=np.float64) - - try: - field.evaluate_gradient(gradient_values, coord_system) - print(f" Gradient evaluated successfully") - - # Print gradient results - for i in range(min(3, num_eval_points)): - for c in range(num_components): - grad_components = [] - for d in range(dim): - idx = i * num_components * dim + c * dim + d - grad_components.append(f"{gradient_values[idx]:.4f}") - grad_str = ", ".join(grad_components) - print(f" Point {i}, component {c}: gradient = [{grad_str}]") - except Exception as e: - print(f" Gradient evaluation failed: {e}") - else: - print(f" Gradient evaluation not supported") + val = eval_values[i, c] + assert not np.isnan(val) + assert not np.isinf(val) - print(f"✓ Field evaluation test passed: dim={dim}, order={order}, num_components={num_components}") def test_tag_operations(world, dim): - """Test Omega_h mesh tag operations""" + """Exercise Omega_h mesh tag creation, mutation, and query helpers.""" nx = 5 ny = 5 if dim > 1 else 0 nz = 5 if dim > 2 else 0 - print(f"\nTesting tag operations: dim={dim}") - - # Build mesh mesh = pcms.build_box( - world, - pcms.Family.SIMPLEX, - 1.0, 1.0, 1.0, - nx, ny, nz, + world, + pcms.Family.SIMPLEX, + 1.0, 1.0, 1.0, + nx, ny, nz, False ) + rng = np.random.default_rng(0) - print(f" Mesh has {mesh.nverts()} vertices") - print(f" Mesh has {mesh.nelems()} elements") - - # Test 1: Create empty tags with different data types - print("\n Test 1: Creating empty tags with different dtypes...") mesh.add_tag(0, "test_float", 1, dtype="float64") mesh.add_tag(0, "test_int32", 1, dtype="int32") mesh.add_tag(0, "test_int64", 1, dtype="int64") - - assert mesh.has_tag(0, "test_float"), "Float tag should exist" - assert mesh.has_tag(0, "test_int32"), "Int32 tag should exist" - assert mesh.has_tag(0, "test_int64"), "Int64 tag should exist" - print(" ✓ Empty tags created successfully") - - # Test 2: Create tags with initial data (auto-detect type) - print("\n Test 2: Creating tags with initial numpy arrays...") + + assert mesh.has_tag(0, "test_float") + assert mesh.has_tag(0, "test_int32") + assert mesh.has_tag(0, "test_int64") + nverts = mesh.nverts() - - # Float64 array temp_data = np.linspace(0.0, 100.0, nverts, dtype=np.float64) - mesh.add_tag(0, "temperature", 1, temp_data) - - # Int32 array ids_data = np.arange(nverts, dtype=np.int32) + velocity_data = rng.standard_normal(nverts * dim).astype(np.float64) + + mesh.add_tag(0, "temperature", 1, temp_data) mesh.add_tag(0, "vertex_ids", 1, ids_data) - - # Multi-component vector - velocity_data = np.random.randn(nverts * dim).astype(np.float64) mesh.add_tag(0, "velocity", dim, velocity_data) - - assert mesh.has_tag(0, "temperature"), "Temperature tag should exist" - assert mesh.has_tag(0, "vertex_ids"), "Vertex IDs tag should exist" - assert mesh.has_tag(0, "velocity"), "Velocity tag should exist" - print(" ✓ Tags with initial data created successfully") - - # Test 3: Get tag data and verify - print("\n Test 3: Getting tag data and verifying...") - retrieved_temp = mesh.get_tag(0, "temperature") - retrieved_ids = mesh.get_tag(0, "vertex_ids") - retrieved_vel = mesh.get_tag(0, "velocity") - - assert retrieved_temp.dtype == np.float64, "Temperature should be float64" - assert retrieved_ids.dtype == np.int32, "IDs should be int32" - assert retrieved_vel.shape[0] == nverts * dim, f"Velocity should have {nverts * dim} elements" - - np.testing.assert_allclose(retrieved_temp, temp_data, rtol=1e-15) - np.testing.assert_array_equal(retrieved_ids, ids_data) - print(" ✓ Tag data retrieved and verified successfully") - - # Test 4: Set tag data - print("\n Test 4: Setting tag data...") + + np.testing.assert_allclose(mesh.get_tag(0, "temperature"), temp_data) + np.testing.assert_array_equal(mesh.get_tag(0, "vertex_ids"), ids_data) + np.testing.assert_allclose(mesh.get_tag(0, "velocity"), velocity_data) + new_temp = np.ones(nverts, dtype=np.float64) * 50.0 mesh.set_tag(0, "temperature", new_temp) - - updated_temp = mesh.get_tag(0, "temperature") - np.testing.assert_allclose(updated_temp, new_temp, rtol=1e-15) - print(" ✓ Tag data set and verified successfully") - - # Test 5: ArrayType parameter - print("\n Test 5: Testing ArrayType parameter...") - # Create a symmetric matrix tag (stress tensor) + np.testing.assert_allclose(mesh.get_tag(0, "temperature"), new_temp) + if dim == 3: - ncomps = 6 # xx, yy, zz, xy, xz, yz - stress_data = np.random.randn(mesh.nelems() * ncomps).astype(np.float64) - mesh.add_tag(dim, "stress", ncomps, stress_data, - internal=False, - array_type=pcms.ArrayType.SymmetricSquareMatrix) - assert mesh.has_tag(dim, "stress"), "Stress tag should exist on elements" - print(" ✓ SymmetricSquareMatrix ArrayType tag created") - - # Create an internal tag (not written to file) + ncomps = 6 + stress_data = rng.standard_normal(mesh.nelems() * ncomps).astype( + np.float64) + mesh.add_tag(dim, "stress", ncomps, stress_data, + internal=False, + array_type=pcms.ArrayType.SymmetricSquareMatrix) + assert mesh.has_tag(dim, "stress") + internal_data = np.zeros(nverts, dtype=np.float64) mesh.add_tag(0, "internal_temp", 1, internal_data, internal=True) - assert mesh.has_tag(0, "internal_temp"), "Internal tag should exist" - print(" ✓ Internal tag created") + assert mesh.has_tag(0, "internal_temp") - # Test 6: Tag removal - print("\n Test 6: Removing tags...") mesh.remove_tag(0, "test_float") - assert not mesh.has_tag(0, "test_float"), "test_float tag should be removed" - - # Other tags should still exist - assert mesh.has_tag(0, "temperature"), "temperature tag should still exist" - print(" ✓ Tag removal successful") - - # Test 7: Number of tags - print("\n Test 7: Counting tags...") - ntags = mesh.ntags(0) - print(f" Number of vertex tags: {ntags}") - assert ntags > 0, "Should have at least one vertex tag" - - # Test 8: Element tags - print("\n Test 8: Testing element tags...") + assert not mesh.has_tag(0, "test_float") + assert mesh.has_tag(0, "temperature") + assert mesh.ntags(0) > 0 + nelems = mesh.nelems() - elem_quality = np.random.rand(nelems).astype(np.float64) + elem_quality = rng.random(nelems).astype(np.float64) mesh.add_tag(dim, "quality", 1, elem_quality) - - retrieved_quality = mesh.get_tag(dim, "quality") - assert len(retrieved_quality) == nelems, f"Should have {nelems} quality values" - np.testing.assert_allclose(retrieved_quality, elem_quality, rtol=1e-15) - print(" ✓ Element tags working correctly") + np.testing.assert_allclose(mesh.get_tag(dim, "quality"), elem_quality) - # Test 9: Edge tags (if applicable) if dim >= 2: - print("\n Test 9: Testing edge tags...") - nedges = mesh.nedges() - edge_length = np.ones(nedges, dtype=np.float64) - mesh.add_tag(1, "edge_marker", 1, edge_length) - - retrieved_edge = mesh.get_tag(1, "edge_marker") - assert len(retrieved_edge) == nedges, f"Should have {nedges} edge values" - print(" ✓ Edge tags working correctly") - - # Test 10: Adjacency and globals - print("\n Test 10: Testing adjacency and global IDs...") - - # Get vertex connectivity of elements + nedges = mesh.nedges() + edge_length = np.ones(nedges, dtype=np.float64) + mesh.add_tag(1, "edge_marker", 1, edge_length) + np.testing.assert_allclose(mesh.get_tag(1, "edge_marker"), edge_length) + elem_verts = mesh.ask_elem_verts() - print(f" Element-vertex connectivity shape: {elem_verts.shape}") - assert len(elem_verts) > 0, "Should have element-vertex connectivity" - - # Get global IDs global_ids = mesh.globals(0) - print(f" Global vertex IDs shape: {global_ids.shape}") - assert len(global_ids) == nverts, f"Should have {nverts} global IDs" - - # Get verts of elements verts_of_elems = mesh.ask_verts_of(dim) - print(f" Verts of elements shape: {verts_of_elems.shape}") - assert len(verts_of_elems) > 0, "Should have vert connectivity" - - # Check adjacency existence - has_vert_to_elem = mesh.has_adj(0, dim) - print(f" Has vertex-to-element adjacency: {has_vert_to_elem}") - - print(" ✓ Adjacency and global ID queries successful") - - # Test 11: Ownership - print("\n Test 12: Testing ownership...") owned_verts = mesh.owned(0) - print(f" Owned vertices shape: {owned_verts.shape}") - num_owned = np.sum(owned_verts) - print(f" Number of owned vertices: {num_owned} / {nverts}") - assert len(owned_verts) == nverts, "Should have ownership flag for each vertex" - print(" ✓ Ownership queries successful") - print(f"\n✓ All tag operation tests passed for dim={dim}!") + assert len(elem_verts) > 0 + assert len(global_ids) == nverts + assert len(verts_of_elems) > 0 + assert len(owned_verts) == nverts + assert np.sum(owned_verts) > 0 + assert isinstance(mesh.has_adj(0, dim), (bool, np.bool_)) + def test_entity_coordinates(world, dim): - """Test computing entity coordinates (face centers, edge midpoints, etc.)""" + """Exercise entity-coordinate and averaging helpers on Omega_h meshes.""" nx = 4 ny = 4 if dim > 1 else 0 nz = 4 if dim > 2 else 0 - print(f"\nTesting entity coordinate computation: dim={dim}") - - # Build mesh mesh = pcms.build_box( - world, - pcms.Family.SIMPLEX, - 1.0, 1.0, 1.0, - nx, ny, nz, + world, + pcms.Family.SIMPLEX, + 1.0, 1.0, 1.0, + nx, ny, nz, False ) - print(f" Mesh has {mesh.nverts()} vertices") - - # Get vertex coordinates vertex_coords = mesh.coords() - print(f" Vertex coordinates shape: {vertex_coords.shape}") - - # Test computing coordinates for different entity dimensions + assert len(vertex_coords) == mesh.nverts() * dim + if dim >= 2: - print(f"\n Computing edge coordinates (midpoints)...") - nedges = mesh.nedges() - edge_coords = pcms.average_field(mesh, 1, dim, vertex_coords) - edge_coords_np = np.array(edge_coords).reshape(-1, dim) - print(f" Number of edges: {nedges}") - print(f" Edge coordinates shape: {edge_coords_np.shape}") - assert edge_coords_np.shape[0] == nedges, f"Should have {nedges} edge coordinates" - print(f" First edge center: {edge_coords_np[0]}") - + edge_coords = pcms.average_field(mesh, 1, dim, vertex_coords) + edge_coords_np = np.array(edge_coords).reshape(-1, dim) + assert edge_coords_np.shape[0] == mesh.nedges() + if dim == 2: - print(f"\n Computing element (face) coordinates (centroids)...") - nelems = mesh.nelems() - elem_coords = pcms.average_field(mesh, 2, dim, vertex_coords) - elem_coords_np = np.array(elem_coords).reshape(-1, dim) - print(f" Number of elements: {nelems}") - print(f" Element coordinates shape: {elem_coords_np.shape}") - assert elem_coords_np.shape[0] == nelems, f"Should have {nelems} element coordinates" - print(f" First element center: {elem_coords_np[0]}") - + elem_coords = pcms.average_field(mesh, 2, dim, vertex_coords) + elem_coords_np = np.array(elem_coords).reshape(-1, dim) + assert elem_coords_np.shape[0] == mesh.nelems() + if dim == 3: - print(f"\n Computing face coordinates (centroids)...") - nfaces = mesh.nfaces() - face_coords = pcms.average_field(mesh, 2, dim, vertex_coords) - face_coords_np = np.array(face_coords).reshape(-1, dim) - print(f" Number of faces: {nfaces}") - print(f" Face coordinates shape: {face_coords_np.shape}") - assert face_coords_np.shape[0] == nfaces, f"Should have {nfaces} face coordinates" - print(f" First face center: {face_coords_np[0]}") - - print(f"\n Computing region/element coordinates (centroids)...") - nregions = mesh.nregions() - region_coords = pcms.average_field(mesh, 3, dim, vertex_coords) - region_coords_np = np.array(region_coords).reshape(-1, dim) - print(f" Number of regions: {nregions}") - print(f" Region coordinates shape: {region_coords_np.shape}") - assert region_coords_np.shape[0] == nregions, f"Should have {nregions} region coordinates" - print(f" First region center: {region_coords_np[0]}") - - # Test averaging a field from vertices to entities - print(f"\n Testing field averaging from vertices to elements...") - nverts = mesh.nverts() - vertex_field = np.arange(nverts, dtype=np.float64) + face_coords = pcms.average_field(mesh, 2, dim, vertex_coords) + face_coords_np = np.array(face_coords).reshape(-1, dim) + assert face_coords_np.shape[0] == mesh.nfaces() + + region_coords = pcms.average_field(mesh, 3, dim, vertex_coords) + region_coords_np = np.array(region_coords).reshape(-1, dim) + assert region_coords_np.shape[0] == mesh.nregions() + + vertex_field = np.arange(mesh.nverts(), dtype=np.float64) mesh.add_tag(0, "test_vertex_field", 1, vertex_field) - - # Average to elements - elem_dim = dim # elements are top-dimensional entities - averaged_field = pcms.average_field(mesh, elem_dim, 1, vertex_field) - print(f" Averaged field shape: {averaged_field.shape}") - print(f" First 5 values: {averaged_field[:5]}") - - print(f"\n✓ Entity coordinate computation tests passed for dim={dim}!") + averaged_field = pcms.average_field(mesh, dim, 1, vertex_field) + assert averaged_field.shape[0] == mesh.nents(dim) + def main(): """Run all test cases""" - print("Testing OmegaHFieldLayout Python bindings...") - - # Initialize Omega_h library + print("Testing Omega_h Field Python bindings...") lib = pcms.OmegaHLibrary() world = lib.world() print("Initialized Omega_h library and world") - # Test different configurations - test_layout_methods(world, 2, 1, 1) - test_layout_methods(world, 2, 2, 1) - # test_layout_methods(world, 2, 1, 3) - # test_layout_methods(world, 3, 1, 1) + test_field_methods(world, 2, 1, 1) + test_field_methods(world, 2, 2, 1) - # Test field evaluation - print("\n" + "="*60) + print("\n" + "=" * 60) print("Testing field evaluation...") - print("="*60) + print("=" * 60) test_field_evaluation(world, 2, 1, 1) test_field_evaluation(world, 2, 2, 1) - # Test tag operations - print("\n" + "="*60) + print("\n" + "=" * 60) + print("Testing field transfer...") + print("=" * 60) + test_field_transfer(world, 2, 1, 1) + test_field_transfer(world, 2, 2, 1) + + print("\n" + "=" * 60) print("Testing tag operations...") - print("="*60) + print("=" * 60) test_tag_operations(world, 2) test_tag_operations(world, 3) - # Test entity coordinate computation - print("\n" + "="*60) + print("\n" + "=" * 60) print("Testing entity coordinate computation...") - print("="*60) + print("=" * 60) test_entity_coordinates(world, 2) test_entity_coordinates(world, 3) @@ -530,5 +333,6 @@ def main(): del world del lib + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/pcms/pythonapi/test_uniform_grid_field.py b/src/pcms/pythonapi/test_uniform_grid_field.py index b89864c6b..32a62203d 100644 --- a/src/pcms/pythonapi/test_uniform_grid_field.py +++ b/src/pcms/pythonapi/test_uniform_grid_field.py @@ -1,206 +1,158 @@ """ -Example test file for uniform grid field Python bindings. - -This demonstrates how to use the UniformGridField and UniformGridFieldLayout -classes in Python, similar to how OmegaHField is used. +Field-centric Python examples for uniform-grid-backed function spaces. """ import numpy as np import pcms def test_uniform_grid_field_creation(): - """Test creating a 2D uniform grid field.""" - # Create a 2D uniform grid + """Create a 2D field from a uniform-grid function space.""" + # Create a simple 2D structured grid. grid = pcms.UniformGrid2D() grid.bot_left = [0.0, 0.0] grid.edge_length = [10.0, 10.0] - grid.divisions = [4, 4] # 4x4 cells - - print(f"Grid cells: {grid.get_num_cells()}") # Should be 16 - - # Create field layout with 1 component (scalar field) - layout = pcms.UniformGridFieldLayout2D( + grid.divisions = [4, 4] + + # Build the function space from the grid, then create a Field from it. + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( grid, 1, pcms.CoordinateSystem.Cartesian ) - - print(f"Num components: {layout.get_num_components()}") - print(f"Num owned DOF holders: {layout.get_num_owned_dof_holder()}") - print(f"Num vertices: {layout.get_num_vertices()}") # 5x5 = 25 vertices - - # Create field - field = layout.create_field() + field = factory.create_field() + + # Order-1 2D grids store data at vertices, so a 4x4 cell grid has 5x5 + # DOF holders. + expected_vertices = (grid.divisions[0] + 1) * (grid.divisions[1] + 1) + print(f"Grid cells: {grid.get_num_cells()}") + print(f"Num components: {field.get_num_components()}") + print(f"Num owned DOF holders: {field.get_num_dof_holders()}") + assert field.get_num_components() == 1 + assert field.get_num_dof_holders() == expected_vertices print(f"Field created: {field is not None}") - - return grid, layout, field def test_uniform_grid_field_data_operations(): - """Test setting and getting field data.""" - # Create a simple grid + """Set and get flat DOF data through Field.""" + # A 2x2 cell grid has 3x3 vertex DOF holders. grid = pcms.UniformGrid2D() grid.bot_left = [0.0, 0.0] grid.edge_length = [10.0, 10.0] - grid.divisions = [2, 2] # 2x2 cells, 3x3 vertices - - layout = pcms.UniformGridFieldLayout2D( + grid.divisions = [2, 2] + + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( grid, 1, pcms.CoordinateSystem.Cartesian ) - ug_field = layout.create_field() - - # Get the number of DOF holders (vertices) - num_vertices = layout.get_num_vertices() - print(f"Number of vertices: {num_vertices}") # Should be 9 - - # Create data for vertices (initialize with values) - data = np.arange(num_vertices, dtype=np.float64) + field = factory.create_field() + + # Field data is set and retrieved as a flat 1D DOF array. + data = np.arange(field.get_num_dof_holders(), dtype=np.float64) + print(f"Number of vertices: {field.get_num_dof_holders()}") print(f"Setting data: {data}") - - # Set field data - ug_field.set_dof_holder_data(data) - - # Get field data back - retrieved_data = ug_field.get_dof_holder_data() + field.set_dof_holder_data(data) + retrieved_data = field.get_dof_holder_data() print(f"Retrieved data: {retrieved_data}") - - # Verify they match - assert np.allclose(data, retrieved_data) + np.testing.assert_allclose(retrieved_data, data) print("Data successfully set and retrieved!") -def test_uniform_grid_field_mdspan_2d(): - """Test 2D mdspan output as numpy array.""" +def test_uniform_grid_field_coordinates_2d(): + """Expose 2D DOF-holder coordinates through Field.""" + # Coordinate queries should now flow through Field rather than layout + # objects. grid = pcms.UniformGrid2D() grid.bot_left = [0.0, 0.0] grid.edge_length = [10.0, 10.0] - grid.divisions = [2, 3] # 3x4 vertices + grid.divisions = [2, 3] - layout = pcms.UniformGridFieldLayout2D( + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( grid, 1, pcms.CoordinateSystem.Cartesian ) - ug_field = layout.create_field() + field = factory.create_field() - num_vertices = layout.get_num_vertices() - data = np.arange(num_vertices, dtype=np.float64) - ug_field.set_dof_holder_data(data) + coords = field.get_dof_holder_coordinates() + expected_vertices = (grid.divisions[0] + 1) * (grid.divisions[1] + 1) - mdspan = ug_field.to_numpy() - expected = data.reshape((grid.divisions[0] + 1, grid.divisions[1] + 1)) - assert type(mdspan) == np.ndarray - - assert mdspan.shape == expected.shape - assert np.allclose(mdspan, expected) - print("2D mdspan output verified!") - - -def test_uniform_grid_field_evaluation(): - """Test evaluating field at specific coordinates.""" - # Create grid - grid = pcms.UniformGrid2D() - grid.bot_left = [0.0, 0.0] - grid.edge_length = [10.0, 10.0] - grid.divisions = [2, 2] - - layout = pcms.UniformGridFieldLayout2D( - grid, 1, pcms.CoordinateSystem.Cartesian - ) - ug_field = layout.create_field() - - # Set simple linear field data: f(x,y) = x + y - num_vertices = layout.get_num_vertices() - coords = layout.get_dof_holder_coordinates() - data = np.zeros(num_vertices) - for i in range(num_vertices): - data[i] = coords[i, 0] + coords[i, 1] - - ug_field.set_dof_holder_data(data) - - # Evaluate at some points - eval_coords = np.array([[2.5, 2.5], [7.5, 7.5]]) - - # Get localization hint - hint = ug_field.get_localization_hint( - eval_coords, pcms.CoordinateSystem.Cartesian - ) - - # Evaluate field - results = np.zeros(len(eval_coords)) - ug_field.evaluate( - hint, results, pcms.CoordinateSystem.Cartesian - ) - - print(f"Evaluation results: {results}") - print(f"Expected (approximately): [5.0, 15.0]") + assert isinstance(coords, np.ndarray) + assert coords.shape == (expected_vertices, 2) + np.testing.assert_allclose(coords[0], [0.0, 0.0]) + np.testing.assert_allclose(coords[-1], [10.0, 10.0]) + print("2D field coordinates verified!") def test_uniform_grid_closest_cell(): - """Test finding closest cell to a point.""" + """UniformGrid helpers remain available for grid setup.""" + # Grid setup helpers remain public even though layout classes do not. grid = pcms.UniformGrid2D() grid.bot_left = [0.0, 0.0] grid.edge_length = [10.0, 10.0] grid.divisions = [4, 4] - - # Test point in middle of grid - point = np.array([5.0, 5.0]) - cell_id = grid.closest_cell_id(point) - print(f"Point {point} is in cell {cell_id}") - - # Test point outside grid (should clamp to closest cell) - point_outside = np.array([-1.0, -1.0]) - cell_id_outside = grid.closest_cell_id(point_outside) - print(f"Point {point_outside} (outside) maps to cell {cell_id_outside}") + + cell_id = grid.closest_cell_id(np.array([5.0, 5.0])) + cell_id_outside = grid.closest_cell_id(np.array([-1.0, -1.0])) + print(f"Point {[5.0, 5.0]} is in cell {cell_id}") + print(f"Point {[-1.0, -1.0]} (outside) maps to cell {cell_id_outside}") + assert cell_id >= 0 + assert cell_id_outside >= 0 -def test_3d_uniform_grid(): - """Test 3D uniform grid field.""" +def test_uniform_grid_field_coordinates_3d(): + """Expose 3D DOF-holder coordinates and data through Field.""" + # 3D coordinate access and data operations follow the same pattern as 2D. grid = pcms.UniformGrid3D() grid.bot_left = [0.0, 0.0, 0.0] grid.edge_length = [10.0, 10.0, 10.0] - grid.divisions = [2, 2, 2] # 2x2x2 cells - - print(f"3D Grid cells: {grid.get_num_cells()}") # Should be 8 - - layout = pcms.UniformGridFieldLayout3D( + grid.divisions = [2, 1, 3] + + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( grid, 1, pcms.CoordinateSystem.Cartesian ) - - print(f"3D Grid vertices: {layout.get_num_vertices()}") # 3x3x3 = 27 - - ug_field = layout.create_field() - - # Set and get data - num_vertices = layout.get_num_vertices() - data = np.ones(num_vertices) * 42.0 - ug_field.set_dof_holder_data(data) - - retrieved = ug_field.get_dof_holder_data() - assert np.allclose(data, retrieved) - print("3D field data successfully set and retrieved!") + field = factory.create_field() + coords = field.get_dof_holder_coordinates() + expected_vertices = ((grid.divisions[0] + 1) * + (grid.divisions[1] + 1) * + (grid.divisions[2] + 1)) -def test_uniform_grid_field_mdspan_3d(): - """Test 3D mdspan output as numpy array.""" - grid = pcms.UniformGrid3D() - grid.bot_left = [0.0, 0.0, 0.0] - grid.edge_length = [10.0, 10.0, 10.0] - grid.divisions = [2, 1, 3] # 3x2x4 vertices + assert coords.shape == (expected_vertices, 3) + np.testing.assert_allclose(coords[0], [0.0, 0.0, 0.0]) + np.testing.assert_allclose(coords[-1], [10.0, 10.0, 10.0]) + print("3D field coordinates verified!") - layout = pcms.UniformGridFieldLayout3D( - grid, 1, pcms.CoordinateSystem.Cartesian - ) - ug_field = layout.create_field() + # Data set/get on a 3D grid field. + data = np.ones(expected_vertices, dtype=np.float64) * 42.0 + field.set_dof_holder_data(data) + np.testing.assert_allclose(field.get_dof_holder_data(), data) + print("3D field data set/get verified!") - num_vertices = layout.get_num_vertices() - data = np.arange(num_vertices, dtype=np.float64) - ug_field.set_dof_holder_data(data) - mdspan = ug_field.to_numpy() - expected = data.reshape( - (grid.divisions[0] + 1, grid.divisions[1] + 1, grid.divisions[2] + 1) - ) +def test_uniform_grid_field_evaluation(): + """Evaluate a uniform-grid field at explicit query points.""" + grid = pcms.UniformGrid2D() + grid.bot_left = [0.0, 0.0] + grid.edge_length = [10.0, 10.0] + grid.divisions = [2, 2] - assert mdspan.shape == expected.shape - assert np.allclose(mdspan, expected) - print("3D mdspan output verified!") + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( + grid, 1, pcms.CoordinateSystem.Cartesian + ) + field = factory.create_field() + + # Set a linear field f(x,y) = x + y so the expected values are exact. + coords = field.get_dof_holder_coordinates() + data = np.array([coords[i, 0] + coords[i, 1] + for i in range(field.get_num_dof_holders())], + dtype=np.float64) + field.set_dof_holder_data(data) + + eval_coords = np.array([[2.5, 2.5], [7.5, 7.5]], dtype=np.float64) + request = pcms.EvaluationRequest.from_coordinates(eval_coords) + evaluator = factory.create_point_evaluator(request) + results = np.zeros((len(eval_coords), 1), dtype=np.float64) + evaluator.evaluate(field, results) + + print(f"Evaluation results: {results.flatten()}") + print(f"Expected (approximately): [5.0, 15.0]") + np.testing.assert_allclose(results.flatten(), [5.0, 15.0], atol=1e-10) + print("Uniform grid field evaluation verified!") def test_uniform_grid_workflow(world): @@ -223,150 +175,70 @@ def test_uniform_grid_workflow(world): 4, 4, 0, False ) - - # Create uniform grid from mesh with 4x4 divisions + + # Create a uniform grid that covers the mesh bounding box, then construct a + # binary mask field directly as a Field object. grid = pcms.create_uniform_grid_from_mesh(mesh, [4, 4]) print(f"Created uniform grid with {grid.get_num_cells()} cells") - - # Create binary mask field (returns tuple of (layout, field)) - mask_layout, mask_field = pcms.create_uniform_grid_binary_field(mesh, [4, 4]) + mask_field = pcms.create_uniform_grid_binary_field(mesh, [4, 4]) mask_data = mask_field.get_dof_holder_data() print(f"Created mask field with {len(mask_data)} vertices") - - # Create Omega_h field layout with linear elements - omega_h_layout = pcms.create_lagrange_layout( - mesh, 1 - ) - omega_h_field = omega_h_layout.create_field() - - # Initialize omega_h field with f(x,y) = x + 2*y - coords = omega_h_layout.get_dof_holder_coordinates() - num_nodes = omega_h_layout.get_num_owned_dof_holder() - omega_h_data = np.zeros(num_nodes) - - for i in range(num_nodes): - x = coords[i, 0] - y = coords[i, 1] - omega_h_data[i] = x + 2.0 * y - + + # Build a mesh-backed source field and initialize it with f(x,y)=x+2y. + omega_h_factory = pcms.LagrangeFunctionSpace.from_mesh(mesh, 1) + omega_h_field = omega_h_factory.create_field() + + coords = omega_h_field.get_dof_holder_coordinates() + omega_h_data = np.zeros(omega_h_field.get_num_dof_holders(), dtype=np.float64) + for i in range(omega_h_field.get_num_dof_holders()): + omega_h_data[i] = coords[i, 0] + 2.0 * coords[i, 1] omega_h_field.set_dof_holder_data(omega_h_data) - print(f"Initialized Omega_h field with {num_nodes} nodes") - - # Create uniform grid field layout - ug_layout = pcms.UniformGridFieldLayout2D( + print(f"Initialized Omega_h field with {omega_h_field.get_num_dof_holders()} nodes") + + ug_factory = pcms.LagrangeFunctionSpace.from_uniform_grid( grid, 1, pcms.CoordinateSystem.Cartesian ) - ug_field = ug_layout.create_field() - - # Transfer from omega_h field to uniform grid field using interpolation - pcms.interpolate_field(omega_h_field, ug_field) + ug_field = ug_factory.create_field() + + # Transfer from the unstructured mesh field to the uniform-grid field. + interp = pcms.Interpolator(omega_h_factory, ug_factory) + interp.apply(omega_h_field, ug_field) print("Field interpolation completed") - - # Get uniform grid field data and coordinates + ug_field_data = ug_field.get_dof_holder_data() - ug_coords = ug_layout.get_dof_holder_coordinates() - - # Verify ug_field values - print("\nVerifying uniform grid field values:") + ug_coords = ug_field.get_dof_holder_coordinates() + + # The 4x4 cell grid should produce 5x5 vertex DOFs. + assert len(mask_data) == 25 + assert len(ug_field_data) == 25 + + # Verify interpolation at every target DOF holder. num_errors = 0 - for j in range(grid.divisions[1] + 1): - for i in range(grid.divisions[0] + 1): - vertex_id = j * (grid.divisions[0] + 1) + i - x = ug_coords[vertex_id, 0] - y = ug_coords[vertex_id, 1] - expected = x + 2.0 * y - actual = ug_field_data[vertex_id] - - error = abs(expected - actual) - if error > 1e-10: - num_errors += 1 - print(f" Vertex ({i}, {j}) at ({x:.2f}, {y:.2f}): " - f"expected {expected:.4f}, got {actual:.4f}, error {error:.2e}") - + for vertex_id in range(len(ug_field_data)): + x = ug_coords[vertex_id, 0] + y = ug_coords[vertex_id, 1] + expected = x + 2.0 * y + actual = ug_field_data[vertex_id] + error = abs(actual - expected) + if error > 1e-10: + num_errors += 1 + print( + f"Vertex {vertex_id} at ({x:.2f}, {y:.2f}): " + f"expected {expected:.4f}, got {actual:.4f}, error {error:.2e}" + ) + assert error < 1e-10 if num_errors == 0: print("All uniform grid field values verified successfully!") - else: - print(f"Found {num_errors} verification errors") - - # Verify mask field values (all should be 1 for vertices inside the mesh) - print("\nVerifying mask field values:") - mask_errors = 0 - nx, ny = grid.divisions[0] + 1, grid.divisions[1] + 1 - print(f"\nBinary mask field ({nx}x{ny} vertices) visualization:") - print("(Each position shows the mask value at that vertex)") - for j in range(ny - 1, -1, -1): # Print from top to bottom - row = [] - for i in range(nx): - vertex_id = j * nx + i - mask_value = mask_data[vertex_id] - row.append(str(int(mask_value))) - if mask_value != 1.0: - mask_errors += 1 - print(f" Row {j}: [{' '.join(row)}]") - - if mask_errors == 0: - print("All mask field values verified successfully!") - else: - print(f"Found {mask_errors} mask errors") - - # Serialize the uniform grid field - print("\nSerializing uniform grid field:") - num_vertices = ug_layout.get_num_vertices() - buffer = np.zeros(num_vertices) - permutation = np.arange(num_vertices, dtype=np.int32) # Identity permutation - - bytes_written = ug_field.serialize(buffer, permutation) - print(f"Serialized {bytes_written} values to buffer") - - print(f"\nSerialized uniform grid field data ({grid.divisions[0]+1}x{grid.divisions[1]+1} vertices) visualization:") - print("(Each vertex shows its field value)") - for j in range(grid.divisions[1], -1, -1): # Print from top to bottom - row_values = [] - for i in range(grid.divisions[0] + 1): - vertex_id = j * (grid.divisions[0] + 1) + i - value = buffer[vertex_id] - row_values.append(f"{value:6.3f}") - print(f" Row {j}: [{' '.join(row_values)}]") - - # Also print with coordinates for reference - print(f"\nDetailed vertex information:") - for j in range(grid.divisions[1] + 1): - for i in range(grid.divisions[0] + 1): - vertex_id = j * (grid.divisions[0] + 1) + i - x = ug_coords[vertex_id, 0] - y = ug_coords[vertex_id, 1] - value = buffer[vertex_id] - print(f" V[{i},{j}] (id={vertex_id:2d}) at ({x:.3f}, {y:.3f}): value = {value:.6f}") - - # Test deserialization - print("\nTesting deserialization:") - ug_field_copy = ug_layout.create_field() - ug_field_copy.deserialize(buffer, permutation) - - ug_field_copy_data = ug_field_copy.get_dof_holder_data() - if np.allclose(ug_field_data, ug_field_copy_data): - print("✓ Deserialization successful - data matches!") - else: - print("✗ Deserialization failed - data mismatch!") - print(f" Max difference: {np.max(np.abs(ug_field_data - ug_field_copy_data))}") - - assert num_errors == 0, f"Field verification failed with {num_errors} errors" - assert mask_errors == 0, f"Mask verification failed with {mask_errors} errors" def test_omega_h_to_omega_h_transfer_workflow(world): """ - Test field transfer from one Omega_h mesh to another Omega_h mesh. + Transfer a field from one Omega_h mesh to another Omega_h mesh. - This test: - 1. Creates a source Omega_h mesh - 2. Creates a target Omega_h mesh - 3. Creates Lagrange layouts and fields on both - 4. Initializes the source with f(x,y) = x + 2*y - 5. Transfers the field to the target mesh - 6. Verifies the transferred values at target DOF holders + This preserves coverage for the workflow where both source and target use + mesh-backed function spaces rather than a uniform grid target. """ - # Source mesh (coarser) + # Source mesh (coarser). src_mesh = pcms.build_box( world, pcms.Family.SIMPLEX, @@ -375,7 +247,7 @@ def test_omega_h_to_omega_h_transfer_workflow(world): False ) - # Target mesh (finer) + # Target mesh (finer). tgt_mesh = pcms.build_box( world, pcms.Family.SIMPLEX, @@ -384,81 +256,72 @@ def test_omega_h_to_omega_h_transfer_workflow(world): False ) - # Create Lagrange layouts and fields - src_layout = pcms.create_lagrange_layout( - src_mesh, 1 - ) - tgt_layout = pcms.create_lagrange_layout( - tgt_mesh, 1 - ) - src_field = src_layout.create_field() - tgt_field = tgt_layout.create_field() - - # Initialize source field with f(x,y) = x + 2*y - src_coords = src_layout.get_dof_holder_coordinates() - src_num_nodes = src_layout.get_num_owned_dof_holder() - src_data = np.zeros(src_num_nodes) - for i in range(src_num_nodes): - x = src_coords[i, 0] - y = src_coords[i, 1] - src_data[i] = x + 2.0 * y + # Create function spaces and fields on both meshes. + src_factory = pcms.LagrangeFunctionSpace.from_mesh(src_mesh, 1) + tgt_factory = pcms.LagrangeFunctionSpace.from_mesh(tgt_mesh, 1) + src_field = src_factory.create_field() + tgt_field = tgt_factory.create_field() + print("Created source and target Omega_h fields") + + # Initialize the source field with f(x,y)=x+2y. + src_coords = src_field.get_dof_holder_coordinates() + src_data = np.zeros(src_field.get_num_dof_holders()) + for i in range(len(src_data)): + src_data[i] = src_coords[i, 0] + 2.0 * src_coords[i, 1] src_field.set_dof_holder_data(src_data) + print(f"Initialized source field with {len(src_data)} DOF values") - # Transfer field to target mesh - pcms.interpolate_field(src_field, tgt_field) + # Transfer onto the target mesh and verify the target DOF values. + interp = pcms.Interpolator(src_factory, tgt_factory) + interp.apply(src_field, tgt_field) + print("Omega_h to Omega_h field interpolation completed") - # Verify target field values at target DOF holders - tgt_coords = tgt_layout.get_dof_holder_coordinates() + tgt_coords = tgt_field.get_dof_holder_coordinates() tgt_data = tgt_field.get_dof_holder_data() errors = 0 - for i in range(tgt_layout.get_num_owned_dof_holder()): - x = tgt_coords[i, 0] - y = tgt_coords[i, 1] - expected = x + 2.0 * y - actual = tgt_data[i] - if abs(expected - actual) > 1e-10: + for i in range(tgt_field.get_num_dof_holders()): + expected = tgt_coords[i, 0] + 2.0 * tgt_coords[i, 1] + error = abs(expected - tgt_data[i]) + if error > 1e-10: errors += 1 - assert errors == 0, f"Omega_h transfer verification failed with {errors} errors" + assert error < 1e-10 + print("Omega_h to Omega_h transfer verification completed") -if __name__ == "__main__": +def main(): lib = pcms.OmegaHLibrary() world = lib.world() + print("=" * 60) print("Testing UniformGrid Field Creation") print("=" * 60) test_uniform_grid_field_creation() - + print("\n" + "=" * 60) print("Testing UniformGrid Field Data Operations") print("=" * 60) test_uniform_grid_field_data_operations() print("\n" + "=" * 60) - print("Testing UniformGrid Field mdspan (2D)") + print("Testing UniformGrid Field Coordinates (2D)") print("=" * 60) - test_uniform_grid_field_mdspan_2d() - + test_uniform_grid_field_coordinates_2d() + print("\n" + "=" * 60) - print("Testing UniformGrid Field Evaluation") + print("Testing UniformGrid Field Coordinates (3D)") print("=" * 60) - test_uniform_grid_field_evaluation() - + test_uniform_grid_field_coordinates_3d() + print("\n" + "=" * 60) print("Testing Closest Cell ID") print("=" * 60) test_uniform_grid_closest_cell() - - print("\n" + "=" * 60) - print("Testing 3D UniformGrid") - print("=" * 60) - test_3d_uniform_grid() print("\n" + "=" * 60) - print("Testing UniformGrid Field mdspan (3D)") + print("Testing UniformGrid Field Evaluation") print("=" * 60) - test_uniform_grid_field_mdspan_3d() - + test_uniform_grid_field_evaluation() + print("\n" + "=" * 60) print("Testing UniformGrid Workflow") print("=" * 60) @@ -468,9 +331,13 @@ def test_omega_h_to_omega_h_transfer_workflow(world): print("Testing Omega_h to Omega_h Transfer Workflow") print("=" * 60) test_omega_h_to_omega_h_transfer_workflow(world) - + print("\n" + "=" * 60) print("All tests passed!") print("=" * 60) del world + + +if __name__ == "__main__": + main() diff --git a/src/pcms/pythonapi/uniform_grid_workflow_guide.md b/src/pcms/pythonapi/uniform_grid_workflow_guide.md index 9ed68f667..fe842111a 100644 --- a/src/pcms/pythonapi/uniform_grid_workflow_guide.md +++ b/src/pcms/pythonapi/uniform_grid_workflow_guide.md @@ -113,9 +113,8 @@ Create a mask where each uniform grid vertex is marked as 1 (inside mesh) or 0 ( ```python # Create binary mask indicating which vertices are inside the mesh -# Returns tuple of (layout, field) - layout must be kept alive while using field -mask_layout, mask_field = pcms.create_uniform_grid_binary_field(mesh, [4, 4]) -print(f"Created mask field with {mask_layout.get_num_vertices()} vertices") +mask_field = pcms.create_uniform_grid_binary_field(mesh, [4, 4]) +print(f"Created mask field with {mask_field.get_num_dof_holders()} vertices") ``` **Accessing Values**: @@ -133,18 +132,19 @@ mask_value = mask_data[vertex_id] # Returns 0.0 or 1.0 #### Option A: Create Field Programmatically ```python -# Create field layout with linear (order=1) elements and 1 component (scalar) -omega_h_layout = pcms.create_lagrange_layout( +# Create a concrete FunctionSpace with linear (order=1) elements and +# 1 component (scalar) +omega_h_factory = pcms.LagrangeFunctionSpace.from_mesh( mesh, 1, 1, pcms.CoordinateSystem.Cartesian ) -omega_h_field = omega_h_layout.create_field() +omega_h_field = omega_h_factory.create_field() # Initialize field with f(x,y) = x + 2*y -coords = omega_h_layout.get_dof_holder_coordinates() -num_nodes = omega_h_layout.get_num_owned_dof_holder() +coords = omega_h_field.get_dof_holder_coordinates() +num_nodes = omega_h_field.get_num_dof_holders() omega_h_data = np.zeros(num_nodes) for i in range(num_nodes): @@ -154,8 +154,6 @@ for i in range(num_nodes): omega_h_field.set_dof_holder_data(omega_h_data) -# Set out-of-bounds behavior (FILL with 0.0 for points outside mesh) -omega_h_field.set_out_of_bounds_mode(pcms.OutOfBoundsMode.FILL, 0.0) ``` #### Option B: Use Existing Element/Face Field from Mesh Tags @@ -172,29 +170,29 @@ face_field = mesh.get_tag(face_dim, face_tag.name()) # Convert element field to vertex field using averaging vertex_field = pcms.map_entity_field_to_vertices_average(mesh, face_field, face_dim) -# Create vertex-based field layout and set data -omega_h_layout = pcms.create_lagrange_layout(mesh, 1, 1, pcms.CoordinateSystem.Cartesian) -omega_h_field = omega_h_layout.create_field() +# Create vertex-based field and set data +omega_h_factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 1, pcms.CoordinateSystem.Cartesian +) +omega_h_field = omega_h_factory.create_field() omega_h_field.set_dof_holder_data(vertex_field) -# Set out-of-bounds behavior (FILL with 0.0 for points outside mesh) -omega_h_field.set_out_of_bounds_mode(pcms.OutOfBoundsMode.FILL, 0.0) ``` --- -### Step 6: Create Uniform Grid Field Layout +### Step 6: Create Uniform Grid Field Set up the data structure for storing field values on the uniform grid vertices. ```python -# Create uniform grid field layout -ug_layout = pcms.UniformGridFieldLayout2D( +# Create a uniform-grid FunctionSpace +ug_factory = pcms.LagrangeFunctionSpace.from_uniform_grid( grid, 1, # Number of components pcms.CoordinateSystem.Cartesian ) -ug_field = ug_layout.create_field() +ug_field = ug_factory.create_field() ``` --- @@ -204,8 +202,9 @@ ug_field = ug_layout.create_field() Interpolate field values from the unstructured mesh to the structured uniform grid vertices. ```python -# Transfer field from Omega_h mesh to uniform grid -pcms.interpolate_field(omega_h_field, ug_field) +# Transfer field from Omega_h mesh to uniform grid using two FunctionSpaces +interp = pcms.Interpolator(omega_h_factory, ug_factory) +interp.apply(omega_h_field, ug_field) ``` --- @@ -217,7 +216,7 @@ Retrieve interpolated values and verify correctness. ```python # Get field data and coordinates ug_field_data = ug_field.get_dof_holder_data() -ug_coords = ug_layout.get_dof_holder_coordinates() +ug_coords = ug_field.get_dof_holder_coordinates() # Access data at vertex (i, j) vertex_id = j * (grid.divisions[0] + 1) + i @@ -227,15 +226,14 @@ x, y = ug_coords[vertex_id, 0], ug_coords[vertex_id, 1] --- -### Step 9: Export Field Data with `to_mdspan` +### Step 9: Export Flat Field Data -Convert field data to a structured $x \times y$ (or $x \times y \times z$) array -for downstream analyses. +Retrieve the flat DOF arrays for downstream analyses. ```python -# Convert field data to a structured array -grid_values = ug_field.to_numpy() # 2D: (nx+1, ny+1), 3D: (nx+1, ny+1, nz+1) -mask_values = mask_field.to_numpy() # 2D: (nx+1, ny+1), 3D: (nx+1, ny+1, nz+1) +# Get flat DOF arrays +grid_values = ug_field.get_dof_holder_data() +mask_values = mask_field.get_dof_holder_data() # Save to file (example) np.save('field_data.npy', grid_values) @@ -249,18 +247,24 @@ np.save('field_data.npy', grid_values) - `create_uniform_grid_from_mesh(mesh, divisions)` - Create grid from mesh - `create_uniform_grid_binary_field(mesh, divisions)` - Create inside/outside mask -### Field Layout -- `create_lagrange_layout(mesh, order, num_components, coord_system)` - Omega_h field layout -- `UniformGridFieldLayout2D(grid, num_components, coord_system)` - 2D grid layout -- `UniformGridFieldLayout3D(grid, num_components, coord_system)` - 3D grid layout +### Field Factories +- `FunctionSpace` - abstract base class for field spaces in the Python API +- `LagrangeFunctionSpace.from_mesh(mesh, order, num_components, coord_system)` - create an Omega_h-backed FunctionSpace +- `LagrangeFunctionSpace.from_uniform_grid(grid, num_components, coord_system, order=1)` - create a uniform-grid-backed FunctionSpace ### Field Operations -- `layout.create_field()` - Create field from layout +- `space.create_field()` - Create a real-valued field from a concrete FunctionSpace +- `EvaluationRequest.from_coordinates(coords, coord_system=Cartesian, policy=...)` - Build an explicit coordinate-based evaluation request +- `EvaluationRequest.from_function_space(space, policy=...)` - Build an evaluation request from another FunctionSpace's DOF-holder sites +- `space.create_point_evaluator(request)` - Create a reusable point evaluator from an `EvaluationRequest` +- `field.get_num_dof_holders()` - Number of owned DOF holders (nodes/elements) +- `field.get_num_components()` - Number of field components per DOF holder +- `field.get_dof_holder_coordinates()` - DOF holder coordinates as a 2D numpy array - `field.set_dof_holder_data(data)` - Set field values - `field.get_dof_holder_data()` - Get field values -- `field.to_mdspan()` - Get field values as a structured array -- `field.set_out_of_bounds_mode(mode, fill_value=0.0)` - Set behavior for points outside mesh -- `interpolate_field(source_field, target_field)` - Interpolate between fields +- `Interpolator(source_space, target_space)` - Create an interpolator between FunctionSpaces (cached localization) +- `interpolator.apply(source_field, target_field)` - Interpolate between fields +- `Copy(source_space, target_space)` - Create a copy operator for compatible FunctionSpaces - `map_entity_field_to_vertices_average(mesh, field_data, entity_dim)` - Convert element/face field to vertex field by averaging ### Out-of-Bounds Modes @@ -295,8 +299,8 @@ np.save('field_data.npy', grid_values) ### Grid Properties - `grid.get_num_cells()` - Total number of cells - `grid.divisions` - Cell divisions in each dimension -- `layout.get_num_vertices()` - Total number of vertices -- `layout.get_dof_holder_coordinates()` - Vertex coordinates +- `field.get_num_dof_holders()` - Total number of field DOF holders +- `field.get_dof_holder_coordinates()` - DOF holder coordinates --- diff --git a/src/pcms/transfer/CMakeLists.txt b/src/pcms/transfer/CMakeLists.txt new file mode 100644 index 000000000..216b5759a --- /dev/null +++ b/src/pcms/transfer/CMakeLists.txt @@ -0,0 +1,87 @@ +set(PCMS_FIELD_TRANSFER_HEADERS + linear_interpolant.hpp + multidimarray.hpp + interpolation_base.h + interpolation_helpers.h + mesh_intersection.hpp + interpolator.h + copy.h + transfer_operator.hpp +) + + +set(PCMS_FIELD_TRANSFER_SOURCES + mesh_intersection.cpp + interpolation_base.cpp) + + +if(PCMS_ENABLE_MESHFIELDS) + list(APPEND PCMS_FIELD_TRANSFER_HEADERS + load_vector_integrator.hpp + mass_matrix_integrator.hpp) + list(APPEND PCMS_FIELD_TRANSFER_SOURCES + load_vector_integrator.cpp) +endif() + +if(PCMS_ENABLE_PETSC AND PCMS_ENABLE_MESHFIELDS) + list(APPEND PCMS_FIELD_TRANSFER_HEADERS + calculate_load_vector.hpp + calculate_mass_matrix.hpp + conservative_projection_solver.hpp + omega_h_conservative_projection.hpp + coo_assembly_utils.hpp + petsc_utils.hpp) + list(APPEND PCMS_FIELD_TRANSFER_SOURCES + calculate_load_vector.cpp + calculate_mass_matrix.cpp + conservative_projection_solver.cpp + omega_h_conservative_projection.cpp + petsc_utils.cpp + coo_assembly_utils.cpp) +endif() + +add_library(pcms_transfer ${PCMS_FIELD_TRANSFER_SOURCES}) +set_target_properties(pcms_transfer PROPERTIES + OUTPUT_NAME pcmstransfer + EXPORT_NAME transfer) +target_sources(pcms_transfer PUBLIC + FILE_SET transfer + TYPE HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. + FILES ${PCMS_FIELD_TRANSFER_HEADERS}) + +add_library(pcms::transfer ALIAS pcms_transfer) +target_compile_features(pcms_transfer PUBLIC cxx_std_20) + +target_link_libraries(pcms_transfer PUBLIC + pcms::core + pcms::field + pcms::localization + Omega_h::omega_h) + +if(PCMS_ENABLE_MESHFIELDS) + target_link_libraries(pcms_transfer PUBLIC meshfields::meshfields) +endif() + +if(PCMS_ENABLE_PETSC AND PCMS_ENABLE_MESHFIELDS) + target_link_libraries(pcms_transfer PRIVATE PETSc::PETSc) +endif() + +target_compile_definitions(pcms_transfer PUBLIC R3D_USE_KOKKOS) + +target_include_directories(pcms_transfer INTERFACE + $ + $ + $) + +install( + TARGETS pcms_transfer + EXPORT pcms_transfer-targets + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms/transfer + FILE_SET transfer DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +install( + EXPORT pcms_transfer-targets + NAMESPACE pcms:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) diff --git a/src/pcms/transfer/calculate_load_vector.cpp b/src/pcms/transfer/calculate_load_vector.cpp new file mode 100644 index 000000000..f351a4ef1 --- /dev/null +++ b/src/pcms/transfer/calculate_load_vector.cpp @@ -0,0 +1,52 @@ +#include "pcms/transfer/calculate_load_vector.hpp" +#include "pcms/transfer/coo_assembly_utils.hpp" +#include "pcms/transfer/load_vector_integrator.hpp" +#include "pcms/transfer/petsc_utils.hpp" + +namespace pcms +{ +PetscErrorCode calculateLoadVector(Omega_h::Mesh& target_mesh, + Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, + const Omega_h::Reals& source_values, + Vec* loadVec_out) +{ + + PetscFunctionBeginUser; + PetscInt nnz = 0; + PetscInt* coo_i = nullptr; + PetscCall(build_linear_triangle_coo_rows(target_mesh, &coo_i, &nnz)); + PetscScalar* coo_vals = nullptr; + PetscCall(PetscMalloc1(nnz, &coo_vals)); + + // Fill COO values + auto elmLoadVector = + buildLoadVector(target_mesh, source_mesh, intersection, source_values); + + auto hostElmLoadVector = Kokkos::create_mirror_view(elmLoadVector); + Kokkos::deep_copy(hostElmLoadVector, elmLoadVector); + PetscCheck(static_cast(hostElmLoadVector.extent(0)) == nnz, + PETSC_COMM_SELF, PETSC_ERR_ARG_SIZ, + "Element load vector size (%d) does not match COO nnz (%d)", + static_cast(hostElmLoadVector.extent(0)), nnz); + + for (PetscInt e = 0; e < nnz; ++e) { + coo_vals[e] = hostElmLoadVector(e); + } + + // create vector with preallocated COO structure + Vec vec; + PetscCall(createSeqVec(PETSC_COMM_WORLD, target_mesh.nverts(), &vec)); + PetscCall(VecSetPreallocationCOO(vec, nnz, coo_i)); + PetscCall(VecSetValuesCOO(vec, coo_vals, ADD_VALUES)); + PetscCall(PetscFree(coo_i)); + PetscCall(PetscFree(coo_vals)); + + if (target_mesh.nelems() < 10) { + PetscCall(VecView(vec, PETSC_VIEWER_STDOUT_WORLD)); + } + + *loadVec_out = vec; + PetscFunctionReturn(PETSC_SUCCESS); +} +} // namespace pcms diff --git a/src/pcms/transfer/calculate_load_vector.hpp b/src/pcms/transfer/calculate_load_vector.hpp new file mode 100644 index 000000000..b19683389 --- /dev/null +++ b/src/pcms/transfer/calculate_load_vector.hpp @@ -0,0 +1,59 @@ +/** + * @file calculate_load_vector.hpp + * @brief Routines for assembling global load vector in conservative field + * projection. + * + * Provides functionality to compute and assemble the global load vector + * used in Galerkin-based conservative field transfer between non-conforming + * meshes. + * + */ + +#ifndef PCMS_TRANSFER_CALCULATE_LOAD_VECTOR_HPP +#define PCMS_TRANSFER_CALCULATE_LOAD_VECTOR_HPP +#include +#include +#include + +/** + * @brief Assembles the global load vector. + * + * This function computes the unassembled local load vector contributions for + * each triangular element in the target mesh using `buildLoadVector()` and then + * assembles them into a global PETSc vector in COO format. + * + * + * @param target_mesh The target Omega_h mesh to which the scalar field is being + * projected. + * @param source_mesh The source Omega_h mesh containing the original scalar + * field values. + * @param intersection Precomputed intersection data for each target element. + * Includes the number and indices of intersecting source + * elements. + * @param source_values Nodal scalar field values defined on the source mesh. + * @param[out] loadVec_out Pointer to a PETSc Vec where the assembled load + * vector will be stored. + * + * @return PetscErrorCode Returns PETSC_SUCCESS if successful, or an appropriate + * PETSc error code otherwise. + * + * @note + * - Works for 2D linear triangular elements. + * - Uses COO-style preallocation and insertion into the PETSc vector. + * - Internally calls `buildLoadVector()` to compute per-element contributions. + * - The resulting vector is used as the right-hand side (RHS) in a projection + * solve. + * + * @see buildLoadVector,IntersectionResults + */ + +namespace pcms +{ +// FIXME use PCMS error handling rather than returning a PETSC error code +PetscErrorCode calculateLoadVector(Omega_h::Mesh& target_mesh, + Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, + const Omega_h::Reals& source_values, + Vec* loadVec_out); +} // namespace pcms +#endif // PCMS_TRANSFER_CALCULATE_LOAD_VECTOR_HPP diff --git a/src/pcms/transfer/calculate_mass_matrix.cpp b/src/pcms/transfer/calculate_mass_matrix.cpp new file mode 100644 index 000000000..2b54e4c6d --- /dev/null +++ b/src/pcms/transfer/calculate_mass_matrix.cpp @@ -0,0 +1,74 @@ +#include "pcms/transfer/calculate_mass_matrix.hpp" +#include "pcms/transfer/coo_assembly_utils.hpp" +#include "pcms/transfer/mass_matrix_integrator.hpp" +#include "pcms/transfer/petsc_utils.hpp" +#include "pcms/utility/memory_spaces.h" + +namespace pcms +{ +/** + * @brief Creates a PETSc matrix based on mesh connectivity + * + * This function creates a sparse matrix with the proper sparsity pattern + * according to the mesh connectivity. The matrix size corresponds to the + * number of vertices in the mesh. + * + * @param mesh The Omega_h mesh to create the matrix from + * @param[out] A Pointer to the PETSc matrix to be created + * @return PetscErrorCode PETSc error code (PETSC_SUCCESS if successful) + */ +static PetscErrorCode create_linear_triangle_coo_matrix(Omega_h::Mesh& mesh, + Mat* A) +{ + PetscInt* coo_rows = nullptr; + PetscInt* coo_cols = nullptr; + PetscInt matSize = 0; + PetscFunctionBeginUser; + PetscCall(createSeqAIJMat(PETSC_COMM_WORLD, mesh.nverts(), mesh.nverts(), 0, + nullptr, A)); + PetscCall( + build_linear_triangle_coo_rows_cols(mesh, &coo_rows, &coo_cols, &matSize)); + PetscCall(MatSetPreallocationCOO(*A, matSize, coo_rows, coo_cols)); + PetscCall(PetscFree2(coo_rows, coo_cols)); + PetscFunctionReturn(PETSC_SUCCESS); +} +PetscErrorCode calculateMassMatrix(Omega_h::Mesh& mesh, Mat* mass_out) +{ + PetscFunctionBeginUser; + + MeshField::OmegahMeshField + omf(mesh); + + const auto ShapeOrder = 1; + auto coordField = omf.getCoordField(); + const auto [shp, map] = + MeshField::Omegah::getTriangleElement(mesh); + MeshField::FieldElement coordFe(mesh.nelems(), coordField, shp, map); + + auto elmMassMatrix = buildMassMatrix(mesh, coordFe); + const PetscInt expected_nnz = 9 * mesh.nelems(); + PetscCheck(static_cast(elmMassMatrix.extent(0)) == expected_nnz, + PETSC_COMM_SELF, PETSC_ERR_ARG_SIZ, + "Element mass data size (%d) does not match expected linear COO " + "entries (%d)", + static_cast(elmMassMatrix.extent(0)), expected_nnz); + + Mat mass; + PetscCall(create_linear_triangle_coo_matrix(mesh, &mass)); + PetscCall(MatZeroEntries(mass)); + PetscCall( + MatSetValuesCOO(mass, elmMassMatrix.data(), + INSERT_VALUES)); // FIXME fails here on gpu, calls into host + // implementation... AFAIK, petsc checks + // the type of the input array of values to + // decide which backend to use... + // + if (mesh.nelems() < 10) { + PetscCall(MatView(mass, PETSC_VIEWER_STDOUT_WORLD)); + } + + *mass_out = mass; + PetscFunctionReturn(PETSC_SUCCESS); +} +} // namespace pcms diff --git a/src/pcms/transfer/calculate_mass_matrix.hpp b/src/pcms/transfer/calculate_mass_matrix.hpp new file mode 100644 index 000000000..0889fdc8f --- /dev/null +++ b/src/pcms/transfer/calculate_mass_matrix.hpp @@ -0,0 +1,34 @@ +/** + * @file calculateMassMatrix.hpp + * @brief Functions for calculating mass matrices on finite element meshes + * @author [Cameron Smith] + * @date April, 2025 + * + * This file contains functions for creating and computing mass matrices + * for finite element calculations using Omega_h mesh structures and PETSc. + */ + +#ifndef PCMS_TRANSFER_CALCULATE_MASS_MATRIX_HPP +#define PCMS_TRANSFER_CALCULATE_MASS_MATRIX_HPP + +#include +#include + +namespace pcms +{ +/** + * @brief Calculates the mass matrix for a given mesh + * + * This function constructs a mass matrix based on the provided mesh using + * a finite element approach. It creates coordinate field elements, builds + * the mass matrix using the massMatrixIntegrator, and sets up the PETSc matrix + * with appropriate values. + * + * @param mesh The Omega_h mesh to calculate the mass matrix for + * @param[out] mass_out Pointer to the resulting mass matrix + * @return PetscErrorCode PETSc error code (PETSC_SUCCESS if successful) + */ + +PetscErrorCode calculateMassMatrix(Omega_h::Mesh& mesh, Mat* mass_out); +} // namespace pcms +#endif // PCMS_TRANSFER_CALCULATE_MASS_MATRIX_HPP diff --git a/src/pcms/transfer/conservative_projection_solver.cpp b/src/pcms/transfer/conservative_projection_solver.cpp new file mode 100644 index 000000000..148454f84 --- /dev/null +++ b/src/pcms/transfer/conservative_projection_solver.cpp @@ -0,0 +1,144 @@ +#include + +#include "pcms/transfer/conservative_projection_solver.hpp" +#include "pcms/transfer/petsc_utils.hpp" +#include "pcms/transfer/calculate_load_vector.hpp" +#include "pcms/transfer/calculate_mass_matrix.hpp" + +namespace pcms +{ +/** + * @brief Solves a linear system Ax = b using PETSc's KSP solvers + * + * Uses PETSc's Krylov Subspace solvers to find x in Ax = b. + * The solver can be configured through PETSc runtime options. + * + * @param A The system matrix + * @param b The right-hand side vector + * @return Vec Solution vector x + */ +static Vec solveLinearSystem(Mat A, Vec b) +{ + PetscInt m, n; + PetscErrorCode ierr; + + ierr = MatGetSize(A, &m, &n); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + Vec x; + ierr = createSeqVec(PETSC_COMM_WORLD, n, &x); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + KSP ksp; + ierr = KSPCreate(PETSC_COMM_WORLD, &ksp); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + ierr = KSPSetOperators(ksp, A, A); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + ierr = KSPSetComputeSingularValues(ksp, PETSC_TRUE); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + ierr = KSPSetFromOptions(ksp); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + ierr = KSPSetUp(ksp); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + ierr = KSPSolve(ksp, b, x); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + /// compute and print condition number estimate + PetscReal smax = 0.0, smin = 0.0; + ierr = KSPComputeExtremeSingularValues(ksp, &smax, &smin); + if (!ierr && smin > 0.0) { + PetscPrintf(PETSC_COMM_WORLD, + "Estimated condition number of matrix A: %.6e\n", smax / smin); + } else { + PetscPrintf(PETSC_COMM_WORLD, + "Condition number estimate unavailable (smin <= 0 or error)\n"); + } + + ierr = KSPDestroy(&ksp); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + return x; +} + +static Omega_h::Reals vecToOmegaHReals(Vec vec) +{ + PetscInt n = 0; + PetscErrorCode ierr = VecGetSize(vec, &n); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + const PetscScalar* array = nullptr; + ierr = VecGetArrayRead(vec, &array); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + auto values_host = Omega_h::HostWrite(n); + for (PetscInt i = 0; i < n; ++i) { + values_host[i] = array[i]; + } + + ierr = VecRestoreArrayRead(vec, &array); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + return Omega_h::Reals(values_host); +} + +Omega_h::Reals solveGalerkinProjection(Omega_h::Mesh& target_mesh, + Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, + const Omega_h::Reals& source_values) +{ + if ((PetscInt)source_values.size() != + source_mesh.coords().size() / source_mesh.dim()) { + std::cerr << "ERROR: source_values size (" << source_values.size() + << ") doesn't match expected size (" + << source_mesh.coords().size() / source_mesh.dim() << ")" + << std::endl; + throw std::runtime_error("source_values length mismatch"); + } + + Mat mass; + PetscErrorCode ierr = calculateMassMatrix(target_mesh, &mass); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + Vec vec; + ierr = calculateLoadVector(target_mesh, source_mesh, intersection, + source_values, &vec); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + Vec x = solveLinearSystem(mass, vec); + auto solution_vector = vecToOmegaHReals(x); + + ierr = VecDestroy(&x); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + ierr = MatDestroy(&mass); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + ierr = VecDestroy(&vec); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + return solution_vector; +} +Omega_h::Reals rhsVectorMI(Omega_h::Mesh& target_mesh, + Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, + const Omega_h::Reals& source_values) +{ + Vec vec; + PetscErrorCode ierr; + ierr = calculateLoadVector(target_mesh, source_mesh, intersection, + source_values, &vec); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + auto rhsvector = vecToOmegaHReals(vec); + + ierr = VecDestroy(&vec); + CHKERRABORT(PETSC_COMM_WORLD, ierr); + + return rhsvector; +} +} // namespace pcms diff --git a/src/pcms/transfer/conservative_projection_solver.hpp b/src/pcms/transfer/conservative_projection_solver.hpp new file mode 100644 index 000000000..d3e46a373 --- /dev/null +++ b/src/pcms/transfer/conservative_projection_solver.hpp @@ -0,0 +1,72 @@ +/** + * @file conservative_projection_solver.hpp + * @brief Solves the conservative projection of scalar fields between + * non-matching meshes. + * + * Provides the main interface to perform Galerkin projection of scalar fields + * from a source mesh to a target mesh using conservative transfer using a + * supermesh generated from mesh intersections. + * + * The solver computes the right-hand side (load vector), assembles the mass + * matrix, and solves the resulting linear system to obtain projected nodal + * values. + * + */ + +#ifndef PCMS_TRANSFER_CONSERVATIVE_PROJECTION_SOLVER_HPP +#define PCMS_TRANSFER_CONSERVATIVE_PROJECTION_SOLVER_HPP + +#include +#include + +#include + +namespace pcms +{ + +/** + * @brief Solves a conservative galerkin projection problem to transfer scalar + * field values onto a target mesh. + * + * This function assembles and solves a linear system of the form: + * \f[ + * M \cdot x = f + * \f] + * where: + * - \f$M\f$ is the mass matrix on the target mesh (based on P1 finite + * elements), + * - \f$f\f$ is the load vector computed on the supermesh, + * - \f$x\f$ is the unknown nodal field on the target mesh (solution). + * + * The method computes the conservative field transfer between two non-matching + * meshes using mesh intersections (supermesh). + * + * ### Algorithm Steps: + * 1. Compute and assemble mass matrix and load vector + * 2. Solve the linear system using PETSc. + * 3. Return the solution as a nodal field on the target mesh. + * + * @param target_mesh The Omega_h mesh where the field is projected. + * @param source_mesh The Omega_h mesh containing the original field data. + * @param intersection Precomputed intersection information between source and + * target meshes. + * @param source_values Nodal scalar field values on the source mesh. + * + * @return A vector of nodal values on the target mesh after projection + * (Omega_h::Reals). + * + * + */ + +Omega_h::Reals solveGalerkinProjection(Omega_h::Mesh& target_mesh, + Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, + const Omega_h::Reals& source_values); + +Omega_h::Reals rhsVectorMI(Omega_h::Mesh& target_mesh, + Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, + const Omega_h::Reals& source_values); +} // namespace pcms + +#endif // PCMS_TRANSFER_CONSERVATIVE_PROJECTION_SOLVER_HPP diff --git a/src/pcms/transfer/coo_assembly_utils.cpp b/src/pcms/transfer/coo_assembly_utils.cpp new file mode 100644 index 000000000..601889bce --- /dev/null +++ b/src/pcms/transfer/coo_assembly_utils.cpp @@ -0,0 +1,93 @@ +#include "pcms/transfer/coo_assembly_utils.hpp" + +namespace pcms +{ +namespace +{ +constexpr PetscInt num_nodes_per_tri = 3; +} + +PetscErrorCode build_linear_triangle_coo_rows(Omega_h::Mesh& mesh, + PetscInt** rows_out, + PetscInt* nnz_out) +{ + PetscFunctionBeginUser; + PetscCheck(rows_out != nullptr, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, + "rows_out must not be null"); + PetscCheck(nnz_out != nullptr, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, + "nnz_out must not be null"); + PetscCheck(mesh.dim() == 2, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, + "Expected 2D mesh, got dim=%d", mesh.dim()); + PetscCheck(mesh.family() == OMEGA_H_SIMPLEX, PETSC_COMM_SELF, + PETSC_ERR_ARG_WRONG, + "Expected simplex mesh family for linear-triangle COO assembly"); + + const PetscInt nnz = mesh.nelems() * num_nodes_per_tri; + + auto elm_verts = Omega_h::HostRead(mesh.ask_elem_verts()); + PetscInt* rows = nullptr; + PetscCall(PetscMalloc1(nnz, &rows)); + + PetscInt idx = 0; + for (PetscInt e = 0; e < mesh.nelems(); ++e) { + for (PetscInt vi = 0; vi < num_nodes_per_tri; ++vi) { + rows[idx++] = elm_verts[num_nodes_per_tri * e + vi]; + } + } + PetscCheck(idx == nnz, PETSC_COMM_SELF, PETSC_ERR_PLIB, + "COO row fill count mismatch: idx=%d nnz=%d", idx, nnz); + + *rows_out = rows; + *nnz_out = nnz; + PetscFunctionReturn(PETSC_SUCCESS); +} + +PetscErrorCode build_linear_triangle_coo_rows_cols(Omega_h::Mesh& mesh, + PetscInt** rows_out, + PetscInt** cols_out, + PetscInt* nnz_out) +{ + PetscFunctionBeginUser; + PetscCheck(rows_out != nullptr, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, + "rows_out must not be null"); + PetscCheck(cols_out != nullptr, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, + "cols_out must not be null"); + PetscCheck(nnz_out != nullptr, PETSC_COMM_SELF, PETSC_ERR_ARG_NULL, + "nnz_out must not be null"); + PetscCheck(mesh.dim() == 2, PETSC_COMM_SELF, PETSC_ERR_ARG_WRONG, + "Expected 2D mesh, got dim=%d", mesh.dim()); + PetscCheck(mesh.family() == OMEGA_H_SIMPLEX, PETSC_COMM_SELF, + PETSC_ERR_ARG_WRONG, + "Expected simplex mesh family for linear-triangle COO assembly"); + + const PetscInt nnz = mesh.nelems() * num_nodes_per_tri * num_nodes_per_tri; + + auto elm_verts = Omega_h::HostRead(mesh.ask_elem_verts()); + PetscInt* rows = nullptr; + PetscInt* cols = nullptr; + PetscCall(PetscMalloc2(nnz, &rows, nnz, &cols)); + + /* determine for each entry in each element stiffness matrix the global row + * and column */ + /* since the element is triangular with piecewise linear basis functions + * there are three degrees of freedom per element, one for each vertex */ + PetscInt idx = 0; + for (PetscInt e = 0; e < mesh.nelems(); ++e) { + for (PetscInt vi = 0; vi < num_nodes_per_tri; ++vi) { + for (PetscInt vj = 0; vj < num_nodes_per_tri; ++vj) { + rows[idx] = elm_verts[num_nodes_per_tri * e + vi]; + cols[idx] = elm_verts[num_nodes_per_tri * e + vj]; + ++idx; + } + } + } + PetscCheck(idx == nnz, PETSC_COMM_SELF, PETSC_ERR_PLIB, + "COO row/col fill count mismatch: idx=%d nnz=%d", idx, nnz); + + *rows_out = rows; + *cols_out = cols; + *nnz_out = nnz; + PetscFunctionReturn(PETSC_SUCCESS); +} + +} // namespace pcms diff --git a/src/pcms/transfer/coo_assembly_utils.hpp b/src/pcms/transfer/coo_assembly_utils.hpp new file mode 100644 index 000000000..2df62fe59 --- /dev/null +++ b/src/pcms/transfer/coo_assembly_utils.hpp @@ -0,0 +1,28 @@ +#ifndef PCMS_TRANSFER_COO_ASSEMBLY_UTILS_HPP +#define PCMS_TRANSFER_COO_ASSEMBLY_UTILS_HPP + +#include +#include + +namespace pcms +{ + +// Builds COO row indices for linear triangles (3 vertices per element). +// Order is deterministic by (element, local_vertex). +// Allocates `rows_out` with PetscMalloc1; caller owns and must PetscFree. +PetscErrorCode build_linear_triangle_coo_rows(Omega_h::Mesh& mesh, + PetscInt** rows_out, + PetscInt* nnz_out); + +// Builds COO row/column index pairs for linear-triangle element matrices. +// Order is deterministic by (element, local_row_vertex, local_col_vertex). +// Allocates `rows_out` and `cols_out` with PetscMalloc2; caller owns and must +// PetscFree2. +PetscErrorCode build_linear_triangle_coo_rows_cols(Omega_h::Mesh& mesh, + PetscInt** rows_out, + PetscInt** cols_out, + PetscInt* nnz_out); + +} // namespace pcms + +#endif // PCMS_TRANSFER_COO_ASSEMBLY_UTILS_HPP diff --git a/src/pcms/transfer/copy.h b/src/pcms/transfer/copy.h new file mode 100644 index 000000000..f650fed51 --- /dev/null +++ b/src/pcms/transfer/copy.h @@ -0,0 +1,61 @@ +#ifndef PCMS_TRANSFER_FIELD2_H_ +#define PCMS_TRANSFER_FIELD2_H_ +#include "pcms/field/field.h" +#include "pcms/field/field_data.h" +#include "pcms/field/function_space.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/profile.h" +#include "pcms/transfer/transfer_operator.hpp" + +namespace pcms +{ + +namespace detail +{ + +inline bool CompatibleMetadata(const FieldMetadata& source, + const FieldMetadata& target) noexcept +{ + return source.value_type == target.value_type && + source.value_coordinate_system == target.value_coordinate_system; +} + +template +void CheckCopyCompatible(const Field& source, const Field& target) +{ + if (&source.GetLayout() != &target.GetLayout()) { + throw pcms_error("Copy: source and target layouts differ"); + } + if (!CompatibleMetadata(source.GetData().GetMetadata(), + target.GetData().GetMetadata())) { + throw pcms_error("Copy: source and target metadata differ"); + } +} + +} // namespace detail + +template +class Copy : public TransferOperator +{ +public: + Copy(const FunctionSpace& source_space, const FunctionSpace& target_space) + { + auto source_layout = source_space.GetLayout(); + auto target_layout = target_space.GetLayout(); + if (source_layout.get() != target_layout.get()) { + throw pcms_error("Copy: source and target function spaces have " + "different layouts"); + } + } + + void Apply(const Field& source, Field& target) const override + { + PCMS_FUNCTION_TIMER; + detail::CheckCopyCompatible(source, target); + target.SetDOFHolderDataHost(source.GetDOFHolderDataHost()); + } +}; + +} // namespace pcms + +#endif // PCMS_TRANSFER_FIELD2_H_ diff --git a/src/pcms/transfer/field_compatibility.hpp b/src/pcms/transfer/field_compatibility.hpp new file mode 100644 index 000000000..c60178a49 --- /dev/null +++ b/src/pcms/transfer/field_compatibility.hpp @@ -0,0 +1,41 @@ +#ifndef PCMS_TRANSFER_FIELD_COMPATIBILITY_H +#define PCMS_TRANSFER_FIELD_COMPATIBILITY_H + +#include "pcms/discretization/discretization.h" +#include "pcms/field/field.h" +#include "pcms/field/field_layout.h" +#include "pcms/utility/common.h" +#include + +namespace pcms +{ +namespace detail +{ + +// Verify that a field handed to a TransferOperator::Apply belongs to the same +// space the operator was constructed from. +// Throws pcms_error on mismatch. +template +inline void CheckTransferFieldLayout(const Field& field, + const FieldLayout& expected, + const char* role) +{ + const FieldLayout& actual = field.GetLayout(); + if (&actual == &expected) { + return; + } + auto actual_disc = actual.GetDiscretization(); + auto expected_disc = expected.GetDiscretization(); + if (actual_disc && expected_disc && + actual_disc->SameEntities(*expected_disc)) { + return; + } + throw pcms_error(std::string("TransferOperator: ") + role + + " field does not match the function space the operator was " + "constructed from"); +} + +} // namespace detail +} // namespace pcms + +#endif // PCMS_TRANSFER_FIELD_COMPATIBILITY_H diff --git a/src/pcms/interpolator/interpolation_base.cpp b/src/pcms/transfer/interpolation_base.cpp similarity index 79% rename from src/pcms/interpolator/interpolation_base.cpp rename to src/pcms/transfer/interpolation_base.cpp index 70d10115b..4112ba27e 100644 --- a/src/pcms/interpolator/interpolation_base.cpp +++ b/src/pcms/transfer/interpolation_base.cpp @@ -4,30 +4,11 @@ #include "interpolation_base.h" #include "interpolation_helpers.h" +#include "pcms/utility/mesh_geometry.h" #include - -Omega_h::Reals getCentroids(Omega_h::Mesh& mesh) +namespace pcms { - OMEGA_H_CHECK_PRINTF( - mesh.dim() == 2, "Only 2D meshes are supported but found %d\n", mesh.dim()); - - const auto& coords = mesh.coords(); - Omega_h::Write centroids(mesh.nfaces() * mesh.dim(), 0.0); - - auto face2node = mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; - Omega_h::parallel_for( - mesh.nfaces(), OMEGA_H_LAMBDA(Omega_h::LO face) { - auto nodes = Omega_h::gather_verts<3>(face2node, face); - Omega_h::Few, 3> face_coords = - Omega_h::gather_vectors<3, 2>(coords, nodes); - Omega_h::Vector<2> centroid = Omega_h::average(face_coords); - centroids[2 * face + 0] = centroid[0]; - centroids[2 * face + 1] = centroid[1]; - }); - - return {centroids}; -} MLSMeshInterpolation::MLSMeshInterpolation(Omega_h::Mesh& source_mesh, double radius, @@ -45,11 +26,11 @@ MLSMeshInterpolation::MLSMeshInterpolation(Omega_h::Mesh& source_mesh, { single_mesh_ = true; target_coords_ = source_mesh_.coords(); - source_coords_ = getCentroids(source_mesh_); OMEGA_H_CHECK_PRINTF(source_mesh_.dim() == 2, "Only 2D meshes are supported but found %d\n", source_mesh_.dim()); + source_coords_ = pcms::get_entity_centroids(source_mesh_, Omega_h::FACE); source_field_ = Omega_h::HostWrite(source_mesh_.nfaces(), "source field"); @@ -88,62 +69,6 @@ MLSMeshInterpolation::MLSMeshInterpolation( find_supports(min_req_supports_, 3 * min_req_supports_); } -KOKKOS_INLINE_FUNCTION -double pointDistanceSquared(const double x1, const double y1, const double z1, - const double x2, const double y2, const double z2) -{ - return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2); -} - -// replace with Kokkos::minmax_element when out of experimental -// https://kokkos.org/kokkos-core-wiki/API/algorithms/std-algorithms/all/StdMinMaxElement.html -void minmax(Omega_h::Read num_supports, - unsigned& min_supports_found, unsigned& max_supports_found) -{ - using minMaxReducerType = Kokkos::MinMax; - using minMaxValueType = minMaxReducerType::value_type; - minMaxValueType minmax; - Kokkos::parallel_reduce( - num_supports.size(), - KOKKOS_LAMBDA(int i, minMaxValueType& update) { - if (num_supports[i] < update.min_val) - update.min_val = num_supports[i]; - if (num_supports[i] > update.max_val) - update.max_val = num_supports[i]; - }, - minMaxReducerType(minmax)); - Kokkos::fence(); - min_supports_found = minmax.min_val; - max_supports_found = minmax.max_val; -} - -void adapt_radii(unsigned min_req_supports, unsigned max_allowed_supports, - Omega_h::LO n_targets, Omega_h::Write radii2_l, - Omega_h::Write num_supports) -{ - Omega_h::parallel_for( - "increase radius", n_targets, OMEGA_H_LAMBDA(const int& i) { - Omega_h::LO nsupports = num_supports[i]; - if (nsupports < min_req_supports) { - double factor = - Omega_h::Real(min_req_supports) / Omega_h::Real(nsupports); - OMEGA_H_CHECK_PRINTF(factor > 1.0, - "Factor should be more than 1.0: %f\n", factor); - factor = (nsupports == 0 || factor > 1.5) ? 1.5 : factor; - radii2_l[i] *= factor; - } else if (nsupports > max_allowed_supports) { // if too many supports - double factor = - Omega_h::Real(min_req_supports) / Omega_h::Real(nsupports); - OMEGA_H_CHECK_PRINTF(factor < 1.0, - "Factor should be less than 1.0: %f\n", factor); - factor = (factor < 0.1) ? 0.33 : factor; - radii2_l[i] *= factor; - } - num_supports[i] = 0; // reset the support pointer - }); - Kokkos::fence(); -} - struct ScanSupportIdxFunctor { Omega_h::Write support_ptr_l; @@ -165,6 +90,15 @@ struct ScanSupportIdxFunctor } }; +KOKKOS_INLINE_FUNCTION +Omega_h::Vector<3> load_point(const Omega_h::Reals& coords, int id, int dim) +{ + Omega_h::Vector<3> p{0, 0, 0}; + for (int d = 0; d < dim; ++d) + p[d] = coords[id * dim + d]; + return p; +} + struct FillSupportIdxFunctor { const int dim; @@ -195,22 +129,15 @@ struct FillSupportIdxFunctor void operator()(const int& target_id) const { auto target_radius2 = radii2_l[target_id]; - auto target_coord = Omega_h::Vector<3>{0, 0, 0}; - for (int d = 0; d < dim; ++d) { - target_coord[d] = target_coords_l[target_id * dim + d]; - } + auto target_coord = load_point(target_coords_l, target_id, dim); auto start_ptr = support_ptr_l[target_id]; auto end_ptr = support_ptr_l[target_id + 1]; for (int source_id = 0; source_id < n_sources; source_id++) { - auto source_coord = Omega_h::Vector<3>{0, 0, 0}; - for (int d = 0; d < dim; ++d) { - source_coord[d] = source_coords_l[source_id * dim + d]; - } + auto source_coord = load_point(source_coords_l, source_id, dim); auto dist2 = - pointDistanceSquared(source_coord[0], source_coord[1], source_coord[2], - target_coord[0], target_coord[1], target_coord[2]); + pcms::distance_squared(&source_coord[0], &target_coord[0], dim); if (dist2 <= target_radius2) { supports_idx_l[start_ptr] = source_id; start_ptr++; @@ -284,21 +211,14 @@ struct NSquareSearchFunctor KOKKOS_INLINE_FUNCTION void operator()(const int& target_id) const { - auto target_coord = Omega_h::Vector<3>{0, 0, 0}; - for (int d = 0; d < dim; ++d) { - target_coord[d] = target_coords_l[target_id * dim + d]; - } + auto target_coord = load_point(target_coords_l, target_id, dim); auto target_radius2 = radii2_l[target_id]; // TODO: parallel with kokkos parallel_for for (int i = 0; i < n_sources; i++) { - auto source_coord = Omega_h::Vector<3>{0, 0, 0}; - for (int d = 0; d < dim; ++d) { - source_coord[d] = source_coords_l[i * dim + d]; - } + auto source_coord = load_point(source_coords_l, i, dim); auto dist2 = - pointDistanceSquared(source_coord[0], source_coord[1], source_coord[2], - target_coord[0], target_coord[1], target_coord[2]); + pcms::distance_squared(&source_coord[0], &target_coord[0], dim); if (dist2 <= target_radius2) { num_supports_l[target_id]++; // only one thread is updating } @@ -474,8 +394,8 @@ void MLSPointCloudInterpolation::eval( // TODO: make the basis function a template or pass it as a parameter auto target_field_write = mls_interpolation( - Omega_h::Reals(source_field_), source_coords_, target_coords_, supports_, 2, - degree_, pcms::RadialBasisFunction::RBF_GAUSSIAN, lambda_, 1e-6, + Omega_h::Reals(source_field_), source_coords_, target_coords_, supports_, + dim_, degree_, pcms::RadialBasisFunction::RBF_GAUSSIAN, lambda_, 1e-6, decay_factor_); target_field_ = Omega_h::HostWrite(target_field_write); @@ -548,3 +468,4 @@ size_t MLSMeshInterpolation::getTargetSize() const return target_mesh_.nverts(); } } +} // namespace pcms diff --git a/src/pcms/interpolator/interpolation_base.h b/src/pcms/transfer/interpolation_base.h similarity index 97% rename from src/pcms/interpolator/interpolation_base.h rename to src/pcms/transfer/interpolation_base.h index c891d458d..80d946c5a 100644 --- a/src/pcms/interpolator/interpolation_base.h +++ b/src/pcms/transfer/interpolation_base.h @@ -9,16 +9,18 @@ * */ -#ifndef PCMS_INTERPOLATION_BASE_H -#define PCMS_INTERPOLATION_BASE_H +#ifndef PCMS_TRANSFER_INTERPOLATION_BASE_H +#define PCMS_TRANSFER_INTERPOLATION_BASE_H -#include "mls_interpolation.hpp" -#include "adj_search.hpp" +#include "pcms/field/evaluator/mls_interpolation.hpp" +#include "pcms/localization/adj_search.hpp" #include "interpolation_helpers.h" #include #include "pcms/utility/arrays.h" #include +namespace pcms +{ /** * @brief Pure virtual base class for interpolation methods * @details Provides external interface for interpolation methods. @@ -293,5 +295,6 @@ class MLSMeshInterpolation final : public InterpolationBase void find_supports(unsigned min_req_supports = 10, unsigned max_allowed_supports = 30); }; +} // namespace pcms -#endif // PCMS_INTERPOLATION_BASE_H +#endif // PCMS_TRANSFER_INTERPOLATION_BASE_H diff --git a/src/pcms/transfer/interpolation_helpers.h b/src/pcms/transfer/interpolation_helpers.h new file mode 100644 index 000000000..2502bd1bf --- /dev/null +++ b/src/pcms/transfer/interpolation_helpers.h @@ -0,0 +1,22 @@ +#ifndef PCMS_TRANSFER_INTERPOLATION_HELPERS_H +#define PCMS_TRANSFER_INTERPOLATION_HELPERS_H + +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include +#include + +namespace pcms +{ + +// FIXME, evaluate if this is needed. If so, move to utility library +void copyHostScalarArrayView2HostWrite( + pcms::Rank1View source, + Omega_h::HostWrite& target); + +void copyHostWrite2ScalarArrayView( + const Omega_h::HostWrite& source, + pcms::Rank1View target); + +} // namespace pcms +#endif // PCMS_TRANSFER_INTERPOLATION_HELPERS_H diff --git a/src/pcms/transfer/interpolator.h b/src/pcms/transfer/interpolator.h new file mode 100644 index 000000000..1fc24e8db --- /dev/null +++ b/src/pcms/transfer/interpolator.h @@ -0,0 +1,87 @@ +#ifndef PCMS_FIELD_INTERPOLATOR_H +#define PCMS_FIELD_INTERPOLATOR_H + +#include "pcms/field/field.h" +#include "pcms/field/field_data.h" +#include "pcms/field/function_space.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/transfer/field_compatibility.hpp" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/profile.h" +#include "pcms/utility/types.h" +#include +#include +#include + +namespace pcms +{ + +// Interpolator separates the expensive localization step from the cheap +// repeated evaluation step, making it efficient to use in a coupling loop. +// +// Construct once per source×target function-space pair (localizes target DOF +// coordinates into the source mesh), then call Apply repeatedly for different +// field states at zero additional localization cost. Any Field sharing the +// same target FunctionSpace can be passed to Apply. +// +// Usage: +// Interpolator interp(src_space, tgt_space); +// interp.Apply(src_field, tgt_field); // cheap; called in coupling loop +// interp.Apply(src_field_next, tgt_field_next); // reuses cached localization +template +class Interpolator : public TransferOperator +{ +public: + // Expensive: localizes target DOF coords into source mesh. Called once. + Interpolator(const FunctionSpace& source_space, + const FunctionSpace& target_space, OutOfBoundsPolicy policy = {}) + : num_points_(static_cast(target_space.GetLayout() + ->GetDOFHolderCoordinates() + .GetCoordinates() + .extent(0))), + n_comp_(target_space.GetLayout()->GetNumComponents()), + source_layout_(source_space.GetLayout()), + target_layout_(target_space.GetLayout()), + evaluator_(source_space.CreatePointEvaluator( + EvaluationRequest::FromFunctionSpace(target_space, policy))) + { + } + + // Cheap: apply to any Field whose layout matches the target space. + // Localization is not repeated. + void Apply(const Field& source, Field& target) const override + { + PCMS_FUNCTION_TIMER; + detail::CheckTransferFieldLayout(source, *source_layout_, "source"); + detail::CheckTransferFieldLayout(target, *target_layout_, "target"); + const LO num_points = num_points_; + const int n_comp = n_comp_; + Kokkos::View output("interp_output", num_points, + n_comp); + auto output_view = MakeRank2View(output); + evaluator_->Evaluate(source, output_view); + Kokkos::View flat( + "interp_flat", static_cast(num_points) * n_comp); + Kokkos::parallel_for( + Kokkos::RangePolicy(0, num_points), + KOKKOS_LAMBDA(LO i) { + for (int c = 0; c < n_comp; ++c) { + flat(i * n_comp + c) = output(i, c); + } + }); + target.GetData().SetDOFHolderData(make_const_array_view(flat)); + } + +private: + LO num_points_; + int n_comp_; + std::shared_ptr source_layout_; + std::shared_ptr target_layout_; + std::unique_ptr> evaluator_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_INTERPOLATOR_H diff --git a/src/pcms/interpolator/linear_interpolant.hpp b/src/pcms/transfer/linear_interpolant.hpp similarity index 96% rename from src/pcms/interpolator/linear_interpolant.hpp rename to src/pcms/transfer/linear_interpolant.hpp index 82be08f57..c058d48f9 100644 --- a/src/pcms/interpolator/linear_interpolant.hpp +++ b/src/pcms/transfer/linear_interpolant.hpp @@ -1,11 +1,14 @@ -#ifndef INTERPOLANT_HPP -#define INTERPOLANT_HPP +#ifndef PCMS_TRANSFER_LINEAR_INTERPOLANT_HPP +#define PCMS_TRANSFER_LINEAR_INTERPOLANT_HPP #include #include "multidimarray.hpp" #define MAX_DIM 10 +namespace pcms +{ + KOKKOS_INLINE_FUNCTION void find_indices(const IntVecView& num_bins, const RealVecView& range, const RealVecView& point, int* indices) @@ -183,5 +186,6 @@ double test_function(double* coord) } return fun_value; } +} // namespace pcms -#endif +#endif // PCMS_TRANSFER_LINEAR_INTERPOLANT_HPP diff --git a/src/pcms/transfer/load_vector_integrator.cpp b/src/pcms/transfer/load_vector_integrator.cpp new file mode 100644 index 000000000..221e39058 --- /dev/null +++ b/src/pcms/transfer/load_vector_integrator.cpp @@ -0,0 +1,430 @@ +#include "pcms/transfer/load_vector_integrator.hpp" + +namespace pcms +{ + +/** + * @brief Converts barycentric coordinates to global (physical) coordinates. + * + * Given barycentric coordinates within a 2D triangle and the coordinates of + * the triangle's vertices, this function computes the corresponding global + * position. + * + * @param barycentric_coord The barycentric coordinates \f$(\lambda_1, + * \lambda_2, \lambda_3)\f$ of the point. + * @param verts_coord The coordinates of the triangle's three vertices in global + * space. + * @return The 2D global coordinates corresponding to the given barycentric + * position. + */ + +[[nodiscard]] OMEGA_H_INLINE Omega_h::Vector<2> global_from_barycentric( + const MeshField::Vector3& barycentric_coord, + const Omega_h::Few, 3>& verts_coord) +{ + Omega_h::Vector<2> real_coords = {0.0, 0.0}; + + for (int i = 0; i < 3; ++i) { + real_coords[0] += barycentric_coord[i] * verts_coord[i][0]; + real_coords[1] += barycentric_coord[i] * verts_coord[i][1]; + } + return real_coords; +} + +/** + * @brief Computes the barycentric coordinates of a 2D point with respect to a + * triangle. + * + * Given a point in global (x, y) coordinates and the coordinates of the three + * vertices of a triangle, this function evaluates the barycentric coordinates + * \f$(\lambda_1, \lambda_2, \lambda_3)\f$ of the point with respect to that + * triangle. + * + * @param point The 2D global coordinates of the point to evaluate (in + * Omega_h::Vector<2> format). + * @param verts_coord The vertex coordinates of the triangle (in r3d::Vector<2> + * format). + * @return A vector of three barycentric coordinates corresponding to the input + * point. + * + */ + +[[nodiscard]] OMEGA_H_INLINE Omega_h::Vector<3> evaluate_barycentric( + const Omega_h::Vector<2>& point, + const r3d::Few, 3>& verts_coord) +{ + Omega_h::Few, 3> omegah_vector; + for (int i = 0; i < 3; ++i) { + omegah_vector[i][0] = verts_coord[i][0]; + omegah_vector[i][1] = verts_coord[i][1]; + } + + auto barycentric_coordinate = + Omega_h::barycentric_from_global<2, 2>(point, omegah_vector); + + return barycentric_coordinate; +} +/** + * @brief Evaluates the value of a linear function at a given point using + * barycentric coordinates. + * + * This function computes the interpolated value of a nodal scalar field over a + * triangle, using barycentric coordinates within the specified element. + * + * @param nodal_values The global array of nodal field values. + * @param faces2nodes The element-to-node connectivity array. + * @param bary_coords The barycentric coordinates of the evaluation point within + * the triangle. + * @param elm_id The ID of the triangle element being evaluated. + * @return The interpolated function value at the given point. + * + * @note This function assumes linear (3-node) triangular element. + */ + +[[nodiscard]] OMEGA_H_INLINE double evaluate_function_value( + const Omega_h::Reals& nodal_values, const Omega_h::LOs& faces2nodes, + const Omega_h::Vector<3>& bary_coords, const int elm_id) +{ + + double value = 0; + const auto elm_verts = Omega_h::gather_verts<3>(faces2nodes, elm_id); + for (int i = 0; i < 3; ++i) { + int nid = elm_verts[i]; + value += nodal_values[nid] * bary_coords[i]; + } + + return value; +} + +/** + * @brief Deduplicate, reorder, and orient a 2D polygon produced by r3d. + * + * This function performs all necessary cleanup and reconstruction of a polygon + * returned by `r3d::intersect_simplices()`, which may contain: + * - duplicated vertices, + * - unordered vertex lists, + * - invalid or inconsistent neighbor links (`pnbrs`), + * - negative orientation (CW instead of CCW). + * + * The cleanup proceeds with the following stages: + * + * **(1) Geometric deduplication:** + * Vertices whose coordinates are equal within a tolerance `tol` are collapsed + * into a single unique vertex. A compacted vertex list is built. + * + * **(2) CCW vertex reordering:** + * The remaining unique vertices are sorted by their polar angle around the + * polygon centroid. This yields a globally consistent counter-clockwise (CCW) + * + * + * **(3) Rebuilding neighbor links:** + * After sorting, each vertex's two neighbors (`pnbrs[0]` and `pnbrs[1]`) are + * reassigned to form a closed CCW cycle: + * + * pnbrs[0] = previous vertex in CCW order + * pnbrs[1] = next vertex in CCW order + * + * + * + * @param poly The polygon to clean, and reorder. + * + * @param tol Tolerance for geometric duplicate detection (default: 1e-12). + * + * @return The number of unique, CCW-ordered vertices remaining in the polygon. + * + * @see r3d::Polytope + * @see r3d::measure + * @see r3d::intersect_simplices + */ + +[[nodiscard]] OMEGA_H_INLINE int remove_duplicate_vertices_and_fix_links( + r3d::Polytope<2>& poly, const double tol = 1e-12) +{ + + const int old_n = poly.nverts; + int new_n = 0; + + // geometric deupliactes filtering + for (int i = 0; i < old_n; ++i) { + const auto& pi = poly.verts[i].pos; + bool dup = false; + for (int j = 0; j < new_n; ++j) { + const auto& pj = poly.verts[j].pos; + if (Kokkos::fabs(pi[0] - pj[0]) < tol && + Kokkos::fabs(pi[1] - pj[1]) < tol) { + dup = true; + break; + } + } + if (!dup) { + poly.verts[new_n] = poly.verts[i]; + ++new_n; + } + } + poly.nverts = new_n; + + // CCW reorder verts by angle about centroid + if (new_n >= 3) { + // centroid + double cx = 0.0; + double cy = 0.0; + for (int i = 0; i < new_n; ++i) { + cx += poly.verts[i].pos[0]; + cy += poly.verts[i].pos[1]; + } + cx /= new_n; + cy /= new_n; + + // angle sort + int order[r3d::MaxVerts<2>::value]; + for (int i = 0; i < new_n; ++i) { + order[i] = i; + } + + for (int i = 0; i < new_n - 1; ++i) { + for (int j = i + 1; j < new_n; ++j) { + const double a1 = Kokkos::atan2(poly.verts[order[i]].pos[1] - cy, + poly.verts[order[i]].pos[0] - cx); + const double a2 = Kokkos::atan2(poly.verts[order[j]].pos[1] - cy, + poly.verts[order[j]].pos[0] - cx); + if (a1 > a2) { + int t = order[i]; + order[i] = order[j]; + order[j] = t; + } + } + } + + r3d::Vertex<2> tmp[r3d::MaxVerts<2>::value]; + for (int i = 0; i < new_n; ++i) { + tmp[i] = poly.verts[order[i]]; + } + + for (int i = 0; i < new_n; ++i) { + poly.verts[i] = tmp[i]; + } + + // set circular neighbors consistent with verts[] order + for (int i = 0; i < new_n; ++i) { + const int prev = (i - 1 + new_n) % new_n; + const int next = (i + 1) % new_n; + poly.verts[i].pnbrs[0] = prev; // CCW prev + poly.verts[i].pnbrs[1] = next; // CCW next + } + + // ensure positive orientation (if measure is still negative, reverse) + double area = r3d::measure(poly); + if (area < 0.0) { + // reverse verts and relink + for (int i = 0; i < new_n / 2; ++i) { + r3d::Vertex<2> t = poly.verts[i]; + poly.verts[i] = poly.verts[new_n - 1 - i]; + poly.verts[new_n - 1 - i] = t; + } + for (int i = 0; i < new_n; ++i) { + const int prev = (i - 1 + new_n) % new_n; + const int next = (i + 1) % new_n; + poly.verts[i].pnbrs[0] = prev; + poly.verts[i].pnbrs[1] = next; + } + } + } else { + // clear links + for (int i = 0; i < new_n; ++i) { + poly.verts[i].pnbrs[0] = -1; + poly.verts[i].pnbrs[1] = -1; + } + } + + return new_n; +} + +template +OMEGA_H_INLINE void for_each_intersection_subtriangle( + const int elm, const IntersectionResults& intersection, + const Omega_h::Reals& tgt_coords, const Omega_h::Reals& src_coords, + const Omega_h::LOs& tgt_faces2nodes, const Omega_h::LOs& src_faces2nodes, + TriangleOp&& op) +{ + auto tgt_elm_vert_coords = + get_vert_coords_of_elem(tgt_coords, tgt_faces2nodes, elm); + const int start = intersection.tgt2src_offsets[elm]; + const int end = intersection.tgt2src_offsets[elm + 1]; + + for (int i = start; i < end; ++i) { + const int current_src_elm = intersection.tgt2src_indices[i]; + auto src_elm_vert_coords = + get_vert_coords_of_elem(src_coords, src_faces2nodes, current_src_elm); + r3d::Polytope<2> poly; + r3d::intersect_simplices(poly, tgt_elm_vert_coords, src_elm_vert_coords); + auto nverts = remove_duplicate_vertices_and_fix_links(poly, 1e-12); + ; + auto poly_area = r3d::measure(poly); + + for (int j = 1; j < nverts - 1; ++j) { + // build triangle from poly.verts[0], poly.verts[j], + // poly.verts[j+1] + auto& p0 = poly.verts[0].pos; + auto& p1 = poly.verts[j].pos; + auto& p2 = poly.verts[j + 1].pos; + + Omega_h::Few, 3> tri_coords; + tri_coords[0] = {p0[0], p0[1]}; + tri_coords[1] = {p1[0], p1[1]}; + tri_coords[2] = {p2[0], p2[1]}; + + Omega_h::Few, 2> basis; + basis[0] = tri_coords[1] - tri_coords[0]; + basis[1] = tri_coords[2] - tri_coords[0]; + + Omega_h::Real area = + Kokkos::fabs(Omega_h::triangle_area_from_basis(basis)); + + const double EPS_AREA = abs_tol + rel_tol * poly_area; + if (area <= EPS_AREA) + continue; // drops duplicates and colinear/degenerates + + op(tri_coords, tgt_elm_vert_coords, src_elm_vert_coords, current_src_elm, + area); + } + } +} + +Kokkos::View buildLoadVector( + Omega_h::Mesh& target_mesh, Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, const Omega_h::Reals& source_values) +{ + + const auto& tgt_coords = target_mesh.coords(); + const auto& src_coords = source_mesh.coords(); + const auto& tgt_faces2nodes = + target_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto& src_faces2nodes = + source_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + + IntegrationData<2> integrationPoints; + int npts = integrationPoints.size(); + + // TODO: Make it generalised; hardcoded for liner 2D + Kokkos::View elmLoadVector( + "elmLoadVector", static_cast(target_mesh.nelems()) * 3); + Kokkos::parallel_for( + "calculate load vector", target_mesh.nelems(), + KOKKOS_LAMBDA(const int& elm) { + Omega_h::Vector<3> part_integration = {0.0, 0.0, 0.0}; + for_each_intersection_subtriangle( + elm, intersection, tgt_coords, src_coords, tgt_faces2nodes, + src_faces2nodes, + [&](const Omega_h::Few, 3>& tri_coords, + const r3d::Few, 3>& tgt_elm_vert_coords, + const r3d::Few, 3>& src_elm_vert_coords, + const int current_src_elm, const Omega_h::Real area) { + for (int ip = 0; ip < npts; ++ip) { + auto bary = integrationPoints.bary_coords(ip); + auto weight = integrationPoints.weights(ip); + + // convert barycentric to real coords in triangle + auto real_coords = global_from_barycentric(bary, tri_coords); + + // evaluate shape function (barycentric wrt target for linear) + auto shape_fn = + evaluate_barycentric(real_coords, tgt_elm_vert_coords); + + // evaluate function at point (barycentric wrt source for linear) + auto src_bary = + evaluate_barycentric(real_coords, src_elm_vert_coords); + auto fval = evaluate_function_value(source_values, src_faces2nodes, + src_bary, current_src_elm); + + // integration + for (int k = 0; k < 3; ++k) { + part_integration[k] += shape_fn[k] * fval * weight * 2 * area; + } + } + }); + + for (int j = 0; j < 3; ++j) { + elmLoadVector(elm * 3 + j) = part_integration[j]; + } + }); + + return elmLoadVector; +} +Errors evaluate_proj_and_cons_errors(Omega_h::Mesh& target_mesh, + Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, + const Omega_h::Reals& target_values, + const Omega_h::Reals& source_values) +{ + + const auto& tgt_coords = target_mesh.coords(); + const auto& src_coords = source_mesh.coords(); + const auto& tgt_faces2nodes = + target_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto& src_faces2nodes = + source_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + + IntegrationData<2> integrationPoints; + int npts = integrationPoints.size(); + + constexpr double EPS_DEN = 1e-30; + + Kokkos::View accum("accum", 4); + Kokkos::deep_copy(accum, 0.0); + + Kokkos::parallel_for( + "evaluate relative errors", target_mesh.nelems(), + KOKKOS_LAMBDA(const int& elm) { + double N2 = 0.0, D2 = 0.0, C = 0.0, QD = 0.0; + for_each_intersection_subtriangle( + elm, intersection, tgt_coords, src_coords, tgt_faces2nodes, + src_faces2nodes, + [&](const Omega_h::Few, 3>& tri_coords, + const r3d::Few, 3>& tgt_elm_vert_coords, + const r3d::Few, 3>& src_elm_vert_coords, + const int current_src_elm, const Omega_h::Real area) { + for (int ip = 0; ip < npts; ++ip) { + auto bary = integrationPoints.bary_coords(ip); + auto weight = integrationPoints.weights(ip); + + // convert barycentric to real coords in triangle + auto real_coords = global_from_barycentric(bary, tri_coords); + auto tgt_bary = + evaluate_barycentric(real_coords, tgt_elm_vert_coords); + + // evaluate shape function (barycentric wrt target for linear) + auto tgtVal = evaluate_function_value( + target_values, tgt_faces2nodes, tgt_bary, elm); + + // evaluate function at point (barycentric wrt source for linear) + auto src_bary = + evaluate_barycentric(real_coords, src_elm_vert_coords); + auto srcVal = evaluate_function_value( + source_values, src_faces2nodes, src_bary, current_src_elm); + + // integration + auto diff = srcVal - tgtVal; + auto w = 2 * weight * area; + N2 += diff * diff * w; + D2 += srcVal * srcVal * w; + C += diff * w; + QD += srcVal * w; + } + }); + + Kokkos::atomic_add(&accum(0), N2); + Kokkos::atomic_add(&accum(1), D2); + Kokkos::atomic_add(&accum(2), C); + Kokkos::atomic_add(&accum(3), QD); + }); + + auto h_accum = Kokkos::create_mirror(accum); + Kokkos::deep_copy(h_accum, accum); + const double proj_err = + Kokkos::sqrt(h_accum(0)) / Kokkos::max(Kokkos::sqrt(h_accum(1)), EPS_DEN); + const double cons_err = + Kokkos::fabs(h_accum(2)) / Kokkos::max(Kokkos::fabs(h_accum(3)), EPS_DEN); + + return Errors{.proj_err = proj_err, .cons_err = cons_err}; +} +} // namespace pcms diff --git a/src/pcms/transfer/load_vector_integrator.hpp b/src/pcms/transfer/load_vector_integrator.hpp new file mode 100644 index 000000000..4e1d2fad4 --- /dev/null +++ b/src/pcms/transfer/load_vector_integrator.hpp @@ -0,0 +1,220 @@ +/** + * @file load_vector_integrator.hpp + * @brief Functions for computing load vectors in conservative field projection. + * + * This file implements routines to compute the element-wise load vector + * (right-hand side) contributions used in Galerkin projection of scalar + * fields from a source mesh to a target mesh. + * + * The integration is performed over polygonal intersections (supermesh) between + * source and target elements using barycentric quadrature. The resulting values + * represent unassembled local contributions that can later be combined into a + * global load vector. + * + * @note + * - Assumes 2D linear triangular meshes. + * - Intersection data is provided via the `IntersectionResults` structure. + */ +#ifndef PCMS_TRANSFER_LOAD_VECTOR_INTEGRATOR_HPP +#define PCMS_TRANSFER_LOAD_VECTOR_INTEGRATOR_HPP + +#include +#include +#include +#include +#include +#include + +namespace pcms +{ +/** + * @brief Computes the load vector for each target element in the conservative + * field transfer. + * + * This routine is used for constructing the right-hand side (RHS) of the + * conservative field transfer formulation, projecting field quantities from the + * source mesh to the target mesh. + * + * The underlying algorithm computes contributions to the load vector + * using geometric intersection data between source and target elements. + * + * @note Currently this method works for a two-dimensional linear triangles. + */ + +/** + * @brief Provides barycentric integration points and weights for a triangle + * element. + * + * This templated struct stores the barycentric coordinates + * and quadrature weights for performing numerical integration over a reference + * triangle. It is used for integrating functions over elements in the + * conservative field transfer. + * + * @tparam order The quadrature order (number of integration points and + * polynomial accuracy). + */ +template +struct IntegrationData +{ + // Barycentric coordinates of integration points + Kokkos::View bary_coords; + + // Quadrature weights associated with each integration point + Kokkos::View weights; + + /** + * @brief Constructs the integration data for a given quadrature order + * + * Initializes barycentric coordinates and weights using + * MeshField's predefined triangle quadrature rules. + */ + IntegrationData() + { + auto ip_vec = MeshField::getIntegrationPoints(order); + std::size_t num_ip = ip_vec.size(); + + bary_coords = Kokkos::View("bary_coords", num_ip); + weights = Kokkos::View("weights", num_ip); + + auto bary_coords_host = Kokkos::create_mirror_view(bary_coords); + auto weights_host = Kokkos::create_mirror_view(weights); + + for (std::size_t i = 0; i < num_ip; ++i) { + bary_coords_host(i) = ip_vec[i].param; + weights_host(i) = ip_vec[i].weight; + } + + Kokkos::deep_copy(bary_coords, bary_coords_host); + Kokkos::deep_copy(weights, weights_host); + } + + /** + * @brief Returns the number of integration points + * + * @return Number of integration points for the selected order. + */ + + int size() const { return bary_coords.extent(0); } +}; + +/** + * @brief Computes the per-element RHS load vectors for conservative field + * projection from source to target mesh. + * + * This function computes local (element-wise) right-hand side (RHS) + * contributions for the Galerkin projection of a scalar field from the source + * mesh to the target mesh. It integrates over the polygonal intersection + * regions between each target element and its intersecting source elements + * using barycentric quadrature. + * + * The output is a flat array containing unassembled load vector contributions + * at the nodes of each target triangle. + * + * @param target_mesh The target mesh object receiving the projected scalar + * field. + * @param source_mesh The source mesh object containing the original scalar + * field values. + * @param intersection Precomputed intersection data for each target element. + * Includes the number and indices of intersecting source + * elements. + * @param source_values Scalar field values defined at the nodes of the source + * mesh. + * + * @return A Kokkos view containing per-element load vectors. + * Each triangle contributes 3 values (one per node), so the view has + * size 3 × (number of target elements). + * + * @note + * - This function assumes 2D linear triangular elements. + * - Degenerate or near-zero-area intersection polygons are skipped. + * - Each polygon is triangulated using a fan structure and integrated using + * barycentric quadrature rules. + * - The returned vector must be assembled into a global RHS vector in a later + * step. + * + * @see evaluate_barycentric, evaluate_function_value, global_from_barycentric + * @see IntersectionResults + */ + +Kokkos::View buildLoadVector( + Omega_h::Mesh& target_mesh, Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, const Omega_h::Reals& source_values); +/// Holds projection and conservation error metrics returned by +/// evaluate_pro_and_cons_errors(). +struct Errors +{ + double proj_err; ///< L2 projection error computed on the supermesh. + double cons_err; ///< Relative conservation error over the supermesh. +}; + +/** + * @brief Computes projection and conservation errors over the supermesh for + * scalar field transfer. + * + * This function quantifies the accuracy and conservation properties of + * conservative field transfer between nonconforming meshes using + * supermesh-based integration. It returns a struct containing two error + * metrics: + * + * - **Projection Error (`proj_err`)** — Measures the L2 norm of the difference + * between the projected source field and the target field over the + * supermesh.This reflects how accurately the field has been projected. + * + * - **Conservation Error (`cons_err`)** — Relative difference in the integrated + * field values between source and target representations. This captures + * conservation loss across the transfer. + * + * ### Mathematical Definitions: + * \f[ + * \text{proj\_err} = \frac{ \| q_D - q_T \|_{L_2(\Omega_S)} } + * { \| q_D \|_{L_2(\Omega_S)} }, \quad + * \text{cons\_err} = \frac{ \left| \int_{\Omega_S} q_D - \int_{\Omega_S} q_T + * \right| } { \left| \int_{\Omega_S} q_D \right| } + * \f] + * + * where: + * - \f$q_D\f$ is the scalar field defined on the source mesh (mesh from where + * the field is defined), + * - \f$q_T\f$ is the projected field on the target mesh (mesh to where the + * field is projected), + * - \f$\Omega_S\f$ is the supermesh formed by polygonal intersections of source + * and target elements. + * + * Integration is performed by triangulating each intersection region and + * applying barycentric quadrature. Degenerate or near-zero-area triangles are + * skipped based on area tolerance. + * + * + * @param target_mesh The target mesh object receiving the projected scalar + * field. + * @param source_mesh The source mesh object containing the original scalar + * field values. + * @param intersection Precomputed intersection data for each target element. + * Includes the number and indices of intersecting source + * elements. + * @param target_values Nodal scalar field values evaluated on the target mesh + * using galerkin projection. + * @param source_values Scalar field values defined at the nodes of the source + * mesh. + * + * + * @return A struct containing: + * - `proj_err`: Exact L2 projection error over the supermesh. + * - `cons_err`: Relative conservation error over the supermesh. + * + * @note + * - Assumes 2D linear (P1) triangular elements. + * - Ideal for validating conservative transfer schemes or testing projection + * fidelity. + * + * @see IntersectionResults, buildLoadVector + */ + +Errors evaluate_proj_and_cons_errors(Omega_h::Mesh& target_mesh, + Omega_h::Mesh& source_mesh, + const IntersectionResults& intersection, + const Omega_h::Reals& target_values, + const Omega_h::Reals& source_values); +} // namespace pcms + +#endif // PCMS_TRANSFER_LOAD_VECTOR_INTEGRATOR_HPP diff --git a/src/pcms/transfer/mass_matrix_integrator.hpp b/src/pcms/transfer/mass_matrix_integrator.hpp new file mode 100644 index 000000000..1a2b4b592 --- /dev/null +++ b/src/pcms/transfer/mass_matrix_integrator.hpp @@ -0,0 +1,88 @@ +#ifndef PCMS_TRANSFER_MASS_MATRIX_INTEGRATOR_HPP +#define PCMS_TRANSFER_MASS_MATRIX_INTEGRATOR_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pcms +{ +// computes the mass matrix for each element +template +class MassMatrixIntegrator : public MeshField::Integrator +{ +public: + MassMatrixIntegrator(Omega_h::Mesh& mesh_in, FieldElement& fe_in, + int order = 2) + : mesh(mesh_in), + fe(fe_in), + subMatrixSize(3 * 3), // FIXME remove hard coded size + elmMassMatrix("elmMassMatrix", mesh_in.nelems() * 3 * 3), + Integrator(order) + { + Kokkos::deep_copy(elmMassMatrix, 0); + assert(mesh.dim() == 2); // TODO support 1d,2d,3d + assert(mesh.family() == OMEGA_H_SIMPLEX); + } + void atPoints(Kokkos::View p, + Kokkos::View w, + Kokkos::View dV) + { + // std::cerr << "MassMatrixIntegrator::atPoints(...)\n"; + const size_t numPtsPerElem = p.extent(0) / mesh.nelems(); + // std::cerr << " Number points per Elem : " << numPtsPerElem << "\n"; + assert(numPtsPerElem >= 1); + const size_t ptDim = p.extent(1); + assert(ptDim == fe.MeshEntDim + 1); + // Copy values needed in the kernel to avoid capturing host references + // (mesh and fe are host objects and cannot be dereferenced on the device) + const auto numElems = mesh.nelems(); + const auto shapeFn = fe.shapeFn; + const auto subMat = subMatrixSize; + auto massMatrix = elmMassMatrix; + Kokkos::parallel_for( + "eval", numElems, KOKKOS_LAMBDA(const int& elm) { + const auto first = elm * numPtsPerElem; + const auto last = first + numPtsPerElem; + for (auto pt = first; pt < last; pt++) { + // FIXME better way to fill? pass kokkos::subview to getValues? + Kokkos::Array + localCoord; + for (auto i = 0; i < localCoord.size(); i++) { + localCoord[i] = p(pt, i); + } + const auto N = shapeFn.getValues(localCoord); + const auto wPt = w(pt); + const auto dVPt = dV(pt); + // printf("Shape Functions: %f, %f, %f \n", N[0], N[1], N[2]); + // printf("wPt, dVPt: %f, %f \n", wPt, dVPt); + for (auto i = 0; i < N.size(); i++) { + for (auto j = 0; j < N.size(); j++) { + massMatrix(elm * subMat + i * 3 + j) += N[i] * N[j] * wPt * dVPt; + } + } + } + }); + } + Omega_h::Mesh& mesh; + FieldElement& fe; + const int subMatrixSize; + Kokkos::View + elmMassMatrix; // numNodes^2 entries per element +}; + +template +Kokkos::View buildMassMatrix(Omega_h::Mesh& mesh, + FieldElement& coordFe) +{ + MassMatrixIntegrator mmi(mesh, coordFe); + mmi.process(coordFe); + return mmi.elmMassMatrix; +} +} // namespace pcms +#endif // PCMS_TRANSFER_MASS_MATRIX_INTEGRATOR_HPP diff --git a/src/pcms/transfer/mesh_intersection.cpp b/src/pcms/transfer/mesh_intersection.cpp new file mode 100644 index 000000000..9cb37de36 --- /dev/null +++ b/src/pcms/transfer/mesh_intersection.cpp @@ -0,0 +1,146 @@ +#include "pcms/transfer/mesh_intersection.hpp" +#include "pcms/utility/mesh_geometry.h" +#include "pcms/utility/omega_h_array_utils.h" + +namespace pcms +{ +void FindIntersections::adjBasedIntersectSearch( + const Omega_h::LOs& tgt2src_offsets, + Omega_h::Write& nIntersections, + Omega_h::Write& tgt2src_indices, bool is_count_only) +{ + + const auto& tgt_coords = target_mesh_.coords(); + const auto& src_coords = source_mesh_.coords(); + const auto& tgt_faces2nodes = + target_mesh_.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto& src_faces2nodes = + source_mesh_.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto& src_elem_areas = measure_elements_real(&source_mesh_); + const auto& tgt_elem_areas = measure_elements_real(&target_mesh_); + const auto& t2t = + source_mesh_.ask_dual(); // gives connected element neighbors + const auto& t2tt = t2t.a2ab; + const auto& tt2t = t2t.ab2b; + + const auto flat_centroids = + pcms::get_entity_centroids(target_mesh_, Omega_h::FACE); + // Convert layout_right 1D Omega_h array to 2D Kokkos view with correct layout + auto centroids = ConvertCoordsTo2D(flat_centroids, target_mesh_.nfaces(), 2); + + pcms::GridPointSearch2D search_cell(source_mesh_, 20, 20); + auto results = search_cell(centroids); + + auto nfaces_target = target_mesh_.nfaces(); + Omega_h::parallel_for( + nfaces_target, + OMEGA_H_LAMBDA(const Omega_h::LO id) { + Queue queue; + Track visited; + + auto current_cell_id = results(id).element_id; + auto current_tgt_elm_area = tgt_elem_areas[id]; + + OMEGA_H_CHECK_PRINTF(current_cell_id >= 0, + "ERROR: source cell id not found for given target " + "centroid %d (%f, %f)\n", + id, centroids(id, 0), centroids(id, 1)); + + auto tgt_elm_vert_coords = + get_vert_coords_of_elem(tgt_coords, tgt_faces2nodes, id); + + Omega_h::LO start_counter; + if (!is_count_only) { + start_counter = tgt2src_offsets[id]; + } + + int count = 0; + + count++; + visited.push_back(current_cell_id); + queue.push_back(current_cell_id); + + if (!is_count_only) { + int idx_count = count - 1; + tgt2src_indices[start_counter + idx_count] = current_cell_id; + } + + while (!queue.isEmpty()) { + Omega_h::LO currentElm = queue.front(); + queue.pop_front(); + auto start = t2tt[currentElm]; + auto end = t2tt[currentElm + 1]; + + for (int i = start; i < end; ++i) { + auto neighborElmId = tt2t[i]; + + if (visited.notVisited(neighborElmId)) { + visited.push_back(neighborElmId); + auto elm_vert_coords = get_vert_coords_of_elem( + src_coords, src_faces2nodes, neighborElmId); + r3d::Polytope<2> intersection; + r3d::intersect_simplices(intersection, tgt_elm_vert_coords, + elm_vert_coords); + auto intersected_area = r3d::measure(intersection); + auto current_src_elm_area = src_elem_areas[neighborElmId]; + auto scale = + Kokkos::fmax(current_tgt_elm_area, current_src_elm_area); + auto eps = Kokkos::fmax(abs_tol, rel_tol * scale); + if (intersection.nverts >= 3 && intersected_area >= eps) { + count++; + + OMEGA_H_CHECK_PRINTF( + count < 500, "WARNING: count exceeds 500 for target %d", id); + + queue.push_back(neighborElmId); + + if (!is_count_only) { + Omega_h::LO idx_count = count - 1; + tgt2src_indices[start_counter + idx_count] = neighborElmId; + + } // end of tgt2src_indices check + + } // end of intersection with bbox check + + } // end of not visited check + + } // end of loop over adj elements to the current element + + } // end of while loop + + nIntersections[id] = count; + }, // end of lambda + "count the number of intersections for each target element"); +} +IntersectionResults intersectTargets(Omega_h::Mesh& source_mesh, + Omega_h::Mesh& target_mesh) +{ + FindIntersections intersect(source_mesh, target_mesh); + + auto nfaces_target = target_mesh.nfaces(); + + Omega_h::Write nIntersections( + nfaces_target, 0, "number of intersections in each target vertex"); + + Omega_h::Write tgt2src_indices; + + intersect.adjBasedIntersectSearch(Omega_h::LOs(), nIntersections, + tgt2src_indices, true); + + Kokkos::fence(); + auto tgt2src_offsets = Omega_h::offset_scan(Omega_h::Read(nIntersections), + "offsets for intersections"); + auto ntotal_intersections = tgt2src_offsets.last(); + + Kokkos::fence(); + + tgt2src_indices = Omega_h::Write( + ntotal_intersections, 0, + "indices of the source elements that intersect the given target element"); + + intersect.adjBasedIntersectSearch(tgt2src_offsets, nIntersections, + tgt2src_indices, false); + return {.tgt2src_offsets = tgt2src_offsets, + .tgt2src_indices = Omega_h::read(tgt2src_indices)}; +} +} // namespace pcms diff --git a/src/pcms/transfer/mesh_intersection.hpp b/src/pcms/transfer/mesh_intersection.hpp new file mode 100644 index 000000000..84a52114c --- /dev/null +++ b/src/pcms/transfer/mesh_intersection.hpp @@ -0,0 +1,115 @@ +#ifndef PCMS_TRANSFER_MESH_INTERSECTION_HPP +#define PCMS_TRANSFER_MESH_INTERSECTION_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace pcms +{ +constexpr static double abs_tol = 1e-18; /// abs tolerance +constexpr static double rel_tol = 1e-12; /// rel tolerance + +[[nodiscard]] OMEGA_H_INLINE r3d::Few, 3> +get_vert_coords_of_elem(const Omega_h::Reals& coords, + const Omega_h::LOs& faces2nodes, const int id) +{ + const auto elm_verts = Omega_h::gather_verts<3>(faces2nodes, id); + + const Omega_h::Matrix<2, 3> elm_vert_coords = + Omega_h::gather_vectors<3, 2>(coords, elm_verts); + + r3d::Few, 3> r3d_vector; + for (int i = 0; i < 3; ++i) { + r3d_vector[i][0] = elm_vert_coords[i][0]; + r3d_vector[i][1] = elm_vert_coords[i][1]; + } + + return r3d_vector; +} + +/** + * @brief Stores results of mesh element intersections for conservative + * transfer. + * + * Contains mappings from each target element to the list of source elements + * that intersect with it. Used to guide integration over overlapping regions. + * + * - `tgt2src_offsets[i]` is the offset into `tgt2src_indices` where source + * elements for target element `i` begin. + * - `tgt2src_indices` contains flattened indices of source elements per target. + */ +struct IntersectionResults +{ + Omega_h::LOs tgt2src_offsets; + Omega_h::LOs tgt2src_indices; +}; + +class FindIntersections +{ +private: + Omega_h::Mesh& source_mesh_; + Omega_h::Mesh& target_mesh_; + +public: + FindIntersections(Omega_h::Mesh& source_mesh, Omega_h::Mesh& target_mesh) + : source_mesh_(source_mesh), target_mesh_(target_mesh) + { + } + + /** + * @brief Performs adjacency-based intersection search between target and + * source elements. + * + * For each target element, starting from the source element that contains its + * centroid, a queue-based BFS traversal is used over the adjacency graph of + * source elements. If an element intersects the target triangle (based on + * area tolerance), it is included. + * + * @param tgt2src_offsets Offsets array (only used when writing indices). + * @param[out] nIntersections Number of intersecting source elements per + * target element. + * @param[out] tgt2src_indices Indices of intersecting source elements. + * @param is_count_only If true, only counts intersections; if false, also + * fills tgt2src_indices. + * + * @note This method assumes 2D linear triangles and uses + * `r3d::intersect_simplices` for geometric intersection. + * + * @see r3d::intersect_simplices, intersectTargets + */ + void adjBasedIntersectSearch(const Omega_h::LOs& tgt2src_offsets, + Omega_h::Write& nIntersections, + Omega_h::Write& tgt2src_indices, + bool is_count_only); +}; + +/** + * @brief Computes source-target element intersections for conservative + * projection. + * + * For each target element in the target mesh, this function identifies source + * elements from the source mesh that geometrically intersect with it using an + * adjacency-based breadth-first search strategy. The result is returned as a + * compact mapping. + * + * @param source_mesh The source Omega_h mesh. + * @param target_mesh The target Omega_h mesh. + * @return An IntersectionResults struct containing target-to-source mapping + * data. + * + * @note The intersection test is done using 2D polygon intersection routines + * from r3d. Only valid (non-degenerate) polygonal intersections are included. + * + * @see FindIntersections::adjBasedIntersectSearch + */ + +IntersectionResults intersectTargets(Omega_h::Mesh& source_mesh, + Omega_h::Mesh& target_mesh); +} // namespace pcms +#endif // PCMS_TRANSFER_MESH_INTERSECTION_HPP diff --git a/src/pcms/interpolator/multidimarray.hpp b/src/pcms/transfer/multidimarray.hpp similarity index 84% rename from src/pcms/interpolator/multidimarray.hpp rename to src/pcms/transfer/multidimarray.hpp index b5da3b159..8e08948f3 100644 --- a/src/pcms/interpolator/multidimarray.hpp +++ b/src/pcms/transfer/multidimarray.hpp @@ -1,8 +1,11 @@ -#ifndef MULTIDIMARRAY_HPP -#define MULTIDIMARRAY_HPP +#ifndef PCMS_TRANSFER_MULTIDIMARRAY_HPP +#define PCMS_TRANSFER_MULTIDIMARRAY_HPP #include +namespace pcms +{ + using RealMatView = Kokkos::View; using IntMatView = Kokkos::View; using RealVecView = Kokkos::View; @@ -33,4 +36,5 @@ inline int calculateTotalSize(const HostIntVecView& dimensions) } return size; } -#endif +} // namespace pcms +#endif // PCMS_TRANSFER_MULTIDIMARRAY_HPP diff --git a/src/pcms/transfer/omega_h_conservative_projection.cpp b/src/pcms/transfer/omega_h_conservative_projection.cpp new file mode 100644 index 000000000..3b70e427d --- /dev/null +++ b/src/pcms/transfer/omega_h_conservative_projection.cpp @@ -0,0 +1,99 @@ +#include "pcms/transfer/omega_h_conservative_projection.hpp" +#include "pcms/utility/arrays.h" +#include "pcms/utility/assert.h" +#include +#include + +namespace pcms +{ + +namespace +{ + +void CheckSupportedLayout( + const FunctionSpace& space, + const std::shared_ptr& layout, const char* role) +{ + if (layout == nullptr) { + throw pcms_error(std::string("OmegaHConservativeProjection: ") + role + + " space must use OmegaHLagrangeLayout"); + } + if (layout->GetOrder() != 1) { + throw pcms_error(std::string("OmegaHConservativeProjection: ") + role + + " space must be order-1"); + } + if (layout->GetNumComponents() != 1) { + throw pcms_error(std::string("OmegaHConservativeProjection: ") + role + + " space must have exactly one component"); + } + if (space.GetCoordinateSystem() != CoordinateSystem::Cartesian) { + throw pcms_error(std::string("OmegaHConservativeProjection: ") + role + + " space must use Cartesian coordinates"); + } +} + +void CheckApplyCompatible(const Field& source, const Field& target, + const OmegaHLagrangeLayout& source_layout, + const OmegaHLagrangeLayout& target_layout) +{ + if (&source.GetLayout() != &source_layout) { + throw pcms_error( + "OmegaHConservativeProjection::Apply: source field layout mismatch"); + } + if (&target.GetLayout() != &target_layout) { + throw pcms_error( + "OmegaHConservativeProjection::Apply: target field layout mismatch"); + } + + const auto& source_md = source.GetData().GetMetadata(); + const auto& target_md = target.GetData().GetMetadata(); + if (source_md.value_type != FieldValueType::Scalar || + target_md.value_type != FieldValueType::Scalar) { + throw pcms_error( + "OmegaHConservativeProjection::Apply: only scalar fields are supported"); + } + if (source_md.value_coordinate_system != target_md.value_coordinate_system) { + throw pcms_error("OmegaHConservativeProjection::Apply: source and target " + "value coordinate systems differ"); + } +} + +Omega_h::Reals MakeOmegaHReals(Rank1View values) +{ + Omega_h::HostWrite values_host(values.size()); + for (size_t i = 0; i < values.size(); ++i) { + values_host[i] = values[i]; + } + return Omega_h::Reals(values_host); +} + +} // namespace + +OmegaHConservativeProjection::OmegaHConservativeProjection( + const FunctionSpace& source_space, const FunctionSpace& target_space) + : source_layout_(std::dynamic_pointer_cast( + source_space.GetLayout())), + target_layout_(std::dynamic_pointer_cast( + target_space.GetLayout())) +{ + CheckSupportedLayout(source_space, source_layout_, "source"); + CheckSupportedLayout(target_space, target_layout_, "target"); + + intersections_ = + intersectTargets(source_layout_->GetMesh(), target_layout_->GetMesh()); +} + +void OmegaHConservativeProjection::Apply(const Field& source, + Field& target) const +{ + CheckApplyCompatible(source, target, *source_layout_, *target_layout_); + + const auto source_values = MakeOmegaHReals(source.GetDOFHolderDataHost()); + const auto target_values = solveGalerkinProjection( + target_layout_->GetMesh(), source_layout_->GetMesh(), intersections_, + source_values); + auto target_values_h = Omega_h::HostRead(target_values); + target.SetDOFHolderDataHost(make_const_array_view(target_values_h)); +} + +} // namespace pcms diff --git a/src/pcms/transfer/omega_h_conservative_projection.hpp b/src/pcms/transfer/omega_h_conservative_projection.hpp new file mode 100644 index 000000000..386cb260c --- /dev/null +++ b/src/pcms/transfer/omega_h_conservative_projection.hpp @@ -0,0 +1,34 @@ +#ifndef PCMS_TRANSFER_OMEGA_H_CONSERVATIVE_PROJECTION_H +#define PCMS_TRANSFER_OMEGA_H_CONSERVATIVE_PROJECTION_H + +#include "pcms/field/function_space.h" +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/transfer/conservative_projection_solver.hpp" +#include "pcms/transfer/transfer_operator.hpp" +#include + +namespace pcms +{ + +// Conservative Galerkin projection between Omega_h order-1 Lagrange spaces. +// +// Construction caches the source-target mesh intersections. Apply() assembles +// the projection system for the current source coefficients, solves it, and +// writes the projected scalar field into the target Field. +class OmegaHConservativeProjection : public TransferOperator +{ +public: + OmegaHConservativeProjection(const FunctionSpace& source_space, + const FunctionSpace& target_space); + + void Apply(const Field& source, Field& target) const override; + +private: + std::shared_ptr source_layout_; + std::shared_ptr target_layout_; + IntersectionResults intersections_; +}; + +} // namespace pcms + +#endif // PCMS_TRANSFER_OMEGA_H_CONSERVATIVE_PROJECTION_H diff --git a/src/pcms/transfer/petsc_utils.cpp b/src/pcms/transfer/petsc_utils.cpp new file mode 100644 index 000000000..fe51a4f5c --- /dev/null +++ b/src/pcms/transfer/petsc_utils.cpp @@ -0,0 +1,33 @@ +// petsc requires these headers be included before petscvec +#if defined(PETSC_HAVE_KOKKOS) +#include +#include +#endif + +#include "pcms/transfer/petsc_utils.hpp" + +namespace pcms +{ +PetscErrorCode createSeqAIJMat(MPI_Comm comm, PetscInt m, PetscInt n, + PetscInt nz, const PetscInt nnz[], Mat* mat) +{ + PetscFunctionBeginUser; +#if defined(PETSC_HAVE_KOKKOS) + PetscCall(MatCreateSeqAIJKokkos(comm, m, n, nz, nnz, mat)); +#else + PetscCall(MatCreateSeqAIJ(comm, m, n, nz, nnz, mat)); +#endif + PetscFunctionReturn(PETSC_SUCCESS); +} + +PetscErrorCode createSeqVec(MPI_Comm comm, PetscInt n, Vec* vec) +{ + PetscFunctionBeginUser; +#if defined(PETSC_HAVE_KOKKOS) + PetscCall(VecCreateSeqKokkos(comm, n, vec)); +#else + PetscCall(VecCreateSeq(comm, n, vec)); +#endif + PetscFunctionReturn(PETSC_SUCCESS); +} +} // namespace pcms diff --git a/src/pcms/transfer/petsc_utils.hpp b/src/pcms/transfer/petsc_utils.hpp new file mode 100644 index 000000000..77b12bb1e --- /dev/null +++ b/src/pcms/transfer/petsc_utils.hpp @@ -0,0 +1,30 @@ +#ifndef PCMS_TRANSFER_PETSC_UTILS_HPP +#define PCMS_TRANSFER_PETSC_UTILS_HPP + +#include +#include +#include +#include + +// GPU builds require PETSc with Kokkos support for device-resident +// matrix/vector types +#if (defined(KOKKOS_ENABLE_CUDA) || defined(KOKKOS_ENABLE_HIP)) && \ + !defined(PETSC_HAVE_KOKKOS) +#error "GPU build (CUDA/HIP) requires PETSc compiled with Kokkos support \ +(configure with --with-kokkos)" +#endif + +namespace pcms +{ +// Creates a sequential AIJ matrix using the Kokkos backend when available, +// falling back to standard sequential AIJ on CPU-only builds. +// Pass nz=0 and nnz=nullptr when using COO preallocation afterwards. +PetscErrorCode createSeqAIJMat(MPI_Comm comm, PetscInt m, PetscInt n, + PetscInt nz, const PetscInt nnz[], Mat* mat); + +// Creates a sequential vector using the Kokkos backend when available, +// falling back to standard sequential vector on CPU-only builds. +PetscErrorCode createSeqVec(MPI_Comm comm, PetscInt n, Vec* vec); +} // namespace pcms + +#endif // PCMS_TRANSFER_PETSC_UTILS_HPP diff --git a/src/pcms/transfer/transfer_operator.hpp b/src/pcms/transfer/transfer_operator.hpp new file mode 100644 index 000000000..adf36c1df --- /dev/null +++ b/src/pcms/transfer/transfer_operator.hpp @@ -0,0 +1,20 @@ +#ifndef PCMS_TRANSFER_TRANSFER_H +#define PCMS_TRANSFER_TRANSFER_H + +namespace pcms +{ + +template +class Field; + +template +class TransferOperator +{ +public: + virtual void Apply(const Field& source, Field& target) const = 0; + virtual ~TransferOperator() noexcept = default; +}; + +} // namespace pcms + +#endif diff --git a/src/pcms/transfer_field.h b/src/pcms/transfer_field.h deleted file mode 100644 index e92be53bf..000000000 --- a/src/pcms/transfer_field.h +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef PCMS_COUPLING_TRANSFER_FIELD_H -#define PCMS_COUPLING_TRANSFER_FIELD_H -#include -#include "pcms/utility/arrays.h" -#include "pcms/field_evaluation_methods.h" -#include "pcms/field.h" -#include "pcms/utility/profile.h" - -namespace pcms -{ - -/** - * Pointwise copy of data in source_field to target field. Source and target - * fields must have the same size and implicit field iteration order - * @tparam Field - * @param source_field - * @param target_field - */ -template -void copy_field(const Field& source_field, Field& target_field) -{ - PCMS_FUNCTION_TIMER; - const auto source_data = get_nodal_data(source_field); - set_nodal_data(target_field, make_array_view(source_data)); -} - -template > -void interpolate_field(const SourceField& source_field, - TargetField& target_field, EvaluationMethod method = {}) -{ - PCMS_FUNCTION_TIMER; - // TODO: same_topology - // if (same_topology(source_field, target_field)) { - // copy_field(source_field,target_field); - // return; - //} - - // One downside of this option is that it requires the source has to implement - // a get_nodal_coordinates function that wouldn't otherwise be needed - using source_coordinate_type = typename decltype(get_nodal_coordinates( - std::declval()))::value_type; - using target_coordinate_type = typename decltype(get_nodal_coordinates( - std::declval()))::value_type; - - static constexpr bool source_field_has_coordinate_system = - detail::HasCoordinateSystem::value; - static constexpr bool target_field_has_coordinate_system = - detail::HasCoordinateSystem::value; - static constexpr bool needs_coordinate_transform = - (source_field_has_coordinate_system && - target_field_has_coordinate_system) && - !std::is_same_v; - // field value_types must either both have coordinate systems, or both not - // have coordinate systems - static_assert(source_field_has_coordinate_system == - target_field_has_coordinate_system, - "cannot mix data with and without coordinate systems"); - - auto coordinates = get_nodal_coordinates(target_field); - auto coordinates_view = make_const_array_view(coordinates); - - if constexpr (needs_coordinate_transform) { - static_assert(!needs_coordinate_transform, - "coordinate transforms not finalized"); - // using target_coordinate_system = - // typename target_coordinate_type::coordinate_system; - // auto transformed_coordinates = - // coordinate_transform(coordinates_view); - // const auto data = - // evaluate(source_field, method, - // make_array_view(transformed_coordinates)); - // set_nodal_data(target_field, make_array_view(data)); - } else { - const auto data = evaluate(source_field, method, coordinates_view); - set_nodal_data(target_field, make_array_view(data)); - } -} -} // namespace pcms - -#endif // PCMS_COUPLING_TRANSFER_FIELD_H diff --git a/src/pcms/transfer_field2.h b/src/pcms/transfer_field2.h deleted file mode 100644 index 2ce53bbed..000000000 --- a/src/pcms/transfer_field2.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef PCMS_TRANSFER_FIELD2_H_ -#define PCMS_TRANSFER_FIELD2_H_ -#include -#include -#include "pcms/utility/arrays.h" -#include "field.h" -#include "pcms/field_evaluation_methods.h" -#include "pcms/utility/profile.h" -#include "pcms/field.h" - -namespace pcms -{ - -template -void copy_field2(const FieldT& source, FieldT& target) -{ - PCMS_FUNCTION_TIMER; - if (typeid(source) != typeid(target)) { - // TODO when moved to PCMS throw PCMS exception - throw std::runtime_error("Mismatched types"); - } - - target.SetDOFHolderData(source.GetDOFHolderData()); -} - -template -void interpolate_field2(const FieldT& source, FieldT& target) -{ - PCMS_FUNCTION_TIMER; - if (source.GetCoordinateSystem() != target.GetCoordinateSystem()) { - // TODO when moved to PCMS throw PCMS exception - throw std::runtime_error("Coordinate system mismatch"); - } - - auto coords = target.GetLayout().GetDOFHolderCoordinates(); - std::vector evaluation(coords.GetCoordinates().size() / 2); - FieldDataView data_view{make_array_view(evaluation), - source.GetCoordinateSystem()}; - auto locale = source.GetLocalizationHint(coords); - source.Evaluate(locale, data_view); - target.SetDOFHolderData(make_const_array_view(evaluation)); -} - -} // namespace pcms - -#endif // PCMS_TRANSFER_FIELD2_H_ diff --git a/src/pcms/utility/CMakeLists.txt b/src/pcms/utility/CMakeLists.txt index bb3d8f895..041069a82 100644 --- a/src/pcms/utility/CMakeLists.txt +++ b/src/pcms/utility/CMakeLists.txt @@ -2,13 +2,19 @@ set( PCMS_UTILITY_HEADERS arrays.h assert.h + bounding_box.h common.h + entity_types.h + mesh_geometry.h memory_spaces.h + mpi_type.h + omega_h_array_utils.h types.h array_mask.h inclusive_scan.h profile.h print.h + uniform_grid.h ) set( @@ -27,17 +33,13 @@ target_include_directories( "$" "$") target_link_libraries(pcms_utility PUBLIC Kokkos::kokkos perfstubs) -target_compile_features(pcms_utility PUBLIC cxx_std_17) +target_compile_features(pcms_utility PUBLIC cxx_std_20) set_target_properties( pcms_utility PROPERTIES OUTPUT_NAME pcmsutility EXPORT_NAME utility ) -if (PCMS_ENABLE_SPDLOG) - target_link_libraries(pcms_utility PUBLIC spdlog::spdlog) -endif () - ## export the library target_sources(pcms_utility PUBLIC FILE_SET utilities @@ -59,4 +61,4 @@ install( EXPORT pcms_utility-targets NAMESPACE pcms:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms -) \ No newline at end of file +) diff --git a/src/pcms/utility/array_mask.h b/src/pcms/utility/array_mask.h index 94e03bef3..b51f5c5cc 100644 --- a/src/pcms/utility/array_mask.h +++ b/src/pcms/utility/array_mask.h @@ -133,8 +133,8 @@ class ArrayMask PCMS_ALWAYS_ASSERT((LO)output_array.size() == mask_.size()); PCMS_ALWAYS_ASSERT((LO)filtered_data.size() == num_active_entries_); - REDEV_ALWAYS_ASSERT(filtered_data.size() == permutation.size() || - permutation.empty()); + PCMS_ALWAYS_ASSERT(filtered_data.size() == permutation.size() || + permutation.empty()); auto mask = mask_; Kokkos::parallel_for( Kokkos::RangePolicy(0, mask_.size()), diff --git a/src/pcms/utility/arrays.h b/src/pcms/utility/arrays.h index 3155cf472..29e0d4269 100644 --- a/src/pcms/utility/arrays.h +++ b/src/pcms/utility/arrays.h @@ -4,6 +4,8 @@ #include "pcms/utility/types.h" #include "pcms/utility/memory_spaces.h" +#include + namespace pcms { @@ -14,6 +16,18 @@ template struct memory_space_accessor : public Kokkos::default_accessor { using memory_space = MemorySpace; + + // Default constructor + memory_space_accessor() = default; + + // Converting constructor to allow adding const qualification + template >> + constexpr memory_space_accessor( + const memory_space_accessor&) noexcept + { + } }; } // namespace detail @@ -30,26 +44,30 @@ auto make_mdspan(const ContainerType& /* unused */) // TODO make_mdspan template -using View = Kokkos::mdspan< - ElementType, Kokkos::dextents, Kokkos::layout_right, - detail::memory_space_accessor, - MemorySpace>>; + typename LayoutPolicy = Kokkos::layout_right, typename IndexType = LO> +using View = + Kokkos::mdspan, LayoutPolicy, + detail::memory_space_accessor< + std::remove_reference_t, MemorySpace>>; -template -using Rank1View = View<1, ElementType, MemorySpace>; +template +using Rank1View = View<1, ElementType, MemorySpace, LayoutPolicy>; -template -using Rank2View = View<2, ElementType, MemorySpace>; +template +using Rank2View = View<2, ElementType, MemorySpace, LayoutPolicy>; -template -using Rank3View = View<3, ElementType, MemorySpace>; +template +using Rank3View = View<3, ElementType, MemorySpace, LayoutPolicy>; -template -using Rank4View = View<4, ElementType, MemorySpace>; +template +using Rank4View = View<4, ElementType, MemorySpace, LayoutPolicy>; template -using GlobalIDView = View<1, const GO, MemorySpace, GO>; +using GlobalIDView = View<1, const GO, MemorySpace, Kokkos::layout_right, GO>; namespace detail { @@ -97,6 +115,46 @@ struct arr_trait> template using element_type_t = typename arr_trait::value>::type; +// Layout selector: detects layout from source type +template > +struct layout_selector +{ + using type = Kokkos::layout_right; // Default for host arrays and std::array +}; + +// Specialization for types with array_layout (Kokkos Views) +template +struct layout_selector> +{ + using type = std::conditional_t< + std::is_same_v, + Kokkos::layout_left, Kokkos::layout_right>; +}; + +template +using layout_selector_t = typename layout_selector::type; + +// Default layout selector for memory spaces +// GPU memory spaces typically use LayoutLeft for better coalescing +template > +struct default_layout_for_memory_space +{ + using type = Kokkos::layout_right; // Default for host +}; + +#ifdef PCMS_HAS_DISTINCT_DEVICE_MEMORY_SPACE +// Specialization for device memory space - use LayoutLeft for GPU +template <> +struct default_layout_for_memory_space +{ + using type = Kokkos::layout_left; +}; +#endif + +template +using default_layout_for_memory_space_t = + typename default_layout_for_memory_space::type; + } // namespace detail // default implementation of make_array_view template , @@ -127,12 +185,70 @@ auto make_const_array_view(T& array) return Rank1View{data(array), size(array)}; } +// Helper functions to create mdspan views from Kokkos Views with layout +// detection +template +auto MakeRank1View(Kokkos::View& view) +{ + using KokkosView = Kokkos::View; + using MemorySpace = typename KokkosView::memory_space; + using LayoutPolicy = detail::layout_selector_t; + return Rank1View(view.data(), view.extent(0)); +} + +template +auto MakeRank1View(const Kokkos::View& view) +{ + using KokkosView = Kokkos::View; + using MemorySpace = typename KokkosView::memory_space; + using LayoutPolicy = detail::layout_selector_t; + return Rank1View(view.data(), + view.extent(0)); +} + +template +auto MakeRank2View(Kokkos::View& view) +{ + using KokkosView = Kokkos::View; + using MemorySpace = typename KokkosView::memory_space; + using LayoutPolicy = detail::layout_selector_t; + return Rank2View(view.data(), view.extent(0), + view.extent(1)); +} + +template +auto MakeRank2View(const Kokkos::View& view) +{ + using KokkosView = Kokkos::View; + using MemorySpace = typename KokkosView::memory_space; + using LayoutPolicy = detail::layout_selector_t; + return Rank2View( + view.data(), view.extent(0), view.extent(1)); +} + +template +auto MakeConstRank2View(const Kokkos::View& view) +{ + using KokkosView = Kokkos::View; + using MemorySpace = typename KokkosView::memory_space; + using LayoutPolicy = detail::layout_selector_t; + return Rank2View( + view.data(), view.extent(0), view.extent(1)); +} + +template +auto MakeConstRank2View(Omega_h::Read array, int dim) +{ + return Rank2View( + array.data(), array.size() / dim, dim); +} + // utility function to deep copy between layout incompatible views // layout imcompatible view can't be deep_copied directly between different // memory spaces, the workaround is provided from // https://kokkos.org/kokkos-core-wiki/API/core/view/deep_copy.html#how-to-get-layout-incompatible-views-copied template -auto deep_copy_mismatch_layouts(DestView& dest, const SrcView& src) +auto DeepCopyMismatchLayouts(DestView& dest, const SrcView& src) { static_assert(Kokkos::is_view::value && Kokkos::is_view::value, @@ -157,6 +273,18 @@ auto deep_copy_mismatch_layouts(DestView& dest, const SrcView& src) Kokkos::deep_copy(dest, src); } } +template +void ConvertMismatchLayoutView2D(DestView& dest, const SrcView& src) +{ + Kokkos::parallel_for( + "ConvertMismatchLayoutView2D", + Kokkos::RangePolicy(0, src.extent(0)), + KOKKOS_LAMBDA(int i) { + for (int j = 0; j < src.extent(1); ++j) { + dest(i, j) = src(i, j); + } + }); +} // utility function to fill a view with sequentially increasing values template @@ -165,5 +293,91 @@ void iota_view(Kokkos::View view, T start = 0) Kokkos::parallel_for( "iota_view", view.extent(0), KOKKOS_LAMBDA(LO i) { view[i] = start + i; }); } + +// utility function to copy device Rank1View to device Kokkos View +template +void CopyDeviceRank1ViewToDeviceView(Kokkos::View dest, + Rank1View src) +{ + if (dest.extent(0) != src.size()) { + throw pcms_error("CopyDeviceRank1ViewToDeviceView: size mismatch"); + } + Kokkos::parallel_for( + "CopyDeviceRank1ViewToDeviceView", dest.extent(0), + KOKKOS_LAMBDA(LO i) { dest(i) = src(i); }); +} + +// utility function to copy device Rank1View to host Kokkos View +template +void CopyDeviceRank1ViewToHostView(Kokkos::View dest, + Rank1View src) +{ + if (dest.extent(0) != src.size()) { + throw pcms_error("CopyDeviceRank1ViewToHostView: size mismatch"); + } + Kokkos::View src_tmp( + "CopyDeviceRank1ViewToHostView_tmp", src.size()); + Kokkos::parallel_for( + "CopyDeviceRank1ViewToHostView", src.size(), + KOKKOS_LAMBDA(LO i) { src_tmp(i) = src(i); }); + Kokkos::deep_copy(dest, src_tmp); +} + +template +void CopyHostRank1ViewToDeviceView(Kokkos::View dest, + Rank1View src) +{ + if (dest.extent(0) != src.size()) { + throw pcms_error("CopyHostRank1ViewToDeviceView: size mismatch"); + } + Kokkos::View src_tmp("CopyHostRank1ViewToDeviceView_tmp", + src.size()); + for (size_t i = 0; i < src.size(); ++i) { + src_tmp(i) = src(i); + } + Kokkos::deep_copy(dest, src_tmp); +} + +// utility function to copy from Rank1View to Rank1View +template +void CopyRank1ViewToHost(Rank1View dest, + Rank1View src) +{ + if (dest.size() != src.size()) { + throw pcms_error("CopyRank1ViewToHost: size mismatch"); + } + Kokkos::View src_tmp("CopyRank1ViewToHost_tmp", + src.size()); + Kokkos::parallel_for( + "CopyRank1ViewToHost", src.size(), + KOKKOS_LAMBDA(LO i) { src_tmp(i) = src(i); }); + Kokkos::View src_tmp_h("CopyRank1ViewToHost_tmp_h", + src.size()); + Kokkos::deep_copy(src_tmp_h, src_tmp); + for (size_t i = 0; i < src.size(); ++i) { + dest(i) = src_tmp_h(i); + } +} + +template +void CopyRank1ViewToDevice(Rank1View dest, + Rank1View src) +{ + if (dest.size() != src.size()) { + throw pcms_error("CopyRank1ViewToDevice: size mismatch"); + } + Kokkos::View src_tmp("CopyRank1ViewToDevice_tmp", + src.size()); + for (size_t i = 0; i < src.size(); ++i) { + src_tmp(i) = src(i); + } + Kokkos::View src_tmp_d("CopyRank1ViewToDevice_tmp_d", + src.size()); + Kokkos::deep_copy(src_tmp_d, src_tmp); + Kokkos::parallel_for( + "CopyRank1ViewToDevice", src.size(), + KOKKOS_LAMBDA(LO i) { dest(i) = src_tmp_d(i); }); +} + } // namespace pcms #endif // PCMS_COUPLING_ARRAYS_H diff --git a/src/pcms/bounding_box.h b/src/pcms/utility/bounding_box.h similarity index 89% rename from src/pcms/bounding_box.h rename to src/pcms/utility/bounding_box.h index 9bc7ca0ba..32a0c21b4 100644 --- a/src/pcms/bounding_box.h +++ b/src/pcms/utility/bounding_box.h @@ -10,9 +10,9 @@ template struct AABBox { static constexpr int dim = DIM; - std::array center; + Kokkos::Array center; // half length of bounding box - std::array half_width; + Kokkos::Array half_width; }; template diff --git a/src/pcms/utility/entity_types.h b/src/pcms/utility/entity_types.h new file mode 100644 index 000000000..90e36bb47 --- /dev/null +++ b/src/pcms/utility/entity_types.h @@ -0,0 +1,14 @@ +#ifndef PCMS_UTILITY_ENTITY_TYPES_H +#define PCMS_UTILITY_ENTITY_TYPES_H + +namespace pcms +{ + +inline constexpr int Vertex = 0; +inline constexpr int Edge = 1; +inline constexpr int Face = 2; +inline constexpr int Region = 3; + +} // namespace pcms + +#endif // PCMS_UTILITY_ENTITY_TYPES_H diff --git a/src/pcms/utility/memory_spaces.h b/src/pcms/utility/memory_spaces.h index 4be367f39..b927d6342 100644 --- a/src/pcms/utility/memory_spaces.h +++ b/src/pcms/utility/memory_spaces.h @@ -10,6 +10,20 @@ namespace pcms using HostMemorySpace = Kokkos::HostSpace; using DefaultExecutionSpace = Kokkos::DefaultExecutionSpace; +// DeviceMemorySpace is the memory space of the default GPU execution space when +// Kokkos is built with a GPU backend. When no GPU backend is present it aliases +// HostMemorySpace so that generic code using DeviceMemorySpace compiles without +// change. PCMS_HAS_DISTINCT_DEVICE_MEMORY_SPACE is defined only when the device +// memory space actually differs from the host memory space; use it to guard +// device-only overloads. +#if defined(KOKKOS_ENABLE_CUDA) || defined(KOKKOS_ENABLE_HIP) || \ + defined(KOKKOS_ENABLE_SYCL) +#define PCMS_HAS_DISTINCT_DEVICE_MEMORY_SPACE +using DeviceMemorySpace = Kokkos::DefaultExecutionSpace::memory_space; +#else +using DeviceMemorySpace = HostMemorySpace; +#endif + } // namespace pcms #endif // PCMS_COUPLING_MEMORY_SPACES_H diff --git a/src/pcms/utility/mesh_geometry.h b/src/pcms/utility/mesh_geometry.h new file mode 100644 index 000000000..ef40719a7 --- /dev/null +++ b/src/pcms/utility/mesh_geometry.h @@ -0,0 +1,98 @@ +#ifndef PCMS_UTILITY_MESH_GEOMETRY_H +#define PCMS_UTILITY_MESH_GEOMETRY_H + +#include +#include +#include + +namespace pcms +{ + +KOKKOS_INLINE_FUNCTION +Omega_h::Real distance_squared(const Omega_h::Real* p1, const Omega_h::Real* p2, + int dim) +{ + Omega_h::Real dx = p1[0] - p2[0]; + Omega_h::Real dy = p1[1] - p2[1]; + Omega_h::Real dz = (dim == 3) ? (p1[2] - p2[2]) : 0.0; + return dx * dx + dy * dy + dz * dz; +} + +inline Omega_h::Reals get_entity_centroids(Omega_h::Mesh& mesh, + Omega_h::Int entity_dim) +{ + OMEGA_H_CHECK_PRINTF(entity_dim >= Omega_h::VERT && entity_dim <= mesh.dim(), + "Unsupported entity_dim=%d for mesh dim=%d\n", + entity_dim, mesh.dim()); + + if (entity_dim == Omega_h::VERT) { + return mesh.coords(); + } + + const auto dim = mesh.dim(); + const auto nents = mesh.nents(entity_dim); + const auto ent2verts = mesh.ask_down(entity_dim, Omega_h::VERT).ab2b; + const auto coords = mesh.coords(); + Omega_h::Write centroids(nents * dim, 0.0, "entity centroids"); + + if (dim == 2 && entity_dim == Omega_h::EDGE) { + Omega_h::parallel_for( + "entity_centroids_edge_2d", nents, OMEGA_H_LAMBDA(const Omega_h::LO ent) { + const auto verts = Omega_h::gather_verts<2>(ent2verts, ent); + const auto vert_coords = Omega_h::gather_vectors<2, 2>(coords, verts); + const auto centroid = Omega_h::average(vert_coords); + centroids[2 * ent + 0] = centroid[0]; + centroids[2 * ent + 1] = centroid[1]; + }); + } else if (dim == 2 && entity_dim == Omega_h::FACE) { + Omega_h::parallel_for( + "entity_centroids_face_2d", nents, OMEGA_H_LAMBDA(const Omega_h::LO ent) { + const auto verts = Omega_h::gather_verts<3>(ent2verts, ent); + const auto vert_coords = Omega_h::gather_vectors<3, 2>(coords, verts); + const auto centroid = Omega_h::average(vert_coords); + centroids[2 * ent + 0] = centroid[0]; + centroids[2 * ent + 1] = centroid[1]; + }); + } else if (dim == 3 && entity_dim == Omega_h::EDGE) { + Omega_h::parallel_for( + "entity_centroids_edge_3d", nents, OMEGA_H_LAMBDA(const Omega_h::LO ent) { + const auto verts = Omega_h::gather_verts<2>(ent2verts, ent); + const auto vert_coords = Omega_h::gather_vectors<2, 3>(coords, verts); + const auto centroid = Omega_h::average(vert_coords); + centroids[3 * ent + 0] = centroid[0]; + centroids[3 * ent + 1] = centroid[1]; + centroids[3 * ent + 2] = centroid[2]; + }); + } else if (dim == 3 && entity_dim == Omega_h::FACE) { + Omega_h::parallel_for( + "entity_centroids_face_3d", nents, OMEGA_H_LAMBDA(const Omega_h::LO ent) { + const auto verts = Omega_h::gather_verts<3>(ent2verts, ent); + const auto vert_coords = Omega_h::gather_vectors<3, 3>(coords, verts); + const auto centroid = Omega_h::average(vert_coords); + centroids[3 * ent + 0] = centroid[0]; + centroids[3 * ent + 1] = centroid[1]; + centroids[3 * ent + 2] = centroid[2]; + }); + } else if (dim == 3 && entity_dim == Omega_h::REGION) { + Omega_h::parallel_for( + "entity_centroids_region_3d", nents, + OMEGA_H_LAMBDA(const Omega_h::LO ent) { + const auto verts = Omega_h::gather_verts<4>(ent2verts, ent); + const auto vert_coords = Omega_h::gather_vectors<4, 3>(coords, verts); + const auto centroid = Omega_h::average(vert_coords); + centroids[3 * ent + 0] = centroid[0]; + centroids[3 * ent + 1] = centroid[1]; + centroids[3 * ent + 2] = centroid[2]; + }); + } else { + OMEGA_H_CHECK_PRINTF( + false, "Centroid computation not implemented for entity_dim=%d dim=%d\n", + entity_dim, dim); + } + + return Omega_h::Reals(centroids); +} + +} // namespace pcms + +#endif // PCMS_UTILITY_MESH_GEOMETRY_H diff --git a/src/pcms/utility/mpi_type.h b/src/pcms/utility/mpi_type.h new file mode 100644 index 000000000..4c133f68f --- /dev/null +++ b/src/pcms/utility/mpi_type.h @@ -0,0 +1,77 @@ +#ifndef PCMS_MPI_TYPE_H +#define PCMS_MPI_TYPE_H + +#include "pcms/utility/types.h" +#include +#include + +namespace pcms +{ + +template +[[nodiscard]] constexpr MPI_Datatype GetMPIType(T) noexcept +{ + if constexpr (std::is_same_v) { + return MPI_CHAR; + } else if constexpr (std::is_same_v) { + return MPI_SHORT; + } else if constexpr (std::is_same_v) { + return MPI_INT; + } else if constexpr (std::is_same_v) { + return MPI_LONG; + } else if constexpr (std::is_same_v) { + return MPI_LONG_LONG; + } else if constexpr (std::is_same_v) { + return MPI_SIGNED_CHAR; + } else if constexpr (std::is_same_v) { + return MPI_UNSIGNED_CHAR; + } else if constexpr (std::is_same_v) { + return MPI_UNSIGNED_SHORT; + } else if constexpr (std::is_same_v) { + return MPI_UNSIGNED; + } else if constexpr (std::is_same_v) { + return MPI_UNSIGNED_LONG; + } else if constexpr (std::is_same_v) { + return MPI_UNSIGNED_LONG_LONG; + } else if constexpr (std::is_same_v) { + return MPI_FLOAT; + } else if constexpr (std::is_same_v) { + return MPI_DOUBLE; + } else if constexpr (std::is_same_v) { + return MPI_LONG_DOUBLE; + } else if constexpr (std::is_same_v) { + return MPI_WCHAR; + } else if constexpr (std::is_same_v) { + return MPI_INT8_T; + } else if constexpr (std::is_same_v) { + return MPI_INT16_T; + } else if constexpr (std::is_same_v) { + return MPI_INT32_T; + } else if constexpr (std::is_same_v) { + return MPI_INT64_T; + } else if constexpr (std::is_same_v) { + return MPI_UINT8_T; + } else if constexpr (std::is_same_v) { + return MPI_UINT16_T; + } else if constexpr (std::is_same_v) { + return MPI_UINT32_T; + } else if constexpr (std::is_same_v) { + return MPI_UINT64_T; + } else if constexpr (std::is_same_v) { + return MPI_CXX_BOOL; + } else if constexpr (std::is_same_v>) { + return MPI_CXX_FLOAT_COMPLEX; + } else if constexpr (std::is_same_v>) { + return MPI_CXX_DOUBLE_COMPLEX; + } else if constexpr (std::is_same_v>) { + return MPI_CXX_LONG_DOUBLE_COMPLEX; + } else { + static_assert(detail::dependent_always_false::value, + "type has unknown map to MPI_Datatype"); + return {}; + } +} + +} // namespace pcms + +#endif // PCMS_MPI_TYPE_H diff --git a/src/pcms/utility/omega_h_array_utils.h b/src/pcms/utility/omega_h_array_utils.h new file mode 100644 index 000000000..9b76d97a3 --- /dev/null +++ b/src/pcms/utility/omega_h_array_utils.h @@ -0,0 +1,70 @@ +#ifndef PCMS_UTILITY_OMEGA_H_ARRAY_UTILS_H +#define PCMS_UTILITY_OMEGA_H_ARRAY_UTILS_H + +#include +#include "pcms/utility/memory_spaces.h" + +namespace pcms +{ + +template +Omega_h::Reals flatten_to_omega_h_reals_host(const View2D& coords, + const char* label = "flat_coords") +{ + const int n = static_cast(coords.extent(0)); + const int dim = static_cast(coords.extent(1)); + + Omega_h::HostWrite flat(n * dim, label); + for (int i = 0; i < n; ++i) + for (int d = 0; d < dim; ++d) + flat[i * dim + d] = coords(i, d); + + return Omega_h::Reals(flat); +} + +template +Omega_h::Reals flatten_to_omega_h_reals(const View2D& coords, + const char* label = "flat_coords") +{ + const int n = static_cast(coords.extent(0)); + const int dim = static_cast(coords.extent(1)); + + Omega_h::Write flat(n * dim, label); + Kokkos::parallel_for( + "flatten_to_omega_h_reals", Kokkos::RangePolicy<>(0, n), + KOKKOS_LAMBDA(int i) { + for (int d = 0; d < dim; ++d) { + flat[i * dim + d] = coords(i, d); + } + }); + + return Omega_h::Reals(flat); +} + +// Convert 1D Omega_h coordinate array to 2D Kokkos view +// This copy is needed because Omega_h provides coordinates as a 1D array of +// length nents*dim, which is always layout_right. Directly creating a 2D mdspan +// with a different layout would access the 1D array with the wrong stride and +// lead to incorrect coordinates. By copying to a new 2D view, we ensure correct +// memory access regardless of the layout of the destination view. +template +inline Kokkos::View ConvertCoordsTo2D( + const Omega_h::Read& coords_1d, IntType1 nents, IntType2 dim) +{ + const int n = static_cast(nents); + const int d = static_cast(dim); + Kokkos::View coords_2d("coords_2d", n, d); + + Kokkos::parallel_for( + "convert_coords_to_2d", n * d, KOKKOS_LAMBDA(int i) { + int e = i / d; + int dd = i % d; + coords_2d(e, dd) = coords_1d[i]; + }); + + return coords_2d; +} + +} // namespace pcms + +#endif // PCMS_UTILITY_OMEGA_H_ARRAY_UTILS_H diff --git a/src/pcms/utility/print.cpp b/src/pcms/utility/print.cpp index 04ef43368..0f1747ff7 100644 --- a/src/pcms/utility/print.cpp +++ b/src/pcms/utility/print.cpp @@ -20,13 +20,13 @@ FILE* getStderr() void setStdout(FILE* out) { - assert(out != NULL); + assert(out != nullptr); pcms_stdout = out; } void setStderr(FILE* err) { - assert(err != NULL); + assert(err != nullptr); pcms_stderr = err; } -} // namespace pcms \ No newline at end of file +} // namespace pcms diff --git a/src/pcms/utility/print.h b/src/pcms/utility/print.h index d8b8fd820..5ac8fd3b1 100644 --- a/src/pcms/utility/print.h +++ b/src/pcms/utility/print.h @@ -3,11 +3,6 @@ #include "pcms/configuration.h" #include -#ifdef PCMS_ENABLE_SPDLOG -#include "spdlog/spdlog.h" -#include -#endif - #include namespace pcms @@ -27,9 +22,7 @@ void setStderr(FILE* err); template void printError(const char* fmt, const Args&... args) { -#if defined(PCMS_ENABLE_SPDLOG) && defined(PCMS_ENABLE_PRINT) - spdlog::error("{}", fmt::sprintf(fmt, args...)); -#elif defined(PCMS_ENABLE_PRINT) +#if defined(PCMS_ENABLE_PRINT) fprintf(getStdout(), fmt, args...); #endif } @@ -37,10 +30,7 @@ void printError(const char* fmt, const Args&... args) template KOKKOS_INLINE_FUNCTION void printInfo(const char* fmt, const Args&... args) { -#if defined(PCMS_ENABLE_SPDLOG) && defined(PCMS_ENABLE_PRINT) && \ - !defined(ACTIVE_GPU_EXECUTION) - spdlog::info("{}", fmt::sprintf(fmt, args...)); -#elif defined(PCMS_ENABLE_PRINT) && !defined(ACTIVE_GPU_EXECUTION) +#if defined(PCMS_ENABLE_PRINT) && !defined(ACTIVE_GPU_EXECUTION) fprintf(getStdout(), fmt, args...); #endif } @@ -48,16 +38,15 @@ KOKKOS_INLINE_FUNCTION void printInfo(const char* fmt, const Args&... args) template KOKKOS_INLINE_FUNCTION void printDebugInfo(const char* fmt, const Args&... args) { -#if !defined(NDEBUG) && defined(PCMS_PRINT_ENABLED) +#if !defined(NDEBUG) && defined(PCMS_ENABLE_PRINT) #if !defined(ACTIVE_GPU_EXECUTION) -#if defined(PCMS_SPDLOG_ENABLED) - spdlog::debug("{}", fmt::sprintf(fmt, args...)); -#else fprintf(getStdout(), fmt, args...); -#endif #else // For GPU execution printf(fmt, args...); #endif +#else + (void)fmt; + ((void)args, ...); #endif } diff --git a/src/pcms/utility/profile.h b/src/pcms/utility/profile.h index 9cc3b8208..6f20352af 100644 --- a/src/pcms/utility/profile.h +++ b/src/pcms/utility/profile.h @@ -2,6 +2,11 @@ #define PCMS_SRC_PCMS_PROFILE_H #include -#define PCMS_FUNCTION_TIMER PERFSTUBS_SCOPED_TIMER_FUNC() +// perfstubs currently has a buffer overflow +// I am disabling this, as it could cause surprising issues until I have time to +// understand and resolve the problem see +// https://github.com/SCOREC/pcms/issues/284 +// #define PCMS_FUNCTION_TIMER PERFSTUBS_SCOPED_TIMER_FUNC() +#define PCMS_FUNCTION_TIMER #endif // PCMS_SRC_PCMS_PROFILE_H diff --git a/src/pcms/utility/types.h b/src/pcms/utility/types.h index 97f9bbdcf..b36c72c8d 100644 --- a/src/pcms/utility/types.h +++ b/src/pcms/utility/types.h @@ -1,5 +1,6 @@ #ifndef PCMS_COUPLING_TYPES_H #define PCMS_COUPLING_TYPES_H +#include "assert.h" #include #include @@ -9,22 +10,13 @@ enum class Type { Real, LO, - GO + GO, + Int8, + Float }; using Real = double; using LO = int32_t; using GO = int64_t; -template -constexpr Type TypeEnumFromType(T) -{ - if constexpr (std::is_same_v) { - return Type::Real; - } else if constexpr (std::is_same_v) { - return Type::LO; - } else if constexpr (std::is_same_v) { - return Type::GO; - } -}; namespace detail { @@ -41,6 +33,41 @@ template using type_identity_t = typename type_identity::type; } // namespace detail +template +constexpr Type TypeEnumFromType() +{ + if constexpr (std::is_same_v) { + return Type::Real; + } else if constexpr (std::is_same_v) { + return Type::LO; + } else if constexpr (std::is_same_v) { + return Type::GO; + } else if constexpr (std::is_same_v) { + return Type::Int8; + } else if constexpr (std::is_same_v) { + return Type::Float; + } else { + static_assert(detail::dependent_always_false::value, + "T is not a supported field type"); + } +}; + +// Dispatches on a runtime Type value by instantiating F with the corresponding +// type tag. F must accept detail::type_identity for each of the five +// supported scalar types and return a consistent type. +template +auto apply_to_type(Type t, F&& f) +{ + switch (t) { + case Type::Int8: return f(detail::type_identity{}); + case Type::LO: return f(detail::type_identity{}); + case Type::GO: return f(detail::type_identity{}); + case Type::Float: return f(detail::type_identity{}); + case Type::Real: return f(detail::type_identity{}); + } + throw pcms_error("apply_to_type: unhandled Type value"); +} + } // namespace pcms #endif // PCMS_COUPLING_TYPES_H diff --git a/src/pcms/uniform_grid.h b/src/pcms/utility/uniform_grid.h similarity index 98% rename from src/pcms/uniform_grid.h rename to src/pcms/utility/uniform_grid.h index 8a89049b4..1a478fec3 100644 --- a/src/pcms/uniform_grid.h +++ b/src/pcms/utility/uniform_grid.h @@ -1,6 +1,6 @@ #ifndef PCMS_COUPLING_UNIFORM_GRID_H #define PCMS_COUPLING_UNIFORM_GRID_H -#include "pcms/bounding_box.h" +#include "pcms/utility/bounding_box.h" #include "Omega_h_vector.hpp" #include "Omega_h_bbox.hpp" #include "Omega_h_mesh.hpp" @@ -58,7 +58,7 @@ struct UniformGrid auto index = GetDimensionedIndex(idx); reverse(index); - std::array half_width, center; + Kokkos::Array half_width, center; for (size_t i = 0; i < dim; ++i) { half_width[i] = edge_length[i] / divisions[i] / 2; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a659be421..5ea457642 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -182,7 +182,7 @@ if(PCMS_ENABLE_OMEGA_H) TESTNAME test_proxy_coupling_4p TIMEOUT - 10 + 60 NAME1 rdv EXE1 @@ -372,20 +372,15 @@ if(Catch2_FOUND) test_coordinate.cpp test_bounding_box.cpp) if(PCMS_ENABLE_XGC) list(APPEND PCMS_UNIT_TEST_SOURCES test_xgc_reverse_classification.cpp - test_xgc_field_adapter.cpp) + test_xgc_field_data.cpp) endif() if(PCMS_ENABLE_OMEGA_H) - add_executable(field_transfer_example field_transfer_example.cpp) - target_link_libraries(field_transfer_example PUBLIC pcms::core - pcms::interpolator) list( APPEND PCMS_UNIT_TEST_SOURCES test_error_handling.cpp - test_field_transfer.cpp test_uniform_grid.cpp - test_omega_h_copy.cpp test_field_evaluation.cpp test_field_interpolation.cpp test_field_copy.cpp @@ -397,17 +392,60 @@ if(Catch2_FOUND) test_svd_serial.cpp test_uniform_grid_field.cpp test_interpolation_class.cpp - test_omega_h_field2_outofbounds.cpp) + test_mesh_geometry.cpp + test_polynomial_reconstruction_function_space.cpp + test_polynomial_reconstruction_mls_evaluation.cpp + test_localization_factory.cpp + test_omega_h_field2_outofbounds.cpp + test_omega_h_lagrange_field.cpp + test_point_evaluator.cpp) endif() + + if(PCMS_ENABLE_MFEM) + list(APPEND PCMS_UNIT_TEST_SOURCES test_mfem_adapter.cpp) + endif() + + if(PCMS_ENABLE_MESHFIELDS) + list(APPEND PCMS_UNIT_TEST_SOURCES + test_load_vector.cpp) + endif() + + if(PCMS_ENABLE_PETSC AND PCMS_ENABLE_MESHFIELDS) + list(APPEND PCMS_UNIT_TEST_SOURCES + test_mesh_intersection_field_transfer.cpp) + endif() + add_executable(unit_tests ${PCMS_UNIT_TEST_SOURCES}) + + if(PCMS_ENABLE_MESHFIELDS) + target_link_libraries(unit_tests PUBLIC meshfields::meshfields) + endif() + + if(PCMS_ENABLE_MFEM) + target_link_libraries(unit_tests PUBLIC mfem) + endif() + target_link_libraries(unit_tests PUBLIC Catch2::Catch2 pcms::core - pcms::interpolator Kokkos::kokkoskernels) + pcms::transfer Kokkos::kokkoskernels) target_include_directories(unit_tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + if(PCMS_ENABLE_PETSC) + target_link_libraries(unit_tests PRIVATE PETSc::PETSc) + endif() + + target_link_libraries(unit_tests PUBLIC + Catch2::Catch2 + pcms::core + pcms_transfer + pcms_transfer + ) + + target_include_directories(unit_tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + add_executable(test_interpolation_on_ltx_mesh test_interpolation_on_ltx_mesh.cpp) target_link_libraries(test_interpolation_on_ltx_mesh PUBLIC Catch2::Catch2WithMain pcms::core - pcms::interpolator) + pcms::transfer) target_include_directories(test_interpolation_on_ltx_mesh PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) add_test(NAME test_interpolation_on_ltx_mesh COMMAND test_interpolation_on_ltx_mesh @@ -416,12 +454,40 @@ if(Catch2_FOUND) --data_root ${PCMS_TEST_DATA_DIR}/ltx-interpolation-case/ ) - include(Catch) - catch_discover_tests(unit_tests) + include(Catch) + Catch_discover_tests(unit_tests) else() message(WARNING "Catch2 not found. Disabling Unit Tests") endif() +if(PCMS_ENABLE_MFEM) + add_executable(test_mfem_coupling test_mfem_coupling.cpp) + target_link_libraries(test_mfem_coupling PUBLIC pcms::core mfem) + if(HOST_NPROC GREATER_EQUAL 2) + dual_mpi_test( + TESTNAME + test_mfem_coupling + TIMEOUT + 60 + NAME1 + server + EXE1 + $ + PROCS1 + 1 + ARGS1 + -1 + NAME2 + client + EXE2 + $ + PROCS2 + 1 + ARGS2 + 0) + endif() +endif() + if(PCMS_ENABLE_C) find_package(Kokkos REQUIRED) if(PCMS_ENABLE_XGC) @@ -434,12 +500,17 @@ if(PCMS_ENABLE_C) test_proxy_coupling_xgc_server.cpp) target_link_libraries(test_proxy_couple_xgc_cpp_interface PUBLIC pcms::core MPI::MPI_C test_support) + # New test with proper overlap mask feature (no hacky ownership modification) + add_executable(test_proxy_couple_xgc_overlap_interface + test_proxy_coupling_xgc_server_overlap.cpp) + target_link_libraries(test_proxy_couple_xgc_overlap_interface + PUBLIC pcms::core MPI::MPI_C test_support) if(HOST_NPROC GREATER_EQUAL 3) dual_mpi_test( TESTNAME xgc_proxy_to_xgc TIMEOUT - 10 + 30 NAME1 cinterface EXE1 @@ -458,13 +529,37 @@ if(PCMS_ENABLE_C) ${d3d1p} ${d3d1p_cpn} 0) + # Test with proper overlap mask feature (XGC adapter) + dual_mpi_test( + TESTNAME + xgc_proxy_to_xgc_overlap + TIMEOUT + 30 + NAME1 + cinterface + EXE1 + $ + PROCS1 + 2 + ARGS1 + ${PCMS_TEST_DATA_DIR}/d3d/meshRclassification.txt + NAME2 + cppinterface_overlap + EXE2 + $ + PROCS2 + 1 + ARGS2 + ${d3d1p} + ${d3d1p_cpn} + 0) endif() if(HOST_NPROC GREATER_EQUAL 6) dual_mpi_test( TESTNAME xgc_proxy_to_omega TIMEOUT - 10 + 30 NAME1 cinterface EXE1 @@ -483,6 +578,30 @@ if(PCMS_ENABLE_C) ${d3d1p} ${d3d2p_cpn} 1) + # Test with proper overlap mask feature (Omega-h adapter) + dual_mpi_test( + TESTNAME + xgc_proxy_to_omega_overlap + TIMEOUT + 30 + NAME1 + cinterface + EXE1 + $ + PROCS1 + 4 + ARGS1 + ${PCMS_TEST_DATA_DIR}/d3d/meshRclassification.txt + NAME2 + cppinterface_overlap + EXE2 + $ + PROCS2 + 2 + ARGS2 + ${d3d1p} + ${d3d2p_cpn} + 1) endif() endif() endif() @@ -498,7 +617,7 @@ if(PCMS_ENABLE_Fortran) add_executable(test_interpolation_fortran_api test_interpolation.f90) target_link_libraries(test_interpolation_fortran_api - PUBLIC pcms::fortranapi::interpolator) + PUBLIC pcms::fortranapi::transfer) set_target_properties(test_interpolation_fortran_api PROPERTIES LINKER_LANGUAGE Fortran) add_test( @@ -512,7 +631,7 @@ if(PCMS_ENABLE_Fortran) TESTNAME xgc_fortran_proxy_to_omega TIMEOUT - 10 + 30 NAME1 finterface EXE1 diff --git a/test/field_test_utils.h b/test/field_test_utils.h new file mode 100644 index 000000000..f3b2a9bd5 --- /dev/null +++ b/test/field_test_utils.h @@ -0,0 +1,367 @@ +#ifndef PCMS_TEST_FIELD_TEST_UTILS_H +#define PCMS_TEST_FIELD_TEST_UTILS_H + +#include +#include +#include "pcms/field/field.h" +#include "pcms/field/field_data.h" +#include "pcms/field/field_evaluator_factory.h" +#include "pcms/field/evaluation_request.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/coupler/field_serializer.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/localization/adj_search.hpp" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include +#include +#include + +// Shared utilities for field tests that apply equally to MeshFields-backed +// and native OmegaH-backed field implementations. + +namespace pcms::test +{ + +// Affine test function — exactly representable on linear elements. +KOKKOS_INLINE_FUNCTION Real linear_f(Real x, Real y) +{ + return x + 2.0 * y; +} + +// Interior test points for a unit [0,1]^2 box mesh. +inline std::vector StandardEvalCoords2D() +{ + return {0.1, 0.2, 0.5, 0.5, 0.7, 0.3, 0.9, 0.1, 0.2, 0.8}; +} + +inline bool AreArraysEqualUnordered( + const Omega_h::HostRead& array1, + const Omega_h::HostRead& array2, int start, int end) +{ + std::unordered_map freq1, freq2; + for (int i = start; i < end; ++i) { + freq1[array1[i]]++; + freq2[array2[i]]++; + } + return freq1 == freq2; +} + +inline void CheckSupportResultsEquivalent(const SupportResults& actual, + const SupportResults& expected) +{ + auto actual_ptr = Omega_h::HostRead(actual.supports_ptr); + auto actual_idx = Omega_h::HostRead(actual.supports_idx); + auto expected_ptr = Omega_h::HostRead(expected.supports_ptr); + auto expected_idx = Omega_h::HostRead(expected.supports_idx); + + REQUIRE(actual_ptr.size() == expected_ptr.size()); + REQUIRE(actual_idx.size() == expected_idx.size()); + + for (int i = 0; i < actual_ptr.size(); ++i) + REQUIRE(actual_ptr[i] == expected_ptr[i]); + + for (int i = 0; i < actual_ptr.size() - 1; ++i) { + CAPTURE(i); + REQUIRE(AreArraysEqualUnordered(actual_idx, expected_idx, actual_ptr[i], + actual_ptr[i + 1])); + } +} + +inline std::vector CopyOmegaHRealsToVector(const Omega_h::Reals& coords) +{ + auto coords_read = Omega_h::HostRead(coords); + return std::vector(coords_read.data(), + coords_read.data() + coords_read.size()); +} + +// Copy coordinates from device memory to a host view. +// This handles potential layout mismatches between host and device memory +// spaces. +template +inline Kokkos::View CopyCoordinatesToHost( + const CoordView& coords_device, int nents, int dim) +{ + auto coords_view = + Kokkos::View("coords_view", nents, dim); + auto coords_view_device = + Kokkos::create_mirror(DeviceMemorySpace(), coords_view); + Kokkos::parallel_for( + "copy_coords_to_host_view", Kokkos::RangePolicy<>(0, nents), + KOKKOS_LAMBDA(int i) { + for (int d = 0; d < dim; ++d) { + coords_view_device(i, d) = coords_device(i, d); + } + }); + Kokkos::deep_copy(coords_view, coords_view_device); + return coords_view; +} + +template +inline std::vector EvaluateReferenceFunction(const std::vector& pts, + Func func) +{ + using MemorySpace = typename ExecutionSpace::memory_space; + + int n = static_cast(pts.size()) / 2; + auto pts_host = Kokkos::View(pts.data(), n, 2); + auto pts_device = + Kokkos::create_mirror_view_and_copy(DeviceMemorySpace(), pts_host); + + Kokkos::View expected_device("expected_device", n); + Kokkos::parallel_for( + "field_test_utils_expected", Kokkos::RangePolicy(0, n), + KOKKOS_LAMBDA(int i) { + expected_device(i) = func(pts_device(i, 0), pts_device(i, 1)); + }); + + auto expected_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), expected_device); + return std::vector(expected_host.data(), + expected_host.data() + expected_host.extent(0)); +} + +template +struct SetFieldFunctor +{ + Kokkos::View data; + Kokkos::View coords; + Func f; + + SetFieldFunctor(Kokkos::View data_, + Kokkos::View coords_, Func f_) + : data(data_), coords(coords_), f(f_) + { + } + + KOKKOS_INLINE_FUNCTION + void operator()(int i) const { data(i) = f(coords(i, 0), coords(i, 1)); } +}; + +// Set scalar DOF data by sampling func at each DOF-holder coordinate. +template +inline void SetField(const FieldLayout& layout, FieldData& field, + Func func) +{ + using MemorySpace = typename ExecutionSpace::memory_space; + + auto dof_coords = layout.GetDOFHolderCoordinates().GetCoordinates(); + int n = static_cast(dof_coords.extent(0)); + Kokkos::View coords_device("coords_device", n); + Kokkos::parallel_for( + "field_test_utils_copy_coords", Kokkos::RangePolicy(0, n), + KOKKOS_LAMBDA(int i) { + coords_device(i, 0) = dof_coords(i, 0); + coords_device(i, 1) = dof_coords(i, 1); + }); + + Kokkos::View data_device("data_device", n); + Kokkos::parallel_for("field_test_utils_set_field", + Kokkos::RangePolicy(0, n), + SetFieldFunctor{data_device, coords_device, func}); + + auto data_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), data_device); + field.SetDOFHolderDataHost( + Rank1View(data_host.data(), n)); +} + +template +inline void SetField(Field& field, Func func) +{ + SetField(field.GetLayout(), field.GetData(), func); +} + +template +inline void SetField(FieldData& field, const FieldLayout& layout, + Func func) +{ + SetField(layout, field, func); +} + +// Check that serialize followed by deserialize round-trips the data. +// Uses an identity permutation so permutation[i] = i. +inline void CheckSerializeDeserialize(const FieldLayout& layout, + FieldData& field) +{ + auto data_before = field.GetDOFHolderDataHost(); + int n = static_cast(data_before.size()); + + std::vector buffer(n); + std::vector perm(n); + for (int i = 0; i < n; ++i) + perm[i] = i; + + Rank1View buf_view(buffer.data(), n); + Rank1View perm_view(perm.data(), n); + + FieldSerializer serializer; + serializer.Serialize(field, layout, buf_view, perm_view); + serializer.Deserialize( + field, layout, Rank1View(buf_view), perm_view); + + auto data_after = field.GetDOFHolderDataHost(); + REQUIRE(data_after.size() == data_before.size()); + for (int i = 0; i < n; ++i) { + REQUIRE(data_after[i] == Catch::Approx(data_before[i])); + } +} + +inline void CheckSerializeDeserialize(Field& field) +{ + CheckSerializeDeserialize(field.GetLayout(), field.GetData()); +} + +// Helper structure to hold device coordinates with proper lifetime management +struct DeviceCoordinates +{ + Kokkos::View view; + CoordinateView coordinate_view; +}; + +// Helper function to create device CoordinateView from a vector of interleaved +// points Returns both the underlying View (to keep memory alive) and the +// CoordinateView pts contains interleaved coordinates: [x0, y0, x1, y1, ...] +// for 2D or [x0, y0, z0, x1, y1, z1, ...] for 3D +inline DeviceCoordinates CreateDeviceCoordinateView( + const std::vector& pts, CoordinateSystem coord_system, int dim = 2) +{ + int n = static_cast(pts.size()) / dim; + // Create host view from input data + Kokkos::View coords_host("coords_host", n, dim); + for (int i = 0; i < n; ++i) { + for (int d = 0; d < dim; ++d) { + coords_host(i, d) = pts[dim * i + d]; + } + } + // Copy to device - using default layout for device + auto coords_device = + Kokkos::View("coords_device", n, dim); + DeepCopyMismatchLayouts(coords_device, coords_host); + auto coords_view = pcms::MakeRank2View(coords_device); + return DeviceCoordinates{coords_device, CoordinateView{ + coord_system, coords_view}}; +} + +// Evaluate field at explicit test points using a PointEvaluator and check +// results. +template +void CheckEvaluation(const PointEvaluator& evaluator, + const Field& field, const std::vector& pts, + Func func, double abs_tol = 1e-10) +{ + int n = static_cast(pts.size()) / 2; + + Kokkos::View out_device("out_device", n); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + Rank2View out(out_device.data(), n, 1); + // Rank2View out(eval.data(), n, 1); + evaluator.Evaluate(field, out); + auto out_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); + + auto expected = EvaluateReferenceFunction(pts, func); + + for (int i = 0; i < n; ++i) { + INFO("Point " << i << " (" << pts[2 * static_cast(i)] << ", " + << pts[2 * static_cast(i) + 1] << ")" + << " got=" << out_host(i) << " expected=" << expected[i]); + REQUIRE(out_host(i) == Catch::Approx(expected[i]).margin(abs_tol)); + } +} + +// Overload that creates the evaluator from any factory with +// CreatePointEvaluator and GetCoordinateSystem (e.g. LagrangeFunctionSpace, +// FieldEvaluatorFactory). +template +void CheckEvaluation(const Factory& factory, const Field& field, + const std::vector& pts, Func func, + double abs_tol = 1e-10) +{ + auto device_coords = + CreateDeviceCoordinateView(pts, factory.GetCoordinateSystem()); + auto evaluator = factory.template CreatePointEvaluator( + EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + CheckEvaluation(*evaluator, field, pts, func, abs_tol); +} + +// Evaluate field at points known to be outside the mesh and verify fill value. +inline void CheckFillMode(const PointEvaluator& evaluator, + const Field& field, Real fill_value, + const std::vector& outside_pts) +{ + int n = static_cast(outside_pts.size()) / 2; + + Kokkos::View out_device("out_device", n); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + Rank2View out(out_device.data(), n, 1); + evaluator.Evaluate(field, out); + auto out_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); + + for (int i = 0; i < n; ++i) { + REQUIRE(out_host(i) == fill_value); + } +} + +// Overload that creates the evaluator from any factory with FILL policy. +template +void CheckFillMode(const Factory& factory, const Field& field, + Real fill_value, const std::vector& outside_pts) +{ + auto device_coords = + CreateDeviceCoordinateView(outside_pts, factory.GetCoordinateSystem()); + OutOfBoundsPolicy policy{OutOfBoundsMode::FILL, fill_value}; + auto evaluator = factory.template CreatePointEvaluator( + EvaluationRequest::FromCoordinates(device_coords.coordinate_view, policy)); + CheckFillMode(*evaluator, field, fill_value, outside_pts); +} + +// Check a mix of inside/outside points. inside points verified against func, +// outside points against fill_value. +template +void CheckEvaluationWithFill(const Factory& factory, const Field& field, + const std::vector& pts, + const std::vector& is_inside, Func func, + Real fill_value, double abs_tol = 1e-10) +{ + int n = static_cast(pts.size()) / 2; + REQUIRE(static_cast(is_inside.size()) == n); + + auto device_coords = + CreateDeviceCoordinateView(pts, factory.GetCoordinateSystem()); + OutOfBoundsPolicy policy{OutOfBoundsMode::FILL, fill_value}; + auto evaluator = factory.template CreatePointEvaluator( + EvaluationRequest::FromCoordinates(device_coords.coordinate_view, policy)); + + Kokkos::View out_device("out_device", n); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + Rank2View out(out_device.data(), n, 1); + evaluator->Evaluate(field, out); + auto out_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); + + auto expected = EvaluateReferenceFunction(pts, func); + + for (int i = 0; i < n; ++i) { + INFO("Point " << i << " (" << pts[2 * static_cast(i)] << ", " + << pts[2 * static_cast(i) + 1] << ")" + << " got=" << out_host(i)); + if (is_inside[i]) { + INFO(" expected=" << expected[i]); + REQUIRE(out_host(i) == Catch::Approx(expected[i]).margin(abs_tol)); + } else { + REQUIRE(out_host(i) == fill_value); + } + } +} + +} // namespace pcms::test + +#endif // PCMS_TEST_FIELD_TEST_UTILS_H diff --git a/test/field_transfer_example.cpp b/test/field_transfer_example.cpp deleted file mode 100644 index 2939dcc5d..000000000 --- a/test/field_transfer_example.cpp +++ /dev/null @@ -1,117 +0,0 @@ -#include -#include -#include -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -// for transfer operation dummy test -#include -#include - -using pcms::Real; -using OHField = pcms::MeshFieldsAdapter; -using OHShim = pcms::OmegaHFieldAdapter; -using pcms::copy_field; -using pcms::get_nodal_coordinates; -using pcms::get_nodal_data; -using pcms::interpolate_field; -using pcms::make_array_view; -using pcms::set_nodal_data; - -inline constexpr int num_trials = 1000; - -struct MeanCombiner -{ - void operator()(const std::vector>& fields, - OHField& combined) const - { - auto field_size = combined.Size(); - Omega_h::Write combined_array(field_size); - for (auto& field : fields) { - assert(field.get().Size() == field_size); - auto field_array = get_nodal_data(field.get()); - Omega_h::parallel_for( - field_size, - OMEGA_H_LAMBDA(int i) { combined_array[i] += field_array[i]; }); - } - auto num_fields = fields.size(); - Omega_h::parallel_for( - field_size, OMEGA_H_LAMBDA(int i) { - combined_array[i] = combined_array[i] / num_fields; - }); - set_nodal_data(combined, make_array_view(Omega_h::Read(combined_array))); - } -}; -void SetApplicationFields(const OHField& app_a_field, - const OHField& app_b_field); - -using pcms::CoupledField; -using pcms::FieldCommunicator; -using pcms::InternalField; -using pcms::ProcessType; - -void test_standalone(Omega_h::Mesh& internal_mesh, Omega_h::Mesh& app_mesh) -{ - OHField app_a_field("app_a_field", app_mesh); - OHField app_b_field("app_b_field", app_mesh); - SetApplicationFields(app_a_field, app_b_field); - - OHField internal_app_a_field("internal_app_a_field", internal_mesh); - OHField internal_app_b_field("internal_app_b_field", internal_mesh); - OHField internal_combined("internal_combined", internal_mesh); - - for (int i = 0; i < num_trials; ++i) { - interpolate_field(app_a_field, internal_app_a_field, pcms::Lagrange<1>{}); - interpolate_field(app_b_field, internal_app_b_field, - pcms::NearestNeighbor{}); - - // combine after interpolation - MeanCombiner combiner{}; - std::vector> internal_fields{ - internal_app_a_field, internal_app_b_field}; - - combiner(internal_fields, internal_combined); - } -} -void SetApplicationFields(const OHField& app_a_field, - const OHField& app_b_field) -{ - auto app_nodal_coords = get_nodal_coordinates(app_a_field); - Omega_h::Write values_a(app_a_field.Size()); - Omega_h::Write values_b(app_b_field.Size()); - assert(app_a_field.Size() == app_b_field.Size()); - assert(app_a_field.Size() == app_nodal_coords.size() / 2); - for (int i = 0; i < app_nodal_coords.size() / 2; ++i) { - auto x = (app_nodal_coords[i * 2] - 0.5) * 3.1415 * 2; - auto y = (app_nodal_coords[i * 2 + 1] - 0.5) * 3.1415 * 2; - values_b[i] = sin(x) * sin(y); - values_a[i] = cos(x) * cos(y); - } - set_nodal_data(app_a_field, make_array_view(Omega_h::Read(values_a))); - set_nodal_data(app_b_field, make_array_view(Omega_h::Read(values_b))); -} - -int main(int argc, char** argv) -{ - - auto lib = Omega_h::Library{&argc, &argv}; - auto world = lib.world(); - auto internal_mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 40, 40, 0, false); - auto app_mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 10, 10, 0, false); - - assert(internal_mesh.dim() == 2 && app_mesh.dim() == 2); - auto point1 = std::chrono::steady_clock::now(); - test_standalone(internal_mesh, app_mesh); - auto point2 = std::chrono::steady_clock::now(); - - Omega_h::vtk::write_parallel("internal_mesh.vtk", &internal_mesh, - internal_mesh.dim()); - Omega_h::vtk::write_parallel("app_mesh.vtk", &app_mesh, app_mesh.dim()); - auto point3 = std::chrono::steady_clock::now(); - std::cout << "Test Standalone: " - << std::chrono::duration(point2 - point1).count() << "\n"; - std::cout << "Write Files: " - << std::chrono::duration(point3 - point2).count() << "\n"; - - return 0; -} diff --git a/test/test_bounding_box.cpp b/test/test_bounding_box.cpp index bda75d9bf..f29f9567a 100644 --- a/test/test_bounding_box.cpp +++ b/test/test_bounding_box.cpp @@ -1,5 +1,5 @@ #include -#include +#include using pcms::AABBox; using pcms::intersects; diff --git a/test/test_coordinate.cpp b/test/test_coordinate.cpp index b016d3b3a..849ab1c6a 100644 --- a/test/test_coordinate.cpp +++ b/test/test_coordinate.cpp @@ -1,7 +1,7 @@ #include #include -#include -#include +#include +#include #include "mdspan/mdspan.hpp" #include diff --git a/test/test_coordinate_transform.cpp b/test/test_coordinate_transform.cpp index 58f4bf681..2c9c76734 100644 --- a/test/test_coordinate_transform.cpp +++ b/test/test_coordinate_transform.cpp @@ -1,4 +1,4 @@ -#include +#include #include TEST_CASE("cartesian to spherical") {} diff --git a/test/test_field_communication.cpp b/test/test_field_communication.cpp index 53eb41466..b27ef4287 100644 --- a/test/test_field_communication.cpp +++ b/test/test_field_communication.cpp @@ -3,14 +3,16 @@ #include #include #include +#include #include #include #include #include -#include "pcms/adapter/meshfields/mesh_fields_adapter2.h" -#include "pcms/field_communicator2.h" -#include "pcms/field_communicator.h" -#include "pcms/create_field.h" +#include "pcms/coupler/field_communicator.hpp" +#include "pcms/coupler/coupler.hpp" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/field_metadata.h" +#include "pcms/field/data/simple.h" #include "test_support.h" namespace ts = test_support; @@ -59,6 +61,61 @@ redev::ClassPtn setupServerPartition(Omega_h::Mesh& mesh, return redev::ClassPtn(MPI_COMM_WORLD, ptn.ranks, ptn.modelEnts); } +// Test that two fields sharing a layout via AddLayout use the same +// FieldLayoutCommunicator rather than creating separate ones. +static void test_shared_layout(Omega_h::Library& lib, + std::string_view mesh_file, + std::string_view cpn_file, bool is_server) +{ + auto world = lib.world(); + MPI_Comm mpi_comm = world->get_impl(); + Omega_h::Mesh mesh(&lib); + Omega_h::binary::read(std::string(mesh_file).c_str(), lib.world(), &mesh); + + if (is_server) { + auto partition = setupServerPartition(mesh, cpn_file); + pcms::Coupler cpl("shared_layout_server", mpi_comm, true, + redev::Partition{partition}); + auto* app = cpl.AddApplication("shared_layout"); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto layout = factory.GetLayout(); + app->AddLayout("shared", layout); + PCMS_ALWAYS_ASSERT(app->GetLayoutCommunicatorCount() == 1); + auto f1 = app->AddField("field_a", factory.CreateField()); + PCMS_ALWAYS_ASSERT(app->GetLayoutCommunicatorCount() == + 1); // still 1 after adding field + auto f2 = app->AddField("field_b", factory.CreateField()); + PCMS_ALWAYS_ASSERT(app->GetLayoutCommunicatorCount() == 1); // still 1 + app->ReceivePhase([&]() { + f1.Receive(); + f2.Receive(); + }); + app->SendPhase([&]() { + f1.Send(); + f2.Send(); + }); + } else { + pcms::Coupler cpl("shared_layout_client", mpi_comm, false, + redev::Partition{}); + auto* app = cpl.AddApplication("shared_layout"); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto layout = factory.GetLayout(); + app->AddLayout("shared", layout); + auto f1 = app->AddField("field_a", factory.CreateField()); + auto f2 = app->AddField("field_b", factory.CreateField()); + app->SendPhase([&]() { + f1.Send(); + f2.Send(); + }); + app->ReceivePhase([&]() { + f1.Receive(); + f2.Receive(); + }); + } +} + void client1(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, int order, const adios2::Params& params) { @@ -66,9 +123,10 @@ void client1(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, auto channel = rdv.CreateAdiosChannel("field2_chan1", params, redev::TransportType::BP4); - auto layout = pcms::CreateLagrangeLayout(mesh, order, 1, - pcms::CoordinateSystem::Cartesian); - auto gids = layout->GetGids(); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, order, 1, pcms::CoordinateSystem::Cartesian); + auto layout = factory.GetLayout(); + auto gids = layout->GetGidsHost(); const auto n = layout->GetNumOwnedDofHolder(); Omega_h::HostWrite ids(n); PCMS_ALWAYS_ASSERT(n == gids.size()); @@ -76,12 +134,13 @@ void client1(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, "id gid", Kokkos::RangePolicy(0, n), [=](int i) { ids[i] = gids[i]; }); - auto field = layout->CreateFieldReal(); - field->SetDOFHolderData(pcms::make_const_array_view(ids)); + auto field = factory.CreateField(pcms::FieldMetadata{}); + field.SetDOFHolderDataHost(pcms::make_const_array_view(ids)); pcms::FieldLayoutCommunicator layout_comm(comm_name + "1", comm, rdv, channel, *layout); - pcms::FieldCommunicator2 field_comm(layout_comm, *field); + pcms::FieldCommunicator field_comm(layout_comm.GetName(), + layout_comm, field); channel.BeginSendCommunicationPhase(); field_comm.Send(); @@ -95,22 +154,24 @@ void client2(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, auto channel = rdv.CreateAdiosChannel("field2_chan2", params, redev::TransportType::BP4); - auto layout = pcms::CreateLagrangeLayout(mesh, order, 1, - pcms::CoordinateSystem::Cartesian); - auto gids = layout->GetGids(); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, order, 1, pcms::CoordinateSystem::Cartesian); + auto layout = factory.GetLayout(); + auto gids = layout->GetGidsHost(); const auto n = layout->GetNumOwnedDofHolder(); - auto field = layout->CreateFieldReal(); + auto field = factory.CreateField(pcms::FieldMetadata{}); pcms::FieldLayoutCommunicator layout_comm(comm_name + "2", comm, rdv, channel, *layout); - pcms::FieldCommunicator2 field_comm(layout_comm, *field); + pcms::FieldCommunicator field_comm(layout_comm.GetName(), + layout_comm, field); channel.BeginReceiveCommunicationPhase(); field_comm.Receive(); channel.EndReceiveCommunicationPhase(); - auto copied_array = field->GetDOFHolderData(); - auto owned = layout->GetOwned(); + auto copied_array = field.GetDOFHolderDataHost(); + auto owned = layout->GetOwnedHost(); PCMS_ALWAYS_ASSERT(copied_array.size() == gids.size()); PCMS_ALWAYS_ASSERT(owned.size() == gids.size()); @@ -153,21 +214,24 @@ void server(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, auto channel2 = rdv.CreateAdiosChannel("field2_chan2", params, redev::TransportType::BP4); - auto layout = pcms::CreateLagrangeLayout(mesh, order, 1, - pcms::CoordinateSystem::Cartesian); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, order, 1, pcms::CoordinateSystem::Cartesian); + auto layout = factory.GetLayout(); const auto n = layout->GetNumOwnedDofHolder(); Omega_h::HostWrite ids(n); Kokkos::parallel_for( "id 0", Kokkos::RangePolicy(0, n), [=](int i) { ids[i] = 0; }); - auto field = layout->CreateFieldReal(); + auto field = factory.CreateField(pcms::FieldMetadata{}); pcms::FieldLayoutCommunicator layout_comm1(comm_name + "1", comm, rdv, channel1, *layout); pcms::FieldLayoutCommunicator layout_comm2(comm_name + "2", comm, rdv, channel2, *layout); - pcms::FieldCommunicator2 field_comm1(layout_comm1, *field); - pcms::FieldCommunicator2 field_comm2(layout_comm2, *field); + pcms::FieldCommunicator field_comm1(layout_comm1.GetName(), + layout_comm1, field); + pcms::FieldCommunicator field_comm2(layout_comm2.GetName(), + layout_comm2, field); channel1.BeginReceiveCommunicationPhase(); field_comm1.Receive(); @@ -180,34 +244,50 @@ void server(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, int main(int argc, char** argv) { - auto lib = Omega_h::Library(&argc, &argv); - auto world = lib.world(); - int rank = world->rank(); - if (argc != 4) { - std::cerr << "Usage: " << argv[0] - << " /path/to/omega_h/mesh" - << "/path/to/partitionFile.cpn\n"; - exit(EXIT_FAILURE); - } - int clientId = atoi(argv[1]); - REDEV_ALWAYS_ASSERT(clientId >= -1 && clientId <= 1); - const auto meshFile = argv[2]; - const auto classPartitionFile = argv[3]; - - Omega_h::Mesh mesh = Omega_h::binary::read(meshFile, world); - adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; - MPI_Comm mpi_comm = lib.world()->get_impl(); - - switch (clientId) { - case -1: - server(mpi_comm, mesh, "lin_field_comm", 1, params, classPartitionFile); - break; - case 0: client1(mpi_comm, mesh, "lin_field_comm", 1, params); break; - case 1: client2(mpi_comm, mesh, "lin_field_comm", 1, params); break; - default: - std::cerr << "Unhandled client id (should be -1,0,1)\n"; + try { + auto lib = Omega_h::Library(&argc, &argv); + auto world = lib.world(); + int rank = world->rank(); + if (argc != 4) { + std::cerr << "Usage: " << argv[0] + << " /path/to/omega_h/mesh" + << " /path/to/partitionFile.cpn\n"; exit(EXIT_FAILURE); - } + } + int clientId = atoi(argv[1]); + REDEV_ALWAYS_ASSERT(clientId >= -1 && clientId <= 3); + const auto meshFile = argv[2]; + const auto classPartitionFile = argv[3]; - return 0; + Omega_h::Mesh mesh = Omega_h::binary::read(meshFile, world); + adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; + MPI_Comm mpi_comm = lib.world()->get_impl(); + + switch (clientId) { + case -1: + server(mpi_comm, mesh, "lin_field_comm", 1, params, classPartitionFile); + break; + case 0: client1(mpi_comm, mesh, "lin_field_comm", 1, params); break; + case 1: client2(mpi_comm, mesh, "lin_field_comm", 1, params); break; + case 2: + test_shared_layout(lib, meshFile, classPartitionFile, + /*is_server=*/true); + break; + case 3: + test_shared_layout(lib, meshFile, classPartitionFile, + /*is_server=*/false); + break; + default: + std::cerr << "Unhandled client id (should be -1,0,1,2,3)\n"; + exit(EXIT_FAILURE); + } + + return 0; + } catch (const std::exception& e) { + std::cerr << "Exception caught in main: " << e.what() << std::endl; + return 1; + } catch (...) { + std::cerr << "Unknown exception caught in main" << std::endl; + return 1; + } } diff --git a/test/test_field_copy.cpp b/test/test_field_copy.cpp index 015a8167c..988a62d4f 100644 --- a/test/test_field_copy.cpp +++ b/test/test_field_copy.cpp @@ -3,10 +3,10 @@ #include #include #include -#include -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter2.h" -#include "pcms/create_field.h" +#include "pcms/transfer/copy.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/field_metadata.h" +#include "pcms/utility/assert.h" #include using pcms::Real; @@ -19,20 +19,23 @@ void test_copy(Omega_h::CommPtr world, int dim, int order, int num_components) auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, nx, ny, nz, false); - auto layout = pcms::CreateLagrangeLayout(mesh, order, num_components, - pcms::CoordinateSystem::Cartesian); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, order, num_components, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto layout = factory.GetLayout(); int ndata = layout->GetNumOwnedDofHolder() * num_components; Omega_h::HostWrite ids(ndata); Kokkos::parallel_for( Kokkos::RangePolicy(0, ndata), [=](int i) { ids[i] = i; }); - auto original = layout->CreateFieldReal(); - original->SetDOFHolderData(pcms::make_const_array_view(ids)); + auto original = factory.CreateField(pcms::FieldMetadata{}); + original.SetDOFHolderDataHost(pcms::make_const_array_view(ids)); - auto copied = layout->CreateFieldReal(); - pcms::copy_field2(*original, *copied); - auto copied_array = copied->GetDOFHolderData(); + auto copied = factory.CreateField(pcms::FieldMetadata{}); + pcms::Copy copy(factory, factory); + copy.Apply(original, copied); + auto copied_array = copied.GetDOFHolderDataHost(); REQUIRE(copied_array.size() == ndata); int sum = 0; @@ -48,6 +51,12 @@ void test_copy(Omega_h::CommPtr world, int dim, int order, int num_components) TEST_CASE("copy omega_h_field2 data") { auto lib = Omega_h::Library{}; + test_copy(lib.world(), 2, 0, 1); test_copy(lib.world(), 2, 1, 1); - test_copy(lib.world(), 2, 2, 1); + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 1, 100, + 100, 0, false); + REQUIRE_THROWS_AS(pcms::LagrangeFunctionSpace::FromMesh( + mesh, 2, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH), + pcms::pcms_error); } diff --git a/test/test_field_evaluation.cpp b/test/test_field_evaluation.cpp index 7a25defee..2a2094a72 100644 --- a/test/test_field_evaluation.cpp +++ b/test/test_field_evaluation.cpp @@ -1,140 +1,99 @@ #include -#include -#include #include #include -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter2.h" -#include "pcms/create_field.h" -#include -#include +#include +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/field_metadata.h" +#include "pcms/utility/assert.h" +#include "field_test_utils.h" +#include using pcms::Real; -TEST_CASE("evaluate linear 2d omega_h_field") +// Non-linear test function used to exercise quadratic interpolation. +KOKKOS_INLINE_FUNCTION static Real sin_f(Real x, Real y) { - auto lib = Omega_h::Library{}; - auto world = lib.world(); - auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 100, 100, 0, false); - auto layout = - pcms::CreateLagrangeLayout(mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - const auto nverts = mesh.nents(0); - auto mesh_coords = mesh.coords(); - auto f = KOKKOS_LAMBDA(Real x, Real y) - { - return std::sin(20 * x * y) / 2 + 0.5; - }; - Omega_h::Write test_f(nverts); - Omega_h::parallel_for( - nverts, OMEGA_H_LAMBDA(int i) { - Real x = mesh_coords[2 * i + 0]; - Real y = mesh_coords[2 * i + 1]; - test_f[i] = f(x, y); - }); - Omega_h::HostWrite test_f_host(test_f); - auto field = layout->CreateFieldReal(); - field->SetDOFHolderData(pcms::make_const_array_view(test_f_host)); - - std::vector coords = { - 0.7681, 0.886, 0.5337, 0.5205, 0.8088, 0.1513, 0.13, - 0.43, 0.5484, 0.8263, 0.006119, 0.8642, 0.5889, 0.5622, - 0.9268, 0.1749, 0.2615, 0.1468, 0.9793, 0.9612, - }; - - std::vector evaluation(coords.size() / 2); - pcms::Rank1View eval_view{evaluation.data(), - evaluation.size()}; - pcms::Rank2View coords_view( - coords.data(), coords.size() / 2, 2); - pcms::FieldDataView data_view( - eval_view, field->GetCoordinateSystem()); - pcms::CoordinateView coordinate_view{ - field->GetCoordinateSystem(), coords_view}; - - auto locale = field->GetLocalizationHint(coordinate_view); - field->Evaluate(locale, data_view); - - for (int i = 0; i < coords.size() / 2; ++i) { - Real x = coords[2 * i + 0]; - Real y = coords[2 * i + 1]; + return std::sin(20 * x * y) / 2 + 0.5; +} - Real test_value = evaluation[i]; - Real reference_value = f(x, y); - Real percent_error = - 100 * std::abs(test_value - reference_value) / reference_value; +// Standard interior test points shared across all evaluation tests. +static const std::vector kEvalCoords = { + 0.7681, 0.886, 0.5337, 0.5205, 0.8088, 0.1513, 0.13, + 0.43, 0.5484, 0.8263, 0.006119, 0.8642, 0.5889, 0.5622, + 0.9268, 0.1749, 0.2615, 0.1468, 0.9793, 0.9612, +}; - REQUIRE(percent_error < 1.0); - } +TEST_CASE("evaluate linear 2d omega_h_field") +{ + auto lib = Omega_h::Library{}; + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 100, + 100, 0, false); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + pcms::test::SetField( + field.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + pcms::test::CheckEvaluation( + factory, field, pcms::test::StandardEvalCoords2D(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); } -TEST_CASE("evaluate quadratic 2d omega_h_field") +#ifdef PCMS_ENABLE_MESHFIELDS +TEST_CASE("evaluate quadratic 2d meshfields_field") { auto lib = Omega_h::Library{}; - auto world = lib.world(); - auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 100, 100, 0, false); - auto layout = - pcms::CreateLagrangeLayout(mesh, 2, 1, pcms::CoordinateSystem::Cartesian); + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 100, + 100, 0, false); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 2, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); + + // Quadratic DOF holders span vertices and edge midpoints; set them inline. const auto nverts = mesh.nents(0); const auto nedges = mesh.nents(1); auto mesh_coords = mesh.coords(); auto edge_verts = mesh.ask_verts_of(1); - auto f = KOKKOS_LAMBDA(Real x, Real y) - { - return std::sin(20 * x * y) / 2 + 0.5; - }; + Omega_h::Write test_f(nverts + nedges); Omega_h::parallel_for( nverts, OMEGA_H_LAMBDA(int i) { - Real x = mesh_coords[2 * i + 0]; - Real y = mesh_coords[2 * i + 1]; - test_f[i] = f(x, y); + test_f[i] = sin_f(mesh_coords[2 * static_cast(i)], + mesh_coords[2 * static_cast(i) + 1]); }); Omega_h::parallel_for( nedges, OMEGA_H_LAMBDA(int i) { - auto endpoints = Omega_h::gather_verts<2>(edge_verts, i); - Real x0 = mesh_coords[2 * endpoints[0] + 0]; - Real y0 = mesh_coords[2 * endpoints[0] + 1]; - Real x1 = mesh_coords[2 * endpoints[1] + 0]; - Real y1 = mesh_coords[2 * endpoints[1] + 1]; - Real cx = (x0 + x1) / 2; - Real cy = (y0 + y1) / 2; - test_f[nverts + i] = f(cx, cy); + auto ep = Omega_h::gather_verts<2>(edge_verts, i); + Real cx = (mesh_coords[2 * static_cast(ep[0])] + + mesh_coords[2 * static_cast(ep[1])]) / + 2; + Real cy = (mesh_coords[2 * static_cast(ep[0]) + 1] + + mesh_coords[2 * static_cast(ep[1]) + 1]) / + 2; + test_f[nverts + i] = sin_f(cx, cy); }); Omega_h::HostWrite test_f_host(test_f); - auto field = layout->CreateFieldReal(); - field->SetDOFHolderData(pcms::make_const_array_view(test_f_host)); - - std::vector coords = { - 0.7681, 0.886, 0.5337, 0.5205, 0.8088, 0.1513, 0.13, - 0.43, 0.5484, 0.8263, 0.006119, 0.8642, 0.5889, 0.5622, - 0.9268, 0.1749, 0.2615, 0.1468, 0.9793, 0.9612, - }; - - std::vector evaluation(coords.size() / 2); - pcms::Rank1View eval_view{evaluation.data(), - evaluation.size()}; - pcms::Rank2View coords_view( - coords.data(), coords.size() / 2, 2); - pcms::FieldDataView data_view( - eval_view, field->GetCoordinateSystem()); - pcms::CoordinateView coordinate_view{ - field->GetCoordinateSystem(), coords_view}; - - auto locale = field->GetLocalizationHint(coordinate_view); - field->Evaluate(locale, data_view); - - for (int i = 0; i < coords.size() / 2; ++i) { - Real x = coords[2 * i + 0]; - Real y = coords[2 * i + 1]; + auto field = factory.CreateField(pcms::FieldMetadata{}); + field.GetData().SetDOFHolderDataHost( + pcms::make_const_array_view(test_f_host)); + + pcms::test::CheckEvaluation( + factory, field, kEvalCoords, + OMEGA_H_LAMBDA(Real x, Real y) { return std::sin(20 * x * y) / 2 + 0.5; }, + 1.0e-2); +} +#endif - Real test_value = evaluation[i]; - Real reference_value = f(x, y); - Real percent_error = - 100 * std::abs(test_value - reference_value) / reference_value; +TEST_CASE("evaluate quadratic 2d omega_h_field throws") +{ + auto lib = Omega_h::Library{}; + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 100, + 100, 0, false); - REQUIRE(percent_error < 1.0); - } + REQUIRE_THROWS_AS(pcms::LagrangeFunctionSpace::FromMesh( + mesh, 2, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH), + pcms::pcms_error); } diff --git a/test/test_field_interpolation.cpp b/test/test_field_interpolation.cpp index 413f06ca3..dde9cf80b 100644 --- a/test/test_field_interpolation.cpp +++ b/test/test_field_interpolation.cpp @@ -4,75 +4,67 @@ #include #include #include -#include -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter2.h" -#include "pcms/create_field.h" +#include "pcms/transfer/interpolator.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/field_metadata.h" +#include "pcms/utility/assert.h" +#include "field_test_utils.h" #include #include using pcms::Real; +KOKKOS_INLINE_FUNCTION static Real interpolation_linear_f(Real x, Real y) +{ + return -0.3 * x + 0.5 * y; +} + TEST_CASE("interpolate linear 2d omega_h_field") { auto lib = Omega_h::Library{}; auto world = lib.world(); auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 100, 100, 0, false); - auto layout = - pcms::CreateLagrangeLayout(mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - const auto nverts = mesh.nents(0); - auto mesh_coords = mesh.coords(); - auto f = KOKKOS_LAMBDA(Real x, Real y) - { - return -0.3 * x + 0.5 * y; - }; - Omega_h::Write test_f(nverts); - Omega_h::parallel_for( - nverts, OMEGA_H_LAMBDA(int i) { - Real x = mesh_coords[2 * i + 0]; - Real y = mesh_coords[2 * i + 1]; - test_f[i] = f(x, y); - }); - Omega_h::HostWrite test_f_host(test_f); - auto field = layout->CreateFieldReal(); - auto interpolated = layout->CreateFieldReal(); - field->SetDOFHolderData(pcms::make_const_array_view(test_f_host)); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + auto interpolated = factory.CreateField(pcms::FieldMetadata{}); + pcms::test::SetField( + field, OMEGA_H_LAMBDA(Real x, Real y) { return -0.3 * x + 0.5 * y; }); - pcms::interpolate_field2(*field, *interpolated); - auto interpolated_dof = interpolated->GetDOFHolderData(); - auto original_dof = field->GetDOFHolderData(); + pcms::Interpolator interp(factory, factory); + interp.Apply(field, interpolated); + auto interpolated_dof = interpolated.GetDOFHolderDataHost(); + auto original_dof = field.GetDOFHolderDataHost(); REQUIRE(interpolated_dof.size() == original_dof.size()); - // assumes that GetDOFHolderData will return a host view - for (int i = 0; i < interpolated_dof.size(); ++i) { + for (int i = 0; i < static_cast(interpolated_dof.size()); ++i) { REQUIRE_THAT(interpolated_dof[i], Catch::Matchers::WithinRel(original_dof[i], 0.001) || Catch::Matchers::WithinAbs(original_dof[i], 1E-10)); } } -TEST_CASE("interpolate quadratic 2d omega_h_field") +#ifdef PCMS_ENABLE_MESHFIELDS +TEST_CASE("interpolate quadratic 2d meshfields_field") { auto lib = Omega_h::Library{}; auto world = lib.world(); auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 100, 100, 0, false); - auto layout = - pcms::CreateLagrangeLayout(mesh, 2, 1, pcms::CoordinateSystem::Cartesian); + auto factory2 = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 2, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); + auto layout = factory2.GetLayout(); const auto nverts = mesh.nents(0); const auto nedges = mesh.nents(1); auto mesh_coords = mesh.coords(); auto edge_verts = mesh.ask_verts_of(1); - auto f = KOKKOS_LAMBDA(Real x, Real y) - { - return -0.3 * x + 0.5 * y; - }; Omega_h::Write test_f(nverts + nedges); Omega_h::parallel_for( nverts, OMEGA_H_LAMBDA(int i) { Real x = mesh_coords[2 * i + 0]; Real y = mesh_coords[2 * i + 1]; - test_f[i] = f(x, y); + test_f[i] = interpolation_linear_f(x, y); }); Omega_h::parallel_for( nedges, OMEGA_H_LAMBDA(int i) { @@ -83,25 +75,87 @@ TEST_CASE("interpolate quadratic 2d omega_h_field") Real y1 = mesh_coords[2 * endpoints[1] + 1]; Real cx = (x0 + x1) / 2; Real cy = (y0 + y1) / 2; - test_f[nverts + i] = f(cx, cy); + test_f[nverts + i] = interpolation_linear_f(cx, cy); }); Omega_h::HostWrite test_f_host(test_f); - auto field = layout->CreateFieldReal(); - auto interpolated = layout->CreateFieldReal(); - field->SetDOFHolderData(pcms::make_const_array_view(test_f_host)); + auto field = factory2.CreateField(pcms::FieldMetadata{}); + auto interpolated = factory2.CreateField(pcms::FieldMetadata{}); + field.SetDOFHolderDataHost(pcms::make_const_array_view(test_f_host)); - // interpolate the field from one mesh to another mesh with the same - // coordinates - pcms::interpolate_field2(*field, *interpolated); + pcms::Interpolator interp(factory2, factory2); + interp.Apply(field, interpolated); - auto interpolated_dof = interpolated->GetDOFHolderData(); - auto original_dof = field->GetDOFHolderData(); + auto interpolated_dof = interpolated.GetDOFHolderDataHost(); + auto original_dof = field.GetDOFHolderDataHost(); REQUIRE(interpolated_dof.size() == original_dof.size()); - // assumes that GetDOFHolderData will return a host view - for (int i = 0; i < interpolated_dof.size(); ++i) { + for (int i = 0; i < static_cast(interpolated_dof.size()); ++i) { REQUIRE_THAT(interpolated_dof[i], Catch::Matchers::WithinRel(original_dof[i], 0.001) || Catch::Matchers::WithinAbs(original_dof[i], 1E-10)); } } +#endif + +TEST_CASE("interpolate quadratic 2d omega_h_field throws") +{ + auto lib = Omega_h::Library{}; + auto world = lib.world(); + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 100, 100, 0, false); + + REQUIRE_THROWS_AS(pcms::LagrangeFunctionSpace::FromMesh( + mesh, 2, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH), + pcms::pcms_error); +} + +// Interpolator test: construct once (localize), apply twice with different +// source data, verify correct results each time. This demonstrates that the +// localization only happens once (at construction) while Apply can be called +// cheaply in a coupling loop. +TEST_CASE("Interpolator: construct once, apply twice with different data") +{ + auto lib = Omega_h::Library{}; + auto world = lib.world(); + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 100, 100, 0, false); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + + auto source = factory.CreateField(pcms::FieldMetadata{}); + auto target = factory.CreateField(pcms::FieldMetadata{}); + + // Construct Interpolator once — localization happens here. + // Because source and target share the same layout (same factory), + // the target DOF holder coordinates are the source mesh node coordinates. + pcms::Interpolator interp(factory, factory); + + // First application: linear function f(x,y) = -0.3*x + 0.5*y + pcms::test::SetField( + source, OMEGA_H_LAMBDA(Real x, Real y) { return -0.3 * x + 0.5 * y; }); + interp.Apply(source, target); + + { + auto src_dof = source.GetDOFHolderDataHost(); + auto tgt_dof = target.GetDOFHolderDataHost(); + REQUIRE(tgt_dof.size() == src_dof.size()); + for (int i = 0; i < static_cast(tgt_dof.size()); ++i) { + REQUIRE_THAT(tgt_dof[i], Catch::Matchers::WithinRel(src_dof[i], 0.001) || + Catch::Matchers::WithinAbs(src_dof[i], 1E-10)); + } + } + + // Second application: constant function f(x,y) = 7.0 + // Apply is called without re-constructing the interpolator (no + // re-localization). + pcms::test::SetField(source, OMEGA_H_LAMBDA(Real, Real) { return 7.0; }); + interp.Apply(source, target); + + { + auto tgt_dof = target.GetDOFHolderDataHost(); + for (int i = 0; i < static_cast(tgt_dof.size()); ++i) { + REQUIRE_THAT(tgt_dof[i], Catch::Matchers::WithinAbs(7.0, 1E-10)); + } + } +} diff --git a/test/test_field_transfer.cpp b/test/test_field_transfer.cpp deleted file mode 100644 index 0e3f1ab5e..000000000 --- a/test/test_field_transfer.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include -#include -#include -#include - -TEST_CASE("field copy", "[field transfer]") -{ - Omega_h::Library lib; - auto mesh = - Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 1, 10, 10, 0, false); - pcms::MeshFieldsAdapter f1("source", mesh); - Omega_h::Write data(mesh.nents(0)); - Omega_h::parallel_for(data.size(), OMEGA_H_LAMBDA(int i) { data[i] = i; }); - mesh.add_tag(0, "source", 1, data); - pcms::MeshFieldsAdapter f2("target", mesh); - pcms::copy_field(f1, f2); - auto target_array = mesh.get_array(0, "target"); - REQUIRE(target_array.size() == mesh.nents(0)); - int result = 0; - Kokkos::parallel_reduce( - target_array.size(), - KOKKOS_LAMBDA(int i, int& lsum) { lsum += target_array[i]; }, result); - auto n = target_array.size() - 1; - REQUIRE(result == n * (n + 1) / 2); -} -static int sum_array(const Omega_h::Read& target_array) -{ - int result = 0; - Kokkos::parallel_reduce( - target_array.size(), - KOKKOS_LAMBDA(int i, int& lsum) { lsum += target_array[i]; }, result); - return result; -} - -TEST_CASE("field interpolation (identical fields)", "[field transfer]") -{ - Omega_h::Library lib; - auto mesh = - Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 1, 10, 10, 0, false); - pcms::MeshFieldsAdapter f1("source", mesh); - f1.ConstructSearch(10, 10); - Omega_h::Write data(mesh.nents(0)); - Omega_h::parallel_for(data.size(), OMEGA_H_LAMBDA(int i) { data[i] = i; }); - mesh.add_tag(0, "source", 1, data); - pcms::MeshFieldsAdapter f2("target", mesh); - - SECTION("Nearest Neighbor") - { - pcms::interpolate_field(f1, f2, pcms::NearestNeighbor{}); - auto target_array = mesh.get_array(0, "target"); - REQUIRE(target_array.size() == mesh.nents(0)); - int result = sum_array(target_array); - auto n = target_array.size() - 1; - REQUIRE(result == n * (n + 1) / 2); - } - SECTION("Lagrange<1>") - { - pcms::interpolate_field(f1, f2, pcms::Lagrange<1>{}); - auto target_array = mesh.get_array(0, "target"); - REQUIRE(target_array.size() == mesh.nents(0)); - int result = sum_array(target_array); - auto n = target_array.size() - 1; - REQUIRE(result == n * (n + 1) / 2); - } -} diff --git a/test/test_interpolation_class.cpp b/test/test_interpolation_class.cpp index 5d9e459eb..7a34af4da 100644 --- a/test/test_interpolation_class.cpp +++ b/test/test_interpolation_class.cpp @@ -7,7 +7,8 @@ #include #include -#include +#include +#include #include #include @@ -132,9 +133,10 @@ TEST_CASE("Test MLSMeshInterpolation") "Started --------------------\n"); pcms::printInfo("Mesh based search...\n"); auto mls_single = - MLSMeshInterpolation(source_mesh, 0.12, 15, 3, true, 0.0, 5.0); + pcms::MLSMeshInterpolation(source_mesh, 0.12, 15, 3, true, 0.0, 5.0); - auto source_points_reals = getCentroids(source_mesh); + auto source_points_reals = + pcms::get_entity_centroids(source_mesh, Omega_h::FACE); auto source_points_host = Omega_h::HostRead(source_points_reals); auto source_points_host_write = @@ -157,7 +159,7 @@ TEST_CASE("Test MLSMeshInterpolation") target_points_host_write.data(), target_points_host_write.size()); REQUIRE(source_mesh.dim() == 2); pcms::printInfo("Point cloud based search...\n"); - auto point_mls = MLSPointCloudInterpolation( + auto point_mls = pcms::MLSPointCloudInterpolation( source_points_view, target_points_view, 2, 0.12, 15, 3, true, 0.0, 5.0); Omega_h::Write sinxcosy_centroid(source_mesh.nfaces(), @@ -280,8 +282,8 @@ TEST_CASE("Test MLSMeshInterpolation") // translate_mesh(&target_mesh, Omega_h::Vector<2>{(1.0 - 0.999) / 2.0, // (1.0 - 0.999) / 2.0}); - auto mls_double = MLSMeshInterpolation(source_mesh, target_mesh, 0.12, 15, - 3, true, 0.0, 5.0); + auto mls_double = pcms::MLSMeshInterpolation(source_mesh, target_mesh, 0.12, + 15, 3, true, 0.0, 5.0); Omega_h::HostWrite source_data_host_write(source_sinxcosy_node); Omega_h::HostWrite interpolated_data_hwrite( @@ -309,6 +311,51 @@ TEST_CASE("Test MLSMeshInterpolation") } } +TEST_CASE("MLSPointCloudInterpolation honors provided dimension in eval") +{ + auto source_points = Omega_h::HostWrite(27 * 3); + int idx = 0; + for (int z = 0; z < 3; ++z) { + for (int y = 0; y < 3; ++y) { + for (int x = 0; x < 3; ++x) { + source_points[idx++] = x; + source_points[idx++] = y; + source_points[idx++] = z; + } + } + } + auto target_points = source_points; + + auto source_points_view = pcms::Rank1View( + source_points.data(), source_points.size()); + auto target_points_view = pcms::Rank1View( + target_points.data(), target_points.size()); + + // Degree-1 polynomial that depends on z to catch accidental 2D behavior. + auto source_values = Omega_h::HostWrite(27); + for (int i = 0; i < 27; ++i) { + const auto x = source_points[3 * i + 0]; + const auto y = source_points[3 * i + 1]; + const auto z = source_points[3 * i + 2]; + source_values[i] = 1.0 + 2.0 * x - 3.0 * y + 5.0 * z; + } + + auto output_values = Omega_h::HostWrite(27, "output_values"); + auto source_values_view = pcms::Rank1View( + source_values.data(), source_values.size()); + auto output_values_view = pcms::Rank1View( + output_values.data(), output_values.size()); + + auto mls = pcms::MLSPointCloudInterpolation( + source_points_view, target_points_view, 3, 2.5, 10, 1, true, 0.0, 5.0); + + REQUIRE_NOTHROW(mls.eval(source_values_view, output_values_view)); + for (int i = 0; i < output_values.size(); ++i) { + CHECK_THAT(output_values[i], + Catch::Matchers::WithinAbs(source_values[i], 1e-6)); + } +} + bool isClose(Omega_h::HostWrite& array1, Omega_h::HostWrite& array2, double percent_diff) { diff --git a/test/test_interpolation_on_ltx_mesh.cpp b/test/test_interpolation_on_ltx_mesh.cpp index 6d33d4072..a348acae9 100644 --- a/test/test_interpolation_on_ltx_mesh.cpp +++ b/test/test_interpolation_on_ltx_mesh.cpp @@ -13,7 +13,8 @@ #include #include #include -#include +#include +#include #include #include @@ -78,7 +79,7 @@ TEST_CASE("Test Interpolation on LTX Mesh", "[interpolation]") // --------------------- Initialize Interpolators -------------- // const int degas2_num_elems = degas2_mesh.nelems(); const auto degas2_mesh_centroids_host = - Omega_h::HostRead(getCentroids(degas2_mesh)); + Omega_h::HostRead(pcms::get_entity_centroids(degas2_mesh, Omega_h::FACE)); printf("[INFO] Degas2 Mesh loaded from %s with %d elements\n", degas2_mesh_filename.c_str(), degas2_num_elems); const auto degas2_mesh_centroids_view = @@ -93,12 +94,12 @@ TEST_CASE("Test Interpolation on LTX Mesh", "[interpolation]") pcms::Rank1View( xgc_mesh_points.data(), xgc_mesh_points.size()); - auto xgc_to_degas2_interpolator = - MLSPointCloudInterpolation(xgc_mesh_points_view, degas2_mesh_centroids_view, - 2, 0.000001, 10, 1, true, 0.0, 50.0); - auto degas2_to_xgc_interpolator = - MLSPointCloudInterpolation(degas2_mesh_centroids_view, xgc_mesh_points_view, - 2, 0.01, 10, 1, true, 1e-3, 50.0); + auto xgc_to_degas2_interpolator = pcms::MLSPointCloudInterpolation( + xgc_mesh_points_view, degas2_mesh_centroids_view, 2, 0.000001, 10, 1, true, + 0.0, 50.0); + auto degas2_to_xgc_interpolator = pcms::MLSPointCloudInterpolation( + degas2_mesh_centroids_view, xgc_mesh_points_view, 2, 0.01, 10, 1, true, + 1e-3, 50.0); printf("[INFO] Interpolators initialized.\n"); // ---------------------- Load Data ---------------------- // @@ -471,8 +472,8 @@ void write_xgc_mesh_as_vtu( file << " \n"; for (size_t i = 0; i < n_points; ++i) { - file << " " << node_coords[2 * i] << " " << node_coords[2 * i + 1] - << " 0.0\n"; + file << " " << node_coords[2 * static_cast(i)] << " " + << node_coords[2 * static_cast(i) + 1] << " 0.0\n"; } file << " \n"; file << " \n"; diff --git a/test/test_intersections.cpp b/test/test_intersections.cpp new file mode 100644 index 000000000..02598f0a3 --- /dev/null +++ b/test/test_intersections.cpp @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TEST_CASE("Mesh intersection test with source and target", "[intersection]") +{ + + Omega_h::Library lib; + + Omega_h::Reals coords_target({ + 0.0, 0.0, // v0 + 1.0, 0.0, // v1 + 1.0, 1.0, // v2 + 0.0, 1.0 // v3 + }); + + // Target Mesh with two triangles + // Two triangles, CCW + // T0: (v0,v1,v3) = (0,1,3) + // T1: (v1,v2,v3) = (1,2,3) + Omega_h::LOs ev2v_target({0, 1, 3, 1, 2, 3}); + + Omega_h::Mesh tgt_mesh(&lib); + Omega_h::build_from_elems_and_coords(&tgt_mesh, OMEGA_H_SIMPLEX, 2, + ev2v_target, coords_target); + + // 2D coordinates (x,y) : 4 vertices + Omega_h::Reals coords_source({ + 0.0, 0.0, // v0 + 0.5, 0.0, // v1 + 1.0, 0.0, // v2 + 0.0, 0.5, // v3 + 0.5, 0.5, // v4 + 1.0, 0.5, // v5 + 0.0, 1.0, // v6 + 0.5, 1.0, // v7 + 1.0, 1.0 // v8 + }); + + // TARGET triangulations + Omega_h::LOs ev2v_source( + {0, 1, 4, 0, 4, 3, 1, 2, 5, 1, 5, 4, 3, 4, 7, 3, 7, 6, 4, 5, 8, 4, 8, 7}); + Omega_h::Mesh src_mesh(&lib); + Omega_h::build_from_elems_and_coords(&src_mesh, OMEGA_H_SIMPLEX, 2, + ev2v_source, coords_source); + + REQUIRE(src_mesh.dim() == 2); + REQUIRE(tgt_mesh.dim() == 2); + + int num_tgt_elems = tgt_mesh.nelems(); + + REQUIRE(src_mesh.dim() == 2); + REQUIRE(tgt_mesh.dim() == 2); + + const int nsrc = src_mesh.nelems(); + const int ntgt = tgt_mesh.nelems(); + + REQUIRE(nsrc > 0); + REQUIRE(ntgt > 0); + + const auto src_coords = src_mesh.coords(); + const auto tgt_coords = tgt_mesh.coords(); + + const auto src_faces2verts = + src_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto tgt_faces2verts = + tgt_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + + auto tgt_elm_area = Omega_h::measure_elements_real(&tgt_mesh); + + // ============================ + // Compute intersection mapping + // ============================ + auto intersection = intersectTargets(src_mesh, tgt_mesh); + + auto offsets = Omega_h::HostRead(intersection.tgt2src_offsets); + auto indices = Omega_h::HostRead(intersection.tgt2src_indices); + + SECTION("Offsets and indices sizes") + { + REQUIRE(offsets.size() == static_cast(ntgt + 1)); + REQUIRE(indices.size() == static_cast(offsets[ntgt])); + } + + SECTION("All intersection elements indices valid") + { + for (int i = 0; i < indices.size(); ++i) { + REQUIRE(indices[i] >= 0); + REQUIRE(indices[i] < nsrc); + } + } + + SECTION("Each target element should intersect at least one source element") + { + for (int t = 0; t < ntgt; ++t) { + REQUIRE(offsets[t + 1] - offsets[t] > 0); + } + } + + SECTION("Total intersection area matches target mesh area") + { + Omega_h::Write total_intersected_area(ntgt, 0.0); + + Omega_h::parallel_for( + ntgt, OMEGA_H_LAMBDA(int t) { + auto tgt_vert_coords = + get_vert_coords_of_elem(tgt_coords, tgt_faces2verts, t); + int start = intersection.tgt2src_offsets[t]; + int end = intersection.tgt2src_offsets[t + 1]; + + Omega_h::Real sum_area = 0.0; + + for (int i = start; i < end; ++i) { + int sid = intersection.tgt2src_indices[i]; + auto src_vert_coords = + get_vert_coords_of_elem(src_coords, src_faces2verts, sid); + + r3d::Polytope<2> poly; + r3d::intersect_simplices(poly, tgt_vert_coords, src_vert_coords); + + sum_area += r3d::measure(poly); + } + total_intersected_area[t] = sum_area; + printf("element = %d, num source intersections = %d, sum area = %f\n", + t, end - start, sum_area); + }); + + auto expected = Omega_h::HostRead(tgt_elm_area); + auto computed = Omega_h::HostRead(Omega_h::read(total_intersected_area)); + + double tol = 1e-6; + + for (int t = 0; t < ntgt; ++t) { + CAPTURE(t, expected[t], computed[t]); + CHECK_THAT(expected[t], Catch::Matchers::WithinAbs(computed[t], tol)); + } + } + + SECTION("Simple r3d intersection sanity test") + { + + r3d::Few, 3> A = {r3d::Vector<2>{{0.2, 0.2}}, + r3d::Vector<2>{{0.7, 0.2}}, + r3d::Vector<2>{{0.2, 0.7}}}; + + r3d::Few, 3> B = {r3d::Vector<2>{{0.0, 0.0}}, + r3d::Vector<2>{{1.0, 0.0}}, + r3d::Vector<2>{{0.0, 1.0}}}; + + r3d::Polytope<2> P1; + r3d::intersect_simplices(P1, A, B); + double vol1 = r3d::measure(P1); + REQUIRE(P1.nverts == 3); + REQUIRE(Omega_h::are_close(vol1, 0.5 * 0.5 * 0.5)); + } +} diff --git a/test/test_load_vector.cpp b/test/test_load_vector.cpp new file mode 100644 index 000000000..1a38df50e --- /dev/null +++ b/test/test_load_vector.cpp @@ -0,0 +1,143 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +TEST_CASE("Load vector computation on intersected regions", "[load_vector]") +{ + + Omega_h::Library lib; + + // 2D coordinates (x,y) : 4 vertices + Omega_h::Reals coords({ + 0.0, 0.0, // v0 + 1.0, 0.0, // v1 + 1.0, 1.0, // v2 + 0.0, 1.0 // v3 + }); + + // Target Mesh with two triangles + // Two triangles, CCW + // T0: (v0,v1,v3) = (0,1,3) + // T1: (v1,v2,v3) = (1,2,3) + Omega_h::LOs ev2v_target({0, 1, 3, 1, 2, 3}); + + Omega_h::Mesh target_mesh(&lib); + Omega_h::build_from_elems_and_coords(&target_mesh, OMEGA_H_SIMPLEX, 2, + ev2v_target, coords); + + // Source Mesh with two triangles + // Two triangles, CCW + // T0: (v0,v1,v3) = (0,1,2) + // T1: (v1,v2,v3) = (0,2,3) + Omega_h::LOs ev2v_source({0, 1, 2, 0, 2, 3}); + + Omega_h::Mesh source_mesh(&lib); + Omega_h::build_from_elems_and_coords(&source_mesh, OMEGA_H_SIMPLEX, 2, + ev2v_source, coords); + + REQUIRE(source_mesh.dim() == 2); + REQUIRE(target_mesh.dim() == 2); + + int num_tgt_elems = target_mesh.nelems(); + + auto intersection = pcms::intersectTargets(source_mesh, target_mesh); + SECTION("check localization routine for coincident cases") + { + Kokkos::View points("test_points", 3); + auto points_h = Kokkos::create_mirror_view(points); + points_h(0, 0) = 0.0; + points_h(0, 1) = 0.0; + points_h(1, 0) = 1.0; + points_h(1, 1) = 0.0; + points_h(2, 0) = 1.0; + points_h(2, 1) = 1.0; + + Kokkos::deep_copy(points, points_h); + pcms::GridPointSearch2D search_cell(target_mesh, 20, 20); + + auto d_results = search_cell(points); + + auto h_results = Kokkos::create_mirror_view(d_results); + + Kokkos::deep_copy(h_results, d_results); + + REQUIRE(h_results.extent(0) == 3); + + for (int i = 0; i < h_results.extent(0); ++i) { + auto r = h_results(i); + std::cout << "tri_id = " << r.element_id << "\n"; + std::cout << "dim = " << static_cast(r.dimensionality) << "\n"; + // std::cout << "parametric = " << r.parametric_coords << "\n"; + INFO("tri_id : " << r.element_id); + REQUIRE(r.element_id >= 0); + } + } + + SECTION("Basic shape and consistency of result") + { + + // Fill dummy values at each vertex of source + Omega_h::Write values(source_mesh.nverts(), 1.0); + + auto load_vector = + pcms::buildLoadVector(target_mesh, source_mesh, intersection, values); + + auto load_vector_host = Kokkos::create_mirror(load_vector); + Kokkos::deep_copy(load_vector_host, load_vector); + + REQUIRE(static_cast(load_vector_host.extent(0)) == num_tgt_elems * 3); + + for (int i = 0; i < load_vector_host.extent(0); ++i) + REQUIRE(load_vector_host(i) >= + 0.0); // Since function is 1.0 and everything is positive + } + + SECTION("Integration is zero if source values are zero") + { + Omega_h::Write zero_field(source_mesh.nverts(), 0.0); + + auto load_vector = + pcms::buildLoadVector(target_mesh, source_mesh, intersection, zero_field); + + auto load_vector_host = Kokkos::create_mirror_view(load_vector); + Kokkos::deep_copy(load_vector_host, load_vector); + + for (int i = 0; i < load_vector_host.extent(0); ++i) { + REQUIRE(load_vector_host(i) == Catch::Approx(0.0)); + } + } + + SECTION("load vector computed after the intersection of simple target and " + "source elements") + { + // the source elements are triangle1 (0,0), (1,0) & (1,1) and triangle2 + // (0,0), (1,1) & (0,1) the target elements are triangle1 (0,0), (1,0) & + // (0,1) and triangle2 (1,0), (1,1) & (0,1) + + Omega_h::Write constant_field(source_mesh.nverts(), 2.0); + + auto load_vector = pcms::buildLoadVector(target_mesh, source_mesh, + intersection, constant_field); + + auto load_vector_host = Kokkos::create_mirror_view(load_vector); + Kokkos::deep_copy(load_vector_host, load_vector); + + double expected_load_vector[6] = {0.333333, 0.333333, 0.333333, + 0.333333, 0.333333, 0.333333}; + double tolerance = 1e-6; + for (int i = 0; i < load_vector_host.extent(0); ++i) { + + CAPTURE(i, expected_load_vector[i], load_vector_host[i], tolerance); + CHECK_THAT(expected_load_vector[i], + Catch::Matchers::WithinAbs(load_vector_host[i], tolerance)); + } + } +} diff --git a/test/test_localization_factory.cpp b/test/test_localization_factory.cpp new file mode 100644 index 000000000..571ac1751 --- /dev/null +++ b/test/test_localization_factory.cpp @@ -0,0 +1,201 @@ +#include + +#include +#include + +#include "pcms/field/layout/omega_h_entity.h" +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/field/layout/point_cloud.h" +#include "pcms/field/evaluator/mls_options.h" +#include "pcms/localization/adj_search.hpp" +#include "pcms/localization/localization_path_selection.h" +#include "pcms/localization/mesh_localization.h" +#include "pcms/localization/mls_support_helpers.h" +#include "pcms/localization/point_cloud_localization.h" +#include "pcms/discretization/discretization/omega_h.hpp" +#include "pcms/utility/arrays.h" +#include "field_test_utils.h" + +namespace +{ + +std::shared_ptr MakeMeshEntityLayout( + Omega_h::Mesh& mesh, int entity_dim) +{ + return std::make_shared( + mesh, entity_dim, 1, pcms::CoordinateSystem::Cartesian); +} + +} // namespace + +TEST_CASE( + "LocalizationFactory: point-cloud Build matches BuildPointCloudSupports") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 1, 8, 8, 0, false); + + pcms::MLSOptions options; + options.radius = 0.2; + options.min_req_supports = 10; + options.adapt_radius = true; + + const int dim = mesh.dim(); + auto source_coords = mesh.coords(); + auto target_coords = pcms::test::CopyOmegaHRealsToVector(source_coords); + auto target_device = pcms::test::CreateDeviceCoordinateView( + target_coords, pcms::CoordinateSystem::Cartesian, dim); + + auto coords_read = Omega_h::HostRead(source_coords); + + // auto coords_dev = Kokkos::create_mirror_view_and_copy( + // Kokkos::DefaultExecutionSpace{}, coords_host); + auto coords_dev = Kokkos::View( + "point_cloud_coords", mesh.nverts(), dim); + auto coords_host = Kokkos::create_mirror(pcms::HostMemorySpace(), coords_dev); + for (int i = 0; i < mesh.nverts(); ++i) + for (int d = 0; d < dim; ++d) + coords_host(i, d) = coords_read[i * dim + d]; + Kokkos::deep_copy(coords_dev, coords_host); + + auto layout = std::make_shared( + dim, coords_dev, pcms::CoordinateSystem::Cartesian); + pcms::PointCloudLocalizationFactory factory(layout, options); + + auto actual = factory.Build(target_device.coordinate_view); + auto expected = pcms::BuildPointCloudSupports( + source_coords, source_coords, dim, options.radius, options.min_req_supports, + options.adapt_radius); + + pcms::test::CheckSupportResultsEquivalent(actual, expected); +} + +TEST_CASE("LocalizationFactory: vertex adjacency Build matches two-mesh " + "searchNeighbors") +{ + auto lib = Omega_h::Library{}; + auto world = lib.world(); + auto source_mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 12, 12, 0, false); + auto target_mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 10, 10, 0, false); + + pcms::MLSOptions options; + options.radius = 0.12; + options.min_req_supports = 15; + options.adapt_radius = true; + + pcms::AdjacencyLocalizationFactory factory(source_mesh, Omega_h::VERT, + options); + auto target_coords = + pcms::test::CopyOmegaHRealsToVector(target_mesh.coords()); + auto target_device = pcms::test::CreateDeviceCoordinateView( + target_coords, pcms::CoordinateSystem::Cartesian, target_mesh.dim()); + + auto actual = factory.Build(target_device.coordinate_view); + + Omega_h::Real radius_sq = options.radius * options.radius; + auto expected = pcms::searchNeighbors( + source_mesh, target_mesh, radius_sq, + static_cast(options.min_req_supports), + static_cast(3 * options.min_req_supports), + options.adapt_radius); + + pcms::test::CheckSupportResultsEquivalent(actual, expected); +} + +TEST_CASE("Localization path selection: vertex source uses adjacency for " + "pointwise requests") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 1, 8, 8, 0, false); + + auto source_layout = std::make_shared( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + + REQUIRE(pcms::detail::SelectLocalizationPath(*source_layout) == + pcms::detail::LocalizationPath::VertexAdjacencySearch); +} + +TEST_CASE( + "Localization path selection: centroid-to-vertex uses same-mesh adjacency") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 1, 8, 8, 0, false); + + auto source_layout = MakeMeshEntityLayout(mesh, pcms::Face); + auto target_layout = std::make_shared( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + + REQUIRE( + pcms::detail::SelectLocalizationPath(*source_layout, target_layout.get()) == + pcms::detail::LocalizationPath::CentroidToVertexAdjacencySearch); +} + +TEST_CASE("Localization path selection: centroid source uses point-cloud " + "supports for arbitrary points") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 1, 8, 8, 0, false); + + auto source_layout = MakeMeshEntityLayout(mesh, pcms::Face); + + REQUIRE(pcms::detail::SelectLocalizationPath(*source_layout) == + pcms::detail::LocalizationPath::PointCloudSupports); +} + +TEST_CASE("LocalizationFactory: centroid-to-vertex Build correct on same-mesh " + "fast path") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 1, 8, 8, 0, false); + + pcms::MLSOptions options; + options.radius = 0.25; + options.min_req_supports = 8; + options.adapt_radius = true; + + // Source: face centroids on the mesh; target: mesh vertices. + pcms::AdjacencyLocalizationFactory factory(mesh, Omega_h::FACE, options); + auto actual = factory.BuildSameMeshCentroidToVertex(); + + // Reference: direct call to the single-mesh centroid-to-vertex overload. + Omega_h::Real radius_sq = options.radius * options.radius; + auto expected = pcms::searchNeighbors( + mesh, radius_sq, static_cast(options.min_req_supports), + options.adapt_radius); + + pcms::test::CheckSupportResultsEquivalent(actual, expected); +} + +TEST_CASE("LocalizationFactory: correct with different meshes") +{ + auto lib = Omega_h::Library{}; + auto world = lib.world(); + auto source_mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 8, 8, 0, false); + auto target_mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 6, 6, 0, false); + + pcms::MLSOptions options; + options.radius = 0.25; + options.min_req_supports = 8; + options.adapt_radius = true; + + pcms::AdjacencyLocalizationFactory factory(source_mesh, Omega_h::FACE, + options); + + auto target_coords = + pcms::test::CopyOmegaHRealsToVector(target_mesh.coords()); + auto target_device = pcms::test::CreateDeviceCoordinateView( + target_coords, pcms::CoordinateSystem::Cartesian, target_mesh.dim()); + + // Should not throw — falls back to point-cloud N^2 path. + auto result = factory.Build(target_device.coordinate_view); + auto ptr_host = Omega_h::HostRead(result.supports_ptr); + REQUIRE(ptr_host.size() == target_mesh.nverts() + 1); +} diff --git a/test/test_mesh_geometry.cpp b/test/test_mesh_geometry.cpp new file mode 100644 index 000000000..1c327b1b3 --- /dev/null +++ b/test/test_mesh_geometry.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include +#include +#include + +TEST_CASE("pcms::get_entity_centroids supports simplex mesh entities") +{ + auto lib = Omega_h::Library{}; + auto world = lib.world(); + + SECTION("2D edge centroids") + { + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 1.0, 4, 4, 0, false); + + auto centroids = pcms::get_entity_centroids(mesh, Omega_h::EDGE); + REQUIRE(centroids.size() == mesh.nedges() * mesh.dim()); + } + + SECTION("2D face centroids") + { + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 1.0, 4, 4, 0, false); + + auto centroids = pcms::get_entity_centroids(mesh, Omega_h::FACE); + REQUIRE(centroids.size() == mesh.nfaces() * mesh.dim()); + } + + SECTION("3D edge centroids") + { + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 1.0, 3, 3, 3, false); + + auto centroids = pcms::get_entity_centroids(mesh, Omega_h::EDGE); + REQUIRE(centroids.size() == mesh.nedges() * mesh.dim()); + } + + SECTION("3D element centroids") + { + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 1.0, 3, 3, 3, false); + + auto centroids = pcms::get_entity_centroids(mesh, Omega_h::REGION); + REQUIRE(centroids.size() == mesh.nelems() * mesh.dim()); + } + + SECTION("Vertex coordinates passthrough") + { + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 1.0, 2, 2, 0, false); + + auto centroids = pcms::get_entity_centroids(mesh, Omega_h::VERT); + auto coords = mesh.coords(); + REQUIRE(centroids.size() == coords.size()); + } +} + +TEST_CASE("pcms::distance_squared handles 2D and 3D") +{ + SECTION("2D distance") + { + const Omega_h::Real p1[3] = {1.0, 2.0, 99.0}; + const Omega_h::Real p2[3] = {4.0, 6.0, -5.0}; + REQUIRE(pcms::distance_squared(p1, p2, 2) == Catch::Approx(25.0)); + } + + SECTION("3D distance") + { + const Omega_h::Real p1[3] = {1.0, 2.0, 3.0}; + const Omega_h::Real p2[3] = {4.0, 6.0, 8.0}; + REQUIRE(pcms::distance_squared(p1, p2, 3) == Catch::Approx(50.0)); + } +} diff --git a/test/test_mesh_intersection_field_transfer.cpp b/test/test_mesh_intersection_field_transfer.cpp new file mode 100644 index 000000000..5a72b55fe --- /dev/null +++ b/test/test_mesh_intersection_field_transfer.cpp @@ -0,0 +1,225 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "field_test_utils.h" + +namespace +{ + +double integrate_linear_field(Omega_h::Mesh& mesh, const Omega_h::Reals& u) +{ + const auto elem_areas = Omega_h::measure_elements_real(&mesh); + const auto elem_verts = mesh.ask_elem_verts(); + const auto nverts = mesh.nverts(); + + REQUIRE(static_cast(u.size()) == nverts); + + const auto elem_areas_h = Omega_h::HostRead(elem_areas); + const auto elem_verts_h = Omega_h::HostRead(elem_verts); + const auto u_h = Omega_h::HostRead(u); + + double integral = 0.0; + for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { + const Omega_h::LO v0 = elem_verts_h[3 * e + 0]; + const Omega_h::LO v1 = elem_verts_h[3 * e + 1]; + const Omega_h::LO v2 = elem_verts_h[3 * e + 2]; + + const double area = elem_areas_h[e]; + const double avg = (u_h[v0] + u_h[v1] + u_h[v2]) / 3.0; + integral += area * avg; + } + return integral; +} + +Omega_h::Reals make_omega_h_reals( + pcms::Rank1View values) +{ + Omega_h::HostWrite values_h(values.size()); + for (size_t i = 0; i < values.size(); ++i) { + values_h[i] = values[i]; + } + return Omega_h::Reals(values_h); +} + +} // namespace + +TEST_CASE("mesh intersection linear/constant conservation", + "[transfer][mesh_intersection]") +{ + Omega_h::Library lib; + + Omega_h::Reals coords({ + 0.0, 0.0, // v0 + 1.0, 0.0, // v1 + 1.0, 1.0, // v2 + 0.0, 1.0 // v3 + }); + + // Source mesh: diagonal (v0-v2) + Omega_h::LOs ev2v_source({0, 1, 2, 0, 2, 3}); + Omega_h::Mesh source_mesh(&lib); + Omega_h::build_from_elems_and_coords(&source_mesh, OMEGA_H_SIMPLEX, 2, + ev2v_source, coords); + + // Target mesh: opposite diagonal (v1-v3) + Omega_h::LOs ev2v_target({0, 1, 3, 1, 2, 3}); + Omega_h::Mesh target_mesh(&lib); + Omega_h::build_from_elems_and_coords(&target_mesh, OMEGA_H_SIMPLEX, 2, + ev2v_target, coords); + + const auto src_coords = source_mesh.coords(); + + auto intersections = pcms::intersectTargets(source_mesh, target_mesh); + + SECTION("constant field is preserved and conserved") + { + const double c = 2.0; + Omega_h::Write source_const(source_mesh.nverts()); + Omega_h::parallel_for( + source_const.size(), OMEGA_H_LAMBDA(int i) { source_const[i] = c; }); + + auto projected = pcms::solveGalerkinProjection(target_mesh, source_mesh, + intersections, source_const); + auto projected_h = Omega_h::HostRead(projected); + + REQUIRE(static_cast(projected.size()) == target_mesh.nverts()); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + REQUIRE(projected_h[i] == Catch::Approx(c).margin(1e-10)); + } + + const double source_integral = + integrate_linear_field(source_mesh, source_const); + const double target_integral = + integrate_linear_field(target_mesh, projected); + REQUIRE(target_integral == Catch::Approx(source_integral).margin(1e-10)); + } + + SECTION("linear field is reproduced on target vertices") + { + Omega_h::Write source_linear(source_mesh.nverts()); + Omega_h::parallel_for( + source_mesh.nverts(), OMEGA_H_LAMBDA(int i) { + const double x = src_coords[2 * i + 0]; + const double y = src_coords[2 * i + 1]; + source_linear[i] = x + y; + }); + + auto projected = pcms::solveGalerkinProjection( + target_mesh, source_mesh, intersections, source_linear); + auto projected_h = Omega_h::HostRead(projected); + const auto tgt_coords_h = + Omega_h::HostRead(target_mesh.coords()); + + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + const double expected = tgt_coords_h[2 * i + 0] + tgt_coords_h[2 * i + 1]; + REQUIRE(projected_h[i] == Catch::Approx(expected).margin(1e-9)); + } + } +} + +TEST_CASE("OmegaHConservativeProjection matches conservative projection solver", + "[transfer][mesh_intersection]") +{ + Omega_h::Library lib; + + Omega_h::Reals coords({0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0}); + + Omega_h::LOs ev2v_source({0, 1, 2, 0, 2, 3}); + Omega_h::Mesh source_mesh(&lib); + Omega_h::build_from_elems_and_coords(&source_mesh, OMEGA_H_SIMPLEX, 2, + ev2v_source, coords); + + Omega_h::LOs ev2v_target({0, 1, 3, 1, 2, 3}); + Omega_h::Mesh target_mesh(&lib); + Omega_h::build_from_elems_and_coords(&target_mesh, OMEGA_H_SIMPLEX, 2, + ev2v_target, coords); + + // Add classification for all dimensions + // Vertices (dim 0) + source_mesh.add_tag( + 0, "class_dim", 1, + Omega_h::Read(source_mesh.nverts(), Omega_h::I8(0))); + source_mesh.add_tag( + 0, "class_id", 1, + Omega_h::Read(source_mesh.nverts(), Omega_h::ClassId(0))); + + // Edges (dim 1) + source_mesh.add_tag( + 1, "class_dim", 1, + Omega_h::Read(source_mesh.nedges(), Omega_h::I8(1))); + source_mesh.add_tag( + 1, "class_id", 1, + Omega_h::Read(source_mesh.nedges(), Omega_h::ClassId(0))); + + // Faces (dim 2) + source_mesh.add_tag( + 2, "class_dim", 1, + Omega_h::Read(source_mesh.nelems(), Omega_h::I8(2))); + source_mesh.add_tag( + 2, "class_id", 1, + Omega_h::Read(source_mesh.nelems(), Omega_h::ClassId(0))); + + // Add classification for all dimensions + // Vertices (dim 0) + target_mesh.add_tag( + 0, "class_dim", 1, + Omega_h::Read(target_mesh.nverts(), Omega_h::I8(0))); + target_mesh.add_tag( + 0, "class_id", 1, + Omega_h::Read(target_mesh.nverts(), Omega_h::ClassId(0))); + + // Edges (dim 1) + target_mesh.add_tag( + 1, "class_dim", 1, + Omega_h::Read(target_mesh.nedges(), Omega_h::I8(1))); + target_mesh.add_tag( + 1, "class_id", 1, + Omega_h::Read(target_mesh.nedges(), Omega_h::ClassId(0))); + + // Faces (dim 2) + target_mesh.add_tag( + 2, "class_dim", 1, + Omega_h::Read(target_mesh.nelems(), Omega_h::I8(2))); + target_mesh.add_tag( + 2, "class_id", 1, + Omega_h::Read(target_mesh.nelems(), Omega_h::ClassId(0))); + + auto source_space = pcms::LagrangeFunctionSpace::FromMesh( + source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::LagrangeFunctionSpace::FromMesh( + target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + auto source = source_space.CreateField(); + auto target = target_space.CreateField(); + + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { + return x * x + x * y + 0.5 * y * y; + }); + + const auto intersections = pcms::intersectTargets(source_mesh, target_mesh); + const auto expected = pcms::solveGalerkinProjection( + target_mesh, source_mesh, intersections, + make_omega_h_reals(source.GetDOFHolderDataHost())); + const auto expected_h = Omega_h::HostRead(expected); + + pcms::OmegaHConservativeProjection projection(source_space, target_space); + projection.Apply(source, target); + + const auto target_values = target.GetDOFHolderDataHost(); + REQUIRE(static_cast(target_values.size()) == + target_mesh.nverts()); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + REQUIRE(target_values[i] == Catch::Approx(expected_h[i]).margin(1e-12)); + } +} diff --git a/test/test_mfem_adapter.cpp b/test/test_mfem_adapter.cpp new file mode 100644 index 000000000..87a6e7122 --- /dev/null +++ b/test/test_mfem_adapter.cpp @@ -0,0 +1,139 @@ +#include + +#include +#include +#include +#include + +#include + +#include + +namespace +{ + +double LinearField(const mfem::Vector& x) +{ + return 1.0 + 2.0 * x[0] + 3.0 * x[1]; +} + +} // namespace + +TEST_CASE("MFEM vertex-scalar field adapter") +{ + // Common setup (re-run for each SECTION): 2x2 Cartesian quad mesh + // (9 vertices), order-1 H1 scalar space. + auto serial = mfem::Mesh::MakeCartesian2D(2, 2, mfem::Element::QUADRILATERAL); + mfem::ParMesh pmesh(MPI_COMM_WORLD, serial); + mfem::H1_FECollection fec(1, pmesh.Dimension()); + mfem::ParFiniteElementSpace pfes(&pmesh, &fec); + mfem::ParGridFunction gf(&pfes); + gf = 0.0; + + SECTION("layout reports vertex-scalar properties") + { + pcms::MFEMLayout layout(pmesh, pfes, pcms::CoordinateSystem::Cartesian); + + REQUIRE(layout.GetNumComponents() == 1); + REQUIRE(layout.GetDimension() == 2); + REQUIRE(layout.GetNumOwnedDofHolder() == pmesh.GetNV()); + REQUIRE(layout.IsDistributed()); + + auto gids = layout.GetGidsHost(); + REQUIRE(static_cast(gids.size()) == pmesh.GetNV()); + + auto coords = layout.GetDOFHolderCoordinates().GetCoordinates(); + REQUIRE(static_cast(coords.extent(0)) == pmesh.GetNV()); + REQUIRE(static_cast(coords.extent(1)) == 2); + + // Owned mask covers exactly the global unique vertices across ranks. + auto owned = layout.GetOwnedHost(); + int local_owned = 0; + for (size_t i = 0; i < owned.size(); ++i) { + if (owned[i]) + ++local_owned; + } + int total_owned = 0; + MPI_Allreduce(&local_owned, &total_owned, 1, MPI_INT, MPI_SUM, + MPI_COMM_WORLD); + REQUIRE(total_owned == static_cast(layout.GetNumGlobalDofHolder())); + } + + SECTION("field data round-trips through the grid function") + { + mfem::FunctionCoefficient coeff(LinearField); + gf.ProjectCoefficient(coeff); + + pcms::MFEMLayout layout(pmesh, pfes, pcms::CoordinateSystem::Cartesian); + pcms::MFEMVertexFieldData data(pfes, gf); + + auto host = data.GetDOFHolderDataHost(); + REQUIRE(static_cast(host.size()) == pmesh.GetNV()); + + std::vector captured(host.size()); + for (size_t v = 0; v < host.size(); ++v) { + captured[v] = host[v]; + } + + // Write the captured values back and confirm the grid function is restored. + gf = 0.0; + Kokkos::View in("in", captured.size()); + for (size_t v = 0; v < captured.size(); ++v) { + in(v) = captured[v]; + } + data.SetDOFHolderDataHost(pcms::make_const_array_view(in)); + + auto host2 = data.GetDOFHolderDataHost(); + auto owned = layout.GetOwnedHost(); + for (size_t v = 0; v < host2.size(); ++v) { + if (owned[v]) { + REQUIRE(host2[v] == captured[v]); + } + } + } + + SECTION("serializer identity round-trip on a single rank") + { + int nproc = 0; + MPI_Comm_size(MPI_COMM_WORLD, &nproc); + if (nproc != 1) { + SUCCEED("serializer identity round-trip is only checked on one rank"); + return; + } + + mfem::FunctionCoefficient coeff(LinearField); + gf.ProjectCoefficient(coeff); + + auto fs = pcms::MFEMFieldFactory( + pmesh, pfes, gf, pcms::CoordinateSystem::Cartesian); + auto field = fs.CreateField(); + const auto& layout = field.GetLayout(); + + const auto n = static_cast(layout.GetNumOwnedDofHolder()); + + // Identity permutation: buffer index equals DOF-holder index. + Kokkos::View perm("perm", n); + for (size_t i = 0; i < n; ++i) { + perm(i) = static_cast(i); + } + Kokkos::View buffer("buffer", n); + + pcms::FieldSerializer serializer; + serializer.Serialize(field.GetData(), layout, + pcms::make_array_view(buffer), + pcms::make_const_array_view(perm)); + + // Deserialize into a fresh field bound to a second grid function. + mfem::ParGridFunction gf2(&pfes); + gf2 = 0.0; + pcms::MFEMVertexFieldData data2(pfes, gf2); + serializer.Deserialize(data2, layout, pcms::make_const_array_view(buffer), + pcms::make_const_array_view(perm)); + + auto restored = data2.GetDOFHolderDataHost(); + auto original = field.GetDOFHolderDataHost(); + for (size_t v = 0; v < restored.size(); ++v) { + REQUIRE(restored[v] == original[v]); + } + } +} diff --git a/test/test_mfem_coupling.cpp b/test/test_mfem_coupling.cpp new file mode 100644 index 000000000..9b2508b58 --- /dev/null +++ b/test/test_mfem_coupling.cpp @@ -0,0 +1,212 @@ +// Two-application coupling test for the MFEM field adapter. +// +// A client application owns an MFEM order-1 vertex scalar field and sends it, +// restricted to a masked overlap domain, to a rendezvous "coupler" server. +// The overlap domain is selected from MFEM element attributes following the +// create_mask strategy in the mfem-pcms-example: a vertex participates if it is +// incident to an element with the target attribute. Routing uses a single-rank +// RCB (coordinate) partition. +// +// Usage: test_mfem_coupling + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +using pcms::Real; + +namespace +{ + +constexpr int TargetAttribute = 2; + +// Seeded into receiver DOFs outside the overlap before receiving. Distinct from +// any sent value (gid + 1 >= 1), so a correct masked receive must leave it +// untouched. +constexpr Real OutsideOverlapSentinel = -7.0; + +// Build a 4x4 quad mesh and tag the left half (centroid x < 0.5) with the +// target attribute; the rest keep attribute 1. The same construction runs on +// both apps so vertex global ids and coordinates match. +mfem::Mesh MakeAttributedMesh() +{ + auto mesh = mfem::Mesh::MakeCartesian2D(4, 4, mfem::Element::QUADRILATERAL); + for (int e = 0; e < mesh.GetNE(); ++e) { + mfem::Vector center; + mesh.GetElementCenter(e, center); + mesh.SetAttribute(e, center[0] < 0.5 ? TargetAttribute : 1); + } + mesh.SetAttributes(); + return mesh; +} + +redev::Partition MakeRCBPartition(int dim) +{ + redev::LOs ranks(1); + std::iota(ranks.begin(), ranks.end(), 0); + redev::Reals cuts = {0}; + return redev::Partition{redev::RCBPtn{dim, ranks, cuts}}; +} + +// Expected sent value at a vertex: its global id + 1 (strictly positive so it +// is distinguishable from the receiver's initial zero state). +Real ExpectedValue(pcms::GO gid) +{ + return static_cast(gid + 1); +} + +int RunClient(MPI_Comm comm) +{ + auto serial = MakeAttributedMesh(); + mfem::ParMesh pmesh(comm, serial); + mfem::H1_FECollection fec(1, pmesh.Dimension()); + mfem::ParFiniteElementSpace pfes(&pmesh, &fec); + mfem::ParGridFunction gf(&pfes); + gf = 0.0; + + pcms::Coupler cpl("mfem_overlap_coupler", comm, false, redev::Partition{}); + auto* app = cpl.AddApplication("mfem_app"); + + auto fs = pcms::MFEMFieldFactory(pmesh, pfes, gf, + pcms::CoordinateSystem::Cartesian); + auto layout = fs.GetLayout(); + + auto overlap_view = + pcms::MFEMLayout::OverlapMaskFromAttribute(pmesh, TargetAttribute); + app->SetLayoutOverlapMask( + "field", std::make_unique( + static_cast(layout->GetNumOwnedDofHolder()), + overlap_view)); + app->AddLayout("field", layout); + + auto handle = app->AddField("field", fs.CreateField()); + + // Seed the field so each vertex holds (gid + 1). + auto gids = layout->GetGidsHost(); + const auto n = static_cast(layout->GetNumOwnedDofHolder()); + Kokkos::View values("client_values", n); + for (size_t v = 0; v < n; ++v) { + values(v) = ExpectedValue(gids[v]); + } + handle.GetField().SetDOFHolderDataHost(pcms::make_const_array_view(values)); + + app->SendPhase([&]() { handle.Send(); }); + return 0; +} + +int RunServer(MPI_Comm comm) +{ + auto serial = MakeAttributedMesh(); + mfem::ParMesh pmesh(comm, serial); + mfem::H1_FECollection fec(1, pmesh.Dimension()); + mfem::ParFiniteElementSpace pfes(&pmesh, &fec); + mfem::ParGridFunction gf(&pfes); + gf = 0.0; + + pcms::Coupler cpl("mfem_overlap_coupler", comm, true, + MakeRCBPartition(pmesh.SpaceDimension())); + auto* app = cpl.AddApplication("mfem_app"); + + auto fs = pcms::MFEMFieldFactory(pmesh, pfes, gf, + pcms::CoordinateSystem::Cartesian); + auto layout = fs.GetLayout(); + app->AddLayout("field", layout); + auto handle = app->AddField("field", fs.CreateField()); + + // Seed the whole receiver field with a sentinel. The masked send only carries + // the overlap DOFs, so a correct receive must overwrite only those and leave + // every DOF outside the overlap holding the sentinel. + { + const auto n = static_cast(layout->GetNumOwnedDofHolder()); + Kokkos::View seed("server_seed", n); + for (size_t v = 0; v < n; ++v) { + seed(v) = OutsideOverlapSentinel; + } + handle.GetField().SetDOFHolderDataHost(pcms::make_const_array_view(seed)); + } + + app->ReceivePhase([&]() { handle.Receive(); }); + + // Verify: every overlap vertex received the expected value. The overlap set + // is recomputed from the identical mesh's attributes. + auto overlap = + pcms::MFEMLayout::OverlapMaskFromAttribute(pmesh, TargetAttribute); + auto gids = layout->GetGidsHost(); + auto received = handle.GetField().GetDOFHolderDataHost(); + + int overlap_count = 0; + int mismatches = 0; + int corrupted_outside = 0; + for (size_t v = 0; v < received.size(); ++v) { + if (overlap(v)) { + ++overlap_count; + const Real expected = ExpectedValue(gids[v]); + if (received[v] != expected) { + ++mismatches; + std::cerr << "Mismatch at vertex " << v << ": expected " << expected + << " got " << received[v] << "\n"; + } + } else if (received[v] != OutsideOverlapSentinel) { + // A DOF outside the overlap was overwritten on receive. + ++corrupted_outside; + } + } + + const int nv = pmesh.GetNV(); + std::cout << "MFEM overlap coupling: " << overlap_count << " / " << nv + << " overlap vertices received; " << corrupted_outside + << " DOFs outside the overlap corrupted\n"; + + if (overlap_count == 0 || overlap_count == nv) { + std::cerr << "Overlap mask is trivial (count=" << overlap_count + << ", nv=" << nv << "); test is not meaningful\n"; + return 1; + } + if (mismatches != 0) { + std::cerr << "MFEM overlap coupling FAILED with " << mismatches + << " mismatches\n"; + return 1; + } + if (corrupted_outside != 0) { + std::cerr << "MFEM overlap coupling FAILED: " << corrupted_outside + << " DOFs outside the overlap were overwritten on receive " + "(expected to be preserved)\n"; + return 1; + } + std::cout << "MFEM overlap coupling PASSED\n"; + return 0; +} + +} // namespace + +int main(int argc, char** argv) +{ + MPI_Init(&argc, &argv); + int rc = 0; + { + Kokkos::ScopeGuard kokkos(argc, argv); + if (argc != 2) { + std::cerr << "Usage: " << argv[0] << " \n"; + MPI_Finalize(); + return EXIT_FAILURE; + } + const int role = std::atoi(argv[1]); + try { + rc = (role == -1) ? RunServer(MPI_COMM_WORLD) : RunClient(MPI_COMM_WORLD); + } catch (const std::exception& e) { + std::cerr << "Exception: " << e.what() << "\n"; + rc = 1; + } + } + MPI_Finalize(); + return rc; +} diff --git a/test/test_mls_basis.cpp b/test/test_mls_basis.cpp index bb22cadc1..76769f990 100644 --- a/test/test_mls_basis.cpp +++ b/test/test_mls_basis.cpp @@ -1,7 +1,7 @@ #include #include -#include -#include +#include +#include #include #include diff --git a/test/test_normalisation.cpp b/test/test_normalisation.cpp index 12147cf1f..881350be1 100644 --- a/test/test_normalisation.cpp +++ b/test/test_normalisation.cpp @@ -1,10 +1,10 @@ #include #include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include #include #include #include diff --git a/test/test_ohClassPtn_appRibPtn.cpp b/test/test_ohClassPtn_appRibPtn.cpp index a128ef9af..7132f11ed 100644 --- a/test/test_ohClassPtn_appRibPtn.cpp +++ b/test/test_ohClassPtn_appRibPtn.cpp @@ -13,142 +13,155 @@ namespace ts = test_support; int main(int argc, char** argv) { - auto lib = Omega_h::Library(&argc, &argv); - auto world = lib.world(); - const int rank = world->rank(); - if (argc != 3) { - std::cerr << "Usage: " << argv[0] - << " <1=isRendezvousApp,0=isParticipant> /path/to/omega_h/mesh\n"; - std::cerr << "WARNING: this test is currently hardcoded for the " - "xgc1_data/Cyclone_ITG/Cyclone_ITG_deltaf_23mesh/mesh.osh\n"; - std::cerr << "mesh for the rendezvous processes and " - "xgc1_data/Cyclone_ITG/Cyclone_ITG_deltaf_23mesh mesh/2p.osh " - "for the\n"; - std::cerr << "for the non-rendezvous processes\n"; - exit(EXIT_FAILURE); - } - OMEGA_H_CHECK(argc == 3); - auto isRdv = atoi(argv[1]); - Omega_h::Mesh mesh(&lib); - Omega_h::binary::read(argv[2], lib.world(), &mesh); - // partition the omegah mesh by classification and return the rank-to-classid - // array - const auto classPartition = - isRdv ? ts::migrateAndGetPartition(mesh) : ts::ClassificationPartition(); - if (isRdv) { - ts::writeVtk(mesh, "rdvSplit", 0); - } else { - REDEV_ALWAYS_ASSERT(world->size() == 2); - if (!rank) - REDEV_ALWAYS_ASSERT(mesh.nelems() == 11); - ts::writeVtk(mesh, "appSplit", 0); - } - auto partition = redev::ClassPtn(MPI_COMM_WORLD, classPartition.ranks, - classPartition.modelEnts); - redev::Redev rdv(MPI_COMM_WORLD, redev::Partition{std::move(partition)}, - static_cast(isRdv)); + try { + auto lib = Omega_h::Library(&argc, &argv); + auto world = lib.world(); + const int rank = world->rank(); + if (argc != 3) { + std::cerr + << "Usage: " << argv[0] + << " <1=isRendezvousApp,0=isParticipant> /path/to/omega_h/mesh\n"; + std::cerr << "WARNING: this test is currently hardcoded for the " + "xgc1_data/Cyclone_ITG/Cyclone_ITG_deltaf_23mesh/mesh.osh\n"; + std::cerr + << "mesh for the rendezvous processes and " + "xgc1_data/Cyclone_ITG/Cyclone_ITG_deltaf_23mesh mesh/2p.osh " + "for the\n"; + std::cerr << "for the non-rendezvous processes\n"; + exit(EXIT_FAILURE); + } + OMEGA_H_CHECK(argc == 3); + auto isRdv = atoi(argv[1]); + Omega_h::Mesh mesh(&lib); + Omega_h::binary::read(argv[2], lib.world(), &mesh); + // partition the omegah mesh by classification and return the + // rank-to-classid array + const auto classPartition = + isRdv ? ts::migrateAndGetPartition(mesh) : ts::ClassificationPartition(); + if (isRdv) { + ts::writeVtk(mesh, "rdvSplit", 0); + } else { + REDEV_ALWAYS_ASSERT(world->size() == 2); + if (!rank) + REDEV_ALWAYS_ASSERT(mesh.nelems() == 11); + ts::writeVtk(mesh, "appSplit", 0); + } + auto partition = redev::ClassPtn(MPI_COMM_WORLD, classPartition.ranks, + classPartition.modelEnts); + redev::Redev rdv(MPI_COMM_WORLD, redev::Partition{std::move(partition)}, + static_cast(isRdv)); - const std::string name = "meshVtxIds"; + const std::string name = "meshVtxIds"; - adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; - auto channel = - rdv.CreateAdiosChannel(name, params, redev::TransportType::BP4); - auto commPair = channel.CreateComm(name, rdv.GetMPIComm()); + adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; + auto channel = + rdv.CreateAdiosChannel(name, params, redev::TransportType::BP4); + auto commPair = channel.CreateComm(name, rdv.GetMPIComm()); - // Build the dest, offsets, and permutation arrays for the forward - // send from non-rendezvous to rendezvous. - ts::OutMsg appOut = - !isRdv ? ts::prepareAppOutMessage( - mesh, std::get(rdv.GetPartition())) - : ts::OutMsg(); - if (!isRdv) { - commPair.SetOutMessageLayout(appOut.dest, appOut.offset); - } + // Build the dest, offsets, and permutation arrays for the forward + // send from non-rendezvous to rendezvous. + ts::OutMsg appOut = + !isRdv ? ts::prepareAppOutMessage( + mesh, std::get(rdv.GetPartition())) + : ts::OutMsg(); + if (!isRdv) { + commPair.SetOutMessageLayout(appOut.dest, appOut.offset); + } - redev::GOs rdvInPermute; - ts::CSR rdvOutPermute; - ts::OutMsg rdvOut; + redev::GOs rdvInPermute; + ts::CSR rdvOutPermute; + ts::OutMsg rdvOut; - for (int iter = 0; iter < 3; iter++) { - if (!rank) - fprintf(stderr, "isRdv %d iter %d\n", isRdv, iter); - ////////////////////////////////////////////////////// - // the non-rendezvous app sends global vtx ids to rendezvous - ////////////////////////////////////////////////////// - if (!isRdv) { - // fill message array - auto gids = mesh.globals(0); - auto gids_h = Omega_h::HostRead(gids); - redev::GOs msgs(gids_h.size(), 0); - for (size_t i = 0; i < msgs.size(); i++) { - msgs[appOut.permute[i]] = gids_h[i]; - } - auto start = std::chrono::steady_clock::now(); - channel.SendPhase([&]() { commPair.Send(msgs.data()); }); - ts::getAndPrintTime(start, name + " appWrite", rank); - } else { - auto start = std::chrono::steady_clock::now(); - const auto msgs = channel.ReceivePhase( - [&]() { return commPair.Recv(redev::Mode::Synchronous); }); - ts::getAndPrintTime(start, name + " rdvRead", rank); - // attach the ids to the mesh - if (iter == 0) { - // We have received the first input message in the rendezvous - // processes. Using the meta data of the incoming message we will: - //- compute the permutation from the incoming vertex global ids to the - // on-process global ids - //- set the message layout for the reverse (rendezvous->non-rendezvous) - // send by - // building the dest and offsets array. - //- compute the reverse send's permutation array using the layout of - // global vertex ids in 'msgs'. - // These operations only need to be done once per coupling as long as - // the topology and partition of the rendezvous and non-rendezvous - // meshes remains the same. - auto rdvIn = commPair.GetInMessageLayout(); - rdvInPermute = ts::getRdvPermutation(mesh, msgs); - rdvOut = ts::prepareRdvOutMessage(mesh, rdvIn); - REDEV_ALWAYS_ASSERT(rdvOut.dest == redev::LOs({0, 1})); - if (!rank) - REDEV_ALWAYS_ASSERT(rdvOut.offset == redev::LOs({0, 4, 9})); - if (rank) - REDEV_ALWAYS_ASSERT(rdvOut.offset == redev::LOs({0, 8, 15})); - commPair.SetOutMessageLayout(rdvOut.dest, rdvOut.offset); - rdvOutPermute = ts::getRdvOutPermutation(mesh, msgs); - } - ts::checkAndAttachIds(mesh, "inVtxGids", msgs, rdvInPermute); - ts::writeVtk(mesh, "rdvInGids", iter); - } // end non-rdv -> rdv - ////////////////////////////////////////////////////// - // the rendezvous app sends global vtx ids to non-rendezvous - ////////////////////////////////////////////////////// - if (isRdv) { - // fill message array - auto gids = mesh.globals(0); - auto gids_h = Omega_h::HostRead(gids); - redev::GOs msgs(rdvOutPermute.off.back()); - for (int i = 0; i < gids_h.size(); i++) { - for (int j = rdvOutPermute.off[i]; j < rdvOutPermute.off[i + 1]; j++) { - msgs[rdvOutPermute.val[j]] = gids_h[i]; - } - } - auto start = std::chrono::steady_clock::now(); - channel.SendPhase([&]() { commPair.Send(msgs.data()); }); - ts::getAndPrintTime(start, name + " rdvWrite", rank); - } else { - auto start = std::chrono::steady_clock::now(); - const auto msgs = channel.ReceivePhase( - [&]() { return commPair.Recv(redev::Mode::Synchronous); }); - ts::getAndPrintTime(start, name + " appRead", rank); - { // check incoming messages are in the correct order + for (int iter = 0; iter < 3; iter++) { + if (!rank) + fprintf(stderr, "isRdv %d iter %d\n", isRdv, iter); + ////////////////////////////////////////////////////// + // the non-rendezvous app sends global vtx ids to rendezvous + ////////////////////////////////////////////////////// + if (!isRdv) { + // fill message array auto gids = mesh.globals(0); auto gids_h = Omega_h::HostRead(gids); - REDEV_ALWAYS_ASSERT(msgs.size() == static_cast(gids_h.size())); + redev::GOs msgs(gids_h.size(), 0); for (size_t i = 0; i < msgs.size(); i++) { - REDEV_ALWAYS_ASSERT(gids_h[i] == msgs[appOut.permute[i]]); + msgs[appOut.permute[i]] = gids_h[i]; + } + auto start = std::chrono::steady_clock::now(); + channel.SendPhase([&]() { commPair.Send(msgs.data()); }); + ts::getAndPrintTime(start, name + " appWrite", rank); + } else { + auto start = std::chrono::steady_clock::now(); + const auto msgs = channel.ReceivePhase( + [&]() { return commPair.Recv(redev::Mode::Synchronous); }); + ts::getAndPrintTime(start, name + " rdvRead", rank); + // attach the ids to the mesh + if (iter == 0) { + // We have received the first input message in the rendezvous + // processes. Using the meta data of the incoming message we will: + //- compute the permutation from the incoming vertex global ids to the + // on-process global ids + //- set the message layout for the reverse + //(rendezvous->non-rendezvous) + // send by + // building the dest and offsets array. + //- compute the reverse send's permutation array using the layout of + // global vertex ids in 'msgs'. + // These operations only need to be done once per coupling as long as + // the topology and partition of the rendezvous and non-rendezvous + // meshes remains the same. + auto rdvIn = commPair.GetInMessageLayout(); + rdvInPermute = ts::getRdvPermutation(mesh, msgs); + rdvOut = ts::prepareRdvOutMessage(mesh, rdvIn); + REDEV_ALWAYS_ASSERT(rdvOut.dest == redev::LOs({0, 1})); + if (!rank) + REDEV_ALWAYS_ASSERT(rdvOut.offset == redev::LOs({0, 4, 9})); + if (rank) + REDEV_ALWAYS_ASSERT(rdvOut.offset == redev::LOs({0, 8, 15})); + commPair.SetOutMessageLayout(rdvOut.dest, rdvOut.offset); + rdvOutPermute = ts::getRdvOutPermutation(mesh, msgs); + } + ts::checkAndAttachIds(mesh, "inVtxGids", msgs, rdvInPermute); + ts::writeVtk(mesh, "rdvInGids", iter); + } // end non-rdv -> rdv + ////////////////////////////////////////////////////// + // the rendezvous app sends global vtx ids to non-rendezvous + ////////////////////////////////////////////////////// + if (isRdv) { + // fill message array + auto gids = mesh.globals(0); + auto gids_h = Omega_h::HostRead(gids); + redev::GOs msgs(rdvOutPermute.off.back()); + for (int i = 0; i < gids_h.size(); i++) { + for (int j = rdvOutPermute.off[i]; j < rdvOutPermute.off[i + 1]; + j++) { + msgs[rdvOutPermute.val[j]] = gids_h[i]; + } } - } - } // end rdv -> non-rdv - } // end iter loop - return 0; + auto start = std::chrono::steady_clock::now(); + channel.SendPhase([&]() { commPair.Send(msgs.data()); }); + ts::getAndPrintTime(start, name + " rdvWrite", rank); + } else { + auto start = std::chrono::steady_clock::now(); + const auto msgs = channel.ReceivePhase( + [&]() { return commPair.Recv(redev::Mode::Synchronous); }); + ts::getAndPrintTime(start, name + " appRead", rank); + { // check incoming messages are in the correct order + auto gids = mesh.globals(0); + auto gids_h = Omega_h::HostRead(gids); + REDEV_ALWAYS_ASSERT(msgs.size() == + static_cast(gids_h.size())); + for (size_t i = 0; i < msgs.size(); i++) { + REDEV_ALWAYS_ASSERT(gids_h[i] == msgs[appOut.permute[i]]); + } + } + } // end rdv -> non-rdv + } // end iter loop + return 0; + } catch (const std::exception& e) { + std::cerr << "Exception caught in main: " << e.what() << std::endl; + return 1; + } catch (...) { + std::cerr << "Unknown exception caught in main" << std::endl; + return 1; + } } diff --git a/test/test_ohOverlap.cpp b/test/test_ohOverlap.cpp index 04ae96449..c9691b08d 100644 --- a/test/test_ohOverlap.cpp +++ b/test/test_ohOverlap.cpp @@ -9,6 +9,7 @@ #include "pcms.h" #include "test_support.h" #include +#include "pcms/utility/types.h" namespace ts = test_support; diff --git a/test/test_omega_h_copy.cpp b/test/test_omega_h_copy.cpp deleted file mode 100644 index a655f2977..000000000 --- a/test/test_omega_h_copy.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include - -TEST_CASE("copy omega_h_field data") -{ - auto lib = Omega_h::Library{}; - auto world = lib.world(); - auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 100, 100, 0, false); - const auto nverts = mesh.nents(0); - Omega_h::Write ids(nverts); - Omega_h::parallel_for(nverts, OMEGA_H_LAMBDA(int i) { ids[i] = i; }); - mesh.add_tag(0, "test_ids", 1, Omega_h::Read(ids)); - const bool tag_already_exists = GENERATE(true, false); - if (tag_already_exists) { - Omega_h::Write zeros(nverts, 0); - mesh.add_tag(0, "copied", 1, zeros); - } - SECTION("No filter") - { - pcms::MeshFieldsAdapter original("test_ids", mesh); - pcms::MeshFieldsAdapter copied("copied", mesh); - REQUIRE(original.Size() == copied.Size()); - pcms::copy_field(original, copied); - auto copied_array = pcms::get_nodal_data(copied); - REQUIRE(copied_array.size() == copied.Size()); - REQUIRE(copied_array.size() == nverts); - int sum = 0; - Kokkos::parallel_reduce( - nverts, - KOKKOS_LAMBDA(int i, int& local_sum) { - local_sum += (ids[i] == copied_array[i]); - }, - sum); - REQUIRE(sum == nverts); - } - SECTION("trivial positive mask") - { - Omega_h::Write mask(nverts, 1); - pcms::MeshFieldsAdapter original("test_ids", mesh, mask); - pcms::MeshFieldsAdapter copied("copied", mesh, mask); - REQUIRE(original.Size() == copied.Size()); - pcms::copy_field(original, copied); - auto copied_array = pcms::get_nodal_data(copied); - REQUIRE(copied_array.size() == copied.Size()); - REQUIRE(copied_array.size() == nverts); - int sum = 0; - Kokkos::parallel_reduce( - nverts, - KOKKOS_LAMBDA(int i, int& local_sum) { - local_sum += (ids[i] == copied_array[i]); - }, - sum); - REQUIRE(sum == nverts); - } - SECTION("every other mask") - { - Omega_h::Write mask(nverts, 0); - Omega_h::parallel_for(nverts, OMEGA_H_LAMBDA(int i) { mask[i] = i % 2; }); - pcms::MeshFieldsAdapter original("test_ids", mesh, mask); - pcms::MeshFieldsAdapter copied("copied", mesh, mask); - REQUIRE(original.Size() == copied.Size()); - pcms::copy_field(original, copied); - auto copied_array = pcms::get_nodal_data(copied); - auto original_array = pcms::get_nodal_data(original); - REQUIRE(copied_array.size() == copied.Size()); - REQUIRE(original_array.size() == original.Size()); - int sum = 0; - Kokkos::parallel_reduce( - original_array.size(), - KOKKOS_LAMBDA(int i, int& local_sum) { - local_sum += (original_array[i] == copied_array[i]); - }, - sum); - REQUIRE(sum == original_array.size()); - } -} diff --git a/test/test_omega_h_field2_outofbounds.cpp b/test/test_omega_h_field2_outofbounds.cpp index ab77f05f5..da0cd3381 100644 --- a/test/test_omega_h_field2_outofbounds.cpp +++ b/test/test_omega_h_field2_outofbounds.cpp @@ -3,8 +3,9 @@ #include #include #include -#include "pcms/adapter/meshfields/mesh_fields_adapter2.h" -#include "pcms/create_field.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/field_metadata.h" +#include "field_test_utils.h" #include #include @@ -14,33 +15,16 @@ TEST_CASE("omega_h_field2 out of bounds FILL mode") { auto lib = Omega_h::Library{}; auto world = lib.world(); - // Create a 1x1 box mesh (coords from 0 to 1) auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 0, false); - auto layout = - pcms::CreateLagrangeLayout(mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - const auto nverts = mesh.nents(0); - auto mesh_coords = mesh.coords(); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + pcms::test::SetField( + field.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + y; }); - // Set up a simple linear field - auto f = KOKKOS_LAMBDA(Real x, Real y) - { - return x + y; - }; - Omega_h::Write test_f(nverts); - Omega_h::parallel_for( - nverts, OMEGA_H_LAMBDA(int i) { - Real x = mesh_coords[2 * i + 0]; - Real y = mesh_coords[2 * i + 1]; - test_f[i] = f(x, y); - }); - Omega_h::HostWrite test_f_host(test_f); - auto field = layout->CreateFieldReal(); - field->SetDOFHolderData(pcms::make_const_array_view(test_f_host)); - - // Set FILL mode with fill value of -999.0 Real fill_value = -999.0; - field->SetOutOfBoundsMode(pcms::OutOfBoundsMode::FILL, fill_value); // Test points - mix of inside and outside std::vector coords = { @@ -51,32 +35,8 @@ TEST_CASE("omega_h_field2 out of bounds FILL mode") -0.1, 0.5, // outside (x < 0) - should return fill_value }; - std::vector evaluation(coords.size() / 2); - pcms::Rank1View eval_view{evaluation.data(), - evaluation.size()}; - pcms::Rank2View coords_view( - coords.data(), coords.size() / 2, 2); - pcms::FieldDataView data_view( - eval_view, field->GetCoordinateSystem()); - pcms::CoordinateView coordinate_view{ - field->GetCoordinateSystem(), coords_view}; - - auto locale = field->GetLocalizationHint(coordinate_view); - field->Evaluate(locale, data_view); - - // Check results - // Point 0: (0.5, 0.5) - inside, should be close to f(0.5, 0.5) = 1.0 - REQUIRE(std::abs(evaluation[0] - 1.0) < 0.1); - - // Point 1: (1.5, 0.5) - outside, should be fill_value - REQUIRE(evaluation[1] == fill_value); - - // Point 2: (0.5, -0.1) - outside, should be fill_value - REQUIRE(evaluation[2] == fill_value); - - // Point 3: (0.3, 0.7) - inside, should be close to f(0.3, 0.7) = 1.0 - REQUIRE(std::abs(evaluation[3] - 1.0) < 0.1); - - // Point 4: (-0.1, 0.5) - outside, should be fill_value - REQUIRE(evaluation[4] == fill_value); -} \ No newline at end of file + std::vector is_inside = {true, false, false, true, false}; + pcms::test::CheckEvaluationWithFill( + factory, field, coords, is_inside, + OMEGA_H_LAMBDA(Real x, Real y) { return x + y; }, fill_value, 1.0e-10); +} diff --git a/test/test_omega_h_lagrange_field.cpp b/test/test_omega_h_lagrange_field.cpp new file mode 100644 index 000000000..96da54f6b --- /dev/null +++ b/test/test_omega_h_lagrange_field.cpp @@ -0,0 +1,338 @@ +#include +#include +#include +#include +#include + +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/field_metadata.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/mesh_geometry.h" +#include "field_test_utils.h" + +#include +#include +#include + +using pcms::LO; +using pcms::Real; + +static Omega_h::Mesh MakeBox2D(Omega_h::CommPtr world, int nx = 10, int ny = 10) +{ + return Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, nx, ny, 0, + false); +} + +// ---- Layout tests ----------------------------------------------------------- + +TEST_CASE("OmegaHLagrangeLayout order-1 properties") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + pcms::OmegaHLagrangeLayout layout(mesh, 1, 2, + pcms::CoordinateSystem::Cartesian); + + REQUIRE(layout.GetOrder() == 1); + REQUIRE(layout.GetNumComponents() == 2); + REQUIRE(layout.GetNumOwnedDofHolder() == mesh.nents(0)); + REQUIRE(layout.GetNumGlobalDofHolder() == mesh.nglobal_ents(0)); + REQUIRE(layout.IsDistributed()); // always true for Omega_h mesh layouts + + // DOF holder coordinates should match vertex coordinates + auto coords_device = layout.GetDOFHolderCoordinates().GetCoordinates(); + int nverts = mesh.nents(0); + auto coords_view = + pcms::test::CopyCoordinatesToHost(coords_device, nverts, mesh.dim()); + auto mesh_coords = Omega_h::HostRead(mesh.coords()); + REQUIRE(static_cast(coords_view.extent(0)) == nverts); + REQUIRE(static_cast(coords_view.extent(1)) == mesh.dim()); + for (int v = 0; v < nverts; ++v) { + for (int d = 0; d < mesh.dim(); ++d) { + REQUIRE(coords_view(v, d) == + Catch::Approx(mesh_coords[v * mesh.dim() + d])); + } + } + + // GetEntOffsets: vertices are at slot 0, all other slots = nverts + auto offsets = layout.GetEntOffsets(); + REQUIRE(offsets[0] == 0); + for (int i = 1; i < pcms::ent_offsets_len; ++i) + REQUIRE(offsets[i] == nverts); +} + +TEST_CASE("OmegaHLagrangeLayout order-0 properties") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + pcms::OmegaHLagrangeLayout layout(mesh, 0, 1, + pcms::CoordinateSystem::Cartesian); + + REQUIRE(layout.GetOrder() == 0); + REQUIRE(layout.GetNumComponents() == 1); + REQUIRE(layout.GetNumOwnedDofHolder() == mesh.nelems()); + REQUIRE(layout.GetNumGlobalDofHolder() == mesh.nglobal_ents(mesh.dim())); + + // DOF holder coordinates should match element centroids + auto coords_device = layout.GetDOFHolderCoordinates().GetCoordinates(); + int nelems = mesh.nelems(); + auto coords_view = + pcms::test::CopyCoordinatesToHost(coords_device, nelems, mesh.dim()); + auto centroids = + Omega_h::HostRead(pcms::get_entity_centroids(mesh, mesh.dim())); + REQUIRE(static_cast(coords_view.extent(0)) == nelems); + for (int e = 0; e < nelems; ++e) { + for (int d = 0; d < mesh.dim(); ++d) { + REQUIRE(coords_view(e, d) == + Catch::Approx(centroids[e * mesh.dim() + d])); + } + } + + // GetEntOffsets: all DOFs are at entity_dim = mesh.dim() (slot 2 for 2D) + auto offsets = layout.GetEntOffsets(); + for (int i = 0; i <= mesh.dim(); ++i) + REQUIRE(offsets[i] == 0); + for (int i = mesh.dim() + 1; i < pcms::ent_offsets_len; ++i) + REQUIRE(offsets[i] == nelems); +} + +TEST_CASE("OmegaHLagrangeLayout invalid order throws") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + REQUIRE_THROWS_AS( + pcms::OmegaHLagrangeLayout(mesh, 2, 1, pcms::CoordinateSystem::Cartesian), + std::invalid_argument); + REQUIRE_THROWS_AS( + pcms::OmegaHLagrangeLayout(mesh, -1, 1, pcms::CoordinateSystem::Cartesian), + std::invalid_argument); +} + +TEST_CASE("OmegaHLagrangeLayout layout sharing") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + + auto f1 = factory.CreateField(pcms::FieldMetadata{}); + auto f2 = factory.CreateField(pcms::FieldMetadata{}); + + REQUIRE(&f1.GetLayout() == &f2.GetLayout()); +} + +// ---- Order-1 field tests ---------------------------------------------------- + +TEST_CASE("OmegaHLagrangeField order-1: set/get DOF data round-trip") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + int n = factory.GetLayout()->GetNumOwnedDofHolder(); + std::vector data(n); + for (int i = 0; i < n; ++i) + data[i] = static_cast(i); + + pcms::Rank1View view(data.data(), n); + field.GetData().SetDOFHolderDataHost(view); + + auto got = field.GetData().GetDOFHolderDataHost(); + REQUIRE(static_cast(got.size()) == n); + for (int i = 0; i < n; ++i) + REQUIRE(got[i] == Catch::Approx(data[i])); +} + +TEST_CASE("OmegaHLagrangeField order-1: linear function evaluation") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world(), 20, 20); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + pcms::test::SetField( + field.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + pcms::test::CheckEvaluation( + factory, field, pcms::test::StandardEvalCoords2D(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); +} + +// The same linear evaluation test run on the MeshFields-backed order-1 field +// ensures both backends produce identical results for the same inputs. +TEST_CASE("MeshFieldsAdapter order-1: linear function evaluation (shared util)") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world(), 20, 20); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + pcms::test::SetField( + field.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + pcms::test::CheckEvaluation( + factory, field, pcms::test::StandardEvalCoords2D(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); +} + +TEST_CASE("OmegaHLagrangeField order-1: out-of-bounds FILL mode") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + pcms::test::SetField( + field.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + + Real fill_value = -999.0; + std::vector outside{-0.5, 0.5, 1.5, 0.5, 0.5, -0.5, 0.5, 1.5}; + pcms::test::CheckFillMode(factory, field, fill_value, outside); +} + +TEST_CASE("OmegaHLagrangeField order-1: serialize / deserialize round-trip") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + pcms::test::SetField( + field.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + pcms::test::CheckSerializeDeserialize(*factory.GetLayout(), field.GetData()); +} + +// ---- Order-0 field tests ---------------------------------------------------- + +TEST_CASE("OmegaHLagrangeField order-0: set/get DOF data round-trip") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 0, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + int n = factory.GetLayout()->GetNumOwnedDofHolder(); + std::vector data(n, 3.14); + pcms::Rank1View view(data.data(), n); + field.GetData().SetDOFHolderDataHost(view); + + auto got = field.GetData().GetDOFHolderDataHost(); + REQUIRE(static_cast(got.size()) == n); + for (int i = 0; i < n; ++i) + REQUIRE(got[i] == Catch::Approx(3.14)); +} + +TEST_CASE("OmegaHLagrangeField order-0: constant field evaluation") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world(), 10, 10); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 0, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + const Real kValue = 42.0; + int nelems = mesh.nelems(); + std::vector data(nelems, kValue); + pcms::Rank1View view(data.data(), nelems); + field.GetData().SetDOFHolderDataHost(view); + + auto pts = pcms::test::StandardEvalCoords2D(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, factory.GetCoordinateSystem()); + auto evaluator = factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View eval_device("eval", n); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + auto out = pcms::Rank2View( + eval_device.data(), n, 1); + evaluator->Evaluate(field, out); + auto eval_host = + Kokkos::create_mirror_view_and_copy(pcms::HostMemorySpace(), eval_device); + + for (int i = 0; i < n; ++i) + REQUIRE(eval_host(i) == Catch::Approx(kValue)); +} + +TEST_CASE("OmegaHLagrangeField order-0: out-of-bounds FILL mode") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 0, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + int nelems = mesh.nelems(); + std::vector data(nelems, 1.0); + pcms::Rank1View view(data.data(), nelems); + field.GetData().SetDOFHolderDataHost(view); + + Real fill_value = -1.0; + std::vector outside{-0.5, 0.5, 1.5, 0.5}; + pcms::test::CheckFillMode(factory, field, fill_value, outside); +} + +TEST_CASE("OmegaHLagrangeField order-0: serialize / deserialize round-trip") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 0, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + int nelems = mesh.nelems(); + std::vector data(nelems); + for (int i = 0; i < nelems; ++i) + data[i] = static_cast(i); + pcms::Rank1View view(data.data(), nelems); + field.GetData().SetDOFHolderDataHost(view); + + pcms::test::CheckSerializeDeserialize(*factory.GetLayout(), field.GetData()); +} + +// ---- Layout sharing communicator contract ----------------------------------- +// +// BEHAVIORAL CONTRACT (requires MPI/redev — exercised by +// test_field_communication with clientId=2/3 via test_shared_layout): +// +// When two fields are added to an Application2 that share the same FieldLayout +// (e.g. both created from the same LagrangeFunctionSpace), the Application2 +// must reuse a single FieldLayoutCommunicator for both fields rather than +// creating separate communicators. This is verified by: +// - Calling AddLayout() registers exactly one FieldLayoutCommunicator +// - Calling AddField() for additional fields with the same layout leaves the +// count at one (Application2::GetLayoutCommunicatorCount() == 1) +// See test/test_field_communication.cpp::test_shared_layout for the full test. + +// ---- Temporary factory lifetime safety -------------------------------------- + +TEST_CASE("OmegaHLagrangeField: field valid after layout destruction") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + + std::optional> field; + { + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + field.emplace(factory.CreateField(pcms::FieldMetadata{})); + } // factory goes out of scope; field keeps layout alive + + pcms::test::SetField( + *field, OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + // Just verify data was set correctly (no evaluator needed for this lifetime + // test) + auto data = field->GetDOFHolderDataHost(); + REQUIRE(data.size() > 0); +} diff --git a/test/test_point_evaluator.cpp b/test/test_point_evaluator.cpp new file mode 100644 index 000000000..ae6b9245a --- /dev/null +++ b/test/test_point_evaluator.cpp @@ -0,0 +1,414 @@ +#include +#include +#include +#include + +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/function_space/polynomial_reconstruction.hpp" +#include "pcms/field/function_space/spline.h" +#include "pcms/field/field_data.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/field_metadata.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/utility/arrays.h" +#include "field_test_utils.h" + +using pcms::CoordinateSystem; +using pcms::Real; + +// ============================================================================ +// OmegaH order-1 — basic evaluation via new API +// ============================================================================ + +TEST_CASE("PointEvaluator: OmegaH order-1 linear evaluation") +{ + auto lib = Omega_h::Library{}; + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 100, + 100, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + auto field_data = factory.CreateField(); + pcms::test::SetField( + field_data.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + + auto pts = pcms::test::StandardEvalCoords2D(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + auto evaluator = factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + pcms::test::CheckEvaluation( + *evaluator, field_data, pts, + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); +} + +// ============================================================================ +// OmegaH order-1 — repeated evaluation: same PointEvaluator, two FieldDatas +// ============================================================================ + +TEST_CASE("PointEvaluator: same evaluator reused for two FieldData objects") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 50, 50, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + auto field_a = factory.CreateField(); + auto field_b = factory.CreateField(); + + // field_a: linear_f; field_b: constant 42 + pcms::test::SetField( + field_a.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + pcms::test::SetField( + field_b.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real, Real) { return Real(42); }); + + auto pts = pcms::test::StandardEvalCoords2D(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + + // Create the PointEvaluator once + auto evaluator = factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View out_a_device("out_a", n); + Kokkos::View out_b_device("out_b", n); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + auto view_a = pcms::Rank2View( + out_a_device.data(), n, 1); + auto view_b = pcms::Rank2View( + out_b_device.data(), n, 1); + + // Evaluate field_a then field_b with the same evaluator + evaluator->Evaluate(field_a, view_a); + evaluator->Evaluate(field_b, view_b); + + auto out_a_host = + Kokkos::create_mirror_view_and_copy(pcms::HostMemorySpace(), out_a_device); + auto out_b_host = + Kokkos::create_mirror_view_and_copy(pcms::HostMemorySpace(), out_b_device); + + for (int i = 0; i < n; ++i) { + Real x = pts[2 * static_cast(i)], + y = pts[2 * static_cast(i) + 1]; + REQUIRE(out_a_host(i) == + Catch::Approx(pcms::test::linear_f(x, y)).margin(1e-10)); + REQUIRE(out_b_host(i) == Catch::Approx(42.0).margin(1e-10)); + } +} + +// ============================================================================ +// OmegaH order-1 — OutOfBoundsPolicy::FILL +// ============================================================================ + +TEST_CASE("PointEvaluator: OmegaH order-1 out-of-bounds fill") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 20, 20, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + auto field_data = factory.CreateField(); + pcms::test::SetField( + field_data.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + + // Points clearly outside [0,1]^2 + const std::vector outside_pts = {-0.5, 0.5, 1.5, 0.5, + 0.5, -0.5, 0.5, 1.5}; + auto device_coords = pcms::test::CreateDeviceCoordinateView( + outside_pts, CoordinateSystem::Cartesian); + pcms::OutOfBoundsPolicy policy{pcms::OutOfBoundsMode::FILL, -999.0}; + auto evaluator = + factory.CreatePointEvaluator(pcms::EvaluationRequest::FromCoordinates( + device_coords.coordinate_view, policy)); + pcms::test::CheckFillMode(*evaluator, field_data, -999.0, outside_pts); +} + +// ============================================================================ +// UniformGrid — basic evaluation via new API +// ============================================================================ + +TEST_CASE("PointEvaluator: UniformGrid order-1 linear evaluation") +{ + // 2D grid: [0,1]^2 with 10x10 divisions + const int N = 10; + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {1.0, 1.0}; + grid.divisions = {N, N}; + + auto factory = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, CoordinateSystem::Cartesian, 1); + + auto field_data = factory.CreateField(); + pcms::test::SetField( + field_data.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + auto pts = pcms::test::StandardEvalCoords2D(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + auto evaluator = factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + pcms::test::CheckEvaluation( + *evaluator, field_data, pts, + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }, 1e-8); +} + +TEST_CASE("PointEvaluator: SplineFunctionSpace uniform-grid evaluation") +{ + const int N = 10; + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {1.0, 1.0}; + grid.divisions = {N, N}; + + auto factory = pcms::SplineFunctionSpace::FromUniformGrid( + grid, CoordinateSystem::Cartesian); + + auto field_data = factory.CreateField(); + pcms::test::SetField( + field_data.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + auto pts = pcms::test::StandardEvalCoords2D(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + auto evaluator = factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + pcms::test::CheckEvaluation( + *evaluator, field_data, pts, + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }, 1e-8); +} + +// ============================================================================ +// FieldLayout — metadata interface +// ============================================================================ + +TEST_CASE("FieldLayout: metadata queries") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto layout = factory.GetLayout(); + + auto coords = layout->GetDOFHolderCoordinates(); + REQUIRE(coords.GetCoordinateSystem() == CoordinateSystem::Cartesian); + REQUIRE(coords.GetCoordinates().extent(0) > 0); + REQUIRE(coords.GetCoordinates().extent(1) == 2); +} + +// ============================================================================ +// FieldData / FieldLayout metadata queries +// ============================================================================ + +TEST_CASE("FieldData: layout metadata queries") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto field_data = factory.CreateField(); + + auto coords = factory.GetLayout()->GetDOFHolderCoordinates(); + REQUIRE(coords.GetCoordinateSystem() == CoordinateSystem::Cartesian); + REQUIRE(coords.GetCoordinates().extent(0) > 0); + REQUIRE(coords.GetCoordinates().extent(1) == 2); +} + +// ============================================================================ +// CreateFieldData / SimpleFieldData round-trip +// ============================================================================ + +TEST_CASE("SimpleFieldData: set and get DOF holder data round-trip") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto field_data = factory.CreateField(); + + auto& layout = *factory.GetLayout(); + int n = layout.GetNumOwnedDofHolder(); + REQUIRE(n > 0); + + // Write sequential values + std::vector data_in(n); + for (int i = 0; i < n; ++i) + data_in[i] = static_cast(i) * 0.5; + + field_data.GetData().SetDOFHolderDataHost( + pcms::Rank1View(data_in.data(), n)); + + auto data_out = field_data.GetData().GetDOFHolderDataHost(); + REQUIRE(static_cast(data_out.size()) == n); + for (int i = 0; i < n; ++i) { + REQUIRE(data_out[i] == Catch::Approx(data_in[i])); + } +} + +// ============================================================================ +// MeshFields FieldEvaluatorFactory metadata (only when MeshFields is enabled) +// ============================================================================ + +#ifdef PCMS_ENABLE_MESHFIELDS +TEST_CASE("FieldLayout: MeshFields metadata queries") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); + + auto layout = factory.GetLayout(); + auto coords = layout->GetDOFHolderCoordinates(); + REQUIRE(coords.GetCoordinateSystem() == CoordinateSystem::Cartesian); + REQUIRE(coords.GetCoordinates().extent(0) > 0); + REQUIRE(coords.GetCoordinates().extent(1) == 2); +} + +TEST_CASE("PointEvaluator: MeshFields order-1 linear evaluation") +{ + auto lib = Omega_h::Library{}; + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 100, + 100, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); + + auto field_data = factory.CreateField(); + pcms::test::SetField( + field_data.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + + auto pts = pcms::test::StandardEvalCoords2D(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + auto evaluator = factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + pcms::test::CheckEvaluation( + *evaluator, field_data, pts, + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); +} + +TEST_CASE("PointEvaluator: MeshFields out-of-bounds fill") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 20, 20, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); + + auto field_data = factory.CreateField(); + pcms::test::SetField( + field_data.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + + const std::vector outside_pts = {-0.5, 0.5, 1.5, 0.5, + 0.5, -0.5, 0.5, 1.5}; + auto device_coords = pcms::test::CreateDeviceCoordinateView( + outside_pts, CoordinateSystem::Cartesian); + pcms::OutOfBoundsPolicy policy{pcms::OutOfBoundsMode::FILL, -999.0}; + auto evaluator = + factory.CreatePointEvaluator(pcms::EvaluationRequest::FromCoordinates( + device_coords.coordinate_view, policy)); + pcms::test::CheckFillMode(*evaluator, field_data, -999.0, outside_pts); +} + +TEST_CASE( + "PointEvaluator: MeshFields same evaluator reused for two FieldData objects") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 50, 50, 0, false); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); + + auto field_a = factory.CreateField(); + auto field_b = factory.CreateField(); + pcms::test::SetField( + field_a.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + pcms::test::SetField( + field_b.GetData(), *factory.GetLayout(), + OMEGA_H_LAMBDA(Real, Real) { return Real(42); }); + + auto pts = pcms::test::StandardEvalCoords2D(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + + auto evaluator = factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View out_a_device("out_a", n); + Kokkos::View out_b_device("out_b", n); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + auto view_a = pcms::Rank2View( + out_a_device.data(), n, 1); + auto view_b = pcms::Rank2View( + out_b_device.data(), n, 1); + + evaluator->Evaluate(field_a, view_a); + evaluator->Evaluate(field_b, view_b); + + auto out_a_host = + Kokkos::create_mirror_view_and_copy(pcms::HostMemorySpace(), out_a_device); + auto out_b_host = + Kokkos::create_mirror_view_and_copy(pcms::HostMemorySpace(), out_b_device); + + for (int i = 0; i < n; ++i) { + Real x = pts[2 * static_cast(i)], + y = pts[2 * static_cast(i) + 1]; + REQUIRE(out_a_host(i) == + Catch::Approx(pcms::test::linear_f(x, y)).margin(1e-10)); + REQUIRE(out_b_host(i) == Catch::Approx(42.0).margin(1e-10)); + } +} + +TEST_CASE("LagrangeFunctionSpace: MeshFields rejects multi-component fields") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 0, false); + + REQUIRE_THROWS_AS(pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 2, CoordinateSystem::Cartesian, "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields), + pcms::pcms_error); +} +#endif // PCMS_ENABLE_MESHFIELDS diff --git a/test/test_point_search.cpp b/test/test_point_search.cpp index 58d5100e7..6d8dc6a12 100644 --- a/test/test_point_search.cpp +++ b/test/test_point_search.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include #include diff --git a/test/test_polynomial_reconstruction_function_space.cpp b/test/test_polynomial_reconstruction_function_space.cpp new file mode 100644 index 000000000..9e0dc810a --- /dev/null +++ b/test/test_polynomial_reconstruction_function_space.cpp @@ -0,0 +1,201 @@ +#include +#include + +#include "pcms/field/function_space/polynomial_reconstruction.hpp" +#include "pcms/field/field_metadata.h" +#include "pcms/discretization/discretization.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/layout/point_cloud.h" +#include "field_test_utils.h" + +#include +#include +#include + +using pcms::CoordinateSystem; +using pcms::HostMemorySpace; +using pcms::LO; +using pcms::Rank1View; +using pcms::Rank2View; +using pcms::Real; + +namespace +{ + +std::vector MakeCoords2D() +{ + return {0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0}; +} + +} // namespace + +TEST_CASE( + "PolynomialReconstructionFunctionSpace creates point-cloud layout metadata") +{ + auto coords = MakeCoords2D(); + Rank2View coords_view(coords.data(), 4, 2); + + auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian); + auto layout = factory.GetLayout(); + + REQUIRE(layout->GetNumComponents() == 1); + REQUIRE(layout->GetNumOwnedDofHolder() == 4); + REQUIRE(layout->GetNumGlobalDofHolder() == 4); + REQUIRE_FALSE(layout->IsDistributed()); + + auto dof_coords = layout->GetDOFHolderCoordinates().GetCoordinates(); + auto dof_coords_host = pcms::test::CopyCoordinatesToHost(dof_coords, 4, 2); + + REQUIRE(static_cast(dof_coords_host.extent(0)) == 4); + REQUIRE(static_cast(dof_coords_host.extent(1)) == 2); + for (int i = 0; i < 4; ++i) { + REQUIRE(dof_coords_host(i, 0) == + Catch::Approx(coords[2 * static_cast(i)])); + REQUIRE(dof_coords_host(i, 1) == + Catch::Approx(coords[2 * static_cast(i) + 1])); + } +} + +TEST_CASE("PolynomialReconstructionFunctionSpace fields share layout") +{ + auto coords = MakeCoords2D(); + Rank2View coords_view(coords.data(), 4, 2); + + auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian); + auto source = factory.CreateField(pcms::FieldMetadata{}); + auto target = factory.CreateField(pcms::FieldMetadata{}); + + REQUIRE(&source.GetLayout() == &target.GetLayout()); +} + +TEST_CASE("PolynomialReconstructionFunctionSpace point-cloud field set/get DOF " + "round-trip") +{ + auto coords = MakeCoords2D(); + Rank2View coords_view(coords.data(), 4, 2); + + auto field = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian) + .CreateField(pcms::FieldMetadata{}); + + std::vector data{1.0, 2.0, 3.0, 4.0}; + Rank1View data_view(data.data(), data.size()); + field.GetData().SetDOFHolderDataHost(data_view); + + auto got = field.GetData().GetDOFHolderDataHost(); + REQUIRE(got.size() == data.size()); + for (LO i = 0; i < static_cast(data.size()); ++i) { + REQUIRE(got[i] == Catch::Approx(data[i])); + } +} + +TEST_CASE("PolynomialReconstructionFunctionSpace point-cloud field serialize / " + "deserialize round-trip") +{ + auto coords = MakeCoords2D(); + Rank2View coords_view(coords.data(), 4, 2); + + auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + + std::vector data{5.0, 6.0, 7.0, 8.0}; + Rank1View data_view(data.data(), data.size()); + field.GetData().SetDOFHolderDataHost(data_view); + + pcms::test::CheckSerializeDeserialize(*factory.GetLayout(), field.GetData()); +} + +TEST_CASE("PolynomialReconstructionFunctionSpace field keeps layout alive " + "after temporary factory destruction") +{ + auto coords = MakeCoords2D(); + Rank2View coords_view(coords.data(), 4, 2); + + auto field = [&]() { + auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian); + return factory.CreateField(pcms::FieldMetadata{}); + }(); + + auto point_cloud_layout = + dynamic_cast(&field.GetLayout()); + REQUIRE(point_cloud_layout != nullptr); + REQUIRE(point_cloud_layout->GetNumOwnedDofHolder() == 4); + + std::vector data{9.0, 10.0, 11.0, 12.0}; + Rank1View data_view(data.data(), data.size()); + field.SetDOFHolderDataHost(data_view); + + auto got = field.GetDOFHolderDataHost(); + REQUIRE(got[0] == Catch::Approx(9.0)); + REQUIRE(got[3] == Catch::Approx(12.0)); +} + +TEST_CASE("Different layouts on the same mesh report SameEntities") +{ + Omega_h::Library lib; + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 2, + 2, 0, false); + + auto nodal = pcms::PolynomialReconstructionFunctionSpace::FromMesh( + mesh, pcms::Face, CoordinateSystem::Cartesian); + auto lagrange = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian); + + auto nodal_disc = nodal.GetLayout()->GetDiscretization(); + auto lagrange_disc = lagrange.GetLayout()->GetDiscretization(); + + REQUIRE(nodal_disc != nullptr); + REQUIRE(lagrange_disc != nullptr); + REQUIRE(nodal_disc->SameEntities(*lagrange_disc)); + + REQUIRE(nodal.GetLayout()->GetNumOwnedDofHolder() == mesh.nfaces()); + REQUIRE(lagrange.GetLayout()->GetNumOwnedDofHolder() == mesh.nverts()); + + REQUIRE(nodal_disc->GetNumEntities(pcms::Face) == mesh.nfaces()); + REQUIRE(lagrange_disc->GetNumEntities(pcms::Vertex) == mesh.nverts()); +} + +TEST_CASE("Layouts on different meshes do not report SameEntities") +{ + Omega_h::Library lib; + auto mesh_a = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, + 2, 2, 0, false); + auto mesh_b = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, + 3, 3, 0, false); + + auto nodal_a = pcms::PolynomialReconstructionFunctionSpace::FromMesh( + mesh_a, pcms::Vertex, CoordinateSystem::Cartesian); + auto nodal_b = pcms::PolynomialReconstructionFunctionSpace::FromMesh( + mesh_b, pcms::Vertex, CoordinateSystem::Cartesian); + + auto disc_a = nodal_a.GetLayout()->GetDiscretization(); + auto disc_b = nodal_b.GetLayout()->GetDiscretization(); + + REQUIRE_FALSE(disc_a->SameEntities(*disc_b)); +} + +TEST_CASE( + "Standalone point-cloud layout does not report SameEntities with mesh layout") +{ + Omega_h::Library lib; + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 2, + 2, 0, false); + + auto lagrange = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, CoordinateSystem::Cartesian); + + auto coords = MakeCoords2D(); + Rank2View coords_view(coords.data(), 4, 2); + auto standalone = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian); + + auto mesh_disc = lagrange.GetLayout()->GetDiscretization(); + auto point_cloud_disc = standalone.GetLayout()->GetDiscretization(); + + REQUIRE_FALSE(mesh_disc->SameEntities(*point_cloud_disc)); + REQUIRE_FALSE(point_cloud_disc->SameEntities(*mesh_disc)); +} diff --git a/test/test_polynomial_reconstruction_mls_evaluation.cpp b/test/test_polynomial_reconstruction_mls_evaluation.cpp new file mode 100644 index 000000000..3aef08d84 --- /dev/null +++ b/test/test_polynomial_reconstruction_mls_evaluation.cpp @@ -0,0 +1,408 @@ +#include +#include + +#include "pcms/field/function_space/polynomial_reconstruction.hpp" +#include "pcms/field/evaluator/mls_options.h" +#include "pcms/field/field_metadata.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "field_test_utils.h" + +#include +#include +#include +#include + +using pcms::CoordinateSystem; +using pcms::CoordinateView; +using pcms::DeviceMemorySpace; +using pcms::HostMemorySpace; +using pcms::Rank1View; +using pcms::Rank2View; +using pcms::Real; + +namespace +{ + +// Build a regular NxN grid of source points over [0,1]^2. +std::vector MakeGrid2D(int N) +{ + std::vector pts; + pts.reserve(static_cast(N) * N * 2); + for (int j = 0; j < N; ++j) { + for (int i = 0; i < N; ++i) { + pts.push_back(static_cast(i) / (N - 1)); + pts.push_back(static_cast(j) / (N - 1)); + } + } + return pts; +} + +std::vector MakeGrid3D(int N) +{ + std::vector pts; + pts.reserve(static_cast(N) * N * N * 3); + for (int k = 0; k < N; ++k) { + for (int j = 0; j < N; ++j) { + for (int i = 0; i < N; ++i) { + pts.push_back(static_cast(i) / (N - 1)); + pts.push_back(static_cast(j) / (N - 1)); + pts.push_back(static_cast(k) / (N - 1)); + } + } + } + return pts; +} + +// MLSOptions tuned for a 7x7 source grid on [0,1]^2. +pcms::MLSOptions DefaultTestOptions() +{ + pcms::MLSOptions opts; + opts.radius = 0.35; + opts.min_req_supports = 6; + opts.degree = 1; + opts.adapt_radius = true; + return opts; +} + +pcms::MLSOptions DefaultTestOptions3D() +{ + pcms::MLSOptions opts; + opts.radius = 0.8; + opts.min_req_supports = 10; + opts.degree = 1; + opts.adapt_radius = true; + return opts; +} + +// Interior query points for a unit box — same as StandardEvalCoords2D. +std::vector QueryPoints() +{ + return pcms::test::StandardEvalCoords2D(); +} + +pcms::MLSOptions SweepTestOptions(unsigned degree, + pcms::RadialBasisFunction basis) +{ + pcms::MLSOptions opts; + opts.radius = 0.45; + opts.min_req_supports = (degree < 2u) ? 8u : 16u; + opts.degree = degree; + opts.adapt_radius = true; + opts.basis = basis; + return opts; +} + +const char* BasisName(pcms::RadialBasisFunction basis) +{ + switch (basis) { + case pcms::RadialBasisFunction::RBF_GAUSSIAN: return "RBF_GAUSSIAN"; + case pcms::RadialBasisFunction::RBF_C4: return "RBF_C4"; + case pcms::RadialBasisFunction::RBF_CONST: return "RBF_CONST"; + case pcms::RadialBasisFunction::RBF_MULTIQUADRIC: return "RBF_MULTIQUADRIC"; + default: return "UNEXPECTED_BASIS"; + } +} + +template +void CheckPolynomialReproduction(unsigned degree, + pcms::RadialBasisFunction basis, Func func, + double abs_tol) +{ + CAPTURE(BasisName(basis)); + CAPTURE(degree); + + auto src = MakeGrid2D(9); + Rank2View coords_view(src.data(), 81, 2); + + auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian, SweepTestOptions(degree, basis)); + auto field = fs.CreateField(pcms::FieldMetadata{}); + pcms::test::SetField(field.GetData(), *fs.GetLayout(), func); + + auto pts = QueryPoints(); + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + auto evaluator = fs.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + pcms::test::CheckEvaluation(*evaluator, field, pts, func, abs_tol); +} + +} // namespace + +// ============================================================================ +// Polynomial reproduction sweep +// ============================================================================ + +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: reproduces " + "representative polynomials " + "across degree and basis options") +{ + auto bases = std::array{pcms::RadialBasisFunction::RBF_GAUSSIAN, + pcms::RadialBasisFunction::RBF_C4, + pcms::RadialBasisFunction::RBF_CONST, + pcms::RadialBasisFunction::RBF_MULTIQUADRIC}; + + for (auto basis : bases) { + CheckPolynomialReproduction( + 0, basis, OMEGA_H_LAMBDA(Real, Real) { return 3.14; }, 5e-3); + CheckPolynomialReproduction( + 1, basis, OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }, 5e-3); + CheckPolynomialReproduction( + 2, basis, + OMEGA_H_LAMBDA(Real x, Real y) { return x * x + x * y + 2.0 * y * y; }, + 5e-3); + } +} + +// ============================================================================ +// PointEvaluator reused across two FieldData objects +// ============================================================================ + +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: same PointEvaluator " + "reused for two " + "FieldData objects") +{ + auto src = MakeGrid2D(7); + Rank2View coords_view(src.data(), 49, 2); + + auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian, DefaultTestOptions()); + auto field_a = fs.CreateField(pcms::FieldMetadata{}); + auto field_b = fs.CreateField(pcms::FieldMetadata{}); + + pcms::test::SetField( + field_a.GetData(), *fs.GetLayout(), + OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + const Real cval = 7.0; + pcms::test::SetField( + field_b.GetData(), *fs.GetLayout(), + OMEGA_H_LAMBDA(Real, Real) { return cval; }); + + auto pts = QueryPoints(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + auto evaluator = fs.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View out_a_device("out_a", n); + Kokkos::View out_b_device("out_b", n); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + Rank2View view_a(out_a_device.data(), + n, 1); + Rank2View view_b(out_b_device.data(), + n, 1); + + evaluator->Evaluate(field_a, view_a); + evaluator->Evaluate(field_b, view_b); + + auto out_a_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_a_device); + auto out_b_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_b_device); + + for (int i = 0; i < n; ++i) { + Real x = pts[2 * static_cast(i)], + y = pts[2 * static_cast(i) + 1]; + REQUIRE(out_a_host(i) == + Catch::Approx(pcms::test::linear_f(x, y)).margin(5e-3)); + REQUIRE(out_b_host(i) == Catch::Approx(cval).margin(5e-3)); + } +} + +// ============================================================================ +// Non-scalar output view must throw +// ============================================================================ + +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: Evaluate throws for " + "num_components != 1") +{ + auto src = MakeGrid2D(5); + Rank2View coords_view(src.data(), 25, 2); + + auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian); + auto field = fs.CreateField(pcms::FieldMetadata{}); + + auto pts = QueryPoints(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + auto evaluator = fs.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + // Two-component output — must throw + Kokkos::View out_device("out", + static_cast(n) * 2); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + auto out_view = + Rank2View(out_device.data(), n, 2); + REQUIRE_THROWS(evaluator->Evaluate(field, out_view)); +} + +// ============================================================================ +// Default MLSOptions (no explicit options) — smoke test +// ============================================================================ + +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: default MLSOptions — " + "smoke evaluation") +{ + auto src = MakeGrid2D(7); + Rank2View coords_view(src.data(), 49, 2); + + // Use default options — no third argument + auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian); + auto field = fs.CreateField(pcms::FieldMetadata{}); + pcms::test::SetField( + field.GetData(), *fs.GetLayout(), + OMEGA_H_LAMBDA(Real, Real) { return Real(1.0); }); + + auto pts = QueryPoints(); + int n = static_cast(pts.size()) / 2; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + auto evaluator = fs.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View out_device("out", n); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + auto out_view = + Rank2View(out_device.data(), n, 1); + // Just verify it runs without error and returns finite values + REQUIRE_NOTHROW(evaluator->Evaluate(field, out_view)); + auto out_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); + for (int i = 0; i < n; ++i) + REQUIRE(std::isfinite(out_host(i))); +} + +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: CreatePointEvaluator " + "rejects coordinate " + "system mismatch") +{ + auto src = MakeGrid2D(5); + Rank2View coords_view(src.data(), 25, 2); + + auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cylindrical, DefaultTestOptions()); + + auto pts = QueryPoints(); + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + REQUIRE_THROWS(fs.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view))); +} + +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: CreatePointEvaluator " + "rejects non-Cartesian " + "point-cloud coordinates") +{ + auto src = MakeGrid2D(5); + Rank2View coords_view(src.data(), 25, 2); + + auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cylindrical, DefaultTestOptions()); + + auto pts = QueryPoints(); + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cylindrical); + REQUIRE_THROWS(fs.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view))); +} + +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: radius option is " + "interpreted as a physical cutoff") +{ + std::vector src{0.0, 0.0, 0.6, 0.0}; + Rank2View coords_view(src.data(), 2, 2); + + pcms::MLSOptions opts; + opts.radius = 0.5; + opts.min_req_supports = 1; + opts.degree = 0; + opts.adapt_radius = false; + opts.basis = pcms::RadialBasisFunction::RBF_CONST; + + auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian, opts); + auto field = fs.CreateField(pcms::FieldMetadata{}); + std::vector dof_values{1.0, 5.0}; + field.GetData().SetDOFHolderDataHost(Rank1View( + dof_values.data(), dof_values.size())); + + std::vector pts{0.0, 0.0}; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); + auto evaluator = fs.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + Kokkos::View out_device("out", 1); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + Rank2View out_view(out_device.data(), + 1, 1); + evaluator->Evaluate(field, out_view); + auto out_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); + + REQUIRE(out_host(0) == Catch::Approx(1.0).margin(1e-8)); +} + +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: 3D point clouds preserve " + "z coordinates in " + "layout and evaluation") +{ + auto src = MakeGrid3D(3); + Rank2View coords_view(src.data(), 27, 3); + + auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( + coords_view, CoordinateSystem::Cartesian, DefaultTestOptions3D()); + auto layout_coords_device = + fs.GetLayout()->GetDOFHolderCoordinates().GetCoordinates(); + auto layout_coords = + pcms::test::CopyCoordinatesToHost(layout_coords_device, 27, 3); + REQUIRE(static_cast(layout_coords.extent(1)) == 3); + for (int i = 0; i < 27; ++i) { + REQUIRE(layout_coords(i, 0) == Catch::Approx(src[3 * i + 0])); + REQUIRE(layout_coords(i, 1) == Catch::Approx(src[3 * i + 1])); + REQUIRE(layout_coords(i, 2) == Catch::Approx(src[3 * i + 2])); + } + + auto field = fs.CreateField(pcms::FieldMetadata{}); + std::vector dof_values(27); + for (int i = 0; i < 27; ++i) { + const Real x = src[3 * i + 0]; + const Real y = src[3 * i + 1]; + const Real z = src[3 * i + 2]; + dof_values[i] = x + 2.0 * y + 3.0 * z; + } + field.GetData().SetDOFHolderDataHost(Rank1View( + dof_values.data(), dof_values.size())); + + std::vector pts{0.5, 0.5, 0.25, 0.5, 0.5, 0.75}; + auto device_coords = + pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian, 3); + auto evaluator = fs.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + Kokkos::View out_device("out", 2); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + Rank2View out_view(out_device.data(), + 2, 1); + evaluator->Evaluate(field, out_view); + auto out_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); + + // This 3D case is a small, low-resolution support cloud with MLS weights + // built from a finite-radius neighborhood rather than exact nodal lookup, so + // it is checked with a slightly looser tolerance than the denser 2D tests. + REQUIRE(out_host(0) == Catch::Approx(2.25).margin(1e-2)); + REQUIRE(out_host(1) == Catch::Approx(3.75).margin(1e-2)); + REQUIRE(out_host(1) - out_host(0) == Catch::Approx(1.5).margin(1e-2)); +} diff --git a/test/test_proxy_coupling.cpp b/test/test_proxy_coupling.cpp index 6f8a3ffcb..759cd8d3f 100644 --- a/test/test_proxy_coupling.cpp +++ b/test/test_proxy_coupling.cpp @@ -6,17 +6,14 @@ #include #include #include "test_support.h" -#include "pcms/coupler2.h" -#include "pcms/create_field.h" +#include "pcms/coupler/coupler.hpp" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/field_metadata.h" #include #include -using pcms::Copy; using pcms::GO; -using pcms::Lagrange; using pcms::make_array_view; -using pcms::MeshFieldsAdapter; -using pcms::OmegaHFieldAdapter; using namespace std::chrono_literals; @@ -24,11 +21,11 @@ static constexpr bool done = true; static constexpr int COMM_ROUNDS = 4; namespace ts = test_support; -void initializeFieldWithGids(pcms::FieldT* field, +void initializeFieldWithGids(const pcms::FieldLayout& layout, + pcms::FieldData* field, pcms::Real multiplier = 1.0) { - auto& layout = field->GetLayout(); - auto gids = layout.GetGids(); + auto gids = layout.GetGidsHost(); const auto n = layout.GetNumOwnedDofHolder(); Omega_h::HostWrite ids(n); @@ -38,17 +35,17 @@ void initializeFieldWithGids(pcms::FieldT* field, Kokkos::RangePolicy(0, n), [=](int i) { ids[i] = gids[i] * multiplier; }); - field->SetDOFHolderData(pcms::make_const_array_view(ids)); + field->SetDOFHolderDataHost(pcms::make_const_array_view(ids)); } -bool validateField(pcms::FieldT* field, +bool validateField(const pcms::FieldLayout& layout, + pcms::FieldData* field, const std::string& field_name, int rank, pcms::Real multiplier = 1.0) { - auto& layout = field->GetLayout(); - auto gids = layout.GetGids(); - auto copied_array = field->GetDOFHolderData(); - auto owned = layout.GetOwned(); + auto gids = layout.GetGidsHost(); + auto copied_array = field->GetDOFHolderDataHost(); + auto owned = layout.GetOwnedHost(); const auto n = layout.GetNumOwnedDofHolder(); PCMS_ALWAYS_ASSERT(copied_array.size() == gids.size()); @@ -88,21 +85,21 @@ void xgc_delta_f(MPI_Comm comm, Omega_h::Mesh& mesh) int rank; MPI_Comm_rank(comm, &rank); - pcms::Coupler2 coupler("proxy_couple", comm, false, {}); - pcms::Application2* app = coupler.AddApplication("proxy_couple_xgc_delta_f"); + pcms::Coupler coupler("proxy_couple", comm, false, {}); + pcms::Application* app = coupler.AddApplication("proxy_couple_xgc_delta_f"); - auto& layout = app->AddLayout( - "gids", - pcms::CreateLagrangeLayout(mesh, 1, 1, pcms::CoordinateSystem::Cartesian)); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + app->AddLayout("gids", factory.GetLayout()); - auto gids_field = layout.CreateFieldReal(); - auto gids2_field = layout.CreateFieldReal(); + auto gids_field = factory.CreateField(pcms::FieldMetadata{}); + auto gids2_field = factory.CreateField(pcms::FieldMetadata{}); - auto* gids_ptr = gids_field.get(); - auto* gids2_ptr = gids2_field.get(); + auto* gids_ptr = &gids_field.GetData(); + auto* gids2_ptr = &gids2_field.GetData(); - initializeFieldWithGids(gids_ptr, 1.0); - initializeFieldWithGids(gids2_ptr, 2.0); + initializeFieldWithGids(*factory.GetLayout(), gids_ptr, 1.0); + initializeFieldWithGids(*factory.GetLayout(), gids2_ptr, 2.0); app->AddField("gids", std::move(gids_field)); app->AddField("gids2", std::move(gids2_field)); @@ -116,7 +113,7 @@ void xgc_delta_f(MPI_Comm comm, Omega_h::Mesh& mesh) app->ReceiveField("gids"); //(Alt) df_gid_field->Receive(); app->EndReceivePhase(); - if (!validateField(gids_ptr, "gids", rank, 1.0)) { + if (!validateField(*factory.GetLayout(), gids_ptr, "gids", rank, 1.0)) { std::cerr << "xgc_delta_f: Field validation failed at round " << i << std::endl; exit(EXIT_FAILURE); @@ -129,18 +126,18 @@ void xgc_total_f(MPI_Comm comm, Omega_h::Mesh& mesh) int rank; MPI_Comm_rank(comm, &rank); - pcms::Coupler2 coupler("proxy_couple", comm, false, {}); - pcms::Application2* app = coupler.AddApplication("proxy_couple_xgc_total_f"); + pcms::Coupler coupler("proxy_couple", comm, false, {}); + pcms::Application* app = coupler.AddApplication("proxy_couple_xgc_total_f"); - auto& layout = app->AddLayout( - "gids", - pcms::CreateLagrangeLayout(mesh, 1, 1, pcms::CoordinateSystem::Cartesian)); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + app->AddLayout("gids", factory.GetLayout()); - auto gids_field = layout.CreateFieldReal(); + auto gids_field = factory.CreateField(pcms::FieldMetadata{}); - auto* gids_ptr = gids_field.get(); + auto* gids_ptr = &gids_field.GetData(); - initializeFieldWithGids(gids_ptr, 10.0); + initializeFieldWithGids(*factory.GetLayout(), gids_ptr, 10.0); app->AddField("gids", std::move(gids_field)); @@ -153,7 +150,7 @@ void xgc_total_f(MPI_Comm comm, Omega_h::Mesh& mesh) app->ReceiveField("gids"); //(Alt) tf_gid_field->Receive(); app->EndReceivePhase(); - if (!validateField(gids_ptr, "gids", rank, 10.0)) { + if (!validateField(*factory.GetLayout(), gids_ptr, "gids", rank, 10.0)) { std::cerr << "xgc_total_f: Field validation failed at round " << i << std::endl; exit(EXIT_FAILURE); @@ -169,27 +166,29 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) // coupling server using same mesh as application // note the xgc_coupler stores a reference to the internal mesh and it is the // user responsibility to keep it alive! - pcms::Coupler2 cpl( - "proxy_couple", comm, true, - redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); + pcms::Coupler cpl("proxy_couple", comm, true, + redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); const auto partition = std::get(cpl.GetPartition()); auto* total_f = cpl.AddApplication("proxy_couple_xgc_total_f"); auto* delta_f = cpl.AddApplication("proxy_couple_xgc_delta_f"); - auto& layout_total = total_f->AddLayout( - "gids", - pcms::CreateLagrangeLayout(mesh, 1, 1, pcms::CoordinateSystem::Cartesian)); + auto factory_total = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + total_f->AddLayout("gids", factory_total.GetLayout()); - auto& layout_delta = delta_f->AddLayout( - "gids", - pcms::CreateLagrangeLayout(mesh, 1, 1, pcms::CoordinateSystem::Cartesian)); + auto factory_delta = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + delta_f->AddLayout("gids", factory_delta.GetLayout()); // TODO, fields should have a transfer policy rather than parameters - auto total_gids_field = layout_total.CreateFieldReal(); - auto delta_gids_field = layout_delta.CreateFieldReal(); - auto delta_gids2_field = layout_delta.CreateFieldReal(); + auto total_gids_field = + factory_total.CreateField(pcms::FieldMetadata{}); + auto delta_gids_field = + factory_delta.CreateField(pcms::FieldMetadata{}); + auto delta_gids2_field = + factory_delta.CreateField(pcms::FieldMetadata{}); - auto* total_gids_ptr = total_gids_field.get(); - auto* delta_gids_ptr = delta_gids_field.get(); - auto* delta_gids2_ptr = delta_gids2_field.get(); + auto* total_gids_ptr = &total_gids_field.GetData(); + auto* delta_gids_ptr = &delta_gids_field.GetData(); + auto* delta_gids2_ptr = &delta_gids2_field.GetData(); total_f->AddField("gids", std::move(total_gids_field)); delta_f->AddField("gids", std::move(delta_gids_field)); @@ -201,7 +200,8 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) total_f->ReceiveField("gids"); total_f->EndReceivePhase(); - if (!validateField(total_gids_ptr, "gids", rank, 10.0)) { + if (!validateField(*factory_total.GetLayout(), total_gids_ptr, "gids", + rank, 10.0)) { std::cerr << "xgc_coupler: total_f field validation failed at round " << i << std::endl; exit(EXIT_FAILURE); @@ -211,7 +211,8 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) delta_f->ReceiveField("gids"); delta_f->EndReceivePhase(); - if (!validateField(delta_gids_ptr, "gids", rank, 1.0)) { + if (!validateField(*factory_delta.GetLayout(), delta_gids_ptr, "gids", + rank, 1.0)) { std::cerr << "xgc_coupler: delta_f field validation failed at round " << i << std::endl; exit(EXIT_FAILURE); diff --git a/test/test_proxy_coupling_xgc_server.cpp b/test/test_proxy_coupling_xgc_server.cpp index e108924ea..a46a3873f 100644 --- a/test/test_proxy_coupling_xgc_server.cpp +++ b/test/test_proxy_coupling_xgc_server.cpp @@ -3,18 +3,19 @@ #include #include #include +#include + #include "test_support.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include "pcms/adapter/xgc/xgc_field_adapter.h" +#include "pcms/field/function_space/xgc.h" +#include "pcms/field/layout/xgc.h" +#include "pcms/coupler/serializer/xgc.h" +#include "pcms/coupler/coupler.hpp" +#include "pcms/field/field_metadata.h" +#include "pcms/field/function_space/lagrange.h" using pcms::ConstructRCFromOmegaHMesh; -using pcms::Copy; using pcms::GO; -using pcms::Lagrange; using pcms::make_array_view; -using pcms::MeshFieldsAdapter; -using pcms::OmegaHFieldAdapter; -using pcms::ReadReverseClassificationVertex; using pcms::ReverseClassificationVertex; static constexpr bool done = true; @@ -37,39 +38,104 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) auto is_overlap = ts::markServerOverlapRegion(mesh, partition, ts::IsModelEntInOverlap{}); + (void)is_overlap; auto* application = cpl.AddApplication("proxy_couple"); constexpr int nplanes = 2; std::array, nplanes> data; - std::vector fields; + std::vector> fields; for (int i = 0; i < nplanes; ++i) { data[i].resize(mesh.nverts()); std::stringstream ss; ss << "xgc_gids_plane_" << i; - auto field_adapter = pcms::XGCFieldAdapter( - ss.str(), comm, make_array_view(data[i]), rc, ts::IsModelEntInOverlap{}); - fields.push_back(application->AddField(ss.str(), std::move(field_adapter))); + // FIXME: The current C/Fortran proxy API couples layout registration to + // field registration, so each XGC plane is registered as a separate layout + // communicator even though the layouts are geometrically identical. + auto function_space = pcms::XGCFieldFactory( + rc, ts::IsModelEntInOverlap{}, static_cast(mesh.nverts())); + auto field = function_space.CreateField( + std::make_unique>( + function_space.GetXGCLayout(), pcms::FieldMetadata{}, + make_array_view(data[i]))); + application->AddLayout(ss.str(), function_space.GetLayout()); + std::unique_ptr> serializer = + std::make_unique>(comm); + fields.push_back( + application->AddField(ss.str(), std::move(field), std::move(serializer))); } do { application->ReceivePhase([&]() { std::for_each(fields.begin(), fields.end(), - [](pcms::CoupledField* f) { f->Receive(); }); + [](const pcms::FieldHandle& f) { f.Receive(); }); }); application->SendPhase([&]() { std::for_each(fields.begin(), fields.end(), - [](pcms::CoupledField* f) { f->Send(); }); + [](const pcms::FieldHandle& f) { f.Send(); }); }); application->ReceivePhase([&]() { std::for_each(fields.begin(), fields.end(), - [](pcms::CoupledField* f) { f->Receive(); }); + [](const pcms::FieldHandle& f) { f.Receive(); }); }); application->SendPhase([&]() { std::for_each(fields.begin(), fields.end(), - [](pcms::CoupledField* f) { f->Send(); }); + [](const pcms::FieldHandle& f) { f.Send(); }); }); } while (!done); + // Result verification + int rank; + MPI_Comm_rank(comm, &rank); + if (rank == 0) { + std::cout << "\n=== XGC Overlap Verification (OLD approach) ===" + << std::endl; + std::cout << "Total vertices: " << mesh.nverts() << std::endl; + + int overlap_count = 0; + if (mesh.has_tag(0, "isOverlap")) { + auto is_overlap = mesh.get_array(0, "isOverlap"); + auto is_overlap_h = Omega_h::HostRead(is_overlap); + for (int i = 0; i < mesh.nverts(); ++i) { + if (is_overlap_h[i]) { + overlap_count++; + } + } + std::cout << "Overlap vertices (from mesh tag): " << overlap_count + << std::endl; + } else { + std::cout << "No 'isOverlap' tag found on mesh" << std::endl; + } + + int overlap_count_func = 0; + auto class_dims = mesh.get_array(0, "class_dim"); + auto class_ids = mesh.get_array(0, "class_id"); + auto class_dims_h = Omega_h::HostRead(class_dims); + auto class_ids_h = Omega_h::HostRead(class_ids); + + for (int i = 0; i < mesh.nverts(); ++i) { + if (ts::IsModelEntInOverlap{}(class_dims_h[i], class_ids_h[i])) { + overlap_count_func++; + } + } + std::cout << "Overlap vertices (from function): " << overlap_count_func + << std::endl; + std::cout << "Non-overlap vertices: " + << (mesh.nverts() - overlap_count_func) << std::endl; + std::cout << "Overlap ratio: " + << (100.0 * overlap_count_func / mesh.nverts()) << "%" + << std::endl; + + // Show field data statistics + std::cout << "\nField data (plane 0) first 10 values: "; + for (int i = 0; i < std::min(10, static_cast(data[0].size())); ++i) { + std::cout << data[0][i] << " "; + } + std::cout << std::endl; + std::cout << "Layout communicator count: " + << application->GetLayoutCommunicatorCount() << std::endl; + std::cout << "========================================\n" << std::endl; + } + Omega_h::vtk::write_parallel("proxy_couple", &mesh, mesh.dim()); } void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, @@ -97,30 +163,41 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, auto is_overlap = ts::markServerOverlapRegion(mesh, partition, ts::IsModelEntInOverlap{}); constexpr int nplanes = 2; - std::vector fields; + std::vector> fields; for (int i = 0; i < nplanes; ++i) { std::stringstream ss; ss << "xgc_gids_plane_" << i; - auto field_adapter = - pcms::OmegaHFieldAdapter(ss.str(), mesh, is_overlap, numbering); - fields.push_back(application->AddField(ss.str(), std::move(field_adapter))); + // FIXME: The current C/Fortran proxy API couples layout registration to + // field registration, so each XGC plane is registered as a separate layout + // communicator even though the layouts are geometrically identical. + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, numbering, + pcms::LagrangeFunctionSpace::Backend::OmegaH); + application->AddLayout(ss.str(), factory.GetLayout()); + auto field = + factory.CreateField(std::make_unique>( + factory.GetLayout(), pcms::FieldMetadata{})); + std::unique_ptr> serializer = + std::make_unique>(); + fields.push_back( + application->AddField(ss.str(), std::move(field), std::move(serializer))); } do { application->ReceivePhase([&]() { std::for_each(fields.begin(), fields.end(), - [](pcms::CoupledField* f) { f->Receive(); }); + [](const pcms::FieldHandle& f) { f.Receive(); }); }); application->SendPhase([&]() { std::for_each(fields.begin(), fields.end(), - [](pcms::CoupledField* f) { f->Send(); }); + [](const pcms::FieldHandle& f) { f.Send(); }); }); application->ReceivePhase([&]() { std::for_each(fields.begin(), fields.end(), - [](pcms::CoupledField* f) { f->Receive(); }); + [](const pcms::FieldHandle& f) { f.Receive(); }); }); application->SendPhase([&]() { std::for_each(fields.begin(), fields.end(), - [](pcms::CoupledField* f) { f->Send(); }); + [](const pcms::FieldHandle& f) { f.Send(); }); }); } while (!done); Omega_h::vtk::write_parallel("proxy_couple", &mesh, mesh.dim()); @@ -128,42 +205,51 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, int main(int argc, char** argv) { - auto lib = Omega_h::Library(&argc, &argv); - auto world = lib.world(); - const int rank = world->rank(); - int size = world->size(); - if (argc != 4) { - if (!rank) { - std::cerr << "Usage: " << argv[0] - << " " - "" - ""; + try { + auto lib = Omega_h::Library(&argc, &argv); + auto world = lib.world(); + const int rank = world->rank(); + int size = world->size(); + if (argc != 4) { + if (!rank) { + std::cerr << "Usage: " << argv[0] + << " " + "" + ""; + } + exit(EXIT_FAILURE); } - exit(EXIT_FAILURE); - } - const auto meshFile = argv[1]; - const auto classPartitionFile = argv[2]; - int coupler_type = std::stoi(argv[3]); + const auto meshFile = argv[1]; + const auto classPartitionFile = argv[2]; + int coupler_type = std::stoi(argv[3]); - Omega_h::Mesh mesh(&lib); - Omega_h::binary::read(meshFile, lib.world(), &mesh); - MPI_Comm mpi_comm = lib.world()->get_impl(); - if (coupler_type == 0) { - if (size != 1) { - if (!rank) { - std::cerr << "XGC Adapter only works on 1 rank (not a distributed mesh " - "datastructure)" - << std::endl; + Omega_h::Mesh mesh(&lib); + Omega_h::binary::read(meshFile, lib.world(), &mesh); + MPI_Comm mpi_comm = lib.world()->get_impl(); + if (coupler_type == 0) { + if (size != 1) { + if (!rank) { + std::cerr + << "XGC Adapter only works on 1 rank (not a distributed mesh " + "datastructure)" + << std::endl; + } + std::abort(); } + xgc_coupler(mpi_comm, mesh, classPartitionFile); + } else if (coupler_type == 1) { + omegah_coupler(mpi_comm, mesh, classPartitionFile); + } else { + std::cerr << "Invalid coupler type. Choose 1 for XGC, 2 for Omega-h\n"; std::abort(); } - xgc_coupler(mpi_comm, mesh, classPartitionFile); - } else if (coupler_type == 1) { - omegah_coupler(mpi_comm, mesh, classPartitionFile); - } else { - std::cerr << "Invalid coupler type. Choose 1 for XGC, 2 for Omega-h\n"; - std::abort(); + return 0; + } catch (const std::exception& e) { + std::cerr << "Exception caught in main: " << e.what() << std::endl; + return 1; + } catch (...) { + std::cerr << "Unknown exception caught in main" << std::endl; + return 1; } - return 0; } diff --git a/test/test_proxy_coupling_xgc_server_overlap.cpp b/test/test_proxy_coupling_xgc_server_overlap.cpp new file mode 100644 index 000000000..081763727 --- /dev/null +++ b/test/test_proxy_coupling_xgc_server_overlap.cpp @@ -0,0 +1,266 @@ +#include +#include +#include +#include +#include +#include + +#include "test_support.h" +#include "pcms/field/function_space/xgc.h" +#include "pcms/field/layout/xgc.h" +#include "pcms/coupler/serializer/xgc.h" +#include "pcms/coupler/coupler.hpp" +#include "pcms/coupler/overlap_mask.h" +#include "pcms/field/field_metadata.h" +#include "pcms/field/function_space/lagrange.h" + +using pcms::ConstructRCFromOmegaHMesh; +using pcms::GO; +using pcms::make_array_view; +using pcms::ReverseClassificationVertex; + +static constexpr bool done = true; +namespace ts = test_support; + +void xgc_coupler_with_overlap(MPI_Comm comm, Omega_h::Mesh& mesh, + std::string_view cpn_file) +{ + pcms::Coupler cpl("proxy_couple_server", comm, true, + redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); + const auto partition = std::get(cpl.GetPartition()); + + ReverseClassificationVertex rc; + if (mesh.has_tag(0, "simNumbering")) { + rc = ConstructRCFromOmegaHMesh(mesh, "simNumbering"); + } else { + rc = ConstructRCFromOmegaHMesh(mesh, "global", pcms::IndexBase::Zero); + } + + auto* application = cpl.AddApplication("proxy_couple"); + + constexpr int nplanes = 2; + std::array, nplanes> data; + std::vector> fields; + + for (int i = 0; i < nplanes; ++i) { + data[i].resize(mesh.nverts()); + std::stringstream ss; + ss << "xgc_gids_plane_" << i; + + auto overlap_mask = std::make_unique( + mesh.nverts(), [](int dim, int id) -> int8_t { + return ts::IsModelEntInOverlap{}(dim, id); + }); + + application->SetLayoutOverlapMask(ss.str(), std::move(overlap_mask)); + + auto function_space = pcms::XGCFieldFactory( + rc, ts::IsModelEntInOverlap{}, static_cast(mesh.nverts())); + + application->AddLayout(ss.str(), function_space.GetLayout()); + + auto field = function_space.CreateField( + std::make_unique>( + function_space.GetXGCLayout(), pcms::FieldMetadata{}, + make_array_view(data[i]))); + + std::unique_ptr> serializer = + std::make_unique>(comm); + + fields.push_back( + application->AddField(ss.str(), std::move(field), std::move(serializer))); + } + + do { + application->ReceivePhase([&]() { + std::for_each(fields.begin(), fields.end(), + [](const pcms::FieldHandle& f) { f.Receive(); }); + }); + application->SendPhase([&]() { + std::for_each(fields.begin(), fields.end(), + [](const pcms::FieldHandle& f) { f.Send(); }); + }); + application->ReceivePhase([&]() { + std::for_each(fields.begin(), fields.end(), + [](const pcms::FieldHandle& f) { f.Receive(); }); + }); + application->SendPhase([&]() { + std::for_each(fields.begin(), fields.end(), + [](const pcms::FieldHandle& f) { f.Send(); }); + }); + } while (!done); + + int rank; + MPI_Comm_rank(comm, &rank); + if (rank == 0) { + std::cout << "\n=== Field Communication Verification (NEW approach) ===" + << std::endl; + std::cout << "Total DOFs in field: " << data[0].size() << std::endl; + + int received_count = 0; + int zero_count = 0; + for (size_t i = 0; i < data[0].size(); ++i) { + if (data[0][i] != 0) { + received_count++; + } else { + zero_count++; + } + } + + std::cout << "Field values received (non-zero): " << received_count + << std::endl; + std::cout << "Field values not received (zero): " << zero_count + << std::endl; + + std::cout << "\nSample field data (plane 0, first 20 values):" << std::endl; + for (int i = 0; i < std::min(20, static_cast(data[0].size())); ++i) { + std::cout << " DOF[" << i << "] = " << data[0][i] << std::endl; + } + + int overlap_count = 0; + auto class_dims = mesh.get_array(0, "class_dim"); + auto class_ids = mesh.get_array(0, "class_id"); + auto class_dims_h = Omega_h::HostRead(class_dims); + auto class_ids_h = Omega_h::HostRead(class_ids); + + for (int i = 0; i < mesh.nverts(); ++i) { + if (ts::IsModelEntInOverlap{}(class_dims_h[i], class_ids_h[i])) { + overlap_count++; + } + } + + std::cout << "\nExpected overlap DOFs: " << overlap_count << std::endl; + std::cout << "Layout communicator count: " + << application->GetLayoutCommunicatorCount() << std::endl; + std::cout << "======================================================\n" + << std::endl; + } + + Omega_h::vtk::write_parallel("proxy_couple_overlap", &mesh, mesh.dim()); +} + +void omegah_coupler_with_overlap(MPI_Comm comm, Omega_h::Mesh& mesh, + std::string_view cpn_file) +{ + pcms::Coupler cpl("proxy_couple_server", comm, true, + redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); + const auto partition = std::get(cpl.GetPartition()); + auto* application = cpl.AddApplication("proxy_couple"); + + std::string numbering; + if (mesh.has_tag(0, "simNumbering")) { + numbering = "simNumbering"; + } else { + Omega_h::Write gids(mesh.nverts()); + auto globals = mesh.globals(0); + Omega_h::parallel_for( + mesh.nverts(), OMEGA_H_LAMBDA(int i) { gids[i] = globals[i] + 1; }); + mesh.add_tag(0, "simNumbering", 1, Omega_h::Read(gids)); + numbering = "simNumbering"; + } + + constexpr int nplanes = 2; + std::vector> fields; + + for (int i = 0; i < nplanes; ++i) { + std::stringstream ss; + ss << "xgc_gids_plane_" << i; + + auto overlap_mask = std::make_unique( + mesh.nverts(), [](int dim, int id) -> int8_t { + return ts::IsModelEntInOverlap{}(dim, id); + }); + + application->SetLayoutOverlapMask(ss.str(), std::move(overlap_mask)); + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, numbering, + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + application->AddLayout(ss.str(), factory.GetLayout()); + + auto field = + factory.CreateField(std::make_unique>( + factory.GetLayout(), pcms::FieldMetadata{})); + + std::unique_ptr> serializer = + std::make_unique>(); + + fields.push_back( + application->AddField(ss.str(), std::move(field), std::move(serializer))); + } + + do { + application->ReceivePhase([&]() { + std::for_each(fields.begin(), fields.end(), + [](const pcms::FieldHandle& f) { f.Receive(); }); + }); + application->SendPhase([&]() { + std::for_each(fields.begin(), fields.end(), + [](const pcms::FieldHandle& f) { f.Send(); }); + }); + application->ReceivePhase([&]() { + std::for_each(fields.begin(), fields.end(), + [](const pcms::FieldHandle& f) { f.Receive(); }); + }); + application->SendPhase([&]() { + std::for_each(fields.begin(), fields.end(), + [](const pcms::FieldHandle& f) { f.Send(); }); + }); + } while (!done); + + Omega_h::vtk::write_parallel("proxy_couple_overlap", &mesh, mesh.dim()); +} + +int main(int argc, char** argv) +{ + try { + auto lib = Omega_h::Library(&argc, &argv); + auto world = lib.world(); + const int rank = world->rank(); + int size = world->size(); + + if (argc != 4) { + if (!rank) { + std::cerr << "Usage: " << argv[0] + << " " + " " + "\n"; + } + exit(EXIT_FAILURE); + } + + const auto meshFile = argv[1]; + const auto classPartitionFile = argv[2]; + int coupler_type = std::stoi(argv[3]); + + Omega_h::Mesh mesh(&lib); + Omega_h::binary::read(meshFile, lib.world(), &mesh); + MPI_Comm mpi_comm = lib.world()->get_impl(); + + if (coupler_type == 0) { + if (size != 1) { + if (!rank) { + std::cerr + << "XGC Adapter only works on 1 rank (not a distributed mesh " + "datastructure)\n"; + } + std::abort(); + } + xgc_coupler_with_overlap(mpi_comm, mesh, classPartitionFile); + } else if (coupler_type == 1) { + omegah_coupler_with_overlap(mpi_comm, mesh, classPartitionFile); + } else { + std::cerr << "Invalid coupler type. Choose 0 for XGC, 1 for Omega-h\n"; + std::abort(); + } + + return 0; + } catch (const std::exception& e) { + std::cerr << "Exception caught in main: " << e.what() << std::endl; + return 1; + } catch (...) { + std::cerr << "Unknown exception caught in main" << std::endl; + return 1; + } +} diff --git a/test/test_rbf_interp.cpp b/test/test_rbf_interp.cpp index 24ac9a180..bc7a800d3 100644 --- a/test/test_rbf_interp.cpp +++ b/test/test_rbf_interp.cpp @@ -1,8 +1,8 @@ #include #include -#include -#include -#include +#include +#include +#include #include #include #include @@ -46,8 +46,8 @@ void test(Omega_h::Mesh& mesh, Omega_h::Real cutoffDistance, int degree, }; - SupportResults support = - searchNeighbors(mesh, cutoffDistance, min_num_supports); + pcms::SupportResults support = + pcms::searchNeighbors(mesh, cutoffDistance, min_num_supports); for (const auto& rbf : rbf_types) { auto approx_target_values = @@ -94,22 +94,8 @@ TEST_CASE("test_mls_interpolation") const auto& ntargets = mesh.nverts(); - Omega_h::Write source_coordinates( - dim * nfaces, 0, "stores coordinates of cell centroid of each tri element"); - const auto& faces2nodes = mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; - - Kokkos::parallel_for( - "calculate the centroid in each tri element", nfaces, - OMEGA_H_LAMBDA(const Omega_h::LO id) { - const auto current_el_verts = Omega_h::gather_verts<3>(faces2nodes, id); - const Omega_h::Few, 3> current_el_vert_coords = - Omega_h::gather_vectors<3, 2>(target_coordinates, current_el_verts); - auto centroid = Omega_h::average(current_el_vert_coords); - int index = 2 * id; - source_coordinates[index] = centroid[0]; - source_coordinates[index + 1] = centroid[1]; - }); + const auto source_coordinates = pcms::get_entity_centroids(mesh, 2); pcms::Points source_points; source_points.coordinates = diff --git a/test/test_spline_interpolator.cpp b/test/test_spline_interpolator.cpp index 807dbf7b7..1052582fd 100644 --- a/test/test_spline_interpolator.cpp +++ b/test/test_spline_interpolator.cpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include "pcms/field/evaluator/spline_interpolator.hpp" using namespace pcms; @@ -230,6 +230,31 @@ void pspltest1(Kokkos::View res_1d) dotest1(inum, x, z2sin, 1000, xtest, ftest, res_1d); } +void pspltest1_long_periodic(Kokkos::View res_1d) +{ + const double pi2 = 6.28318530718; + const double zero = 0.0; + int inum = 16; + + Kokkos::View zdum_view("zdum_view_long", 1000); + Kokkos::View xtest_view("xtest_view_long", 1000); + Kokkos::View ftest_view("ftest_view_long", 1000); + Kokkos::View x_view("x_view_long", inum); + Kokkos::View zcos_view("zcos_view_long", inum); + Kokkos::View z2sin_view("z2sin_view_long", inum); + + tset(1000, xtest_view, ftest_view, zdum_view, zero - 0.1, pi2 + 0.1); + tset(inum, x_view, z2sin_view, zcos_view, zero, pi2); + + auto x = Rank1View(x_view.data(), x_view.size()); + auto z2sin = + Rank1View(z2sin_view.data(), z2sin_view.size()); + auto xtest = Rank1View(xtest_view.data(), 1000); + auto ftest = Rank1View(ftest_view.data(), 1000); + + dotest1(inum, x, z2sin, 1000, xtest, ftest, res_1d); +} + void dotest2(Rank1View x, Rank1View fx, int nx, Rank1View th, @@ -380,6 +405,13 @@ TEST_CASE("test_cubic_spline_interpolator") REQUIRE(are_equal(res_1d(i), gt_1d[i])); } + Kokkos::View res_1d_long("res_1d_long", 6); + pspltest1_long_periodic(res_1d_long); + REQUIRE(res_1d_long(2) < 1.0e-3); + REQUIRE(res_1d_long(3) < 1.0e-3); + REQUIRE(res_1d_long(4) < 1.0e-3); + REQUIRE(res_1d_long(5) < 1.0e-3); + Kokkos::View res_2d("res_2d", 4); double gt_2d[4] = {1.8312E-03, 6.7151E-04, 1.8312E-03, 6.7151E-04}; pspltest2(res_2d); diff --git a/test/test_spr_meshfields.cpp b/test/test_spr_meshfields.cpp index 3d58e21fa..42a6acf02 100644 --- a/test/test_spr_meshfields.cpp +++ b/test/test_spr_meshfields.cpp @@ -1,8 +1,8 @@ #include #include -#include -#include -#include +#include +#include +#include #include #include #include diff --git a/test/test_support.h b/test/test_support.h index f2826c955..697dca012 100644 --- a/test/test_support.h +++ b/test/test_support.h @@ -5,11 +5,11 @@ #include // steady_clock, duration #include // std::iota #include +#include #include #include #include #include -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" #include namespace test_support diff --git a/test/test_svd_serial.cpp b/test/test_svd_serial.cpp index c638d0483..bdc23290b 100644 --- a/test/test_svd_serial.cpp +++ b/test/test_svd_serial.cpp @@ -1,8 +1,8 @@ #include #include -#include -#include -#include +#include +#include +#include #include #include "KokkosBatched_SVD_Decl.hpp" #include "KokkosBatched_SVD_Serial_Impl.hpp" @@ -50,71 +50,6 @@ TEST_CASE("test_serial_svd") double expected_solution[column] = {-0.142857, 0.285714, 0.428571}; // Approximate values - SECTION("test_svd_factorization") - { - Kokkos::View result("result", row, column); - team_policy tp(1, Kokkos::AUTO); - Kokkos::parallel_for( - "Solve SVD", tp.set_scratch_size(1, Kokkos::PerTeam(2000)), - KOKKOS_LAMBDA(const member_type& team) { - ScratchMatView A(team.team_scratch(1), row, column); - ScratchMatView U(team.team_scratch(1), row, row); - ScratchMatView Vt(team.team_scratch(1), column, column); - ScratchVecView sigma(team.team_scratch(1), column); - ScratchVecView work(team.team_scratch(1), row); - ScratchMatView reconstructedA(team.team_scratch(1), row, column); - Kokkos::parallel_for(Kokkos::TeamThreadRange(team, row), [=](int i) { - for (int j = 0; j < column; ++j) { - A(i, j) = A_data(i, j); - } - }); - - ScratchMatView sigma_mat(team.team_scratch(1), row, column); - detail::fill(0.0, team, sigma_mat); - ScratchMatView Usigma(team.team_scratch(1), row, column); - detail::fill(0.0, team, Usigma); - - if (team.team_rank() == 0) { - - KokkosBatched::SerialSVD::invoke(KokkosBatched::SVD_USV_Tag(), A, U, - sigma, Vt, work, 1e-6); - - for (int i = 0; i < column; ++i) { - sigma_mat(i, i) = sigma(i); - } - - KokkosBatched::SerialGemm< - KokkosBatched::Trans::NoTranspose, - KokkosBatched::Trans::NoTranspose, - KokkosBatched::Algo::Gemm::Unblocked>::invoke(1.0, U, sigma_mat, - 0.0, Usigma); - - KokkosBatched::SerialGemm< - KokkosBatched::Trans::NoTranspose, - KokkosBatched::Trans::NoTranspose, - KokkosBatched::Algo::Gemm::Unblocked>::invoke(1.0, Usigma, Vt, 0.0, - reconstructedA); - } - team.team_barrier(); - Kokkos::parallel_for(Kokkos::TeamThreadRange(team, row), [=](int i) { - for (int j = 0; j < column; ++j) { - result(i, j) = reconstructedA(i, j); - } - }); - }); - - Kokkos::fence(); - auto host_result = Kokkos::create_mirror_view(result); - Kokkos::deep_copy(host_result, result); - - for (int i = 0; i < row; ++i) { - for (int j = 0; j < column; ++j) { - CHECK_THAT(host_result(i, j), - Catch::Matchers::WithinAbs(host_A_data(i, j), tolerance)); - } - } - } // end section - SECTION("test_transpose_function") { diff --git a/test/test_twoClientOverlap.cpp b/test/test_twoClientOverlap.cpp index 9ec719a37..1532b7279 100644 --- a/test/test_twoClientOverlap.cpp +++ b/test/test_twoClientOverlap.cpp @@ -309,29 +309,37 @@ void server(Omega_h::Mesh& mesh, std::string fieldName, int main(int argc, char** argv) { - auto lib = Omega_h::Library(&argc, &argv); - auto world = lib.world(); - const int rank = world->rank(); - if (argc != 4) { - if (!rank) { - std::cerr << "Usage: " << argv[0] - << " /path/to/omega_h/mesh " - "/path/to/partitionFile.cpn\n"; + try { + auto lib = Omega_h::Library(&argc, &argv); + auto world = lib.world(); + const int rank = world->rank(); + if (argc != 4) { + if (!rank) { + std::cerr << "Usage: " << argv[0] + << " /path/to/omega_h/mesh " + "/path/to/partitionFile.cpn\n"; + } + exit(EXIT_FAILURE); } - exit(EXIT_FAILURE); - } - OMEGA_H_CHECK(argc == 4); - const auto clientId = atoi(argv[1]); - REDEV_ALWAYS_ASSERT(clientId >= -1 && clientId <= 1); - const auto meshFile = argv[2]; - const auto classPartitionFile = argv[3]; - Omega_h::Mesh mesh(&lib); - Omega_h::binary::read(meshFile, lib.world(), &mesh); - const std::string name = "meshVtxIds"; - if (clientId == -1) { // rendezvous - server(mesh, name, classPartitionFile); - } else { - client(mesh, name, clientId); + OMEGA_H_CHECK(argc == 4); + const auto clientId = atoi(argv[1]); + REDEV_ALWAYS_ASSERT(clientId >= -1 && clientId <= 1); + const auto meshFile = argv[2]; + const auto classPartitionFile = argv[3]; + Omega_h::Mesh mesh(&lib); + Omega_h::binary::read(meshFile, lib.world(), &mesh); + const std::string name = "meshVtxIds"; + if (clientId == -1) { // rendezvous + server(mesh, name, classPartitionFile); + } else { + client(mesh, name, clientId); + } + return 0; + } catch (const std::exception& e) { + std::cerr << "Exception caught in main: " << e.what() << std::endl; + return 1; + } catch (...) { + std::cerr << "Unknown exception caught in main" << std::endl; + return 1; } - return 0; } diff --git a/test/test_uniform_grid.cpp b/test/test_uniform_grid.cpp index df145357a..d580c9f68 100644 --- a/test/test_uniform_grid.cpp +++ b/test/test_uniform_grid.cpp @@ -1,8 +1,8 @@ #include #include -#include #include #include +#include using pcms::CreateUniformGridFromMesh; using pcms::Uniform2DGrid; diff --git a/test/test_uniform_grid_field.cpp b/test/test_uniform_grid_field.cpp index d1cb858f2..acbbe1a8a 100644 --- a/test/test_uniform_grid_field.cpp +++ b/test/test_uniform_grid_field.cpp @@ -1,94 +1,171 @@ #include #include #include -#include "pcms/adapter/uniform_grid/uniform_grid_field_layout.h" -#include "pcms/adapter/uniform_grid/uniform_grid_field.h" -#include "pcms/uniform_grid.h" +#include "pcms/field/layout/uniform_grid.h" +#include "pcms/field/evaluator/uniform_grid.h" +#include "pcms/field/uniform_grid_binary_field.h" +#include "pcms/field/data/simple.h" +#include "pcms/field/field_metadata.h" +#include "pcms/utility/uniform_grid.h" #include "Omega_h_library.hpp" #include "Omega_h_build.hpp" -#include "pcms/transfer_field2.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter_layout.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter2.h" -#include "pcms/create_field.h" +#include "pcms/transfer/copy.h" +#include "pcms/transfer/interpolator.h" +#include "pcms/field/function_space/lagrange.h" #include "pcms/utility/arrays.h" +#include "field_test_utils.h" #include -using pcms::CreateUniformGridBinaryField; using pcms::CreateUniformGridFromMesh; -// Helper function to initialize omega_h field data with f(x,y) = x + 2*y -std::vector CreateOmegaHFieldData( - const pcms::CoordinateView& coords, int num_nodes) +TEST_CASE("UniformGridDiscretization SameEntities: identical grids") { - auto coords_data = coords.GetCoordinates(); - std::vector omega_h_data(num_nodes); - for (int i = 0; i < num_nodes; ++i) { - pcms::Real x = coords_data(i, 0); - pcms::Real y = coords_data(i, 1); - omega_h_data[i] = x + 2.0 * y; // f(x,y) = x + 2y - } - return omega_h_data; + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {1.0, 1.0}; + grid.divisions = {4, 4}; + + pcms::UniformGridFieldLayout<2> layout_a(grid, 1, + pcms::CoordinateSystem::Cartesian); + pcms::UniformGridFieldLayout<2> layout_b(grid, 1, + pcms::CoordinateSystem::Cartesian); + + auto disc_a = layout_a.GetDiscretization(); + auto disc_b = layout_b.GetDiscretization(); + + REQUIRE(disc_a != nullptr); + REQUIRE(disc_b != nullptr); + REQUIRE(disc_a->SameEntities(*disc_b)); } -// Helper function to verify ug_field values +TEST_CASE("UniformGridDiscretization SameEntities: different grids") +{ + pcms::UniformGrid<2> grid_a; + grid_a.bot_left = {0.0, 0.0}; + grid_a.edge_length = {1.0, 1.0}; + grid_a.divisions = {4, 4}; + + pcms::UniformGrid<2> grid_b; + grid_b.bot_left = {0.0, 0.0}; + grid_b.edge_length = {1.0, 1.0}; + grid_b.divisions = {8, 8}; + + pcms::UniformGridFieldLayout<2> layout_a(grid_a, 1, + pcms::CoordinateSystem::Cartesian); + pcms::UniformGridFieldLayout<2> layout_b(grid_b, 1, + pcms::CoordinateSystem::Cartesian); + + auto disc_a = layout_a.GetDiscretization(); + auto disc_b = layout_b.GetDiscretization(); + + REQUIRE_FALSE(disc_a->SameEntities(*disc_b)); +} + +// Helper to verify ug_field values against f(x,y) = x + 2*y. void VerifyUniformGridFieldValues( const pcms::UniformGrid<2>& grid, const pcms::CoordinateView& ug_coords, const pcms::Rank1View& ug_field_data) { - fprintf(stderr, "\nVerifying ug_field values:\n"); for (int j = 0; j <= grid.divisions[1]; ++j) { for (int i = 0; i <= grid.divisions[0]; ++i) { int vertex_id = j * (grid.divisions[0] + 1) + i; pcms::Real x = ug_coords.GetCoordinates()(vertex_id, 0); pcms::Real y = ug_coords.GetCoordinates()(vertex_id, 1); pcms::Real expected = x + 2.0 * y; - pcms::Real actual = ug_field_data(vertex_id); - fprintf( - stderr, - "ug_field Vertex (%d, %d) at (%.2f, %.2f): expected %.4f, got %.4f\n", - i, j, x, y, expected, actual); + pcms::Real actual = ug_field_data[vertex_id]; REQUIRE(std::abs(expected - actual) <= 1e-10); } } } -// Helper function to verify mask field values +// Helper to verify binary mask field (all values == 1.0). void VerifyMaskFieldValues(const pcms::UniformGrid<2>& grid, - const pcms::UniformGridField<2>& mask_field) + const pcms::Field& mask_field) { - fprintf(stderr, "\nVerifying mask_field values:\n"); - auto mask_data = mask_field.GetDOFHolderData(); + auto mask_data = mask_field.GetDOFHolderDataHost(); for (int j = 0; j <= grid.divisions[1]; ++j) { for (int i = 0; i <= grid.divisions[0]; ++i) { int vertex_id = j * (grid.divisions[0] + 1) + i; - pcms::Real mask_value = mask_data(vertex_id); - fprintf(stderr, "Vertex (%d, %d): mask = %.0f\n", i, j, mask_value); - REQUIRE(mask_value == 1.0); + REQUIRE(mask_data[vertex_id] == 1.0); } } } TEST_CASE("UniformGrid field creation") { - // Create a simple 2D uniform grid pcms::UniformGrid<2> grid; grid.bot_left = {0.0, 0.0}; grid.edge_length = {10.0, 10.0}; grid.divisions = {5, 5}; - // Create field layout with 1 component (scalar field) - pcms::UniformGridFieldLayout<2> layout(grid, 1, - pcms::CoordinateSystem::Cartesian); + auto layout = std::make_shared>( + grid, 1, pcms::CoordinateSystem::Cartesian); - REQUIRE(layout.GetNumComponents() == 1); - REQUIRE(layout.GetNumOwnedDofHolder() == 36); // (5+1)x(5+1) = 36 vertices - REQUIRE(layout.GetNumGlobalDofHolder() == 36); - REQUIRE_FALSE(layout.IsDistributed()); + REQUIRE(layout->GetNumComponents() == 1); + REQUIRE(layout->GetNumOwnedDofHolder() == 36); // (5+1)x(5+1) = 36 vertices + REQUIRE(layout->GetNumGlobalDofHolder() == 36); + REQUIRE_FALSE(layout->IsDistributed()); - // Create field - auto field = layout.CreateFieldReal(); - REQUIRE(field != nullptr); + auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto field = field_space.CreateField(pcms::FieldMetadata{}); + REQUIRE(field.GetDOFHolderDataHost().size() == + static_cast(layout->OwnedSize())); +} + +TEST_CASE("UniformGrid order-0 field creation and evaluation") +{ + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {10.0, 10.0}; + grid.divisions = {2, 2}; + + auto layout = std::make_shared>( + grid, 1, pcms::CoordinateSystem::Cartesian, 0); + auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::CoordinateSystem::Cartesian, 0); + auto field = field_space.CreateField(pcms::FieldMetadata{}); + pcms::UniformGridEvaluatorFactory<2> eval_factory(layout); + + REQUIRE(layout->GetOrder() == 0); + REQUIRE(layout->GetNumOwnedDofHolder() == 4); + + auto coords_device = layout->GetDOFHolderCoordinates().GetCoordinates(); + auto coords = pcms::test::CopyCoordinatesToHost(coords_device, 4, 2); + + REQUIRE(coords(0, 0) == Catch::Approx(2.5)); + REQUIRE(coords(0, 1) == Catch::Approx(2.5)); + REQUIRE(coords(3, 0) == Catch::Approx(7.5)); + REQUIRE(coords(3, 1) == Catch::Approx(7.5)); + + std::vector data = {1.0, 2.0, 3.0, 4.0}; + field.SetDOFHolderDataHost( + pcms::Rank1View(data.data(), + data.size())); + + std::vector eval_coords = {1.0, 1.0, 9.0, 1.0, + 1.0, 9.0, 9.0, 9.0}; + auto device_coords = pcms::test::CreateDeviceCoordinateView( + eval_coords, pcms::CoordinateSystem::Cartesian); + auto evaluator = eval_factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View results_host("results_host", + 4); + Kokkos::View results_device( + "results_device", 4); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + pcms::Rank2View out( + results_device.data(), 4, 1); + evaluator->Evaluate(field, out); + Kokkos::deep_copy(results_host, results_device); + + REQUIRE(results_host(0) == Catch::Approx(1.0)); + REQUIRE(results_host(1) == Catch::Approx(2.0)); + REQUIRE(results_host(2) == Catch::Approx(3.0)); + REQUIRE(results_host(3) == Catch::Approx(4.0)); } TEST_CASE("UniformGrid field data operations", "[uniform_grid_field]") @@ -96,29 +173,26 @@ TEST_CASE("UniformGrid field data operations", "[uniform_grid_field]") pcms::UniformGrid<2> grid; grid.bot_left = {0.0, 0.0}; grid.edge_length = {10.0, 10.0}; - grid.divisions = {4, 4}; // 4x4 = 16 cells + grid.divisions = {4, 4}; - pcms::UniformGridFieldLayout<2> layout(grid, 1, - pcms::CoordinateSystem::Cartesian); - auto field = layout.CreateFieldReal(); + auto layout = std::make_shared>( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto field = field_space.CreateField(pcms::FieldMetadata{}); - // Initialize field data with vertex indices (5x5 = 25 vertices for 4x4 cells) std::vector data(25); - for (size_t i = 0; i < 25; ++i) { + for (size_t i = 0; i < 25; ++i) data[i] = static_cast(i); - } - auto data_view = pcms::Rank1View( - data.data(), data.size()); - field->SetDOFHolderData(data_view); + field.SetDOFHolderDataHost( + pcms::Rank1View(data.data(), + data.size())); - // Retrieve data - auto retrieved = field->GetDOFHolderData(); + auto retrieved = field.GetDOFHolderDataHost(); REQUIRE(retrieved.size() == 25); - - for (size_t i = 0; i < 25; ++i) { + for (size_t i = 0; i < 25; ++i) REQUIRE(retrieved[i] == static_cast(i)); - } } TEST_CASE("UniformGrid field evaluation - piecewise constant") @@ -126,11 +200,14 @@ TEST_CASE("UniformGrid field evaluation - piecewise constant") pcms::UniformGrid<2> grid; grid.bot_left = {0.0, 0.0}; grid.edge_length = {10.0, 10.0}; - grid.divisions = {2, 2}; // 2x2 = 4 cells + grid.divisions = {2, 2}; - pcms::UniformGridFieldLayout<2> layout(grid, 1, - pcms::CoordinateSystem::Cartesian); - auto field = layout.CreateFieldReal(); + auto layout = std::make_shared>( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto field = field_space.CreateField(pcms::FieldMetadata{}); + pcms::UniformGridEvaluatorFactory<2> eval_factory(layout); // Set vertex values for a 2x2 cell grid (3x3 = 9 vertices) // Vertex layout: @@ -144,43 +221,41 @@ TEST_CASE("UniformGrid field evaluation - piecewise constant") 2.0, 2.5, 3.0, // v3, v4, v5 (middle row, y=5) 3.0, 3.5, 4.0 // v6, v7, v8 (top row, y=10) }; - auto data_view = pcms::Rank1View( - data.data(), data.size()); - field->SetDOFHolderData(data_view); + field.SetDOFHolderDataHost( + pcms::Rank1View(data.data(), + data.size())); - // Evaluate at cell centers std::vector eval_coords = { 2.5, 2.5, // Cell 0 center 7.5, 2.5, // Cell 1 center 2.5, 7.5, // Cell 2 center 7.5, 7.5 // Cell 3 center }; - - auto coords_view = pcms::Rank2View( - eval_coords.data(), 4, 2); - auto coord_view = pcms::CoordinateView( - pcms::CoordinateSystem::Cartesian, coords_view); - - auto hint = field->GetLocalizationHint(coord_view); - - std::vector results(4); - auto results_view = - pcms::Rank1View(results.data(), 4); - auto results_field_view = - pcms::FieldDataView( - results_view, pcms::CoordinateSystem::Cartesian); - - field->Evaluate(hint, results_field_view); + auto device_coords = pcms::test::CreateDeviceCoordinateView( + eval_coords, pcms::CoordinateSystem::Cartesian); + auto evaluator = eval_factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View results_host("results_host", + 4); + Kokkos::View results_device( + "results_device", 4); + using LayoutPolicy = + pcms::detail::default_layout_for_memory_space_t; + pcms::Rank2View out( + results_device.data(), 4, 1); + evaluator->Evaluate(field, out); + Kokkos::deep_copy(results_host, results_device); // Check results - interpolated from vertices // Cell 0 center (2.5, 2.5): avg of v0,v1,v3,v4 = (1.0+1.5+2.0+2.5)/4 = 1.75 // Cell 1 center (7.5, 2.5): avg of v1,v2,v4,v5 = (1.5+2.0+2.5+3.0)/4 = 2.25 // Cell 2 center (2.5, 7.5): avg of v3,v4,v6,v7 = (2.0+2.5+3.0+3.5)/4 = 2.75 // Cell 3 center (7.5, 7.5): avg of v4,v5,v7,v8 = (2.5+3.0+3.5+4.0)/4 = 3.25 - REQUIRE(std::abs(results[0] - 1.75) < 1e-10); - REQUIRE(std::abs(results[1] - 2.25) < 1e-10); - REQUIRE(std::abs(results[2] - 2.75) < 1e-10); - REQUIRE(std::abs(results[3] - 3.25) < 1e-10); + REQUIRE(std::abs(results_host(0) - 1.75) < 1e-10); + REQUIRE(std::abs(results_host(1) - 2.25) < 1e-10); + REQUIRE(std::abs(results_host(2) - 2.75) < 1e-10); + REQUIRE(std::abs(results_host(3) - 3.25) < 1e-10); } TEST_CASE("UniformGrid field serialization") @@ -188,55 +263,23 @@ TEST_CASE("UniformGrid field serialization") pcms::UniformGrid<2> grid; grid.bot_left = {0.0, 0.0}; grid.edge_length = {10.0, 10.0}; - grid.divisions = {3, 3}; // 9 cells + grid.divisions = {3, 3}; - pcms::UniformGridFieldLayout<2> layout(grid, 1, - pcms::CoordinateSystem::Cartesian); - auto field = layout.CreateFieldReal(); + auto layout = std::make_shared>( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto field = field_space.CreateField(pcms::FieldMetadata{}); - // Set field data (4x4 = 16 vertices for 3x3 cells) std::vector data(16); - for (size_t i = 0; i < 16; ++i) { + for (size_t i = 0; i < 16; ++i) data[i] = static_cast(i * 10); - } - auto data_view = pcms::Rank1View( - data.data(), data.size()); - field->SetDOFHolderData(data_view); + field.SetDOFHolderDataHost( + pcms::Rank1View(data.data(), + data.size())); - // Create identity permutation - std::vector permutation(16); - for (size_t i = 0; i < 16; ++i) { - permutation[i] = i; - } - - auto perm_view = pcms::Rank1View( - permutation.data(), 16); - - // Serialize - std::vector buffer(16); - auto buffer_view = - pcms::Rank1View(buffer.data(), 16); - int size = field->Serialize(buffer_view, perm_view); - - REQUIRE(size == 16); - - // Verify serialized data - for (size_t i = 0; i < 16; ++i) { - REQUIRE(buffer[i] == data[i]); - } - - // Create new field and deserialize - auto field2 = layout.CreateFieldReal(); - auto buffer_const_view = - pcms::Rank1View(buffer.data(), 16); - field2->Deserialize(buffer_const_view, perm_view); - - // Verify deserialized data - auto retrieved = field2->GetDOFHolderData(); - for (size_t i = 0; i < 16; ++i) { - REQUIRE(retrieved[i] == data[i]); - } + pcms::test::CheckSerializeDeserialize(field); } TEST_CASE("UniformGrid field copy") @@ -244,11 +287,10 @@ TEST_CASE("UniformGrid field copy") pcms::UniformGrid<2> grid; grid.bot_left = {0.0, 0.0}; grid.edge_length = {10.0, 10.0}; - grid.divisions = {2, 2}; // 2x2 grid + grid.divisions = {2, 2}; - pcms::UniformGridFieldLayout<2> layout(grid, 1, - pcms::CoordinateSystem::Cartesian); - auto field = layout.CreateFieldReal(); + auto layout = std::make_shared>( + grid, 1, pcms::CoordinateSystem::Cartesian); // Set vertex values with f(x,y) = x + y at 3x3 vertex positions // Vertices at: (0,0), (5,0), (10,0), (0,5), (5,5), (10,5), (0,10), (5,10), @@ -258,114 +300,62 @@ TEST_CASE("UniformGrid field copy") 5.0, 10.0, 15.0, // y=5: v3(0,5)=5, v4(5,5)=10, v5(10,5)=15 10.0, 15.0, 20.0 // y=10: v6(0,10)=10, v7(5,10)=15, v8(10,10)=20 }; - auto data_view = pcms::Rank1View( - data.data(), data.size()); - field->SetDOFHolderData(data_view); - // Test 1: Evaluate at cell centers - std::vector eval_coords = { - 2.5, 2.5, // Cell 0 center - 7.5, 2.5, // Cell 1 center - 2.5, 7.5, // Cell 2 center - 7.5, 7.5 // Cell 3 center - }; - - auto coords_view = pcms::Rank2View( - eval_coords.data(), 4, 2); - auto coord_view = pcms::CoordinateView( - pcms::CoordinateSystem::Cartesian, coords_view); - - auto hint = field->GetLocalizationHint(coord_view); - - std::vector results(coord_view.GetCoordinates().size() / 2); - auto results_view = - pcms::Rank1View(results.data(), 4); - auto results_field_view = - pcms::FieldDataView( - results_view, pcms::CoordinateSystem::Cartesian); + auto factory = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto field = factory.CreateField(pcms::FieldMetadata{}); + field.SetDOFHolderDataHost( + pcms::Rank1View(data.data(), + data.size())); - field->Evaluate(hint, results_field_view); + auto field2 = factory.CreateField(pcms::FieldMetadata{}); + pcms::Copy copy(factory, factory); + copy.Apply(field, field2); - REQUIRE(std::abs(results[0] - 5.0) < 1e-10); - REQUIRE(std::abs(results[1] - 10.0) < 1e-10); - REQUIRE(std::abs(results[2] - 10.0) < 1e-10); - REQUIRE(std::abs(results[3] - 15.0) < 1e-10); - - // Test 2: test copy_field2 - auto field2 = layout.CreateFieldReal(); - pcms::copy_field2(*field, *field2); - - auto copied_data = field2->GetDOFHolderData(); + auto copied_data = field2.GetDOFHolderDataHost(); REQUIRE(copied_data.size() == data.size()); - for (size_t i = 0; i < data.size(); ++i) { + for (size_t i = 0; i < data.size(); ++i) REQUIRE(copied_data[i] == data[i]); - } } TEST_CASE("Transfer from OmegaH field to UniformGrid field") { Omega_h::Library lib; - - // Create a simple omega_h mesh (2x2 box) auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 2, 2, 0, false); - // Create OmegaH field layout with linear elements - auto omega_h_layout = - pcms::CreateLagrangeLayout(mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto omega_h_field = omega_h_layout->CreateFieldReal(); - - // Initialize omega_h field with a simple function f(x,y) = x + 2*y - auto coords = omega_h_layout->GetDOFHolderCoordinates(); - int num_nodes = omega_h_layout->GetNumOwnedDofHolder(); - std::vector omega_h_data = - CreateOmegaHFieldData(coords, num_nodes); + auto omega_h_factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto omega_h_field = + omega_h_factory.CreateField(pcms::FieldMetadata{}); + pcms::test::SetField( + omega_h_field, + OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); - auto omega_h_data_view = - pcms::Rank1View( - omega_h_data.data(), omega_h_data.size()); - omega_h_field->SetDOFHolderData(omega_h_data_view); - - // Create a uniform grid field covering the same domain [0,1] x [0,1] pcms::UniformGrid<2> grid; - grid.bot_left = {0.0, 0.0}; grid.edge_length = {1.0, 1.0}; - grid.divisions = {2, 2}; // 4x4 grid for finer resolution - - pcms::UniformGridFieldLayout<2> ug_layout(grid, 1, - pcms::CoordinateSystem::Cartesian); - auto ug_field = ug_layout.CreateFieldReal(); - - // Transfer from omega_h field to uniform grid field using interpolation - auto coords_interpolation = ug_layout.GetDOFHolderCoordinates(); - std::vector evaluation( - coords_interpolation.GetCoordinates().size() / 2); - auto evaluation_view = pcms::Rank1View( - evaluation.data(), evaluation.size()); - pcms::FieldDataView data_view{ - evaluation_view, omega_h_field->GetCoordinateSystem()}; - auto locale = omega_h_field->GetLocalizationHint(coords_interpolation); - omega_h_field->Evaluate(locale, data_view); - auto evaluation_view_const = - pcms::Rank1View(evaluation.data(), - evaluation.size()); - ug_field->SetDOFHolderData(evaluation_view_const); - - // Verify the transferred data at uniform grid vertices - auto transferred_data = ug_field->GetDOFHolderData(); - auto ug_coords = ug_layout.GetDOFHolderCoordinates(); - auto ug_coords_data = ug_coords.GetCoordinates(); - int num_ug_nodes = ug_layout.GetNumOwnedDofHolder(); // 5x5 = 25 vertices - - // Check a few sample points + grid.bot_left = {0.0, 0.0}; + grid.divisions = {2, 2}; + auto ug_factory = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto ug_field = ug_factory.CreateField(pcms::FieldMetadata{}); + + pcms::Interpolator interp(omega_h_factory, ug_factory); + interp.Apply(omega_h_field, ug_field); + + auto transferred_data = ug_field.GetDOFHolderDataHost(); + auto ug_coords = ug_factory.GetLayout()->GetDOFHolderCoordinates(); + int num_ug_nodes = ug_factory.GetLayout()->GetNumOwnedDofHolder(); + + // set up_coords to host + auto ug_coords_host = pcms::test::CopyCoordinatesToHost( + ug_coords.GetCoordinates(), num_ug_nodes, 2); + for (int i = 0; i < num_ug_nodes; ++i) { - pcms::Real x = ug_coords_data(i, 0); - pcms::Real y = ug_coords_data(i, 1); + pcms::Real x = ug_coords_host(i, 0); + pcms::Real y = ug_coords_host(i, 1); pcms::Real expected = x + 2.0 * y; - pcms::Real actual = transferred_data[i]; - - // Allow some tolerance for interpolation - REQUIRE(std::abs(actual - expected) < 1e-6); + REQUIRE(std::abs(transferred_data[i] - expected) < 1e-6); } } @@ -376,20 +366,19 @@ TEST_CASE("Create binary field from uniform grid") SECTION("Simple 2D box mesh - all vertices inside") { - // Create a mesh that fills the domain [0,1] x [0,1] auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 10, 10, 0, false); - // Create a 5x5 grid (coarser than mesh) - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, {5, 5}); - auto field_data = field->GetDOFHolderData(); + auto [layout, field] = + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{5, 5}); + auto field_data = field.GetDOFHolderDataHost(); - REQUIRE(field_data.extent(0) == 36); // (5+1) * (5+1) = 36 vertices + REQUIRE(field_data.size() == 36); // (5+1) * (5+1) = 36 vertices - // All grid vertices should be inside the mesh pcms::Real sum = 0.0; - for (size_t i = 0; i < field_data.extent(0); ++i) { - sum += field_data(i); + for (size_t i = 0; i < field_data.size(); ++i) { + REQUIRE((field_data[i] == 0.0 || field_data[i] == 1.0)); + sum += field_data[i]; } REQUIRE(sum == 36.0); } @@ -399,117 +388,109 @@ TEST_CASE("Create binary field from uniform grid") auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 8, 8, 0, false); - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, {10, 8}); - auto field_data = field->GetDOFHolderData(); + auto [layout, field] = + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{10, 8}); + auto field_data = field.GetDOFHolderDataHost(); - REQUIRE(field_data.extent(0) == 99); // (10+1) * (8+1) = 99 vertices + REQUIRE(field_data.size() == 99); // (10+1) * (8+1) = 99 vertices - // Most vertices should be inside pcms::Real inside_count = 0.0; - for (size_t i = 0; i < field_data.extent(0); ++i) { - inside_count += field_data(i); - } + for (size_t i = 0; i < field_data.size(); ++i) + inside_count += field_data[i]; REQUIRE(inside_count > 0.0); REQUIRE(inside_count <= 99.0); } - SECTION("Binary field with equal divisions convenience function") + SECTION("Verify field values are binary") { auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 5, 5, 0, false); - - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, 8); - auto field_data = field->GetDOFHolderData(); + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 8, 8, 0, false); - REQUIRE(field_data.extent(0) == 81); // (8+1) * (8+1) = 81 vertices + auto [layout, field] = + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{10, 10}); + auto field_data = field.GetDOFHolderDataHost(); - pcms::Real inside_count = 0.0; - for (size_t i = 0; i < field_data.extent(0); ++i) { - inside_count += field_data(i); + for (size_t i = 0; i < field_data.size(); ++i) { + REQUIRE((field_data[i] == 0.0 || field_data[i] == 1.0)); } - REQUIRE(inside_count > 0.0); } - SECTION("Fine grid over coarse mesh") + SECTION("Grid larger than mesh - vertices outside should be marked 0") { - // Create a simple mesh auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 2.0, 2.0, 0.0, 4, 4, 0, false); + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 0.5, 0.5, 0.0, 5, 5, 0, false); - // Create a fine grid (20x20) - auto grid = CreateUniformGridFromMesh<2>(mesh, {20, 20}); - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, {20, 20}); - auto field_data = field->GetDOFHolderData(); + pcms::UniformGrid<2> grid; + grid.edge_length = {1.0, 1.0}; + grid.bot_left = {0.0, 0.0}; + grid.divisions = {10, 10}; - REQUIRE(field_data.extent(0) == 441); // (20+1) * (20+1) = 441 vertices + auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, grid); + auto field_data = field.GetDOFHolderDataHost(); - // Verify consistency: check some specific vertices - // Center vertex should be inside (vertex at i=10, j=10) - int center_idx = 10 * 21 + 10; // 21 vertices per row - REQUIRE(field_data(center_idx) == 1.0); + REQUIRE(field_data.size() == 121); // (10+1) * (10+1) = 121 vertices - // Corner vertex should be inside - int corner_idx = 0; // vertex (0, 0) - REQUIRE(field_data(corner_idx) == 1.0); - } + // Count vertices inside and outside + pcms::Real inside_count = 0.0; + for (size_t i = 0; i < field_data.size(); ++i) + inside_count += field_data[i]; + pcms::Real outside_count = field_data.size() - inside_count; - SECTION("Verify field values are binary") - { - auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 5, 5, 0, false); + // Should have both inside (1) and outside (0) vertices + REQUIRE(inside_count > 0.0); + REQUIRE(outside_count > 0.0); - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, 10); - auto field_data = field->GetDOFHolderData(); + int corner_id = 0 * 11 + 0; + REQUIRE(field_data[corner_id] == 1.0); - // All values should be 0 or 1 - for (size_t i = 0; i < field_data.extent(0); ++i) { - pcms::Real val = field_data(i); - REQUIRE((val == 0.0 || val == 1.0)); - } + corner_id = 10 * 11 + 10; + REQUIRE(field_data[corner_id] == 0.0); + + corner_id = 6 * 11 + 6; + REQUIRE(field_data[corner_id] == 0.0); + + auto center_id = 2 * 11 + 2; + REQUIRE(field_data[center_id] == 1.0); } - SECTION("Grid extends beyond mesh - vertices outside should be marked 0") + SECTION("Fine grid over coarse mesh") { - // Create a small mesh in the center of a domain auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 0.5, 0.5, 0.0, 5, 5, 0, false); + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 2.0, 2.0, 0.0, 4, 4, 0, false); - // The mesh occupies [0, 0.5] x [0, 0.5] - // Create a grid that would cover this - auto grid = CreateUniformGridFromMesh<2>(mesh, {10, 10}); - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, {10, 10}); - auto field_data = field->GetDOFHolderData(); + auto [layout, field] = + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{20, 20}); + auto field_data = field.GetDOFHolderDataHost(); - REQUIRE(field_data.extent(0) == 121); // (10+1) * (10+1) = 121 vertices + REQUIRE(field_data.size() == 441); // (20+1) * (20+1) = 441 vertices - // All vertices should be inside since grid is exactly on mesh bbox - pcms::Real inside_count = 0.0; - for (size_t i = 0; i < field_data.extent(0); ++i) { - inside_count += field_data(i); - } - REQUIRE(inside_count > 0.0); + // Verify consistency: check some specific vertices + // Center vertex should be inside (vertex at i=10, j=10) + int center_idx = 10 * 21 + 10; // 21 vertices per row + REQUIRE(field_data[center_idx] == 1.0); + + // Corner vertex should be inside + int corner_idx = 0; // vertex (0 , 0) + REQUIRE(field_data[corner_idx] == 1.0); } SECTION("Test with different aspect ratio") { - // Create a rectangular mesh auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 3.0, 1.0, 0.0, 12, 4, 0, false); - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, {30, 10}); - auto field_data = field->GetDOFHolderData(); + auto [layout, field] = + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{30, 10}); + auto field_data = field.GetDOFHolderDataHost(); - REQUIRE(field_data.extent(0) == 341); // (30+1) * (10+1) = 341 vertices + REQUIRE(field_data.size() == 341); // (30+1) * (10+1) = 341 vertices pcms::Real inside_count = 0.0; - for (size_t i = 0; i < field_data.extent(0); ++i) { - inside_count += field_data(i); - } + for (size_t i = 0; i < field_data.size(); ++i) + inside_count += field_data[i]; REQUIRE(inside_count > 0.0); - // Calculate percentage inside - double inside_percent = 100.0 * inside_count / field_data.extent(0); - // Most vertices should be inside + double inside_percent = 100.0 * inside_count / field_data.size(); REQUIRE(inside_percent > 50.0); } } @@ -524,33 +505,32 @@ TEST_CASE("Binary field integration with grid methods") SECTION("Query field value at specific grid vertex") { - auto grid = CreateUniformGridFromMesh<2>(mesh, {8, 8}); - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, {8, 8}); - auto field_data = field->GetDOFHolderData(); + auto grid = CreateUniformGridFromMesh<2>(mesh, std::array{8, 8}); + auto [layout, field] = + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{8, 8}); + auto field_data = field.GetDOFHolderDataHost(); // Get field value for a specific vertex (middle vertex at i=4, j=4) - pcms::LO vertex_id = 4 * 9 + 4; // 9 = (8+1) vertices per row - REQUIRE(field_data(vertex_id) == 1.0); // Should be inside + pcms::LO vertex_id = 4 * 9 + 4; + REQUIRE(field_data[vertex_id] == 1.0); - // Compute vertex position pcms::Real dx = grid.edge_length[0] / grid.divisions[0]; pcms::Real dy = grid.edge_length[1] / grid.divisions[1]; pcms::Real x = grid.bot_left[0] + 4 * dx; pcms::Real y = grid.bot_left[1] + 4 * dy; - // Verify it's roughly in the middle REQUIRE(x == Catch::Approx(0.5).margin(0.1)); REQUIRE(y == Catch::Approx(0.5).margin(0.1)); } SECTION("Count vertices by region") { - auto grid = CreateUniformGridFromMesh<2>(mesh, 10); - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, 10); - auto field_data = field->GetDOFHolderData(); + auto grid = CreateUniformGridFromMesh<2>(mesh, std::array{10, 10}); + auto [layout, field] = + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{10, 10}); + auto field_data = field.GetDOFHolderDataHost(); - // Count vertices in different quadrants - int q1 = 0, q2 = 0, q3 = 0, q4 = 0; // quadrants + int q1 = 0, q2 = 0, q3 = 0, q4 = 0; pcms::Real dx = grid.edge_length[0] / grid.divisions[0]; pcms::Real dy = grid.edge_length[1] / grid.divisions[1]; @@ -558,7 +538,7 @@ TEST_CASE("Binary field integration with grid methods") for (int j = 0; j <= grid.divisions[1]; ++j) { for (int i = 0; i <= grid.divisions[0]; ++i) { pcms::LO vertex_id = j * (grid.divisions[0] + 1) + i; - if (field_data(vertex_id) == 1.0) { + if (field_data[vertex_id] == 1.0) { pcms::Real x = grid.bot_left[0] + i * dx; pcms::Real y = grid.bot_left[1] + j * dy; @@ -574,7 +554,6 @@ TEST_CASE("Binary field integration with grid methods") } } - // All quadrants should have some inside vertices REQUIRE(q1 > 0); REQUIRE(q2 > 0); REQUIRE(q3 > 0); @@ -602,16 +581,15 @@ TEST_CASE("Performance and edge cases") auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 5, 5, 0, false); - // Create a very fine grid - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, 50); - auto field_data = field->GetDOFHolderData(); + auto [layout, field] = + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{50, 50}); + auto field_data = field.GetDOFHolderDataHost(); - REQUIRE(field_data.extent(0) == 2601); // (50+1) * (50+1) = 2601 vertices + REQUIRE(field_data.size() == 2601); // (50+1) * (50+1) = 2601 vertices pcms::Real inside_count = 0.0; - for (size_t i = 0; i < field_data.extent(0); ++i) { - inside_count += field_data(i); - } + for (size_t i = 0; i < field_data.size(); ++i) + inside_count += field_data[i]; REQUIRE(inside_count > 0.0); } @@ -620,17 +598,15 @@ TEST_CASE("Performance and edge cases") auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 10, 10, 0, false); - // Very coarse grid - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, {2, 2}); - auto field_data = field->GetDOFHolderData(); + auto [layout, field] = + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{2, 2}); + auto field_data = field.GetDOFHolderDataHost(); - REQUIRE(field_data.extent(0) == 9); // (2+1) * (2+1) = 9 vertices + REQUIRE(field_data.size() == 9); // (2+1) * (2+1) = 9 vertices - // All vertices should be inside for this configuration pcms::Real inside_count = 0.0; - for (size_t i = 0; i < field_data.extent(0); ++i) { - inside_count += field_data(i); - } + for (size_t i = 0; i < field_data.size(); ++i) + inside_count += field_data[i]; REQUIRE(inside_count > 0.0); } @@ -639,64 +615,16 @@ TEST_CASE("Performance and edge cases") auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 5.0, 2.0, 0.0, 20, 8, 0, false); - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, {25, 10}); - auto field_data = field->GetDOFHolderData(); - - REQUIRE(field_data.extent(0) == 286); // (25+1) * (10+1) = 286 vertices - - pcms::Real inside_count = 0.0; - for (size_t i = 0; i < field_data.extent(0); ++i) { - inside_count += field_data(i); - } - REQUIRE(inside_count > 0.0); - } - - SECTION("Grid larger than mesh - vertices outside marked as 0") - { - // Create a mesh covering [0, 0.5] x [0, 0.5] - auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 0.5, 0.5, 0.0, 5, 5, 0, false); - - // Manually create a larger grid covering [0, 1] x [0, 1] - pcms::UniformGrid<2> grid; - grid.edge_length = {1.0, 1.0}; - grid.bot_left = {0.0, 0.0}; - grid.divisions = {10, 10}; - - // Create binary field on the larger grid auto [layout, field] = - pcms::CreateUniformGridBinaryFieldFromGrid<2>(mesh, grid); - auto field_data = field->GetDOFHolderData(); + pcms::CreateUniformGridBinaryField<2>(mesh, std::array{25, 10}); + auto field_data = field.GetDOFHolderDataHost(); - REQUIRE(field_data.extent(0) == 121); // (10+1) * (10+1) = 121 vertices + REQUIRE(field_data.size() == 286); // (25+1) * (10+1) = 286 vertices - // Count vertices inside and outside pcms::Real inside_count = 0.0; - for (size_t i = 0; i < field_data.extent(0); ++i) { - inside_count += field_data(i); - } - pcms::Real outside_count = field_data.extent(0) - inside_count; - - // Should have both inside (1) and outside (0) vertices + for (size_t i = 0; i < field_data.size(); ++i) + inside_count += field_data[i]; REQUIRE(inside_count > 0.0); - REQUIRE(outside_count > 0.0); - - // Check specific vertices - // Bottom-left corner should be inside (mesh covers [0, 0.5]) - int corner_id = 0 * 11 + 0; // vertex (0, 0) - REQUIRE(field_data(corner_id) == 1.0); - - // Top-right corner should be outside (mesh ends at 0.5) - corner_id = 10 * 11 + 10; // vertex (10, 10) - REQUIRE(field_data(corner_id) == 0.0); - - // Vertex at i=6, j=6 (coords ~0.6, ~0.6) should be outside - corner_id = 6 * 11 + 6; - REQUIRE(field_data(corner_id) == 0.0); - - // Vertex at i=2, j=2 (coords ~0.2, ~0.2) should be inside - auto center_id = 2 * 11 + 2; - REQUIRE(field_data(center_id) == 1.0); } } @@ -705,42 +633,50 @@ TEST_CASE("UniformGrid workflow") auto lib = Omega_h::Library{}; auto world = lib.world(); - // Create a simple 2D box mesh: 1.0 x 1.0 domain with 4x4 elements auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1.0, 1.0, 0.0, 4, 4, 0, false); auto grid = pcms::CreateUniformGridFromMesh<2>(mesh, {4, 4}); + + auto omega_h_factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + auto omega_h_field = + omega_h_factory.CreateField(pcms::FieldMetadata{}); + pcms::test::SetField( + omega_h_field, + OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); + + auto ug_factory = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::CoordinateSystem::Cartesian); + auto ug_field = ug_factory.CreateField(pcms::FieldMetadata{}); + auto [mask_layout, mask_field] = - pcms::CreateUniformGridBinaryField<2>(mesh, {4, 4}); - - // Create OmegaH field layout with linear elements - auto omega_h_layout = - pcms::CreateLagrangeLayout(mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto omega_h_field = omega_h_layout->CreateFieldReal(); - - // Initialize omega_h field with a simple function f(x,y) = x + 2*y - auto coords = omega_h_layout->GetDOFHolderCoordinates(); - int num_nodes = omega_h_layout->GetNumOwnedDofHolder(); - std::vector omega_h_data = - CreateOmegaHFieldData(coords, num_nodes); - - auto omega_h_data_view = - pcms::Rank1View( - omega_h_data.data(), omega_h_data.size()); - omega_h_field->SetDOFHolderData(omega_h_data_view); - - // Create uniform grid field layout - pcms::UniformGridFieldLayout<2> ug_layout(grid, 1, - pcms::CoordinateSystem::Cartesian); - auto ug_field = ug_layout.CreateFieldReal(); - - // Transfer from omega_h field to uniform grid field using interpolation - pcms::interpolate_field2(*omega_h_field, *ug_field); - auto ug_coords = ug_layout.GetDOFHolderCoordinates(); - - // Verify ug_field values directly from the field object - auto ug_field_data = ug_field->GetDOFHolderData(); - VerifyUniformGridFieldValues(grid, ug_coords, ug_field_data); - - // Verify mask field values - VerifyMaskFieldValues(grid, *mask_field); + pcms::CreateUniformGridBinaryField<2>(mesh, grid); + + pcms::Interpolator interp(omega_h_factory, ug_factory); + interp.Apply(omega_h_field, ug_field); + auto ug_coords_device_view = + ug_factory.GetLayout()->GetDOFHolderCoordinates().GetCoordinates(); + auto ug_coords_host_view = + pcms::test::CopyCoordinatesToHost(ug_coords_device_view, 25, 2); + + auto ug_field_data_device = ug_field.GetDOFHolderData(); + Kokkos::View ug_field_data_device_view( + "", 25); + Kokkos::parallel_for( + "CopyFieldDataToView", 25, KOKKOS_LAMBDA(int i) { + ug_field_data_device_view(i) = ug_field_data_device(i); + }); + auto ug_field_data_host_view = + Kokkos::View("", 25); + Kokkos::deep_copy(ug_field_data_host_view, ug_field_data_device_view); + + pcms::Rank2View ug_coords( + ug_coords_host_view.data(), 25, 2); + pcms::CoordinateView ug_coords_view( + pcms::CoordinateSystem::Cartesian, ug_coords); + pcms::Rank1View ug_field_data( + ug_field_data_host_view.data(), 25); + VerifyUniformGridFieldValues(grid, ug_coords_view, ug_field_data); + + VerifyMaskFieldValues(grid, mask_field); } diff --git a/test/test_xgc_field_adapter.cpp b/test/test_xgc_field_adapter.cpp deleted file mode 100644 index 7a2d3e90e..000000000 --- a/test/test_xgc_field_adapter.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include -#include -#include "pcms/adapter/xgc/xgc_field_adapter.h" -#include - -using pcms::DimID; -using pcms::make_array_view; -using pcms::make_const_array_view; -using pcms::ReverseClassificationVertex; -using pcms::XGCFieldAdapter; - -ReverseClassificationVertex create_dummy_rc(int size) -{ - ReverseClassificationVertex rc; - for (int i = 0; i < size; ++i) { - if (i % 4 == 0) { - rc.Insert({0, 0}, i); - } else { - rc.Insert({0, 1}, i); - } - } - return rc; -} -template -bool is_close(T val1, T2 val2) -{ - static_assert(std::is_integral_v, "T2 should be counter/integral"); - if constexpr (std::is_integral_v) { - return val1 == val2; - } - return fabs(val1 - val2) < 1E-16; -} -template -int check_data(const std::vector& data, - const ReverseClassificationVertex& rc, const Func& in_overlap, - int offset = 0) -{ - - for (const auto& [geom, verts] : rc) { - auto loffset = in_overlap(geom.dim, geom.id) ? offset : 0; - for (auto v : verts) { - if (!is_close(data[v], v + loffset)) - return 1; - } - } - return 0; -} -template -int check_gids(const std::vector& gids, int size, - const ReverseClassificationVertex& rc, const Func& in_overlap, - int offset = 0) -{ - size_t cnt = 0; - for (const auto& [geom, verts] : rc) { - if (in_overlap(geom.dim, geom.id)) { - if (cnt > verts.size()) { - return 2; - } - std::vector sorted_verts{verts.begin(), verts.end()}; - std::sort(sorted_verts.begin(), sorted_verts.end()); - for (auto v : sorted_verts) { - if (!is_close(gids[cnt++], v + offset)) { - return 1; - } - } - } - } - return 0; -} - -bool in_overlap(int, int id) -{ - return (id == 0); -} - -TEMPLATE_TEST_CASE("XGC Field Adapter", "[adapter]", pcms::LO, pcms::Real) -{ - static constexpr auto data_size = 100; - // serialize, deserialize, getgids, reverse_partition_map - std::vector dummy_data(data_size); - std::iota(dummy_data.begin(), dummy_data.end(), 0); - - const auto reverse_classification = create_dummy_rc(data_size); - const auto num_in_overlap = std::accumulate( - reverse_classification.begin(), reverse_classification.end(), 0UL, - [](auto cur, const auto& it) { - return in_overlap(it.first.dim, it.first.id) ? cur + it.second.size() - : cur; - }); - std::cout << "Num in overlap: " << num_in_overlap << "\n"; - - std::cerr << "creating addapter\n"; - XGCFieldAdapter field_adapter("fa", MPI_COMM_SELF, - make_array_view(dummy_data), - reverse_classification, in_overlap); - std::cerr << "getting gids\n"; - auto gids = field_adapter.GetGids(); - REQUIRE(gids.size() == static_cast(num_in_overlap)); - std::cerr << "checking gids\n"; - REQUIRE(check_gids(gids, data_size, reverse_classification, in_overlap, 1) == - 0); - std::vector buffer; - std::vector permutation; - std::cerr << "serializing gids\n"; - auto serialize_size = field_adapter.Serialize( - make_array_view(buffer), make_const_array_view(permutation)); - REQUIRE(serialize_size == num_in_overlap); - buffer.resize(serialize_size); - field_adapter.Serialize(make_array_view(buffer), - make_const_array_view(permutation)); - std::cerr << "checking gids\n"; - // TODO test with not empty permutation array - REQUIRE(check_gids(buffer, data_size, reverse_classification, in_overlap) == - 0); - - std::cerr << "deserialize gids\n"; - // verify that deserializing data writes same values back into dummy data - field_adapter.Deserialize(make_const_array_view(buffer), - make_const_array_view(permutation)); - std::cerr << "check gids\n"; - REQUIRE(check_data(dummy_data, reverse_classification, in_overlap) == 0); - // modify the buffer and verify that the correct data is written into the - // dummy_data - std::cerr << "set data\n"; - for (auto& val : buffer) { - val += 5; - } - std::cerr << "deserialize data\n"; - field_adapter.Deserialize(make_const_array_view(buffer), - make_const_array_view(permutation)); - std::cerr << "check data\n"; - REQUIRE(check_data(dummy_data, reverse_classification, in_overlap, 5) == 0); -} diff --git a/test/test_xgc_field_data.cpp b/test/test_xgc_field_data.cpp new file mode 100644 index 000000000..9d448e497 --- /dev/null +++ b/test/test_xgc_field_data.cpp @@ -0,0 +1,122 @@ +#include +#include +#include "pcms/field/data/xgc.h" +#include "pcms/field/function_space/xgc.h" +#include "pcms/coupler/serializer/xgc.h" +#include +#include + +namespace +{ + +pcms::ReverseClassificationVertex create_dummy_rc(int size) +{ + pcms::ReverseClassificationVertex rc; + for (int i = 0; i < size; ++i) { + if (i % 4 == 0) { + rc.Insert({0, 0}, i); + } else { + rc.Insert({0, 1}, i); + } + } + return rc; +} + +bool in_overlap(int, int id) +{ + return id == 0; +} + +} // namespace + +TEST_CASE("XGC FieldLayout marks overlap entries and gids") +{ + static constexpr int data_size = 16; + auto rc = create_dummy_rc(data_size); + pcms::XGCFieldLayout layout(rc, in_overlap, data_size); + + auto owned = layout.GetOwnedHost(); + auto gids = layout.GetGidsHost(); + auto class_dims = layout.GetDOFHolderClassificationDimensionsHost(); + auto class_ids = layout.GetDOFHolderClassificationIdsHost(); + + for (int i = 0; i < data_size; ++i) { + REQUIRE(gids[i] == i + 1); + if (i % 4 == 0) { + REQUIRE(owned[i]); + REQUIRE(class_dims[i] == 0); + REQUIRE(class_ids[i] == 0); + } else { + REQUIRE(!owned[i]); + REQUIRE(class_dims[i] == -1); + REQUIRE(class_ids[i] == -1); + } + } +} + +TEST_CASE("XGC FieldData serializer preserves inactive entries") +{ + static constexpr int data_size = 16; + auto rc = create_dummy_rc(data_size); + auto layout = + std::make_shared(rc, in_overlap, data_size); + + std::vector data(data_size); + std::iota(data.begin(), data.end(), 0.0); + auto original = data; + pcms::XGCFieldData field(layout, pcms::FieldMetadata{}, + pcms::make_array_view(data)); + pcms::XGCFieldSerializer serializer(MPI_COMM_SELF); + + std::vector permutation(data_size); + std::iota(permutation.begin(), permutation.end(), 0); + std::vector buffer(data_size, -1.0); + + auto owned = layout->GetOwnedHost(); + const int num_owned = + std::count_if(owned.data_handle(), owned.data_handle() + data_size, + [](bool is_owned) { return is_owned; }); + + REQUIRE(serializer.Serialize(field, *layout, pcms::make_array_view(buffer), + pcms::make_const_array_view(permutation)) == + data_size); + REQUIRE(num_owned == 4); + + for (int i = 0; i < data_size; ++i) { + if (owned[i]) { + REQUIRE(buffer[i] == data[i]); + buffer[i] += 100.0; + } else { + REQUIRE(buffer[i] == -1.0); + } + } + + serializer.Deserialize(field, *layout, pcms::make_const_array_view(buffer), + pcms::make_const_array_view(permutation)); + + auto after = field.GetDOFHolderDataHost(); + for (int i = 0; i < data_size; ++i) { + if (owned[i]) { + REQUIRE(after[i] == Catch::Approx(original[i] + 100.0)); + } else { + REQUIRE(after[i] == Catch::Approx(original[i])); + } + } +} + +TEST_CASE("XGCFieldFactory creates fields") +{ + static constexpr int data_size = 16; + auto rc = create_dummy_rc(data_size); + pcms::XGCFieldFactory function_space(rc, in_overlap, data_size); + + std::vector data(data_size); + std::iota(data.begin(), data.end(), 0.0); + auto field = function_space.CreateField( + std::make_unique>( + function_space.GetXGCLayout(), pcms::FieldMetadata{}, + pcms::make_array_view(data))); + + REQUIRE(&field.GetLayout() == function_space.GetLayout().get()); + REQUIRE(function_space.GetCoordinateSystem() == pcms::CoordinateSystem::XGC); +} diff --git a/test/test_xgc_reverse_classification.cpp b/test/test_xgc_reverse_classification.cpp index 467122ce0..4b87fcb9c 100644 --- a/test/test_xgc_reverse_classification.cpp +++ b/test/test_xgc_reverse_classification.cpp @@ -1,4 +1,4 @@ -#include "pcms/adapter/xgc/xgc_reverse_classification.h" +#include "pcms/discretization/discretization/xgc_reverse_classification.h" #include #include diff --git a/test/unit_test_main.cpp b/test/unit_test_main.cpp index 8a12542fc..ade35e8b2 100644 --- a/test/unit_test_main.cpp +++ b/test/unit_test_main.cpp @@ -1,14 +1,36 @@ #include -#include #include +#include +#include + +#ifdef PCMS_ENABLE_PETSC +#include +#endif int main(int argc, char* argv[]) { MPI_Init(&argc, &argv); int result = 0; { - Kokkos::ScopeGuard kokkos{}; + // PETSc uses Kokkos, so initialize Kokkos before PETSc when enabled. + Kokkos::ScopeGuard kokkos{argc, argv}; +#ifdef PCMS_ENABLE_PETSC + PetscBool petsc_initialized = PETSC_FALSE; + PetscBool petsc_initialized_by_main = PETSC_FALSE; + PetscInitialized(&petsc_initialized); + if (!petsc_initialized) { + PetscInitialize(&argc, &argv, nullptr, nullptr); + petsc_initialized_by_main = PETSC_TRUE; + } + // petsc must be finalized in the kokkos ScopeGuard scope kokkos needs to + // deallocate after petsc is done finalizing. + result = Catch::Session().run(argc, argv); + if (petsc_initialized_by_main) { + PetscFinalize(); + } +#else result = Catch::Session().run(argc, argv); +#endif } MPI_Finalize(); return result; diff --git a/test/xgc_n0_coupling_server.cpp b/test/xgc_n0_coupling_server.cpp index c5b433b78..ddbac140a 100644 --- a/test/xgc_n0_coupling_server.cpp +++ b/test/xgc_n0_coupling_server.cpp @@ -3,14 +3,19 @@ #include #include #include "test_support.h" -#include "pcms/adapter/meshfields/mesh_fields_adapter.h" -#include "pcms/adapter/xgc/xgc_field_adapter.h" +#include "pcms/coupler/coupler.hpp" +#include "pcms/coupler/field_serializer.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/field/field.h" +#include "pcms/field/field_metadata.h" +#include "pcms/field/data/simple.h" +#include "pcms/transfer/copy.h" #include +#include -using pcms::Copy; using pcms::GO; using pcms::LO; -using pcms::OmegaHFieldAdapter; namespace ts = test_support; @@ -20,57 +25,68 @@ namespace ts = test_support; // [[nodiscard]] -static pcms::CoupledField* AddField(pcms::Application* application, - const std::string& name, - const std::string& path, - Omega_h::Read is_overlap, - const std::string& numbering, - Omega_h::Mesh& mesh, int plane) +static std::string MakeFieldName(const std::string& name, int plane) { - PCMS_ALWAYS_ASSERT(application != nullptr); std::stringstream field_name; field_name << name; if (plane >= 0) { field_name << "_" << plane; } - return application->AddField( - field_name.str(), pcms::OmegaHFieldAdapter( - path + field_name.str(), mesh, is_overlap, numbering)); + return field_name.str(); +} + +struct RegisteredField +{ + pcms::FieldHandle handle; +}; + +[[nodiscard]] +static RegisteredField AddField( + pcms::Application* application, + const pcms::LagrangeFunctionSpace& function_space, const std::string& name, + const std::string& path, int plane) +{ + PCMS_ALWAYS_ASSERT(application != nullptr); + auto field_name = MakeFieldName(name, plane); + auto field = function_space.CreateField(pcms::FieldMetadata{}); + std::unique_ptr> serializer = + std::make_unique>(); + auto handle = application->AddField(path + field_name, std::move(field), + std::move(serializer)); + return {std::move(handle)}; } struct XGCAnalysis { - using FieldVec = std::vector; + using FieldVec = std::vector; std::array dpot; FieldVec pot0; std::array edensity; std::array idensity; - pcms::CoupledField* psi; - pcms::CoupledField* gids; + std::optional psi; + std::optional gids; }; -static void ReceiveFields(const std::vector& fields) +static void ReceiveFields(const std::vector& fields) { - for (auto* field : fields) { - field->Receive(); + for (const auto& field : fields) { + field.handle.Receive(); } } -static void SendFields(const std::vector& fields) +static void SendFields(const std::vector& fields) { - for (auto* field : fields) { - field->Send(); + for (const auto& field : fields) { + field.handle.Send(); } } -static void CopyFields(const std::vector& from_fields, - const std::vector& to_fields) +static void CopyFields(const std::vector& from_fields, + const std::vector& to_fields) { PCMS_ALWAYS_ASSERT(from_fields.size() == to_fields.size()); for (size_t i = 0; i < from_fields.size(); ++i) { - const auto* from = - from_fields[i]->GetFieldAdapter>(); - auto* to = - to_fields[i]->GetFieldAdapter>(); - copy_field(from->GetField(), to->GetField()); + auto& source = from_fields[i].handle.GetField(); + auto& target = to_fields[i].handle.GetField(); + target.SetDOFHolderDataHost(source.GetDOFHolderDataHost()); } } @@ -202,6 +218,12 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, // return 0; return 1; }); + auto function_space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::CoordinateSystem::Cartesian, is_overlap, numbering, + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto layout = function_space.GetLayout(); + core->AddLayout("core_layout", layout); + edge->AddLayout("edge_layout", layout); auto time2 = std::chrono::steady_clock::now(); elapsed_seconds = time2 - time1; ts::timeMinMaxAvg(elapsed_seconds.count(), min, max, avg); @@ -216,52 +238,48 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, // is_overlap, numbering, mesh, // i)); core_analysis.dpot[0].push_back( - AddField(core, "dpot_0_plane", "core/", is_overlap, numbering, mesh, i)); + AddField(core, function_space, "dpot_0_plane", "core/", i)); core_analysis.dpot[1].push_back( - AddField(core, "dpot_1_plane", "core/", is_overlap, numbering, mesh, i)); + AddField(core, function_space, "dpot_1_plane", "core/", i)); // core_analysis.dpot[3].push_back(AddField(core, "dpot_2_plane", "core/", // is_overlap, numbering, mesh, // i)); core_analysis.pot0.push_back( - AddField(core, "pot0_plane", "core/", is_overlap, numbering, mesh, i)); - core_analysis.edensity[0].push_back(AddField( - core, "edensity_1_plane", "core/", is_overlap, numbering, mesh, i)); - core_analysis.edensity[1].push_back(AddField( - core, "edensity_2_plane", "core/", is_overlap, numbering, mesh, i)); - core_analysis.idensity[0].push_back(AddField( - core, "idensity_1_plane", "core/", is_overlap, numbering, mesh, i)); - core_analysis.idensity[1].push_back(AddField( - core, "idensity_2_plane", "core/", is_overlap, numbering, mesh, i)); + AddField(core, function_space, "pot0_plane", "core/", i)); + core_analysis.edensity[0].push_back( + AddField(core, function_space, "edensity_1_plane", "core/", i)); + core_analysis.edensity[1].push_back( + AddField(core, function_space, "edensity_2_plane", "core/", i)); + core_analysis.idensity[0].push_back( + AddField(core, function_space, "idensity_1_plane", "core/", i)); + core_analysis.idensity[1].push_back( + AddField(core, function_space, "idensity_2_plane", "core/", i)); // edge_analysis.dpot[0].push_back(AddField(edge, "dpot_m1_plane", "edge/", // is_overlap, numbering, mesh, // i)); edge_analysis.dpot[0].push_back( - AddField(edge, "dpot_0_plane", "edge/", is_overlap, numbering, mesh, i)); + AddField(edge, function_space, "dpot_0_plane", "edge/", i)); edge_analysis.dpot[1].push_back( - AddField(edge, "dpot_1_plane", "edge/", is_overlap, numbering, mesh, i)); + AddField(edge, function_space, "dpot_1_plane", "edge/", i)); // edge_analysis.dpot[3].push_back(AddField(edge, "dpot_2_plane", "edge/", // is_overlap, numbering, mesh, // i)); edge_analysis.pot0.push_back( - AddField(edge, "pot0_plane", "edge/", is_overlap, numbering, mesh, i)); - edge_analysis.edensity[0].push_back(AddField( - edge, "edensity_1_plane", "edge/", is_overlap, numbering, mesh, i)); - edge_analysis.edensity[1].push_back(AddField( - edge, "edensity_2_plane", "edge/", is_overlap, numbering, mesh, i)); - edge_analysis.idensity[0].push_back(AddField( - edge, "idensity_1_plane", "edge/", is_overlap, numbering, mesh, i)); - edge_analysis.idensity[1].push_back(AddField( - edge, "idensity_2_plane", "edge/", is_overlap, numbering, mesh, i)); + AddField(edge, function_space, "pot0_plane", "edge/", i)); + edge_analysis.edensity[0].push_back( + AddField(edge, function_space, "edensity_1_plane", "edge/", i)); + edge_analysis.edensity[1].push_back( + AddField(edge, function_space, "edensity_2_plane", "edge/", i)); + edge_analysis.idensity[0].push_back( + AddField(edge, function_space, "idensity_1_plane", "edge/", i)); + edge_analysis.idensity[1].push_back( + AddField(edge, function_space, "idensity_2_plane", "edge/", i)); } - core_analysis.psi = - AddField(core, "psi", "core/", is_overlap, numbering, mesh, -1); - edge_analysis.psi = - AddField(edge, "psi", "edge/", is_overlap, numbering, mesh, -1); - core_analysis.gids = - AddField(core, "gid_debug", "core/", is_overlap, numbering, mesh, -1); - edge_analysis.gids = - AddField(edge, "gid_debug", "edge/", is_overlap, numbering, mesh, -1); + core_analysis.psi = AddField(core, function_space, "psi", "core/", -1); + edge_analysis.psi = AddField(edge, function_space, "psi", "edge/", -1); + core_analysis.gids = AddField(core, function_space, "gid_debug", "core/", -1); + edge_analysis.gids = AddField(edge, function_space, "gid_debug", "edge/", -1); auto time3 = std::chrono::steady_clock::now(); elapsed_seconds = time3 - time2; ts::timeMinMaxAvg(elapsed_seconds.count(), min, max, avg); @@ -270,12 +288,12 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, Omega_h::vtk::write_parallel("initial.vtk", &mesh); edge->BeginReceivePhase(); - edge_analysis.psi->Receive(); - edge_analysis.gids->Receive(); + edge_analysis.psi->handle.Receive(); + edge_analysis.gids->handle.Receive(); edge->EndReceivePhase(); core->BeginReceivePhase(); - core_analysis.psi->Receive(); - core_analysis.gids->Receive(); + core_analysis.psi->handle.Receive(); + core_analysis.gids->handle.Receive(); core->EndReceivePhase(); Omega_h::vtk::write_parallel("psi-only.vtk", &mesh); auto time4 = std::chrono::steady_clock::now(); @@ -295,27 +313,35 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, int main(int argc, char** argv) { - auto lib = Omega_h::Library(&argc, &argv); - auto world = lib.world(); - const int rank = world->rank(); - int size = world->size(); - if (argc != 4) { - if (!rank) { - std::cerr << "Usage: " << argv[0] - << " " - " " - "sml_nphi_total"; + try { + auto lib = Omega_h::Library(&argc, &argv); + auto world = lib.world(); + const int rank = world->rank(); + int size = world->size(); + if (argc != 4) { + if (!rank) { + std::cerr << "Usage: " << argv[0] + << " " + " " + "sml_nphi_total"; + } + exit(EXIT_FAILURE); } - exit(EXIT_FAILURE); - } - const auto meshFile = argv[1]; - const auto classPartitionFile = argv[2]; - const int sml_nphi_total = std::atoi(argv[3]); + const auto meshFile = argv[1]; + const auto classPartitionFile = argv[2]; + const int sml_nphi_total = std::atoi(argv[3]); - Omega_h::Mesh mesh(&lib); - Omega_h::binary::read(meshFile, lib.world(), &mesh); - MPI_Comm mpi_comm = lib.world()->get_impl(); - omegah_coupler(mpi_comm, mesh, classPartitionFile, sml_nphi_total); - return 0; + Omega_h::Mesh mesh(&lib); + Omega_h::binary::read(meshFile, lib.world(), &mesh); + MPI_Comm mpi_comm = lib.world()->get_impl(); + omegah_coupler(mpi_comm, mesh, classPartitionFile, sml_nphi_total); + return 0; + } catch (const std::exception& e) { + std::cerr << "Exception caught in main: " << e.what() << std::endl; + return 1; + } catch (...) { + std::cerr << "Unknown exception caught in main" << std::endl; + return 1; + } } diff --git a/tools/XgcRCfromOsh.cpp b/tools/XgcRCfromOsh.cpp index 2b66a344f..093e7baa5 100644 --- a/tools/XgcRCfromOsh.cpp +++ b/tools/XgcRCfromOsh.cpp @@ -1,5 +1,5 @@ #include -#include "pcms/adapter/xgc/xgc_reverse_classification.h" +#include "pcms/discretization/discretization/xgc_reverse_classification.h" #include "pcms/utility/print.h" #include #include