diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..48415451a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# SWIG output regenerated from client.i; collapsed in GitHub diffs +src/pcms/fortranapi/client_wrap.c linguist-generated +src/pcms/fortranapi/pcms.f90 linguist-generated diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index 784e8f71e..2329580a5 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -105,7 +105,7 @@ jobs: with: repo-name: 'meshFields-openmpi' repo-path: 'SCOREC/meshFields' - repo-ref: 'b1482bbba288df210784b2345eae08e34faabdc4' + repo-ref: 'b9984520c20f73955a3b4e25e4907780b91d430f' cache: true options: '-DCMAKE_CXX_COMPILER=`which mpicxx` -DCMAKE_C_COMPILER=`which mpicc` @@ -165,7 +165,7 @@ jobs: with: repo-name: 'redev-openmpi' repo-path: 'SCOREC/redev' - repo-ref: 'ac09848a5f9b89493e8b679c9080b9efe5538376' + repo-ref: '1e4c876f786b420f060695ba2c1079447e975b69' cache: true options: '-DCMAKE_CXX_COMPILER=`which mpicxx` -DMPIEXEC_EXECUTABLE=`which mpirun` diff --git a/.github/workflows/cmake-test.yml b/.github/workflows/cmake-test.yml index d736a9523..413cd5cff 100644 --- a/.github/workflows/cmake-test.yml +++ b/.github/workflows/cmake-test.yml @@ -16,6 +16,15 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 + env: + # Builds that depend on Kokkos must not share a cache entry across + # shared/static or Serial/OpenMP configurations. + DEP_CACHE_SUFFIX: ${{ matrix.python_api == 'ON' && '-shared' || '' }}${{ matrix.openmp == 'ON' && '-openmp' || '' }} + # Use more than one thread so threading bugs can show up. + OMP_NUM_THREADS: ${{ matrix.openmp == 'ON' && '2' || '1' }} + OMP_PROC_BIND: 'false' + KOKKOS_ENABLE_OPENMP: ${{ matrix.openmp == 'ON' && 'ON' || 'OFF' }} + defaults: run: shell: bash @@ -29,6 +38,7 @@ jobs: python_api: [OFF, ON] meshfields: [ON] petsc: [ON] + openmp: [OFF] include: - build_type: Release memory_test: OFF @@ -44,6 +54,15 @@ jobs: python_api: OFF meshfields: ON petsc: OFF + # Kokkos OpenMP backend (all other entries use Serial) + - build_type: Release + memory_test: OFF + compiler: g++ + language: 'cpp' + python_api: ON + meshfields: ON + petsc: ON + openmp: ON exclude: - build_type: Release memory_test: ON @@ -67,7 +86,7 @@ jobs: if: matrix.python_api == 'ON' run: | sudo apt-get install -yq python3 python3-pip python3-dev python3-pybind11 pybind11-dev - pip3 install numpy + pip3 install numpy pytest - uses: actions/checkout@v4 @@ -86,11 +105,11 @@ jobs: repo-path: 'kokkos/kokkos' repo-ref: '4.6.01' cache: true - cache-suffix: ${{ matrix.python_api == 'ON' && '-shared' || '' }} + cache-suffix: ${{ env.DEP_CACHE_SUFFIX }} options: '-DCMAKE_CXX_STANDARD=20 -DBUILD_SHARED_LIBS=${{ matrix.python_api }} -DKokkos_ENABLE_SERIAL=ON - -DKokkos_ENABLE_OPENMP=OFF + -DKokkos_ENABLE_OPENMP=${{ env.KOKKOS_ENABLE_OPENMP }} -DKokkos_ENABLE_CUDA=OFF -DKokkos_ENABLE_CUDA_LAMBDA=OFF -DKokkos_ENABLE_CUDA_CONSTEXPR=OFF' @@ -102,7 +121,7 @@ jobs: repo-path: 'kokkos/kokkos-kernels' repo-ref: '4.6.01' cache: true - cache-suffix: ${{ matrix.python_api == 'ON' && '-shared' || '' }} + cache-suffix: ${{ env.DEP_CACHE_SUFFIX }} options: '-DCMAKE_CXX_STANDARD=20 -DBUILD_SHARED_LIBS=${{ matrix.python_api }} -DKokkos_DIR=${{ runner.temp }}/build-kokkos/install/lib/cmake/Kokkos' @@ -114,7 +133,7 @@ jobs: repo-path: 'SCOREC/omega_h' repo-ref: '2b2e778472f48a85a5ff5e70083e3a415e58be72' cache: true - cache-suffix: ${{ matrix.python_api == 'ON' && '-shared' || '' }} + cache-suffix: ${{ env.DEP_CACHE_SUFFIX }} options: '-DCMAKE_CXX_COMPILER=`which mpicxx` -DCMAKE_C_COMPILER=`which mpicc` -DCMAKE_BUILD_TYPE="RelWithDebInfo" @@ -132,9 +151,9 @@ jobs: with: repo-name: 'meshFields' repo-path: 'SCOREC/meshFields' - repo-ref: 'b1482bbba288df210784b2345eae08e34faabdc4' + repo-ref: 'b9984520c20f73955a3b4e25e4907780b91d430f' cache: true - cache-suffix: ${{ matrix.python_api == 'ON' && '-shared' || '' }} + cache-suffix: ${{ env.DEP_CACHE_SUFFIX }} options: '-DCMAKE_CXX_COMPILER=`which mpicxx` -DCMAKE_C_COMPILER=`which mpicc` -DBUILD_SHARED_LIBS=${{ matrix.python_api }} @@ -165,7 +184,7 @@ jobs: with: repo-name: 'redev' repo-path: 'SCOREC/redev' - repo-ref: 'ac09848a5f9b89493e8b679c9080b9efe5538376' + repo-ref: '1e4c876f786b420f060695ba2c1079447e975b69' cache: true cache-suffix: ${{ matrix.python_api == 'ON' && '-shared' || '' }} options: '-DCMAKE_CXX_COMPILER=`which mpicxx` @@ -203,7 +222,7 @@ jobs: uses: actions/cache@v3 with: path: ${{ runner.temp }}/petsc - key: build-petsc-${{ steps.clone-petsc.outputs.petsc-commit-hash }}-${{ matrix.python_api }} + key: build-petsc-${{ steps.clone-petsc.outputs.petsc-commit-hash }}-${{ matrix.python_api }}${{ matrix.openmp == 'ON' && '-openmp' || '' }} - name: build petsc if: matrix.petsc == 'ON' && steps.cache-petsc.outputs.cache-hit != 'true' @@ -214,6 +233,7 @@ jobs: --with-kokkos-dir="${{ runner.temp }}/build-kokkos/install/" \ --with-kokkos-kernels-dir="${{ runner.temp }}/build-kokkos-kernels/install/" \ --with-cuda=0 \ + --with-openmp=${{ matrix.openmp == 'ON' && '1' || '0' }} \ --with-shared-libraries=${{ matrix.python_api == 'ON' && '1' || '0' }} \ --download-fblaslapack make all check @@ -288,12 +308,25 @@ jobs: run: | export PYTHONPATH=${{ runner.temp }}/build-pcms/install/lib/python3.12/site-packages:$PYTHONPATH export PYTHONPATH=${{ runner.temp }}/build-omega_h/install/lib/python/dist-packages:$PYTHONPATH - cd ${{ github.workspace }}/src/pcms/pythonapi - python3 test_file_io.py - python3 test_omega_h_field.py - python3 test_uniform_grid_field.py - python3 test_field_copy.py - python3 test_mls_interpolation.py + cd ${{ github.workspace }} + pytest -v --tb=short -p no:cacheprovider + + # The docs need the compiled pcms module for the Python API, so they are + # built in one Python-enabled configuration. + - name: Build documentation + if: matrix.python_api == 'ON' && matrix.openmp != 'ON' + run: | + sudo apt-get install -yq doxygen + pip3 install -r docs/requirements.txt + export PYTHONPATH=${{ runner.temp }}/build-pcms/install/lib/python3.12/site-packages:$PYTHONPATH + export PYTHONPATH=${{ runner.temp }}/build-omega_h/install/lib/python/dist-packages:$PYTHONPATH + sphinx-build -b html docs docs/_build/html + + - name: Upload documentation + if: matrix.python_api == 'ON' && matrix.openmp != 'ON' + uses: actions/upload-pages-artifact@v3 + with: + path: docs/_build/html - name: Test PCMS Installation if: matrix.python_api == 'OFF' @@ -316,3 +349,14 @@ jobs: ${{ 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 + + deploy-docs: + needs: test-build + if: github.event_name == 'push' && github.ref == 'refs/heads/develop' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + steps: + - name: Deploy to GitHub Pages + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/self-hosted.yml b/.github/workflows/self-hosted.yml index 35043edf7..c4f3d61ce 100644 --- a/.github/workflows/self-hosted.yml +++ b/.github/workflows/self-hosted.yml @@ -25,6 +25,8 @@ jobs: submodules: recursive path: 'pcms_${{ github.event.id }}' #under $GITHUB_WORKSPACE + # note: modules match to https://github.com/LACES-LAB/software-dependencies/blob/main/loads-rhel9.sh + # However, due to risk of running user modifiable code, we repeat modules here. - name: setup id: setup shell: bash @@ -38,13 +40,15 @@ jobs: cat << 'EOF' > $workDir/envGcc13.sh #quotes prevent variable expansion in doc contents set +e #avoid exiting when lua modules return non-zero on 'warning' messages source /etc/profile #provides module command - module use /opt/scorec/spack/rhel9/v0222_2/lmod/linux-rhel9-x86_64/Core/ - module load gcc/13.2.0-4eahhas - module load mpich/4.2.3-62uy3hd - module load cmake/3.30.5-5e54py4 - module load cuda/12.6.2-gqq65nw - module load openblas/0.3.28-eubd5ed - export LD_LIBRARY_PATH=/opt/scorec/spack/rhel9/v0222_2/install/linux-rhel9-x86_64_v3/gcc-13.2.0/mpich-4.2.3-62uy3hdwqe75fjfskrrysco5i6gqjblv/lib:$LD_LIBRARY_PATH + module purge + module use /opt/scorec/spack/rhel9/v0201_4/lmod/linux-rhel9-x86_64/Core/ + module load gcc/12.3.0-iil3lno mpich/4.1.1-xpoyz4t cuda/12.1.1-zxa4msk + module load fftw/3.3.10-qqvl57a + module load cmake/3.26.3-xxgzxlv + #module load openblas/0.3.23-wqm7iud + module load netlib-lapack/3.11.0-b22mgwg #netlib-lapack includes blas + module load netlib-scalapack/2.2.0-fzd4jvl + module load python py-pytest py-numpy set -e EOF @@ -54,190 +58,137 @@ jobs: run: | workDir=$PCMS_WORK_DIR source $workDir/envGcc13.sh + bdir=${workDir}/build-pcms - # kokkos - git clone --branch 4.6.01 --depth 1 https://github.com/kokkos/kokkos.git ${workDir}/kokkos - kkbdir=${workDir}/build-kokkos - cmake -S ${workDir}/kokkos -B $kkbdir \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=$kkbdir/install \ - -DCMAKE_CXX_COMPILER=${workDir}/kokkos/bin/nvcc_wrapper \ - -DKokkos_ARCH_AMPERE80=ON \ - -DKokkos_ENABLE_SERIAL=ON \ - -DKokkos_ENABLE_OPENMP=off \ - -DKokkos_ENABLE_CUDA=on \ - -DKokkos_ENABLE_CUDA_LAMBDA=on \ - -DKokkos_ENABLE_CUDA_CONSTEXPR=on \ - -DKokkos_ENABLE_DEBUG=off - cmake --build $kkbdir --target install -j 4 - echo "KOKKOS_BUILD=$kkbdir" >> $GITHUB_ENV - - # kokkos-kernels - git clone --branch 4.6.01 --depth 1 https://github.com/kokkos/kokkos-kernels.git ${workDir}/kokkos-kernels - kkkbdir=${workDir}/build-kokkos-kernels - cmake -S ${workDir}/kokkos-kernels -B $kkkbdir \ - -DCMAKE_INSTALL_PREFIX=$kkkbdir/install \ - -DCMAKE_BUILD_TYPE=Release \ - -DKokkos_ROOT=${kkbdir}/install/ \ - -DBUILD_SHARED_LIBS=off - cmake --build $kkkbdir --target install -j 4 - echo "KOKKOS_KERNELS_BUILD=$kkkbdir" >> $GITHUB_ENV - - # adios2 with and without cuda - git clone https://github.com/ornladios/ADIOS2.git ${workDir}/ADIOS2 - cd ${workDir}/ADIOS2 - git checkout 7dc7887c1c985a0bf4c96cb2fea1ba92af4c8ebd - cd $workDir - adiosbdir=${workDir}/build-ADIOS2 - cmake -S ${workDir}/ADIOS2 -B $adiosbdir \ - -DCMAKE_INSTALL_PREFIX=$adiosbdir/install \ - -DADIOS2_USE_CUDA=on \ - -DADIOS2_USE_ZFP=off - cmake --build $adiosbdir --target install -j 4 - echo "ADIOS_BUILD=$adiosbdir" >> $GITHUB_ENV - - # perfstubs - git clone https://github.com/UO-OACISS/perfstubs.git ${workDir}/perfstubs - cd ${workDir}/perfstubs - git checkout a1fa3feb1d89214e28047f166500351074b5f0c2 - cd $workDir - psbdir=${workDir}/build-perfstubs - cmake -S ${workDir}/perfstubs -B $psbdir \ - -DCMAKE_INSTALL_PREFIX=$psbdir/install \ - -DCMAKE_CXX_COMPILER=mpicxx - cmake --build $psbdir --target install -j 4 - echo "PERFSTUBS_BUILD=$psbdir" >> $GITHUB_ENV - - # redev - git clone https://github.com/SCOREC/redev.git ${workDir}/redev - cd ${workDir}/redev - git checkout ac09848a5f9b89493e8b679c9080b9efe5538376 - cd $workDir - rdbdir=${workDir}/build-redev - cmake -S ${workDir}/redev -B $rdbdir \ - -DCMAKE_INSTALL_PREFIX=$rdbdir/install \ - -DADIOS2_DIR=$adiosbdir/install/lib64/cmake/adios2 \ - -Dperfstubs_DIR=$psbdir/install/lib/cmake \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DBUILD_SHARED_LIBS=OFF - cmake --build $rdbdir --target install -j 4 - echo "REDEV_BUILD=$rdbdir" >> $GITHUB_ENV - - # omega_h - git clone https://github.com/SCOREC/omega_h.git ${workDir}/omegah_h - cd omegah_h - git checkout 1765836a00b9a64b8b3791f1442ac52f147e43b2 - cd $workDir - ohbdir=${workDir}/build-omega_h - cmake -S ${workDir}/omegah_h -B $ohbdir \ - -DCMAKE_INSTALL_PREFIX=$ohbdir/install \ - -DKokkos_DIR=${kkbdir}/install/lib64/cmake/Kokkos \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_SHARED_LIBS=off \ - -DOmega_h_USE_Kokkos=ON \ - -DOmega_h_USE_CUDA=on \ - -DOmega_h_CUDA_ARCH=80 \ - -DOmega_h_USE_MPI=on \ - -DMPIEXEC_EXECUTABLE=mpirun \ - -DBUILD_TESTING=off \ - -DCMAKE_C_COMPILER=mpicc \ - -DCMAKE_CXX_COMPILER=mpicxx - cmake --build $ohbdir --target install -j 4 - echo "OMEGA_H_BUILD=$ohbdir" >> $GITHUB_ENV - - # meshfields - git clone https://github.com/SCOREC/meshFields.git ${workDir}/meshfields - cd ${workDir}/meshfields - git checkout b1482bbba288df210784b2345eae08e34faabdc4 - cd $workDir - mfbdir=${workDir}/build-meshfields - cmake -S ${workDir}/meshfields -B $mfbdir \ - -DCMAKE_INSTALL_PREFIX=$mfbdir/install \ - -DOmega_h_DIR=$ohbdir/install/lib64/cmake/Omega_h \ - -DKokkos_DIR=${kkbdir}/install/lib64/cmake/Kokkos \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DCMAKE_C_COMPILER=mpicc \ - -DMPIEXEC_EXECUTABLE=mpirun - cmake --build $mfbdir --target install -j 4 - echo "MESHFIELDS_BUILD=$mfbdir" >> $GITHUB_ENV - - # catch2 - git clone --branch v3.11.0 https://github.com/catchorg/Catch2.git ${workDir}/Catch2 - c2bdir=${workDir}/build-Catch2 - cmake -S ${workDir}/Catch2 -B $c2bdir \ - -DCMAKE_INSTALL_PREFIX=$c2bdir/install - cmake --build $c2bdir --target install -j 4 - echo "CATCH2_BUILD=$c2bdir" >> $GITHUB_ENV - - # petsc - git clone --branch v3.24.2 https://gitlab.com/petsc/petsc.git ${workDir}/petsc - cd ${workDir}/petsc - ./configure \ - PETSC_ARCH=cuda-kokkos \ - --with-kokkos-dir=$kkbdir/install/ \ - --with-kokkos-kernels-dir=$kkkbdir/install/ \ - --with-cuda=1 \ - --with-shared-libraries=0 \ - --with-openblas-dir="${OPENBLAS_RHEL9_ROOT}" - make all check - cd $workDir - echo "PETSC_BUILD=${workDir}/petsc/cuda-kokkos" >> $GITHUB_ENV + export NVCC_WRAPPER_DEFAULT_COMPILER=`which mpicxx` + export PETSC_OPTIONS='-use_gpu_aware_mpi 0' git clone https://github.com/jacobmerson/pcms_testcases.git ${workDir}/pcms_testcases - - # pcms - petsc off - bdir=${workDir}/build-pcms-petsc-off + # PCMS dependencies are built with https://github.com/LACES-LAB/software-dependencies/blob/main/build-deps-rhel9.sh + # Built artifacts are located in the $DEPENDENCY_DIR + DEPENDENCY_DIR=/users/mersoj2/laces-software/build/ + DEVICE_ARCH=AMPERE80 + # debug output, check if this directory is readable + ls $DEPENDENCY_DIR/${DEVICE_ARCH} + ls $DEPENDENCY_DIR/${DEVICE_ARCH}/redev/install/lib64/cmake/redev/ + echo $DEPENDENCY_DIR/${DEVICE_ARCH}/redev/install/lib64/cmake/redev/ + + # Build PCMS without PETSc cmake -S ${{github.workspace}}/pcms_${{ github.event.id }} -B $bdir \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_C_COMPILER=mpicc \ - -DCMAKE_CXX_COMPILER=mpicxx \ + -DCMAKE_CXX_COMPILER=`which mpicxx` \ + -DCMAKE_C_COMPILER=`which mpicc` \ + -DCMAKE_Fortran_COMPILER=`which mpifort`\ -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/ \ + -DCMAKE_BUILD_TYPE=Release \ + -DCatch2_DIR=$DEPENDENCY_DIR/Catch2/install/lib64/cmake/Catch2/ \ + -Dmeshfields_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/meshFields/install/lib64/cmake/meshfields \ + -DOmega_h_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/omega_h/install/lib64/cmake/Omega_h/ \ + -Dredev_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/redev/install/lib64/cmake/redev/ \ -DPCMS_TEST_DATA_DIR=${workDir}/pcms_testcases/ \ - -DCMAKE_CXX_EXTENSIONS=Off - cmake --build $bdir + -DMPIEXEC_EXECUTABLE=`which mpirun` \ + -DADIOS2_DIR=$DEPENDENCY_DIR/adios2/install/lib64/cmake/adios2/ \ + -Dperfstubs_DIR=$DEPENDENCY_DIR/perfstubs/install/lib/cmake/ \ + -DKokkos_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/kokkos/install/lib64/cmake/Kokkos/ \ + -DKokkosKernels_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/kokkos-kernels/install/lib64/cmake/KokkosKernels/ \ + -DBUILD_TESTING=ON \ + -DPCMS_ENABLE_PETSC=OFF \ + -DPETSC_ARCH="" \ + -DPETSC_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/petsc/install \ + -DPCMS_ENABLE_SPDLOG=OFF + + cmake --build $bdir -j 4 ctest --test-dir $bdir --output-on-failure - # pcms - export PETSC_OPTIONS="-use_gpu_aware_mpi 0" - bdir=${workDir}/build-pcms + # Build pcms with PETSc and python enabled cmake -S ${{github.workspace}}/pcms_${{ github.event.id }} -B $bdir \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_C_COMPILER=mpicc \ - -DCMAKE_CXX_COMPILER=mpicxx \ + -DCMAKE_CXX_COMPILER=`which mpicxx` \ + -DCMAKE_C_COMPILER=`which mpicc` \ + -DCMAKE_Fortran_COMPILER=`which mpifort`\ -DPCMS_TIMEOUT=20 \ + -DCMAKE_BUILD_TYPE=DEBUG \ + -DCatch2_DIR=$DEPENDENCY_DIR/Catch2/install/lib64/cmake/Catch2/ \ + -Dmeshfields_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/meshFields/install/lib64/cmake/meshfields \ + -DOmega_h_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/omega_h/install/lib64/cmake/Omega_h/ \ + -Dredev_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/redev/install/lib64/cmake/redev/ \ + -DPCMS_TEST_DATA_DIR=${workDir}/pcms_testcases/ \ + -DMPIEXEC_EXECUTABLE=`which mpirun` \ + -DADIOS2_DIR=$DEPENDENCY_DIR/adios2/install/lib64/cmake/adios2/ \ + -Dperfstubs_DIR=$DEPENDENCY_DIR/perfstubs/install/lib/cmake/ \ + -DKokkos_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/kokkos/install/lib64/cmake/Kokkos/ \ + -DKokkosKernels_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/kokkos-kernels/install/lib64/cmake/KokkosKernels/ \ + -DBUILD_TESTING=ON \ + -DPCMS_ENABLE_Python=ON \ -DPCMS_ENABLE_PETSC=ON \ - -DPETSC_LINK_STATIC=ON \ - -DPETSC_DIR=${workDir}/petsc \ - -DPETSC_ARCH=cuda-kokkos \ - -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/ \ + -DPETSC_ARCH="" \ + -DPETSC_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/petsc/install \ + -Dpybind11_ROOT=$DEPENDENCY_DIR/pybind11/install \ + -DPCMS_ENABLE_SPDLOG=OFF + + cmake --build $bdir -j 4 + ctest --test-dir $bdir --output-on-failure + + export PYTHONPATH=$DEPENDENCY_DIR/${DEVICE_ARCH}/omega_h/install/lib64/python./site-packages:$bdir/src/pcms/pythonapi:$PYTHONPATH + export LD_LIBRARY_PATH=$DEPENDENCY_DIR/${DEVICE_ARCH}/omega_h/install/lib64:$LD_LIBRARY_PATH + # The examples need Exodus data, which is stored with Git LFS + # and is not fetched by the clone above, so use the copy shared with + # the dependencies. We can resolve this when git-lfs installed on SCOREC asics + export PCMS_TEST_DATA_DIR=/users/mersoj2/laces-software/sources/pcms_testcases + export PCMS_REQUIRE_EXAMPLES=1 + pytest -v -p no:cacheprovider ${{github.workspace}}/pcms_${{ github.event.id }}/test/python + + - name: build pcms (Kokkos OpenMP) + id: build_pcms_kokkos_openmp + shell: bash + run: | + workDir=$PCMS_WORK_DIR + source $workDir/envGcc13.sh + bdir=${workDir}/build-pcms-openmp + + # The NATIVE dependencies use the Kokkos OpenMP (host) backend. + DEPENDENCY_DIR=/users/mersoj2/laces-software/build/ + DEVICE_ARCH=NATIVE + # Use more than one thread so threading bugs can show up. + export OMP_NUM_THREADS=2 + export OMP_PROC_BIND=false + + cmake -S ${{github.workspace}}/pcms_${{ github.event.id }} -B $bdir \ + -DCMAKE_CXX_COMPILER=`which mpicxx` \ + -DCMAKE_C_COMPILER=`which mpicc` \ + -DCMAKE_Fortran_COMPILER=`which mpifort`\ + -DPCMS_TIMEOUT=20 \ + -DCMAKE_BUILD_TYPE=Release \ + -DCatch2_DIR=$DEPENDENCY_DIR/Catch2/install/lib64/cmake/Catch2/ \ + -Dmeshfields_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/meshFields/install/lib64/cmake/meshfields \ + -DOmega_h_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/omega_h/install/lib64/cmake/Omega_h/ \ + -Dredev_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/redev/install/lib64/cmake/redev/ \ -DPCMS_TEST_DATA_DIR=${workDir}/pcms_testcases/ \ - -DCMAKE_CXX_EXTENSIONS=Off - cmake --build $bdir + -DMPIEXEC_EXECUTABLE=`which mpirun` \ + -DADIOS2_DIR=$DEPENDENCY_DIR/adios2/install/lib64/cmake/adios2/ \ + -Dperfstubs_DIR=$DEPENDENCY_DIR/perfstubs/install/lib/cmake/ \ + -DKokkos_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/kokkos/install/lib64/cmake/Kokkos/ \ + -DKokkosKernels_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/kokkos-kernels/install/lib64/cmake/KokkosKernels/ \ + -DBUILD_TESTING=ON \ + -DPCMS_ENABLE_Python=ON \ + -DPCMS_ENABLE_PETSC=ON \ + -DPETSC_ARCH="" \ + -DPETSC_DIR=$DEPENDENCY_DIR/${DEVICE_ARCH}/petsc/install \ + -Dpybind11_ROOT=$DEPENDENCY_DIR/pybind11/install \ + -DPCMS_ENABLE_SPDLOG=OFF + + cmake --build $bdir -j 4 ctest --test-dir $bdir --output-on-failure + export PYTHONPATH=$DEPENDENCY_DIR/${DEVICE_ARCH}/omega_h/install/lib64/python./site-packages:$bdir/src/pcms/pythonapi:$PYTHONPATH + export LD_LIBRARY_PATH=$DEPENDENCY_DIR/${DEVICE_ARCH}/omega_h/install/lib64:$LD_LIBRARY_PATH + pytest -v -p no:cacheprovider ${{github.workspace}}/pcms_${{ github.event.id }}/test/python + + - name: Save Result Link if: ${{ !cancelled() }} #prepare report unless the job was cancelled run: | mkdir -p ./pr echo "${{ github.event.id }}" > ./pr/issueNumber echo "Test Results:" > ./pr/message - echo "- Kokkos CUDA: ${{ steps.build_pcms_kokkos_cuda.outcome }}" >> ./pr/message echo "" >> ./pr/message echo "[(details)](https://github.com/${{github.repository}}/actions/runs/${{ github.run_id }})" >> ./pr/message diff --git a/.github/workflows/update_doxygen_page.yml b/.github/workflows/update_doxygen_page.yml deleted file mode 100644 index a78c4ffa2..000000000 --- a/.github/workflows/update_doxygen_page.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Update Doxygen Page - -on: - push: - branches: - - develop - -jobs: - doxygen: - runs-on: ubuntu-latest - permissions: - contents: read - pages: write - id-token: write - - steps: - - - name: Install Doxygen - run: | - sudo apt-get update - sudo apt-get install doxygen - - - name: Checkout Original - uses: actions/checkout@v4 - - - name: Run Doxygen - run: doxygen - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./html - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1de843758..c11728288 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ testdata .nfs* __pycache__/ +docs/_doxygen/ +docs/api/ +spack/spack.lock diff --git a/CITATION.cff b/CITATION.cff index 1e07bd5b3..93d2ebb09 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -40,6 +40,9 @@ authors: identifiers: - type: doi value: 10.48550/arXiv.2510.18838 + - type: doi + value: 10.1016/j.cpc.2026.110283 + description: Computer Physics Communications article repository-code: 'https://github.com/SCOREC/pcms' abstract: >- PCMS provides infrastructure to couple multiple simulation @@ -60,3 +63,30 @@ keywords: - supercomputing - unstructured-meshes license: BSD-3-Clause +preferred-citation: + type: article + title: 'PCMS: Parallel coupler for multimodel simulations' + journal: Computer Physics Communications + volume: 327 + start: 110283 + year: 2026 + month: 10 + doi: 10.1016/j.cpc.2026.110283 + authors: + - given-names: Jacob S. + family-names: Merson + orcid: 'https://orcid.org/0000-0002-6813-6532' + - given-names: Cameron W. + family-names: Smith + - given-names: Mark S. + family-names: Shephard + - given-names: Fuad + family-names: Hasan + - given-names: Abhiyan + family-names: Paudel + - given-names: Angel + family-names: Castillo-Crooke + - given-names: Joyal + family-names: Mathew + - given-names: Mohammad + family-names: Elahi diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d407a538..5dd0472f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,10 +44,30 @@ 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_TRANSIENT "Enable pcms transient module" ON) 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_EXAMPLES "Build the in-tree C++ examples" ON) + +# Per-thread on-stack ring-buffer capacities for the adjacency-based +# intersection/adjacency BFS (queue_visited.hpp). Defaults are sized for 3D +# tetrahedral meshes on the serial/OpenMP backends. Lower them for device +# (GPU) builds, where each search thread's stack must hold these arrays. +set(PCMS_INTERSECTION_QUEUE_SIZE 1024 CACHE STRING + "Max BFS queue size per target element in the intersection search") +set(PCMS_INTERSECTION_TRACK_SIZE 2048 CACHE STRING + "Max visited-set size per target element in the intersection search") +set(PCMS_INTERSECTION_ABS_TOL 1e-18 CACHE STRING + "Absolute measure tolerance for accepting a clipped intersection") +set(PCMS_INTERSECTION_REL_TOL 1e-12 CACHE STRING + "Relative measure tolerance for accepting a clipped intersection") +set(PCMS_PROJECTION_KSP_RTOL 1e-14 CACHE STRING + "Default relative tolerance for the projection mass-matrix solve") +set(PCMS_PROJECTION_KSP_ATOL 1e-50 CACHE STRING + "Default absolute tolerance for the projection mass-matrix solve") option(PETSC_LINK_STATIC "Use pkg-config --static results for PETSc" ${_pcms_link_petsc_static_default}) @@ -119,12 +139,26 @@ 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) -option(PCMS_ENABLE_MESHFIELDS "Enable MeshFields support" ON) +option(PCMS_ENABLE_MESHFIELDS "Enable MeshFields support" OFF) option(PCMS_ENABLE_PETSC "Enable PETSc support" OFF) +option(PCMS_ENABLE_ARBORX "Enable ArborX support" OFF) + +if (PCMS_ENABLE_ARBORX) + find_package(ArborX REQUIRED) + message(STATUS "Found ArborX: ${ArborX_DIR}") +endif() if(PCMS_ENABLE_MESHFIELDS) find_package(meshfields REQUIRED) @@ -142,6 +176,9 @@ include(test/testing.cmake) # define 'smoke tests' to test the install add_custom_target(test_install DEPENDS check) # maintain test_install target if(NOT BUILD_TESTING) + # include(CTest) only enables testing when BUILD_TESTING is on. Enable it so + # smoke_test is still registered and 'check' can run it. + enable_testing() add_custom_target( check COMMAND ${CMAKE_CTEST_COMMAND} -R smoke_test @@ -150,6 +187,8 @@ endif() add_executable(smoke_test test/test_ohClassPtn.cpp) target_link_libraries(smoke_test pcms::core test_support) +# Uses a mesh vendored in test/data so the install check works without +# PCMS_TEST_DATA_DIR (e.g. with BUILD_TESTING=OFF). dual_mpi_test( TESTNAME smoke_test @@ -158,23 +197,26 @@ dual_mpi_test( NAME1 rdv EXE1 - ./smoke_test + smoke_test PROCS1 2 ARGS1 1 - ${PCMS_TEST_DATA_DIR}/cyclone/23elements/mesh.osh/ + ${CMAKE_CURRENT_SOURCE_DIR}/test/data/cyclone23/mesh.osh/ NAME2 app EXE2 - ./smoke_test + smoke_test PROCS2 1 ARGS2 0 - ${PCMS_TEST_DATA_DIR}/cyclone/23elements/mesh.osh/) + ${CMAKE_CURRENT_SOURCE_DIR}/test/data/cyclone23/mesh.osh/) if(BUILD_TESTING) add_subdirectory(test) endif() add_subdirectory(tools) +if(PCMS_ENABLE_EXAMPLES) + add_subdirectory(examples) +endif() diff --git a/Doxyfile b/Doxyfile index 9999dcc0d..2d9ad0e90 100644 --- a/Doxyfile +++ b/Doxyfile @@ -68,7 +68,7 @@ PROJECT_LOGO = # entered, it will be relative to the location where doxygen was started. If # left blank the current directory will be used. -OUTPUT_DIRECTORY = +OUTPUT_DIRECTORY = docs/_doxygen # If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096 # sub-directories (in 2 levels) under the output directory of each output format @@ -193,7 +193,7 @@ STRIP_FROM_PATH = # specify the list of include paths that are normally passed to the compiler # using the -I flag. -STRIP_FROM_INC_PATH = +STRIP_FROM_INC_PATH = src # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but # less readable) file names. This can be useful is your file systems doesn't @@ -499,7 +499,7 @@ NUM_PROC_THREADS = 1 # normally produced when WARNINGS is set to YES. # The default value is: NO. -EXTRACT_ALL = NO +EXTRACT_ALL = YES # If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will # be included in the documentation. @@ -917,7 +917,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = +INPUT = src # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses @@ -957,52 +957,8 @@ INPUT_FILE_ENCODING = # comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, # *.vhdl, *.ucf, *.qsf and *.ice. -FILE_PATTERNS = *.c \ - *.cc \ - *.cxx \ - *.cpp \ - *.c++ \ - *.java \ - *.ii \ - *.ixx \ - *.ipp \ - *.i++ \ - *.inl \ - *.idl \ - *.ddl \ - *.odl \ - *.h \ - *.hh \ - *.hxx \ - *.hpp \ - *.h++ \ - *.l \ - *.cs \ - *.d \ - *.php \ - *.php4 \ - *.php5 \ - *.phtml \ - *.inc \ - *.m \ - *.markdown \ - *.md \ - *.mm \ - *.dox \ - *.py \ - *.pyw \ - *.f90 \ - *.f95 \ - *.f03 \ - *.f08 \ - *.f18 \ - *.f \ - *.for \ - *.vhd \ - *.vhdl \ - *.ucf \ - *.qsf \ - *.ice +FILE_PATTERNS = *.h \ + *.hpp # The RECURSIVE tag can be used to specify whether or not subdirectories should # be searched for input files as well. @@ -1017,7 +973,9 @@ RECURSIVE = YES # Note that relative paths are relative to the directory from which doxygen is # run. -EXCLUDE = +EXCLUDE = src/pcms/external \ + src/pcms/pythonapi \ + src/pcms/fortranapi # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded @@ -1044,7 +1002,8 @@ EXCLUDE_PATTERNS = # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories use the pattern */test/* -EXCLUDE_SYMBOLS = +EXCLUDE_SYMBOLS = detail \ + *::detail # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include @@ -1255,7 +1214,7 @@ IGNORE_PREFIX = # If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output # The default value is: YES. -GENERATE_HTML = YES +GENERATE_HTML = NO # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of @@ -1385,14 +1344,13 @@ HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. # The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. -HTML_TIMESTAMP = NO +TIMESTAMP = NO # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that @@ -2054,14 +2012,6 @@ LATEX_HIDE_INDICES = NO LATEX_BIB_STYLE = plain -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_TIMESTAMP = NO - # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # path from which the emoji images will be read. If a relative path is entered, # it will be relative to the LATEX_OUTPUT directory. If left blank the @@ -2178,7 +2128,7 @@ MAN_LINKS = NO # captures the structure of the code including all documentation. # The default value is: NO. -GENERATE_XML = NO +GENERATE_XML = YES # The XML_OUTPUT tag is used to specify where the XML pages will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of @@ -2289,7 +2239,7 @@ ENABLE_PREPROCESSING = YES # The default value is: NO. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -MACRO_EXPANSION = NO +MACRO_EXPANSION = YES # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then # the macro expansion is limited to the macros specified with the PREDEFINED and @@ -2297,7 +2247,7 @@ MACRO_EXPANSION = NO # The default value is: NO. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -EXPAND_ONLY_PREDEF = NO +EXPAND_ONLY_PREDEF = YES # If the SEARCH_INCLUDES tag is set to YES, the include files in the # INCLUDE_PATH will be searched if a #include is found. @@ -2330,7 +2280,21 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = +PREDEFINED = PCMS_ENABLE_SERVER \ + PCMS_ENABLE_CLIENT \ + PCMS_ENABLE_XGC \ + PCMS_ENABLE_OMEGA_H \ + PCMS_ENABLE_MFEM \ + PCMS_ENABLE_C \ + PCMS_ENABLE_TRANSIENT \ + PCMS_ENABLE_Fortran \ + PCMS_ENABLE_MESHFIELDS \ + PCMS_ENABLE_PETSC \ + KOKKOS_INLINE_FUNCTION= \ + KOKKOS_FUNCTION= \ + KOKKOS_FORCEINLINE_FUNCTION= \ + OMEGA_H_INLINE= \ + __attribute__(x)= # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/README.md b/README.md index be15e9bb9..be738c36d 100644 --- a/README.md +++ b/README.md @@ -1,274 +1,103 @@ # PCMS: Parallel Coupler For Multimodel Simulations + PCMS provides infrastructure to couple multiple simulation codes at scale on exascale systems. It uses scalable methodologies for transferring data between distributed applications. PCMS takes a hierarchical approach to field transfer using as much information as possible, such as mesh topology, to accommodate conservation and other physical constraints. In the context of limited information, alternative strategies, such as radial basis functions, are used to support field transfer. PCMS has been demonstrated on coupled fusion simulations that make use of volume coupling of high-dimensional data and complex coordinate systems. -Adios2-based xgc_coupler for XGC and GENE +## Run PCMS in a container -## Dependencies - -- CMake 3.19+ -- MPI -- Kokkos -- KokkosKernels -- redev 3.0.0+ (https://github.com/SCOREC/redev) -- Omega\_h 10.2.0+ with MPI enabled (https://github.com/SCOREC/omega_h) -- Catch2 2.\* (for unit tests) (https://github.com/catchorg/Catch2/tree/v2.13.8) - -## Build Instructions - -## Build with modules -SCOREC Rhel7 environment - -``` -module use /opt/scorec/spack/rhel9/v0201_4/lmod/linux-rhel9-x86_64/Core/ -module load gcc/12.3.0-iil3lno -module load mpich/4.1.1-xpoyz4t -module load cmake/3.26.3-2duxfcd -// no need to load CUDA if not using GPUs -module load cuda/12.1.1-zxa4msk -``` - -### Build dependencies with CPU +PCMS provides pre-built containers for x86\_64 and arm64. To use these containers, with docker you cand do the following: +```console +$ docker pull ghcr.io/scorec/pcms:develop +$ docker run --rm -it ghcr.io/scorec/pcms:develop bash ``` -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=20 \ - -DKokkos_ENABLE_SERIAL=ON \ - -DKokkos_ENABLE_OPENMP=OFF \ - -DKokkos_ENABLE_CUDA=OFF \ - -DKokkos_ENABLE_CUDA_LAMBDA=OFF \ - -DKokkos_ENABLE_CUDA_CONSTEXPR=OFF \ - -DBUILD_SHARED_LIBS=OFF -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=20 \ - -DKokkos_ROOT=$PWD/build-kokkos/install/lib64/cmake \ - -DBUILD_SHARED_LIBS=OFF -cmake --build build-kokkos-kernels --target install - -git clone git@github.com:SCOREC/omega_h.git -cmake -S omega_h -B build-omega_h \ - -DCMAKE_INSTALL_PREFIX=$PWD/build-omega_h/install \ - -DCMAKE_C_COMPILER=mpicc \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DBUILD_SHARED_LIBS=OFF \ - -DOmega_h_USE_Kokkos=ON \ - -DOmega_h_USE_CUDA=OFF \ - -DOmega_h_USE_MPI=ON \ - -DMPIEXEC_EXECUTABLE=mpirun \ - -DBUILD_TESTING=OFF \ - -DKokkos_PREFIX=$PWD/build-kokkos/install/lib64/cmake -cmake --build build-omega_h --target install - -git clone git@github.com:ornladios/ADIOS2.git -cmake -S ADIOS2 -B build-ADIOS2 \ - -DCMAKE_INSTALL_PREFIX=build-ADIOS2/install \ - -DADIOS2_USE_CUDA=OFF \ - -DADIOS2_USE_ZFP=off -cmake --build build-ADIOS2 --target install -git clone git@github.com:UO-OACISS/perfstubs.git -cmake -S perfstubs -B build-perfstubs \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DCMAKE_INSTALL_PREFIX=build-perfstubs/install -cmake --build build-perfstubs --target install +| Tag | Contents | +|---|---| +| `develop` | Latest build of `develop`, rebuilt at least weekly | +| `develop-YYYY-MM-DD` | build from specific date | -git clone git@github.com:catchorg/Catch2 -cmake -S Catch2 -B build-Catch2 \ - -DCMAKE_INSTALL_PREFIX=$PWD/build-Catch2/install -cmake --build build-Catch2 --target install +To run the Python examples, mount a checkout of this repository: -git clone git@github.com:SCOREC/redev.git -cmake -S redev -B build-redev \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DMPIEXEC_EXECUTABLE=mpirun \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_INSTALL_PREFIX=build-redev/install \ - -DADIOS2_DIR=$PWD/build-ADIOS2/install/lib64/cmake/adios2 \ - -Dperfstubs_DIR=$PWD/build-perfstubs/install/lib/cmake -cmake --build build-redev --target install -``` -### Build dependencies with CUDA +```console +$ git clone https://github.com/SCOREC/pcms.git +$ docker run --rm -v $PWD/pcms:/pcms -w /pcms ghcr.io/scorec/pcms:develop \ + python3 examples/python-api-example/example_conservative_transfer.py ``` -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=20 \ - -DCMAKE_BUILD_TYPE="Release" \ - -DCMAKE_CXX_COMPILER=$PWD/kokkos/bin/nvcc_wrapper \ - -DKokkos_ARCH_AMPERE80=ON \ - -DKokkos_ENABLE_SERIAL=ON \ - -DKokkos_ENABLE_OPENMP=off \ - -DKokkos_ENABLE_CUDA=on \ - -DKokkos_ENABLE_CUDA_LAMBDA=on \ - -DKokkos_ENABLE_CUDA_CONSTEXPR=on \ - -DKokkos_ENABLE_DEBUG=off -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=20 \ - -DKokkos_ROOT=$PWD/build-kokkos/install/lib64/cmake \ - -DBUILD_SHARED_LIBS=off -cmake --build build-kokkos-kernels --target install +If you run into issues due to containers running as root, you may need to use the following environment variables for OpenMPI to work. +As with any container running as root, exercise caution. +`mpirun`: -git clone git@github.com:SCOREC/omega_h.git -cmake -S omega_h -B build-omega_h \ - -DCMAKE_INSTALL_PREFIX=build-omega_h/install \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_SHARED_LIBS=off \ - -DOmega_h_USE_Kokkos=ON \ - -DOmega_h_USE_CUDA=on \ - -DOmega_h_CUDA_ARCH=80 \ - -DOmega_h_USE_MPI=on \ - -DMPIEXEC_EXECUTABLE=mpirun \ - -DBUILD_TESTING=off \ - -DCMAKE_C_COMPILER=mpicc \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DKokkos_PREFIX=$PWD/build-kokkos/install/lib64/cmake -cmake --build build-omega_h --target install +```console +$ docker run --rm -e OMPI_ALLOW_RUN_AS_ROOT=1 -e OMPI_ALLOW_RUN_AS_ROOT_CONFIRM=1 \ + ghcr.io/scorec/pcms:develop mpirun -n 2 python3 -c "import pcms" +``` -git clone git@github.com:ornladios/ADIOS2.git -cmake -S ADIOS2 -B build-ADIOS2 \ - -DCMAKE_INSTALL_PREFIX=build-ADIOS2/install \ - -DADIOS2_USE_CUDA=on \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DCMAKE_C_COMPILER=mpicc \ - -DADIOS2_USE_ZFP=off -cmake --build build-ADIOS2 --target install +More details about the container build process can be found here +[scorec-spack-packages](https://github.com/SCOREC/scorec-spack-packages#pcms-container-image). -git clone git@github.com:UO-OACISS/perfstubs.git -cmake -S perfstubs -B build-perfstubs \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DCMAKE_INSTALL_PREFIX=build-perfstubs/install -cmake --build build-perfstubs --target install +## Build Instructions -git clone git@github.com:SCOREC/redev.git -cmake -S redev -B build-redev \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DMPIEXEC_EXECUTABLE=mpirun \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_SHARED_LIBS=OFF \ - -DCMAKE_INSTALL_PREFIX=build-redev/install \ - -DADIOS2_DIR=$PWD/build-ADIOS2/install/lib64/cmake/adios2 \ - -Dperfstubs_DIR=$PWD/build-perfstubs/install/lib/cmake -cmake --build build-redev --target install +There are two supported ways to build PCMS from source: -git clone git@github.com:catchorg/Catch2 -cmake -S Catch2 -B build-Catch2 \ - -DCMAKE_INSTALL_PREFIX=$PWD/build-Catch2/install -cmake --build build-Catch2 --target install -``` -### Build, install, and test pcms -``` -git clone git@github.com:jacobmerson/pcms_testcases.git // test data -git clone git@github.com:SCOREC/pcms.git +- [Spack](#build-with-spack) works on any Linux or macOS system. +- On [SCOREC systems](#build-on-scorec-systems), use the module-based scripts. -cmake -S pcms -B build-pcms \ - -DCMAKE_C_COMPILER=mpicc \ - -DCMAKE_CXX_COMPILER=mpicxx \ - -DCMAKE_Fortran_COMPILER=mpifort \ - -DCMAKE_BUILD_TYPE="Debug" \ - -DPCMS_TIMEOUT=100 \ - -Dredev_DIR=$PWD/build-redev/install/lib64/cmake/redev \ - -DOmega_h_DIR=$PWD/build-omega_h/install/lib64/cmake/Omega_h/ \ - -Dperfstubs_DIR=$PWD/build-perfstubs/install/lib/cmake \ - -DCatch2_DIR=$PWD/build-Catch2/install/lib64/cmake/Catch2/ \ - -DKokkosKernels_DIR=$PWD/build-kokkos-kernels/install/lib64/cmake/KokkosKernels/ \ - -DPCMS_TEST_DATA_DIR=$PWD/pcms_testcases -cmake --build build-pcms -j 8 +## Build with Spack -ctest --test-dir build-pcms --output-on-failure -``` +The environment in [`spack/spack.yaml`](spack/spack.yaml) builds PCMS from +your checkout. It fetches the PCMS +recipes from [scorec-spack-packages](https://github.com/SCOREC/scorec-spack-packages), +pins the version of the Spack package repository, and installs prebuilt +dependencies from the SCOREC build cache when they match your system +(Ubuntu 24.04 on x86\_64 or arm64). Elsewhere, Spack builds the dependencies +from source the first time. -## Spack based build -1. Install spack +1. Install Spack 1.2 or newer: ```console - $ mkdir $HOME/$USER/spack - $ cd $HOME/$USER/spack - $ git clone -c feature.manyFiles=true -b v1.0.4 https://github.com/spack/spack.git + $ git clone --depth=2 -b releases/v1.2 https://github.com/spack/spack.git $ . spack/share/spack/setup-env.sh - $ spack repo add https://github.com/spack/spack-packages ``` - We can also add the spack setup line into the `~/.bashrc` with `echo ". spack/share/spack/setup-env.sh" >> ~/.bashrc"`. This will load the spack setup script every time we start our terminal session. -2. Get PCMS spack repo - The following commands will add the pcms recipe files to spack. They are not currently installed inthe upstream spack repository. +2. Spack needs C, C++, and Fortran compilers. On Ubuntu, install them with + `sudo apt install gcc g++ gfortran`. Spack finds them on the first build; + `spack compiler list` shows what it found. +3. Build and install PCMS: ```console - $ git clone https://github.com/jacobmerson/pcms-spack.git - $ spack repo add pcms-spack/spack_repo/pcms - ``` -3. Install compilers / add compilers to spack - ```console - $ sudo apt install gcc g++ gfortran - $ spack compiler find - ``` - You should see a message stating that a compiler was added to your `packages.yaml` file. You can confirm this by looking in your spack packages with `spack config edit packages`. It should contain something similar to the following: - ```yaml - packages: - gcc: - externals: - - spec: gcc@13.3.0 languages:='c,c++,fortran' - prefix: /usr - extra_attributes: - compilers: - c: /usr/bin/gcc - cxx: /usr/bin/g++ - fortran: /usr/bin/gfortran + $ git clone https://github.com/SCOREC/pcms.git + $ cd pcms + $ spack env activate ./spack + $ spack install ``` - -4. Install PCMS repo - ```console - $ mkdir $HOME/$USER/pcms-coupler - $ cd $HOME/$USER/pcms-coupler - $ git clone https://github.com/SCOREC/pcms.git - $ cd pcms/spack - $ spack env create -d env spack.yaml - $ cd env - $ spack env activate . - $ spack install - ``` - -If work in SCOREC system, you can directly use the spack-scorec.yaml which contains compilers information. At this point hopefully, spack will now install all of the relavant dependencies and a baseline build of PCMS. The default environment has PCMS in develop mode. To modify and recompile PCMS you can modify the code and rerun `spack install`. +This environment has PCMS in *develop* mode after changing the source, you can run +`spack install` again to rebuild it. With the environment active, its view is on your `PATH`, so +`python3 -c "import pcms"` uses the Python bindings. -4. Python API (optional) - If you want to use the python API, you can install the `pcms+python` package with spack instaed of `pcms`. This will install the python bindings for PCMS and all of the dependencies needed to use them. - ```console - $ spack remove pcms - $ spack add pcms+python - $ spack install - ``` +To build a different configuration, edit the spec in `spack/spack.yaml` and +run `spack concretize -f` before `spack install`. For example: - 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 - ``` +- `pcms@develop` builds without the optional features (Python, PETSc, NetCDF). +- `+cuda cuda_arch=80` (or `+rocm amdgpu_target=gfx90a`) builds for GPUs. + Also add `~tests` if the build machine has no GPU, because the tests are + run during the build. - Test the python API with `python -c "import pcms; print(pcms.__version__)"`. You should see the version of PCMS printed out without any errors. +`spack info pcms` lists available variants. - Point-evaluator construction in Python uses `EvaluationRequest`, for - example: - ```python - request = pcms.EvaluationRequest.from_coordinates(eval_coords) - evaluator = source_space.create_point_evaluator(request) +On the SCOREC RHEL9 machines, PCMS and its dependencies are built with the +system modules and the scripts in +[laces-lab/software-dependencies](https://github.com/laces-lab/software-dependencies). +Follow the instructions there. - 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. +## Python API -Details instructions for a few systems are available on the wiki. +Point-evaluator construction in Python uses `EvaluationRequest`, for example: -Another Executing Approach: One would also comment out the BUILDING_TESTING in CMakeFiles.txt included in test folder; +```python +request = pcms.EvaluationRequest.from_coordinates(eval_coords) +evaluator = source_space.create_point_evaluator(request) -Assign the full path of testdatas to test_dir in the test_init.cc file; Use "mpirun -np 4 bin/test_init" for the execution. +optimized_request = pcms.EvaluationRequest.from_function_space(target_space) +evaluator = source_space.create_point_evaluator(optimized_request) +``` ## Creating Archive for Release `git archive --format=tar.gz -o /tmp/pcms.tar.gz --prefix=pcms/ develop` diff --git a/cdash/nightly.cmake b/cdash/nightly.cmake index 87e49b0c6..c53abe9c9 100644 --- a/cdash/nightly.cmake +++ b/cdash/nightly.cmake @@ -12,7 +12,7 @@ set(CTEST_DROP_LOCATION "/submit.php?project=SCOREC") set(CTEST_DROP_SITE_CDASH TRUE) set(CTEST_BUILD_NAME "linux-gcc-${CTEST_BUILD_CONFIGURATION}") -set(CTEST_DASHBOARD_ROOT "/users/yus9/lore.scorec.rpi.edu/nightlyBuilds/pcms_build/" ) +set(CTEST_DASHBOARD_ROOT "/users/d_zxg06726/nightlyBuilds/pcms_build/" ) set(CTEST_CMAKE_GENERATOR "Unix Makefiles") set(CTEST_BUILD_FLAGS -j4) @@ -184,8 +184,15 @@ SET(CONFIGURE_OPTIONS "-DCMAKE_CXX_COMPILER=mpicxx" "-DCMAKE_C_COMPILER=mpicc" "-DPCMS_TIMEOUT=100" + "-DPCMS_ENABLE_PETSC=ON" + "-DPETSC_LINK_STATIC=ON" + "-DPETSC_DIR=${CTEST_DASHBOARD_ROOT}/petsc" + "-DPETSC_ARCH=cuda-kokkos" "-Dredev_DIR=${CTEST_DASHBOARD_ROOT}/build-redev/install/lib64/cmake/redev" "-DOmega_h_DIR=${CTEST_DASHBOARD_ROOT}/build-omega_h/install/lib64/cmake/Omega_h/" + "-DADIOS2_DIR=${CTEST_DASHBOARD_ROOT}/build-ADIOS2/install/lib64/cmake/adios2" + "-DKokkos_DIR=${CTEST_DASHBOARD_ROOT}/build-kokkos/install/lib64/cmake/Kokkos" + "-Dmeshfields_DIR=${CTEST_DASHBOARD_ROOT}/build-meshFields/install/lib64/cmake/meshfields" "-Dperfstubs_DIR=${CTEST_DASHBOARD_ROOT}/build-perfstubs/install/lib/cmake" "-DCatch2_DIR=${CTEST_DASHBOARD_ROOT}/build-Catch2/install/lib64/cmake/Catch2/" "-DKokkosKernels_DIR=${CTEST_DASHBOARD_ROOT}/build-kokkos-kernels/install/lib64/cmake/KokkosKernels/" diff --git a/cdash/nightly.sh b/cdash/nightly.sh index 05c3743a8..738465716 100755 --- a/cdash/nightly.sh +++ b/cdash/nightly.sh @@ -1,6 +1,11 @@ #!/bin/bash -x +( +#cdash output root +d=/users/d_zxg06726/nightlyBuilds/pcms_build +exec > $d/nightly_log.txt 2>&1 + source /etc/profile -source /users/yus9/.bash_profile +# source /users/d_zxg06726/.bash_profile #setup lmod export PATH=/usr/share/lmod/lmod/libexec:$PATH @@ -8,14 +13,15 @@ export PATH=/usr/share/lmod/lmod/libexec:$PATH #setup spack modules unset MODULEPATH -module use /opt/scorec/spack/rhel9/v0201_4/lmod/linux-rhel9-x86_64/Core/ -module load gcc/12.3.0-iil3lno -module load mpich/4.1.1-xpoyz4t -module load cmake/3.26.3-2duxfcd -module load cuda/12.1.1-zxa4msk +module use /opt/scorec/spack/rhel9/v0222_2/lmod/linux-rhel9-x86_64/Core/ +module load gcc/13.2.0-4eahhas +module load mpich/4.2.3-62uy3hd +module load cmake/3.30.5-5e54py4 +module load cuda/12.6.2-gqq65nw +module load openblas/0.3.28-eubd5ed +export LD_LIBRARY_PATH=/opt/scorec/spack/rhel9/v0222_2/install/linux-rhel9-x86_64_v3/gcc-13.2.0/mpich-4.2.3-62uy3hdwqe75fjfskrrysco5i6gqjblv/lib:$LD_LIBRARY_PATH +export PETSC_OPTIONS="-use_gpu_aware_mpi 0" -#cdash output root -d=/users/yus9/lore.scorec.rpi.edu/nightlyBuilds/pcms_build cd $d #remove compilation directories created by previous nightly.cmake runs [ -d build ] && rm -rf build/ @@ -31,7 +37,6 @@ cmake -S kokkos -B build-kokkos \ -DKokkos_ENABLE_SERIAL=ON \ -DKokkos_ENABLE_OPENMP=off \ -DKokkos_ENABLE_CUDA=on \ - -DKokkos_ENABLE_CUDA_LAMBDA=on \ -DKokkos_ENABLE_CUDA_CONSTEXPR=on \ -DKokkos_ENABLE_DEBUG=off cmake --build build-kokkos -j 8 --target install @@ -46,11 +51,10 @@ cmake -S kokkos-kernels -B build-kokkos-kernels \ cmake --build build-kokkos-kernels -j 8 --target install #install ADIOS2 -[ ! -d ADIOS2 ] && git clone https://github.com/ornladios/ADIOS2.git -cd ADIOS2 && git pull && cd - +[ ! -d ADIOS2 ] && git clone --branch v2.10.2 https://github.com/ornladios/ADIOS2.git [ -d build-ADIOS2 ] && rm -rf build-ADIOS2 cmake -S ADIOS2 -B build-ADIOS2 \ - -DCMAKE_INSTALL_PREFIX=build-ADIOS2/install \ + -DCMAKE_INSTALL_PREFIX=$d/build-ADIOS2/install \ -DADIOS2_USE_CUDA=on \ -DCMAKE_CXX_COMPILER=mpicxx \ -DCMAKE_C_COMPILER=mpicc \ @@ -60,11 +64,11 @@ cmake --build build-ADIOS2 -j 8 --target install #install perfstubs [ ! -d perfstubs ] && git clone https://github.com/UO-OACISS/perfstubs.git -cd perfstubs && git pull && cd - +cd perfstubs && git checkout a1fa3feb1d89214e28047f166500351074b5f0c2 && cd - [ -d build-perfstubs ] && rm -rf build-perfstubs cmake -S perfstubs -B build-perfstubs \ -DCMAKE_CXX_COMPILER=mpicxx \ - -DCMAKE_INSTALL_PREFIX=build-perfstubs/install + -DCMAKE_INSTALL_PREFIX=$d/build-perfstubs/install cmake --build build-perfstubs -j 8 --target install #install redev @@ -100,14 +104,39 @@ cmake -S omega_h -B build-omega_h \ -DKokkos_PREFIX=$d/build-kokkos/install/lib64/cmake cmake --build build-omega_h -j 8 --target install +#install meshFields +[ ! -d meshFields ] && git clone https://github.com/SCOREC/meshFields +cd meshFields && git pull && cd - +[ -d build-meshFields ] && rm -rf build-meshFields +cmake -S meshFields -B build-meshFields \ + -DCMAKE_INSTALL_PREFIX=build-meshFields/install \ + -DCMAKE_CXX_COMPILER=mpicxx \ + -DCMAKE_C_COMPILER=mpicc \ + -DMPIEXEC_EXECUTABLE=mpirun \ + -DKokkos_DIR=$d/build-kokkos/install/lib64/cmake/Kokkos \ + -DOmega_h_DIR=$d/build-omega_h/install/lib64/cmake/Omega_h +cmake --build build-meshFields -j 8 --target install + #install Catch2 -[ ! -d Catch2 ] && git clone https://github.com/catchorg/Catch2 -cd Catch2 && git pull && cd - +[ ! -d Catch2 ] && git clone --branch v3.11.0 https://github.com/catchorg/Catch2 [ -d build-Catch2 ] && rm -rf build-Catch2 cmake -S Catch2 -B build-Catch2 \ -DCMAKE_INSTALL_PREFIX=$d/build-Catch2/install cmake --build build-Catch2 -j 8 --target install +[ ! -d petsc ] && git clone --branch v3.24.2 https://gitlab.com/petsc/petsc.git +cd petsc +[ -d cuda-kokkos ] && rm -rf cuda-kokkos +./configure \ + PETSC_ARCH=cuda-kokkos \ + --with-kokkos-dir=$d/build-kokkos/install/ \ + --with-kokkos-kernels-dir=$d/build-kokkos-kernels/install/ \ + --with-cuda=1 \ + --with-shared-libraries=0 \ + --with-openblas-dir="${OPENBLAS_RHEL9_ROOT}" +make all +cd - + #download testcases [ ! -d pcms_testcases ] && git clone https://github.com/jacobmerson/pcms_testcases.git cd pcms_testcases && git pull && cd - @@ -116,3 +145,4 @@ touch $d/startedCoreNightly #run nightly.cmake script ctest -V --script $d/nightly.cmake touch $d/doneCoreNightly +) diff --git a/cmake/FindPETSc.cmake b/cmake/FindPETSc.cmake index b556bb74e..e54c68c59 100644 --- a/cmake/FindPETSc.cmake +++ b/cmake/FindPETSc.cmake @@ -1,6 +1,6 @@ find_package(PkgConfig REQUIRED QUIET) -if(DEFINED PETSC_DIR) - if(DEFINED PETSC_ARCH) +if(PETSC_DIR) + if(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}") @@ -16,6 +16,11 @@ if(_petsc_FOUND AND _petsc_VERSION) set(PETSC_VERSION ${_petsc_VERSION}) endif() +# set PETSc paths when found through pkg-config +if(_petsc_FOUND AND NOT PETSC_DIR) + set(PETSC_DIR "${_petsc_PREFIX}") +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 diff --git a/config.cmake.in b/config.cmake.in index bf8b4882e..54ef5c8f7 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -29,7 +29,14 @@ if(@PCMS_ENABLE_OMEGA_H@) find_dependency(Omega_h CONFIG HINTS @Omega_h_DIR@) endif() +if(@PCMS_ENABLE_MFEM@) + find_dependency(MFEM CONFIG HINTS @MFEM_DIR@) +endif() + include("${CMAKE_CURRENT_LIST_DIR}/pcms_utility-targets.cmake") +if(@PCMS_ENABLE_TRANSIENT@) + include("${CMAKE_CURRENT_LIST_DIR}/pcms_transient-targets.cmake") +endif() 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") @@ -49,6 +56,10 @@ if(@PCMS_ENABLE_Fortran@) include("${CMAKE_CURRENT_LIST_DIR}/pcms_fortranapi-targets.cmake") endif() +if(@PCMS_ENABLE_ArborX@) + find_dependency(ArborX CONFIG HINTS @ArborX_DIR@) +endif() + include("${CMAKE_CURRENT_LIST_DIR}/pcms-targets.cmake") # must be called at the end of the config file check_required_components(pcms) diff --git a/ctest/removeBpFiles.cmake b/ctest/removeBpFiles.cmake deleted file mode 100644 index 8b58a8e79..000000000 --- a/ctest/removeBpFiles.cmake +++ /dev/null @@ -1,27 +0,0 @@ -cmake_minimum_required(VERSION 3.15.0...3.21.0) -file( - GLOB bpDirs - LIST_DIRECTORIES TRUE - *.bp*) -message(STATUS "before bpDirs ${bpDirs}") -if(bpDirs) - file(REMOVE_RECURSE ${bpDirs}) - file( - GLOB bpDirs - LIST_DIRECTORIES TRUE - *.bp*) - message(STATUS "after bpDirs ${bpDirs}") -endif() -file( - GLOB sstDirs - LIST_DIRECTORIES TRUE - *.sst*) -message(STATUS "before sstDirs ${sstDirs}") -if(sstDirs) - file(REMOVE_RECURSE ${sstDirs}) - file( - GLOB sstDirs - LIST_DIRECTORIES TRUE - *.sst*) - message(STATUS "after sstDirs ${sstDirs}") -endif() diff --git a/ctest/runMultipleMpiJobs.sh b/ctest/runMultipleMpiJobs.sh index f19bbb8cb..9a2215149 100755 --- a/ctest/runMultipleMpiJobs.sh +++ b/ctest/runMultipleMpiJobs.sh @@ -1,9 +1,14 @@ #!/bin/bash -x -usage="Usage: ... " +usage="Usage: [--clean-adios2] ... " if [[ ($# == 0) || ("${1}" == "-h" || ${1} == "--help") ]]; then echo $usage exit 1 fi +clean=0 +if [[ "${1}" == "--clean-adios2" ]]; then + clean=1 + shift +fi runCmd=${1} preFlags=${2} numProcsFlag=${3} @@ -42,6 +47,10 @@ run() { LOGS+=(${name}.log) } +if [[ ${clean} == 1 ]]; then + rm -rf ./*.bp* ./*.sst* +fi + run $@ shift 4 run $@ diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 1002a7985..000000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,87 +0,0 @@ -# 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/docs/approach.md b/docs/approach.md new file mode 100644 index 000000000..1e07748e5 --- /dev/null +++ b/docs/approach.md @@ -0,0 +1,2 @@ +```{include} ../APPROACH.md +``` diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..139a20b15 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,74 @@ +# Sphinx configuration for the PCMS documentation. +# +# Build from the repository root with: +# pip install -r docs/requirements.txt +# sphinx-build -b html docs docs/_build/html +# +# The C++ API is generated by Doxygen (XML only) and rendered with +# Breathe/Exhale. The Python API is rendered with autodoc from the compiled +# ``pcms`` module, which must be importable (e.g. on PYTHONPATH). + +import pathlib +import subprocess + +from sphinx.util import logging + +logger = logging.getLogger(__name__) + +docs_dir = pathlib.Path(__file__).parent.resolve() +repo_root = docs_dir.parent + +project = "PCMS" +author = "SCOREC" +copyright = "Rensselaer Polytechnic Institute" + +extensions = [ + "breathe", + "exhale", + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.mathjax", +] + +source_suffix = {".rst": "restructuredtext", ".md": "markdown"} +exclude_patterns = ["_build", "_doxygen"] + +html_theme = "sphinx_rtd_theme" + +# -- C++ API ----------------------------------------------------------------- + +# The Doxyfile at the repository root writes XML to docs/_doxygen/xml. +subprocess.run(["doxygen", "Doxyfile"], cwd=repo_root, check=True) + +breathe_projects = {"pcms": str(docs_dir / "_doxygen" / "xml")} +breathe_default_project = "pcms" +breathe_default_members = ("members", "undoc-members") + +exhale_args = { + "containmentFolder": "./api", + "rootFileName": "library_root.rst", + "rootFileTitle": "C++ API", + "doxygenStripFromPath": str(repo_root), + "createTreeView": True, + "exhaleExecutesDoxygen": False, +} + +primary_domain = "cpp" + +# The C API declares its handles as `typedef struct X X;`, which the C++ +# domain reports as duplicate declarations. +suppress_warnings = ["duplicate_declaration.cpp"] + +# -- Python API -------------------------------------------------------------- + +try: + import pcms # noqa: F401 + + tags.add("pcms_python") # noqa: F821 (provided by Sphinx) +except ImportError: + logger.warning( + "the pcms Python module is not importable; the Python API page will " + "be empty. Build PCMS with -DPCMS_ENABLE_Python=ON and add it to " + "PYTHONPATH." + ) + autodoc_mock_imports = ["pcms"] diff --git a/docs/docutils.conf b/docs/docutils.conf new file mode 100644 index 000000000..bf83cf49a --- /dev/null +++ b/docs/docutils.conf @@ -0,0 +1,3 @@ +[parsers] +# Exhale writes the API tree view as very long lines. +line_length_limit = 1000000 diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..41f4d38e8 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,14 @@ +PCMS +==== + +PCMS (Parallel Coupler for Multimodel Simulations) couples parallel +simulation codes by transferring fields between them. + +.. toctree:: + :maxdepth: 2 + :caption: Contents + + readme + approach + python_api + api/library_root diff --git a/docs/python_api.rst b/docs/python_api.rst new file mode 100644 index 000000000..409e483ee --- /dev/null +++ b/docs/python_api.rst @@ -0,0 +1,13 @@ +Python API +========== + +.. only:: not pcms_python + + .. note:: + + These docs were built without the ``pcms`` Python module, so the Python + API is not shown. + +.. automodule:: pcms + :members: + :undoc-members: diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 000000000..451bedaec --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,2 @@ +```{include} ../README.md +``` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..52317a977 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +Sphinx==9.1.0 +sphinx-rtd-theme==3.1.0 +breathe==5.1.0 +exhale==0.3.7 +myst-parser==5.1.0 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 000000000..14e1787e9 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,5 @@ +# In-tree examples. external-usage-example is a standalone project built +# against an installed pcms and is not added here. +if(PCMS_ENABLE_OMEGA_H) + add_subdirectory(coordinate-transformation-example) +endif() diff --git a/examples/coordinate-transformation-example/CMakeLists.txt b/examples/coordinate-transformation-example/CMakeLists.txt new file mode 100644 index 000000000..fde66cb0b --- /dev/null +++ b/examples/coordinate-transformation-example/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(coordinate_transformations coordinate_transformations.cpp) +target_link_libraries(coordinate_transformations PRIVATE pcms::field + pcms::transfer) diff --git a/examples/coordinate-transformation-example/coordinate_transformations.cpp b/examples/coordinate-transformation-example/coordinate_transformations.cpp new file mode 100644 index 000000000..ea2a923a8 --- /dev/null +++ b/examples/coordinate-transformation-example/coordinate_transformations.cpp @@ -0,0 +1,202 @@ +// Coordinate transformations in PCMS. +// +// (a) map points and re-express vector components with a CoordinateMap +// (b) transfer where the source points are cylindrical but both fields store +// Cartesian components: only the points are mapped +// (c) transfer where the source points and components are both cylindrical +// and the target's are Cartesian: points are mapped, values rotated +// +// Each case prints the maximum error against the exact answer. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace pcms; +using DeviceView = Kokkos::View; +using HostView = Kokkos::View; + +namespace +{ + +DeviceView ToDevice(const std::vector& flat, int width) +{ + const int n = static_cast(flat.size()) / width; + HostView host("host", n, width); + for (int i = 0; i < n; ++i) + for (int d = 0; d < width; ++d) + host(i, d) = flat[i * width + d]; + DeviceView dev("dev", n, width); + Kokkos::deep_copy(dev, host); + return dev; +} + +HostView ToHost(Rank2View v) +{ + DeviceView dev("dev", v.extent(0), v.extent(1)); + ConvertMismatchLayoutView2D(dev, v); + HostView host("host", v.extent(0), v.extent(1)); + DeepCopyMismatchLayouts(host, dev); + return host; +} + +ValueBasis CylindricalVector() +{ + return {csys::CylindricalRThetaZ::Create(), ComponentScaling::Physical, + values::Vector}; +} + +/// Sets a 3-component field from f(p0, p1, p2, out) at its DOF holders, where +/// p are the holder coordinates in the field's space. +template +void FillField(Field& field, F f) +{ + auto p = ToHost(field.GetLayout().GetDOFHolderCoordinates().GetValues()); + const int n = static_cast(p.extent(0)); + HostView data("data", n, 3); + for (int i = 0; i < n; ++i) + f(p(i, 0), p(i, 1), p(i, 2), &data(i, 0)); + field.SetDOFHolderDataHost(ValueView( + field.GetData().GetValueBasis(), MakeConstRank2View(data))); +} + +/// Max over the field's DOF holders of |b - (x, y, z)|. +Real MaxErrorFromXYZ(const Field& b) +{ + auto x = ToHost(b.GetLayout().GetDOFHolderCoordinates().GetValues()); + auto v = b.GetDOFHolderDataHost().GetValues(); + Real err = 0.0; + for (size_t i = 0; i < x.extent(0); ++i) + for (int d = 0; d < 3; ++d) + err = std::fmax(err, std::fabs(v(i, d) - x(i, d))); + return err; +} + +/// Source: a box whose coordinates are (r, theta, z) with r in [0, 2] and +/// theta in [0, 1.6]. Target: the Cartesian unit cube, which lies inside it. +void BuildSpaces(Omega_h::Library& lib, Omega_h::Mesh& src_mesh, + Omega_h::Mesh& tgt_mesh, + std::shared_ptr& src, + std::shared_ptr& tgt) +{ + src_mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 2.0, 1.6, 1.1, 8, + 8, 6, false); + src = LagrangeFunctionSpace::FromMesh( + src_mesh, 1, 3, csys::CylindricalRThetaZ::Create(), "global", + LagrangeFunctionSpace::Backend::OmegaH); + tgt_mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1.0, 1.0, 1.0, 4, + 4, 4, false); + tgt = LagrangeFunctionSpace::FromMesh(tgt_mesh, 1, 3, + csys::Cartesian::Create(3), "global", + LagrangeFunctionSpace::Backend::OmegaH); +} + +void CaseA() +{ + std::puts("(a) coordinate transformation"); + auto xyz = ToDevice({1.0, 0.0, 0.5, 0.0, 2.0, -1.0, 1.0, 1.0, 0.0}, 3); + auto bound = CartesianToCylindrical{}.Bind(CoordinateView( + csys::Cartesian::Create(3), MakeConstRank2View(xyz))); + auto rtz = ToHost(bound->MappedPoints().GetValues()); + + // Values run opposite to points: this map re-expresses cylindrical + // components as Cartesian ones, in place. Here, e_theta at each point. + auto v = ToDevice({0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0}, 3); + auto v_cart = bound->TransformValues( + ValueView(CylindricalVector(), MakeRank2View(v))); + auto vh = ToHost(MakeConstRank2View(v)); + Real err = 0.0; + for (int i = 0; i < 3; ++i) { + err = std::fmax(err, std::fabs(vh(i, 0) + std::sin(rtz(i, 1)))); + err = std::fmax(err, std::fabs(vh(i, 1) - std::cos(rtz(i, 1)))); + std::printf(" (r,th,z)=(%.3f, %.3f, %.3f) e_theta=(%.3f, %.3f, %.3f)\n", + rtz(i, 0), rtz(i, 1), rtz(i, 2), vh(i, 0), vh(i, 1), vh(i, 2)); + } + std::printf(" output basis '%s', max |e_theta - (-sin, cos, 0)| = %.2e\n", + std::string(v_cart.GetBasis().system->Kind()).c_str(), err); +} + +void CaseB(Omega_h::Library& lib) +{ + std::puts("(b) cylindrical points, Cartesian components on both sides"); + Omega_h::Mesh src_mesh(&lib), tgt_mesh(&lib); + std::shared_ptr src, tgt; + BuildSpaces(lib, src_mesh, tgt_mesh, src, tgt); + + // A borrowed basis: the space's points are (r, theta, z), but the field's + // components are Cartesian. B = (x, y, z) = (r cos theta, r sin theta, z). + auto b_src = + src->CreateFunction("b", values::Vector, csys::Cartesian::Create(3)); + FillField(b_src, [](Real r, Real theta, Real z, Real* out) { + out[0] = r * std::cos(theta); + out[1] = r * std::sin(theta); + out[2] = z; + }); + auto b_tgt = tgt->CreateFunction("b", values::Vector); + + // The values are already in the target's basis, so the map passes them + // through and only the points are mapped. + TransformedSource src_as_cartesian( + src, std::make_shared()); + Interpolator op(src_as_cartesian, *tgt); + op.Apply(b_src, b_tgt); + // Not exact: B's Cartesian components are not linear in (r, theta, z), so + // the P1 source field only approximates them. + std::printf(" via Interpolator: max |B - (x,y,z)| = %.2e\n", + MaxErrorFromXYZ(b_tgt)); +} + +void CaseC(Omega_h::Library& lib) +{ + std::puts("(c) cylindrical points and components -> Cartesian"); + Omega_h::Mesh src_mesh(&lib), tgt_mesh(&lib); + std::shared_ptr src, tgt; + BuildSpaces(lib, src_mesh, tgt_mesh, src, tgt); + + // B = r e_r + z e_z in physical cylindrical components, which is (x, y, z) + // in Cartesian ones. Its cylindrical components are linear in + // (r, theta, z), so the transfer is exact. + auto b_src = + src->CreateFunction("b", values::Vector, ComponentScaling::Physical); + FillField(b_src, [](Real r, Real, Real z, Real* out) { + out[0] = r; + out[1] = 0.0; + out[2] = z; + }); + auto b_tgt = tgt->CreateFunction("b", values::Vector); + + // `to_source` must end in the source's system: it carries the target's + // Cartesian points to (r, theta, z), and the source's cylindrical + // components back to Cartesian. + TransformedSource src_as_cartesian( + src, std::make_shared()); + Interpolator op(src_as_cartesian, *tgt); + op.Apply(b_src, b_tgt); + std::printf(" max |B - (x,y,z)| = %.2e\n", MaxErrorFromXYZ(b_tgt)); +} + +} // namespace + +int main(int argc, char** argv) +{ + try { + Omega_h::Library lib(&argc, &argv); + CaseA(); + CaseB(lib); + CaseC(lib); + } catch (const std::exception& e) { + std::fprintf(stderr, "error: %s\n", e.what()); + return 1; + } + return 0; +} diff --git a/examples/python-api-example/coordinate_transformations.py b/examples/python-api-example/coordinate_transformations.py new file mode 100644 index 000000000..4f19ee920 --- /dev/null +++ b/examples/python-api-example/coordinate_transformations.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Coordinate transformations in the PCMS Python API. + + (a) map points and re-express vector components with a CoordinateMap + (b) transfer where the source points are cylindrical but both fields store + Cartesian components: only the points are mapped + (c) transfer where the source points and components are both cylindrical + and the target's are Cartesian: points are mapped, values rotated + +Each case prints the maximum error against the exact answer. This is the +Python version of examples/coordinate-transformation-example. +""" +import sys + +import numpy as np +import pcms +import PyOmega_h as omega_h + + +def cylindrical_vector(): + return pcms.ValueBasis(pcms.csys.CylindricalRThetaZ.create(), + pcms.ComponentScaling.Physical, pcms.values.Vector) + + +def fill_field(field, f): + """Set a 3-component field from f(p) at its DOF holders, where p are the + holder coordinates (num_dof_holders × 3) in the field's space.""" + p = field.get_layout().get_dof_holder_coordinates().get_values() + field.set_dof_holder_data( + pcms.ValueView(field.get_data().get_value_basis(), f(p))) + + +def max_error_from_xyz(b): + """Max over the field's DOF holders of |b - (x, y, z)|.""" + x = b.get_layout().get_dof_holder_coordinates().get_values() + return np.max(np.abs(b.get_dof_holder_data().get_values() - x)) + + +def build_spaces(world): + """Source: a box whose coordinates are (r, theta, z) with r in [0, 2] and + theta in [0, 1.6]. Target: the Cartesian unit cube, which lies inside + it.""" + src_mesh = omega_h.build_box(world, omega_h.Family.SIMPLEX, 2.0, 1.6, 1.1, + 8, 8, 6, False) + src = pcms.LagrangeFunctionSpace.from_mesh( + src_mesh, 1, 3, pcms.csys.CylindricalRThetaZ.create(), + backend=pcms.LagrangeFunctionSpace.Backend.OmegaH) + tgt_mesh = omega_h.build_box(world, omega_h.Family.SIMPLEX, 1.0, 1.0, 1.0, + 4, 4, 4, False) + tgt = pcms.LagrangeFunctionSpace.from_mesh( + tgt_mesh, 1, 3, pcms.csys.Cartesian.create(3), + backend=pcms.LagrangeFunctionSpace.Backend.OmegaH) + return src, tgt + + +def case_a(): + print("(a) coordinate transformation") + xyz = pcms.CoordinateView(pcms.csys.Cartesian.create(3), + [[1.0, 0.0, 0.5], [0.0, 2.0, -1.0], + [1.0, 1.0, 0.0]]) + bound = pcms.CartesianToCylindrical().bind(xyz) + rtz = bound.mapped_points().get_values() + + # Values run opposite to points: this map re-expresses cylindrical + # components as Cartesian ones, in place. Here, e_theta at each point. + v = np.tile([0.0, 1.0, 0.0], (3, 1)) + v_cart = bound.transform_values(pcms.ValueView(cylindrical_vector(), v)) + theta = rtz[:, 1] + err = max(np.max(np.abs(v[:, 0] + np.sin(theta))), + np.max(np.abs(v[:, 1] - np.cos(theta)))) + for p, e in zip(rtz, v): + print(" (r,th,z)=(%.3f, %.3f, %.3f) e_theta=(%.3f, %.3f, %.3f)" + % (*p, *e)) + print(" output basis '%s', max |e_theta - (-sin, cos, 0)| = %.2e" + % (v_cart.get_basis().system.kind(), err)) + return err + + +def case_b(world): + print("(b) cylindrical points, Cartesian components on both sides") + src, tgt = build_spaces(world) + + # The space's points are (r, theta, z), but the field's + # components are Cartesian. B = (x, y, z) = (r cos theta, r sin theta, z). + b_src = src.create_function("b", pcms.values.Vector, + system=pcms.csys.Cartesian.create(3)) + fill_field(b_src, lambda p: np.stack( + [p[:, 0] * np.cos(p[:, 1]), p[:, 0] * np.sin(p[:, 1]), p[:, 2]], + axis=1)) + b_tgt = tgt.create_function("b", pcms.values.Vector) + + # The values are already in the target's basis, so the map passes them + # through and only the points are mapped. + src_as_cartesian = pcms.TransformedSource(src, + pcms.CartesianToCylindrical()) + pcms.Interpolator(src_as_cartesian, tgt).apply(b_src, b_tgt) + # B's Cartesian components are not linear in (r, theta, z), so + # the P1 source field only approximates them. + err = max_error_from_xyz(b_tgt) + print(" via Interpolator: max |B - (x,y,z)| = %.2e" % err) + return err + + +def case_c(world): + print("(c) cylindrical points and components -> Cartesian") + src, tgt = build_spaces(world) + + # B = r e_r + z e_z in physical cylindrical components, which is (x, y, z) + # in Cartesian ones. Its cylindrical components are linear in + # (r, theta, z), so the transfer is exact. + b_src = src.create_function("b", pcms.values.Vector, + scaling=pcms.ComponentScaling.Physical) + fill_field(b_src, lambda p: np.stack( + [p[:, 0], np.zeros(len(p)), p[:, 2]], axis=1)) + b_tgt = tgt.create_function("b", pcms.values.Vector) + + # `to_source` carries the target's + # Cartesian points to (r, theta, z), and the source's cylindrical + # components back to Cartesian. + src_as_cartesian = pcms.TransformedSource(src, + pcms.CartesianToCylindrical()) + pcms.Interpolator(src_as_cartesian, tgt).apply(b_src, b_tgt) + err = max_error_from_xyz(b_tgt) + print(" max |B - (x,y,z)| = %.2e" % err) + return err + + +def main(): + lib = omega_h.OmegaHLibrary() + world = lib.world() + errors = [case_a(), case_b(world), case_c(world)] + # (b) is a P1 approximation of a nonlinear field; (a) and (c) are exact. + ok = errors[0] < 1e-12 and errors[1] < 2e-2 and errors[2] < 1e-12 + print("SUCCESS" if ok else "FAILED: error above tolerance") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/python-api-example/example_conservative_transfer.py b/examples/python-api-example/example_conservative_transfer.py index d6e6fd541..4b8ed9d2b 100644 --- a/examples/python-api-example/example_conservative_transfer.py +++ b/examples/python-api-example/example_conservative_transfer.py @@ -30,6 +30,7 @@ import numpy as np import pcms +import PyOmega_h as omega_h # Smooth analytic field sampled onto the source mesh. @@ -39,22 +40,31 @@ def source_function(x, y): def build_and_roundtrip_mesh(world, lib, nx, ny, path): """Build a 2D box mesh, write it to disk, and read it back.""" - mesh = pcms.build_box( + mesh = omega_h.build_box( world, - pcms.Family.SIMPLEX, + omega_h.Family.SIMPLEX, 1.0, 1.0, 0.0, # unit square (z = 0 -> 2D) nx, ny, 0, False, ) - pcms.write_mesh_binary(path, mesh) + omega_h.write_mesh_binary(path, mesh) del mesh gc.collect() - return pcms.read_mesh_binary(path, lib) + return omega_h.read_mesh_binary(path, lib) + + +def scalar_values(field): + """DOF values of a scalar field as a 1D array. + + DOF holder data is (num_dof_holders, num_components); these fields have a + single component. + """ + return field.get_dof_holder_data().get_values()[:, 0] def integral_of_field(mesh, field): """Integral of an order-1 nodal field via element-averaged quadrature.""" - values = field.get_dof_holder_data() + values = scalar_values(field) elem_verts = mesh.ask_elem_verts().reshape(-1, 3) areas = mesh.ask_sizes() # element areas for a 2D simplex mesh elem_avg = values[elem_verts].mean(axis=1) @@ -63,22 +73,22 @@ def integral_of_field(mesh, field): def integral_of_p0_field(mesh, field): """Integral of an order-0 field: sum of per-element value * element area.""" - values = field.get_dof_holder_data() # one value per element + values = scalar_values(field) # one value per element areas = mesh.ask_sizes() return float(np.sum(areas * values)) def evaluate_function_onto_field(space, func): """Create a field on `space` and fill it from `func` at the DOF holders.""" - field = space.create_field() - coords = field.get_dof_holder_coordinates() + field = space.create_function() + coords = field.get_layout().get_dof_holder_coordinates().get_values() field.set_dof_holder_data(func(coords[:, 0], coords[:, 1])) return field def report(name, target_field, target_mesh, reference_coords, reference_values, source_integral, integrate=integral_of_field): - values = target_field.get_dof_holder_data() + values = scalar_values(target_field) err = values - reference_values l2 = np.sqrt(np.mean(err ** 2)) linf = np.max(np.abs(err)) @@ -99,7 +109,7 @@ def _triangulation(mesh, vertex_coords): def _draw_field(ax, triang, field, order, title, vmin, vmax): - values = field.get_dof_holder_data() + values = scalar_values(field) if order == 1: # Nodal (per-vertex) values -> smooth (Gouraud) shading. art = ax.tripcolor(triang, values, shading="gouraud", @@ -138,7 +148,7 @@ def make_plots(out_path, source_mesh, target_mesh, source_vertex_coords, } # Shared color scale across panels so fields are directly comparable. - all_vals = np.concatenate([f.get_dof_holder_data() for _, f, _, _ in panels]) + all_vals = np.concatenate([scalar_values(f) for _, f, _, _ in panels]) vmin, vmax = float(all_vals.min()), float(all_vals.max()) ncols = 3 @@ -161,7 +171,7 @@ def make_plots(out_path, source_mesh, target_mesh, source_vertex_coords, def main(plot_path=None): - lib = pcms.OmegaHLibrary() + lib = omega_h.OmegaHLibrary() world = lib.world() work_dir = tempfile.mkdtemp(prefix="pcms_conservative_transfer_") @@ -179,7 +189,7 @@ def main(plot_path=None): # The conservative-projection operators require the native Omega_h # Lagrange backend (not the default MeshFields layout). source_space = pcms.LagrangeFunctionSpace.from_mesh( - source_mesh, 1, 1, pcms.CoordinateSystem.Cartesian, + source_mesh, 1, 1, backend=pcms.LagrangeFunctionSpace.Backend.OmegaH, ) source_field = evaluate_function_onto_field(source_space, @@ -195,20 +205,21 @@ def main(plot_path=None): f"{target_mesh.nelems()} elems (read from {target_path})") target_space = pcms.LagrangeFunctionSpace.from_mesh( - target_mesh, 1, 1, pcms.CoordinateSystem.Cartesian, + target_mesh, 1, 1, backend=pcms.LagrangeFunctionSpace.Backend.OmegaH, ) # Analytic reference values at the target DOF holders. - target_template = target_space.create_field() - target_coords = target_template.get_dof_holder_coordinates() + target_template = target_space.create_function() + target_coords = (target_template.get_layout() + .get_dof_holder_coordinates().get_values()) reference_values = source_function(target_coords[:, 0], target_coords[:, 1]) print("\nTransferring source field to target field:") # 3a. Mesh-intersection conservative projection. - mi_target = target_space.create_field() + mi_target = target_space.create_function() mesh_intersection = pcms.OmegaHConservativeProjection( source_space, target_space ) @@ -216,8 +227,21 @@ def main(plot_path=None): report("mesh intersection (exact quadrature)", mi_target, target_mesh, target_coords, reference_values, source_integral) - # 3b. Monte Carlo (control-variate) conservative projection. - mc_target = target_space.create_field() + # 3b. Mesh-intersection projection with a row-sum lumped mass matrix. + # The linear solve degenerates to an exact diagonal scaling; the + # integral is still conserved exactly, but the max nodal error + # grows relative to the consistent mass matrix. + lumped_target = target_space.create_function() + lumped_projection = pcms.OmegaHConservativeProjection( + source_space, target_space, + mass_matrix_type=pcms.MassMatrixType.Lumped, + ) + lumped_projection.apply(source_field, lumped_target) + report("mesh intersection (lumped mass)", lumped_target, target_mesh, + target_coords, reference_values, source_integral) + + # 3c. Monte Carlo (control-variate) conservative projection. + mc_target = target_space.create_function() monte_carlo = pcms.OmegaHControlVariateProjection( source_space, target_space, @@ -237,12 +261,13 @@ def main(plot_path=None): # 4a. P1 source -> P0 target (project the linear field to cell averages). target_p0_space = pcms.LagrangeFunctionSpace.from_mesh( - target_mesh, 0, 1, pcms.CoordinateSystem.Cartesian, + target_mesh, 0, 1, backend=pcms.LagrangeFunctionSpace.Backend.OmegaH, ) - p1_to_p0 = target_p0_space.create_field() + p1_to_p0 = target_p0_space.create_function() # Analytic reference at the P0 DOF holders (element centroids). - p0_coords = p1_to_p0.get_dof_holder_coordinates() + p0_coords = (p1_to_p0.get_layout() + .get_dof_holder_coordinates().get_values()) p0_reference = source_function(p0_coords[:, 0], p0_coords[:, 1]) proj_p1_p0 = pcms.OmegaHConservativeProjection( source_space, target_p0_space @@ -255,13 +280,13 @@ def main(plot_path=None): # 4b. P0 source -> P1 target. Build a P0 source field by sampling the # analytic function at the source element centroids. source_p0_space = pcms.LagrangeFunctionSpace.from_mesh( - source_mesh, 0, 1, pcms.CoordinateSystem.Cartesian, + source_mesh, 0, 1, backend=pcms.LagrangeFunctionSpace.Backend.OmegaH, ) source_p0_field = evaluate_function_onto_field(source_p0_space, source_function) source_p0_integral = integral_of_p0_field(source_mesh, source_p0_field) - p0_to_p1 = target_space.create_field() + p0_to_p1 = target_space.create_function() proj_p0_p1 = pcms.OmegaHConservativeProjection( source_p0_space, target_space ) @@ -273,7 +298,8 @@ def main(plot_path=None): # 5. Optional before/after visualization. if plot_path is not None: - source_vertex_coords = source_field.get_dof_holder_coordinates() + source_vertex_coords = (source_field.get_layout() + .get_dof_holder_coordinates().get_values()) make_plots( plot_path, source_mesh, target_mesh, source_vertex_coords, target_coords, diff --git a/examples/python-api-example/flow_example.py b/examples/python-api-example/flow_example.py index e1bd5cf3d..8af16de2d 100644 --- a/examples/python-api-example/flow_example.py +++ b/examples/python-api-example/flow_example.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import pcms +import PyOmega_h as omega_h import numpy as np # import matplotlib.pyplot as plt @@ -50,17 +51,17 @@ def demonstrate_face_field_transfer(mesh): ) omega_h_factory = pcms.LagrangeFunctionSpace.from_mesh( - mesh, 1, 1, pcms.CoordinateSystem.Cartesian + mesh, 1, 1 ) - omega_h_field = omega_h_factory.create_field() + omega_h_field = omega_h_factory.create_function() omega_h_field.set_dof_holder_data(vertex_field_values) divisions = [1000, 500] grid = pcms.create_uniform_grid_from_mesh(mesh, divisions) ug_factory = pcms.LagrangeFunctionSpace.from_uniform_grid( - grid, 1, pcms.CoordinateSystem.Cartesian + grid, 1 ) - ug_field = ug_factory.create_field() + ug_field = ug_factory.create_function() print(f"Interpolating field from OmegaH mesh to uniform grid...") interp = pcms.Interpolator( @@ -70,7 +71,7 @@ def demonstrate_face_field_transfer(mesh): ) interp.apply(omega_h_field, ug_field) - transferred_data = ug_field.get_dof_holder_data() + transferred_data = ug_field.get_dof_holder_data().get_values()[:, 0] print(f"Transferred field: min={np.min(transferred_data):.6f}, max={np.max(transferred_data):.6f}, mean={np.mean(transferred_data):.6f}") # Visualize the transferred data @@ -87,21 +88,26 @@ def demonstrate_face_field_transfer(mesh): # print(f"Saved visualization to transferred_field.png") # plt.close() -def main(): - lib = pcms.OmegaHLibrary() +def main(exo_path): + if not hasattr(omega_h, "exodus_open"): + print("PyOmega_h was built without Exodus support " + "(rebuild Omega_h with Omega_h_USE_SEACASExodus=ON)") + return 1 + + lib = omega_h.OmegaHLibrary() world = lib.world() - - exo_handle = pcms.exodus_open("flow.exo", verbose=False) - num_steps = pcms.exodus_get_num_time_steps(exo_handle) + + exo_handle = omega_h.exodus_open(exo_path, verbose=False) + num_steps = omega_h.exodus_get_num_time_steps(exo_handle) print(f"Time steps: {num_steps}") - mesh = pcms.OmegaHMesh(lib) + mesh = omega_h.OmegaHMesh(lib) mesh.set_comm(world) - pcms.read_mesh_exodus(exo_handle, mesh, verbose=True) - pcms.read_exodus_nodal_fields(exo_handle, mesh, 50, verbose=True) # timestep 50 - pcms.read_exodus_element_fields(exo_handle, mesh, 50, verbose=True) - pcms.exodus_close(exo_handle) + omega_h.read_mesh_exodus(exo_handle, mesh, verbose=True) + omega_h.read_exodus_nodal_fields(exo_handle, mesh, 50, verbose=True) # timestep 50 + omega_h.read_exodus_element_fields(exo_handle, mesh, 50, verbose=True) + omega_h.exodus_close(exo_handle) print_mesh_info(mesh) demonstrate_face_field_transfer(mesh) @@ -114,4 +120,11 @@ def main(): if __name__ == "__main__": - exit(main()) + import argparse + parser = argparse.ArgumentParser() + parser.add_argument( + "exo_path", nargs="?", default="flow.exo", + help="Exodus file to read (e.g. pcms_testcases/exodus/flow/flow.exo)", + ) + args = parser.parse_args() + exit(main(args.exo_path)) diff --git a/pyproject.toml b/pyproject.toml index 949bcbcc8..25dc90ec0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,3 +8,10 @@ version = "0.4.0" [tool.scikit-build.cmake.define] PCMS_ENABLE_Python = true + +[tool.pytest.ini_options] +testpaths = ["test/python"] +python_files = "test_*.py" +python_classes = "Test*" +python_functions = "test_*" +addopts = "--strict-markers" diff --git a/spack/spack-scorec.yaml b/spack/spack-scorec.yaml deleted file mode 100644 index d7edec6ec..000000000 --- a/spack/spack-scorec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# This is a Spack Environment file. -# -# It describes a set of packages to be installed, along with -# configuration settings. -spack: - specs: - - pcms - view: true - concretizer: - unify: true - - packages: - all: - providers: - mpi: [mpich] - gcc: - externals: - - spec: gcc@12.4.0 languages:='c,c++,fortran' - prefix: /opt/scorec/spack/rhel9/v0222_2/install/linux-rhel9-x86_64_v3/gcc-11.4.1/gcc-12.4.0-5xg2r2h6cc5mckmjid2g6bftg5kh3cof - extra_attributes: - compilers: - c: /opt/scorec/spack/rhel9/v0222_2/install/linux-rhel9-x86_64_v3/gcc-11.4.1/gcc-12.4.0-5xg2r2h6cc5mckmjid2g6bftg5kh3cof/bin/gcc - cxx: /opt/scorec/spack/rhel9/v0222_2/install/linux-rhel9-x86_64_v3/gcc-11.4.1/gcc-12.4.0-5xg2r2h6cc5mckmjid2g6bftg5kh3cof/bin/g++ - fortran: /opt/scorec/spack/rhel9/v0222_2/install/linux-rhel9-x86_64_v3/gcc-11.4.1/gcc-12.4.0-5xg2r2h6cc5mckmjid2g6bftg5kh3cof/bin/gfortran - develop: - pcms: - spec: pcms@develop - path: ../ - diff --git a/spack/spack.yaml b/spack/spack.yaml index 76dc85ece..ed9e7d37c 100644 --- a/spack/spack.yaml +++ b/spack/spack.yaml @@ -1,20 +1,38 @@ -# This is a Spack Environment file. +# Spack environment for developing PCMS. It builds the PCMS source in this +# checkout (see "develop" below) with every feature the container image has. +# +# spack env activate ./spack +# spack install # -# It describes a set of packages to be installed, along with -# configuration settings. spack: specs: - - pcms - view: true - concretizer: - unify: true + - pcms@develop +python +petsc +netcdf ^omega-h+exodus ^kokkos+serial+openmp - packages: - all: - providers: - mpi: [mpich] develop: pcms: - spec: pcms@develop - path: ../ + spec: pcms@=develop + path: .. + + # Same package versions the build cache was built with. + repos: + scorec: + git: https://github.com/SCOREC/scorec-spack-packages.git + branch: main + builtin: + git: https://github.com/spack/spack-packages.git + branch: releases/v2026.10 + + mirrors: + scorec: + url: oci://ghcr.io/scorec/scorec-spack-buildcache + signed: false + packages: + petsc: + # See environments/pcms in scorec-spack-packages. + require: '+openmp' + + concretizer: + unify: true + + view: true diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 143889fe1..e9d9313c6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,9 @@ 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) +if(PCMS_ENABLE_TRANSIENT) + add_subdirectory(pcms/transient) +endif() add_subdirectory(pcms/discretization) add_subdirectory(pcms/localization) add_subdirectory(pcms/field) @@ -88,6 +91,9 @@ install( add_library(pcms_pcms INTERFACE) target_link_libraries(pcms_pcms INTERFACE pcms::core) +if(PCMS_ENABLE_TRANSIENT) + target_link_libraries(pcms_pcms INTERFACE pcms::transient) +endif() set_target_properties(pcms_pcms PROPERTIES EXPORT_NAME pcms) if (PCMS_ENABLE_Python) # Disable LTO/IPO before adding Python subdirectory to avoid fatbinData conflicts diff --git a/src/pcms/capi/client.cpp b/src/pcms/capi/client.cpp index 3d3200940..414579bf3 100644 --- a/src/pcms/capi/client.cpp +++ b/src/pcms/capi/client.cpp @@ -1,9 +1,9 @@ #include "client.h" #include "pcms.h" +#include "pcms/field/field_factory.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" @@ -13,6 +13,7 @@ #include #include #include +#include "pcms/field/coordinate_systems/cylindrical.hpp" namespace pcms { @@ -23,11 +24,11 @@ namespace detail template struct XGCFieldRegistration { - XGCFunctionSpace function_space; + XGCFieldFactory function_space; MPI_Comm plane_comm; Rank1View data; - XGCFieldRegistration(XGCFunctionSpace fs, MPI_Comm comm, + XGCFieldRegistration(XGCFieldFactory fs, MPI_Comm comm, Rank1View d) : function_space(std::move(fs)), plane_comm(comm), data(d) { @@ -37,10 +38,16 @@ struct XGCFieldRegistration struct DummyFieldRegistration {}; -class EmptyFunctionSpace : public FunctionSpace +class EmptyFieldFactory : public FieldFactory { public: - EmptyFunctionSpace() : layout_(std::make_shared()) {} + EmptyFieldFactory(std::string layout_name, + std::shared_ptr system) + { + auto layout = std::make_shared(std::move(system)); + layout->SetName(std::move(layout_name)); + layout_ = layout; + } [[nodiscard]] std::shared_ptr GetLayout() const noexcept override @@ -48,21 +55,15 @@ class EmptyFunctionSpace : public FunctionSpace return layout_; } - [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept override - { - return CoordinateSystem::Cartesian; - } - protected: - [[nodiscard]] FieldVariant CreateFieldImpl( - Type value_type, FieldMetadata metadata) const override + [[nodiscard]] FieldVariant CreateFieldImpl(Type storage_type, + ValueBasis basis) 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)); - }); + return apply_to_type(storage_type, [this, basis](auto tag) -> FieldVariant { + using T = typename decltype(tag)::type; + return WrapField(layout_, + std::make_unique>(layout_, basis)); + }); } [[nodiscard]] FieldVariant CreateFieldImpl( @@ -75,12 +76,12 @@ class EmptyFunctionSpace : public FunctionSpace PCMS_ALWAYS_ASSERT(fd != nullptr); if (dynamic_cast*>(fd.get()) == nullptr) { throw pcms_error( - "EmptyFunctionSpace::CreateField: requires SimpleFieldData"); + "EmptyFieldFactory::CreateField: requires SimpleFieldData"); } - if (fd->GetDOFHolderDataHost().size() != + if (fd->GetDOFAccess()->GetOwnedValues().size() != detail::ExpectedFlatFieldDataSize(*layout_)) { throw pcms_error( - "EmptyFunctionSpace::CreateField: field data size does not match " + "EmptyFieldFactory::CreateField: field data size does not match " "layout"); } return WrapField(layout_, std::forward(fd)); @@ -88,12 +89,6 @@ class EmptyFunctionSpace : public FunctionSpace 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_; }; @@ -102,8 +97,8 @@ class EmptyFunctionSpace : public FunctionSpace struct ClientState { - std::unique_ptr coupler; - Application* app = nullptr; + std::unique_ptr coupler; + ApplicationComm* app = nullptr; using HandleVariant = std::variant, FieldHandle, FieldHandle, FieldHandle>; std::map field_handles; @@ -117,7 +112,7 @@ using FieldAdapterVariant = std::variant< template using ClientFieldHandle = FieldHandle; -inline ClientState::HandleVariant RegisterField(Application& /*app*/, +inline ClientState::HandleVariant RegisterField(ApplicationComm& /*app*/, std::string name, const std::monostate&, bool) { @@ -127,32 +122,34 @@ inline ClientState::HandleVariant RegisterField(Application& /*app*/, template ClientState::HandleVariant RegisterField( - Application& app, std::string name, + ApplicationComm& app, std::string name, const detail::XGCFieldRegistration& registration, bool participates) { + // The layout's coupling identity is the field name + // not the arbitrary adapter name given at creation. + registration.function_space.SetLayoutName(name); 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)}; + std::move(name), std::make_unique>( + registration.function_space.GetXGCLayout(), ValueBasis{}, + registration.data, registration.plane_comm)); + return ClientState::HandleVariant{ + app.AddField(std::move(field), participates)}; } inline ClientState::HandleVariant RegisterField( - Application& app, std::string name, const detail::DummyFieldRegistration&, + ApplicationComm& 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)}; + // FIXME this is essentially a hack for now since the only CAPI client + // is XGC so we need to match the XGCLayout. + // The CAPI needs to be completely reworked for PCMS2, but that + // should happen after we finalize new coordinate transformations + // and higher level field control + auto function_space = + detail::EmptyFieldFactory{name, csys::CylindricalRZ::Create()}; + auto field = function_space.CreateField(std::move(name)); + return ClientState::HandleVariant{ + app.AddField(std::move(field), participates)}; } } // namespace pcms @@ -162,7 +159,7 @@ inline ClientState::HandleVariant RegisterField( { auto* client = new pcms::ClientState{}; client->coupler = - std::make_unique(name, comm, false, redev::Partition{}); + std::make_unique(name, comm, false, redev::Partition{}); client->app = client->coupler->AddApplication(name); return {reinterpret_cast(client), reinterpret_cast(client->app)}; @@ -186,13 +183,26 @@ void pcms_destroy_reverse_classification(PcmsReverseClassificationHandle rc) delete reinterpret_cast(rc.pointer); } +void pcms_set_overlap_mask(PcmsClientHandle client_handle, const char* name, + in_overlap_function in_overlap) +{ + auto* app = + reinterpret_cast(client_handle.appPointer); + PCMS_ALWAYS_ASSERT(app != nullptr); + PCMS_ALWAYS_ASSERT(in_overlap != nullptr); + // Fields registered through the C API use their name as the layout name. + app->SetLayoutOverlapMask(name, + std::make_unique(in_overlap)); +} + 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* app = + reinterpret_cast(client_handle.appPointer); auto* adapter = reinterpret_cast(adapter_handle.pointer); PCMS_ALWAYS_ASSERT(client != nullptr); @@ -215,14 +225,16 @@ PcmsFieldHandle pcms_add_field(PcmsClientHandle client_handle, const char* name, void pcms_send_field_name(PcmsClientHandle client_handle, const char* name) { - auto* app = reinterpret_cast(client_handle.appPointer); + 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); + auto* app = + reinterpret_cast(client_handle.appPointer); PCMS_ALWAYS_ASSERT(app != nullptr); app->ReceiveField(name); } @@ -247,11 +259,10 @@ template void pcms_create_xgc_field_adapter_t( 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::FieldAdapterVariant& field_adapter) { PCMS_ALWAYS_ASSERT((size > 0) ? (data != nullptr) : true); - auto function_space = - pcms::XGCFunctionSpace(reverse_classification, in_overlap, size); + auto function_space = pcms::XGCFieldFactory(reverse_classification, size); pcms::Rank1View data_view( reinterpret_cast(data), size); field_adapter.emplace>( @@ -260,7 +271,7 @@ void pcms_create_xgc_field_adapter_t( 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) + const PcmsReverseClassificationHandle rc) { auto* field_adapter = new pcms::FieldAdapterVariant{}; PCMS_ALWAYS_ASSERT(rc.pointer != nullptr); @@ -269,24 +280,20 @@ PcmsFieldAdapterHandle pcms_create_xgc_field_adapter( PCMS_ALWAYS_ASSERT(reverse_classification != nullptr); switch (data_type) { case PCMS_DOUBLE: - pcms_create_xgc_field_adapter_t(name, comm, data, size, - *reverse_classification, - in_overlap, *field_adapter); + pcms_create_xgc_field_adapter_t( + name, comm, data, size, *reverse_classification, *field_adapter); break; case PCMS_FLOAT: - pcms_create_xgc_field_adapter_t(name, comm, data, size, - *reverse_classification, - in_overlap, *field_adapter); + pcms_create_xgc_field_adapter_t( + name, comm, data, size, *reverse_classification, *field_adapter); break; case PCMS_INT: - pcms_create_xgc_field_adapter_t(name, comm, data, size, - *reverse_classification, in_overlap, - *field_adapter); + pcms_create_xgc_field_adapter_t( + name, comm, data, size, *reverse_classification, *field_adapter); break; case PCMS_LONG_INT: - pcms_create_xgc_field_adapter_t(name, comm, data, size, - *reverse_classification, - in_overlap, *field_adapter); + pcms_create_xgc_field_adapter_t( + name, comm, data, size, *reverse_classification, *field_adapter); break; default: PCMS_ALWAYS_ASSERT(false, MPI_COMM_WORLD, @@ -321,28 +328,28 @@ int pcms_reverse_classification_count_verts(PcmsReverseClassificationHandle rc) void pcms_begin_send_phase(PcmsClientHandle h) { - auto* app = reinterpret_cast(h.appPointer); + 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); + 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); + 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); + auto* app = reinterpret_cast(h.appPointer); PCMS_ALWAYS_ASSERT(app != nullptr); app->EndReceivePhase(); } diff --git a/src/pcms/capi/client.h b/src/pcms/capi/client.h index 5aa8eec64..b17a664f1 100644 --- a/src/pcms/capi/client.h +++ b/src/pcms/capi/client.h @@ -65,13 +65,18 @@ void pcms_destroy_reverse_classification(PcmsReverseClassificationHandle); // likely to be needed for production cases int pcms_reverse_classification_count_verts(PcmsReverseClassificationHandle); +PcmsFieldAdapterHandle pcms_create_xgc_field_adapter( + const char* name, MPI_Comm plane_comm, void* data, int size, + PcmsType data_type, const PcmsReverseClassificationHandle rc); + // takes in overlap function takes a geometric dimension and a geometric id // C doesn't have a builtin bool type, so we use int for compatability with C++ typedef int8_t (*in_overlap_function)(int, int); -PcmsFieldAdapterHandle pcms_create_xgc_field_adapter( - const char* name, MPI_Comm plane_comm, void* data, int size, - PcmsType data_type, const PcmsReverseClassificationHandle rc, - in_overlap_function in_overlap); +// Restricts the field registered under `name` to the DOF holders whose +// classification satisfies in_overlap (unclassified holders are passed as +// (-1, -1)). +void pcms_set_overlap_mask(PcmsClientHandle client_handle, const char* name, + in_overlap_function in_overlap); PcmsFieldAdapterHandle pcms_create_dummy_field_adapter(); diff --git a/src/pcms/configuration.h.in b/src/pcms/configuration.h.in index 51859ec0a..418d7e585 100644 --- a/src/pcms/configuration.h.in +++ b/src/pcms/configuration.h.in @@ -2,8 +2,19 @@ #cmakedefine PCMS_ENABLE_CLIENT #cmakedefine PCMS_ENABLE_XGC #cmakedefine PCMS_ENABLE_OMEGA_H +#cmakedefine PCMS_ENABLE_MFEM #cmakedefine PCMS_ENABLE_C +#cmakedefine PCMS_ENABLE_TRANSIENT #cmakedefine PCMS_ENABLE_PRINT #cmakedefine PCMS_ENABLE_Fortran #cmakedefine PCMS_ENABLE_MESHFIELDS #cmakedefine PCMS_ENABLE_PETSC +#cmakedefine PCMS_ENABLE_ARBORX + +#cmakedefine PCMS_INTERSECTION_QUEUE_SIZE @PCMS_INTERSECTION_QUEUE_SIZE@ +#cmakedefine PCMS_INTERSECTION_TRACK_SIZE @PCMS_INTERSECTION_TRACK_SIZE@ + +#cmakedefine PCMS_INTERSECTION_ABS_TOL @PCMS_INTERSECTION_ABS_TOL@ +#cmakedefine PCMS_INTERSECTION_REL_TOL @PCMS_INTERSECTION_REL_TOL@ +#cmakedefine PCMS_PROJECTION_KSP_RTOL @PCMS_PROJECTION_KSP_RTOL@ +#cmakedefine PCMS_PROJECTION_KSP_ATOL @PCMS_PROJECTION_KSP_ATOL@ diff --git a/src/pcms/coupler/CMakeLists.txt b/src/pcms/coupler/CMakeLists.txt index 82afceb32..397694120 100644 --- a/src/pcms/coupler/CMakeLists.txt +++ b/src/pcms/coupler/CMakeLists.txt @@ -7,6 +7,7 @@ set(PCMS_COUPLER_HEADERS field_exchange_planner.h partition.h overlap_mask.h + global_communicator.h ) @@ -16,11 +17,6 @@ set(PCMS_COUPLER_SOURCES 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 diff --git a/src/pcms/coupler/coupler.cpp b/src/pcms/coupler/coupler.cpp index a0c83e7db..f3ffe3efb 100644 --- a/src/pcms/coupler/coupler.cpp +++ b/src/pcms/coupler/coupler.cpp @@ -3,32 +3,23 @@ namespace pcms { -FieldLayoutCommunicator& Application::GetLayoutCommunicator( - const FieldLayout& layout) +FieldLayoutCommunicator& ApplicationComm::GetOrCreateLayoutCommunicator( + const FieldLayout& layout, bool participates) { PCMS_FUNCTION_TIMER; - auto it = field_layout_communicators_.find(&layout); + const std::string& name = layout.GetName(); + if (name.empty()) { + throw pcms_error( + "Application: the field's layout has no name; name it at construction " + "(From*(..., layout_name)) before registering it with a coupler"); + } + + // Fields whose layouts share a name share one GID-exchange communicator. + auto it = field_layout_communicators_.find(name); 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); @@ -39,24 +30,22 @@ const FieldLayout& Application::AddLayout( &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; + auto [it2, inserted] = field_layout_communicators_.emplace( + name, std::make_unique( + name, mpi_comm_subset, redev_, channel_, layout, + std::make_unique(), own_mpi_comm, + overlap_mask)); + return *it2->second; } -void Application::SetLayoutOverlapMask( +void ApplicationComm::SetLayoutOverlapMask( const std::string& layout_name, std::unique_ptr overlap_mask) { layout_overlap_masks_[layout_name] = std::move(overlap_mask); diff --git a/src/pcms/coupler/coupler.hpp b/src/pcms/coupler/coupler.hpp index d2d12cb1a..7bdf23118 100644 --- a/src/pcms/coupler/coupler.hpp +++ b/src/pcms/coupler/coupler.hpp @@ -11,18 +11,56 @@ #include "pcms/utility/assert.h" #include "pcms/utility/common.h" #include "pcms/utility/profile.h" +#include "pcms/coupler/global_communicator.h" #include namespace pcms { +template +class GlobalDataInterface +{ +public: + GlobalDataInterface(std::string name, + Rank1View data, + MPI_Comm mpi_comm, redev::Channel& channel) + : data_(data), + variable_name_(name), + mpi_comm_(mpi_comm), + comm_(name, mpi_comm_, channel) + { + PCMS_FUNCTION_TIMER; + } + + void Send(redev::Mode mode = redev::Mode::Synchronous) + { + PCMS_FUNCTION_TIMER; + comm_.Send(data_, variable_name_, mode); + } -class Application; + void Receive(redev::Mode mode = redev::Mode::Synchronous) + { + PCMS_FUNCTION_TIMER; + comm_.Receive(data_, variable_name_, mode); + } + +private: + Rank1View data_; + std::string variable_name_; + MPI_Comm mpi_comm_; + GlobalCommunicator comm_; +}; +using GlobalDataVariant = + std::variant, GlobalDataInterface, + GlobalDataInterface, GlobalDataInterface, + GlobalDataInterface>; + +class ApplicationComm; template class FieldHandle { public: - FieldHandle(Application* app, std::string name) + FieldHandle(ApplicationComm* app, std::string name) : app_(app), name_(std::move(name)) { } @@ -32,16 +70,61 @@ class FieldHandle [[nodiscard]] Field& GetField() const; private: - Application* app_; + ApplicationComm* app_; std::string name_; }; +template +class DataHandle +{ +public: + DataHandle(ApplicationComm* 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]] const std::string& GetName() const noexcept { return name_; } -class Application +private: + ApplicationComm* app_; + std::string name_; +}; + +// FunctionHandle is a FieldHandle that additionally carries the function space, +// so it can be evaluated / used to build transfer operators. Only AddFunction +// produces one; CreateTransfer accepts FunctionHandle (not FieldHandle), which +// makes passing a comm-only field to a transfer a compile error. +template +class FunctionHandle : public FieldHandle +{ +public: + FunctionHandle(ApplicationComm* app, std::string name, + std::shared_ptr space) + : FieldHandle(app, std::move(name)), space_(std::move(space)) + { + } + + [[nodiscard]] const FunctionSpace& GetSpace() const noexcept + { + return *space_; + } + [[nodiscard]] std::shared_ptr GetSpacePtr() + const noexcept + { + return space_; + } + +private: + std::shared_ptr space_; +}; + +class ApplicationComm { public: - Application(std::string name, MPI_Comm comm, redev::Redev& redev, - adios2::Params params, redev::TransportType transport_type, - std::string path) + ApplicationComm(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), @@ -50,26 +133,30 @@ class Application 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 + // Set the overlap mask for a field's layout by name. Must be set before the + // AddField/AddFunction that first creates that layout's communicator. void SetLayoutOverlapMask(const std::string& layout_name, std::unique_ptr overlap_mask); + // Register a comm-only field, identified by its own name. The layout's + // communicator is created on first use and shared by any later + // field/function whose layout has the same name. The field's data must + // provide DOFAccess: values are exchanged through it, and receiving writes + // them back through it on every rank, including ranks that do not + // participate, so backends that keep replicas can synchronize them. template - FieldHandle AddField(std::string name, Field&& field, - bool participates = true); - + FieldHandle AddField(Field&& field, bool participates = true); + // Registers a reference to user-owned data. The Application does not take + // ownership; it uses the registered reference for subsequent send and receive + // operations. template - FieldHandle AddField(std::string name, Field&& field, - std::unique_ptr> serializer, - bool participates = true); + DataHandle AddData(std::string name, Rank1View data, + MPI_Comm mpi_comm); + // Register a transferable field: like AddField but retains the function space + // (via the stored Function) and returns a FunctionHandle usable in transfers. + template + FunctionHandle AddFunction(Function&& function, + bool participates = true); void SendField(const std::string& name, redev::Mode mode = redev::Mode::Synchronous) @@ -91,6 +178,25 @@ class Application [mode](auto& field_communicator) { field_communicator->Receive(); }, detail::find_or_error(name, field_communicators_)); }; + void SendData(const std::string& name, + redev::Mode mode = redev::Mode::Synchronous) + { + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(InSendPhase()); + + std::visit([mode](auto& data_interface) { data_interface.Send(mode); }, + detail::find_or_error(name, global_data_interfaces_)); + } + + void ReceiveData(const std::string& name, + redev::Mode mode = redev::Mode::Synchronous) + { + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(InReceivePhase()); + + std::visit([mode](auto& data_interface) { data_interface.Receive(mode); }, + detail::find_or_error(name, global_data_interfaces_)); + } [[nodiscard]] bool InSendPhase() const noexcept { PCMS_FUNCTION_TIMER; @@ -144,25 +250,63 @@ class Application [[nodiscard]] Field& GetField(const std::string& name); private: - FieldLayoutCommunicator& GetLayoutCommunicator(const FieldLayout& layout); + // Returns the communicator for `layout`, creating it (MPI split + overlap + // mask lookup + planner) on first use and reusing it for any later + // field/function whose layout has the same name. + FieldLayoutCommunicator& GetOrCreateLayoutCommunicator( + const FieldLayout& layout, bool participates); + + template + void RegisterFieldCommunicator(Field& field_obj, bool participates); MPI_Comm mpi_comm_; redev::Redev& redev_; redev::Channel channel_; - std::vector> layouts_; std::map fields_; + std::map functions_; 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> + std::map> field_layout_communicators_; std::map> layout_overlap_masks_; + std::map global_data_interfaces_; }; -class Coupler +template +DataHandle ApplicationComm::AddData(std::string name, + Rank1View data, + MPI_Comm mpi_comm) +{ + PCMS_FUNCTION_TIMER; + auto [it, inserted] = global_data_interfaces_.try_emplace( + name, std::in_place_type>, name, data, mpi_comm, + channel_); + if (!inserted) { + throw pcms_error("Global data interface with this name already exists"); + } + return DataHandle{this, std::move(name)}; +} + +template +void DataHandle::Send(redev::Mode mode) const +{ + PCMS_ALWAYS_ASSERT(app_ != nullptr); + + app_->SendData(name_, mode); +} +template +void DataHandle::Receive(redev::Mode mode) const +{ + PCMS_ALWAYS_ASSERT(app_ != nullptr); + + app_->ReceiveData(name_, mode); +} + +class CouplerComm { private: redev::Redev SetUpRedev(bool isServer, redev::Partition partition) @@ -174,15 +318,15 @@ class Coupler } public: - Coupler(std::string name, MPI_Comm comm, bool isServer, - redev::Partition partition) + CouplerComm(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( + ApplicationComm* AddApplication( std::string name, std::string path = "", redev::TransportType transport_type = redev::TransportType::BP4, adios2::Params params = {{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}) @@ -209,7 +353,7 @@ class Coupler MPI_Comm mpi_comm_; redev::Redev redev_; // gather and scatter operations have reference to internal fields - std::map applications_; + std::map applications_; }; } // namespace pcms @@ -236,49 +380,94 @@ pcms::Field& pcms::FieldHandle::GetField() const } template -pcms::Field& pcms::Application::GetField(const std::string& name) +pcms::Field& pcms::ApplicationComm::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"); + auto field_it = fields_.find(name); + if (field_it != fields_.end()) { + auto* field = std::get_if>(&field_it->second); + if (field == nullptr) { + throw pcms_error("Field stored with different type than requested"); + } + return *field; + } + auto fn_it = functions_.find(name); + if (fn_it != functions_.end()) { + auto* function = std::get_if>(&fn_it->second); + if (function == nullptr) { + throw pcms_error("Field stored with different type than requested"); + } + return *function; } - return *field; + throw pcms_error("Field '" + name + "' not found"); } template -pcms::FieldHandle pcms::Application::AddField(std::string name, - Field&& field, - bool participates) +void pcms::ApplicationComm::RegisterFieldCommunicator(Field& field_obj, + bool participates) { - return AddField(std::move(name), std::move(field), - std::make_unique>(), participates); + const std::string& name = field_obj.GetName(); + if (field_obj.GetData().GetDOFAccess() == nullptr) { + throw pcms_error("Application: field '" + name + + "' cannot be exchanged: its values are not accessible " + "through PCMS (the field data provides no DOFAccess)"); + } + FieldLayoutCommunicator& layout_communicator = + GetOrCreateLayoutCommunicator(field_obj.GetLayout(), participates); + FieldCommunicator2Ptr field_communicator = + std::make_unique>(name, layout_communicator, + field_obj); + auto [it, inserted] = + field_communicators_.emplace(name, std::move(field_communicator)); + if (!inserted) { + throw pcms_error("Field with this name already exists"); + } } template -pcms::FieldHandle pcms::Application::AddField( - std::string name, Field&& field, - std::unique_ptr> serializer, bool participates) +pcms::FieldHandle pcms::ApplicationComm::AddField(Field&& field, + bool participates) { PCMS_FUNCTION_TIMER; - (void)participates; + std::string name = field.GetName(); + if (name.empty()) { + throw pcms_error("Application::AddField: field has no name"); + } 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) { + try { + RegisterFieldCommunicator(std::get>(field_it->second), + participates); + } catch (...) { fields_.erase(field_it); - throw pcms_error("Field with this name already exists"); + throw; } return FieldHandle{this, std::move(name)}; } +template +pcms::FunctionHandle pcms::ApplicationComm::AddFunction( + Function&& function, bool participates) +{ + PCMS_FUNCTION_TIMER; + std::string name = function.GetName(); + if (name.empty()) { + throw pcms_error("Application::AddFunction: function has no name"); + } + auto space = function.GetSpacePtr(); + auto [fn_it, fn_inserted] = functions_.emplace(name, std::move(function)); + if (!fn_inserted) { + throw pcms_error("Field with this name already exists"); + } + try { + RegisterFieldCommunicator(std::get>(fn_it->second), + participates); + } catch (...) { + functions_.erase(fn_it); + throw; + } + return FunctionHandle{this, std::move(name), std::move(space)}; +} + #endif // COUPLER2_H_ diff --git a/src/pcms/coupler/field_communicator.hpp b/src/pcms/coupler/field_communicator.hpp index b18745070..8dc8de534 100644 --- a/src/pcms/coupler/field_communicator.hpp +++ b/src/pcms/coupler/field_communicator.hpp @@ -21,20 +21,10 @@ class FieldCommunicator public: FieldCommunicator(const std::string& name, FieldLayoutCommunicator& layout_comm, Field& field) - : FieldCommunicator(name, layout_comm, field, - std::make_unique>()) + : comm_buffer_{}, layout_comm_(layout_comm), field_(field) { - } - - 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()); + PCMS_ALWAYS_ASSERT(layout_comm.GetLayout().GetName() == + field.GetLayout().GetName()); // The exchange plan is per DOF holder; the field payload carries // num_components values per holder, so the buffer is scaled accordingly. comm_buffer_.resize( @@ -50,8 +40,7 @@ class FieldCommunicator PCMS_FUNCTION_TIMER; PCMS_ALWAYS_ASSERT(layout_comm_.GetChannel().InSendCommunicationPhase()); auto buffer = make_array_view(comm_buffer_); - serializer_->Serialize(field_.GetData(), field_.GetLayout(), buffer, - layout_comm_.GetPermutationArray()); + serializer_.Serialize(field_, buffer, layout_comm_.GetPermutationArray()); comm_.Send(buffer.data_handle(), mode); } @@ -63,9 +52,8 @@ class FieldCommunicator // mode because we make an immediate call to deserialize after a call to // receive. auto data = comm_.Recv(redev::Mode::Synchronous); - serializer_->Deserialize(field_.GetData(), field_.GetLayout(), - make_const_array_view(data), - layout_comm_.GetPermutationArray()); + serializer_.Deserialize(field_, make_const_array_view(data), + layout_comm_.GetPermutationArray()); } private: @@ -73,7 +61,7 @@ class FieldCommunicator redev::BidirectionalComm comm_; FieldLayoutCommunicator& layout_comm_; Field& field_; - std::unique_ptr> serializer_; + FieldSerializer serializer_; }; template diff --git a/src/pcms/coupler/field_layout_communicator.cpp b/src/pcms/coupler/field_layout_communicator.cpp index 00dabfb72..f20b53945 100644 --- a/src/pcms/coupler/field_layout_communicator.cpp +++ b/src/pcms/coupler/field_layout_communicator.cpp @@ -1,4 +1,5 @@ #include "pcms/coupler/field_layout_communicator.h" +#include "pcms/field/gid_permutation.hpp" namespace pcms { @@ -108,6 +109,10 @@ void FieldLayoutCommunicator::UpdateLayout() void FieldLayoutCommunicator::UpdateLayoutNull() { PCMS_FUNCTION_TIMER; + // This rank exchanges nothing, but it still unpacks on receive (backends + // that keep replicas synchronize them there), so every holder is unmapped. + plan_.permutation = MakeUnmappedPermutation( + static_cast(layout_.GetNumOwnedDofHolder())); if (redev_.GetProcessType() == redev::ProcessType::Client) { channel_.BeginSendCommunicationPhase(); channel_.EndSendCommunicationPhase(); diff --git a/src/pcms/coupler/field_serializer.h b/src/pcms/coupler/field_serializer.h index 723929cf5..883b61418 100644 --- a/src/pcms/coupler/field_serializer.h +++ b/src/pcms/coupler/field_serializer.h @@ -10,26 +10,22 @@ namespace pcms { +// Packs a field's owned values into, and unpacks them from, an exchange +// buffer, through the field's DOFAccess. Any field with DOFAccess is +// exchanged this way. Backends that need extra work on receive (e.g., +// synchronizing replicas) do it in DOFAccess::SetOwnedValues. template class FieldSerializer { public: - virtual int Serialize(const FieldData& field, const FieldLayout& layout, - Rank1View buffer, - Rank1View permutation) const + int Serialize(const Field& field, Rank1View buffer, + Rank1View permutation) const { - auto data = field.GetDOFHolderDataHost(); - auto owned = layout.GetOwnedHost(); - // The exchange plan is per DOF holder: owned[i] and permutation[i] are - // indexed by holder. All num_components components of a holder share its - // location, so they occupy one contiguous block permutation[i]*num_comp in - // the wire buffer. + const auto data = field.GetDOFHolderDataHost().GetValues(); if (buffer.size() > 0) { const LO num_dof = static_cast(data.extent(0)); const LO num_comp = static_cast(data.extent(1)); for (LO i = 0; i < num_dof; ++i) { - // A negative permutation entry marks a holder outside the exchange - // (non-owned, or owned but outside the overlap region); it has no slot. if (permutation[i] >= 0) { for (LO c = 0; c < num_comp; ++c) { buffer[permutation[i] * num_comp + c] = data(i, c); @@ -40,29 +36,34 @@ class FieldSerializer return static_cast(buffer.size()); } - virtual void Deserialize( - FieldData& field, const FieldLayout& layout, - Rank1View buffer, - Rank1View permutation) const + void Deserialize(Field& field, Rank1View buffer, + Rank1View permutation) const { + const auto& layout = field.GetLayout(); const LO num_dof = layout.GetNumOwnedDofHolder(); const LO num_comp = layout.GetNumComponents(); + // We initialize with local data so that + // data that doesn't get received from deserialized + // doesn't get zeroed out. Long term FIXME + // is to make Get/SetData operate over only part of the + // arrays + const auto current = field.GetDOFHolderDataHost().GetValues(); Kokkos::View sorted("sorted", layout.OwnedSize()); for (LO i = 0; i < num_dof; ++i) { - // A negative permutation entry marks a holder outside the exchange (owned - // but outside the overlap region); no data was received for it, so its - // zero-initialized `sorted` slot is left as-is. + for (LO c = 0; c < num_comp; ++c) { + sorted[i * num_comp + c] = current(i, c); + } + } + for (LO i = 0; i < num_dof; ++i) { if (permutation[i] >= 0) { for (LO c = 0; c < num_comp; ++c) { sorted[i * num_comp + c] = buffer[permutation[i] * num_comp + c]; } } } - field.SetDOFHolderDataHost( + field.SetDOFHolderDataUncheckedHost( Rank2View(sorted.data(), num_dof, num_comp)); } - - virtual ~FieldSerializer() noexcept = default; }; } // namespace pcms diff --git a/src/pcms/coupler/global_communicator.h b/src/pcms/coupler/global_communicator.h new file mode 100644 index 000000000..fc3bf3479 --- /dev/null +++ b/src/pcms/coupler/global_communicator.h @@ -0,0 +1,53 @@ +#ifndef PCMS_GLOBAL_COMMUNICATOR_H +#define PCMS_GLOBAL_COMMUNICATOR_H +#include +#include +#include +namespace pcms +{ +using redev::Mode; +template +struct GlobalCommunicator +{ + using value_type = T; + +public: + GlobalCommunicator(std::string name, MPI_Comm mpi_comm, + redev::Channel& channel) + : mpi_comm(mpi_comm), channel_(channel), name_(std::move(name)) + { + PCMS_FUNCTION_TIMER; + comm_ = channel_.CreateComm(name_, mpi_comm, redev::CommType::Global); + } + GlobalCommunicator(const GlobalCommunicator&) = delete; + GlobalCommunicator& operator=(const GlobalCommunicator&) = delete; + GlobalCommunicator(GlobalCommunicator&&) = default; + GlobalCommunicator& operator=(GlobalCommunicator&&) = default; + + void Send(Rank1View msg, std::string VarName, + Mode mode = Mode::Synchronous) + { + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(channel_.InSendCommunicationPhase()); + auto msg_size = static_cast(msg.extent(0)); + comm_.SetCommParams(VarName, msg_size); + comm_.Send(msg.data_handle(), mode); + } + void Receive(Rank1View destination, std::string VarName, + Mode mode = Mode::Synchronous) + { + PCMS_FUNCTION_TIMER; + PCMS_ALWAYS_ASSERT(channel_.InReceiveCommunicationPhase()); + auto msg_size = static_cast(destination.extent(0)); + comm_.SetCommParams(VarName, msg_size); + comm_.Recv(destination.data_handle(), msg_size, mode); + } + +private: + MPI_Comm mpi_comm; + redev::Channel& channel_; + std::string name_; + redev::BidirectionalComm comm_; +}; +} // namespace pcms +#endif // PCMS_GLOBAL_COMMUNICATOR_H \ No newline at end of file diff --git a/src/pcms/coupler/overlap_mask.h b/src/pcms/coupler/overlap_mask.h index db795b45f..1a917abb4 100644 --- a/src/pcms/coupler/overlap_mask.h +++ b/src/pcms/coupler/overlap_mask.h @@ -3,6 +3,7 @@ #include "pcms/field/field_layout.h" #include "pcms/utility/arrays.h" +#include "pcms/utility/assert.h" #include #include @@ -14,17 +15,27 @@ struct OverlapMask Kokkos::View is_overlap_; std::function in_overlap_func_; - // Default constructor: all DOFs are in overlap + // Default constructor: all DOF holders 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)) + // Construct from a function that determines overlap from a DOF holder's + // classification (dimension, id). Holders without classification are passed + // as (-1, -1). + explicit OverlapMask(std::function in_overlap) + : 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 @@ -46,19 +57,22 @@ struct OverlapMask } } - // Get the mask, evaluating the function if needed - Rank1View GetMask( + // Get the mask, evaluating the function against the layout if one was given + Kokkos::View 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])); - } + if (!in_overlap_func_) { + return is_overlap_; + } + auto class_dims = layout.GetDOFHolderClassificationDimensionsHost(); + auto class_ids = layout.GetDOFHolderClassificationIdsHost(); + Kokkos::View is_overlap("overlap_info", + class_dims.size()); + 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_); + return is_overlap; } }; diff --git a/src/pcms/coupler/serializer/xgc.h b/src/pcms/coupler/serializer/xgc.h deleted file mode 100644 index e26583775..000000000 --- a/src/pcms/coupler/serializer/xgc.h +++ /dev/null @@ -1,104 +0,0 @@ -#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(); - // Per-holder plan: owned[i]/permutation[i] index holders; a holder's - // num_components values form one contiguous block in the wire buffer. - if (buffer.size() > 0) { - const LO num_dof = static_cast(data.extent(0)); - const LO num_comp = static_cast(data.extent(1)); - for (LO i = 0; i < num_dof; ++i) { - // A negative permutation entry marks a holder outside the exchange - // (non-owned, or owned but outside the overlap region); it has no slot. - if (permutation[i] >= 0) { - for (LO c = 0; c < num_comp; ++c) { - buffer[permutation[i] * num_comp + c] = data(i, c); - } - } - } - } - 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(); - const LO num_dof = static_cast(current.extent(0)); - const LO num_comp = static_cast(current.extent(1)); - std::vector full_data(current.size()); - for (LO i = 0; i < num_dof; ++i) { - for (LO c = 0; c < num_comp; ++c) { - full_data[i * num_comp + c] = current(i, c); - } - } - if (rank_participates_) { - for (LO i = 0; i < num_dof; ++i) { - // A negative permutation entry marks a holder outside the exchange - // (owned but outside the overlap region); no data was received for it, - // so it keeps its current value (pre-filled above). - if (permutation[i] >= 0) { - for (LO c = 0; c < num_comp; ++c) { - full_data[i * num_comp + c] = buffer[permutation[i] * num_comp + c]; - } - } - } - } - - MPI_Bcast(full_data.data(), static_cast(full_data.size()), - pcms::GetMPIType(T{}), 0, plane_comm_); - - xgc_field->SetDOFHolderDataHost(Rank2View( - full_data.data(), layout.GetNumOwnedDofHolder(), - layout.GetNumComponents())); - } - -private: - MPI_Comm plane_comm_; - bool rank_participates_; -}; - -} // namespace pcms - -#endif // PCMS_XGC_FIELD_SERIALIZER_H diff --git a/src/pcms/field/CMakeLists.txt b/src/pcms/field/CMakeLists.txt index fc15c3a6c..496f60e86 100644 --- a/src/pcms/field/CMakeLists.txt +++ b/src/pcms/field/CMakeLists.txt @@ -2,17 +2,24 @@ find_package(KokkosKernels REQUIRED) set(PCMS_FIELD_HEADERS field.h - coordinate_system.h + coordinate_system.hpp + coordinate_view.hpp + coordinate_systems/cartesian.hpp + coordinate_systems/cylindrical.hpp + coordinate_map.hpp + value_view.hpp element_dispatch.h evaluation_request.h gid_permutation.hpp 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 + point_evaluator_factory.hpp + transformed_source.hpp data/simple.h function_space/polynomial_reconstruction.hpp layout/point_cloud.h @@ -20,6 +27,9 @@ set(PCMS_FIELD_HEADERS ) set(PCMS_FIELD_SOURCES field_layout.cpp + coordinate_system.cpp + coordinate_map.cpp + transformed_source.cpp gid_permutation.cpp function_space/polynomial_reconstruction.cpp layout/empty.cpp @@ -46,6 +56,7 @@ if (PCMS_ENABLE_OMEGA_H) layout/uniform_grid.h uniform_grid_binary_field.h evaluator/uniform_grid.h + evaluator/spline_interpolator.hpp evaluator/uniform_grid_spline.h eqdsk_field.h evaluator/mls_options.h @@ -59,6 +70,18 @@ if (PCMS_ENABLE_OMEGA_H) ) endif () +if (PCMS_ENABLE_MFEM) + list(APPEND PCMS_FIELD_SOURCES + layout/mfem.cpp + data/mfem.cpp + ) + list(APPEND PCMS_FIELD_HEADERS + layout/mfem.hpp + data/mfem.hpp + function_space/mfem.hpp + ) +endif () + if (PCMS_ENABLE_MESHFIELDS) list(APPEND PCMS_FIELD_SOURCES layout/mesh_fields.cpp @@ -102,6 +125,10 @@ if(PCMS_ENABLE_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 () diff --git a/src/pcms/field/coordinate.h b/src/pcms/field/coordinate.h deleted file mode 100644 index 0bea892c5..000000000 --- a/src/pcms/field/coordinate.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef PCMS_COUPLING_COORDINATE_H -#define PCMS_COUPLING_COORDINATE_H -#include -#include "pcms/utility/types.h" -#include -namespace pcms -{ -template -class Coordinate -{ -public: - using ScalarT = CoordT; - template - constexpr Coordinate(Args... args) : data_{std::forward(args)...} - { - } - [[nodiscard]] - constexpr const std::array& Values() const noexcept - { - return data_; - } - [[nodiscard]] - constexpr ScalarT operator[](std::size_t i) const - { - return data_[i]; - } - -private: - std::array data_; -}; - -// TODO add explicit conversion to underlying type -template -struct CoordinateElement -{ - using value_type = ElementType; - using reference = value_type&; - using const_reference = const value_type&; - using coordinate_system = CoordinateSystem; - CoordinateElement(value_type data) : data_(std::move(data)) {} - - [[nodiscard]] - KOKKOS_FORCEINLINE_FUNCTION constexpr const_reference underlying() - const noexcept - { - return data_; - } - [[nodiscard]] - KOKKOS_FORCEINLINE_FUNCTION constexpr reference underlying() noexcept - { - return data_; - } - -private: - value_type data_; -}; - -} // namespace pcms - -#endif // PCMS_COUPLING_COORDINATE_H diff --git a/src/pcms/field/coordinate_map.cpp b/src/pcms/field/coordinate_map.cpp new file mode 100644 index 000000000..512b91ceb --- /dev/null +++ b/src/pcms/field/coordinate_map.cpp @@ -0,0 +1,344 @@ +#include "pcms/field/coordinate_map.hpp" +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/field/coordinate_systems/cylindrical.hpp" +#include "pcms/utility/assert.h" +#include "pcms/utility/profile.h" +#include +#include + +namespace pcms +{ + +namespace +{ + +using ExecutionSpace = DeviceMemorySpace::execution_space; + +const ValueBasis& CartesianPhysicalVector() +{ + static const ValueBasis basis{csys::Cartesian::Create(3), + ComponentScaling::Physical, + VarianceSignature{Variance::Contravariant}}; + return basis; +} + +const ValueBasis& CylindricalPhysicalVector() +{ + static const ValueBasis basis{csys::CylindricalRThetaZ::Create(), + ComponentScaling::Physical, + VarianceSignature{Variance::Contravariant}}; + return basis; +} + +Kokkos::View AllocateCoordinates(size_t num_points, + int dim) +{ + return Kokkos::View( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "mapped_points"), + num_points, static_cast(dim)); +} + +Kokkos::View AllocateTrig(const std::string& label, + size_t num_points) +{ + return Kokkos::View( + Kokkos::view_alloc(Kokkos::WithoutInitializing, label), num_points); +} + +/// Direction the query points travel through a cylindrical map. +enum class PointDirection +{ + /// (x, y, z) -> (r, theta, z), with theta from atan2(y, x) in (-pi, pi] + ToCylindrical, + /// (r, theta, z) -> (x, y, z) + ToCartesian +}; + +// Kernels as free functions: CUDA extended lambdas may not be defined inside +// member functions of classes with private members. + +/// @brief Maps query points between Cartesian and cylindrical coordinates and +/// caches the angle each point sits at. +/// +/// @param in query points, one per row, in the map's source coordinate system: +/// (x, y, z) for ToCylindrical, (r, theta, z) for ToCartesian +/// @param out receives the mapped points, one per row of `in`, in the map's +/// target coordinate system: (r, theta, z) for ToCylindrical, +/// (x, y, z) for ToCartesian. Must be preallocated with the extents of +/// `in` +/// @param cos_theta receives cos(theta) at each point; one entry per row of +/// `in` +/// @param sin_theta receives sin(theta) at each point; one entry per row of +/// `in` +/// @param direction which way the points are mapped +/// +/// @details This is the per-point work of Bind for both cylindrical maps. It +/// produces the mapped coordinates the source is evaluated at and, in the same +/// pass, the cos/sin of each point's angle. The angle is what the bound map +/// later needs to rotate vector components between the (e_r, e_theta, e_z) and +/// (e_x, e_y, e_z) bases, so caching it here lets every later TransformValues +/// call, one per evaluated field, reuse it instead of recomputing trig. z +/// passes through unchanged. The kernel is launched asynchronously and reads +/// `in`; CoordinateMap::Bind fences before the caller may release it. +void BindCylindricalPoints(Rank2View in, + Kokkos::View out, + Kokkos::View cos_theta, + Kokkos::View sin_theta, + PointDirection direction) +{ + Kokkos::parallel_for( + "cylindrical_map_bind", + Kokkos::RangePolicy(0, static_cast(in.extent(0))), + KOKKOS_LAMBDA(const LO i) { + Real c, s; + if (direction == PointDirection::ToCylindrical) { + const Real x = in(i, 0); + const Real y = in(i, 1); + const Real theta = Kokkos::atan2(y, x); + c = Kokkos::cos(theta); + s = Kokkos::sin(theta); + out(i, 0) = Kokkos::sqrt(x * x + y * y); + out(i, 1) = theta; + } else { + const Real r = in(i, 0); + c = Kokkos::cos(in(i, 1)); + s = Kokkos::sin(in(i, 1)); + out(i, 0) = r * c; + out(i, 1) = r * s; + } + out(i, 2) = in(i, 2); + cos_theta(i) = c; + sin_theta(i) = s; + }); +} + +void RotateValues(const Kokkos::View& c, + const Kokkos::View& s, Real sign, + Rank2View values, LO n) +{ + Kokkos::parallel_for( + "cylindrical_map_rotate_values", Kokkos::RangePolicy(0, n), + KOKKOS_LAMBDA(const LO i) { + const Real v0 = values(i, 0); + const Real v1 = values(i, 1); + const Real si = sign * s(i); + values(i, 0) = v0 * c(i) - v1 * si; + values(i, 1) = v0 * si + v1 * c(i); + }); +} + +/// Rotates physical vector components about z by +theta (cylindrical to +/// Cartesian) or -theta (Cartesian to cylindrical) at each bound point. +class BoundCylindricalRotation final : public BoundCoordinateMap +{ +public: + BoundCylindricalRotation(std::shared_ptr system, + Kokkos::View mapped, + Kokkos::View cos_theta, + Kokkos::View sin_theta, + const ValueBasis& stored, const ValueBasis& output, + Real sign) + : BoundCoordinateMap(std::move(system), std::move(mapped)), + cos_theta_(std::move(cos_theta)), + sin_theta_(std::move(sin_theta)), + stored_(stored), + output_(output), + sign_(sign) + { + } + + /// Values already in the output basis pass through unchanged: components + /// with respect to the output basis do not depend on where the point is + /// expressed, so only the points needed mapping. + [[nodiscard]] ValueBasis OutputBasis(const ValueBasis& stored) const override + { + if (!SameValueBasis(stored, stored_) && !SameValueBasis(stored, output_)) { + throw pcms_error( + "BoundCoordinateMap::OutputBasis: this map re-expresses values stored " + "in '" + + std::string(stored_.system->Kind()) + "' or '" + + std::string(output_.system->Kind()) + + "' physical contravariant components; it cannot re-express the basis " + "the field declares"); + } + return output_; + } + + ValueView TransformValues( + ValueView values) const override + { + PCMS_FUNCTION_TIMER; + const ValueBasis output = ValidateTransformValues( + "BoundCylindricalRotation::TransformValues", values); + if (!SameValueBasis(values.GetBasis(), output_)) { + RotateValues(cos_theta_, sin_theta_, sign_, values.GetValues(), + NumPoints()); + } + return ValueView(output, values.GetValues()); + } + +private: + Kokkos::View cos_theta_; + Kokkos::View sin_theta_; + ValueBasis stored_; + ValueBasis output_; + Real sign_; +}; + +// (R, Z) -> (R, theta0, Z). A free function: CUDA does not allow device +// lambdas in protected members such as BindImpl. +void EmbedPoloidalPoints(Rank2View in, + Kokkos::View out, + Real theta0) +{ + Kokkos::parallel_for( + "embed_poloidal_points", + Kokkos::RangePolicy(0, static_cast(in.extent(0))), + KOKKOS_LAMBDA(const LO i) { + out(i, 0) = in(i, 0); + out(i, 1) = theta0; + out(i, 2) = in(i, 1); + }); +} + +/// Values run opposite to points: a map into cylindrical coordinates rotates +/// cylindrical components into Cartesian ones, and vice versa. +std::unique_ptr BindCylindrical( + const CoordinateView& query_points, + std::shared_ptr target, PointDirection direction) +{ + const auto in = query_points.GetValues(); + const auto n = in.extent(0); + auto mapped = AllocateCoordinates(n, 3); + auto cos_theta = AllocateTrig("bind_cos_theta", n); + auto sin_theta = AllocateTrig("bind_sin_theta", n); + BindCylindricalPoints(in, mapped, cos_theta, sin_theta, direction); + const bool to_cylindrical = direction == PointDirection::ToCylindrical; + const auto& stored = + to_cylindrical ? CylindricalPhysicalVector() : CartesianPhysicalVector(); + const auto& output = + to_cylindrical ? CartesianPhysicalVector() : CylindricalPhysicalVector(); + return std::make_unique( + std::move(target), std::move(mapped), std::move(cos_theta), + std::move(sin_theta), stored, output, to_cylindrical ? 1.0 : -1.0); +} + +} // namespace + +BoundCoordinateMap::BoundCoordinateMap( + std::shared_ptr system, + Kokkos::View mapped) + : system_(std::move(system)), mapped_(std::move(mapped)) +{ +} + +CoordinateView BoundCoordinateMap::MappedPoints() const +{ + return CoordinateView(system_, + MakeConstRank2View(mapped_)); +} + +ValueBasis BoundCoordinateMap::OutputBasis(const ValueBasis&) const +{ + throw pcms_error("BoundCoordinateMap::OutputBasis: this coordinate map maps " + "points only and cannot re-express component values"); +} + +ValueView BoundCoordinateMap::TransformValues( + ValueView) const +{ + throw pcms_error("BoundCoordinateMap::TransformValues: this coordinate map " + "maps points only and cannot re-express component values"); +} + +ValueBasis BoundCoordinateMap::ValidateTransformValues( + const char* who, const ValueView& values) const +{ + ValueBasis output = OutputBasis(values.GetBasis()); + if (static_cast(values.extent(0)) != NumPoints()) { + throw pcms_error(std::string(who) + + ": value count does not match the bound point set"); + } + return output; +} + +CoordinateMap::CoordinateMap(std::shared_ptr source, + std::shared_ptr target) + : source_(std::move(source)), target_(std::move(target)) +{ + if (source_ == nullptr || target_ == nullptr) { + throw pcms_error("CoordinateMap: coordinate systems must not be null"); + } +} + +std::unique_ptr CoordinateMap::Bind( + const CoordinateView& query_points) const +{ + PCMS_FUNCTION_TIMER; + if (!SameCoordinateSystem(query_points.GetCoordinateSystem(), source_)) { + throw pcms_error("CoordinateMap::Bind: points are tagged with coordinate " + "system '" + + std::string(query_points.GetCoordinateSystem()->Kind()) + + "' but this map's source coordinate system is '" + + std::string(source_->Kind()) + "'"); + } + auto bound = BindImpl(query_points); + // BindImpl kernels read the caller's point buffer, which the caller may + // release as soon as Bind returns. + ExecutionSpace().fence("CoordinateMap::Bind: complete binding"); + if (bound == nullptr || + bound->NumPoints() != + static_cast(query_points.GetValues().extent(0)) || + !SameCoordinateSystem(bound->MappedPoints().GetCoordinateSystem(), + target_)) { + throw pcms_error("CoordinateMap::Bind: BindImpl must return one mapped " + "point per query point, in the target coordinate system"); + } + return bound; +} + +CartesianToCylindrical::CartesianToCylindrical() + : CoordinateMap(csys::Cartesian::Create(3), + csys::CylindricalRThetaZ::Create()) +{ +} + +std::unique_ptr CartesianToCylindrical::BindImpl( + const CoordinateView& query_points) const +{ + return BindCylindrical(query_points, GetTargetCoordinateSystem(), + PointDirection::ToCylindrical); +} + +CylindricalToCartesian::CylindricalToCartesian() + : CoordinateMap(csys::CylindricalRThetaZ::Create(), + csys::Cartesian::Create(3)) +{ +} + +std::unique_ptr CylindricalToCartesian::BindImpl( + const CoordinateView& query_points) const +{ + return BindCylindrical(query_points, GetTargetCoordinateSystem(), + PointDirection::ToCartesian); +} + +PoloidalPlaneEmbedding::PoloidalPlaneEmbedding(Real theta0) + : CoordinateMap(csys::CylindricalRZ::Create(), + csys::CylindricalRThetaZ::Create()), + theta0_(theta0) +{ +} + +std::unique_ptr PoloidalPlaneEmbedding::BindImpl( + const CoordinateView& query_points) const +{ + const auto in = query_points.GetValues(); + auto mapped = AllocateCoordinates(in.extent(0), 3); + EmbedPoloidalPoints(in, mapped, theta0_); + // The base bound map re-expresses no vector values; scalars never reach it. + return std::make_unique(GetTargetCoordinateSystem(), + std::move(mapped)); +} + +} // namespace pcms diff --git a/src/pcms/field/coordinate_map.hpp b/src/pcms/field/coordinate_map.hpp new file mode 100644 index 000000000..529460bed --- /dev/null +++ b/src/pcms/field/coordinate_map.hpp @@ -0,0 +1,143 @@ +#ifndef PCMS_FIELD_COORDINATE_MAP_HPP +#define PCMS_FIELD_COORDINATE_MAP_HPP + +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/coordinate_view.hpp" +#include "pcms/field/value_view.hpp" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include +#include + +namespace pcms +{ + +// A CoordinateMap bound to one query point set. Bind performs every per-point +// computation the map needs: the mapped coordinates and the state that +// re-expressing component values at those points requires. That state is the +// map's differential, which does not depend on the value basis -- the basis +// only selects which arithmetic is applied to it -- so one bound map serves +// every field evaluated at that point set. +// +// Rank-0 values never reach OutputBasis or TransformValues: scalars are basis +// invariant, so callers forward them without consulting the map. +class BoundCoordinateMap +{ +public: + BoundCoordinateMap(std::shared_ptr system, + Kokkos::View mapped); + + /// Query points expressed in the map's target coordinate system. + [[nodiscard]] CoordinateView MappedPoints() const; + + [[nodiscard]] LO NumPoints() const noexcept + { + return static_cast(mapped_.extent(0)); + } + + /// Value basis produced for rank >= 1 values stored in `stored`. Throws when + /// this map cannot re-express that basis. + [[nodiscard]] virtual ValueBasis OutputBasis(const ValueBasis& stored) const; + + /// Re-expresses `values` in place and returns the same buffer tagged + /// `OutputBasis(values.GetBasis())`. + virtual ValueView TransformValues( + ValueView values) const; + + virtual ~BoundCoordinateMap() = default; + + BoundCoordinateMap(const BoundCoordinateMap&) = delete; + BoundCoordinateMap& operator=(const BoundCoordinateMap&) = delete; + +protected: + /// Throws unless `values` holds NumPoints() rows in a basis this map + /// re-expresses; returns the basis the transformed values are in. + ValueBasis ValidateTransformValues( + const char* who, const ValueView& values) const; + +private: + std::shared_ptr system_; + Kokkos::View mapped_; +}; + +class CoordinateMap +{ +public: + [[nodiscard]] const std::shared_ptr& + GetSourceCoordinateSystem() const noexcept + { + return source_; + } + [[nodiscard]] const std::shared_ptr& + GetTargetCoordinateSystem() const noexcept + { + return target_; + } + + /// Performs every per-point computation for `query_points`, which must be in + /// this map's source coordinate system. The returned object owns its + /// results and is complete on return, so `query_points` may be released. + [[nodiscard]] std::unique_ptr Bind( + const CoordinateView& query_points) const; + + virtual ~CoordinateMap() = default; + +protected: + CoordinateMap(std::shared_ptr source, + std::shared_ptr target); + + /// Binds `query_points`, already checked to be in the source coordinate + /// system. May leave kernels in flight; Bind fences after it returns. + [[nodiscard]] virtual std::unique_ptr BindImpl( + const CoordinateView& query_points) const = 0; + +private: + std::shared_ptr source_; + std::shared_ptr target_; +}; + +// Maps points Cartesian -> cylindrical, so values run the other way: a field +// stored in cylindrical components is re-expressed in Cartesian ones. +class CartesianToCylindrical final : public CoordinateMap +{ +public: + CartesianToCylindrical(); + +protected: + [[nodiscard]] std::unique_ptr BindImpl( + const CoordinateView& query_points) const override; +}; + +class CylindricalToCartesian final : public CoordinateMap +{ +public: + CylindricalToCartesian(); + +protected: + [[nodiscard]] std::unique_ptr BindImpl( + const CoordinateView& query_points) const override; +}; + +// Places points of a poloidal (R, Z) plane at a fixed toroidal angle: +// (R, Z) -> (R, theta0, Z). This lets a 2D CylindricalRZ target query a 3D +// CylindricalRThetaZ source through a TransformedSource. +// +// Only scalar values (including multi-component scalars) are currently +// supported. +class PoloidalPlaneEmbedding final : public CoordinateMap +{ +public: + /// @param theta0 toroidal angle of the plane, in radians + explicit PoloidalPlaneEmbedding(Real theta0); + +protected: + [[nodiscard]] std::unique_ptr BindImpl( + const CoordinateView& query_points) const override; + +private: + Real theta0_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_COORDINATE_MAP_HPP diff --git a/src/pcms/field/coordinate_system.cpp b/src/pcms/field/coordinate_system.cpp new file mode 100644 index 000000000..1e57dae66 --- /dev/null +++ b/src/pcms/field/coordinate_system.cpp @@ -0,0 +1,32 @@ +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/utility/assert.h" +#include + +namespace pcms +{ +std::shared_ptr ResolveCoordinateSystem( + std::shared_ptr system, int dim) +{ + if (system == nullptr) { + throw pcms_error( + "ResolveCoordinateSystem: coordinate system must not be null"); + } + // To avoid requiring the user specifying the dimension twice in + // FunctionSpace constructors, we have a default + // Cartesian constructor that constructs with dimension 0. + if (dynamic_cast(system.get()) != nullptr && + system->Dimension() == 0) { + return csys::Cartesian::Create(dim); + } + if (system->Dimension() != dim) { + throw pcms_error("ResolveCoordinateSystem: coordinate system '" + + std::string(system->Kind()) + "' has " + + std::to_string(system->Dimension()) + + " coordinate columns but the data has " + + std::to_string(dim)); + } + return system; +} + +} // namespace pcms diff --git a/src/pcms/field/coordinate_system.h b/src/pcms/field/coordinate_system.h deleted file mode 100644 index ba627cd24..000000000 --- a/src/pcms/field/coordinate_system.h +++ /dev/null @@ -1,99 +0,0 @@ -#ifndef PCMS_COORDINATE_SYSTEM_H -#define PCMS_COORDINATE_SYSTEM_H -#include "pcms/utility/arrays.h" - -namespace pcms -{ - -enum class CoordinateSystem -{ - Cartesian, - Cylindrical, - XGC, - GNET, - BEAMS3D -}; - -template > -class CoordinateView -{ -public: - CoordinateView( - CoordinateSystem cs, - Rank2View coords) noexcept - : coordinate_system_(cs), coordinates_(coords) - { - } - - [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept - { - return coordinate_system_; - } - - [[nodiscard]] Rank2View GetValues() - const noexcept - { - return coordinates_; - } - - // 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 - GetValues() noexcept - { - return coordinates_; - } - void SetCoordinateSystem(CoordinateSystem cs) noexcept - { - coordinate_system_ = cs; - } - -private: - CoordinateSystem coordinate_system_; - Rank2View coordinates_; -}; - -class CoordinateTransformation -{ -public: - virtual void Evaluate(const CoordinateView from, - CoordinateView to) = 0; - // TODO - // #IFDEF PCMS_HAS_DEVICE - // virtual void Evaluate(const CoordinateView from, - // CoordinateView to) = 0; - - virtual ~CoordinateTransformation() noexcept = default; -}; - -/* -class CartesianToCylindrical : public CoordinateTransformation { -public: - void Evaluate(const CoordinateView from, -CoordinateView to) override; private: Rank2View from_; -}; - -class CylindricalToCartesian : public CoordinateTransformation { -public: - void Evaluate(const CoordinateView from, CoordinateView to) override; -private: - Rank2View from_; -}; - -// this function creates a transformation object based on the input coordinate -systems -// trhows an exception if the transformation is not supported -std::unique_ptr -CreateCoordinateTransformation(CoordinateSystem from, CoordinateSystem to); - -// this is a convenience function that creates a transformation and applies it -void Transform(const CoordinateView from, CoordinateView to); -*/ - -} // namespace pcms - -#endif // PCMS_COORDINATE_SYSTEM_H diff --git a/src/pcms/field/coordinate_system.hpp b/src/pcms/field/coordinate_system.hpp new file mode 100644 index 000000000..2fd41c132 --- /dev/null +++ b/src/pcms/field/coordinate_system.hpp @@ -0,0 +1,48 @@ +#ifndef PCMS_FIELD_COORDINATE_SYSTEM_HPP +#define PCMS_FIELD_COORDINATE_SYSTEM_HPP + +#include +#include + +namespace pcms +{ + +class CoordinateSystem +{ +public: + [[nodiscard]] virtual std::string_view Kind() const noexcept = 0; + + [[nodiscard]] virtual int Dimension() const noexcept = 0; + + [[nodiscard]] virtual bool HasOrthogonalBasis() const noexcept = 0; + + [[nodiscard]] virtual bool HasUnitScaleFactors() const noexcept = 0; + + [[nodiscard]] virtual bool operator==( + const CoordinateSystem& other) const noexcept = 0; + + virtual ~CoordinateSystem() = default; +}; + +[[nodiscard]] inline bool SameCoordinateSystem( + const std::shared_ptr& a, + const std::shared_ptr& b) noexcept +{ + if (a == nullptr || b == nullptr) { + return a == b; + } + return *a == *b; +} + +[[nodiscard]] inline bool HasIdentityMetric( + const CoordinateSystem& system) noexcept +{ + return system.HasOrthogonalBasis() && system.HasUnitScaleFactors(); +} + +[[nodiscard]] std::shared_ptr ResolveCoordinateSystem( + std::shared_ptr system, int dim); + +} // namespace pcms + +#endif // PCMS_FIELD_COORDINATE_SYSTEM_HPP diff --git a/src/pcms/field/coordinate_systems.h b/src/pcms/field/coordinate_systems.h deleted file mode 100644 index c01792f6f..000000000 --- a/src/pcms/field/coordinate_systems.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef PCMS_COUPLING_COORDINATE_SYSTEMS_H -#define PCMS_COUPLING_COORDINATE_SYSTEMS_H -// current design coordinate systems are just struct tags for each system, but -// eventually they may carry more data along with them -namespace pcms -{ -struct Cylindrical; -struct Cartesian; -} // namespace pcms - -#endif // PCMS_COUPLING_COORDINATE_SYSTEMS_H diff --git a/src/pcms/field/coordinate_systems/cartesian.hpp b/src/pcms/field/coordinate_systems/cartesian.hpp new file mode 100644 index 000000000..02b64199e --- /dev/null +++ b/src/pcms/field/coordinate_systems/cartesian.hpp @@ -0,0 +1,59 @@ +#ifndef PCMS_FIELD_COORDINATE_SYSTEMS_CARTESIAN_HPP +#define PCMS_FIELD_COORDINATE_SYSTEMS_CARTESIAN_HPP + +#include "pcms/field/coordinate_system.hpp" +#include "pcms/utility/assert.h" +#include + +namespace pcms +{ +namespace csys +{ + +class Cartesian final : public CoordinateSystem +{ +public: + [[nodiscard]] static std::shared_ptr Create(int dim) + { + if (dim < 1 || dim > 3) { + throw pcms_error("csys::Cartesian::Create: dimension must be 1..3, got " + + std::to_string(dim)); + } + return std::shared_ptr(new Cartesian(dim)); + } + + [[nodiscard]] static std::shared_ptr Deferred() + { + return std::shared_ptr(new Cartesian(0)); + } + + [[nodiscard]] std::string_view Kind() const noexcept override + { + return dim_ == 0 ? "cartesian (dimension deferred)" : "cartesian"; + } + [[nodiscard]] int Dimension() const noexcept override { return dim_; } + [[nodiscard]] bool HasOrthogonalBasis() const noexcept override + { + return true; + } + [[nodiscard]] bool HasUnitScaleFactors() const noexcept override + { + return true; + } + [[nodiscard]] bool operator==( + const CoordinateSystem& other) const noexcept override + { + const auto* p = dynamic_cast(&other); + return p != nullptr && p->dim_ == dim_; + } + +private: + explicit Cartesian(int dim) noexcept : dim_(dim) {} + + int dim_; +}; + +} // namespace csys +} // namespace pcms + +#endif // PCMS_FIELD_COORDINATE_SYSTEMS_CARTESIAN_HPP diff --git a/src/pcms/field/coordinate_systems/cylindrical.hpp b/src/pcms/field/coordinate_systems/cylindrical.hpp new file mode 100644 index 000000000..ecbbb41dd --- /dev/null +++ b/src/pcms/field/coordinate_systems/cylindrical.hpp @@ -0,0 +1,76 @@ +#ifndef PCMS_FIELD_COORDINATE_SYSTEMS_CYLINDRICAL_HPP +#define PCMS_FIELD_COORDINATE_SYSTEMS_CYLINDRICAL_HPP + +#include "pcms/field/coordinate_system.hpp" + +namespace pcms +{ +namespace csys +{ + +class CylindricalRThetaZ final : public CoordinateSystem +{ +public: + [[nodiscard]] static std::shared_ptr Create() + { + return std::shared_ptr(new CylindricalRThetaZ()); + } + + [[nodiscard]] std::string_view Kind() const noexcept override + { + return "cylindrical_rthetaz"; + } + [[nodiscard]] int Dimension() const noexcept override { return 3; } + [[nodiscard]] bool HasOrthogonalBasis() const noexcept override + { + return true; + } + [[nodiscard]] bool HasUnitScaleFactors() const noexcept override + { + return false; + } + [[nodiscard]] bool operator==( + const CoordinateSystem& other) const noexcept override + { + return dynamic_cast(&other) != nullptr; + } + +private: + CylindricalRThetaZ() noexcept = default; +}; + +class CylindricalRZ final : public CoordinateSystem +{ +public: + [[nodiscard]] static std::shared_ptr Create() + { + return std::shared_ptr(new CylindricalRZ()); + } + + [[nodiscard]] std::string_view Kind() const noexcept override + { + return "cylindrical_rz"; + } + [[nodiscard]] int Dimension() const noexcept override { return 2; } + [[nodiscard]] bool HasOrthogonalBasis() const noexcept override + { + return true; + } + [[nodiscard]] bool HasUnitScaleFactors() const noexcept override + { + return true; + } + [[nodiscard]] bool operator==( + const CoordinateSystem& other) const noexcept override + { + return dynamic_cast(&other) != nullptr; + } + +private: + CylindricalRZ() noexcept = default; +}; + +} // namespace csys +} // namespace pcms + +#endif // PCMS_FIELD_COORDINATE_SYSTEMS_CYLINDRICAL_HPP diff --git a/src/pcms/field/coordinate_transform.h b/src/pcms/field/coordinate_transform.h deleted file mode 100644 index acc5a7b43..000000000 --- a/src/pcms/field/coordinate_transform.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef PCMS_COUPLING_COORDINATE_TRANSFORM_H -#define PCMS_COUPLING_COORDINATE_TRANSFORM_H -#include "coordinate.h" -#include "coordinate_systems.h" -#include -namespace pcms -{ -template -constexpr Coordinate CoordinateTransform( - Coordinate original_coordinate) noexcept -{ - static_assert(detail::dependent_always_false::value, - "Missing coordinate transform specialization."); -} - -template <> -constexpr Coordinate CoordinateTransform( - Coordinate coord) noexcept -{ - auto r = coord[0]; - auto theta = coord[1]; - auto x = r * cos(theta); - auto y = r * sin(theta); - return {x, y, coord[2]}; -} -template <> -constexpr Coordinate CoordinateTransform( - Coordinate coord) noexcept -{ - auto x = coord[0]; - auto y = coord[1]; - auto r = sqrt(x * x + y * y); - auto theta = atan2(y, x); - return {r, theta, coord[2]}; -} - -} // namespace pcms - -#endif // PCMS_COUPLING_COORDINATE_TRANSFORM_H diff --git a/src/pcms/field/coordinate_view.hpp b/src/pcms/field/coordinate_view.hpp new file mode 100644 index 000000000..075ec727b --- /dev/null +++ b/src/pcms/field/coordinate_view.hpp @@ -0,0 +1,46 @@ +#ifndef PCMS_FIELD_COORDINATE_VIEW_HPP +#define PCMS_FIELD_COORDINATE_VIEW_HPP + +#include "pcms/field/coordinate_system.hpp" +#include "pcms/utility/arrays.h" +#include "pcms/utility/assert.h" +#include +#include + +namespace pcms +{ + +template > +class CoordinateView +{ +public: + CoordinateView(std::shared_ptr coordinate_system, + Rank2View coords) + : system_(ResolveCoordinateSystem(std::move(coordinate_system), + static_cast(coords.extent(1)))), + coordinates_(coords) + { + } + + [[nodiscard]] const std::shared_ptr& + GetCoordinateSystem() const noexcept + { + return system_; + } + + [[nodiscard]] Rank2View GetValues() + const noexcept + { + return coordinates_; + } + +private: + std::shared_ptr system_; + Rank2View coordinates_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_COORDINATE_VIEW_HPP diff --git a/src/pcms/field/data/mesh_fields.h b/src/pcms/field/data/mesh_fields.h index 4c27a49d3..0a60b59af 100644 --- a/src/pcms/field/data/mesh_fields.h +++ b/src/pcms/field/data/mesh_fields.h @@ -4,7 +4,7 @@ #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/field/value_view.hpp" #include "pcms/utility/assert.h" #include "pcms/utility/arrays.h" @@ -14,19 +14,46 @@ namespace pcms { +namespace +{ +// Functor that flattens component-major 2D data (LayoutLeft) into dof-major +// order. template -class MeshFieldsFieldData : public FieldData +struct FlattenFunctor +{ + Rank2View data_; + Kokkos::View flat_; + LO row_off_; + size_t nc_; + FlattenFunctor(Rank2View d, + Kokkos::View f, LO ro, size_t nc) + : data_(d), flat_(f), row_off_(ro), nc_(nc) + { + } + KOKKOS_INLINE_FUNCTION void operator()(LO local) const + { + LO global_dof = row_off_ + local; + for (size_t c = 0; c < nc_; ++c) { + flat_(local * nc_ + c) = data_(global_dof, c); + } + } +}; +} // namespace + +template +class MeshFieldsFieldData + : public FieldData + , public DOFAccess { public: MeshFieldsFieldData(std::shared_ptr layout, - FieldMetadata metadata) + ValueBasis basis) : layout_(std::move(layout)), - metadata_(metadata), + basis_(std::move(basis)), mesh_field_(MakeMeshFieldBackend(*layout_)), - host_data_("meshfields_field_data", - static_cast(layout_->OwnedSize())), device_data_("meshfields_field_data_device", - static_cast(layout_->OwnedSize())) + static_cast(layout_->GetNumOwnedDofHolder()), + static_cast(layout_->GetNumComponents())) { if (!mesh_field_) { throw pcms_error( @@ -34,40 +61,24 @@ class MeshFieldsFieldData : public FieldData } } - const FieldMetadata& GetMetadata() const override { return metadata_; } - - Rank2View GetDOFHolderDataHost() const override + FieldValueType GetValueType() const override { - Kokkos::deep_copy(host_data_, device_data_); - return Rank2View(host_data_.data(), - layout_->GetNumOwnedDofHolder(), - layout_->GetNumComponents()); + return ValueTypeOfRank(basis_.Rank()); } + const ValueBasis& GetValueBasis() const override { return basis_; } - void SetDOFHolderDataHost(Rank2View values) override - { - PCMS_ALWAYS_ASSERT(values.size() == - static_cast(layout_->OwnedSize())); - CopyHostRank2ViewToDeviceView(device_data_, values); - SyncBackend(GetDOFHolderData()); - } + const DOFAccess* GetDOFAccess() const noexcept override { return this; } + DOFAccess* GetDOFAccess() noexcept override { return this; } - Rank2View GetDOFHolderData() const override + Rank2View GetOwnedValues() const override { - // The Rank2View will wrap the dof-major data with layout left when device - // memory is enabled. This may cause issues in multi component cases. See - // issue #342 - return Rank2View( - device_data_.data(), layout_->GetNumOwnedDofHolder(), - layout_->GetNumComponents()); + return MakeConstRank2View(device_data_); } - void SetDOFHolderData(Rank2View values) override + void SetOwnedValues(Rank2View values) override { - PCMS_ALWAYS_ASSERT(values.size() == - static_cast(layout_->OwnedSize())); CopyDeviceRank2ViewToDeviceView(device_data_, values); - SyncBackend(GetDOFHolderData()); + SyncBackend(GetOwnedValues()); } std::shared_ptr> GetMeshFieldBackend() const @@ -76,23 +87,30 @@ class MeshFieldsFieldData : public FieldData } private: + // Serialization boundary: meshfields' SetData consumes flat node-major + // spans, so the shaped data is explicitly repacked into a flat node-major + // staging buffer here — the one place this backend handles flat memory. void SyncBackend(Rank2View data) { auto nodes_per_dim = layout_->GetNodesPerDim(); auto num_components = layout_->GetNumComponents(); auto& mesh = layout_->GetMesh(); - // data is [dof_holder][component], contiguous node-major, so each mesh - // dimension owns a contiguous block of rows; SetData consumes a flat - // node-major span over that block. + // device_data_ is rank-2 LayoutLeft (component-major), but SetData + // expects a flat dof-major span. size_t row_offset = 0; for (int i = 0; i <= mesh.dim(); ++i) { if (nodes_per_dim[i]) { size_t num_rows = static_cast(mesh.nents(i)) * static_cast(nodes_per_dim[i]); size_t len = num_rows * static_cast(num_components); - Rank1View subspan{ - data.data_handle() + row_offset * static_cast(num_components), - len}; + Kokkos::View flat("sync_flat", len); + Kokkos::parallel_for( + "SyncBackendReorder", + Kokkos::RangePolicy( + 0, static_cast(num_rows)), + FlattenFunctor(data, flat, static_cast(row_offset), + num_components)); + Rank1View subspan(flat.data(), len); mesh_field_->SetData(subspan, nodes_per_dim[i], num_components, i); row_offset += num_rows; } @@ -100,10 +118,9 @@ class MeshFieldsFieldData : public FieldData } std::shared_ptr layout_; - FieldMetadata metadata_; + ValueBasis basis_; std::shared_ptr> mesh_field_; - mutable Kokkos::View host_data_; - Kokkos::View device_data_; + Kokkos::View device_data_; }; } // namespace pcms diff --git a/src/pcms/field/data/mfem.cpp b/src/pcms/field/data/mfem.cpp new file mode 100644 index 000000000..00405bad9 --- /dev/null +++ b/src/pcms/field/data/mfem.cpp @@ -0,0 +1,97 @@ +#include "pcms/field/data/mfem.hpp" + +#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, + ValueBasis basis) + : pfes_(pfes), + gf_(gf), + basis_(std::move(basis)), + host_data_("mfem_field_data_host", pfes.GetNDofs(), 1), + device_data_("mfem_field_data_device", pfes.GetNDofs(), 1) +{ + PCMS_ALWAYS_ASSERT(pfes_.GetVDim() == 1); + if (basis_.Rank() != 0) { + throw pcms_error("MFEMVertexFieldData: only scalar fields are supported"); + } +} + +FieldValueType MFEMVertexFieldData::GetValueType() const +{ + return ValueTypeOfRank(basis_.Rank()); +} + +const ValueBasis& MFEMVertexFieldData::GetValueBasis() const +{ + return basis_; +} + +Rank2View MFEMVertexFieldData::GatherHost() 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, 0) = (lt >= 0) ? static_cast(true_values[lt]) : Real{0}; + } + + return MakeConstRank2View(host_data_); +} + +void MFEMVertexFieldData::ScatterHost( + Rank2View data) +{ + PCMS_FUNCTION_TIMER; + const int nv = pfes_.GetNDofs(); + PCMS_ALWAYS_ASSERT(static_cast(data.extent(0)) == nv); + PCMS_ALWAYS_ASSERT(data.extent(1) == 1); + + // 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; + 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, 0)); + } + } + + gf_.SetFromTrueDofs(true_values); +} + +Rank2View MFEMVertexFieldData::GetOwnedValues() + const +{ + GatherHost(); + DeepCopyMismatchLayouts(device_data_, host_data_); + return MakeConstRank2View(device_data_); +} + +void MFEMVertexFieldData::SetOwnedValues( + Rank2View data) +{ + CopyDeviceRank2ViewToDeviceView(device_data_, data); + DeepCopyMismatchLayouts(host_data_, device_data_); + ScatterHost(MakeConstRank2View(host_data_)); +} + +} // namespace pcms diff --git a/src/pcms/field/data/mfem.hpp b/src/pcms/field/data/mfem.hpp new file mode 100644 index 000000000..be7837a13 --- /dev/null +++ b/src/pcms/field/data/mfem.hpp @@ -0,0 +1,44 @@ +#ifndef PCMS_FIELD_DATA_MFEM_HPP +#define PCMS_FIELD_DATA_MFEM_HPP + +#include "pcms/field/field_data.h" +#include "pcms/field/value_view.hpp" +#include "pcms/utility/arrays.h" + +#include + +namespace pcms +{ + +// FieldData backend for an MFEM order-1 H1 (vertex) scalar field. +class MFEMVertexFieldData + : public FieldData + , public DOFAccess +{ +public: + MFEMVertexFieldData(mfem::ParFiniteElementSpace& pfes, + mfem::ParGridFunction& gf, ValueBasis basis = {}); + + FieldValueType GetValueType() const override; + const ValueBasis& GetValueBasis() const override; + + const DOFAccess* GetDOFAccess() const noexcept override { return this; } + DOFAccess* GetDOFAccess() noexcept override { return this; } + + Rank2View GetOwnedValues() const override; + void SetOwnedValues(Rank2View data) override; + +private: + Rank2View GatherHost() const; + void ScatterHost(Rank2View data); + + mfem::ParFiniteElementSpace& pfes_; + mfem::ParGridFunction& gf_; + ValueBasis basis_; + mutable Kokkos::View host_data_; + mutable Kokkos::View device_data_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_DATA_MFEM_HPP diff --git a/src/pcms/field/data/point_cloud.cpp b/src/pcms/field/data/point_cloud.cpp index d333726bc..d50e512d3 100644 --- a/src/pcms/field/data/point_cloud.cpp +++ b/src/pcms/field/data/point_cloud.cpp @@ -9,44 +9,30 @@ namespace pcms PointCloud::PointCloud(std::shared_ptr layout) : layout_(std::move(layout)), - metadata_{}, - device_data_("", layout_->GetDOFHolderCoordinates().GetValues().extent(0)), - data_host_("", layout_->GetDOFHolderCoordinates().GetValues().extent(0)) + basis_{}, + device_data_("", layout_->GetDOFHolderCoordinates().GetValues().extent(0), + static_cast(layout_->GetNumComponents())) { } -const FieldMetadata& PointCloud::GetMetadata() const +FieldValueType PointCloud::GetValueType() const { - return metadata_; + return ValueTypeOfRank(basis_.Rank()); } -Rank2View PointCloud::GetDOFHolderDataHost() const +const ValueBasis& PointCloud::GetValueBasis() const { - Kokkos::deep_copy(data_host_, device_data_); - const auto nc = layout_->GetNumComponents(); - return Rank2View( - data_host_.data(), static_cast(data_host_.size()) / nc, nc); + return basis_; } -void PointCloud::SetDOFHolderDataHost( - Rank2View data) +Rank2View PointCloud::GetOwnedValues() const { - PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(data.size() == device_data_.size()); - CopyHostRank2ViewToDeviceView(device_data_, data); -} - -Rank2View PointCloud::GetDOFHolderData() const -{ - const auto nc = layout_->GetNumComponents(); - return Rank2View( - device_data_.data(), static_cast(device_data_.size()) / nc, nc); + return MakeConstRank2View(device_data_); } -void PointCloud::SetDOFHolderData(Rank2View data) +void PointCloud::SetOwnedValues(Rank2View data) { PCMS_FUNCTION_TIMER; - PCMS_ALWAYS_ASSERT(data.size() == device_data_.size()); CopyDeviceRank2ViewToDeviceView(device_data_, data); } diff --git a/src/pcms/field/data/point_cloud.h b/src/pcms/field/data/point_cloud.h index eedcc0b4d..a7006b9e0 100644 --- a/src/pcms/field/data/point_cloud.h +++ b/src/pcms/field/data/point_cloud.h @@ -2,32 +2,33 @@ #define POINT_CLOUD_H_ #include "pcms/field/field_data.h" -#include "pcms/field/field_metadata.h" +#include "pcms/field/value_view.hpp" #include "pcms/utility/arrays.h" #include "pcms/field/layout/point_cloud.h" #include namespace pcms { -class PointCloud : public FieldData +class PointCloud + : public FieldData + , public DOFAccess { public: PointCloud(std::shared_ptr layout); - const FieldMetadata& GetMetadata() const override; + FieldValueType GetValueType() const override; + const ValueBasis& GetValueBasis() const override; - Rank2View GetDOFHolderDataHost() const override; - void SetDOFHolderDataHost( - Rank2View data) override; + const DOFAccess* GetDOFAccess() const noexcept override { return this; } + DOFAccess* GetDOFAccess() noexcept override { return this; } - Rank2View GetDOFHolderData() const override; - void SetDOFHolderData(Rank2View data) override; + Rank2View GetOwnedValues() const override; + void SetOwnedValues(Rank2View data) override; private: std::shared_ptr layout_; - FieldMetadata metadata_; - Kokkos::View device_data_; - mutable Kokkos::View data_host_; + ValueBasis basis_; + Kokkos::View device_data_; }; } // namespace pcms diff --git a/src/pcms/field/data/simple.h b/src/pcms/field/data/simple.h index 6deda15ae..facfd032e 100644 --- a/src/pcms/field/data/simple.h +++ b/src/pcms/field/data/simple.h @@ -3,7 +3,6 @@ #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" @@ -14,63 +13,49 @@ 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.). +// SimpleFieldData is a generic concrete FieldData backed by shaped +// [dof_holder][component] Kokkos Views. // // 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 +class SimpleFieldData + : public FieldData + , public DOFAccess { public: - SimpleFieldData(std::shared_ptr layout, - FieldMetadata metadata) + SimpleFieldData(std::shared_ptr layout, ValueBasis basis) : layout_(std::move(layout)), - metadata_(metadata), - host_data_("simple_field_data", - static_cast(layout_->OwnedSize())), + basis_(std::move(basis)), device_data_("simple_field_data_device", - static_cast(layout_->OwnedSize())) + static_cast(layout_->GetNumOwnedDofHolder()), + static_cast(layout_->GetNumComponents())) { } - const FieldMetadata& GetMetadata() const override { return metadata_; } - - Rank2View GetDOFHolderDataHost() const override + FieldValueType GetValueType() const override { - Kokkos::deep_copy(host_data_, device_data_); - return Rank2View(host_data_.data(), - layout_->GetNumOwnedDofHolder(), - layout_->GetNumComponents()); + return ValueTypeOfRank(basis_.Rank()); } + const ValueBasis& GetValueBasis() const override { return basis_; } - void SetDOFHolderDataHost(Rank2View values) override - { - PCMS_ALWAYS_ASSERT(values.size() == - static_cast(layout_->OwnedSize())); - CopyHostRank2ViewToDeviceView(device_data_, values); - } + const DOFAccess* GetDOFAccess() const noexcept override { return this; } + DOFAccess* GetDOFAccess() noexcept override { return this; } - Rank2View GetDOFHolderData() const override + Rank2View GetOwnedValues() const override { - return Rank2View( - device_data_.data(), layout_->GetNumOwnedDofHolder(), - layout_->GetNumComponents()); + return MakeConstRank2View(device_data_); } - void SetDOFHolderData(Rank2View values) override + void SetOwnedValues(Rank2View values) override { - PCMS_ALWAYS_ASSERT(values.size() == - static_cast(layout_->OwnedSize())); CopyDeviceRank2ViewToDeviceView(device_data_, values); } private: std::shared_ptr layout_; - FieldMetadata metadata_; - mutable Kokkos::View host_data_; - Kokkos::View device_data_; + ValueBasis basis_; + Kokkos::View device_data_; }; } // namespace pcms diff --git a/src/pcms/field/data/xgc.h b/src/pcms/field/data/xgc.h index cb7281a2d..9caf89d74 100644 --- a/src/pcms/field/data/xgc.h +++ b/src/pcms/field/data/xgc.h @@ -4,21 +4,34 @@ #include "pcms/field/layout/xgc.h" #include "pcms/field/field_data.h" #include "pcms/utility/assert.h" +#include "pcms/utility/mpi_type.h" #include +#include #include namespace pcms { template -class XGCFieldData : public FieldData +class XGCFieldData + : public FieldData + , public DOFAccess { 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) + // + // XGC keeps a full copy of a plane's data on every rank of the plane, and + // only the plane's root rank (rank 0 of plane_comm) exchanges it. When + // plane_comm is given, SetOwnedValues broadcasts the root's values to the + // other ranks of the plane, so every rank of plane_comm must call it. + XGCFieldData(std::shared_ptr layout, ValueBasis basis, + Rank1View data, + MPI_Comm plane_comm = MPI_COMM_NULL) + : layout_(std::move(layout)), + basis_(std::move(basis)), + data_(data), + plane_comm_(plane_comm) { PCMS_ALWAYS_ASSERT(layout_ != nullptr); PCMS_ALWAYS_ASSERT(static_cast(data_.size()) == @@ -27,10 +40,9 @@ class XGCFieldData : public FieldData // Self-allocating constructor: XGCFunctionSpace::CreateFieldImpl uses this // to produce a field with internally-managed storage. - XGCFieldData(std::shared_ptr layout, - FieldMetadata metadata) + XGCFieldData(std::shared_ptr layout, ValueBasis basis) : layout_(std::move(layout)), - metadata_(metadata), + basis_(std::move(basis)), owned_data_("xgc_field_data", static_cast(layout_->GetFullDataSize())), data_(owned_data_.data(), owned_data_.extent(0)) @@ -38,57 +50,67 @@ class XGCFieldData : public FieldData PCMS_ALWAYS_ASSERT(layout_ != nullptr); } - const FieldMetadata& GetMetadata() const override { return metadata_; } + FieldValueType GetValueType() const override + { + return ValueTypeOfRank(basis_.Rank()); + } + const ValueBasis& GetValueBasis() const override { return basis_; } + + const DOFAccess* GetDOFAccess() const noexcept override { return this; } + DOFAccess* GetDOFAccess() noexcept override { return this; } - Rank2View GetDOFHolderDataHost() const override + Rank2View GetOwnedValues() const override { + EnsureDeviceStaging(); const auto nc = layout_->GetNumComponents(); - return Rank2View( - data_.data_handle(), static_cast(data_.size()) / nc, nc); + CopyHostRank2ViewToDeviceView( + device_data_, + Rank2View( + data_.data_handle(), static_cast(data_.size()) / nc, nc)); + return MakeConstRank2View(device_data_); } - void SetDOFHolderDataHost(Rank2View values) override + void SetOwnedValues(Rank2View values) override { - if (values.size() != data_.size()) { - throw pcms_error("XGCFieldData::SetDOFHolderDataHost: size mismatch"); - } - const auto num_dof = values.extent(0); - const auto num_comp = values.extent(1); - for (size_t i = 0; i < num_dof; ++i) { - for (size_t c = 0; c < num_comp; ++c) { - data_(i * num_comp + c) = values(i, c); + EnsureDeviceStaging(); + CopyDeviceRank2ViewToDeviceView(device_data_, values); + // Explicit node-major serialization into the adapter buffer (the mirror + // keeps the device layout, so it is indexed, not aliased). + auto host_mirror = Kokkos::create_mirror_view(device_data_); + Kokkos::deep_copy(host_mirror, device_data_); + const auto nc = host_mirror.extent(1); + for (size_t i = 0; i < host_mirror.extent(0); ++i) { + for (size_t c = 0; c < nc; ++c) { + data_(i * nc + c) = host_mirror(i, c); } } - } - - Rank2View 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); + if (plane_comm_ != MPI_COMM_NULL) { + MPI_Bcast(data_.data_handle(), static_cast(data_.size()), + GetMPIType(T{}), 0, plane_comm_); } - Kokkos::deep_copy(device_data_, host_view); - const auto nc = layout_->GetNumComponents(); - return Rank2View( - device_data_.data(), static_cast(device_data_.extent(0)) / nc, nc); } - void SetDOFHolderData(Rank2View values) override +private: + void EnsureDeviceStaging() const { - // Fill the node-major device scratch from the (possibly non-node-major) - // 2D view, then mirror it back into the canonical host storage. - CopyDeviceRank2ViewToDeviceView(device_data_, values); - CopyRank1ViewToHost(data_, make_const_array_view(device_data_)); + const auto nc = static_cast(layout_->GetNumComponents()); + const auto n = data_.size() / nc; + if (device_data_.extent(0) != n || device_data_.extent(1) != nc) { + device_data_ = Kokkos::View( + Kokkos::view_alloc(Kokkos::WithoutInitializing, + "xgc_field_data_device"), + n, nc); + } } -private: std::shared_ptr layout_; - FieldMetadata metadata_; + ValueBasis basis_; // owned_data_ is non-empty only when the self-allocating constructor is used. Kokkos::View owned_data_; Rank1View data_; - mutable Kokkos::View device_data_; + // Shaped device staging, allocated on first device access. + mutable Kokkos::View device_data_; + MPI_Comm plane_comm_ = MPI_COMM_NULL; }; } // namespace pcms diff --git a/src/pcms/field/eqdsk_field.h b/src/pcms/field/eqdsk_field.h index bb4188c86..5dbbe5284 100644 --- a/src/pcms/field/eqdsk_field.h +++ b/src/pcms/field/eqdsk_field.h @@ -6,13 +6,14 @@ #include "pcms/field/function_space/spline.h" #include +#include "pcms/field/coordinate_systems/cylindrical.hpp" namespace pcms { struct EQDSKField { - SplineFunctionSpace space; + std::shared_ptr space; Field field; }; @@ -20,7 +21,7 @@ struct EQDSKField struct EQDSKFieldWithData { EQDSKData data; - SplineFunctionSpace space; + std::shared_ptr space; Field field; }; @@ -41,10 +42,11 @@ struct EQDSKFieldWithData */ inline EQDSKField MakeEQDSKField(const EQDSKData& eqdsk_data) { + // EQDSK is a poloidal plane in cylindrical RZ coordinates auto space = SplineFunctionSpace::FromUniformGrid( - eqdsk_data.grid, CoordinateSystem::Cartesian); - auto field = space.CreateField(); - field.GetData().SetDOFHolderData(Rank2View( + eqdsk_data.grid, csys::CylindricalRZ::Create()); + Field field = space->CreateFunction(); + field.SetDOFHolderDataUnchecked(Rank2View( eqdsk_data.PSIZR.data(), eqdsk_data.PSIZR.extent(0), 1)); return {std::move(space), std::move(field)}; } diff --git a/src/pcms/field/evaluation_request.h b/src/pcms/field/evaluation_request.h index 943985a8e..e26b6cdaf 100644 --- a/src/pcms/field/evaluation_request.h +++ b/src/pcms/field/evaluation_request.h @@ -1,7 +1,7 @@ #ifndef PCMS_EVALUATION_REQUEST_H #define PCMS_EVALUATION_REQUEST_H -#include "coordinate_system.h" +#include "pcms/field/coordinate_view.hpp" #include "field_layout.h" #include "out_of_bounds_policy.h" #include "pcms/utility/arrays.h" diff --git a/src/pcms/field/evaluator/mesh_fields.h b/src/pcms/field/evaluator/mesh_fields.h index 277664ce2..6f9a8c78c 100644 --- a/src/pcms/field/evaluator/mesh_fields.h +++ b/src/pcms/field/evaluator/mesh_fields.h @@ -33,17 +33,29 @@ class MeshFieldsPointEvaluator : public PointEvaluator { } - void Evaluate( + [[nodiscard]] LO NumPoints() const override + { + return static_cast(hint_.coordinates_.extent(0) + hint_.num_missing_); + } + + [[nodiscard]] Kokkos::View FilledPoints() + const override + { + if (hint_.mode_ != OutOfBoundsMode::FILL) { + return {}; + } + return hint_.missing_indices_d_; + } + + // Public because CUDA forbids extended lambdas in non-public member + // functions. User should call Evaluate + // TODO: add passkey or other mechanism to avoid user calling this + void EvaluateImpl( const Field& field, - Rank2View values) const override + ValueView out) 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())); - // ensure that only scalar fields are supported - PCMS_ALWAYS_ASSERT(layout_->GetNumComponents() == 1); + const auto values = out.GetValues(); auto const* mesh_field_data = dynamic_cast*>(&field.GetData()); if (!mesh_field_data) { @@ -51,27 +63,26 @@ class MeshFieldsPointEvaluator : public PointEvaluator "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) + int ncomp = layout_->GetNumComponents(); + int num_eval = static_cast(eval_results.extent(0)); 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); + Kokkos::MDRangePolicy>({0, 0}, {num_eval, ncomp}), + KOKKOS_CLASS_LAMBDA(LO i, int c) { + values(hint_.indices_d_(i), c) = eval_results(i, c); }); if (hint_.num_missing_ > 0 && hint_.mode_ == OutOfBoundsMode::FILL) { T fill_val = static_cast(fill_value_); + int num_missing = static_cast(hint_.num_missing_); Kokkos::parallel_for( "FillMissingValues", - Kokkos::RangePolicy( - 0, hint_.num_missing_), - KOKKOS_CLASS_LAMBDA(LO i) { - values(hint_.missing_indices_d_(i), 0) = fill_val; + Kokkos::MDRangePolicy>({0, 0}, {num_missing, ncomp}), + KOKKOS_CLASS_LAMBDA(LO i, int c) { + values(hint_.missing_indices_d_(i), c) = fill_val; }); } } @@ -98,19 +109,10 @@ class MeshFieldsEvaluatorFactory : public FieldEvaluatorFactory 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; } @@ -121,10 +123,6 @@ class MeshFieldsEvaluatorFactory : public FieldEvaluatorFactory 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"); diff --git a/src/pcms/field/evaluator/mesh_fields_backend.h b/src/pcms/field/evaluator/mesh_fields_backend.h index 84eb206d0..0db4d1f4e 100644 --- a/src/pcms/field/evaluator/mesh_fields_backend.h +++ b/src/pcms/field/evaluator/mesh_fields_backend.h @@ -6,13 +6,14 @@ #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/localization/uniform_grid_localization.hpp" #include "pcms/field/field.h" // OutOfBoundsMode namespace pcms @@ -26,8 +27,8 @@ class MeshFieldBackend { public: virtual ~MeshFieldBackend() = default; - virtual Kokkos::View evaluate(Kokkos::View localCoords, - Kokkos::View offsets) const = 0; + 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, @@ -37,23 +38,25 @@ class MeshFieldBackend // --------------------------------------------------------------------------- // Concrete backend implementation // --------------------------------------------------------------------------- -template +template class MeshFieldBackendImpl : public MeshFieldBackend { public: MeshFieldBackendImpl(Omega_h::Mesh& mesh) : mesh_(mesh), mesh_field_(mesh), - shape_field_(mesh_field_.template CreateLagrangeField()) + shape_field_( + mesh_field_.template CreateLagrangeField()) { } - Kokkos::View evaluate(Kokkos::View localCoords, - Kokkos::View offsets) const override + Kokkos::View evaluate(Kokkos::View localCoords, + Kokkos::View offsets) const override { - auto self = const_cast*>(this); + auto self = + const_cast*>(this); return self->mesh_field_.triangleLocalPointEval(localCoords, offsets, - shape_field_); + shape_field_.field); } void SetData(Rank1View data, size_t num_nodes, @@ -65,7 +68,7 @@ class MeshFieldBackendImpl : public MeshFieldBackend 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) = + shape_field_.field(ent, n, c, topo) = data[ent * stride + n * num_components + c]; } } @@ -82,7 +85,7 @@ class MeshFieldBackendImpl : public MeshFieldBackend 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); + shape_field_.field(ent, n, c, topo); } } }); @@ -91,14 +94,47 @@ class MeshFieldBackendImpl : public MeshFieldBackend private: Omega_h::Mesh& mesh_; MeshField::OmegahMeshField mesh_field_; - using ShapeField = - decltype(mesh_field_.template CreateLagrangeField()); - ShapeField shape_field_; + using FWC = + decltype(mesh_field_ + .template CreateLagrangeField()); + FWC shape_field_; // FWC = FieldWithController; keeps ctrlr alive }; // --------------------------------------------------------------------------- // Factory function: create a MeshFieldBackend from a layout // --------------------------------------------------------------------------- + +// Helper to dispatch on num_components at runtime for a fixed (Dim, Order). +template +std::shared_ptr> MakeBackendForComponents( + Omega_h::Mesh& mesh, int num_components) +{ + switch (num_components) { + case 1: + return std::make_shared>(mesh); + case 2: + return std::make_shared>(mesh); + case 3: + return std::make_shared>(mesh); + case 4: + return std::make_shared>(mesh); + case 5: + return std::make_shared>(mesh); + case 6: + return std::make_shared>(mesh); + case 7: + return std::make_shared>(mesh); + case 8: + return std::make_shared>(mesh); + case 9: + return std::make_shared>(mesh); + default: + throw pcms_error("MeshFieldBackend: num_components " + + std::to_string(num_components) + + " exceeds maximum supported (9)."); + } +} + template std::shared_ptr> MakeMeshFieldBackend( const MeshFieldsAdapterLayout& layout) @@ -115,18 +151,19 @@ std::shared_ptr> MakeMeshFieldBackend( throw pcms_error("MeshFieldBackend does not support 3D meshes"); } auto nodes_per_dim = layout.GetNodesPerDim(); + int num_components = layout.GetNumComponents(); 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); + case 1: return MakeBackendForComponents(mesh, num_components); + case 2: return MakeBackendForComponents(mesh, num_components); 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); + case 2: return MakeBackendForComponents(mesh, num_components); + case 3: return MakeBackendForComponents(mesh, num_components); default: break; } } @@ -240,7 +277,7 @@ struct FillCoordinatesAndIndicesFunctor const auto owner_idx = owning_elem_ids_(i); LO count = Kokkos::atomic_sub_fetch(&elem_counts_(owner_idx), 1); LO index = offsets_(owner_idx) + count - 1; - for (int j = 0; j < (dim_ + 1); ++j) { + for (int j = 0; j < dim_; ++j) { coordinates_(index, j) = coord[j]; } indices_(index) = i; @@ -404,8 +441,9 @@ struct FillCoordinatesDeviceFunctor global_coords_(orig_idx, 1)}; const auto local = Omega_h::barycentric_from_global<2, 2>(point, vertex_coords); - for (int j = 0; j < (dim_ + 1); ++j) + for (int j = 0; j < dim_; ++j) { coordinates_(index, j) = local[j]; + } indices_(index) = orig_idx; } }; @@ -495,7 +533,7 @@ struct MeshFieldsAdapter2LocalizationHint offsets_ = Kokkos::View("offsets", mesh.nelems() + 1); coordinates_ = Kokkos::View( - "coordinates", num_valid_, mesh.dim() + 1); + "coordinates", num_valid_, mesh.dim()); indices_ = Kokkos::View("indices", num_valid_); if (num_missing_ > 0) { @@ -537,8 +575,9 @@ struct MeshFieldsAdapter2LocalizationHint global_coords(orig_idx, 1)}; const auto local = Omega_h::barycentric_from_global<2, 2>(point, vertex_coords); - for (int j = 0; j < (mesh.dim() + 1); ++j) + for (int j = 0; j < mesh.dim(); ++j) { coordinates_(index, j) = local[j]; + } indices_(index) = static_cast(orig_idx); } @@ -632,7 +671,7 @@ struct MeshFieldsAdapter2LocalizationHint // Step 6: Fill coordinates and indices on device coordinates_d_ = - Kokkos::View("coordinates_d", num_valid_, mesh.dim() + 1); + Kokkos::View("coordinates_d", num_valid_, mesh.dim()); indices_d_ = Kokkos::View("indices_d", num_valid_); const auto tris2verts = mesh.ask_elem_verts(); const auto mesh_coords = mesh.coords(); @@ -654,7 +693,7 @@ struct MeshFieldsAdapter2LocalizationHint // 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); + "coordinates_", num_valid_, mesh.dim()); DeepCopyMismatchLayouts(coordinates_, coordinates_d_); indices_ = Kokkos::create_mirror_view(indices_d_); if (num_missing_ > 0) { @@ -681,4 +720,4 @@ struct MeshFieldsAdapter2LocalizationHint } // namespace pcms -#endif // PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_BACKEND_H +#endif // PCMS_ADAPTER_MESHFIELDS_MESH_FIELDS_BACKEND_H \ No newline at end of file diff --git a/src/pcms/field/evaluator/mls_point_cloud.h b/src/pcms/field/evaluator/mls_point_cloud.h index 7000bd000..1a66ee2d1 100644 --- a/src/pcms/field/evaluator/mls_point_cloud.h +++ b/src/pcms/field/evaluator/mls_point_cloud.h @@ -37,17 +37,33 @@ class MLSPointEvaluator : public PointEvaluator { } - void Evaluate( + /// MLS evaluates every query point; it never fills. + [[nodiscard]] Kokkos::View FilledPoints() + const override + { + return {}; + } + + [[nodiscard]] LO NumPoints() const override + { + return static_cast(target_coords_.size() / dim_); + } + + // Public because CUDA forbids extended lambdas in non-public member + // functions. User call Evaluate. + // TODO: add passkey or other mechanism to protect this + void EvaluateImpl( const Field& field, - Rank2View values) const override + ValueView out) const override { + const auto values = out.GetValues(); 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 auto device_data = field.GetDOFHolderData().GetValues(); const int n_sources = static_cast(device_data.size()); Omega_h::Write src_w(n_sources, "mls_source_values"); @@ -63,7 +79,6 @@ class MLSPointEvaluator : public PointEvaluator 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), diff --git a/src/pcms/field/evaluator/omega_h_lagrange.h b/src/pcms/field/evaluator/omega_h_lagrange.h index 39ed0c94b..2fd55c797 100644 --- a/src/pcms/field/evaluator/omega_h_lagrange.h +++ b/src/pcms/field/evaluator/omega_h_lagrange.h @@ -6,7 +6,7 @@ #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/localization/uniform_grid_localization.hpp" #include "pcms/utility/assert.h" #include "pcms/utility/arrays.h" #include "pcms/utility/profile.h" @@ -37,7 +37,6 @@ struct OmegaHLagrangeLocHint namespace detail { -template struct CopyCoordsFunctor { using DefaultLayout = @@ -56,22 +55,18 @@ struct CopyCoordsFunctor KOKKOS_INLINE_FUNCTION void operator()(const LO i) const { - for (int d = 0; d < Dim; ++d) { + for (int d = 0; d < coords_d.extent(1); ++d) { coords_d(i, d) = raw_coords(i, d); } } }; -template OmegaHLagrangeLocHint BuildLagrangeLocHint( - Omega_h::Mesh& mesh, int mesh_dim, - Kokkos::View::Result*, - DeviceMemorySpace> - results, + Omega_h::Mesh& mesh, int mesh_dim, PointSearch::Results& results, Kokkos::View coords_d, Kokkos::View owning_ids, OutOfBoundsMode mode) { - LO n = static_cast(results.size()); + LO n = static_cast(results.dimensionalities.size()); // First pass: count valid and missing Kokkos::View is_valid("is_valid", n); @@ -79,7 +74,7 @@ OmegaHLagrangeLocHint BuildLagrangeLocHint( "CheckValidity", Kokkos::RangePolicy(0, n), KOKKOS_LAMBDA(LO i) { - bool out = (owning_ids(i) < 0) || (results(i).element_id < 0); + bool out = (owning_ids(i) < 0) || (results.element_ids(i) < 0); is_valid(i) = out ? 0 : 1; }); @@ -179,13 +174,16 @@ OmegaHLagrangeLocHint BuildLagrangeLocHint( mode}; } -inline std::variant MakeSearch( +inline std::shared_ptr 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); + if (mesh.dim() == 2) { + const auto n = DivisionsPerAxisForMesh<2>(mesh.nelems()); + return std::make_shared(mesh, n, n); + } else if (mesh.dim() == 3) { + const auto n = DivisionsPerAxisForMesh<3>(mesh.nelems()); + return std::make_shared(mesh, n, n, n); + } throw std::invalid_argument( "OmegaHLagrangeEvaluatorFactory: only 2D and 3D meshes are supported"); } @@ -217,17 +215,25 @@ class OmegaHLagrangePointEvaluator : public PointEvaluator { } - void Evaluate( + [[nodiscard]] LO NumPoints() const override + { + return static_cast(hint_.elem_ids.extent(0) + + hint_.missing_indices.extent(0)); + } + + // Public because CUDA forbids extended lambdas in non-public member + // functions. User call Evaluate. + // TODO: add passkey or other mechanism to protect this + void EvaluateImpl( const Field& field, - Rank2View values) const override + ValueView out) const override { PCMS_FUNCTION_TIMER; - auto dof_data = field.GetDOFHolderData(); + const auto values = out.GetValues(); + const auto dof_data = field.GetDOFHolderData().GetValues(); 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", @@ -279,6 +285,15 @@ class OmegaHLagrangePointEvaluator : public PointEvaluator } } + [[nodiscard]] Kokkos::View FilledPoints() + const override + { + if (hint_.mode != OutOfBoundsMode::FILL) { + return {}; + } + return hint_.missing_indices; + } + private: std::shared_ptr layout_; OmegaHLagrangeLocHint hint_; @@ -305,53 +320,44 @@ class OmegaHLagrangeEvaluatorFactory : public FieldEvaluatorFactory 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; } + /// The containing-element search over this layout's mesh. Exposed so that + /// operators which must locate points on the same mesh, such as the + /// conservative transfer's intersection search, reuse it rather than build + /// and hold a second copy of the candidate map. + [[nodiscard]] const std::shared_ptr& GetSearch() const noexcept + { + return search_; + } + 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.GetValues(); - LO n_pts = static_cast(raw_coords.extent(0)); int mesh_dim = layout_->GetMesh().dim(); Omega_h::Mesh& mesh = const_cast(layout_->GetMesh()); - - 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); - auto owning_ids = search.GetOwningElementIds(results_d); - - return detail::BuildLagrangeLocHint( + auto results_d = search_->Apply(coords); + auto owning_ids = search_->GetOwningElementIds(results_d); + + auto raw_coords = coords.GetValues(); + 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", static_cast(raw_coords.extent(0)), copy_functor); + + OmegaHLagrangeLocHint hint = detail::BuildLagrangeLocHint( mesh, mesh_dim, results_d, coords_d, owning_ids, policy.mode); - }, - search_); - + return std::make_unique>( layout_, std::move(hint), policy.fill_value); } @@ -363,7 +369,7 @@ class OmegaHLagrangeEvaluatorFactory : public FieldEvaluatorFactory private: std::shared_ptr layout_; - mutable std::variant search_; + std::shared_ptr search_; }; } // namespace pcms diff --git a/src/pcms/field/evaluator/point_cloud.h b/src/pcms/field/evaluator/point_cloud.h index 2ea37d516..9d62ef1ac 100644 --- a/src/pcms/field/evaluator/point_cloud.h +++ b/src/pcms/field/evaluator/point_cloud.h @@ -16,19 +16,11 @@ #include #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: @@ -43,11 +35,6 @@ class PointCloudEvaluatorFactory : public FieldEvaluatorFactory 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 @@ -61,14 +48,15 @@ class PointCloudEvaluatorFactory : public FieldEvaluatorFactory 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) { + // MLS distances are based on the unweighted Euclidean measure + // this is only valid with identity metric + // this almost certainly should be generalized to take metric for distances + // for now, we just check and fail so at least the user gets a warning + if (!HasIdentityMetric(*layout_->GetCoordinateSystem())) { throw pcms_error( - "PointCloudEvaluatorFactory: only Cartesian coordinates are " - "supported for MLS point-cloud evaluation"); + "PointCloudEvaluatorFactory: MLS point-cloud evaluation requires a " + "coordinate system whose metric is the identity; got '" + + std::string(layout_->GetCoordinateSystem()->Kind()) + "'"); } // Extract source coordinates for the MLS solve. diff --git a/src/pcms/field/evaluator/spline_interpolator.hpp b/src/pcms/field/evaluator/spline_interpolator.hpp index c59973829..fcae0c77f 100644 --- a/src/pcms/field/evaluator/spline_interpolator.hpp +++ b/src/pcms/field/evaluator/spline_interpolator.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace pcms { @@ -54,7 +55,7 @@ bool isUniformAscending(Rank1View x, T ztol) KOKKOS_LAMBDA(const LO ix, bool& local_flag) { T zdiffx = x(ix) - x(ix - 1); T zdiff = zdiffx - dxavg; - if (zdiffx < 0.0 || std::abs(zdiff) > zeps) { + if (!(zdiffx > 0) || !(std::abs(zdiff) <= zeps)) { local_flag = false; } }, @@ -65,9 +66,15 @@ bool isUniformAscending(Rank1View x, T ztol) template void grid_valid(Rank1View x) { - PCMS_ALWAYS_ASSERT(x.extent(0) >= 2); - bool is_uniform_ascending = isUniformAscending(x, 1.0e-3); - PCMS_ALWAYS_ASSERT(is_uniform_ascending); + if (x.extent(0) < 2) { + throw pcms_error("spline grid needs at least two points"); + } + bool is_uniform_ascending = isUniformAscending( + x, std::sqrt(std::numeric_limits::epsilon())); + if (!is_uniform_ascending) { + throw pcms_error( + "spline grid must be finite, strictly ascending and uniform"); + } } /** \brief Clamp xget to the grid range [x[0], x[nx-1]] with tolerance @@ -88,10 +95,10 @@ KOKKOS_INLINE_FUNCTION void clamp_to_grid_range(T xget, T& zxget, Rank1View x, LO nx, LO& ier, T tol) { - if (xget < x[0] || xget > x[nx - 1]) { - T zxtol = tol * std::max(std::abs(x[0]), std::abs(x[nx - 1])); + if (!(xget >= x[0] && xget <= x[nx - 1])) { + T zxtol = tol * Kokkos::max(std::abs(x[0]), std::abs(x[nx - 1])); - if (xget < x[0] - zxtol || xget > x[nx - 1] + zxtol) { + if (!(xget >= x[0] - zxtol && xget <= x[nx - 1] + zxtol)) { ier = 1; // Error code for out of range printf("lookup: xget=%.6f out of range %.6f to %.6f\n", xget, x[0], x[nx - 1]); @@ -105,6 +112,29 @@ KOKKOS_INLINE_FUNCTION void clamp_to_grid_range(T xget, T& zxget, } } +/** \brief Shift xget by whole periods into [x[0], x[nx-1]] + * + * \param xget input coordinate + * \param x input grid points + * \param nx number of grid points + * \param periodic whether the axis is periodic + * + * \details Coordinates inside the grid range, and all coordinates on a + * non-periodic axis, are returned unchanged. + */ +template +KOKKOS_INLINE_FUNCTION T wrap_periodic(T xget, Rank1View x, + LO nx, bool periodic) +{ + if (!periodic || (xget >= x[0] && xget <= x[nx - 1])) + return xget; + const T period = x[nx - 1] - x[0]; + T offset = Kokkos::fmod(xget - x[0], period); + if (offset < 0) + offset += period; + return x[0] + offset; +} + /** \brief Detect if the boundary conditions are homogeneous * * \param bcmin boundary condition data @@ -125,6 +155,7 @@ void detect_inhomogeneous_bc(Rank1View bcmin, LO ibcmin, { using execution_space = typename MemorySpace::execution_space; if (ibcmin == 1 || ibcmin == 2) { + LO nonzero = 0; Kokkos::parallel_reduce( "check_nonzero", Kokkos::RangePolicy(0, nx), KOKKOS_LAMBDA(const LO ix, LO& local_flag) { @@ -132,7 +163,8 @@ void detect_inhomogeneous_bc(Rank1View bcmin, LO ibcmin, local_flag = 1; } }, - Kokkos::Max(iflg2)); + Kokkos::Max(nonzero)); + iflg2 = std::max(iflg2, nonzero); } } @@ -174,8 +206,8 @@ KOKKOS_FUNCTION void compute_spline_coefficients(LO k_bc1, LO k_bcn, LO n, LO i3perio = 0; if (n == 3) { - i_bc1 = std::min(6, i_bc1); - i_bcn = std::min(6, i_bcn); + i_bc1 = Kokkos::min(6, i_bc1); + i_bcn = Kokkos::min(6, i_bcn); if (i_bc1 == 0) i3knots += 1; @@ -255,7 +287,6 @@ KOKKOS_FUNCTION void compute_spline_coefficients(LO k_bc1, LO k_bcn, LO n, f(2, 1) = 0.0; f(3, 1) = 0.0; } else if (iord1 == 1 && iord2 == 1) { - T a1 = f(1, 0), an = f(1, 1); f(1, 0) = a1; f(1, 1) = an; h = dx; @@ -270,7 +301,6 @@ KOKKOS_FUNCTION void compute_spline_coefficients(LO k_bc1, LO k_bcn, LO n, f(2, 1) = f(3, 0) * h + f(2, 0); } else if (iord1 == 1 && iord2 == 2) { - T a1 = f(1, 0), bn = f(2, 1); f(1, 0) = a1; f(2, 1) = bn; h = dx; @@ -285,7 +315,6 @@ KOKKOS_FUNCTION void compute_spline_coefficients(LO k_bc1, LO k_bcn, LO n, f(1, 1) = f(3, 0) * h * h / 2 + f(2, 0) * h + a1; } else if (iord1 == 2 && iord2 == 1) { - T b1 = f(2, 0), an = f(1, 1); f(2, 0) = b1; f(1, 1) = an; h = dx; @@ -300,7 +329,6 @@ KOKKOS_FUNCTION void compute_spline_coefficients(LO k_bc1, LO k_bcn, LO n, f(2, 1) = f(3, 0) * h + f(2, 0); } else if (iord1 == 2 && iord2 == 2) { - T b1 = f(2, 0), bn = f(2, 1); f(2, 0) = b1; f(2, 1) = bn; h = dx; @@ -473,6 +501,11 @@ KOKKOS_FUNCTION void compute_spline_coefficients(LO k_bc1, LO k_bcn, LO n, T elem21 = f(3, 0); T elemnn1 = f(3, n - 2); + // Read before the left condition: at n = 4 a not-a-knot left end rescales + // f(2, n - 3) + const T third_diff_n = + i_bcn == 7 ? f(2, n - 2) / (2 * dx) - f(2, n - 3) / (2 * dx) : T(0); + // Left boundary conditions if (i_bc1 == -1) { f(1, 0) = 2.0 * (f(3, 0) + f(3, n - 2)); @@ -510,8 +543,7 @@ KOKKOS_FUNCTION void compute_spline_coefficients(LO k_bc1, LO k_bcn, LO n, elemnn1 = 0.0; } else if (i_bcn == 7) { f(1, n - 1) = -f(3, n - 2); - f(2, n - 1) = f(2, n - 2) / (2 * dx) - f(2, n - 3) / (2 * dx); - f(2, n - 1) = -f(2, n - 1) * f(3, n - 2) * f(3, n - 2) / (3 * dx); + f(2, n - 1) = -third_diff_n * f(3, n - 2) * f(3, n - 2) / (3 * dx); } else if (i_bc1 != -1) { imax = n - 2; f(1, n - 2) = 2.0 * f(3, n - 3) + f(3, n - 2); @@ -661,10 +693,10 @@ KOKKOS_FUNCTION void solve_cubic_spline_explicit( * \param dx displacement from the grid point * \param fspl spline coefficients */ -template +template KOKKOS_INLINE_FUNCTION void cubic_evalfn_explicit( - Kokkos::View selector, Rank1View fval, LO i, - T dx, Rank2View fspl) + Kokkos::View selector, const Out& fval, LO i, T dx, + Rank2View fspl) { LO iaval = 0; @@ -702,11 +734,11 @@ KOKKOS_INLINE_FUNCTION LO lookup_in_grid(T zxget, Rank1View x, { LO nxm = nx - 1; LO ii = static_cast(nxm * (zxget - x[0]) / (x[nx - 1] - x[0])); - LO i = std::min(nxm - 1, ii); + LO i = Kokkos::min(nxm - 1, ii); if (zxget < x[i]) { - i = std::max(0, i - 1); + i = Kokkos::max(0, i - 1); } else if (zxget > x[i + 1]) { - i = std::min(nxm - 1, i + 1); + i = Kokkos::min(nxm - 1, i + 1); } return i; } @@ -731,6 +763,8 @@ KOKKOS_INLINE_FUNCTION void cubic_lookup_explicit(T xget, // Range check clamp_to_grid_range(xget, zxget, x, nx, ier, lookup_tol); + if (ier != 0) + return; i = lookup_in_grid(zxget, x, nx); @@ -752,11 +786,11 @@ KOKKOS_INLINE_FUNCTION void cubic_lookup_explicit(T xget, * single point. Note that this function does nothing when tje input point is * out of the grid range (ier is set to non-zero). */ -template +template KOKKOS_INLINE_FUNCTION void cubic_eval_explicit( - T xget, Kokkos::View iselect, - Rank1View fval, Rank1View x, LO nx, - Rank2View fspl, LO& ier, T lookup_tol) + T xget, Kokkos::View iselect, const Out& fval, + Rank1View x, LO nx, Rank2View fspl, LO& ier, + T lookup_tol) { LO ia = 0; T dxa = 0.0; @@ -804,10 +838,10 @@ KOKKOS_FUNCTION void solve_cubic_spline_compact( * \param hxi inverse step size in the x direction * \param fs2 compact spline coefficients */ -template +template KOKKOS_INLINE_FUNCTION void cubic_evalfn_compact( - Kokkos::View selector, Rank1View fval, LO i, - T xparam, T hx, T hxi, Rank2View fs2) + Kokkos::View selector, const Out& fval, LO i, T xparam, + T hx, T hxi, Rank2View fs2) { const T sixth = 1.0 / 6.0; LO iadr = 0; @@ -882,6 +916,8 @@ KOKKOS_INLINE_FUNCTION void cubic_lookup_compact(T xget, T zxget = xget; clamp_to_grid_range(xget, zxget, x, nx, ier, lookup_tol); + if (ier != 0) + return; i = lookup_in_grid(zxget, x, nx); @@ -901,9 +937,9 @@ KOKKOS_INLINE_FUNCTION void cubic_lookup_compact(T xget, * \param ier error flag, 0 if successful, non-zero otherwise * \param lookup_tol tolerance for out-of-bound checking */ -template +template KOKKOS_INLINE_FUNCTION void cubic_eval_compact( - T xget, Kokkos::View ict, Rank1View fval, + T xget, Kokkos::View ict, const Out& fval, Rank1View x, LO nx, Rank2View fspl, LO& ier, T lookup_tol) { @@ -972,10 +1008,10 @@ KOKKOS_INLINE_FUNCTION void bicubic_lookup_explicit( * \param dy displacement from the grid cell in y direction * \param fspl bicubic spline coefficients */ -template +template KOKKOS_INLINE_FUNCTION void bicubic_evalfn_explicit( - Kokkos::View ict, Rank1View fval, LO i, - LO j, T dx, T dy, Rank4View fspl) + Kokkos::View ict, const Out& fval, LO i, LO j, T dx, T dy, + Rank4View fspl) { LO iaval = 0; // Index for fval if (ict[0] <= 2) { @@ -1175,12 +1211,11 @@ KOKKOS_INLINE_FUNCTION void bicubic_evalfn_explicit( * \param ier error flag, 0 if successful, non-zero otherwise * \param lookup_tol tolerance for looking up the grid point */ -template +template KOKKOS_INLINE_FUNCTION void bicubic_eval_explicit( - T xget, T yget, Kokkos::View iselect, - Rank1View fval, Rank1View x, LO nx, - Rank1View y, LO ny, Rank4View fspl, LO& ier, - T lookup_tol) + T xget, T yget, Kokkos::View iselect, const Out& fval, + Rank1View x, LO nx, Rank1View y, LO ny, + Rank4View fspl, LO& ier, T lookup_tol) { LO i = 0; LO j = 0; @@ -1239,6 +1274,7 @@ void solve_bicubic_spline_explicit(Rank1View x, LO inx, fspl_x_view(0, ix) = fspl(0, 0, ix, iy); }); + team.team_barrier(); if (team.team_rank() == 0) { if (ibcxmin == 1) fspl_x_view(1, 0) = bcxmin[iy]; @@ -1253,6 +1289,7 @@ void solve_bicubic_spline_explicit(Rank1View x, LO inx, compute_spline_coefficients(ibcxmin, ibcxmax, inx, x, fspl_x_view, wk_x_view); } + team.team_barrier(); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, inx), [=](LO ix) { fspl(1, 0, ix, iy) = fspl_x_view(1, ix); @@ -1277,6 +1314,7 @@ void solve_bicubic_spline_explicit(Rank1View x, LO inx, }); // Set linear BCs initially + team.team_barrier(); if (team.team_rank() == 0) { fspl_y_view(1, 0) = 0.0; fspl_y_view(2, 0) = 0.0; @@ -1295,6 +1333,7 @@ void solve_bicubic_spline_explicit(Rank1View x, LO inx, compute_spline_coefficients( ibcymina, ibcymaxa, iny, y, fspl_y_view, wk_y_view); } + team.team_barrier(); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, iny), [=](LO iy) { fspl(ic, 1, ix, iy) = fspl_y_view(1, iy); @@ -1305,9 +1344,6 @@ void solve_bicubic_spline_explicit(Rank1View x, LO inx, }); if (iflg2 == 1) { - LO iasc = 0; // Workspace base for correction splines - LO iinc = 4 * iny; // Spacing between correction splines - T zhxn = get_element_from_span(x, inx - 1) - get_element_from_span(x, inx - 2); T zhy = @@ -1344,7 +1380,10 @@ void solve_bicubic_spline_explicit(Rank1View x, LO inx, auto zcur = Rank1View(zcur_shared.data_handle() + ix * 3, 3); T zdiff1 = 0.0, zdiff2 = 0.0; - auto wk_y = Rank1View(wk_l.data_handle(), iny); + auto wk_y = + Rank1View(wk_l.data_handle() + ix * iny, iny); + auto corr_y = Rank2View( + fspl_l_y.data_handle() + 4 * ix * iny, 4, iny); if (ibcymin == 1) { zcur[0] = @@ -1389,42 +1428,42 @@ void solve_bicubic_spline_explicit(Rank1View x, LO inx, zdiff2 = bcymax[ix] - zcur(0); } - LO iadr = iasc + ix * iinc; for (LO iy = 0; iy < iny; ++iy) - fspl_l_y(ix * 4, iy) = 0.0; + corr_y(0, iy) = 0.0; - fspl_l_y(1 + ix * 4, 0) = 0.0; - fspl_l_y(2 + ix * 4, 0) = 0.0; - fspl_l_y(1 + ix * 4, iny - 1) = 0.0; - fspl_l_y(2 + ix * 4, iny - 1) = 0.0; + corr_y(1, 0) = 0.0; + corr_y(2, 0) = 0.0; + corr_y(1, iny - 1) = 0.0; + corr_y(2, iny - 1) = 0.0; if (ibcymin == 1) - fspl_l_y(1 + ix * 4, 0) = zdiff1; + corr_y(1, 0) = zdiff1; else if (ibcymin == 2) - fspl_l_y(2 + ix * 4, 0) = zdiff1; + corr_y(2, 0) = zdiff1; if (ibcymax == 1) - fspl_l_y(1 + ix * 4, iny - 1) = zdiff2; + corr_y(1, iny - 1) = zdiff2; else if (ibcymax == 2) - fspl_l_y(2 + ix * 4, iny - 1) = zdiff2; + corr_y(2, iny - 1) = zdiff2; - auto fspl_s_y = - Rank2View(fspl_l_y.data_handle() + iadr, 4, iny); - compute_spline_coefficients(ibcymin, ibcymax, iny, y, fspl_s_y, wk_y); + compute_spline_coefficients(ibcymin, ibcymax, iny, y, corr_y, wk_y); }); Kokkos::parallel_for( - Kokkos::MDRangePolicy>({0, 0}, {inx, iny - 1}), + Kokkos::MDRangePolicy>({0, 0}, + {inx, iny - 1}), KOKKOS_LAMBDA(const LO ix, const LO iy) { + auto corr_y = Rank2View( + fspl_l_y.data_handle() + 4 * ix * iny, 4, iny); // Multiply local coefficients - fspl_l_y(2 + ix * 4, iy) *= xo2; - fspl_l_y(3 + ix * 4, iy) *= xo6; + corr_y(2, iy) *= xo2; + corr_y(3, iy) *= xo6; // Conditional accumulation LOo global fspl if (ix < inx - 1) { - fspl(0, 1, ix, iy) += fspl_l_y(1 + ix * 4, iy); - fspl(0, 2, ix, iy) += fspl_l_y(2 + ix * 4, iy); - fspl(0, 3, ix, iy) += fspl_l_y(3 + ix * 4, iy); + fspl(0, 1, ix, iy) += corr_y(1, iy); + fspl(0, 2, ix, iy) += corr_y(2, iy); + fspl(0, 3, ix, iy) += corr_y(3, iy); } }); Kokkos::parallel_for( @@ -1439,9 +1478,12 @@ void solve_bicubic_spline_explicit(Rank1View x, LO inx, for (LO ic = 1; ic < 4; ++ic) { Kokkos::parallel_for(Kokkos::TeamThreadRange(team, inx), [=](LO ix) { - fspl_x_view(0, ix) = fspl_l_y(ic + ix * 4, iy); + auto corr_y = Rank2View( + fspl_l_y.data_handle() + 4 * ix * iny, 4, iny); + fspl_x_view(0, ix) = corr_y(ic, iy); }); + team.team_barrier(); if (team.team_rank() == 0) { fspl_x_view(1, 0) = 0.0; fspl_x_view(2, 0) = 0.0; @@ -1451,6 +1493,7 @@ void solve_bicubic_spline_explicit(Rank1View x, LO inx, compute_spline_coefficients(ibcxmin, ibcxmax, inx, x, fspl_x_view, wk_x_view); } + team.team_barrier(); Kokkos::parallel_for( Kokkos::TeamThreadRange(team, inx - 1), [=](LO ix) { @@ -1493,6 +1536,8 @@ KOKKOS_INLINE_FUNCTION void bicubic_lookup_compact( clamp_to_grid_range(xget, zxget, x, nx, ier, lookup_tol); clamp_to_grid_range(yget, zyget, y, ny, ier, lookup_tol); + if (ier != 0) + return; i = lookup_in_grid(zxget, x, nx); j = lookup_in_grid(zyget, y, ny); @@ -1521,11 +1566,11 @@ KOKKOS_INLINE_FUNCTION void bicubic_lookup_compact( * \param hyi inverse step size in the y direction * \param f bicubic spline coefficients */ -template +template KOKKOS_INLINE_FUNCTION void bicubic_evalfn_compact( - Kokkos::View ict, LO ivec, LO ivecd, - Rank1View fval, LO i, LO j, T xparam, T yparam, T hx, T hxi, - T hy, T hyi, Rank3View f) + Kokkos::View ict, LO ivec, LO ivecd, const Out& fval, LO i, + LO j, T xparam, T yparam, T hx, T hxi, T hy, T hyi, + Rank3View f) { constexpr T sixth = 1.0 / 6.0; const T z36th = sixth * sixth; @@ -1825,12 +1870,11 @@ KOKKOS_INLINE_FUNCTION void bicubic_evalfn_compact( * \param ier error flag, 0 if successful, non-zero otherwise * \param lookup_tol tolerance for the lookup */ -template +template KOKKOS_INLINE_FUNCTION void bicubic_eval_compact( - T xget, T yget, Kokkos::View ict, - Rank1View fval, Rank1View x, LO nx, - Rank1View y, LO ny, Rank3View f, LO& ier, - T lookup_tol) + T xget, T yget, Kokkos::View ict, const Out& fval, + Rank1View x, LO nx, Rank1View y, LO ny, + Rank3View f, LO& ier, T lookup_tol) { // Local variables LO i = 0, j = 0; @@ -1892,6 +1936,7 @@ void solve_bicubic_spline_compact(Rank1View x, LO nx, Kokkos::parallel_for(Kokkos::TeamThreadRange(team, nx), [=](LO ix) { fwk_x_view(0, ix) = f(0, ix, iy); }); + team.team_barrier(); if (team.team_rank() == 0) { if (ibcxmin == 1 || ibcxmin == 2) zbcmin = bcxmin[iy]; @@ -1901,6 +1946,7 @@ void solve_bicubic_spline_compact(Rank1View x, LO nx, solve_cubic_spline_compact(x, nx, fwk_x_view, fwk4_x_view, ibcxmin, zbcmin, ibcxmax, zbcmax, wk_x_view); } + team.team_barrier(); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, nx), [=](LO ix) { f(1, ix, iy) = fwk_x_view(1, ix); }); @@ -1922,6 +1968,7 @@ void solve_bicubic_spline_compact(Rank1View x, LO nx, Kokkos::parallel_for(Kokkos::TeamThreadRange(team, ny), [=](LO iy) { fwk_y_view(0, iy) = f(0, ix, iy); }); + team.team_barrier(); if (team.team_rank() == 0) { if (iflg2 == 1) { if (ibcymin == 1 || ibcymin == 2) @@ -1933,6 +1980,7 @@ void solve_bicubic_spline_compact(Rank1View x, LO nx, solve_cubic_spline_compact(y, ny, fwk_y_view, fwk4_y_view, ibcmin, 0.0, ibcmax, 0.0, wk_y_view); } + team.team_barrier(); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, ny), [=](LO iy) { f(2, ix, iy) = fwk_y_view(1, iy); }); }); @@ -1953,6 +2001,7 @@ void solve_bicubic_spline_compact(Rank1View x, LO nx, Kokkos::parallel_for(Kokkos::TeamThreadRange(team, ny), [=](LO iy) { fwk_y_view(0, iy) = f(1, ix, iy); }); + team.team_barrier(); if (team.team_rank() == 0) { if (iflg2 == 1) { if (ibcymin == 1 || ibcymin == 2) @@ -1964,6 +2013,7 @@ void solve_bicubic_spline_compact(Rank1View x, LO nx, solve_cubic_spline_compact(y, ny, fwk_y_view, fwk4_y_view, ibcmin, 0.0, ibcmax, 0.0, wk_y_view); } + team.team_barrier(); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, ny), [=](LO iy) { f(3, ix, iy) = fwk_y_view(1, iy); }); }); @@ -2013,10 +2063,12 @@ void solve_bicubic_spline_compact(Rank1View x, LO nx, Kokkos::parallel_for(Kokkos::TeamThreadRange(team, ny), [=](LO iy) { fwk_y_view(0, iy) = 0.0; }); + team.team_barrier(); if (team.team_rank() == 0) { solve_cubic_spline_compact(y, ny, fwk_y_view, fwk4_y_view, ibcymin, zdiff1, ibcymax, zdiff2, wk_y_view); } + team.team_barrier(); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, ny), [=](LO iy) { fcorr(0, ix, iy) = fwk_y_view(1, iy); }); @@ -2037,10 +2089,12 @@ void solve_bicubic_spline_compact(Rank1View x, LO nx, fwk_x_view(0, ix) = fcorr(0, ix, iy); }); + team.team_barrier(); if (team.team_rank() == 0) { solve_cubic_spline_compact(x, nx, fwk_x_view, fwk4_x_view, ibcxmin, zbcmin, ibcxmax, zbcmax, wk_x_view); } + team.team_barrier(); Kokkos::parallel_for(Kokkos::TeamThreadRange(team, nx), [=](LO ix) { fcorr(1, ix, iy) = fwk_x_view(1, ix); @@ -2048,7 +2102,7 @@ void solve_bicubic_spline_compact(Rank1View x, LO nx, }); Kokkos::parallel_for( - Kokkos::MDRangePolicy>({0, 0}, {nx, ny}), + Kokkos::MDRangePolicy>({0, 0}, {nx, ny}), KOKKOS_LAMBDA(const LO ix, const LO iy) { f(2, ix, iy) += fcorr(0, ix, iy); f(3, ix, iy) += fcorr(1, ix, iy); @@ -2193,6 +2247,7 @@ class ExplicitCubicSplineInterpolator private: Rank1View x_; + bool periodic_x_ = false; Kokkos::View coefficients_; Rank2View get_coefficients_view(); }; @@ -2267,6 +2322,7 @@ class CompactCubicSplineInterpolator private: Rank1View x_; + bool periodic_x_ = false; Kokkos::View coefficients_; Rank2View get_coefficients_view(); }; @@ -2372,6 +2428,8 @@ class ExplicitBiCubicSplineInterpolator private: Rank1View x_; Rank1View y_; + bool periodic_x_ = false; + bool periodic_y_ = false; Kokkos::View coefficients_; Rank4View get_coefficients_view(); }; @@ -2467,6 +2525,8 @@ class CompactBiCubicSplineInterpolator private: Rank1View x_; Rank1View y_; + bool periodic_x_ = false; + bool periodic_y_ = false; Kokkos::View coefficients_; Rank3View get_coefficients_view(); }; @@ -2613,12 +2673,15 @@ ExplicitCubicSplineInterpolator:: { detail::grid_valid(x); x_ = x; + periodic_x_ = ibcxmin == BoundaryCondition::PERIODIC; LO nx = x.extent(0); Kokkos::View fspl_view("coefficients", 4 * nx); coefficients_ = fspl_view; auto fspl = get_coefficients_view(); InitCubicCoeffFunctor init_functor(fspl, values); - Kokkos::parallel_for("initialize_coefficients", nx, init_functor); + Kokkos::parallel_for("initialize_coefficients", + Kokkos::RangePolicy(0, nx), + init_functor); Kokkos::View wk_view("working space", nx); auto wk = Rank1View(wk_view.data(), nx); @@ -2649,12 +2712,15 @@ CompactCubicSplineInterpolator::CompactCubicSplineInterpolator( { detail::grid_valid(x); x_ = x; + periodic_x_ = ibcxmin == BoundaryCondition::PERIODIC; LO nx = x.extent(0); Kokkos::View fspl_view("coefficients", 2 * nx); coefficients_ = fspl_view; auto fspl = get_coefficients_view(); InitCubicCoeffFunctor init_functor(fspl, values); - Kokkos::parallel_for("initialize_coefficients", nx, init_functor); + Kokkos::parallel_for("initialize_coefficients", + Kokkos::RangePolicy(0, nx), + init_functor); Kokkos::View fspl4_view("explicit coefficients", 4 * nx); auto fspl4 = Rank2View(fspl4_view.data(), 4, nx); @@ -2684,16 +2750,16 @@ void ExplicitCubicSplineInterpolator::evaluate( const Kokkos::View selector, Rank1View xvec, Rank2View fval, T lookup_tol) { - LO ivd = fval.extent(1); auto fspl = get_coefficients_view(); Kokkos::parallel_for( Kokkos::RangePolicy(0, xvec.extent(0)), KOKKOS_CLASS_LAMBDA(const LO i) { LO ier = 0; - auto fval_view = - Rank1View(fval.data_handle() + i * ivd, ivd); - detail::cubic_eval_explicit(xvec(i), selector, fval_view, x_, - x_.extent(0), fspl, ier, lookup_tol); + auto fval_view = Kokkos::submdspan(fval, i, Kokkos::full_extent); + const T xq = + detail::wrap_periodic(xvec(i), x_, x_.extent(0), periodic_x_); + detail::cubic_eval_explicit(xq, selector, fval_view, x_, x_.extent(0), + fspl, ier, lookup_tol); }); } @@ -2701,7 +2767,7 @@ template void ExplicitCubicSplineInterpolator::evaluate( Rank1View xvec, Rank2View fval) { - std::array ict_arr = {1, 0, 0}; + Kokkos::Array ict_arr = {1, 0, 0}; Kokkos::View selector("select", 3); Kokkos::parallel_for( Kokkos::RangePolicy(0, selector.size()), @@ -2714,15 +2780,15 @@ void CompactCubicSplineInterpolator::evaluate( Kokkos::View selector, Rank1View xvec, Rank2View fval, T lookup_tol) { - LO ivd = fval.extent(1); auto fspl = get_coefficients_view(); Kokkos::parallel_for( Kokkos::RangePolicy(0, xvec.extent(0)), KOKKOS_CLASS_LAMBDA(const LO i) { LO ier = 0; - auto fval_view = - Rank1View(fval.data_handle() + i * ivd, ivd); - detail::cubic_eval_compact(xvec(i), selector, fval_view, x_, x_.extent(0), + auto fval_view = Kokkos::submdspan(fval, i, Kokkos::full_extent); + const T xq = + detail::wrap_periodic(xvec(i), x_, x_.extent(0), periodic_x_); + detail::cubic_eval_compact(xq, selector, fval_view, x_, x_.extent(0), fspl, ier, lookup_tol); }); } @@ -2734,7 +2800,7 @@ template void CompactCubicSplineInterpolator::evaluate( Rank1View xvec, Rank2View fval) { - std::array ict_arr = {1, 0, 0}; + Kokkos::Array ict_arr = {1, 0, 0}; Kokkos::View selector("select", 3); Kokkos::parallel_for( Kokkos::RangePolicy(0, selector.size()), @@ -2750,15 +2816,17 @@ void ExplicitBiCubicSplineInterpolator::evaluate( PCMS_ALWAYS_ASSERT(xvec.extent(0) == yvec.extent(0) && xvec.extent(0) == fval.extent(0)); - LO ivd = fval.extent(1); auto fspl = get_coefficients_view(); Kokkos::parallel_for( Kokkos::RangePolicy(0, xvec.extent(0)), KOKKOS_CLASS_LAMBDA(const LO i) { LO ier = 0; - auto fval_view = - Rank1View(fval.data_handle() + i * ivd, ivd); - detail::bicubic_eval_explicit(xvec(i), yvec(i), iselect, fval_view, x_, + auto fval_view = Kokkos::submdspan(fval, i, Kokkos::full_extent); + const T xq = + detail::wrap_periodic(xvec(i), x_, x_.extent(0), periodic_x_); + const T yq = + detail::wrap_periodic(yvec(i), y_, y_.extent(0), periodic_y_); + detail::bicubic_eval_explicit(xq, yq, iselect, fval_view, x_, x_.extent(0), y_, y_.extent(0), fspl, ier, lookup_tol); }); @@ -2769,7 +2837,7 @@ void ExplicitBiCubicSplineInterpolator::evaluate( Rank1View xvec, Rank1View yvec, Rank2View fval) { - LO ict_arr[6] = {1, 0, 0, 0, 0, 0}; + Kokkos::Array ict_arr = {1, 0, 0, 0, 0, 0}; Kokkos::View ict("select", 6); Kokkos::parallel_for( Kokkos::RangePolicy(0, ict.size()), @@ -2785,17 +2853,18 @@ void CompactBiCubicSplineInterpolator::evaluate( PCMS_ALWAYS_ASSERT(xvec.extent(0) == yvec.extent(0) && xvec.extent(0) == fval.extent(0)); - LO ivd = fval.extent(1); auto fspl = get_coefficients_view(); Kokkos::parallel_for( Kokkos::RangePolicy(0, xvec.extent(0)), KOKKOS_CLASS_LAMBDA(const LO i) { LO ier = 0; - auto fval_view = - Rank1View(fval.data_handle() + i * ivd, ivd); - detail::bicubic_eval_compact(xvec(i), yvec(i), iselect, fval_view, x_, - x_.extent(0), y_, y_.extent(0), fspl, ier, - lookup_tol); + auto fval_view = Kokkos::submdspan(fval, i, Kokkos::full_extent); + const T xq = + detail::wrap_periodic(xvec(i), x_, x_.extent(0), periodic_x_); + const T yq = + detail::wrap_periodic(yvec(i), y_, y_.extent(0), periodic_y_); + detail::bicubic_eval_compact(xq, yq, iselect, fval_view, x_, x_.extent(0), + y_, y_.extent(0), fspl, ier, lookup_tol); }); } @@ -2804,7 +2873,7 @@ void CompactBiCubicSplineInterpolator::evaluate( Rank1View xvec, Rank1View yvec, Rank2View fval) { - LO ict_arr[6] = {1, 0, 0, 0, 0, 0}; + Kokkos::Array ict_arr = {1, 0, 0, 0, 0, 0}; Kokkos::View ict("select", 6); Kokkos::parallel_for( Kokkos::RangePolicy(0, ict.size()), @@ -2867,6 +2936,8 @@ ExplicitBiCubicSplineInterpolator:: x_ = x; y_ = y; + periodic_x_ = ibcxmin == BoundaryCondition::PERIODIC; + periodic_y_ = ibcymin == BoundaryCondition::PERIODIC; LO ny = y.extent(0); LO nx = x.extent(0); Kokkos::View fspl_view("fspl view", 4 * 4 * nx * ny); @@ -2874,8 +2945,9 @@ ExplicitBiCubicSplineInterpolator:: auto fspl = get_coefficients_view(); InitExplicitBiCubicCoeffFunctor init_functor(fspl, values, ny); - Kokkos::parallel_for(Kokkos::MDRangePolicy>({0, 0}, {ny, nx}), - init_functor); + Kokkos::parallel_for( + Kokkos::MDRangePolicy>({0, 0}, {ny, nx}), + init_functor); Kokkos::View wk_view("wk", 9 * nx * ny); auto wk = Rank1View(wk_view.data(), 9 * nx * ny); @@ -2915,6 +2987,8 @@ CompactBiCubicSplineInterpolator:: x_ = x; y_ = y; + periodic_x_ = ibcxmin == BoundaryCondition::PERIODIC; + periodic_y_ = ibcymin == BoundaryCondition::PERIODIC; LO ny = y.extent(0); LO nx = x.extent(0); Kokkos::View fspl_view("fspl view", 4 * nx * ny); @@ -2922,8 +2996,9 @@ CompactBiCubicSplineInterpolator:: auto fspl = get_coefficients_view(); InitCompactBiCubicCoeffFunctor init_functor(fspl, values, ny); - Kokkos::parallel_for(Kokkos::MDRangePolicy>({0, 0}, {ny, nx}), - init_functor); + Kokkos::parallel_for( + Kokkos::MDRangePolicy>({0, 0}, {ny, nx}), + init_functor); Kokkos::View wk_view("wk", 9 * nx * ny); auto wk = Rank1View(wk_view.data(), 9 * nx * ny); diff --git a/src/pcms/field/evaluator/uniform_grid.h b/src/pcms/field/evaluator/uniform_grid.h index ed0614585..35fdd293b 100644 --- a/src/pcms/field/evaluator/uniform_grid.h +++ b/src/pcms/field/evaluator/uniform_grid.h @@ -44,6 +44,42 @@ struct UniformGridFieldLocalizationHint size_t num_out_of_bounds_; }; +namespace detail +{ + +/// Indices of the points `hint` fills, in increasing order; empty unless its +/// mode is FILL. +template +Kokkos::View FilledPointIndices( + const UniformGridFieldLocalizationHint& hint) +{ + if (hint.mode_ != OutOfBoundsMode::FILL || hint.num_out_of_bounds_ == 0) { + return {}; + } + const auto is_out_of_bounds = hint.is_out_of_bounds_; + Kokkos::View indices( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "filled_points"), + hint.num_out_of_bounds_); + LO count = 0; + Kokkos::parallel_scan( + "uniform_grid_filled_points", + Kokkos::RangePolicy( + 0, static_cast(is_out_of_bounds.extent(0))), + KOKKOS_LAMBDA(const LO i, LO& update, const bool final) { + if (is_out_of_bounds(i)) { + if (final) { + indices(update) = i; + } + ++update; + } + }, + count); + PCMS_ALWAYS_ASSERT(static_cast(count) == hint.num_out_of_bounds_); + return indices; +} + +} // namespace detail + // UniformGridPointEvaluator implements PointEvaluator for structured // uniform grids. Localization results (cell indices, parametric coordinates) // are computed once at construction and cached for repeated Evaluate calls. @@ -65,21 +101,35 @@ class UniformGridPointEvaluator : public PointEvaluator : layout_(std::move(layout)), grid_(layout_->GetGrid()), hint_(std::move(hint)), - fill_value_(fill_value) + fill_value_(fill_value), + filled_points_(detail::FilledPointIndices(hint_)) + { + } + + [[nodiscard]] Kokkos::View FilledPoints() + const override { + return filled_points_; } - void Evaluate( + [[nodiscard]] LO NumPoints() const override + { + return static_cast(hint_.coordinates_.extent(0)); + } + + // Public because CUDA forbids extended lambdas in non-public member + // functions. User call Evaluate. + // TODO: add passkey or other mechanism to protect this + void EvaluateImpl( const Field& field, - Rank2View values) const override + ValueView out) const override { PCMS_FUNCTION_TIMER; - auto dof_data = field.GetDOFHolderData(); - LO num_points = static_cast(hint_.coordinates_.extent(0)); + const auto values = out.GetValues(); + const auto dof_data = field.GetDOFHolderData().GetValues(); + LO num_points = NumPoints(); 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())); @@ -127,7 +177,9 @@ class UniformGridPointEvaluator : public PointEvaluator "set_dimensions_view", Kokkos::RangePolicy(0, Dim), KOKKOS_LAMBDA(const unsigned d) { - dimensions_view(d) = cell_divisions[d] + 1; + // cell_dim_indices uses GetDimensionedIndex(), which returns indices in + // the opposite order of the coordinates, so we reverse the order here. + dimensions_view(d) = cell_divisions[Dim - 1 - d] + 1; }); RealMatView parametric_coords("parametric_coords", num_points, Dim); @@ -140,7 +192,10 @@ class UniformGridPointEvaluator : public PointEvaluator 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); + // cell_dim_indices is filled from GetDimensionedIndex(), which + // returns indices in the opposite order of the coordinates + parametric_coords(i, Dim - 1 - d) = + (coord - cell_min) / (cell_max - cell_min); } }); @@ -153,18 +208,16 @@ class UniformGridPointEvaluator : public PointEvaluator 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())); + RealMatView values_interp( + "values_interp", static_cast(layout_->GetNumOwnedDofHolder()), + static_cast(n_comp)); 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, 0); }); + Kokkos::MDRangePolicy>( + {0, 0}, {static_cast(layout_->GetNumOwnedDofHolder()), n_comp}), + KOKKOS_LAMBDA(const LO i, const int c) { + values_interp(i, c) = dof_data(i, c); + }); auto interpolator = RegularGridInterpolator( parametric_coords, values_interp, cell_indices_interp, dimensions_view); @@ -174,12 +227,12 @@ class UniformGridPointEvaluator : public PointEvaluator Real fv = fill_value_; Kokkos::parallel_for( "evaluate_order1_device", - Kokkos::RangePolicy(0, num_points), - KOKKOS_LAMBDA(const LO i) { + 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, 0) = fv; + values(i, c) = fv; } else { - values(i, 0) = interpolated(i); + values(i, c) = interpolated(i, c); } }); } @@ -189,6 +242,7 @@ class UniformGridPointEvaluator : public PointEvaluator UniformGrid grid_; UniformGridFieldLocalizationHint hint_; Real fill_value_; + Kokkos::View filled_points_; }; // UniformGridEvaluatorFactory implements FieldEvaluatorFactory for @@ -209,11 +263,6 @@ class UniformGridEvaluatorFactory : public FieldEvaluatorFactory 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; } @@ -225,21 +274,12 @@ class UniformGridEvaluatorFactory : public FieldEvaluatorFactory 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.GetValues(); LO num_points = static_cast(coordinates.extent(0)); diff --git a/src/pcms/field/evaluator/uniform_grid_spline.h b/src/pcms/field/evaluator/uniform_grid_spline.h index 3fb693add..bd60147fc 100644 --- a/src/pcms/field/evaluator/uniform_grid_spline.h +++ b/src/pcms/field/evaluator/uniform_grid_spline.h @@ -46,6 +46,7 @@ class UniformGridSplinePointEvaluator2D grid_(layout_->GetGrid()), hint_(std::move(hint)), fill_value_(fill_value), + filled_points_(detail::FilledPointIndices(hint_)), x_coords_("uniform_grid_spline_x", grid_.divisions[0] + 1), y_coords_("uniform_grid_spline_y", grid_.divisions[1] + 1) { @@ -67,15 +68,32 @@ class UniformGridSplinePointEvaluator2D detail::InitCoordsFunctor{y_coords_, bot_left_y, dy}); } - void Evaluate( + [[nodiscard]] Kokkos::View FilledPoints() + const override + { + return filled_points_; + } + + [[nodiscard]] LO NumPoints() const override + { + return static_cast(hint_.coordinates_.extent(0)); + } + + // Public because CUDA forbids extended lambdas in non-public member + // functions. User call Evaluate. + // TODO: add passkey or other mechanism to protect this + void EvaluateImpl( const Field& field, - Rank2View values) const override + ValueView out) 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); + const auto values = out.GetValues(); + LO num_points = NumPoints(); + if (values.extent(1) != 1) { + throw pcms_error("UniformGridSplinePointEvaluator2D: only scalar " + "(num_components==1) fields are supported"); + } - auto dof_data = field.GetDOFHolderData(); + const auto dof_data = field.GetDOFHolderData().GetValues(); LO nx = grid_.divisions[0] + 1; LO ny = grid_.divisions[1] + 1; PCMS_ALWAYS_ASSERT(static_cast(dof_data.size()) == nx * ny); @@ -174,6 +192,7 @@ class UniformGridSplinePointEvaluator2D UniformGrid<2> grid_; UniformGridFieldLocalizationHint<2> hint_; Real fill_value_; + Kokkos::View filled_points_; Kokkos::View x_coords_; Kokkos::View y_coords_; }; @@ -189,11 +208,6 @@ class UniformGridSplineEvaluatorFactory2D : public FieldEvaluatorFactory 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; } @@ -203,10 +217,6 @@ class UniformGridSplineEvaluatorFactory2D : public FieldEvaluatorFactory { 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 " diff --git a/src/pcms/field/field.h b/src/pcms/field/field.h index 433717d04..ab70297bd 100644 --- a/src/pcms/field/field.h +++ b/src/pcms/field/field.h @@ -2,12 +2,15 @@ #define PCMS_COUPLING_FIELD_H #include "field_data.h" -#include "field_evaluator_factory.h" #include "field_layout.h" #include "pcms/utility/arrays.h" #include "pcms/utility/memory_spaces.h" #include "pcms/utility/types.h" #include +#include +#include +#include +#include #include namespace pcms @@ -15,12 +18,13 @@ namespace pcms class FunctionSpace; -// Field is a composed per-field object: it owns coefficient data and holds -// a shared reference to the evaluator factory so the function space stays alive -// as long as the field does. +// Field is the (layout + data) bundle: it pairs coefficient data with the +// FieldLayout that gives the DOF-holder ordering its meaning. It is the +// currency of the communication path and of TransferOperator::Apply; it is NOT +// evaluatable on its own (evaluation semantics live on FunctionSpace). // -// Fields are typically created via LagrangeFunctionSpace::CreateField(). -// They are move-only (unique_ptr member). +// Fields are created via FunctionSpace::CreateField(). They are move-only +// (unique_ptr member). template class Field { @@ -29,58 +33,200 @@ class Field Field& operator=(Field&&) = default; Field(const Field&) = delete; Field& operator=(const Field&) = delete; + ~Field() = default; FieldData& GetData() noexcept { return *data_; } const FieldData& GetData() const noexcept { return *data_; } const FieldLayout& GetLayout() const { return *layout_; } - Rank2View GetDOFHolderDataHost() const + [[nodiscard]] const std::string& GetName() const noexcept { return name_; } + + // The DOF-holder accessors below read and write the values of the DOF + // holders this rank owns through the data's DOFAccess, and throw if the data + // has none (e.g. a field of an evaluation-only source). Writes synchronize + // the backend's other copies and are collective; see DOFAccess. + + // Copies to the host unless device memory is host memory. The returned view + // is valid until the next call or until the field is written or destroyed. + [[nodiscard]] ValueView GetDOFHolderDataHost() const { - return data_->GetDOFHolderDataHost(); + return ValueView(data_->GetValueBasis(), + OwnedValuesHost()); } - void SetDOFHolderDataHost(Rank2View v) + [[nodiscard]] ValueView GetDOFHolderData() const { - data_->SetDOFHolderDataHost(v); + return ValueView( + data_->GetValueBasis(), RequireDOFAccess().GetOwnedValues()); } - Rank2View GetDOFHolderData() const + void SetDOFHolderDataHost(ValueView v) { - return data_->GetDOFHolderData(); + CheckWriteTag(v.GetBasis()); + CheckWriteExtents(v.GetValues().extent(0), v.GetValues().extent(1)); + SetOwnedValuesFromHost(v.GetValues()); } - void SetDOFHolderData(Rank2View v) + void SetDOFHolderData(ValueView v) { - data_->SetDOFHolderData(v); + CheckWriteTag(v.GetBasis()); + CheckWriteExtents(v.GetValues().extent(0), v.GetValues().extent(1)); + RequireDOFAccess().SetOwnedValues(v.GetValues()); } -private: - class CtorKey + // unchecked is needed for optimization in some places, but should generally + // not be used unless you know what you are doing + void SetDOFHolderDataUncheckedHost(Rank2View v) { - CtorKey() = default; - friend class FunctionSpace; - }; + SetOwnedValuesFromHost(v); + } - Field(CtorKey, std::shared_ptr layout, - std::shared_ptr> evaluator_factory, + // unchecked is needed for optimization in some places, but should generally + // not be used unless you know what you are doing + void SetDOFHolderDataUnchecked(Rank2View v) + { + RequireDOFAccess().SetOwnedValues(v); + } + +protected: + Field(std::string name, std::shared_ptr layout, std::unique_ptr> data) - : layout_(std::move(layout)), - evaluator_factory_(std::move(evaluator_factory)), - data_(std::move(data)) + : name_(std::move(name)), layout_(std::move(layout)), data_(std::move(data)) + { + } + + void CheckWriteTag(const ValueBasis& basis) const + { + if (!SameValueBasis(basis, data_->GetValueBasis())) { + throw pcms_error( + "Field::SetDOFHolderData: the written view's declared value " + "type/basis does not match this field's declaration (use the " + "Unchecked overload only for untagged wire/raw data)"); + } + } + + void CheckWriteExtents(size_t num_holders, size_t num_components) const + { + if (num_holders != static_cast(layout_->GetNumOwnedDofHolder()) || + num_components != static_cast(layout_->GetNumComponents())) { + throw std::invalid_argument( + "Field::SetDOFHolderData: the written view is shaped [" + + std::to_string(num_holders) + "][" + std::to_string(num_components) + + "] but this field's layout is [" + + std::to_string(layout_->GetNumOwnedDofHolder()) + "][" + + std::to_string(layout_->GetNumComponents()) + "]"); + } + } + + const DOFAccess& RequireDOFAccess() const + { + const auto* access = data_->GetDOFAccess(); + if (access == nullptr) { + throw pcms_error("Field '" + name_ + + "': its values are not accessible through PCMS (the " + "field data provides no DOFAccess)"); + } + return *access; + } + + DOFAccess& RequireDOFAccess() + { + return const_cast&>(std::as_const(*this).RequireDOFAccess()); + } + + Rank2View OwnedValuesHost() const + { + const auto values = RequireDOFAccess().GetOwnedValues(); + if constexpr (std::is_same_v) { + return values; + } else { + // Stage in a device view of the default layout so the copy to the + // host handles whatever layout the backend's view carries. + Kokkos::View staged( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "owned_values_staged"), + values.extent(0), values.extent(1)); + CopyDeviceRank2ViewToRank2View(MakeRank2View(staged), values); + host_values_ = Kokkos::View( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "owned_values_host"), + values.extent(0), values.extent(1)); + DeepCopyMismatchLayouts(host_values_, staged); + return MakeConstRank2View(host_values_); + } + } + + void SetOwnedValuesFromHost(Rank2View values) { + if constexpr (std::is_same_v) { + RequireDOFAccess().SetOwnedValues(values); + } else { + Kokkos::View staged( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "owned_values_staged"), + values.extent(0), values.extent(1)); + CopyHostRank2ViewToDeviceView(staged, values); + RequireDOFAccess().SetOwnedValues(MakeConstRank2View(staged)); + } } + // FieldFactory constructs Fields (via WrapField); FunctionSpace constructs + // Fields the same way and moves a Field's name, layout and data out to build + // a Function (via CreateFunction). Both stamp the field name. + friend class FieldFactory; friend class FunctionSpace; +private: + std::string name_; std::shared_ptr layout_; - std::shared_ptr> evaluator_factory_; std::unique_ptr> data_; + // Host copy backing GetDOFHolderDataHost when device memory is not host + // memory. + mutable Kokkos::View host_values_; +}; + +// Function is a Field that additionally knows its FunctionSpace, so it can +// be evaluated and used to build transfer operators. It is-a Field so the +// communication path (FieldCommunicator holds Field&) and +// TransferOperator::Apply(const Field&) bind a Function slice-free. It adds no +// virtuals: moving a Function into a Field slot (e.g. AddField(Field&&)) +// intentionally slices off the space reference, leaving a valid comm-only +// Field. +template +class Function : public Field +{ +public: + Function(Function&&) = default; + Function& operator=(Function&&) = default; + Function(const Function&) = delete; + Function& operator=(const Function&) = delete; + + const FunctionSpace& GetSpace() const noexcept { return *space_; } + std::shared_ptr GetSpacePtr() const noexcept + { + return space_; + } + +protected: + Function(std::string name, std::shared_ptr layout, + std::unique_ptr> data, + std::shared_ptr space) + : Field(std::move(name), std::move(layout), std::move(data)), + space_(std::move(space)) + { + } + + friend class FunctionSpace; + +private: + std::shared_ptr space_; }; using FieldVariant = std::variant, Field, Field, Field, Field>; +using FunctionVariant = + std::variant, Function, Function, + Function, Function>; + } // namespace pcms #endif // PCMS_COUPLING_FIELD_H diff --git a/src/pcms/field/field_data.h b/src/pcms/field/field_data.h index fb3846cd0..bed91f930 100644 --- a/src/pcms/field/field_data.h +++ b/src/pcms/field/field_data.h @@ -1,7 +1,7 @@ #ifndef PCMS_FIELD_DATA_H #define PCMS_FIELD_DATA_H -#include "field_metadata.h" +#include "pcms/field/value_view.hpp" #include "pcms/utility/arrays.h" #include "pcms/utility/memory_spaces.h" #include @@ -10,62 +10,38 @@ 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. -// - DOF-holder data is exposed as a 2D [num_dof_holder][num_components] view -// (the canonical shape); the DOF-holder ordering must match the layout. -// The default layout_right means it is contiguous node-major (component -// fastest, i.e. flat index i*num_components + c) on every backend, so a -// flat/wire representation is just the contiguous data_handle(). -// - 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. +// DOFAccess is the optional capability of field data whose values PCMS can +// read and write directly: the values of the DOF holders this rank owns, shaped +// [owned DOF holder][component] in the layout's order. Coupler exchanges and +// transfer targets use it; evaluation never needs it. +template +class DOFAccess +{ +public: + // Valid until the field data is next written or destroyed. + virtual Rank2View GetOwnedValues() const = 0; + + // Writes the owned values, then brings any other copies the backend keeps + // (ghosts, replicas on other ranks) up to date. Collective over the ranks + // that share those copies. + virtual void SetOwnedValues(Rank2View values) = 0; + + virtual ~DOFAccess() noexcept = default; +}; + +// FieldData holds whatever a backend needs to represent a field. It is +// opaque. It should only be directly used by the type that created it. template class FieldData { public: using value_type = T; - virtual const FieldMetadata& GetMetadata() const = 0; - - // Direct access to DOF-holder coefficient data as a 2D - // [num_dof_holder][num_components] view. The returned view remains valid - // until the FieldData is mutated or destroyed. - virtual Rank2View GetDOFHolderDataHost() const = 0; - virtual void SetDOFHolderDataHost( - Rank2View values) = 0; + virtual FieldValueType GetValueType() const = 0; + virtual const ValueBasis& GetValueBasis() const = 0; - // The returned view remains valid until the FieldData is mutated or - // destroyed. - virtual Rank2View GetDOFHolderData() const = 0; - virtual void SetDOFHolderData( - Rank2View values) = 0; + virtual const DOFAccess* GetDOFAccess() const noexcept { return nullptr; } + virtual DOFAccess* GetDOFAccess() noexcept { return nullptr; } virtual ~FieldData() noexcept = default; }; diff --git a/src/pcms/field/field_evaluator_factory.h b/src/pcms/field/field_evaluator_factory.h index a6226d979..e1c066c08 100644 --- a/src/pcms/field/field_evaluator_factory.h +++ b/src/pcms/field/field_evaluator_factory.h @@ -2,7 +2,7 @@ #define PCMS_FIELD_EVALUATOR_FACTORY_H #include "field_layout.h" -#include "coordinate_system.h" +#include "pcms/field/coordinate_view.hpp" #include "evaluation_request.h" #include "out_of_bounds_policy.h" #include "point_evaluator.h" @@ -46,7 +46,6 @@ namespace pcms // public: // std::unique_ptr> CreateFieldReal() const; // std::shared_ptr GetLayout() const; -// CoordinateSystem GetCoordinateSystem() const; // // // convenience — delegates to the internal FieldEvaluatorFactory: // std::unique_ptr> CreatePointEvaluator( @@ -63,17 +62,6 @@ class FieldEvaluatorFactory // 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; diff --git a/src/pcms/field/field_factory.h b/src/pcms/field/field_factory.h new file mode 100644 index 000000000..936672632 --- /dev/null +++ b/src/pcms/field/field_factory.h @@ -0,0 +1,123 @@ +#ifndef PCMS_FIELD_FACTORY_H +#define PCMS_FIELD_FACTORY_H + +#include "field.h" +#include "field_data.h" +#include "field_layout.h" +#include "pcms/field/value_view.hpp" +#include "pcms/utility/types.h" +#include +#include +#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 constructs Field bundles ({layout, data}) with no evaluation +// capability and no shared-ownership requirement. Comm-only backends (e.g. +// XGCFieldFactory) derive it directly. FunctionSpace is a separate abstraction +// (not a FieldFactory) that additionally supports evaluation and produces +// Functions; a FieldFactory can never be passed where a FunctionSpace is +// required, which is the compile-time form of "comm-only cannot be evaluated". +class FieldFactory +{ +public: + virtual std::shared_ptr GetLayout() const noexcept = 0; + + virtual ~FieldFactory() noexcept = default; + + template + [[nodiscard]] Field CreateField( + std::string name = "", VarianceSignature variance = values::Scalar, + std::optional scaling = std::nullopt) const; + + template + [[nodiscard]] Field CreateField( + std::string name, VarianceSignature variance, + std::shared_ptr system, + std::optional scaling = std::nullopt) const; + + // Expert API: wrap externally constructed field data into a Field. The + // concrete factory validates backend-specific field-data type and storage + // size compatibility. + template + [[nodiscard]] Field CreateField(std::string name, + std::unique_ptr> data) const; + +protected: + template + static Field WrapField(std::shared_ptr layout, + std::unique_ptr> data) + { + return Field(std::string{}, std::move(layout), std::move(data)); + } + + virtual FieldVariant CreateFieldImpl(Type storage_type, + ValueBasis basis) const = 0; + + virtual FieldVariant CreateFieldImpl(FieldDataVariant data) const = 0; +}; + +template +Field FieldFactory::CreateField( + std::string name, VarianceSignature variance, + std::optional scaling) const +{ + return CreateField(std::move(name), variance, nullptr, scaling); +} + +template +Field FieldFactory::CreateField( + std::string name, VarianceSignature variance, + std::shared_ptr system, + std::optional scaling) const +{ + static_assert(is_supported_field_type_v, + "T is not a supported field type"); + ValueBasis basis = detail::MakeValueBasis( + GetLayout()->GetCoordinateSystem(), variance, std::move(system), scaling); + detail::ValidateValueSemantics(basis, GetLayout()->GetNumComponents()); + Field field = std::get>( + CreateFieldImpl(TypeEnumFromType(), std::move(basis))); + field.name_ = std::move(name); + return field; +} + +template +Field FieldFactory::CreateField(std::string name, + 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"); + } + detail::ValidateValueSemantics(data->GetValueBasis(), + GetLayout()->GetNumComponents()); + Field field = + std::get>(CreateFieldImpl(FieldDataVariant{std::move(data)})); + field.name_ = std::move(name); + return field; +} + +} // namespace pcms + +#endif // PCMS_FIELD_FACTORY_H diff --git a/src/pcms/field/field_layout.h b/src/pcms/field/field_layout.h index d01a13a06..1c8477347 100644 --- a/src/pcms/field/field_layout.h +++ b/src/pcms/field/field_layout.h @@ -2,11 +2,13 @@ #define PCMS_FIELD_LAYOUT_H #include #include +#include #include #include "pcms/discretization/discretization.h" #include "pcms/utility/types.h" #include "pcms/utility/arrays.h" -#include "coordinate_system.h" +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/coordinate_view.hpp" namespace pcms { @@ -19,6 +21,12 @@ using ReversePartitionMap = std::map>; class FieldLayout { public: + // Optional identifier for this layout. Set once at construction (via a From* + // factory) and empty by default. Consumers that need to identify a layout by + // a stable name use it; it carries no meaning within the field layer itself. + [[nodiscard]] const std::string& GetName() const noexcept { return name_; } + void SetName(std::string name) { name_ = std::move(name); } + virtual std::shared_ptr GetDiscretization() const noexcept = 0; @@ -56,6 +64,16 @@ class FieldLayout virtual EntOffsetsArray GetEntOffsets() const = 0; + [[nodiscard]] const std::shared_ptr& + GetCoordinateSystem() const + { + if (system_ == nullptr) { + throw pcms_error("FieldLayout::GetCoordinateSystem: layout has no " + "coordinate system set"); + } + return system_; + } + virtual CoordinateView GetDOFHolderCoordinates() const = 0; virtual int GetDimension() const = 0; @@ -76,7 +94,19 @@ class FieldLayout // construction of the mutable cache would introduce. void BuildGlobalToLocalPermutation(); + void SetCoordinateSystem( + std::shared_ptr coordinate_system) + { + if (coordinate_system == nullptr) { + throw pcms_error( + "FieldLayout::SetCoordinateSystem: coordinate system must not be null"); + } + system_ = std::move(coordinate_system); + } + private: + std::shared_ptr system_; + std::string name_; Kokkos::View global_to_local_host_; Kokkos::View global_to_local_; }; diff --git a/src/pcms/field/field_metadata.h b/src/pcms/field/field_metadata.h deleted file mode 100644 index 8dfca3c43..000000000 --- a/src/pcms/field/field_metadata.h +++ /dev/null @@ -1,30 +0,0 @@ -#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 index a5e6f867e..78aeb8a58 100644 --- a/src/pcms/field/function_space.h +++ b/src/pcms/field/function_space.h @@ -1,52 +1,42 @@ #ifndef PCMS_FUNCTION_SPACE_H #define PCMS_FUNCTION_SPACE_H -#include "coordinate_system.h" +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/coordinate_view.hpp" #include "evaluation_request.h" #include "field.h" #include "field_data.h" -#include "field_evaluator_factory.h" +#include "field_factory.h" #include "field_layout.h" -#include "field_metadata.h" +#include "pcms/field/value_view.hpp" #include "out_of_bounds_policy.h" #include "point_evaluator.h" +#include "pcms/field/point_evaluator_factory.hpp" #include "pcms/discretization/discretization.h" #include "pcms/utility/arrays.h" #include "pcms/utility/memory_spaces.h" #include "pcms/utility/types.h" #include +#include #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; - // FunctionSpace is an abstract interface representing an evaluatable field -// space: layout, evaluation rules, and coordinate interpretation. +// space: layout, evaluation rules, and coordinate interpretation. It produces +// Functions (a Field bound to this space). // // Concrete implementations (e.g. LagrangeFunctionSpace) provide backends for -// specific discretizations or mesh types. -// -// FunctionSpace is used as the parameter type for operation objects such as -// Interpolator, so that operations are not coupled to a specific backend. +// specific discretizations or mesh types, and their From* factories return a +// shared_ptr. A Function co-owns its space via shared_ptr (obtained through +// enable_shared_from_this), which is why concrete spaces are always +// shared-owned; a stack-allocated FunctionSpace is not constructible (the +// backend constructors are private and reachable only via the shared_ptr +// From* factories). class FunctionSpace + : public PointEvaluatorFactory + , public std::enable_shared_from_this { public: virtual std::shared_ptr GetDiscretization() @@ -57,82 +47,96 @@ class FunctionSpace virtual std::shared_ptr GetLayout() const noexcept = 0; - virtual CoordinateSystem GetCoordinateSystem() const noexcept = 0; + [[nodiscard]] const std::shared_ptr& + GetCoordinateSystem() const override + { + return GetLayout()->GetCoordinateSystem(); + } virtual ~FunctionSpace() noexcept = default; - // Create a new field with freshly allocated data for this function space. - // Compile-time error for unsupported T; runtime error for T unsupported by - // the concrete backend. template - [[nodiscard]] Field CreateField(FieldMetadata metadata = {}) const; + [[nodiscard]] Function CreateFunction( + std::string name = "", VarianceSignature variance = values::Scalar, + std::optional scaling = std::nullopt) const; - // Expert API: wrap externally constructed field data into a Field for this - // function space. The concrete function space validates backend-specific - // field-data type and storage size compatibility. template - [[nodiscard]] Field CreateField(std::unique_ptr> data) const; - - // 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. + [[nodiscard]] Function CreateFunction( + std::string name, VarianceSignature variance, + std::shared_ptr system, + std::optional scaling = std::nullopt) const; + + // Expert API: wrap externally constructed field data into a Function for this + // space. The concrete space validates backend-specific field-data type and + // storage size compatibility. template - [[nodiscard]] std::unique_ptr> CreatePointEvaluator( - const EvaluationRequest& request) const; + [[nodiscard]] Function CreateFunction( + std::string name, std::unique_ptr> data) const; protected: template static Field WrapField(std::shared_ptr layout, - std::unique_ptr> data, - std::shared_ptr> - evaluator_factory = nullptr) + std::unique_ptr> data) { - return Field(typename Field::CtorKey{}, std::move(layout), - std::move(evaluator_factory), std::move(data)); + return Field(std::string{}, std::move(layout), std::move(data)); } - virtual FieldVariant CreateFieldImpl(Type value_type, - FieldMetadata metadata) const = 0; + template + static Function WrapFunction(std::string name, + std::shared_ptr layout, + std::unique_ptr> data, + std::shared_ptr space) + { + return Function(std::move(name), std::move(layout), std::move(data), + std::move(space)); + } - virtual FieldVariant CreateFieldImpl(FieldDataVariant data) const = 0; + virtual FieldVariant CreateFieldImpl(Type storage_type, + ValueBasis basis) const = 0; - virtual PointEvaluatorVariant CreatePointEvaluatorImpl( - Type value_type, const EvaluationRequest& request) const = 0; + virtual FieldVariant CreateFieldImpl(FieldDataVariant data) const = 0; }; template -Field FunctionSpace::CreateField(FieldMetadata metadata) const +Function FunctionSpace::CreateFunction( + std::string name, VarianceSignature variance, + std::optional scaling) const { - static_assert(is_supported_field_type_v, - "T is not a supported field type"); - return std::get>(CreateFieldImpl(TypeEnumFromType(), metadata)); + return CreateFunction(std::move(name), variance, nullptr, scaling); } template -Field FunctionSpace::CreateField(std::unique_ptr> data) const +Function FunctionSpace::CreateFunction( + std::string name, VarianceSignature variance, + std::shared_ptr system, + std::optional scaling) const { static_assert(is_supported_field_type_v, "T is not a supported field type"); - if (!data) { - throw pcms_error("FunctionSpace::CreateField: data must not be null"); - } - return std::get>(CreateFieldImpl(FieldDataVariant{std::move(data)})); + ValueBasis basis = detail::MakeValueBasis(GetCoordinateSystem(), variance, + std::move(system), scaling); + detail::ValidateValueSemantics(basis, GetLayout()->GetNumComponents()); + Field field = std::get>( + CreateFieldImpl(TypeEnumFromType(), std::move(basis))); + return WrapFunction(std::move(name), std::move(field.layout_), + std::move(field.data_), shared_from_this()); } template -std::unique_ptr> FunctionSpace::CreatePointEvaluator( - const EvaluationRequest& request) const +Function FunctionSpace::CreateFunction( + std::string name, std::unique_ptr> data) const { static_assert(is_supported_field_type_v, "T is not a supported field type"); - return std::get>>( - CreatePointEvaluatorImpl(TypeEnumFromType(), request)); + if (!data) { + throw pcms_error("FunctionSpace::CreateFunction: data must not be null"); + } + detail::ValidateValueSemantics(data->GetValueBasis(), + GetLayout()->GetNumComponents()); + Field field = + std::get>(CreateFieldImpl(FieldDataVariant{std::move(data)})); + return WrapFunction(std::move(name), std::move(field.layout_), + std::move(field.data_), shared_from_this()); } inline EvaluationRequest EvaluationRequest::FromCoordinates( diff --git a/src/pcms/field/function_space/lagrange.cpp b/src/pcms/field/function_space/lagrange.cpp index 8328e2668..faeba79ef 100644 --- a/src/pcms/field/function_space/lagrange.cpp +++ b/src/pcms/field/function_space/lagrange.cpp @@ -43,7 +43,7 @@ void ValidateLagrangeWrappedFieldData(const FieldLayout& layout, } } - if (data.GetDOFHolderDataHost().size() != + if (data.GetDOFAccess()->GetOwnedValues().size() != detail::ExpectedFlatFieldDataSize(layout)) { throw pcms_error( "LagrangeFunctionSpace::CreateField: field data size does not match " @@ -54,8 +54,8 @@ void ValidateLagrangeWrappedFieldData(const FieldLayout& layout, } // namespace LagrangeFunctionSpace::LagrangeFunctionSpace( - std::shared_ptr layout, - std::function create_field_data_fn, + Key, 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)), @@ -63,10 +63,10 @@ LagrangeFunctionSpace::LagrangeFunctionSpace( { } -LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( +std::shared_ptr LagrangeFunctionSpace::FromMesh( Omega_h::Mesh& mesh, int order, int num_components, - CoordinateSystem coordinate_system, std::string global_id_name, - Backend backend) + std::shared_ptr coordinate_system, + std::string global_id_name, Backend backend, std::string layout_name) { // https://github.com/SCOREC/meshFields/issues/88 if (backend == Backend::MeshFields && order == 0) { @@ -75,11 +75,6 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( } 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; @@ -89,24 +84,25 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( auto mesh_layout = std::make_shared( mesh, nodes_per_dim, num_components, coordinate_system, std::move(global_id_name)); + mesh_layout->SetName(layout_name); auto eval_factory = std::make_shared>(mesh_layout); - return LagrangeFunctionSpace( - mesh_layout, - [mesh_layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + return std::make_shared( + Key{}, mesh_layout, + [mesh_layout](Type t, ValueBasis basis) -> FieldDataVariant { if (t == Type::Float) { if constexpr (std::is_same_v || std::is_same_v) { - return std::make_unique>(mesh_layout, - metadata); + return std::make_unique>( + mesh_layout, std::move(basis)); } throw pcms_error( "LagrangeFunctionSpace: MeshFields backend does not support " "float in this build"); } if (t == Type::Real) { - return std::make_unique>(mesh_layout, - metadata); + return std::make_unique>( + mesh_layout, std::move(basis)); } throw pcms_error( "LagrangeFunctionSpace: MeshFields backend only supports the " @@ -126,27 +122,29 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( } auto layout = std::make_shared( mesh, order, num_components, coordinate_system, std::move(global_id_name)); + layout->SetName(std::move(layout_name)); auto eval_factory = std::make_shared>(layout); - return LagrangeFunctionSpace( - layout, - [layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + return std::make_shared( + Key{}, layout, + [layout](Type t, ValueBasis basis) -> 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); + return std::make_unique>(layout, basis); } }); }, std::move(eval_factory)); } -LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( +std::shared_ptr 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) + std::shared_ptr coordinate_system, + Omega_h::Read owned_mask, std::string global_id_name, + Backend backend, std::string layout_name) { if (backend == Backend::MeshFields) { throw pcms_error( @@ -161,26 +159,28 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromMesh( auto layout = std::make_shared( mesh, order, num_components, coordinate_system, std::move(owned_mask), std::move(global_id_name)); + layout->SetName(std::move(layout_name)); auto eval_factory = std::make_shared>(layout); - return LagrangeFunctionSpace( - layout, - [layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + return std::make_shared( + Key{}, layout, + [layout](Type t, ValueBasis basis) -> 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); + return std::make_unique>(layout, basis); } }); }, std::move(eval_factory)); } -LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( +std::shared_ptr LagrangeFunctionSpace::FromUniformGrid( const UniformGrid<2>& grid, int num_components, - CoordinateSystem coordinate_system, int order) + std::shared_ptr coordinate_system, int order, + std::string layout_name) { if (order != 0 && order != 1) { throw std::invalid_argument("LagrangeFunctionSpace::FromUniformGrid: only " @@ -188,26 +188,28 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( } auto ug_layout = std::make_shared>( grid, num_components, coordinate_system, order); + ug_layout->SetName(std::move(layout_name)); auto eval_factory = std::make_shared>(ug_layout); - return LagrangeFunctionSpace( - ug_layout, - [ug_layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + return std::make_shared( + Key{}, ug_layout, + [ug_layout](Type t, ValueBasis basis) -> 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); + return std::make_unique>(ug_layout, basis); } }); }, std::move(eval_factory)); } -LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( +std::shared_ptr LagrangeFunctionSpace::FromUniformGrid( const UniformGrid<3>& grid, int num_components, - CoordinateSystem coordinate_system, int order) + std::shared_ptr coordinate_system, int order, + std::string layout_name) { if (order != 0 && order != 1) { throw std::invalid_argument("LagrangeFunctionSpace::FromUniformGrid: only " @@ -215,17 +217,18 @@ LagrangeFunctionSpace LagrangeFunctionSpace::FromUniformGrid( } auto ug_layout = std::make_shared>( grid, num_components, coordinate_system, order); + ug_layout->SetName(std::move(layout_name)); auto eval_factory = std::make_shared>(ug_layout); - return LagrangeFunctionSpace( - ug_layout, - [ug_layout](Type t, FieldMetadata metadata) -> FieldDataVariant { + return std::make_shared( + Key{}, ug_layout, + [ug_layout](Type t, ValueBasis basis) -> 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); + return std::make_unique>(ug_layout, basis); } }); }, @@ -238,21 +241,21 @@ std::shared_ptr LagrangeFunctionSpace::GetLayout() return layout_; } -CoordinateSystem LagrangeFunctionSpace::GetCoordinateSystem() const noexcept +std::shared_ptr> +LagrangeFunctionSpace::GetEvaluatorFactory() const noexcept { - return evaluator_factory_->GetCoordinateSystem(); + return evaluator_factory_; } -FieldVariant LagrangeFunctionSpace::CreateFieldImpl( - Type value_type, FieldMetadata metadata) const +FieldVariant LagrangeFunctionSpace::CreateFieldImpl(Type storage_type, + ValueBasis basis) const { - auto field_data = create_field_data_fn_(value_type, metadata); + auto field_data = create_field_data_fn_(storage_type, std::move(basis)); 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), - evaluator_factory_); + return WrapField(layout_, std::forward(fd)); }, std::move(field_data)); } @@ -269,8 +272,7 @@ FieldVariant LagrangeFunctionSpace::CreateFieldImpl(FieldDataVariant data) const "LagrangeFunctionSpace: int8_t is not a supported field type"); } else { ValidateLagrangeWrappedFieldData(*layout_, *fd); - return WrapField(layout_, std::forward(fd), - evaluator_factory_); + return WrapField(layout_, std::forward(fd)); } }, std::move(data)); diff --git a/src/pcms/field/function_space/lagrange.h b/src/pcms/field/function_space/lagrange.h index 54b2f695e..ff2078917 100644 --- a/src/pcms/field/function_space/lagrange.h +++ b/src/pcms/field/function_space/lagrange.h @@ -7,11 +7,11 @@ #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/value_view.hpp" #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/field/coordinate_system.hpp" #include "pcms/utility/arrays.h" #include "pcms/utility/types.h" #include "pcms/utility/memory_spaces.h" @@ -27,7 +27,19 @@ namespace pcms class LagrangeFunctionSpace : public FunctionSpace { + // Passkey: only members (the From* factories) can create a Key, so only they + // can construct a space, while std::make_shared can still call the ctor. + struct Key + { + explicit Key() = default; + }; + public: + LagrangeFunctionSpace( + Key, std::shared_ptr layout, + std::function create_field_data_fn, + std::shared_ptr> evaluator_factory) noexcept; + enum class Backend { MeshFields, @@ -41,34 +53,43 @@ class LagrangeFunctionSpace : public FunctionSpace #endif // Unstructured mesh — dispatches to MeshFields or native Omega_h backend - [[nodiscard]] static LagrangeFunctionSpace FromMesh( + [[nodiscard]] static std::shared_ptr FromMesh( Omega_h::Mesh& mesh, int order, int num_components, - CoordinateSystem coordinate_system, std::string global_id_name = "global", - Backend backend = DefaultBackend); + std::shared_ptr mesh_coordinate_system, + std::string global_id_name = "global", Backend backend = DefaultBackend, + std::string layout_name = ""); - [[nodiscard]] static LagrangeFunctionSpace FromMesh( + [[nodiscard]] static std::shared_ptr 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); + std::shared_ptr mesh_coordinate_system, + Omega_h::Read owned_mask, + std::string global_id_name = "global", Backend backend = DefaultBackend, + std::string layout_name = ""); // Structured uniform grid — order-1 H1-conforming nodal field on a regular // grid - [[nodiscard]] static LagrangeFunctionSpace FromUniformGrid( + [[nodiscard]] static std::shared_ptr FromUniformGrid( const UniformGrid<2>& grid, int num_components, - CoordinateSystem coordinate_system, int order = 1); + std::shared_ptr mesh_coordinate_system, + int order = 1, std::string layout_name = ""); - [[nodiscard]] static LagrangeFunctionSpace FromUniformGrid( + [[nodiscard]] static std::shared_ptr FromUniformGrid( const UniformGrid<3>& grid, int num_components, - CoordinateSystem coordinate_system, int order = 1); + std::shared_ptr mesh_coordinate_system, + int order = 1, std::string layout_name = ""); [[nodiscard]] std::shared_ptr GetLayout() const noexcept override; - [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept override; + // Expert API: the backend's evaluator factory, for operators that can reuse + // the localization structures it owns instead of rebuilding them. Null for + // backends without one. + [[nodiscard]] std::shared_ptr> + GetEvaluatorFactory() const noexcept; protected: - [[nodiscard]] FieldVariant CreateFieldImpl( - Type value_type, FieldMetadata metadata) const override; + [[nodiscard]] FieldVariant CreateFieldImpl(Type storage_type, + ValueBasis basis) const override; [[nodiscard]] FieldVariant CreateFieldImpl( FieldDataVariant data) const override; @@ -77,13 +98,8 @@ class LagrangeFunctionSpace : public FunctionSpace 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::function create_field_data_fn_; std::shared_ptr> evaluator_factory_; }; diff --git a/src/pcms/field/function_space/mfem.hpp b/src/pcms/field/function_space/mfem.hpp new file mode 100644 index 000000000..a3b8820d9 --- /dev/null +++ b/src/pcms/field/function_space/mfem.hpp @@ -0,0 +1,93 @@ +#ifndef PCMS_MFEM_FIELD_FACTORY_HPP +#define PCMS_MFEM_FIELD_FACTORY_HPP + +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/data/mfem.hpp" +#include "pcms/field/field_factory.h" +#include "pcms/field/layout/mfem.hpp" +#include "pcms/utility/assert.h" +#include "pcms/utility/common.h" + +#include + +#include +#include +#include + +namespace pcms +{ + +// Communication-only 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. Point evaluation is not +// supported (mirrors XGCFieldFactory). +// 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, + std::shared_ptr coordinate_system, + std::string layout_name = "") + : layout_(std::make_shared(pmesh, pfes, + std::move(coordinate_system))), + pfes_(pfes), + gf_(gf) + { + layout_->SetName(std::move(layout_name)); + } + + void SetLayoutName(std::string name) const + { + layout_->SetName(std::move(name)); + } + + [[nodiscard]] std::shared_ptr GetLayout() + const noexcept override + { + return layout_; + } + + [[nodiscard]] std::shared_ptr GetMFEMLayout() const noexcept + { + return layout_; + } + +protected: + [[nodiscard]] FieldVariant CreateFieldImpl(Type storage_type, + ValueBasis basis) const override + { + if (storage_type != Type::Real) { + throw pcms_error("MFEM adapter only supports Real (double) fields"); + } + return WrapField(layout_, std::make_unique( + pfes_, gf_, std::move(basis))); + } + + [[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)->GetDOFAccess()->GetOwnedValues().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_MFEM_FIELD_FACTORY_HPP diff --git a/src/pcms/field/function_space/polynomial_reconstruction.cpp b/src/pcms/field/function_space/polynomial_reconstruction.cpp index 060b9c245..2097ead77 100644 --- a/src/pcms/field/function_space/polynomial_reconstruction.cpp +++ b/src/pcms/field/function_space/polynomial_reconstruction.cpp @@ -12,48 +12,44 @@ #include #include +#include namespace pcms { PolynomialReconstructionFunctionSpace::PolynomialReconstructionFunctionSpace( - std::shared_ptr layout, + Key, std::shared_ptr layout, std::shared_ptr> evaluator_factory) noexcept : layout_(std::move(layout)), evaluator_factory_(std::move(evaluator_factory)) { } -PolynomialReconstructionFunctionSpace +std::shared_ptr PolynomialReconstructionFunctionSpace::Create( - Rank2View coords, CoordinateSystem coordinate_system, - MLSOptions options) + CoordinateView coords, MLSOptions options) { - int dim = static_cast(coords.extent(1)); - Kokkos::View host_view( - coords.data_handle(), coords.extent(0), coords.extent(1)); + const auto values = coords.GetValues(); + const int dim = static_cast(values.extent(1)); + Kokkos::View host_view( + values.data_handle(), values.extent(0), values.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 pc_layout = std::make_shared( + dim, device_view, coords.GetCoordinateSystem()); 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)); + return std::make_shared( + Key{}, pc_layout, std::move(eval_factory)); } -PolynomialReconstructionFunctionSpace +std::shared_ptr PolynomialReconstructionFunctionSpace::FromMesh( Omega_h::Mesh& mesh, int source_entity_dim, - CoordinateSystem coordinate_system, MLSOptions options) + std::shared_ptr 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 " @@ -61,13 +57,20 @@ PolynomialReconstructionFunctionSpace::FromMesh( } auto mesh_layout = std::make_shared( - mesh, source_entity_dim, 1, coordinate_system); + mesh, source_entity_dim, 1, std::move(coordinate_system)); + // MLS assumes unweighted Euclidean in distance calculation + if (!HasIdentityMetric(*mesh_layout->GetCoordinateSystem())) { + throw pcms_error( + "PolynomialReconstructionFunctionSpace::FromMesh: MLS requires a " + "coordinate system whose metric is the identity; got '" + + std::string(mesh_layout->GetCoordinateSystem()->Kind()) + "'"); + } 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)); + return std::make_shared( + Key{}, mesh_layout, std::move(eval_factory)); } std::shared_ptr @@ -76,16 +79,10 @@ PolynomialReconstructionFunctionSpace::GetLayout() const noexcept return layout_; } -CoordinateSystem PolynomialReconstructionFunctionSpace::GetCoordinateSystem() - const noexcept -{ - return evaluator_factory_->GetCoordinateSystem(); -} - FieldVariant PolynomialReconstructionFunctionSpace::CreateFieldImpl( - Type value_type, FieldMetadata metadata) const + Type storage_type, ValueBasis basis) const { - return apply_to_type(value_type, [&](auto tag) -> FieldVariant { + return apply_to_type(storage_type, [&](auto tag) -> FieldVariant { using T = typename decltype(tag)::type; if constexpr (!std::is_same_v) { throw pcms_error( @@ -93,8 +90,8 @@ FieldVariant PolynomialReconstructionFunctionSpace::CreateFieldImpl( "supported"); } else { return WrapField( - layout_, std::make_unique>(layout_, metadata), - evaluator_factory_); + layout_, + std::make_unique>(layout_, std::move(basis))); } }); } @@ -114,13 +111,13 @@ FieldVariant PolynomialReconstructionFunctionSpace::CreateFieldImpl( "PolynomialReconstructionFunctionSpace::CreateField: requires " "SimpleFieldData"); } - if (fd->GetDOFHolderDataHost().size() != + if (fd->GetDOFAccess()->GetOwnedValues().size() != detail::ExpectedFlatFieldDataSize(*layout_)) { throw pcms_error( "PolynomialReconstructionFunctionSpace::CreateField: field data size " "does not match layout"); } - return WrapField(layout_, std::move(fd), evaluator_factory_); + return WrapField(layout_, std::move(fd)); } PointEvaluatorVariant diff --git a/src/pcms/field/function_space/polynomial_reconstruction.hpp b/src/pcms/field/function_space/polynomial_reconstruction.hpp index 0222c282f..71e6646fc 100644 --- a/src/pcms/field/function_space/polynomial_reconstruction.hpp +++ b/src/pcms/field/function_space/polynomial_reconstruction.hpp @@ -4,10 +4,10 @@ #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/value_view.hpp" #include "pcms/field/function_space.h" #include "pcms/field/out_of_bounds_policy.h" -#include "pcms/field/coordinate_system.h" +#include "pcms/field/coordinate_view.hpp" #include "pcms/field/evaluator/mls_options.h" #include "pcms/utility/arrays.h" #include "pcms/utility/memory_spaces.h" @@ -23,23 +23,32 @@ class FieldEvaluatorFactory; class PolynomialReconstructionFunctionSpace : public FunctionSpace { + // Passkey: only members (the Create/FromMesh factories) can create a Key, so + // only they can construct a space, while std::make_shared can call the ctor. + struct Key + { + explicit Key() = default; + }; + public: - [[nodiscard]] static PolynomialReconstructionFunctionSpace Create( - Rank2View coords, CoordinateSystem coordinate_system, - MLSOptions options = {}); + PolynomialReconstructionFunctionSpace( + Key, std::shared_ptr layout, + std::shared_ptr> evaluator_factory) noexcept; - [[nodiscard]] static PolynomialReconstructionFunctionSpace FromMesh( - Omega_h::Mesh& mesh, int source_entity_dim, - CoordinateSystem coordinate_system, MLSOptions options = {}); + [[nodiscard]] static std::shared_ptr + Create(CoordinateView coords, MLSOptions options = {}); + + [[nodiscard]] static std::shared_ptr + FromMesh(Omega_h::Mesh& mesh, int source_entity_dim, + std::shared_ptr 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(Type storage_type, + ValueBasis basis) const override; [[nodiscard]] FieldVariant CreateFieldImpl( FieldDataVariant data) const override; @@ -48,10 +57,6 @@ class PolynomialReconstructionFunctionSpace : public FunctionSpace 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_; }; diff --git a/src/pcms/field/function_space/spline.cpp b/src/pcms/field/function_space/spline.cpp index f4530fc8c..f169a9f40 100644 --- a/src/pcms/field/function_space/spline.cpp +++ b/src/pcms/field/function_space/spline.cpp @@ -11,20 +11,22 @@ namespace pcms { SplineFunctionSpace::SplineFunctionSpace( - std::shared_ptr layout, + Key, 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) +std::shared_ptr SplineFunctionSpace::FromUniformGrid( + const UniformGrid<2>& grid, + std::shared_ptr mesh_coordinate_system) { - auto layout = - std::make_shared>(grid, 1, coordinate_system, 1); + auto layout = std::make_shared>( + grid, 1, std::move(mesh_coordinate_system), 1); auto evaluator_factory = std::make_shared(layout); - return SplineFunctionSpace(layout, std::move(evaluator_factory)); + return std::make_shared(Key{}, layout, + std::move(evaluator_factory)); } std::shared_ptr SplineFunctionSpace::GetLayout() @@ -33,22 +35,17 @@ std::shared_ptr SplineFunctionSpace::GetLayout() return layout_; } -CoordinateSystem SplineFunctionSpace::GetCoordinateSystem() const noexcept +FieldVariant SplineFunctionSpace::CreateFieldImpl(Type storage_type, + ValueBasis basis) const { - return evaluator_factory_->GetCoordinateSystem(); -} - -FieldVariant SplineFunctionSpace::CreateFieldImpl(Type value_type, - FieldMetadata metadata) const -{ - return apply_to_type(value_type, [&](auto tag) -> FieldVariant { + return apply_to_type(storage_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), - evaluator_factory_); + layout_, + std::make_unique>(layout_, std::move(basis))); } }); } @@ -64,13 +61,13 @@ FieldVariant SplineFunctionSpace::CreateFieldImpl(FieldDataVariant data) const throw pcms_error( "SplineFunctionSpace::CreateField: requires SimpleFieldData"); } - if (fd->GetDOFHolderDataHost().size() != + if (fd->GetDOFAccess()->GetOwnedValues().size() != detail::ExpectedFlatFieldDataSize(*layout_)) { throw pcms_error( "SplineFunctionSpace::CreateField: field data size does not match " "layout"); } - return WrapField(layout_, std::move(fd), evaluator_factory_); + return WrapField(layout_, std::move(fd)); } PointEvaluatorVariant SplineFunctionSpace::CreatePointEvaluatorImpl( diff --git a/src/pcms/field/function_space/spline.h b/src/pcms/field/function_space/spline.h index 15c7d7128..531505e8a 100644 --- a/src/pcms/field/function_space/spline.h +++ b/src/pcms/field/function_space/spline.h @@ -5,7 +5,7 @@ #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/value_view.hpp" #include "pcms/field/function_space.h" #include "pcms/field/out_of_bounds_policy.h" #include "pcms/field/point_evaluator.h" @@ -21,18 +21,28 @@ namespace pcms class SplineFunctionSpace : public FunctionSpace { + // Passkey: only members (the From* factories) can create a Key, so only they + // can construct a space, while std::make_shared can still call the ctor. + struct Key + { + explicit Key() = default; + }; + public: - [[nodiscard]] static SplineFunctionSpace FromUniformGrid( - const UniformGrid<2>& grid, CoordinateSystem coordinate_system); + SplineFunctionSpace( + Key, std::shared_ptr layout, + std::shared_ptr> evaluator_factory) noexcept; + + [[nodiscard]] static std::shared_ptr FromUniformGrid( + const UniformGrid<2>& grid, + std::shared_ptr mesh_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(Type storage_type, + ValueBasis basis) const override; [[nodiscard]] FieldVariant CreateFieldImpl( FieldDataVariant data) const override; @@ -41,10 +51,6 @@ class SplineFunctionSpace : public FunctionSpace 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_; }; diff --git a/src/pcms/field/function_space/xgc.h b/src/pcms/field/function_space/xgc.h index 164abc7a6..ee0b63b2a 100644 --- a/src/pcms/field/function_space/xgc.h +++ b/src/pcms/field/function_space/xgc.h @@ -1,26 +1,31 @@ -#ifndef PCMS_XGC_FUNCTION_SPACE_H -#define PCMS_XGC_FUNCTION_SPACE_H +#ifndef PCMS_XGC_FIELD_FACTORY_H +#define PCMS_XGC_FIELD_FACTORY_H #include "pcms/field/data/xgc.h" #include "pcms/field/field.h" -#include "pcms/field/function_space.h" -#include "pcms/field/field_metadata.h" +#include "pcms/field/field_factory.h" +#include "pcms/field/value_view.hpp" #include "pcms/utility/common.h" -#include #include namespace pcms { -class XGCFunctionSpace : public FunctionSpace +// XGC is a comm-only backend +class XGCFieldFactory : public FieldFactory { public: - XGCFunctionSpace(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)) + XGCFieldFactory(const ReverseClassificationVertex& reverse_classification, + LO num_plane_nodes, std::string layout_name = "") + : layout_(std::make_shared(reverse_classification, + num_plane_nodes)) { + layout_->SetName(std::move(layout_name)); + } + + void SetLayoutName(std::string name) const + { + layout_->SetName(std::move(name)); } [[nodiscard]] std::shared_ptr GetLayout() @@ -29,19 +34,20 @@ class XGCFunctionSpace : public FunctionSpace return layout_; } - [[nodiscard]] CoordinateSystem GetCoordinateSystem() const noexcept override + [[nodiscard]] std::shared_ptr GetXGCLayout() + const noexcept { - return CoordinateSystem::XGC; + return layout_; } protected: - [[nodiscard]] FieldVariant CreateFieldImpl( - Type value_type, FieldMetadata metadata) const override + [[nodiscard]] FieldVariant CreateFieldImpl(Type storage_type, + ValueBasis basis) const override { - return apply_to_type(value_type, [&](auto tag) -> FieldVariant { + return apply_to_type(storage_type, [&](auto tag) -> FieldVariant { using T = typename decltype(tag)::type; return WrapField(layout_, - std::make_unique>(layout_, metadata)); + std::make_unique>(layout_, basis)); }); } @@ -55,12 +61,12 @@ class XGCFunctionSpace : public FunctionSpace PCMS_ALWAYS_ASSERT(fd != nullptr); if (dynamic_cast*>(fd.get()) == nullptr) { throw pcms_error( - "XGCFunctionSpace::CreateField: requires XGCFieldData"); + "XGCFieldFactory::CreateField: requires XGCFieldData"); } - if (fd->GetDOFHolderDataHost().size() != + if (fd->GetDOFAccess()->GetOwnedValues().size() != static_cast(layout_->GetFullDataSize())) { throw pcms_error( - "XGCFunctionSpace::CreateField: field data size does not match " + "XGCFieldFactory::CreateField: field data size does not match " "layout"); } return WrapField(layout_, std::move(fd)); @@ -68,23 +74,10 @@ class XGCFunctionSpace : public FunctionSpace std::move(data)); } - [[nodiscard]] PointEvaluatorVariant CreatePointEvaluatorImpl( - Type /*value_type*/, const EvaluationRequest& /*request*/) const override - { - throw pcms_error("XGCFunctionSpace does not support point evaluation"); - } - -public: - [[nodiscard]] std::shared_ptr GetXGCLayout() - const noexcept - { - return layout_; - } - private: - std::shared_ptr layout_; + std::shared_ptr layout_; }; } // namespace pcms -#endif // PCMS_XGC_FUNCTION_SPACE_H +#endif // PCMS_XGC_FIELD_FACTORY_H diff --git a/src/pcms/field/layout/empty.cpp b/src/pcms/field/layout/empty.cpp index 83fef4858..cdd6dc0b4 100644 --- a/src/pcms/field/layout/empty.cpp +++ b/src/pcms/field/layout/empty.cpp @@ -1,9 +1,32 @@ #include "pcms/field/layout/empty.h" +#include "pcms/utility/assert.h" namespace pcms { -EmptyFieldLayout::EmptyFieldLayout() +namespace +{ + +// helper/indirection to ensure that we don't end up with a Cartesian system +// with dimension 0 +int RequireDimension(const std::shared_ptr& system) +{ + if (system == nullptr) { + throw pcms_error("EmptyFieldLayout: coordinate system must not be null"); + } + if (system->Dimension() == 0) { + throw pcms_error( + "EmptyFieldLayout: requires a concrete coordinate system; the " + "dimension-deferred Cartesian placeholder has no coordinate data " + "here to resolve against"); + } + return system->Dimension(); +} + +} // namespace + +EmptyFieldLayout::EmptyFieldLayout( + std::shared_ptr system, int num_components) : owned_("null_owned", 0), gids_("null_gids", 0), class_dims_("null_class_dims", 0), @@ -12,8 +35,13 @@ EmptyFieldLayout::EmptyFieldLayout() 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) + coords_("null_coords", 0, RequireDimension(system)), + num_components_(num_components) { + if (num_components_ < 1) { + throw pcms_error("EmptyFieldLayout: num_components must be positive"); + } + SetCoordinateSystem(std::move(system)); discretization_ = std::make_shared(); } @@ -25,7 +53,7 @@ std::shared_ptr EmptyFieldLayout::GetDiscretization() int EmptyFieldLayout::GetNumComponents() const { - return 1; + return num_components_; } LO EmptyFieldLayout::GetNumOwnedDofHolder() const @@ -62,12 +90,12 @@ CoordinateView EmptyFieldLayout::GetDOFHolderCoordinates() const { auto coords_view = MakeConstRank2View(coords_); - return CoordinateView{CoordinateSystem::XGC, coords_view}; + return CoordinateView{GetCoordinateSystem(), coords_view}; } int EmptyFieldLayout::GetDimension() const { - return 2; + return GetCoordinateSystem()->Dimension(); } Rank1View diff --git a/src/pcms/field/layout/empty.h b/src/pcms/field/layout/empty.h index 991d58ff8..c3e129701 100644 --- a/src/pcms/field/layout/empty.h +++ b/src/pcms/field/layout/empty.h @@ -11,7 +11,11 @@ namespace pcms class EmptyFieldLayout : public FieldLayout { public: - EmptyFieldLayout(); + /// @param system coordinate system of the (absent) DOF holders + /// @param num_components components per value of fields on this layout; + /// fields whose values live outside PCMS still declare their width + explicit EmptyFieldLayout(std::shared_ptr system, + int num_components = 1); std::shared_ptr GetDiscretization() const noexcept override; @@ -41,6 +45,7 @@ class EmptyFieldLayout : public FieldLayout Kokkos::View classification_ids_host_; Kokkos::View coords_; std::shared_ptr discretization_; + int num_components_; }; } // namespace pcms diff --git a/src/pcms/field/layout/mesh_fields.cpp b/src/pcms/field/layout/mesh_fields.cpp index fa0866623..e93e02bc4 100644 --- a/src/pcms/field/layout/mesh_fields.cpp +++ b/src/pcms/field/layout/mesh_fields.cpp @@ -3,6 +3,7 @@ #include "pcms/utility/inclusive_scan.h" #include "pcms/utility/profile.h" #include +#include #include namespace pcms @@ -36,59 +37,133 @@ Omega_h::Write GetGidsHelper(LO total_ents, return owned_gids; } -// this is a workaround to specify the parametric coordinates for MeshFields to -// be replaced when https://github.com/SCOREC/meshFields/issues/70 is resolved -struct ComputeVertexCoordsFunctor +namespace { - Kokkos::View dof_holder_coords_; - Omega_h::Reals coords_; - size_t offset_; - - ComputeVertexCoordsFunctor(Kokkos::View dof_holder_coords, - Omega_h::Reals coords, size_t offset) - : dof_holder_coords_(dof_holder_coords), coords_(coords), offset_(offset) - { +// Maps a meshFields topology to the dimension. +KOKKOS_INLINE_FUNCTION int TopologyToDim(MeshField::Mesh_Topology topo) +{ + switch (topo) { + case MeshField::Vertex: return 0; + case MeshField::Edge: return 1; + case MeshField::Triangle: return 2; + case MeshField::Tetrahedron: return 3; + default: return -1; } +} - KOKKOS_INLINE_FUNCTION - void operator()(LO i) const - { - dof_holder_coords_(offset_ + i, 0) = coords_[2 * i + 0]; - dof_holder_coords_(offset_ + i, 1) = coords_[2 * i + 1]; +// Gets the appropriate MeshField element +template +auto GetMeshFieldElement(Omega_h::Mesh& mesh) +{ + if constexpr (Dim == 2) { + return MeshField::Omegah::getTriangleElement(mesh); + } else { + return MeshField::Omegah::getTetrahedronElement(mesh); } -}; +} -// this is a workaround to specify the parametric coordinates for MeshFields to -// be replaced when https://github.com/SCOREC/meshFields/issues/70 is resolved -struct ComputeEdgeCoordsFunctor +template +void BuildDofHolderCoordsFromMeshFieldImpl( + Omega_h::Mesh& mesh, Kokkos::View holder_coords, + const std::array& nodes_per_dim) { - Kokkos::View dof_holder_coords_; - Omega_h::Reals coords_; - Omega_h::LOs edge_verts_; - size_t offset_; + const auto elem = GetMeshFieldElement(mesh); - ComputeEdgeCoordsFunctor(Kokkos::View dof_holder_coords, - Omega_h::Reals coords, Omega_h::LOs edge_verts, - size_t offset) - : dof_holder_coords_(dof_holder_coords), - coords_(coords), - edge_verts_(edge_verts), - offset_(offset) + using ShapeT = std::decay_t; + constexpr size_t numNodes = ShapeT::numNodes; + constexpr size_t meshDim = ShapeT::meshEntDim; + constexpr MeshField::Mesh_Topology elemTopo = + (Dim == 2) ? MeshField::Triangle : MeshField::Tetrahedron; + const auto map = elem.map; + + // Compute the starting offset of DOF holders for each entity dimension. + Kokkos::Array dof_holder_offset; { + LO cur_offset = 0; + for (int d = 0; d < 4; ++d) { + dof_holder_offset[d] = cur_offset; + if (d <= mesh.dim() && nodes_per_dim[d] != 0) + cur_offset += mesh.nents(d); + } } - KOKKOS_INLINE_FUNCTION - void operator()(LO i) const - { - auto verts = Omega_h::gather_verts<2>(edge_verts_, i); - Real x0 = coords_[2 * verts[0] + 0]; - Real y0 = coords_[2 * verts[0] + 1]; - Real x1 = coords_[2 * verts[1] + 0]; - Real y1 = coords_[2 * verts[1] + 1]; - dof_holder_coords_(offset_ + i, 0) = (x0 + x1) / 2; - dof_holder_coords_(offset_ + i, 1) = (y0 + y1) / 2; + const LO nelems = mesh.nelems(); + const auto vtx_coords = mesh.coords(); + const auto param_coords = elem.shp.getNodeParametricCoords(); + + Kokkos::parallel_for( + "MeshFieldsDofHolderCoords", + Kokkos::RangePolicy(0, nelems), + KOKKOS_LAMBDA(const LO e) { + constexpr size_t NVerts = Dim + 1; + + Real Xv[NVerts * Dim]; + for (size_t b = 0; b < NVerts; ++b) { + const auto hv = map(static_cast(b), 0, e, elemTopo); + const LO v = hv.entity; + for (size_t d = 0; d < Dim; ++d) + Xv[b * Dim + d] = vtx_coords[static_cast(v) * Dim + d]; + } + + for (size_t n = 0; n < numNodes; ++n) { + const auto h = map(static_cast(n), 0, e, elemTopo); + + // Compute the barycentric coordinates L[b] at the node's parametric + // coordinates. + Real L[NVerts]; + L[0] = 1; + for (size_t d = 0; d < Dim; ++d) { + const Real xi_d = param_coords[n * Dim + d]; + L[d + 1] = xi_d; + L[0] -= xi_d; + } + + Real X[Dim] = {0}; + for (size_t b = 0; b < NVerts; ++b) + for (size_t d = 0; d < Dim; ++d) + X[d] += L[b] * Xv[b * Dim + d]; + + const int dim_of_holder = TopologyToDim(h.topo); + const LO row = dof_holder_offset[dim_of_holder] + h.entity; + for (size_t d = 0; d < Dim; ++d) + holder_coords(row, d) = X[d]; + } + }); +} + +void BuildDofHolderCoordsFromMeshField(Omega_h::Mesh& mesh, + Kokkos::View holder_coords, + const std::array& nodes_per_dim) +{ + int dim = mesh.dim(); + int order = 0; + for (int i = 0; i <= dim; ++i) { + if (nodes_per_dim[i] == 1) + ++order; + else if (nodes_per_dim[i] != 0) { + std::cerr << "Unsupported" << std::endl; + std::abort(); + } } -}; + + if (dim == 2 && order == 1) + BuildDofHolderCoordsFromMeshFieldImpl<2, 1>(mesh, holder_coords, + nodes_per_dim); + else if (dim == 2 && order == 2) + BuildDofHolderCoordsFromMeshFieldImpl<2, 2>(mesh, holder_coords, + nodes_per_dim); + else if (dim == 3 && order == 1) + BuildDofHolderCoordsFromMeshFieldImpl<3, 1>(mesh, holder_coords, + nodes_per_dim); + else if (dim == 3 && order == 2) + BuildDofHolderCoordsFromMeshFieldImpl<3, 2>(mesh, holder_coords, + nodes_per_dim); + else { + std::cerr << "Unsupported element/order combination" << std::endl; + std::abort(); + } +} +} // namespace struct CopyClassInfoFunctor { @@ -127,11 +202,11 @@ struct CopyClassInfoFunctor MeshFieldsAdapterLayout::MeshFieldsAdapterLayout( Omega_h::Mesh& mesh, std::array nodes_per_dim, int num_components, - CoordinateSystem coordinate_system, std::string global_id_name) + std::shared_ptr coordinate_system, + std::string global_id_name) : mesh_(mesh), global_id_name_(global_id_name), num_components_(num_components), - coordinate_system_(coordinate_system), nodes_per_dim_(nodes_per_dim), dof_holder_coords_("", GetNumOwnedDofHolder(), mesh_.dim()), class_ids_(GetNumEnts()), @@ -140,6 +215,8 @@ MeshFieldsAdapterLayout::MeshFieldsAdapterLayout( owned_host_("", class_dims_.size()) { PCMS_FUNCTION_TIMER; + SetCoordinateSystem( + ResolveCoordinateSystem(std::move(coordinate_system), mesh_.dim())); LO total_ents = GetNumEnts(); auto tag = mesh_.get_tagbase(0, global_id_name_); @@ -152,32 +229,9 @@ MeshFieldsAdapterLayout::MeshFieldsAdapterLayout( std::abort(); } - auto coords = mesh_.coords(); + BuildDofHolderCoordsFromMeshField(mesh_, dof_holder_coords_, nodes_per_dim); size_t offset = 0; - for (int i = 0; i <= mesh_.dim(); ++i) { - if (nodes_per_dim[i] == 1) { - if (i == 0) { - ComputeVertexCoordsFunctor functor(dof_holder_coords_, coords, offset); - Kokkos::parallel_for(mesh_.nents(0), functor); - } else if (i == 1) { - auto edge_verts = mesh_.ask_verts_of(1); - ComputeEdgeCoordsFunctor functor(dof_holder_coords_, coords, edge_verts, - offset); - Kokkos::parallel_for(mesh_.nents(1), functor); - } else { - std::cerr << "Unsupported" << std::endl; - std::abort(); - } - } else if (nodes_per_dim[i] != 0) { - std::cerr << "Unsupported" << std::endl; - std::abort(); - } - - offset += mesh.nents(i); - } - - offset = 0; for (int i = 0; i <= mesh_.dim(); ++i) { if (nodes_per_dim_[i]) { auto ids = mesh_.get_array(i, "class_id"); @@ -259,7 +313,7 @@ CoordinateView MeshFieldsAdapterLayout::GetDOFHolderCoordinates() const { auto coords_view = MakeConstRank2View(dof_holder_coords_); - return CoordinateView{coordinate_system_, coords_view}; + return CoordinateView{GetCoordinateSystem(), coords_view}; } bool MeshFieldsAdapterLayout::IsDistributed() const diff --git a/src/pcms/field/layout/mesh_fields.h b/src/pcms/field/layout/mesh_fields.h index 1c44760c6..004fd2028 100644 --- a/src/pcms/field/layout/mesh_fields.h +++ b/src/pcms/field/layout/mesh_fields.h @@ -6,7 +6,6 @@ #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" #include @@ -16,10 +15,10 @@ namespace pcms class MeshFieldsAdapterLayout : public FieldLayout { public: - MeshFieldsAdapterLayout(Omega_h::Mesh& mesh, std::array nodes_per_dim, - int num_components, - CoordinateSystem coordinate_system, - std::string global_id_name = "global"); + MeshFieldsAdapterLayout( + Omega_h::Mesh& mesh, std::array nodes_per_dim, int num_components, + std::shared_ptr coordinate_system, + std::string global_id_name = "global"); std::shared_ptr GetDiscretization() const noexcept override; @@ -60,11 +59,8 @@ class MeshFieldsAdapterLayout : public FieldLayout Omega_h::HostWrite gids_host_; std::string global_id_name_; int num_components_; - CoordinateSystem coordinate_system_; std::array nodes_per_dim_; Kokkos::View dof_holder_coords_; - Kokkos::View - dof_holder_coords_device_right_; Omega_h::Write class_ids_; Omega_h::Write class_dims_; Omega_h::HostWrite class_ids_host_; diff --git a/src/pcms/field/layout/mfem.cpp b/src/pcms/field/layout/mfem.cpp new file mode 100644 index 000000000..3e70e584c --- /dev/null +++ b/src/pcms/field/layout/mfem.cpp @@ -0,0 +1,171 @@ +#include "pcms/field/layout/mfem.hpp" + +#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, + std::shared_ptr coordinate_system) + : pmesh_(pmesh), pfes_(pfes), dim_(pmesh.SpaceDimension()) +{ + PCMS_FUNCTION_TIMER; + SetCoordinateSystem( + ResolveCoordinateSystem(std::move(coordinate_system), dim_)); + + AssertVertexScalarSpace(); + + const int nv = pmesh_.GetNV(); + + 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); + + mfem::Array vertex_gids; + pmesh_.GetGlobalVertexIndices(vertex_gids); + gids_host_ = Kokkos::View("mfem_gids", nv); + owned_host_ = Kokkos::View("mfem_owned", nv); + 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 +{ + 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 +{ + return static_cast(pmesh_.GetNV()); +} + +GO MFEMLayout::GetNumGlobalDofHolder() const +{ + 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{GetCoordinateSystem(), + MakeConstRank2View(coords_)}; +} + +bool MFEMLayout::IsDistributed() const +{ + return true; +} + +EntOffsetsArray MFEMLayout::GetEntOffsets() const +{ + EntOffsetsArray offsets{}; + offsets.fill(0); + const auto n = static_cast(GetNumOwnedDofHolder()); + 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.hpp b/src/pcms/field/layout/mfem.hpp new file mode 100644 index 000000000..edad78ead --- /dev/null +++ b/src/pcms/field/layout/mfem.hpp @@ -0,0 +1,66 @@ +#ifndef PCMS_FIELD_LAYOUT_MFEM_HPP +#define PCMS_FIELD_LAYOUT_MFEM_HPP + +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/field_layout.h" + +#include + +#include + +namespace pcms +{ + +// Field layout for an MFEM order-1 H1 (vertex) scalar field. +class MFEMLayout : public FieldLayout +{ +public: + MFEMLayout(mfem::ParMesh& pmesh, mfem::ParFiniteElementSpace& pfes, + std::shared_ptr 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 + static Kokkos::View OverlapMaskFromAttribute( + mfem::ParMesh& pmesh, int attribute); + +private: + void AssertVertexScalarSpace() const; + + mfem::ParMesh& pmesh_; + mfem::ParFiniteElementSpace& pfes_; + int dim_; + + 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_HPP diff --git a/src/pcms/field/layout/omega_h_entity.cpp b/src/pcms/field/layout/omega_h_entity.cpp index 32f390437..bbf5e4ae1 100644 --- a/src/pcms/field/layout/omega_h_entity.cpp +++ b/src/pcms/field/layout/omega_h_entity.cpp @@ -50,15 +50,16 @@ Kokkos::View BuildOwned(Omega_h::Mesh& mesh, } // namespace -OmegaHEntityLayout::OmegaHEntityLayout(Omega_h::Mesh& mesh, int entity_dim, - int num_components, - CoordinateSystem coordinate_system, - std::string global_id_name) - : dimension_(mesh.dim()), +OmegaHEntityLayout::OmegaHEntityLayout( + Omega_h::Mesh& mesh, int entity_dim, int num_components, + std::shared_ptr coordinate_system, + std::string global_id_name) + // dimension_ is the first member, so this runs before the class_ids_/ + // class_dims_ initializers below read the mesh's classification tags. + : dimension_((EnsureClassification(mesh), 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())), @@ -70,6 +71,8 @@ OmegaHEntityLayout::OmegaHEntityLayout(Omega_h::Mesh& mesh, int entity_dim, classification_ids_host_("classification_ids", mesh.nents(entity_dim)), discretization_(std::make_shared(mesh)) { + SetCoordinateSystem( + ResolveCoordinateSystem(std::move(coordinate_system), dimension_)); PCMS_ALWAYS_ASSERT(entity_dim_ >= 0 && entity_dim_ <= dimension_); gids_host_ = Omega_h::HostWrite(gids_); @@ -120,12 +123,8 @@ GlobalIDView OmegaHEntityLayout::GetGidsHost() const 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}; + return CoordinateView{GetCoordinateSystem(), + MakeConstRank2View(coords_2d_)}; } bool OmegaHEntityLayout::IsDistributed() const diff --git a/src/pcms/field/layout/omega_h_entity.h b/src/pcms/field/layout/omega_h_entity.h index 4c60545ae..65f651478 100644 --- a/src/pcms/field/layout/omega_h_entity.h +++ b/src/pcms/field/layout/omega_h_entity.h @@ -4,21 +4,16 @@ #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::shared_ptr coordinate_system, std::string global_id_name = "global"); std::shared_ptr GetDiscretization() @@ -47,7 +42,6 @@ class OmegaHEntityLayout : public FieldLayout int entity_dim_; int num_components_; GO num_global_dof_holder_; - CoordinateSystem coordinate_system_; Omega_h::Write gids_; Omega_h::HostWrite gids_host_; diff --git a/src/pcms/field/layout/omega_h_lagrange.cpp b/src/pcms/field/layout/omega_h_lagrange.cpp index a325140f9..df1b6f25c 100644 --- a/src/pcms/field/layout/omega_h_lagrange.cpp +++ b/src/pcms/field/layout/omega_h_lagrange.cpp @@ -3,7 +3,6 @@ #include "pcms/utility/mesh_geometry.h" #include "pcms/utility/omega_h_array_utils.h" #include "pcms/utility/profile.h" -#include #include namespace pcms @@ -49,35 +48,6 @@ Omega_h::Write BuildGids(Omega_h::Mesh& mesh, int entity_dim, return gids; } -// Some meshes (e.g. certain XGC meshes) are stored without any geometric -// classification (no class_id/class_dim tags). The layout and its -// discretization read these tags on every mesh dimension, so derive a default -// classification when it is missing. classify_elements + -// finalize_classification populate a geometrically meaningful class_dim on all -// dimensions but do not create class_id (that normally comes from a geometric -// model we don't have here), so fill any missing class_id with 0. -// Classification is only consulted for coupling DOF matching -// (field_exchange_planner); standalone field transfer never reads it, so a -// default id is sufficient to keep such meshes usable. -void EnsureClassification(Omega_h::Mesh& mesh) -{ - if (mesh.has_tag(mesh.dim(), "class_id") && - mesh.has_tag(mesh.dim(), "class_dim")) { - return; - } - if (!mesh.has_tag(mesh.dim(), "class_dim")) { - Omega_h::classify_elements(&mesh); - Omega_h::finalize_classification(&mesh); - } - for (int d = 0; d <= mesh.dim(); ++d) { - if (!mesh.has_tag(d, "class_id")) { - mesh.add_tag( - d, "class_id", 1, - Omega_h::Read(mesh.nents(d), 0, "class_id")); - } - } -} - Kokkos::View BuildOwned(Omega_h::Mesh& mesh, int entity_dim) { @@ -104,17 +74,18 @@ Kokkos::View BuildOwned( } // namespace -OmegaHLagrangeLayout::OmegaHLagrangeLayout(Omega_h::Mesh& mesh, int order, - int num_components, - CoordinateSystem coordinate_system, - std::string global_id_name) +OmegaHLagrangeLayout::OmegaHLagrangeLayout( + Omega_h::Mesh& mesh, int order, int num_components, + std::shared_ptr 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; + SetCoordinateSystem( + ResolveCoordinateSystem(std::move(coordinate_system), mesh_.dim())); int entity_dim = EntityDimForOrder(order_, mesh_.dim()); EnsureClassification(mesh_); @@ -152,15 +123,16 @@ OmegaHLagrangeLayout::OmegaHLagrangeLayout(Omega_h::Mesh& mesh, int order, OmegaHLagrangeLayout::OmegaHLagrangeLayout( Omega_h::Mesh& mesh, int order, int num_components, - CoordinateSystem coordinate_system, Omega_h::Read owned_mask, - std::string global_id_name) + std::shared_ptr 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; + SetCoordinateSystem( + ResolveCoordinateSystem(std::move(coordinate_system), mesh_.dim())); int entity_dim = EntityDimForOrder(order_, mesh_.dim()); EnsureClassification(mesh_); @@ -171,6 +143,7 @@ OmegaHLagrangeLayout::OmegaHLagrangeLayout( owned_ = BuildOwned(mesh_, entity_dim, owned_mask); 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")); @@ -235,14 +208,8 @@ GlobalIDView OmegaHLagrangeLayout::GetGids() const 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}; + return CoordinateView{GetCoordinateSystem(), + MakeConstRank2View(coords_2d_)}; } bool OmegaHLagrangeLayout::IsDistributed() const diff --git a/src/pcms/field/layout/omega_h_lagrange.h b/src/pcms/field/layout/omega_h_lagrange.h index 68154979e..7951bb0e6 100644 --- a/src/pcms/field/layout/omega_h_lagrange.h +++ b/src/pcms/field/layout/omega_h_lagrange.h @@ -5,7 +5,6 @@ #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 @@ -18,13 +17,15 @@ namespace pcms 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"); + OmegaHLagrangeLayout( + Omega_h::Mesh& mesh, int order, int num_components, + std::shared_ptr coordinate_system, + std::string global_id_name = "global"); + OmegaHLagrangeLayout( + Omega_h::Mesh& mesh, int order, int num_components, + std::shared_ptr coordinate_system, + Omega_h::Read owned_mask, + std::string global_id_name = "global"); std::shared_ptr GetDiscretization() const noexcept override; @@ -57,7 +58,6 @@ class OmegaHLagrangeLayout : public FieldLayout Omega_h::Mesh& mesh_; int order_; int num_components_; - CoordinateSystem coordinate_system_; std::string global_id_name_; Omega_h::Write gids_; diff --git a/src/pcms/field/layout/point_cloud.cpp b/src/pcms/field/layout/point_cloud.cpp index ffa9ca4d7..1c020e279 100644 --- a/src/pcms/field/layout/point_cloud.cpp +++ b/src/pcms/field/layout/point_cloud.cpp @@ -37,25 +37,28 @@ void InitializePointCloudClassification( } // namespace -PointCloudLayout::PointCloudLayout(int dim, Kokkos::View coords, - CoordinateSystem coordinate_system) - : PointCloudLayout(dim, coords, coordinate_system, +PointCloudLayout::PointCloudLayout( + int dim, Kokkos::View coords, + std::shared_ptr coordinate_system) + : PointCloudLayout(dim, coords, std::move(coordinate_system), MakePointCloudDiscretization(dim, coords), Vertex) { } PointCloudLayout::PointCloudLayout( - int dim, Kokkos::View coords, CoordinateSystem coordinate_system, + int dim, Kokkos::View coords, + std::shared_ptr 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)) { + SetCoordinateSystem(ResolveCoordinateSystem( + std::move(coordinate_system), static_cast(coords_.extent(1)))); components_ = 1; namespace KE = Kokkos::Experimental; @@ -116,7 +119,7 @@ CoordinateView PointCloudLayout::GetDOFHolderCoordinates() const { auto coords_view = MakeConstRank2View(coords_); - return CoordinateView{coordinate_system_, coords_view}; + return CoordinateView{GetCoordinateSystem(), coords_view}; } bool PointCloudLayout::IsDistributed() const diff --git a/src/pcms/field/layout/point_cloud.h b/src/pcms/field/layout/point_cloud.h index 50e6e9874..1754c0add 100644 --- a/src/pcms/field/layout/point_cloud.h +++ b/src/pcms/field/layout/point_cloud.h @@ -11,9 +11,9 @@ class PointCloudLayout : public FieldLayout { public: PointCloudLayout(int dim, Kokkos::View coords, - CoordinateSystem coordinate_system); + std::shared_ptr coordinate_system); PointCloudLayout(int dim, Kokkos::View coords, - CoordinateSystem coordinate_system, + std::shared_ptr coordinate_system, std::shared_ptr discretization, int classification_entity_dim); @@ -48,7 +48,6 @@ class PointCloudLayout : public FieldLayout private: int dim_; int components_; - CoordinateSystem coordinate_system_; Kokkos::View coords_; Kokkos::View owned_; Kokkos::View gids_; diff --git a/src/pcms/field/layout/uniform_grid.cpp b/src/pcms/field/layout/uniform_grid.cpp index 08f4823d1..5887d0966 100644 --- a/src/pcms/field/layout/uniform_grid.cpp +++ b/src/pcms/field/layout/uniform_grid.cpp @@ -120,11 +120,10 @@ struct InitilizeGidsAndOwnedFunctor template UniformGridFieldLayout::UniformGridFieldLayout( - UniformGrid grid, int num_components, CoordinateSystem coordinate_system, - int order) + UniformGrid grid, int num_components, + std::shared_ptr 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()), @@ -133,6 +132,8 @@ UniformGridFieldLayout::UniformGridFieldLayout( owned_host_("owned_host", GetNumDofHolders()) { PCMS_FUNCTION_TIMER; + SetCoordinateSystem(ResolveCoordinateSystem(std::move(coordinate_system), + static_cast(Dim))); PCMS_ALWAYS_ASSERT(order_ == 0 || order_ == 1); LO num_dofs = GetNumDofHolders(); @@ -234,7 +235,7 @@ CoordinateView UniformGridFieldLayout::GetDOFHolderCoordinates() const { auto coords_view = MakeConstRank2View(dof_holder_coords_); - return CoordinateView{coordinate_system_, coords_view}; + return CoordinateView{GetCoordinateSystem(), coords_view}; } template diff --git a/src/pcms/field/layout/uniform_grid.h b/src/pcms/field/layout/uniform_grid.h index f4a78a854..21828edd8 100644 --- a/src/pcms/field/layout/uniform_grid.h +++ b/src/pcms/field/layout/uniform_grid.h @@ -4,7 +4,6 @@ #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" @@ -16,8 +15,9 @@ template class UniformGridFieldLayout : public FieldLayout { public: - UniformGridFieldLayout(UniformGrid grid, int num_components, - CoordinateSystem coordinate_system, int order = 1); + UniformGridFieldLayout( + UniformGrid grid, int num_components, + std::shared_ptr coordinate_system, int order = 1); std::shared_ptr GetDiscretization() const noexcept override; @@ -52,7 +52,6 @@ class UniformGridFieldLayout : public FieldLayout UniformGrid grid_; int num_components_; - CoordinateSystem coordinate_system_; int order_; Kokkos::View gids_; Kokkos::View owned_; diff --git a/src/pcms/field/layout/xgc.cpp b/src/pcms/field/layout/xgc.cpp index 18729189f..54d9eecfd 100644 --- a/src/pcms/field/layout/xgc.cpp +++ b/src/pcms/field/layout/xgc.cpp @@ -1,5 +1,7 @@ #include "pcms/field/layout/xgc.h" #include "pcms/utility/assert.h" +#include "pcms/utility/entity_types.h" +#include "pcms/field/coordinate_systems/cylindrical.hpp" namespace pcms { @@ -10,60 +12,39 @@ struct InitilizeXGCMembersFunctor 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) + Rank1View vert_class_dims_; + Rank1View vert_class_ids_; + + InitilizeXGCMembersFunctor( + Kokkos::View owned, + Kokkos::View gids, + Kokkos::View class_dims, + Kokkos::View class_ids, + Rank1View vert_class_dims, + Rank1View vert_class_ids) + : owned_(owned), + gids_(gids), + class_dims_(class_dims), + class_ids_(class_ids), + vert_class_dims_(vert_class_dims), + vert_class_ids_(vert_class_ids) { } KOKKOS_INLINE_FUNCTION void operator()(LO i) const { - owned_(i) = false; + owned_(i) = true; gids_(i) = static_cast(i) + 1; - class_dims_(i) = -1; - class_ids_(i) = -1; + // ClassificationDimension is int8_t, a small integer rather than a char + // NOLINTNEXTLINE(bugprone-signed-char-misuse) + class_dims_(i) = vert_class_dims_(i); + class_ids_(i) = vert_class_ids_(i); } }; -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) + const ReverseClassificationVertex& reverse_classification, LO num_plane_nodes) : owned_("xgc_owned", num_plane_nodes), gids_("xgc_gids", num_plane_nodes), class_dims_("xgc_class_dims", num_plane_nodes), @@ -75,33 +56,18 @@ XGCFieldLayout::XGCFieldLayout( coords_("xgc_coords", num_plane_nodes, 2), num_plane_nodes_(num_plane_nodes) { - PCMS_ALWAYS_ASSERT(static_cast(in_overlap)); + // XGC poloidal planes are in the RZ plane of a cylindrical coordinate system + SetCoordinateSystem(csys::CylindricalRZ::Create()); + auto discretization = std::make_shared( + reverse_classification, num_plane_nodes_); 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 - } + InitilizeXGCMembersFunctor( + owned_, gids_, class_dims_, class_ids_, + discretization->GetEntityClassificationDimensions(Vertex), + discretization->GetEntityClassificationIds(Vertex))); Kokkos::deep_copy(owned_host_, owned_); Kokkos::deep_copy(gids_host_, gids_); @@ -109,8 +75,7 @@ XGCFieldLayout::XGCFieldLayout( 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_); + discretization_ = std::move(discretization); } std::shared_ptr XGCFieldLayout::GetDiscretization() @@ -160,12 +125,8 @@ EntOffsetsArray XGCFieldLayout::GetEntOffsets() const CoordinateView XGCFieldLayout::GetDOFHolderCoordinates() const { - using LayoutPolicy = - detail::default_layout_for_memory_space_t; - return CoordinateView{ - CoordinateSystem::XGC, - Rank2View( - coords_.data(), num_plane_nodes_, 2)}; + return CoordinateView{GetCoordinateSystem(), + MakeConstRank2View(coords_)}; } int XGCFieldLayout::GetDimension() const diff --git a/src/pcms/field/layout/xgc.h b/src/pcms/field/layout/xgc.h index a755dcb0b..8d3e3ff43 100644 --- a/src/pcms/field/layout/xgc.h +++ b/src/pcms/field/layout/xgc.h @@ -3,9 +3,7 @@ #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 { @@ -14,7 +12,6 @@ class XGCFieldLayout : public FieldLayout { public: XGCFieldLayout(const ReverseClassificationVertex& reverse_classification, - std::function in_overlap, LO num_plane_nodes); std::shared_ptr GetDiscretization() diff --git a/src/pcms/field/point_evaluator.h b/src/pcms/field/point_evaluator.h index 8250c4ebc..9ccd66fe9 100644 --- a/src/pcms/field/point_evaluator.h +++ b/src/pcms/field/point_evaluator.h @@ -1,8 +1,13 @@ #ifndef PCMS_POINT_EVALUATOR_H #define PCMS_POINT_EVALUATOR_H +#include "pcms/field/value_view.hpp" #include "pcms/utility/arrays.h" #include "pcms/utility/memory_spaces.h" +#include "pcms/utility/types.h" +#include +#include +#include namespace pcms { @@ -26,21 +31,78 @@ class Field; // 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] +// Evaluate writes results into a tagged rank-2 output view with shape: +// [NumPoints()][num_components of the field] // The caller must provide the full output buffer. Successful Evaluate calls // fill the entire buffer. +// +// The write is tagged: the ValueView the caller supplies must claim +// OutputBasis(stored basis of the field) (all rank-0 bases compare equal, so +// scalar callers may tag with ValueBasis{}). +// +// Evaluate checks the output's shape and tag before calling EvaluateImpl, so +// implementations may assume both. template > class PointEvaluator { public: - virtual void Evaluate( - const Field& field, - Rank2View values) const = 0; + /// Writes the field's values at the query points into `values`. + /// @throws std::invalid_argument if `values` is not shaped + /// [NumPoints()][number of components of the field] + /// @throws pcms_error if `values` does not claim the basis this evaluator + /// writes for the field + void Evaluate(const Field& field, + ValueView values) const + { + CheckOutputShape(field, values); + detail::CheckWrittenValueBasis( + "PointEvaluator::Evaluate", values.GetBasis(), + OutputBasis(field.GetData().GetValueBasis())); + EvaluateImpl(field, values); + } + + /// Number of query points, fixed at construction. + [[nodiscard]] virtual LO NumPoints() const = 0; + + /// Value basis this evaluator writes for a field stored in `stored`. + [[nodiscard]] virtual ValueBasis OutputBasis(const ValueBasis& stored) const + { + return stored; + } + + /// Query points whose output rows Evaluate sets to the out-of-bounds fill + /// value rather than to field values; empty unless the evaluator was created + /// with OutOfBoundsMode::FILL. Fixed at construction. + [[nodiscard]] virtual Kokkos::View + FilledPoints() const = 0; virtual ~PointEvaluator() noexcept = default; + +protected: + /// Evaluate after its checks: `values` is shaped and tagged correctly. + virtual void EvaluateImpl( + const Field& field, + ValueView values) const = 0; + +private: + void CheckOutputShape( + const Field& field, + const ValueView& values) const + { + const auto num_points = static_cast(NumPoints()); + const auto num_components = + static_cast(field.GetLayout().GetNumComponents()); + if (values.extent(0) != num_points || values.extent(1) != num_components) { + throw std::invalid_argument( + "PointEvaluator::Evaluate: the output view is shaped [" + + std::to_string(values.extent(0)) + "][" + + std::to_string(values.extent(1)) + "] but must be [" + + std::to_string(num_points) + "][" + std::to_string(num_components) + + "] (query points x field components)"); + } + } }; // Variant types using default layout for device memory space diff --git a/src/pcms/field/point_evaluator_factory.hpp b/src/pcms/field/point_evaluator_factory.hpp new file mode 100644 index 000000000..5622a9fcb --- /dev/null +++ b/src/pcms/field/point_evaluator_factory.hpp @@ -0,0 +1,61 @@ +#ifndef PCMS_FIELD_POINT_EVALUATOR_FACTORY_HPP +#define PCMS_FIELD_POINT_EVALUATOR_FACTORY_HPP + +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/evaluation_request.h" +#include "pcms/field/field_factory.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/types.h" +#include +#include +#include + +namespace pcms +{ + +class PointEvaluatorFactory +{ +public: + /// Coordinate system that query points handed to CreatePointEvaluator must + /// be expressed in. + [[nodiscard]] virtual const std::shared_ptr& + GetCoordinateSystem() const = 0; + + /// Creates a point evaluator bound to the request's query points. + /// @param request query coordinates, out-of-bounds policy, and optional + /// provenance; the coordinates must be in GetCoordinateSystem() + template + [[nodiscard]] std::unique_ptr> CreatePointEvaluator( + const EvaluationRequest& request) const; + + virtual ~PointEvaluatorFactory() noexcept = default; + +protected: + virtual PointEvaluatorVariant CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const = 0; +}; + +template +std::unique_ptr> PointEvaluatorFactory::CreatePointEvaluator( + const EvaluationRequest& request) const +{ + static_assert(is_supported_field_type_v, + "T is not a supported field type"); + if (!SameCoordinateSystem(request.coords.GetCoordinateSystem(), + GetCoordinateSystem())) { + throw pcms_error( + "CreatePointEvaluator: query coordinate system '" + + std::string(request.coords.GetCoordinateSystem()->Kind()) + + "' is not the factory's coordinate system '" + + std::string(GetCoordinateSystem()->Kind()) + + "'; map the points with a CoordinateMap or evaluate through a " + "TransformedSource"); + } + return std::get>>( + CreatePointEvaluatorImpl(TypeEnumFromType(), request)); +} + +} // namespace pcms + +#endif // PCMS_FIELD_POINT_EVALUATOR_FACTORY_HPP diff --git a/src/pcms/field/transformed_source.cpp b/src/pcms/field/transformed_source.cpp new file mode 100644 index 000000000..15ec29178 --- /dev/null +++ b/src/pcms/field/transformed_source.cpp @@ -0,0 +1,79 @@ +#include "pcms/field/transformed_source.hpp" +#include "pcms/field/function_space.h" +#include + +namespace pcms +{ + +namespace detail +{ + +void FillRows(Rank2View values, + const Kokkos::View& rows, + Real fill_value) +{ + const auto num_cols = static_cast(values.extent(1)); + Kokkos::parallel_for( + "transformed_source_fill_rows", + Kokkos::RangePolicy( + 0, static_cast(rows.extent(0))), + KOKKOS_LAMBDA(const LO k) { + for (int c = 0; c < num_cols; ++c) { + values(rows(k), c) = fill_value; + } + }); +} + +} // namespace detail + +TransformedSource::TransformedSource( + std::shared_ptr source, + std::shared_ptr to_source) + : source_(std::move(source)), to_source_(std::move(to_source)) +{ + if (source_ == nullptr) { + throw pcms_error("TransformedSource: source must not be null"); + } + if (to_source_ == nullptr) { + throw pcms_error("TransformedSource: to_source must not be null"); + } + if (!SameCoordinateSystem(to_source_->GetTargetCoordinateSystem(), + source_->GetCoordinateSystem())) { + throw pcms_error( + "TransformedSource: to_source maps into '" + + std::string(to_source_->GetTargetCoordinateSystem()->Kind()) + + "' but the source's coordinate system is '" + + std::string(source_->GetCoordinateSystem()->Kind()) + + "'; expected a map ending in '" + + std::string(source_->GetCoordinateSystem()->Kind()) + "'"); + } + system_ = to_source_->GetSourceCoordinateSystem(); +} + +template +std::unique_ptr> TransformedSource::Create( + const EvaluationRequest& request) const +{ + PCMS_FUNCTION_TIMER; + auto bound = to_source_->Bind(request.coords); + auto inner = source_->CreatePointEvaluator( + EvaluationRequest::FromCoordinates(bound->MappedPoints(), request.policy)); + return std::make_unique>( + std::move(bound), std::move(inner), request.policy); +} + +PointEvaluatorVariant TransformedSource::CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const +{ + switch (value_type) { + case Type::Real: return Create(request); + case Type::LO: return Create(request); + case Type::GO: return Create(request); + case Type::Int8: return Create(request); + case Type::Float: return Create(request); + } + throw pcms_error("TransformedSource::CreatePointEvaluator: unsupported " + "value type"); +} + +} // namespace pcms diff --git a/src/pcms/field/transformed_source.hpp b/src/pcms/field/transformed_source.hpp new file mode 100644 index 000000000..e27f033f6 --- /dev/null +++ b/src/pcms/field/transformed_source.hpp @@ -0,0 +1,138 @@ +#ifndef PCMS_FIELD_TRANSFORMED_SOURCE_HPP +#define PCMS_FIELD_TRANSFORMED_SOURCE_HPP + +#include "pcms/field/coordinate_map.hpp" +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/evaluation_request.h" +#include "pcms/field/field.h" +#include "pcms/field/field_data.h" +#include "pcms/field/out_of_bounds_policy.h" +#include "pcms/field/point_evaluator.h" +#include "pcms/field/point_evaluator_factory.hpp" +#include "pcms/field/value_view.hpp" +#include "pcms/utility/arrays.h" +#include "pcms/utility/assert.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/profile.h" +#include "pcms/utility/types.h" +#include +#include +#include +#include + +namespace pcms +{ + +namespace detail +{ + +/// Sets every component of the listed rows of `values` to `fill_value`. +void FillRows(Rank2View values, + const Kokkos::View& rows, + Real fill_value); + +} // namespace detail + +/// Point evaluator over mapped query points that re-expresses evaluated +/// components in the bound map's output basis. +template +class TransformedPointEvaluator final : public PointEvaluator +{ +public: + /// @param bound map bound to the query points; owns the mapped coordinates + /// `inner` was built over, so it is declared first and outlives it + /// @param inner evaluator bound to `bound`'s mapped points + /// @param policy out-of-bounds policy `inner` was built with + TransformedPointEvaluator(std::unique_ptr bound, + std::unique_ptr> inner, + OutOfBoundsPolicy policy) + : bound_(std::move(bound)), inner_(std::move(inner)), policy_(policy) + { + } + + [[nodiscard]] LO NumPoints() const override { return inner_->NumPoints(); } + + // Public because CUDA forbids extended lambdas in non-public member + // functions. User should call Evaluate + // TODO: add passkey or other mechanism to avoid user calling this + void EvaluateImpl(const Field& field, + ValueView values) const override + { + PCMS_FUNCTION_TIMER; + const ValueBasis inner_out = + inner_->OutputBasis(field.GetData().GetValueBasis()); + if (inner_out.Rank() == 0) { + inner_->Evaluate(field, values); + return; + } + if constexpr (std::is_same_v) { + const ValueView source_values( + inner_out, values.GetValues()); + inner_->Evaluate(field, source_values); + bound_->TransformValues(source_values); + // Fill rows hold no field value to re-express, so the rotation must + // not change them. + detail::FillRows(values.GetValues(), inner_->FilledPoints(), + policy_.fill_value); + } else { + throw pcms_error("TransformedPointEvaluator::Evaluate: basis " + "transformations require T == Real"); + } + } + + [[nodiscard]] Kokkos::View FilledPoints() + const override + { + return inner_->FilledPoints(); + } + + [[nodiscard]] ValueBasis OutputBasis(const ValueBasis& stored) const override + { + const ValueBasis inner_out = inner_->OutputBasis(stored); + if (inner_out.Rank() == 0) { + return inner_out; + } + return bound_->OutputBasis(inner_out); + } + +private: + std::unique_ptr bound_; + std::unique_ptr> inner_; + OutOfBoundsPolicy policy_; +}; + +/// Presents a point-evaluator factory as one in another coordinate system: +/// query points are mapped into the source's system and evaluated components +/// are re-expressed at the query points. +class TransformedSource final : public PointEvaluatorFactory +{ +public: + /// @param source factory whose evaluators do the evaluation; co-owned, so + /// nesting requires the outer view to hold a shared_ptr to the inner + /// @param to_source map from this object's coordinate system to `source`'s + TransformedSource(std::shared_ptr source, + std::shared_ptr to_source); + + [[nodiscard]] const std::shared_ptr& + GetCoordinateSystem() const override + { + return system_; + } + +protected: + PointEvaluatorVariant CreatePointEvaluatorImpl( + Type value_type, const EvaluationRequest& request) const override; + +private: + template + std::unique_ptr> Create( + const EvaluationRequest& request) const; + + std::shared_ptr source_; + std::shared_ptr to_source_; + std::shared_ptr system_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_TRANSFORMED_SOURCE_HPP diff --git a/src/pcms/field/uniform_grid_binary_field.h b/src/pcms/field/uniform_grid_binary_field.h index 6a71de7a9..a27fe7489 100644 --- a/src/pcms/field/uniform_grid_binary_field.h +++ b/src/pcms/field/uniform_grid_binary_field.h @@ -4,8 +4,9 @@ #include "pcms/field/layout/uniform_grid.h" #include "pcms/field/data/simple.h" #include "pcms/field/function_space/lagrange.h" +#include "pcms/field/value_view.hpp" #include "pcms/field/field_metadata.h" -#include "pcms/localization/point_search.h" +#include "pcms/localization/uniform_grid_localization.hpp" #include "pcms/utility/arrays.h" #include "pcms/utility/types.h" #include "pcms/utility/uniform_grid.h" @@ -15,6 +16,7 @@ #include #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" namespace pcms { @@ -37,17 +39,17 @@ std::pair>, Field> CreateUniformGridBinaryField(Omega_h::Mesh& mesh, const UniformGrid& grid) { auto function_space = LagrangeFunctionSpace::FromUniformGrid( - grid, 1, CoordinateSystem::Cartesian); + grid, 1, csys::Cartesian::Deferred()); auto layout = std::dynamic_pointer_cast>( - function_space.GetLayout()); + function_space->GetLayout()); PCMS_ALWAYS_ASSERT(layout != nullptr); - auto field = function_space.template CreateField(FieldMetadata{}); + Field field = function_space->template CreateFunction(""); auto coord_view = layout->GetDOFHolderCoordinates(); auto coords = coord_view.GetValues(); LO n = layout->GetNumOwnedDofHolder(); - Kokkos::View coords_d("coords_d", n); + Kokkos::View coords_d("coords_d", n, Dim); Kokkos::parallel_for( "CopyCoords", Kokkos::RangePolicy<>(0, n), KOKKOS_LAMBDA(int i) { for (int d = 0; d < Dim; ++d) { @@ -56,23 +58,25 @@ CreateUniformGridBinaryField(Omega_h::Mesh& mesh, const UniformGrid& grid) }); // Run point-in-mesh search - Kokkos::View::Result*> results_d; + PointSearch::Results results_d; if constexpr (Dim == 2) { GridPointSearch2D search(mesh, grid.divisions[0], grid.divisions[1]); - results_d = search(coords_d); + results_d = search.Apply(pcms::CoordinateView( + pcms::CoordinateSystem::Cartesian, pcms::MakeConstRank2View(coords_d))); } else { GridPointSearch3D search(mesh, grid.divisions[0], grid.divisions[1], grid.divisions[2]); - results_d = search(coords_d); + results_d = search.Apply(pcms::CoordinateView( + pcms::CoordinateSystem::Cartesian, pcms::MakeConstRank2View(coords_d))); } - auto results_h = - Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, results_d); + auto result_ids_h = Kokkos::create_mirror_view_and_copy( + Kokkos::HostSpace{}, results_d.element_ids); 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; + data(i) = (result_ids_h(i) >= 0) ? 1.0 : 0.0; - field.SetDOFHolderDataHost( + field.SetDOFHolderDataUncheckedHost( Rank2View(data.data(), n, 1)); return {std::move(layout), std::move(field)}; diff --git a/src/pcms/field/value_view.hpp b/src/pcms/field/value_view.hpp new file mode 100644 index 000000000..bed7eddc8 --- /dev/null +++ b/src/pcms/field/value_view.hpp @@ -0,0 +1,302 @@ +#ifndef PCMS_FIELD_VALUE_VIEW_HPP +#define PCMS_FIELD_VALUE_VIEW_HPP + +#include "pcms/field/coordinate_system.hpp" +#include "pcms/utility/arrays.h" +#include "pcms/utility/assert.h" +#include +#include +#include +#include +#include +#include +#include + +namespace pcms +{ + +enum class FieldValueType +{ + Scalar, + Vector, + Tensor +}; + +// Index variance: Contravariant == components on the basis dx/dq^i, +// Covariant == components on the dual grad q^i. +enum class Variance : std::uint8_t +{ + Contravariant, + Covariant +}; + +// How the components are scaled relative to the coordinate-induced basis. +// +// each component physical coordinates all have the same units +// the natural coordinates are the standard, unscaled coordinate bases +enum class ComponentScaling : std::uint8_t +{ + Physical, + Natural +}; + +class VarianceSignature +{ +public: + static constexpr int max_rank = 4; + + VarianceSignature() = default; + VarianceSignature(std::initializer_list entries) + { + if (entries.size() > static_cast(max_rank)) { + throw pcms_error("VarianceSignature: rank must be 0.." + + std::to_string(max_rank) + ", got " + + std::to_string(entries.size())); + } + for (Variance v : entries) { + entries_[static_cast(rank_++)] = v; + } + } + + [[nodiscard]] int Rank() const noexcept { return rank_; } + [[nodiscard]] Variance operator[](int i) const noexcept + { + return entries_[static_cast(i)]; + } + + friend bool operator==(const VarianceSignature& a, + const VarianceSignature& b) noexcept + { + if (a.rank_ != b.rank_) { + return false; + } + for (int i = 0; i < a.rank_; ++i) { + if (a.entries_[static_cast(i)] != + b.entries_[static_cast(i)]) { + return false; + } + } + return true; + } + +private: + std::array entries_{}; + int rank_ = 0; +}; + +struct ValueBasis +{ + std::shared_ptr system = nullptr; + ComponentScaling scaling = ComponentScaling::Physical; + VarianceSignature variance{}; + + [[nodiscard]] int Rank() const noexcept { return variance.Rank(); } + + friend bool operator==(const ValueBasis&, const ValueBasis&) = default; +}; + +[[nodiscard]] inline FieldValueType ValueTypeOfRank(int rank) +{ + switch (rank) { + case 0: return FieldValueType::Scalar; + case 1: return FieldValueType::Vector; + case 2: return FieldValueType::Tensor; + default: break; + } + throw pcms_error("ValueTypeOfRank: no FieldValueType label for rank " + + std::to_string(rank)); +} + +[[nodiscard]] inline std::optional DerivedComponentScaling( + const CoordinateSystem& system) noexcept +{ + if (!system.HasOrthogonalBasis()) { + return ComponentScaling::Natural; + } + if (system.HasUnitScaleFactors()) { + return ComponentScaling::Physical; + } + return std::nullopt; +} + +[[nodiscard]] inline bool SameValueBasis(const ValueBasis& a, + const ValueBasis& b) noexcept +{ + if (a.Rank() == 0 && b.Rank() == 0) { + return true; + } + if (a.Rank() != b.Rank() || !SameCoordinateSystem(a.system, b.system) || + a.scaling != b.scaling) { + return false; + } + return a.scaling == ComponentScaling::Physical || a.variance == b.variance; +} + +namespace detail +{ + +/// Gate for a tagged write: throws pcms_error unless the basis the caller +/// claimed on the output view is the basis the callee is about to write. +/// @param context callee name used to prefix the error message +/// @param claimed basis tagged onto the caller-supplied output view +/// @param written basis the callee actually writes +inline void CheckWrittenValueBasis(const char* context, + const ValueBasis& claimed, + const ValueBasis& written) +{ + if (!SameValueBasis(claimed, written)) { + throw pcms_error(std::string(context) + + ": the output view's declared value type/basis does not " + "match the basis this call writes"); + } +} + +inline void ValidateValueBasis(const ValueBasis& basis) +{ + if (basis.Rank() == 0) { + return; + } + if (basis.Rank() > 2) { + throw pcms_error("ValidateValueBasis: rank-" + + std::to_string(basis.Rank()) + + " values are not yet supported (maximum supported rank " + "is 2)"); + } + if (basis.system == nullptr) { + throw pcms_error( + "ValidateValueBasis: Vector/Tensor values require a basis coordinate " + "system"); + } + if (basis.scaling == ComponentScaling::Physical && + !basis.system->HasOrthogonalBasis()) { + throw pcms_error( + "ValidateValueBasis: coordinate system '" + + std::string(basis.system->Kind()) + + "' has a non-orthogonal basis, so physical components are undefined; " + "use ComponentScaling::Natural, or borrow an orthogonal system's basis"); + } +} + +inline void ValidateValueSemantics(const ValueBasis& basis, int num_components) +{ + ValidateValueBasis(basis); + const int rank = basis.Rank(); + if (rank == 0) { + return; + } + int expected = 1; + for (int i = 0; i < rank; ++i) { + expected *= basis.system->Dimension(); + } + if (num_components != expected) { + throw pcms_error("ValidateValueSemantics: rank-" + std::to_string(rank) + + " values on coordinate system '" + + std::string(basis.system->Kind()) + "' (dimension " + + std::to_string(basis.system->Dimension()) + ") require " + + std::to_string(expected) + " components, got " + + std::to_string(num_components)); + } +} + +inline ValueBasis MakeValueBasis( + const std::shared_ptr& space_system, + VarianceSignature variance, + std::shared_ptr system, // null == the space's own + std::optional scaling) +{ + if (variance.Rank() == 0) { + return ValueBasis{}; + } + if (system == nullptr) { + system = space_system; + } + ComponentScaling resolved; + if (scaling.has_value()) { + resolved = *scaling; + } else if (auto derived = DerivedComponentScaling(*system)) { + resolved = *derived; + } else { + throw pcms_error( + "CreateFunction: coordinate system '" + std::string(system->Kind()) + + "' admits both physical and natural components and they differ, so " + "the scaling must be stated (pass ComponentScaling::Physical or " + "ComponentScaling::Natural)"); + } + return ValueBasis{std::move(system), resolved, variance}; +} + +} // namespace detail + +namespace values +{ + +inline const VarianceSignature Scalar{}; +inline const VarianceSignature Vector{Variance::Contravariant}; +inline const VarianceSignature Covector{Variance::Covariant}; +inline const VarianceSignature Tensor{Variance::Contravariant, + Variance::Contravariant}; + +[[nodiscard]] inline VarianceSignature Of( + std::initializer_list variance) +{ + return VarianceSignature{variance}; +} + +} // namespace values + +// view for field values that is tagged with the coordinate system +template > +class ValueView +{ +public: + using element_type = ElementType; + + ValueView(ValueBasis basis, + Rank2View values) + : basis_(std::move(basis)), values_(values) + { + detail::ValidateValueSemantics(basis_, static_cast(values_.extent(1))); + } + + template && + std::is_same_v>> + ValueView(const ValueView& other) + : basis_(other.GetBasis()), values_(other.GetValues()) + { + } + + [[nodiscard]] int Rank() const noexcept { return basis_.Rank(); } + [[nodiscard]] FieldValueType GetValueType() const + { + return ValueTypeOfRank(basis_.Rank()); + } + + [[nodiscard]] const ValueBasis& GetBasis() const noexcept { return basis_; } + + [[nodiscard]] Rank2View GetValues() + const noexcept + { + return values_; + } + + [[nodiscard]] size_t extent(size_t r) const noexcept + { + return values_.extent(r); + } + [[nodiscard]] size_t size() const noexcept { return values_.size(); } + // Index through GetValues(): a ValueView carries a shared_ptr and must not + // be captured by a device kernel. + decltype(auto) operator()(size_t i, size_t c) const = delete; + +private: + ValueBasis basis_; + Rank2View values_; +}; + +} // namespace pcms + +#endif // PCMS_FIELD_VALUE_VIEW_HPP diff --git a/src/pcms/fortranapi/client.i b/src/pcms/fortranapi/client.i index f86dc731e..39ebe3ff3 100644 --- a/src/pcms/fortranapi/client.i +++ b/src/pcms/fortranapi/client.i @@ -30,7 +30,8 @@ enum PcmsType struct PcmsClientHandle { - void* pointer; + void* couplerPointer; + void* appPointer; }; typedef struct PcmsClientHandle PcmsClientHandle; struct PcmsOmegaHMeshHandle @@ -63,11 +64,13 @@ void pcms_destroy_reverse_classification(PcmsReverseClassificationHandle); int pcms_reverse_classification_count_verts(PcmsReverseClassificationHandle); -typedef int8_t (*in_overlap_function)(int, int); PcmsFieldAdapterHandle pcms_create_xgc_field_adapter( const char* name, MPI_Comm plane_comm, void* data, int size, - PcmsType data_type, const PcmsReverseClassificationHandle rc, - in_overlap_function in_overlap); + PcmsType data_type, const PcmsReverseClassificationHandle rc); + +typedef int8_t (*in_overlap_function)(int, int); +void pcms_set_overlap_mask(PcmsClientHandle client_handle, const char* name, + in_overlap_function in_overlap); PcmsFieldAdapterHandle pcms_create_dummy_field_adapter(); diff --git a/src/pcms/fortranapi/client_wrap.c b/src/pcms/fortranapi/client_wrap.c index 5c65d3ef9..7162d3d0e 100644 --- a/src/pcms/fortranapi/client_wrap.c +++ b/src/pcms/fortranapi/client_wrap.c @@ -277,28 +277,61 @@ SWIGINTERN SwigArrayWrapper SwigArrayWrapper_uninitialized() return result; } -SWIGEXPORT void _wrap_PcmsClientHandle_pointer_set(SwigClassWrapper* farg1, - void const** farg2) +SWIGEXPORT void _wrap_PcmsClientHandle_couplerPointer_set( + SwigClassWrapper* farg1, void const** farg2) +{ + struct PcmsClientHandle* arg1 = (struct PcmsClientHandle*)0; + void* arg2 = (void*)0; + + SWIG_check_nonnull(farg1->cptr, "struct PcmsClientHandle *", + "PcmsClientHandle", "PcmsClientHandle::couplerPointer", + return); + arg1 = (struct PcmsClientHandle*)farg1->cptr; + arg2 = (void*)(*farg2); + if (arg1) + (arg1)->couplerPointer = arg2; +} + +SWIGEXPORT void* _wrap_PcmsClientHandle_couplerPointer_get( + SwigClassWrapper* farg1) +{ + void* fresult; + struct PcmsClientHandle* arg1 = (struct PcmsClientHandle*)0; + void* result = 0; + + SWIG_check_nonnull(farg1->cptr, "struct PcmsClientHandle *", + "PcmsClientHandle", "PcmsClientHandle::couplerPointer", + return 0); + arg1 = (struct PcmsClientHandle*)farg1->cptr; + result = (void*)((arg1)->couplerPointer); + fresult = (void*)(result); + return fresult; +} + +SWIGEXPORT void _wrap_PcmsClientHandle_appPointer_set(SwigClassWrapper* farg1, + void const** farg2) { struct PcmsClientHandle* arg1 = (struct PcmsClientHandle*)0; void* arg2 = (void*)0; SWIG_check_nonnull(farg1->cptr, "struct PcmsClientHandle *", - "PcmsClientHandle", "PcmsClientHandle::pointer", return); + "PcmsClientHandle", "PcmsClientHandle::appPointer", + return); arg1 = (struct PcmsClientHandle*)farg1->cptr; arg2 = (void*)(*farg2); if (arg1) (arg1)->appPointer = arg2; } -SWIGEXPORT void* _wrap_PcmsClientHandle_pointer_get(SwigClassWrapper* farg1) +SWIGEXPORT void* _wrap_PcmsClientHandle_appPointer_get(SwigClassWrapper* farg1) { void* fresult; struct PcmsClientHandle* arg1 = (struct PcmsClientHandle*)0; void* result = 0; SWIG_check_nonnull(farg1->cptr, "struct PcmsClientHandle *", - "PcmsClientHandle", "PcmsClientHandle::pointer", return 0); + "PcmsClientHandle", "PcmsClientHandle::appPointer", + return 0); arg1 = (struct PcmsClientHandle*)farg1->cptr; result = (void*)((arg1)->appPointer); fresult = (void*)(result); @@ -672,8 +705,7 @@ SWIGEXPORT int _wrap_pcms_reverse_classification_count_verts( SWIGEXPORT SwigClassWrapper _wrap_pcms_create_xgc_field_adapter( SwigArrayWrapper* farg1, int const* farg2, void const** farg3, - int const* farg4, int const* farg5, SwigClassWrapper* farg6, - in_overlap_function farg7) + int const* farg4, int const* farg5, SwigClassWrapper* farg6) { SwigClassWrapper fresult; char* arg1 = (char*)0; @@ -682,7 +714,6 @@ SWIGEXPORT SwigClassWrapper _wrap_pcms_create_xgc_field_adapter( int arg4; PcmsType arg5; PcmsReverseClassificationHandle arg6; - in_overlap_function arg7 = (in_overlap_function)0; PcmsFieldAdapterHandle result; arg1 = (char*)(farg1->data); @@ -694,16 +725,14 @@ SWIGEXPORT SwigClassWrapper _wrap_pcms_create_xgc_field_adapter( arg3 = (void*)(*farg3); arg4 = (int)(*farg4); arg5 = (PcmsType)(*farg5); - SWIG_check_nonnull( - farg6->cptr, "PcmsReverseClassificationHandle", - "PcmsReverseClassificationHandle", - "pcms_create_xgc_field_adapter(char const *,MPI_Comm,void " - "*,int,PcmsType,PcmsReverseClassificationHandle const,in_overlap_function)", - return SwigClassWrapper_uninitialized()); + SWIG_check_nonnull(farg6->cptr, "PcmsReverseClassificationHandle", + "PcmsReverseClassificationHandle", + "pcms_create_xgc_field_adapter(char const *,MPI_Comm,void " + "*,int,PcmsType,PcmsReverseClassificationHandle const)", + return SwigClassWrapper_uninitialized()); arg6 = *((PcmsReverseClassificationHandle*)(farg6->cptr)); - arg7 = (in_overlap_function)(*farg7); result = pcms_create_xgc_field_adapter((char const*)arg1, arg2, arg3, arg4, - arg5, arg6, arg7); + arg5, arg6); fresult.cptr = (PcmsFieldAdapterHandle*)memcpy( (PcmsFieldAdapterHandle*)calloc(1, sizeof(PcmsFieldAdapterHandle)), &result, sizeof(PcmsFieldAdapterHandle)); @@ -711,6 +740,24 @@ SWIGEXPORT SwigClassWrapper _wrap_pcms_create_xgc_field_adapter( return fresult; } +SWIGEXPORT void _wrap_pcms_set_overlap_mask(SwigClassWrapper* farg1, + SwigArrayWrapper* farg2, + in_overlap_function farg3) +{ + PcmsClientHandle arg1; + char* arg2 = (char*)0; + in_overlap_function arg3 = (in_overlap_function)0; + + SWIG_check_nonnull( + farg1->cptr, "PcmsClientHandle", "PcmsClientHandle", + "pcms_set_overlap_mask(PcmsClientHandle,char const *,in_overlap_function)", + return); + arg1 = *((PcmsClientHandle*)(farg1->cptr)); + arg2 = (char*)(farg2->data); + arg3 = (in_overlap_function)(*farg3); + pcms_set_overlap_mask(arg1, (char const*)arg2, arg3); +} + SWIGEXPORT SwigClassWrapper _wrap_pcms_create_dummy_field_adapter() { SwigClassWrapper fresult; diff --git a/src/pcms/fortranapi/pcms.f90 b/src/pcms/fortranapi/pcms.f90 index 8d431d805..ec4852708 100644 --- a/src/pcms/fortranapi/pcms.f90 +++ b/src/pcms/fortranapi/pcms.f90 @@ -39,8 +39,10 @@ module pcms type, public :: PcmsClientHandle type(SwigClassWrapper), public :: swigdata contains - procedure :: set_pointer => swigf_PcmsClientHandle_pointer_set - procedure :: get_pointer => swigf_PcmsClientHandle_pointer_get + procedure :: set_couplerPointer => swigf_PcmsClientHandle_couplerPointer_set + procedure :: get_couplerPointer => swigf_PcmsClientHandle_couplerPointer_get + procedure :: set_appPointer => swigf_PcmsClientHandle_appPointer_set + procedure :: get_appPointer => swigf_PcmsClientHandle_appPointer_get procedure :: release => swigf_PcmsClientHandle_release procedure, private :: swigf_PcmsClientHandle_op_assign__ generic :: assignment(=) => swigf_PcmsClientHandle_op_assign__ @@ -95,6 +97,7 @@ module pcms public :: pcms_destroy_reverse_classification public :: pcms_reverse_classification_count_verts public :: pcms_create_xgc_field_adapter + public :: pcms_set_overlap_mask public :: pcms_create_dummy_field_adapter public :: pcms_destroy_field_adapter public :: pcms_add_field @@ -140,205 +143,222 @@ function in_overlap_func(dimension, id) & ! WRAPPER DECLARATIONS interface - subroutine swigc_PcmsClientHandle_pointer_set(farg1, farg2) & - bind(C, name = "_wrap_PcmsClientHandle_pointer_set") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR), intent(in) :: farg2 - end subroutine - - function swigc_PcmsClientHandle_pointer_get(farg1) & - bind(C, name = "_wrap_PcmsClientHandle_pointer_get") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR) :: fresult - end function - - function swigc_new_PcmsClientHandle() & - bind(C, name = "_wrap_new_PcmsClientHandle") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper) :: fresult - end function +subroutine swigc_PcmsClientHandle_couplerPointer_set(farg1, farg2) & +bind(C, name="_wrap_PcmsClientHandle_couplerPointer_set") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR), intent(in) :: farg2 +end subroutine - subroutine swigc_delete_PcmsClientHandle(farg1) & - bind(C, name = "_wrap_delete_PcmsClientHandle") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - end subroutine +function swigc_PcmsClientHandle_couplerPointer_get(farg1) & +bind(C, name="_wrap_PcmsClientHandle_couplerPointer_get") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR) :: fresult +end function - subroutine swigc_PcmsClientHandle_op_assign__(farg1, farg2) & - bind(C, name = "_wrap_PcmsClientHandle_op_assign__") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - type(SwigClassWrapper), intent(in) :: farg2 - end subroutine +subroutine swigc_PcmsClientHandle_appPointer_set(farg1, farg2) & +bind(C, name="_wrap_PcmsClientHandle_appPointer_set") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR), intent(in) :: farg2 +end subroutine - subroutine swigc_PcmsOmegaHMeshHandle_pointer_set(farg1, farg2) & - bind(C, name = "_wrap_PcmsOmegaHMeshHandle_pointer_set") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR), intent(in) :: farg2 - end subroutine - - function swigc_PcmsOmegaHMeshHandle_pointer_get(farg1) & - bind(C, name = "_wrap_PcmsOmegaHMeshHandle_pointer_get") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR) :: fresult - end function - - function swigc_new_PcmsOmegaHMeshHandle() & - bind(C, name = "_wrap_new_PcmsOmegaHMeshHandle") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper) :: fresult - end function +function swigc_PcmsClientHandle_appPointer_get(farg1) & +bind(C, name="_wrap_PcmsClientHandle_appPointer_get") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR) :: fresult +end function - subroutine swigc_delete_PcmsOmegaHMeshHandle(farg1) & - bind(C, name = "_wrap_delete_PcmsOmegaHMeshHandle") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - end subroutine +function swigc_new_PcmsClientHandle() & +bind(C, name="_wrap_new_PcmsClientHandle") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper) :: fresult +end function - subroutine swigc_PcmsOmegaHMeshHandle_op_assign__(farg1, farg2) & - bind(C, name = "_wrap_PcmsOmegaHMeshHandle_op_assign__") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - type(SwigClassWrapper), intent(in) :: farg2 - end subroutine +subroutine swigc_delete_PcmsClientHandle(farg1) & +bind(C, name="_wrap_delete_PcmsClientHandle") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +end subroutine - subroutine swigc_PcmsReverseClassificationHandle_pointer_set(farg1, farg2) & - bind(C, name = "_wrap_PcmsReverseClassificationHandle_pointer_set") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR), intent(in) :: farg2 - end subroutine - - function swigc_PcmsReverseClassificationHandle_pointer_get(farg1) & - bind(C, name = "_wrap_PcmsReverseClassificationHandle_pointer_get") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR) :: fresult - end function - - function swigc_new_PcmsReverseClassificationHandle() & - bind(C, name = "_wrap_new_PcmsReverseClassificationHandle") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper) :: fresult - end function +subroutine swigc_PcmsClientHandle_op_assign__(farg1, farg2) & +bind(C, name="_wrap_PcmsClientHandle_op_assign__") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +type(SwigClassWrapper), intent(in) :: farg2 +end subroutine - subroutine swigc_delete_PcmsReverseClassificationHandle(farg1) & - bind(C, name = "_wrap_delete_PcmsReverseClassificationHandle") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - end subroutine +subroutine swigc_PcmsOmegaHMeshHandle_pointer_set(farg1, farg2) & +bind(C, name="_wrap_PcmsOmegaHMeshHandle_pointer_set") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR), intent(in) :: farg2 +end subroutine - subroutine swigc_PcmsReverseClassificationHandle_op_assign__(farg1, farg2) & - bind(C, name = "_wrap_PcmsReverseClassificationHandle_op_assign__") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - type(SwigClassWrapper), intent(in) :: farg2 - end subroutine +function swigc_PcmsOmegaHMeshHandle_pointer_get(farg1) & +bind(C, name="_wrap_PcmsOmegaHMeshHandle_pointer_get") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR) :: fresult +end function - subroutine swigc_PcmsFieldAdapterHandle_pointer_set(farg1, farg2) & - bind(C, name = "_wrap_PcmsFieldAdapterHandle_pointer_set") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR), intent(in) :: farg2 - end subroutine - - function swigc_PcmsFieldAdapterHandle_pointer_get(farg1) & - bind(C, name = "_wrap_PcmsFieldAdapterHandle_pointer_get") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR) :: fresult - end function - - function swigc_new_PcmsFieldAdapterHandle() & - bind(C, name = "_wrap_new_PcmsFieldAdapterHandle") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper) :: fresult - end function +function swigc_new_PcmsOmegaHMeshHandle() & +bind(C, name="_wrap_new_PcmsOmegaHMeshHandle") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper) :: fresult +end function - subroutine swigc_delete_PcmsFieldAdapterHandle(farg1) & - bind(C, name = "_wrap_delete_PcmsFieldAdapterHandle") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - end subroutine +subroutine swigc_delete_PcmsOmegaHMeshHandle(farg1) & +bind(C, name="_wrap_delete_PcmsOmegaHMeshHandle") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +end subroutine - subroutine swigc_PcmsFieldAdapterHandle_op_assign__(farg1, farg2) & - bind(C, name = "_wrap_PcmsFieldAdapterHandle_op_assign__") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - type(SwigClassWrapper), intent(in) :: farg2 - end subroutine +subroutine swigc_PcmsOmegaHMeshHandle_op_assign__(farg1, farg2) & +bind(C, name="_wrap_PcmsOmegaHMeshHandle_op_assign__") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +type(SwigClassWrapper), intent(in) :: farg2 +end subroutine - subroutine swigc_PcmsFieldHandle_pointer_set(farg1, farg2) & - bind(C, name = "_wrap_PcmsFieldHandle_pointer_set") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR), intent(in) :: farg2 - end subroutine - - function swigc_PcmsFieldHandle_pointer_get(farg1) & - bind(C, name = "_wrap_PcmsFieldHandle_pointer_get") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(in) :: farg1 - type(C_PTR) :: fresult - end function - - function swigc_new_PcmsFieldHandle() & - bind(C, name = "_wrap_new_PcmsFieldHandle") & - result(fresult) - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper) :: fresult - end function +subroutine swigc_PcmsReverseClassificationHandle_pointer_set(farg1, farg2) & +bind(C, name="_wrap_PcmsReverseClassificationHandle_pointer_set") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR), intent(in) :: farg2 +end subroutine - subroutine swigc_delete_PcmsFieldHandle(farg1) & - bind(C, name = "_wrap_delete_PcmsFieldHandle") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - end subroutine +function swigc_PcmsReverseClassificationHandle_pointer_get(farg1) & +bind(C, name="_wrap_PcmsReverseClassificationHandle_pointer_get") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR) :: fresult +end function - subroutine swigc_PcmsFieldHandle_op_assign__(farg1, farg2) & - bind(C, name = "_wrap_PcmsFieldHandle_op_assign__") - use, intrinsic :: ISO_C_BINDING - import :: swigclasswrapper - type(SwigClassWrapper), intent(inout) :: farg1 - type(SwigClassWrapper), intent(in) :: farg2 - end subroutine +function swigc_new_PcmsReverseClassificationHandle() & +bind(C, name="_wrap_new_PcmsReverseClassificationHandle") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper) :: fresult +end function + +subroutine swigc_delete_PcmsReverseClassificationHandle(farg1) & +bind(C, name="_wrap_delete_PcmsReverseClassificationHandle") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +end subroutine + +subroutine swigc_PcmsReverseClassificationHandle_op_assign__(farg1, farg2) & +bind(C, name="_wrap_PcmsReverseClassificationHandle_op_assign__") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +type(SwigClassWrapper), intent(in) :: farg2 +end subroutine + +subroutine swigc_PcmsFieldAdapterHandle_pointer_set(farg1, farg2) & +bind(C, name="_wrap_PcmsFieldAdapterHandle_pointer_set") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR), intent(in) :: farg2 +end subroutine + +function swigc_PcmsFieldAdapterHandle_pointer_get(farg1) & +bind(C, name="_wrap_PcmsFieldAdapterHandle_pointer_get") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR) :: fresult +end function + +function swigc_new_PcmsFieldAdapterHandle() & +bind(C, name="_wrap_new_PcmsFieldAdapterHandle") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper) :: fresult +end function + +subroutine swigc_delete_PcmsFieldAdapterHandle(farg1) & +bind(C, name="_wrap_delete_PcmsFieldAdapterHandle") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +end subroutine + +subroutine swigc_PcmsFieldAdapterHandle_op_assign__(farg1, farg2) & +bind(C, name="_wrap_PcmsFieldAdapterHandle_op_assign__") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +type(SwigClassWrapper), intent(in) :: farg2 +end subroutine + +subroutine swigc_PcmsFieldHandle_pointer_set(farg1, farg2) & +bind(C, name="_wrap_PcmsFieldHandle_pointer_set") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR), intent(in) :: farg2 +end subroutine + +function swigc_PcmsFieldHandle_pointer_get(farg1) & +bind(C, name="_wrap_PcmsFieldHandle_pointer_get") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(C_PTR) :: fresult +end function + +function swigc_new_PcmsFieldHandle() & +bind(C, name="_wrap_new_PcmsFieldHandle") & +result(fresult) +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper) :: fresult +end function + +subroutine swigc_delete_PcmsFieldHandle(farg1) & +bind(C, name="_wrap_delete_PcmsFieldHandle") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +end subroutine + +subroutine swigc_PcmsFieldHandle_op_assign__(farg1, farg2) & +bind(C, name="_wrap_PcmsFieldHandle_op_assign__") +use, intrinsic :: ISO_C_BINDING +import :: swigclasswrapper +type(SwigClassWrapper), intent(inout) :: farg1 +type(SwigClassWrapper), intent(in) :: farg2 +end subroutine function swigc_pcms_create_client(farg1, farg2) & bind(C, name="_wrap_pcms_create_client") & @@ -385,7 +405,7 @@ function swigc_pcms_reverse_classification_count_verts(farg1) & integer(C_INT) :: fresult end function -function swigc_pcms_create_xgc_field_adapter(farg1, farg2, farg3, farg4, farg5, farg6, farg7) & +function swigc_pcms_create_xgc_field_adapter(farg1, farg2, farg3, farg4, farg5, farg6) & bind(C, name="_wrap_pcms_create_xgc_field_adapter") & result(fresult) use, intrinsic :: ISO_C_BINDING @@ -397,10 +417,19 @@ function swigc_pcms_create_xgc_field_adapter(farg1, farg2, farg3, farg4, farg5, integer(C_INT), intent(in) :: farg4 integer(C_INT), intent(in) :: farg5 type(SwigClassWrapper), intent(in) :: farg6 -type(C_FUNPTR), value :: farg7 type(SwigClassWrapper) :: fresult end function +subroutine swigc_pcms_set_overlap_mask(farg1, farg2, farg3) & +bind(C, name="_wrap_pcms_set_overlap_mask") +use, intrinsic :: ISO_C_BINDING +import :: swigarraywrapper +import :: swigclasswrapper +type(SwigClassWrapper), intent(in) :: farg1 +type(SwigArrayWrapper) :: farg2 +type(C_FUNPTR), value :: farg3 +end subroutine + function swigc_pcms_create_dummy_field_adapter() & bind(C, name="_wrap_pcms_create_dummy_field_adapter") & result(fresult) @@ -504,315 +533,340 @@ subroutine swigc_pcms_kokkos_finalize() & contains ! MODULE SUBPROGRAMS - subroutine swigf_PcmsClientHandle_pointer_set(self, pointer) - use, intrinsic :: ISO_C_BINDING - class(PcmsClientHandle), intent(in) :: self - type(C_PTR), intent(in) :: pointer - type(SwigClassWrapper) :: farg1 - type(C_PTR) :: farg2 - - farg1 = self%swigdata - farg2 = pointer - call swigc_PcmsClientHandle_pointer_set(farg1, farg2) - end subroutine - - function swigf_PcmsClientHandle_pointer_get(self) & - result(swig_result) - use, intrinsic :: ISO_C_BINDING - type(C_PTR) :: swig_result - class(PcmsClientHandle), intent(in) :: self - type(C_PTR) :: fresult - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - fresult = swigc_PcmsClientHandle_pointer_get(farg1) - swig_result = fresult - end function - - function swigf_new_PcmsClientHandle() & - result(self) - use, intrinsic :: ISO_C_BINDING - type(PcmsClientHandle) :: self - type(SwigClassWrapper) :: fresult +subroutine swigf_PcmsClientHandle_couplerPointer_set(self, couplerpointer) +use, intrinsic :: ISO_C_BINDING +class(PcmsClientHandle), intent(in) :: self +type(C_PTR), intent(in) :: couplerpointer +type(SwigClassWrapper) :: farg1 +type(C_PTR) :: farg2 - fresult = swigc_new_PcmsClientHandle() - self%swigdata = fresult - end function +farg1 = self%swigdata +farg2 = couplerpointer +call swigc_PcmsClientHandle_couplerPointer_set(farg1, farg2) +end subroutine - subroutine swigf_PcmsClientHandle_release(self) - use, intrinsic :: ISO_C_BINDING - class(PcmsClientHandle), intent(inout) :: self - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - if (btest(farg1%cmemflags, swig_cmem_own_bit)) then - call swigc_delete_PcmsClientHandle(farg1) - endif - farg1%cptr = C_NULL_PTR - farg1%cmemflags = 0 - self%swigdata = farg1 - end subroutine - - subroutine swigf_PcmsClientHandle_op_assign__(self, other) - use, intrinsic :: ISO_C_BINDING - class(PcmsClientHandle), intent(inout) :: self - type(PcmsClientHandle), intent(in) :: other - type(SwigClassWrapper) :: farg1 - type(SwigClassWrapper) :: farg2 - - farg1 = self%swigdata - farg2 = other%swigdata - call swigc_PcmsClientHandle_op_assign__(farg1, farg2) - self%swigdata = farg1 - end subroutine - - subroutine swigf_PcmsOmegaHMeshHandle_pointer_set(self, pointer) - use, intrinsic :: ISO_C_BINDING - class(PcmsOmegaHMeshHandle), intent(in) :: self - type(C_PTR), intent(in) :: pointer - type(SwigClassWrapper) :: farg1 - type(C_PTR) :: farg2 - - farg1 = self%swigdata - farg2 = pointer - call swigc_PcmsOmegaHMeshHandle_pointer_set(farg1, farg2) - end subroutine - - function swigf_PcmsOmegaHMeshHandle_pointer_get(self) & - result(swig_result) - use, intrinsic :: ISO_C_BINDING - type(C_PTR) :: swig_result - class(PcmsOmegaHMeshHandle), intent(in) :: self - type(C_PTR) :: fresult - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - fresult = swigc_PcmsOmegaHMeshHandle_pointer_get(farg1) - swig_result = fresult - end function - - function swigf_new_PcmsOmegaHMeshHandle() & - result(self) - use, intrinsic :: ISO_C_BINDING - type(PcmsOmegaHMeshHandle) :: self - type(SwigClassWrapper) :: fresult +function swigf_PcmsClientHandle_couplerPointer_get(self) & +result(swig_result) +use, intrinsic :: ISO_C_BINDING +type(C_PTR) :: swig_result +class(PcmsClientHandle), intent(in) :: self +type(C_PTR) :: fresult +type(SwigClassWrapper) :: farg1 - fresult = swigc_new_PcmsOmegaHMeshHandle() - self%swigdata = fresult - end function +farg1 = self%swigdata +fresult = swigc_PcmsClientHandle_couplerPointer_get(farg1) +swig_result = fresult +end function - subroutine swigf_PcmsOmegaHMeshHandle_release(self) - use, intrinsic :: ISO_C_BINDING - class(PcmsOmegaHMeshHandle), intent(inout) :: self - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - if (btest(farg1%cmemflags, swig_cmem_own_bit)) then - call swigc_delete_PcmsOmegaHMeshHandle(farg1) - endif - farg1%cptr = C_NULL_PTR - farg1%cmemflags = 0 - self%swigdata = farg1 - end subroutine - - subroutine swigf_PcmsOmegaHMeshHandle_op_assign__(self, other) - use, intrinsic :: ISO_C_BINDING - class(PcmsOmegaHMeshHandle), intent(inout) :: self - type(PcmsOmegaHMeshHandle), intent(in) :: other - type(SwigClassWrapper) :: farg1 - type(SwigClassWrapper) :: farg2 - - farg1 = self%swigdata - farg2 = other%swigdata - call swigc_PcmsOmegaHMeshHandle_op_assign__(farg1, farg2) - self%swigdata = farg1 - end subroutine - - subroutine swigf_PcmsReverseClassificationHandle_pointer_set(self, pointer) - use, intrinsic :: ISO_C_BINDING - class(PcmsReverseClassificationHandle), intent(in) :: self - type(C_PTR), intent(in) :: pointer - type(SwigClassWrapper) :: farg1 - type(C_PTR) :: farg2 - - farg1 = self%swigdata - farg2 = pointer - call swigc_PcmsReverseClassificationHandle_pointer_set(farg1, farg2) - end subroutine - - function swigf_PcmsReverseClassificationHandle_pointer_get(self) & - result(swig_result) - use, intrinsic :: ISO_C_BINDING - type(C_PTR) :: swig_result - class(PcmsReverseClassificationHandle), intent(in) :: self - type(C_PTR) :: fresult - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - fresult = swigc_PcmsReverseClassificationHandle_pointer_get(farg1) - swig_result = fresult - end function - - function swigf_new_PcmsReverseClassificationHandle() & - result(self) - use, intrinsic :: ISO_C_BINDING - type(PcmsReverseClassificationHandle) :: self - type(SwigClassWrapper) :: fresult +subroutine swigf_PcmsClientHandle_appPointer_set(self, apppointer) +use, intrinsic :: ISO_C_BINDING +class(PcmsClientHandle), intent(in) :: self +type(C_PTR), intent(in) :: apppointer +type(SwigClassWrapper) :: farg1 +type(C_PTR) :: farg2 - fresult = swigc_new_PcmsReverseClassificationHandle() - self%swigdata = fresult - end function +farg1 = self%swigdata +farg2 = apppointer +call swigc_PcmsClientHandle_appPointer_set(farg1, farg2) +end subroutine - subroutine swigf_PcmsReverseClassificationHandle_release(self) - use, intrinsic :: ISO_C_BINDING - class(PcmsReverseClassificationHandle), intent(inout) :: self - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - if (btest(farg1%cmemflags, swig_cmem_own_bit)) then - call swigc_delete_PcmsReverseClassificationHandle(farg1) - endif - farg1%cptr = C_NULL_PTR - farg1%cmemflags = 0 - self%swigdata = farg1 - end subroutine - - subroutine swigf_PcmsReverseClassificationHandle_op_assign__(self, other) - use, intrinsic :: ISO_C_BINDING - class(PcmsReverseClassificationHandle), intent(inout) :: self - type(PcmsReverseClassificationHandle), intent(in) :: other - type(SwigClassWrapper) :: farg1 - type(SwigClassWrapper) :: farg2 - - farg1 = self%swigdata - farg2 = other%swigdata - call swigc_PcmsReverseClassificationHandle_op_assign__(farg1, farg2) - self%swigdata = farg1 - end subroutine - - subroutine swigf_PcmsFieldAdapterHandle_pointer_set(self, pointer) - use, intrinsic :: ISO_C_BINDING - class(PcmsFieldAdapterHandle), intent(in) :: self - type(C_PTR), intent(in) :: pointer - type(SwigClassWrapper) :: farg1 - type(C_PTR) :: farg2 - - farg1 = self%swigdata - farg2 = pointer - call swigc_PcmsFieldAdapterHandle_pointer_set(farg1, farg2) - end subroutine - - function swigf_PcmsFieldAdapterHandle_pointer_get(self) & - result(swig_result) - use, intrinsic :: ISO_C_BINDING - type(C_PTR) :: swig_result - class(PcmsFieldAdapterHandle), intent(in) :: self - type(C_PTR) :: fresult - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - fresult = swigc_PcmsFieldAdapterHandle_pointer_get(farg1) - swig_result = fresult - end function - - function swigf_new_PcmsFieldAdapterHandle() & - result(self) - use, intrinsic :: ISO_C_BINDING - type(PcmsFieldAdapterHandle) :: self - type(SwigClassWrapper) :: fresult +function swigf_PcmsClientHandle_appPointer_get(self) & +result(swig_result) +use, intrinsic :: ISO_C_BINDING +type(C_PTR) :: swig_result +class(PcmsClientHandle), intent(in) :: self +type(C_PTR) :: fresult +type(SwigClassWrapper) :: farg1 - fresult = swigc_new_PcmsFieldAdapterHandle() - self%swigdata = fresult - end function +farg1 = self%swigdata +fresult = swigc_PcmsClientHandle_appPointer_get(farg1) +swig_result = fresult +end function - subroutine swigf_PcmsFieldAdapterHandle_release(self) - use, intrinsic :: ISO_C_BINDING - class(PcmsFieldAdapterHandle), intent(inout) :: self - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - if (btest(farg1%cmemflags, swig_cmem_own_bit)) then - call swigc_delete_PcmsFieldAdapterHandle(farg1) - endif - farg1%cptr = C_NULL_PTR - farg1%cmemflags = 0 - self%swigdata = farg1 - end subroutine - - subroutine swigf_PcmsFieldAdapterHandle_op_assign__(self, other) - use, intrinsic :: ISO_C_BINDING - class(PcmsFieldAdapterHandle), intent(inout) :: self - type(PcmsFieldAdapterHandle), intent(in) :: other - type(SwigClassWrapper) :: farg1 - type(SwigClassWrapper) :: farg2 - - farg1 = self%swigdata - farg2 = other%swigdata - call swigc_PcmsFieldAdapterHandle_op_assign__(farg1, farg2) - self%swigdata = farg1 - end subroutine - - subroutine swigf_PcmsFieldHandle_pointer_set(self, pointer) - use, intrinsic :: ISO_C_BINDING - class(PcmsFieldHandle), intent(in) :: self - type(C_PTR), intent(in) :: pointer - type(SwigClassWrapper) :: farg1 - type(C_PTR) :: farg2 - - farg1 = self%swigdata - farg2 = pointer - call swigc_PcmsFieldHandle_pointer_set(farg1, farg2) - end subroutine - - function swigf_PcmsFieldHandle_pointer_get(self) & - result(swig_result) - use, intrinsic :: ISO_C_BINDING - type(C_PTR) :: swig_result - class(PcmsFieldHandle), intent(in) :: self - type(C_PTR) :: fresult - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - fresult = swigc_PcmsFieldHandle_pointer_get(farg1) - swig_result = fresult - end function - - function swigf_new_PcmsFieldHandle() & - result(self) - use, intrinsic :: ISO_C_BINDING - type(PcmsFieldHandle) :: self - type(SwigClassWrapper) :: fresult +function swigf_new_PcmsClientHandle() & +result(self) +use, intrinsic :: ISO_C_BINDING +type(PcmsClientHandle) :: self +type(SwigClassWrapper) :: fresult - fresult = swigc_new_PcmsFieldHandle() - self%swigdata = fresult - end function +fresult = swigc_new_PcmsClientHandle() +self%swigdata = fresult +end function - subroutine swigf_PcmsFieldHandle_release(self) - use, intrinsic :: ISO_C_BINDING - class(PcmsFieldHandle), intent(inout) :: self - type(SwigClassWrapper) :: farg1 - - farg1 = self%swigdata - if (btest(farg1%cmemflags, swig_cmem_own_bit)) then - call swigc_delete_PcmsFieldHandle(farg1) - endif - farg1%cptr = C_NULL_PTR - farg1%cmemflags = 0 - self%swigdata = farg1 - end subroutine - - subroutine swigf_PcmsFieldHandle_op_assign__(self, other) - use, intrinsic :: ISO_C_BINDING - class(PcmsFieldHandle), intent(inout) :: self - type(PcmsFieldHandle), intent(in) :: other - type(SwigClassWrapper) :: farg1 - type(SwigClassWrapper) :: farg2 +subroutine swigf_PcmsClientHandle_release(self) +use, intrinsic :: ISO_C_BINDING +class(PcmsClientHandle), intent(inout) :: self +type(SwigClassWrapper) :: farg1 + +farg1 = self%swigdata +if (btest(farg1%cmemflags, swig_cmem_own_bit)) then +call swigc_delete_PcmsClientHandle(farg1) +endif +farg1%cptr = C_NULL_PTR +farg1%cmemflags = 0 +self%swigdata = farg1 +end subroutine + +subroutine swigf_PcmsClientHandle_op_assign__(self, other) +use, intrinsic :: ISO_C_BINDING +class(PcmsClientHandle), intent(inout) :: self +type(PcmsClientHandle), intent(in) :: other +type(SwigClassWrapper) :: farg1 +type(SwigClassWrapper) :: farg2 - farg1 = self%swigdata - farg2 = other%swigdata - call swigc_PcmsFieldHandle_op_assign__(farg1, farg2) - self%swigdata = farg1 - end subroutine +farg1 = self%swigdata +farg2 = other%swigdata +call swigc_PcmsClientHandle_op_assign__(farg1, farg2) +self%swigdata = farg1 +end subroutine + +subroutine swigf_PcmsOmegaHMeshHandle_pointer_set(self, pointer) +use, intrinsic :: ISO_C_BINDING +class(PcmsOmegaHMeshHandle), intent(in) :: self +type(C_PTR), intent(in) :: pointer +type(SwigClassWrapper) :: farg1 +type(C_PTR) :: farg2 + +farg1 = self%swigdata +farg2 = pointer +call swigc_PcmsOmegaHMeshHandle_pointer_set(farg1, farg2) +end subroutine + +function swigf_PcmsOmegaHMeshHandle_pointer_get(self) & +result(swig_result) +use, intrinsic :: ISO_C_BINDING +type(C_PTR) :: swig_result +class(PcmsOmegaHMeshHandle), intent(in) :: self +type(C_PTR) :: fresult +type(SwigClassWrapper) :: farg1 + +farg1 = self%swigdata +fresult = swigc_PcmsOmegaHMeshHandle_pointer_get(farg1) +swig_result = fresult +end function + +function swigf_new_PcmsOmegaHMeshHandle() & +result(self) +use, intrinsic :: ISO_C_BINDING +type(PcmsOmegaHMeshHandle) :: self +type(SwigClassWrapper) :: fresult + +fresult = swigc_new_PcmsOmegaHMeshHandle() +self%swigdata = fresult +end function + +subroutine swigf_PcmsOmegaHMeshHandle_release(self) +use, intrinsic :: ISO_C_BINDING +class(PcmsOmegaHMeshHandle), intent(inout) :: self +type(SwigClassWrapper) :: farg1 + +farg1 = self%swigdata +if (btest(farg1%cmemflags, swig_cmem_own_bit)) then +call swigc_delete_PcmsOmegaHMeshHandle(farg1) +endif +farg1%cptr = C_NULL_PTR +farg1%cmemflags = 0 +self%swigdata = farg1 +end subroutine + +subroutine swigf_PcmsOmegaHMeshHandle_op_assign__(self, other) +use, intrinsic :: ISO_C_BINDING +class(PcmsOmegaHMeshHandle), intent(inout) :: self +type(PcmsOmegaHMeshHandle), intent(in) :: other +type(SwigClassWrapper) :: farg1 +type(SwigClassWrapper) :: farg2 + +farg1 = self%swigdata +farg2 = other%swigdata +call swigc_PcmsOmegaHMeshHandle_op_assign__(farg1, farg2) +self%swigdata = farg1 +end subroutine + +subroutine swigf_PcmsReverseClassificationHandle_pointer_set(self, pointer) +use, intrinsic :: ISO_C_BINDING +class(PcmsReverseClassificationHandle), intent(in) :: self +type(C_PTR), intent(in) :: pointer +type(SwigClassWrapper) :: farg1 +type(C_PTR) :: farg2 + +farg1 = self%swigdata +farg2 = pointer +call swigc_PcmsReverseClassificationHandle_pointer_set(farg1, farg2) +end subroutine + +function swigf_PcmsReverseClassificationHandle_pointer_get(self) & +result(swig_result) +use, intrinsic :: ISO_C_BINDING +type(C_PTR) :: swig_result +class(PcmsReverseClassificationHandle), intent(in) :: self +type(C_PTR) :: fresult +type(SwigClassWrapper) :: farg1 + +farg1 = self%swigdata +fresult = swigc_PcmsReverseClassificationHandle_pointer_get(farg1) +swig_result = fresult +end function + +function swigf_new_PcmsReverseClassificationHandle() & +result(self) +use, intrinsic :: ISO_C_BINDING +type(PcmsReverseClassificationHandle) :: self +type(SwigClassWrapper) :: fresult + +fresult = swigc_new_PcmsReverseClassificationHandle() +self%swigdata = fresult +end function + +subroutine swigf_PcmsReverseClassificationHandle_release(self) +use, intrinsic :: ISO_C_BINDING +class(PcmsReverseClassificationHandle), intent(inout) :: self +type(SwigClassWrapper) :: farg1 + +farg1 = self%swigdata +if (btest(farg1%cmemflags, swig_cmem_own_bit)) then +call swigc_delete_PcmsReverseClassificationHandle(farg1) +endif +farg1%cptr = C_NULL_PTR +farg1%cmemflags = 0 +self%swigdata = farg1 +end subroutine + +subroutine swigf_PcmsReverseClassificationHandle_op_assign__(self, other) +use, intrinsic :: ISO_C_BINDING +class(PcmsReverseClassificationHandle), intent(inout) :: self +type(PcmsReverseClassificationHandle), intent(in) :: other +type(SwigClassWrapper) :: farg1 +type(SwigClassWrapper) :: farg2 + +farg1 = self%swigdata +farg2 = other%swigdata +call swigc_PcmsReverseClassificationHandle_op_assign__(farg1, farg2) +self%swigdata = farg1 +end subroutine + +subroutine swigf_PcmsFieldAdapterHandle_pointer_set(self, pointer) +use, intrinsic :: ISO_C_BINDING +class(PcmsFieldAdapterHandle), intent(in) :: self +type(C_PTR), intent(in) :: pointer +type(SwigClassWrapper) :: farg1 +type(C_PTR) :: farg2 + +farg1 = self%swigdata +farg2 = pointer +call swigc_PcmsFieldAdapterHandle_pointer_set(farg1, farg2) +end subroutine + +function swigf_PcmsFieldAdapterHandle_pointer_get(self) & +result(swig_result) +use, intrinsic :: ISO_C_BINDING +type(C_PTR) :: swig_result +class(PcmsFieldAdapterHandle), intent(in) :: self +type(C_PTR) :: fresult +type(SwigClassWrapper) :: farg1 + +farg1 = self%swigdata +fresult = swigc_PcmsFieldAdapterHandle_pointer_get(farg1) +swig_result = fresult +end function + +function swigf_new_PcmsFieldAdapterHandle() & +result(self) +use, intrinsic :: ISO_C_BINDING +type(PcmsFieldAdapterHandle) :: self +type(SwigClassWrapper) :: fresult + +fresult = swigc_new_PcmsFieldAdapterHandle() +self%swigdata = fresult +end function + +subroutine swigf_PcmsFieldAdapterHandle_release(self) +use, intrinsic :: ISO_C_BINDING +class(PcmsFieldAdapterHandle), intent(inout) :: self +type(SwigClassWrapper) :: farg1 + +farg1 = self%swigdata +if (btest(farg1%cmemflags, swig_cmem_own_bit)) then +call swigc_delete_PcmsFieldAdapterHandle(farg1) +endif +farg1%cptr = C_NULL_PTR +farg1%cmemflags = 0 +self%swigdata = farg1 +end subroutine + +subroutine swigf_PcmsFieldAdapterHandle_op_assign__(self, other) +use, intrinsic :: ISO_C_BINDING +class(PcmsFieldAdapterHandle), intent(inout) :: self +type(PcmsFieldAdapterHandle), intent(in) :: other +type(SwigClassWrapper) :: farg1 +type(SwigClassWrapper) :: farg2 + +farg1 = self%swigdata +farg2 = other%swigdata +call swigc_PcmsFieldAdapterHandle_op_assign__(farg1, farg2) +self%swigdata = farg1 +end subroutine + +subroutine swigf_PcmsFieldHandle_pointer_set(self, pointer) +use, intrinsic :: ISO_C_BINDING +class(PcmsFieldHandle), intent(in) :: self +type(C_PTR), intent(in) :: pointer +type(SwigClassWrapper) :: farg1 +type(C_PTR) :: farg2 + +farg1 = self%swigdata +farg2 = pointer +call swigc_PcmsFieldHandle_pointer_set(farg1, farg2) +end subroutine + +function swigf_PcmsFieldHandle_pointer_get(self) & +result(swig_result) +use, intrinsic :: ISO_C_BINDING +type(C_PTR) :: swig_result +class(PcmsFieldHandle), intent(in) :: self +type(C_PTR) :: fresult +type(SwigClassWrapper) :: farg1 + +farg1 = self%swigdata +fresult = swigc_PcmsFieldHandle_pointer_get(farg1) +swig_result = fresult +end function + +function swigf_new_PcmsFieldHandle() & +result(self) +use, intrinsic :: ISO_C_BINDING +type(PcmsFieldHandle) :: self +type(SwigClassWrapper) :: fresult + +fresult = swigc_new_PcmsFieldHandle() +self%swigdata = fresult +end function + +subroutine swigf_PcmsFieldHandle_release(self) +use, intrinsic :: ISO_C_BINDING +class(PcmsFieldHandle), intent(inout) :: self +type(SwigClassWrapper) :: farg1 + +farg1 = self%swigdata +if (btest(farg1%cmemflags, swig_cmem_own_bit)) then +call swigc_delete_PcmsFieldHandle(farg1) +endif +farg1%cptr = C_NULL_PTR +farg1%cmemflags = 0 +self%swigdata = farg1 +end subroutine + +subroutine swigf_PcmsFieldHandle_op_assign__(self, other) +use, intrinsic :: ISO_C_BINDING +class(PcmsFieldHandle), intent(inout) :: self +type(PcmsFieldHandle), intent(in) :: other +type(SwigClassWrapper) :: farg1 +type(SwigClassWrapper) :: farg2 + +farg1 = self%swigdata +farg2 = other%swigdata +call swigc_PcmsFieldHandle_op_assign__(farg1, farg2) +self%swigdata = farg1 +end subroutine subroutine SWIGTM_fin_char_Sm_(finp, iminp, temp) @@ -897,7 +951,7 @@ function pcms_reverse_classification_count_verts(arg0) & swig_result = fresult end function -function pcms_create_xgc_field_adapter(name, plane_comm, data, size, data_type, rc, in_overlap) & +function pcms_create_xgc_field_adapter(name, plane_comm, data, size, data_type, rc) & result(swig_result) use, intrinsic :: ISO_C_BINDING type(PcmsFieldAdapterHandle) :: swig_result @@ -907,7 +961,6 @@ function pcms_create_xgc_field_adapter(name, plane_comm, data, size, data_type, integer(C_INT), intent(in) :: size integer(PcmsType), intent(in) :: data_type type(PcmsReverseClassificationHandle), intent(in) :: rc -procedure(in_overlap_func) :: in_overlap type(SwigClassWrapper) :: fresult character(kind=C_CHAR), dimension(:), allocatable, target :: farg1_temp type(SwigArrayWrapper) :: farg1 @@ -916,7 +969,6 @@ function pcms_create_xgc_field_adapter(name, plane_comm, data, size, data_type, integer(C_INT) :: farg4 integer(C_INT) :: farg5 type(SwigClassWrapper) :: farg6 -type(C_FUNPTR) :: farg7 call SWIGTM_fin_char_Sm_(name, farg1, farg1_temp) farg2 = int(plane_comm, C_INT) @@ -924,11 +976,26 @@ function pcms_create_xgc_field_adapter(name, plane_comm, data, size, data_type, farg4 = size farg5 = data_type farg6 = rc%swigdata -farg7 = c_funloc(in_overlap) -fresult = swigc_pcms_create_xgc_field_adapter(farg1, farg2, farg3, farg4, farg5, farg6, farg7) +fresult = swigc_pcms_create_xgc_field_adapter(farg1, farg2, farg3, farg4, farg5, farg6) swig_result%swigdata = fresult end function +subroutine pcms_set_overlap_mask(client_handle, name, in_overlap) +use, intrinsic :: ISO_C_BINDING +type(PcmsClientHandle), intent(in) :: client_handle +character(len=*), target :: name +procedure(in_overlap_func) :: in_overlap +type(SwigClassWrapper) :: farg1 +character(kind=C_CHAR), dimension(:), allocatable, target :: farg2_temp +type(SwigArrayWrapper) :: farg2 +type(C_FUNPTR) :: farg3 + +farg1 = client_handle%swigdata +call SWIGTM_fin_char_Sm_(name, farg2, farg2_temp) +farg3 = c_funloc(in_overlap) +call swigc_pcms_set_overlap_mask(farg1, farg2, farg3) +end subroutine + function pcms_create_dummy_field_adapter() & result(swig_result) use, intrinsic :: ISO_C_BINDING diff --git a/src/pcms/localization/CMakeLists.txt b/src/pcms/localization/CMakeLists.txt index 269b1b776..e8f386ae2 100644 --- a/src/pcms/localization/CMakeLists.txt +++ b/src/pcms/localization/CMakeLists.txt @@ -1,21 +1,29 @@ set( PCMS_LOCALIZATION_HEADERS - point_search.h + localization.h queue_visited.hpp adj_search.hpp mls_support_helpers.h localization_factory.h point_cloud_localization.h mesh_localization.h + uniform_grid_localization.hpp ) set( PCMS_LOCALIZATION_SOURCES - point_search.cpp + localization.cpp adj_search.cpp mls_support_helpers.cpp mesh_localization.cpp + uniform_grid_localization.cpp ) + +if(PCMS_ENABLE_ARBORX) + list(APPEND PCMS_LOCALIZATION_HEADERS arbor_x_localization.hpp) + list(APPEND PCMS_LOCALIZATION_SOURCES arbor_x_localization.cpp) +endif() + add_library(pcms_localization ${PCMS_LOCALIZATION_SOURCES}) add_library(pcms::localization ALIAS pcms_localization) target_include_directories( @@ -33,6 +41,11 @@ target_link_libraries( pcms::discretization Omega_h::omega_h ) + +if (PCMS_ENABLE_ARBORX) + target_link_libraries(pcms_localization PUBLIC ArborX::ArborX) +endif() + target_compile_features(pcms_localization PUBLIC cxx_std_20) set_target_properties( diff --git a/src/pcms/localization/adj_search.cpp b/src/pcms/localization/adj_search.cpp index b15925d71..21c3e1cde 100644 --- a/src/pcms/localization/adj_search.cpp +++ b/src/pcms/localization/adj_search.cpp @@ -7,7 +7,7 @@ namespace pcms // Debug helper retained intentionally: useful for diagnosing point-localization // failures while developing support-search logic. [[maybe_unused]] static void checkTargetPoints( - const Kokkos::View& results, + const GridPointSearch2D::Results& results, const Kokkos::View& owning_cell_ids) { Kokkos::fence(); @@ -22,7 +22,7 @@ namespace pcms printf("%d, ", i); } }; - Omega_h::parallel_for(results.size(), check_target_points, + Omega_h::parallel_for(results.dimensionalities.size(), check_target_points, "check_target_points"); Kokkos::fence(); pcms::printInfo("\n"); @@ -81,8 +81,8 @@ static Omega_h::Write locate_target_cells( Omega_h::Write(nvertices_target, -1, "source cell ids"); if (dim == 2) { - Kokkos::View target_points("target_points", - nvertices_target); + Kokkos::View target_points("target_points", nvertices_target, + 2); Omega_h::parallel_for( nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO i) { target_points(i, 0) = target_coords[i * dim]; @@ -90,8 +90,10 @@ static Omega_h::Write locate_target_cells( }); Kokkos::fence(); - pcms::GridPointSearch2D search_cell(source_mesh, 10, 10); - auto results = search_cell(target_points); + pcms::GridPointSearch2D search_cell(source_mesh, 10, 10); // + auto results = search_cell.Apply( + pcms::CoordinateView(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(target_points))); auto owning_cell_ids = search_cell.GetOwningElementIds(results); Omega_h::parallel_for( nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO i) { @@ -103,8 +105,8 @@ static Omega_h::Write locate_target_cells( source_cell_ids[i] = source_cell_id; }); } else if (dim == 3) { - Kokkos::View target_points("target_points", - nvertices_target); + Kokkos::View target_points("target_points", nvertices_target, + 3); Omega_h::parallel_for( nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO i) { target_points(i, 0) = target_coords[i * dim]; @@ -114,7 +116,9 @@ static Omega_h::Write locate_target_cells( Kokkos::fence(); pcms::GridPointSearch3D search_cell(source_mesh, 10, 10, 10); - auto results = search_cell(target_points); + auto results = search_cell.Apply( + pcms::CoordinateView(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(target_points))); auto owning_cell_ids = search_cell.GetOwningElementIds(results); Omega_h::parallel_for( nvertices_target, OMEGA_H_LAMBDA(const Omega_h::LO i) { diff --git a/src/pcms/localization/adj_search.hpp b/src/pcms/localization/adj_search.hpp index c27b11be5..26aadc75a 100644 --- a/src/pcms/localization/adj_search.hpp +++ b/src/pcms/localization/adj_search.hpp @@ -1,7 +1,7 @@ #ifndef PCMS_LOCALIZATION_ADJ_SEARCH_HPP #define PCMS_LOCALIZATION_ADJ_SEARCH_HPP -#include +#include #include #include #include @@ -22,9 +22,10 @@ class FindSupports public: FindSupports(Omega_h::Mesh& source_mesh_, Omega_h::Mesh& target_mesh_) - : source_mesh(source_mesh_), target_mesh(target_mesh_){}; + : source_mesh(source_mesh_), target_mesh(target_mesh_) {}; - FindSupports(Omega_h::Mesh& mesh_) : source_mesh(mesh_), target_mesh(mesh_){}; + FindSupports(Omega_h::Mesh& mesh_) + : source_mesh(mesh_), target_mesh(mesh_) {}; void adjBasedSearch(Omega_h::Write& supports_ptr, Omega_h::Write& nSupports, diff --git a/src/pcms/localization/arbor_x_localization.cpp b/src/pcms/localization/arbor_x_localization.cpp new file mode 100644 index 000000000..2769cc2cf --- /dev/null +++ b/src/pcms/localization/arbor_x_localization.cpp @@ -0,0 +1,824 @@ +#include "arbor_x_localization.hpp" + +// Access traits for Omega_h +// constructs bounding boxes for elements + +namespace pcms +{ +namespace detail +{ +/** + * @brief Implementation of the Kronecker delta: + * https://en.wikipedia.org/wiki/Kronecker_delta + * @param i an integer + * @param j another integer + * @returns 1 if i==j, 0 if i i!= j + */ +KOKKOS_INLINE_FUNCTION +LO kronecker(LO i, LO j) +{ + return (LO)(i == j); +} + +/** + * @brief Omega_h::Mesh tagged with the templated dimension required for + * compatability with ArborX::BVH + */ +template +struct Omega_h_Mesh_Adapt +{ + const Omega_h::LO size_; + const Omega_h::LOs adjacency; + const Omega_h::Reals coordinates; +}; + +/** + * @brief pcms CoordinateView tagged with the templated dimension and + * memory space required for compatability with ArborX::BVH + */ +template +struct Coordinate_View_Adapt +{ + const Rank2View> + points; +}; + +/** + * @brief Mapping<2> represents a mapping from the global coordinate system + * to the barycentric coordinate system of an element in a 2D + *Omega_h::Mesh The class Mapping<2> calculates the mapping of any point in + *global coordinate space to the barycentric coordinate space belonging to a + *triangle (face) in an Omega_h::Mesh. An instance of Mapping<2> can, from the + *barycentric coordinates constructed by it, determine whether that point + *intersects the face, an edge, a vertex, or lies outside the face. It also + *determines which edge or vertex the point intersects by calculating the offset + *in the Omega_h::Adj::ab2b + */ +template <> +class Mapping<2> +{ +public: + // the dimension of the space + static constexpr int DIM = 2; + // CONSTRUCTORS + /** + * @brief Default constructor + */ + KOKKOS_FUNCTION + Mapping() = default; + /** + * @brief Constructs a barycentric mapping of a triangle in an Omega_h::Mesh + * @param elem_index the index of the desired triangle or tetrahedron in the + * Omega_h::Mesh + * @param mesh the Omega_h::Mesh with the spatial information of the triangle + */ + Mapping(Omega_h::Matrix const& triangle_, + Kokkos::View const& tolerances) + : triangle(triangle_) + { + auto tol_h = Kokkos::create_mirror_view(tolerances); + Kokkos::deep_copy(tol_h, tolerances); + tolerances_[0] = tol_h(0); + tolerances_[1] = tol_h(1); + bary_transform = {triangle[0] - triangle[2], triangle[1] - triangle[2]}; + triangle_area = 0.5 * fabs(Omega_h::determinant(bary_transform)); + bary_transform = Omega_h::invert(bary_transform); + } + /** + * @brief Default destructor + */ + KOKKOS_FUNCTION + ~Mapping() = default; + /** + * @brief Computes the barycentric coordinates of a point in global space + * + * This function computes the barycentric coordinate of a point with respect + * to a triangle in an Omega_h::Mesh + * + * @param p the point to compute the barycentric coordinates of + * @returns an Omega_h::Vector<3> containing the barycentric coordinates + * of p + */ + KOKKOS_FUNCTION + Omega_h::Vector get_bary(Omega_h::Vector const& p) const + { + Omega_h::Vector<2> coeffs = bary_transform * (p - triangle[2]); + return {coeffs[0], coeffs[1], 1 - coeffs[0] - coeffs[1]}; + } + /** + * @brief Determines which entity of a dimension `ent_dim` a point intersects + * from the barycentric coordinates + * + * If the point corresponding to the input barycentric coordinates intersects + * any entities with dimension `ent_dim` bordering the element this mapping is + * constructed from, this function determines the offset in the + * Omega_h::Adj::ab2b structure coresponding to FACE -> `ent_dim` adjacency. + * If the point does not intersect a border entity, this function returns -1 + * + * @param ent_dim the dimension of the entities we want to check intersection + * with + * @param bary_coords the barycentric coordinates computed by this mapping of + * a point in global space + * @returns -1 if the point does not intersect any entities, or the offset of + * the intersected entity in the Omega_h::Adj::ab2b structure + * coresponding to FACE -> `ent_dim` adjacency + */ + KOKKOS_FUNCTION + int which(int ent_dim, Omega_h::Vector const& bary_coords) const + { + if (ent_dim == Omega_h::VERT) { + return which_vert(bary_coords); + } + if (ent_dim == Omega_h::EDGE) { + return which_edge(bary_coords); + } + if (ent_dim == Omega_h::FACE) { + return within_elem(bary_coords); + } + return -1; + } + +private: + /** + * @brief Computes the vertex offset of the point corresponding + * to the input barycentric coordinates + * @param bary_coords the input barycentric coordinates + * @returns the vertex offset if the point corresponding to the given bary. + * coordinates is within a certain (global) tolerance of a vertex -1 if the + * point does not lie within the tolerance of any vertex + */ + KOKKOS_FUNCTION + int which_vert(Omega_h::Vector const& bary_coords) const + { + for (int i = 0; i < 3; i++) { + // distance of the point to vertex i + Omega_h::Vector<2> error = + (bary_coords[0] - kronecker(i, 0)) * triangle[0] + + (bary_coords[1] - kronecker(i, 1)) * triangle[1] + + (bary_coords[2] - kronecker(i, 2)) * triangle[2]; + if (Omega_h::norm_squared(error) <= tolerances_[0] * tolerances_[0]) + return i; + } + return -1; + } + /** + * @brief Computes the edge offset of the point corresponding + * to the input barycentric coordinates + * @param bary_coords the input barycentric coordinates + * @returns the edge offset if the distnce from the point corresponding to the + * given bary. coordinates to the edge with the respective + * offset is within a certain (global) tolerance -1 if the point does not lie + * within the tolerance of any vertex + */ + KOKKOS_FUNCTION + int which_edge(Omega_h::Vector const& bary_coords) const + { + for (int i = 0; i <= 2; i++) { + // Distance of the point to the edge opposite the ith vertex, + // see docs for derivation. + double dist = (2 * bary_coords[i] * triangle_area) * + (2 * bary_coords[i] * triangle_area); + dist /= opposite_edge_len_sq(i); + if (dist <= tolerances_[1] * tolerances_[1] && + bary_coords[(i + 1) % 3] >= 0 && bary_coords[(i + 2) % 3] >= 0) + return (i + 1) % 3; + } + return -1; + } + /** + * @brief Computes whether a point with the given barycentric coordinates + * is within a triangle + * @param bary_coords the input barycentric coordinates + * @returns 0 if the point is within the highest order element and -1 + * otherwise + */ + KOKKOS_FUNCTION + int within_elem(Omega_h::Vector const& bary_coords) const + { + return -1 * (int)!(bary_coords[0] > 0 && bary_coords[1] > 0 && + bary_coords[2] > 0); + } + /** + * @brief Calculates the length of the edge opposite vertex i + * @param i the vertex index opposite the desired edge + * @returns the length of the ith edge + */ + KOKKOS_FUNCTION + double opposite_edge_len_sq(int i) const + { + return Omega_h::norm_squared(triangle[(i + 2) % 3] - triangle[(i + 1) % 3]); + } + + // REPRESENTATION + Omega_h::Vector tolerances_; + // representation of the barycentric coordinate mapping, source: + // https://en.wikipedia.org/wiki/Barycentric_coordinate_system#Edge_approach + Omega_h::Matrix bary_transform; // Column-major order + // Points defining the triangle + Omega_h::Matrix triangle; + // area of the triangle + double triangle_area; +}; + +template <> +class Mapping<3> +{ +public: + static constexpr int DIM = 3; + /** + * @brief Default constructor + */ + KOKKOS_FUNCTION + Mapping() = default; + + Mapping(Omega_h::Matrix const& tetrahedron_, + const Kokkos::View& tolerances) + : tetrahedron(tetrahedron_) + { + auto tol_h = Kokkos::create_mirror_view(tolerances); + Kokkos::deep_copy(tol_h, tolerances); + tolerances_[0] = tol_h(0); + tolerances_[1] = tol_h(1); + tolerances_[2] = tol_h(2); + set_triangle_areas(); + bary_transform = {tetrahedron[0] - tetrahedron[3], + tetrahedron[1] - tetrahedron[3], + tetrahedron[2] - tetrahedron[3]}; + tetrahedron_volume = fabs(Omega_h::determinant(bary_transform)) / 6.; + bary_transform = Omega_h::invert(bary_transform); + } + /** + * @brief Default destructor + */ + KOKKOS_FUNCTION + ~Mapping() = default; + /** + * @brief Computes the barycentric coordinates of a point in global space + * @param p the point to compute the barycentric coordinates of + * @returns an Omega_h::Vector containing the barycentric coordinates + * of p + */ + KOKKOS_FUNCTION + Omega_h::Vector get_bary(Omega_h::Vector const& p) const + { + Omega_h::Vector<3> coeffs = bary_transform * (p - tetrahedron[3]); + return {coeffs[0], coeffs[1], coeffs[2], + 1 - coeffs[0] - coeffs[1] - coeffs[2]}; + } + /** + * @brief Determines which entity of dimension `ent_dim` a point intersects + * from the barycentric coordinates + * + * If the point corresponding to the input barycentric coordinates intersects + * any entities with dimension `ent_dim` bordering the element this mapping is + * constructed from, this function determines the offset in the + * Omega_h::Adj::ab2b structure coresponding to REGION -> `ent_dim` adjacency. + * If the point does not intersect a border entity, this function returns -1 + * + * @param ent_dim the dimension of the entities we want to check intersection + * with + * @param bary_coords the barycentric coordinates computed by this mapping of + * a point in global space + * @returns -1 if the point does not intersect any entities, or the offset of + * the intersected entity in the Omega_h::Adj::ab2b structure + * coresponding to REGION -> `ent_dim` adjacency + */ + KOKKOS_FUNCTION + int which(int ent_dim, Omega_h::Vector const& bary_coords) const + { + if (ent_dim == Omega_h::VERT) { + return which_vert(bary_coords); + } + if (ent_dim == Omega_h::EDGE) { + return which_edge(bary_coords); + } + if (ent_dim == Omega_h::FACE) { + return which_face(bary_coords); + } + if (ent_dim == Omega_h::REGION) { + return within_elem(bary_coords); + } + return -1; + } + +private: + /** + * @brief Computes the vertex offset of the point corresponding + * to the input barycentric coordinates + * @param bary_coords the input barycentric coordinates + * @returns the vertex offset if the point corresponding to the given bary. + *coordinates is within a certain (global) tolerance of a vertex -1 if the + *point does not lie within the tolerance of any vertex + */ + KOKKOS_FUNCTION + int which_vert(Omega_h::Vector const& bary_coords) const + { + for (int i = 0; i < 4; i++) { + // distance to the ith vertex + Omega_h::Vector<3> error = + (bary_coords[0] - kronecker(i, 0)) * tetrahedron[0] + + (bary_coords[1] - kronecker(i, 1)) * tetrahedron[1] + + (bary_coords[2] - kronecker(i, 2)) * tetrahedron[2] + + (bary_coords[3] - kronecker(i, 3)) * tetrahedron[3]; + if (Omega_h::norm_squared(error) <= tolerances_[0] * tolerances_[0]) + return i; + } + return -1; + } + /** + * @brief Computes the edge offset of the point corresponding + * to the input barycentric coordinates + * @param bary_coords the input barycentric coordinates + * @returns the edge offset if the distnce from the point corresponding to the + * given bary. coordinates to the edge with the respective + * offset is within a certain (global) tolerance -1 if the point does not lie + * within the tolerance of any vertex + */ + KOKKOS_FUNCTION + int which_edge(Omega_h::Vector const& bary_coords) const + { + int edge_ = 0; + for (int i = 0; i < 4; i++) { + for (int j = i + 1; j < 4; j++) { + Omega_h::Vector<3> side1 = {0, 0, 0}, side2 = {0, 0, 0}, + side3 = tetrahedron[i] - tetrahedron[j]; + for (int k = 0; k < 4; k++) { + side1 += (bary_coords[k] - kronecker(i, k)) * tetrahedron[k]; + side2 += (bary_coords[k] - kronecker(j, k)) * tetrahedron[k]; + } + + // the norm of the cross product of two vectors is twice the area of the + // triangle those vectors form + double distnce_sq = Omega_h::norm_squared(Omega_h::cross(side1, side2)); + distnce_sq /= Omega_h::norm_squared(side3); + + if (distnce_sq <= tolerances_[1] * tolerances_[1] && + bary_coords[i] > 0 && bary_coords[j] > 0) + return edge(edge_); + edge_++; + } + } + return -1; + } + /** + * @brief Computes the face offset of the point corresponding + * to the input barycentric coordinates + * @param bary_coords the input barycentric coordinates + * @returns if the point corresponding to the bary. coords is within a + * global tolerance of a face, returns the offset of that face + * -1 if the point does not lie within the tolerance of any face + * Algorithm source: + * C.E. Passerello, + * Interference detection using barycentric coordinates, + * Mechanics Research Communications, + * Volume 9, Issue 6, 1982, Pages 373-378, + * https://doi.org/10.1016/0093-6413(82)90034-9. + */ + KOKKOS_FUNCTION + int which_face(Omega_h::Vector const& bary_coords) const + { + for (int i = 0; i < 4; i++) { + if (fabs(3 * tetrahedron_volume * bary_coords[i] / face_areas[i]) <= + tolerances_[2] && + bary_coords[(i + 1) % 4] >= 0 && bary_coords[(i + 2) % 4] >= 0 && + bary_coords[(i + 3) % 4] >= 0) { + return face(i); + } + } + return -1; + } + /** + * @brief Computes whether a point with the given barycentric coordinates + * is within a triangle + * @param bary_coords the input barycentric coordinates + * @returns 0 if the point is within the highet order element and -1 otherwise + */ + KOKKOS_FUNCTION + int within_elem(Omega_h::Vector const& bary_coords) const + { + return -1 * (int)!(bary_coords[0] >= 0 && bary_coords[1] >= 0 && + bary_coords[2] >= 0 && bary_coords[3] >= 0); + } + /** + * @brief Calculates the areas of each face in the tetrahedron the mapping + * corresponds to + * @param index the element ID of the tetrahedron + * @param mesh the Omega_h::Mesh with the spatial information for the + * tetrahedron corresponding to `index` + */ + void set_triangle_areas() + { + for (int i = 0; i < 4; i++) { + Omega_h::Vector<3> edge0 = + tetrahedron[(i + 1) % 4] - tetrahedron[(i + 3) % 4], + edge1 = + tetrahedron[(i + 2) % 4] - tetrahedron[(i + 3) % 4]; + Omega_h::Vector<3> cross = Omega_h::cross(edge0, edge1); + face_areas[i] = 0.5 * Omega_h::norm(cross); + } + } + // returns the actual face/edge offset from the barycentric offset + KOKKOS_INLINE_FUNCTION int face(int i) const + { + return (OFFSETS & 3 << (i * 2)) >> (i * 2); + } + KOKKOS_INLINE_FUNCTION int edge(int i) const + { + return (i < 4 && i > 0) ? (OFFSETS & 3 << ((i - 1) * 2)) >> ((i - 1) * 2) + : i; + } + // representation + Omega_h::Vector tolerances_; + Omega_h::Matrix bary_transform; // Column-major order + Omega_h::Matrix tetrahedron; + Omega_h::Vector face_areas; + double tetrahedron_volume; + // This is used to calculate the face and edge offsets in the cannonical + // ordering: + // https://user-images.githubusercontent.com/56453280/74203616-39d27a80-4c3e-11ea-885d-b0260490e184.png + static const char OFFSETS = 1 << 4 | 3 << 2 | 2; +}; + +/** + * Functor to be called when a point intersects a triangle or tetrahedron + */ +class CallOnIntersect3D +{ +public: + using MemorySpace = TreePointSearch::MemorySpace; + static constexpr int DIM = 3; + + CallOnIntersect3D(const Kokkos::View*, MemorySpace>& mappings_, + const Omega_h::LOs& adjacencies0, + const Omega_h::LOs& adjacencies1, + const Omega_h::LOs& adjacencies2, + Kokkos::View& + dimensionalities_, + Kokkos::View& element_ids_, + Kokkos::View& parametric_coords_) + : mappings(MakeRank1View(mappings_)), + adjacencies{adjacencies0, adjacencies1, adjacencies2}, + dimensionalities(MakeRank1View(dimensionalities_)), + element_ids(MakeRank1View(element_ids_)), + parametric_coords(MakeRank2View(parametric_coords_)) {}; + + template + KOKKOS_FUNCTION void operator()(Predicate const& predicate, + Value const& val) const + { + ArborX::Point const& ax = + ArborX::getGeometry(predicate); + Omega_h::Vector point{ax[0], ax[1], ax[2]}; + int point_ind = ArborX::getData(predicate); + + detail::Mapping<3> const& tm = mappings(val.index); + + // calculate the barycentric coefficients of the point + auto coeffs = tm.get_bary(point); + int offsets[DIM] = {4, 6, 4}; + for (int i = 0; i < DIM; i++) { + int elem = tm.which(i, coeffs); + if (elem >= 0 && + dimensionalities(point_ind) > (TreePointSearch::Dimensionality)i) { + auto elem_ind = adjacencies[i][offsets[i] * val.index + elem]; + dimensionalities(point_ind) = (TreePointSearch::Dimensionality)i; + element_ids(point_ind) = elem_ind; + for (int j = 0; j < DIM + 1; j++) { + parametric_coords(point_ind, j) = coeffs(j); + } + return; + } + } + + if (tm.which(DIM, coeffs) >= 0 && + dimensionalities(point_ind) > TreePointSearch::Dimensionality::REGION) { + dimensionalities(point_ind) = TreePointSearch::Dimensionality::REGION; + element_ids(point_ind) = (LO)val.index; + for (int j = 0; j < DIM + 1; j++) { + parametric_coords(point_ind, j) = coeffs(j); + } + } + } + +private: + Rank1View, MemorySpace> mappings; + Omega_h::LOs adjacencies[3]; + Rank1View dimensionalities; + Rank1View element_ids; + Rank2View parametric_coords; +}; + +class CallOnIntersect2D +{ +public: + using MemorySpace = TreePointSearch::MemorySpace; + static constexpr int DIM = 2; + + CallOnIntersect2D(const Kokkos::View*, MemorySpace>& mappings_, + const Omega_h::LOs& adjacencies0, + const Omega_h::LOs& adjacencies1, + Kokkos::View& + dimensionalities_, + Kokkos::View& element_ids_, + Kokkos::View& parametric_coords_) + : mappings(MakeRank1View(mappings_)), + adjacencies{adjacencies0, adjacencies1}, + dimensionalities(MakeRank1View(dimensionalities_)), + element_ids(MakeRank1View(element_ids_)), + parametric_coords(MakeRank2View(parametric_coords_)) {}; + + template + KOKKOS_FUNCTION void operator()(Predicate const& predicate, + Value const& val) const + { + ArborX::Point const& ax = + ArborX::getGeometry(predicate); + Omega_h::Vector point{ax[0], ax[1]}; + int point_ind = ArborX::getData(predicate); + + detail::Mapping<2> const& tm = mappings(val.index); + + // calculate the barycentric coefficients of the point + auto coeffs = tm.get_bary(point); + + for (int i = 0; i < DIM; i++) { + int elem = tm.which(i, coeffs); + if (elem >= 0 && + dimensionalities(point_ind) > (TreePointSearch::Dimensionality)i) { + auto elem_ind = adjacencies[i][3 * val.index + elem]; + dimensionalities(point_ind) = (TreePointSearch::Dimensionality)i; + element_ids(point_ind) = elem_ind; + for (int j = 0; j < DIM + 1; j++) { + parametric_coords(point_ind, j) = coeffs(j); + } + return; + } + } + if (tm.which(DIM, coeffs) >= 0 && + dimensionalities(point_ind) > TreePointSearch::Dimensionality::FACE) { + dimensionalities(point_ind) = TreePointSearch::Dimensionality::FACE; + element_ids(point_ind) = (LO)val.index; + for (int j = 0; j < DIM + 1; j++) { + parametric_coords(point_ind, j) = coeffs(j); + } + } + } + +private: + Rank1View, MemorySpace> mappings; + Omega_h::LOs adjacencies[2]; + Rank1View dimensionalities; + Rank1View element_ids; + Rank2View parametric_coords; +}; +} // namespace detail +} // namespace pcms + +template +struct ArborX::AccessTraits> +{ + using memory_space = typename Omega_h::ExecSpace::memory_space; + + static KOKKOS_FUNCTION int size( + const pcms::detail::Omega_h_Mesh_Adapt& mesh) + { + return mesh.size_; + } + + static KOKKOS_FUNCTION auto get( + const pcms::detail::Omega_h_Mesh_Adapt& mesh, int i) + { + ArborX::Point min = {INFINITY}; + ArborX::Point max = {-INFINITY}; + for (int j = 0; j < dim + 1; ++j) { + auto cell_vert_id = mesh.adjacency[(dim + 1) * i + j]; + for (int k = 0; k < dim; ++k) { + Omega_h::Real curr_coord = mesh.coordinates[cell_vert_id * dim + k]; + if (min[k] > curr_coord) + min[k] = curr_coord; + if (max[k] < curr_coord) + max[k] = curr_coord; + } + } + return ArborX::Box(min, max); + } +}; + +template +struct ArborX::AccessTraits< + pcms::detail::Coordinate_View_Adapt> +{ + using memory_space = MemorySpace; + static KOKKOS_FUNCTION int size( + pcms::detail::Coordinate_View_Adapt const& coords) + { + return coords.points.extent(0); + } + static KOKKOS_FUNCTION auto get( + pcms::detail::Coordinate_View_Adapt const& coords, int i) + { + ArborX::Point ax_point; + for (int j = 0; j < dim; j++) + ax_point[j] = coords.points(i, j); + return PredicateWithAttachment(intersects(ax_point), i); + } +}; + +namespace pcms +{ +TreePointSearch::Results TreePointSearch::Apply( + const CoordinateView& coords) const +{ + if (coords.GetCoordinateSystem() != pcms::CoordinateSystem::Cartesian) { + throw pcms_error("TreePointSearch::Apply only implemented for" + " Cartesian coordinates"); + } + if (coords.GetValues().extent(1) != mesh_.dim()) { + throw pcms_error("Input coordinate space dimension " + + std::to_string(coords.GetValues().extent(1)) + + " does not match query space dimension " + + std::to_string(mesh_.dim())); + } + + if (mesh_.dim() == 2) { + static constexpr int DIM = 2; + Omega_h::ExecSpace execution_space; + + Kokkos::View dims( + "dimensionalities", coords.GetValues().extent(0)); + Kokkos::deep_copy(dims, TreePointSearch::Dimensionality::NO_INTERSECT); + + Kokkos::View elem_ids("element IDs", + coords.GetValues().extent(0)); + Kokkos::deep_copy(elem_ids, -1); + + Kokkos::View parametric_coords( + "parametric coordinates", coords.GetValues().extent(0), + coords.GetValues().extent(1) + 1); + Kokkos::deep_copy(parametric_coords, -1.0); + + tree->get_tree<2>().query( + execution_space, + detail::Coordinate_View_Adapt{coords.GetValues()}, + detail::CallOnIntersect2D(tree->get_mappings<2>(), + mesh_.get_adj(Omega_h::FACE, 0).ab2b, + mesh_.get_adj(Omega_h::FACE, 1).ab2b, dims, + elem_ids, parametric_coords)); + return Results{dims, elem_ids, parametric_coords}; + } else { + static constexpr int DIM = 3; + Omega_h::ExecSpace execution_space; + + Kokkos::View dims( + "dimensionalities", coords.GetValues().extent(0)); + Kokkos::deep_copy(dims, TreePointSearch::Dimensionality::NO_INTERSECT); + + Kokkos::View elem_ids("element IDs", + coords.GetValues().extent(0)); + Kokkos::deep_copy(elem_ids, -1); + + Kokkos::View parametric_coords( + "parametric coordinates", coords.GetValues().extent(0), + coords.GetValues().extent(1) + 1); + Kokkos::deep_copy(parametric_coords, -1.0); + + tree->get_tree<3>().query( + execution_space, + detail::Coordinate_View_Adapt{coords.GetValues()}, + detail::CallOnIntersect3D(tree->get_mappings<3>(), + mesh_.get_adj(Omega_h::REGION, 0).ab2b, + mesh_.get_adj(Omega_h::REGION, 1).ab2b, + mesh_.get_adj(Omega_h::REGION, 2).ab2b, dims, + elem_ids, parametric_coords)); + return Results{dims, elem_ids, parametric_coords}; + } +} + +[[nodiscard]] LO TreePointSearch::GetOwningElementId( + const TreePointSearch::Results& results, int i) +{ + const Kokkos::View query_id{""}; + const Kokkos::View dim{""}; + Kokkos::parallel_for( + 1, KOKKOS_LAMBDA(const int) { + query_id(0) = (results.element_ids(i) < 0) ? -results.element_ids(i) + : results.element_ids(i); + dim(0) = results.dimensionalities(i); + }); + auto query_id_h = Kokkos::create_mirror_view(query_id); + auto dim_h = Kokkos::create_mirror_view(dim); + Kokkos::deep_copy(query_id_h, query_id); + Kokkos::deep_copy(dim_h, dim); + return pcms::GetOwningElementId(mesh_, mesh_.dim(), + static_cast(dim_h(0)), query_id_h(0)); +} + +[[nodiscard]] Kokkos::View TreePointSearch::GetOwningElementIds( + const TreePointSearch::Results& results) +{ + Kokkos::View owning_ids("Owning element IDs", + results.dimensionalities.size()); + auto vert2elem = mesh_.ask_up(Omega_h::VERT, mesh_.dim()); + auto edge2elem = mesh_.ask_up(Omega_h::EDGE, mesh_.dim()); + auto face2elem = (mesh_.dim() == 3) ? mesh_.ask_up(Omega_h::FACE, mesh_.dim()) + : Omega_h::Adj{}; + auto mesh_dim = mesh_.dim(); + Kokkos::parallel_for( + owning_ids.size(), KOKKOS_LAMBDA(const int i) { + if (static_cast(results.dimensionalities(i)) == mesh_dim) { + owning_ids(i) = abs(results.element_ids(i)); + } else if (static_cast(results.dimensionalities(i)) == 2) { + owning_ids(i) = GetOwningElementIdFromAdj(face2elem, 2, mesh_dim, + results.element_ids(i)); + } else if (static_cast(results.dimensionalities(i)) == 1) { + owning_ids(i) = GetOwningElementIdFromAdj(edge2elem, 1, mesh_dim, + results.element_ids(i)); + } else if (static_cast(results.dimensionalities(i)) == 0) { + owning_ids(i) = GetOwningElementIdFromAdj(vert2elem, 0, mesh_dim, + results.element_ids(i)); + } else { + owning_ids(i) = -1; + } + }); + return owning_ids; +} + +std::unique_ptr TreePointSearch::make_tree( + Omega_h::Mesh& mesh) const +{ + ExecSpace execution_space; + using DeviceType = + Kokkos::Device; + + if (mesh.dim() == 2) { + detail::Omega_h_Mesh_Adapt<2> tagged_mesh{ + mesh.nelems(), mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b, + mesh.coords()}; + + detail::TreeWrapper::Tree_t<2> tree = detail::TreeWrapper::Tree_t<2>( + execution_space, ArborX::Experimental::attach_indices(tagged_mesh)); + + detail::TreeWrapper::Mappings_t<2> mappings = + detail::TreeWrapper::Mappings_t<2>("mappings", mesh.nelems()); + + auto face2vert = + Omega_h::HostRead(mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b); + auto vert_coords = Omega_h::HostRead(mesh.coords()); + + auto mappings_h = Kokkos::create_mirror_view(mappings); + for (int i = 0; i < mesh.nelems(); i++) { + Omega_h::Matrix<2, 3> triangle = { + {vert_coords[face2vert[i * 3] * 2], + vert_coords[face2vert[i * 3] * 2 + 1]}, + {vert_coords[face2vert[i * 3 + 1] * 2], + vert_coords[face2vert[i * 3 + 1] * 2 + 1]}, + {vert_coords[face2vert[i * 3 + 2] * 2], + vert_coords[face2vert[i * 3 + 2] * 2 + 1]}}; + mappings_h[i] = detail::Mapping<2>(triangle, tolerances_); + } + Kokkos::deep_copy(execution_space, mappings, mappings_h); + return std::make_unique(mappings, tree); + } + if (mesh.dim() == 3) { + detail::Omega_h_Mesh_Adapt<3> tagged_mesh{ + mesh.nelems(), mesh.ask_down(Omega_h::REGION, Omega_h::VERT).ab2b, + mesh.coords()}; + detail::TreeWrapper::Tree_t<3> tree = detail::TreeWrapper::Tree_t<3>( + execution_space, ArborX::Experimental::attach_indices(tagged_mesh)); + + detail::TreeWrapper::Mappings_t<3> mappings = + detail::TreeWrapper::Mappings_t<3>("mappings", mesh.nelems()); + + auto region2vert = + Omega_h::HostRead(mesh.ask_down(Omega_h::REGION, Omega_h::VERT).ab2b); + auto vert_coords = Omega_h::HostRead(mesh.coords()); + + auto mappings_h = Kokkos::create_mirror_view(mappings); + for (int i = 0; i < mesh.nelems(); i++) { + Omega_h::Matrix<3, 4> tetrahedron = { + {vert_coords[region2vert[i * 4] * 3], + vert_coords[region2vert[i * 4] * 3 + 1], + vert_coords[region2vert[i * 4] * 3 + 2]}, + {vert_coords[region2vert[i * 4 + 1] * 3], + vert_coords[region2vert[i * 4 + 1] * 3 + 1], + vert_coords[region2vert[i * 4 + 1] * 3 + 2]}, + {vert_coords[region2vert[i * 4 + 2] * 3], + vert_coords[region2vert[i * 4 + 2] * 3 + 1], + vert_coords[region2vert[i * 4 + 2] * 3 + 2]}, + {vert_coords[region2vert[i * 4 + 3] * 3], + vert_coords[region2vert[i * 4 + 3] * 3 + 1], + vert_coords[region2vert[i * 4 + 3] * 3 + 2]}}; + + auto tol_h = Kokkos::create_mirror_view(tolerances_); + Kokkos::deep_copy(tol_h, tolerances_); + mappings_h[i] = detail::Mapping<3>(tetrahedron, tolerances_); + } + Kokkos::deep_copy(execution_space, mappings, mappings_h); + return std::make_unique(mappings, tree); + } + throw pcms_error("Invalid mesh dimension " + std::to_string(mesh.dim()) + + ", TreePointSearch only implemented for 2D and 3D"); +} + +} // namespace pcms diff --git a/src/pcms/localization/arbor_x_localization.hpp b/src/pcms/localization/arbor_x_localization.hpp new file mode 100644 index 000000000..0d58862d3 --- /dev/null +++ b/src/pcms/localization/arbor_x_localization.hpp @@ -0,0 +1,130 @@ +#ifndef POINT_LOCALIZATION_H +#define POINT_LOCALIZATION_H + +#include + +#include +#include +// headers from detail are included to reproduce earlier (index based) version +// of ArborX querying: https://github.com/arborx/ArborX/wiki/MigrationGuide +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "pcms/utility/assert.h" +#include "pcms/utility/types.h" +#include "pcms/utility/arrays.h" +#include "pcms/localization/localization.h" + +namespace pcms +{ + +namespace detail +{ + +template +class Mapping +{}; + +/** + * This is type erasure for dimension templated classes (ArborX structures, + * barycentric Mapping) + */ +struct TreeWrapper +{ + template + using Mappings_t = + Kokkos::View*, Omega_h::ExecSpace::memory_space>; + template + using Tree_t = + ArborX::BVH, unsigned>>; + + template + TreeWrapper(const Mappings_t& mappings, const Tree_t& tree) + { + dim_ = dim; + mappings_ = mappings; + tree_ = tree; + } + /** + * @brief returns a pointer to an ArborX tree + */ + template + inline Tree_t get_tree() + { + if (dim != dim_) { + throw pcms_error("Requested get_tree return " + "type does not match internal Tree_t"); + } + return std::any_cast>(*tree_); + } + + /** + * @brief returns a pointer to a Kokkos::View of Mappings + */ + template + inline Mappings_t get_mappings() + { + if (dim != dim_) { + throw pcms_error("Requested get_mappings return " + "type does not match internal Mappings_t"); + } + return std::any_cast>(*mappings_); + }; + +private: + int dim_; + std::any tree_; + std::any mappings_; +}; +} // namespace detail + +class TreePointSearch : public PointSearch +{ +public: + using Results = PointSearch::Results; + using Dimensionality = Results::Dimensionality; + using ExecSpace = PointSearch::ExecSpace; + using MemorySpace = PointSearch::MemorySpace; + + TreePointSearch(Omega_h::Mesh& mesh) + : PointSearch( + PointSearchTolerances("tree point search tolerances", mesh.dim())), + mesh_(mesh) + { + Kokkos::deep_copy(tolerances_, 1e-12); + tree = make_tree(mesh); + } + TreePointSearch(Omega_h::Mesh& mesh, const PointSearchTolerances& tolerances) + : PointSearch(tolerances), mesh_(mesh), tree(make_tree(mesh)) + { + } + ~TreePointSearch() = default; + /** + * Given a set of points in global coordinates give the ids of the entities + * that the points lie within and the parametric coordinates of each point + * within a triangles or tetrahedra adjacent to the intersected entity. + * If the point does not lie within any triangle element, the id will + * be a negative number. + */ + Results Apply(const CoordinateView& coords) const override; + [[nodiscard]] LO GetOwningElementId(const Results& results, int i) override; + [[nodiscard]] Kokkos::View GetOwningElementIds( + const Results& results) override; + +private: + std::unique_ptr make_tree(Omega_h::Mesh& mesh) const; + // Reference to the input mesh + Omega_h::Mesh& mesh_; + std::unique_ptr tree; +}; + +} // namespace pcms +#endif // POINT_LOCALIZATION_H diff --git a/src/pcms/localization/localization.cpp b/src/pcms/localization/localization.cpp new file mode 100644 index 000000000..e07f04ad6 --- /dev/null +++ b/src/pcms/localization/localization.cpp @@ -0,0 +1,37 @@ +#include "pcms/localization/localization.h" + +namespace pcms +{ + +LO GetOwningElementId(Omega_h::Mesh& mesh, int mesh_dim, int entity_dim, + LO element_id) +{ + if (element_id < 0) + return -1; + + const int target_dim = mesh_dim; // faces for 2D, regions for 3D + + // If entity is already at target dimension, return it directly + if (entity_dim == target_dim) + return element_id; + + // Get the upward adjacency from entity_dim to target_dim + auto upward_adj = mesh.ask_up(entity_dim, target_dim); + auto a2ab_h = Omega_h::HostRead(upward_adj.a2ab); + auto ab2b_h = Omega_h::HostRead(upward_adj.ab2b); + + const auto begin = a2ab_h[element_id]; + const auto end = a2ab_h[element_id + 1]; + if (begin >= end) + return -1; + + // Find the smallest owning element ID + LO owner = ab2b_h[begin]; + for (auto i = begin + 1; i < end; ++i) { + const LO candidate = ab2b_h[i]; + if (candidate < owner) + owner = candidate; + } + return owner; +} +} // namespace pcms diff --git a/src/pcms/localization/localization.h b/src/pcms/localization/localization.h new file mode 100644 index 000000000..0683865e0 --- /dev/null +++ b/src/pcms/localization/localization.h @@ -0,0 +1,186 @@ +#ifndef PCMS_COUPLING_POINT_SEARCH_H +#define PCMS_COUPLING_POINT_SEARCH_H +#include + +#include +#include +#include +#include + +#include "pcms/utility/types.h" +#include "pcms/utility/uniform_grid.h" +#include "pcms/utility/bounding_box.h" +#include "pcms/field/coordinate_system.h" + +namespace pcms +{ + +/** + * Get the owning element (face for 2D, region for 3D) for a given entity. + * + * Given an entity of dimension `entity_dim` with ID `element_id`, returns the + * smallest ID of the owning element of dimension `mesh_dim` (the mesh's + * top-level dimension). + */ +LO GetOwningElementId(Omega_h::Mesh& mesh, int mesh_dim, int entity_dim, + LO element_id); + +template +KOKKOS_INLINE_FUNCTION LO +GetOwningElementIdFromAdj(const Omega_h::Adj& upward_adj, DimEnum entity_dim, + DimEnum target_dim, LO element_id) +{ + if (element_id < 0) + return -1; + + // If entity is already at target dimension, return it directly + if (static_cast(entity_dim) == static_cast(target_dim)) + return element_id; + + const auto begin = upward_adj.a2ab[element_id]; + const auto end = upward_adj.a2ab[element_id + 1]; + if (begin >= end) + return -1; + + // Find the smallest owning element ID + LO owner = upward_adj.ab2b[begin]; + for (auto i = begin + 1; i < end; ++i) { + const LO candidate = upward_adj.ab2b[i]; + if (candidate < owner) + owner = candidate; + } + return owner; +} + +// +// TODO take a bounding box as we may want a bbox that's bigger than the mesh! +// this function is in the public header for testing, but should not be directly +// used +namespace detail +{ +Kokkos::Crs +construct_intersection_map_2d(Omega_h::Mesh& mesh, + Kokkos::View grid, + int num_grid_cells); + +Kokkos::Crs +construct_intersection_map_3d(Omega_h::Mesh& mesh, + Kokkos::View grid, + int num_grid_cells); + +/// O(num_cells * nelems) brute-force candidate map. Test oracle for the +/// element-major build above; never use in production. +Kokkos::Crs +construct_intersection_map_reference_2d(Omega_h::Mesh& mesh, + Kokkos::View grid, + int num_grid_cells); + +/// \see construct_intersection_map_reference_2d +Kokkos::Crs +construct_intersection_map_reference_3d(Omega_h::Mesh& mesh, + Kokkos::View grid, + int num_grid_cells); +} // namespace detail + +[[nodiscard]] KOKKOS_FUNCTION bool triangle_intersects_bbox( + const Omega_h::Matrix<2, 3>& coords, const AABBox<2>& bbox); + +/** + * Point search base class + */ +class PointSearch +{ +public: + using ExecSpace = Omega_h::ExecSpace; + using MemorySpace = ExecSpace::memory_space; + /** + * Results type gives dimensionalities of point intersection, the intersected + * element IDs, and the barycentric coordinate mappings of each point as + * follows: the `i`th input point has intersection dimensionality of + * `dimensionalities(i)`, intersected element ID of `element_ids(i)`, and + * likewise parametric coordinate mapping of `parametric_coords(i,0..d)` + * where `d` is the dimension of the space + */ + struct Results + { + enum class Dimensionality + { + VERTEX = 0, + EDGE = 1, + FACE = 2, + REGION = 3, + NO_INTERSECT = 4 + }; + + Kokkos::View dimensionalities; + Kokkos::View element_ids; + Kokkos::View parametric_coords; + + /// `element_id` of a point outside the mesh whose nearest entity is `id`. + [[nodiscard]] KOKKOS_INLINE_FUNCTION static constexpr LO OutsideId(LO id) + { + return -id - 1; + } + + /// True when the point was not located inside any entity. + [[nodiscard]] KOKKOS_INLINE_FUNCTION constexpr bool IsOutside(LO id) const + { + return id < 0; + } + + /// Id of the entity of dimension `dimensionalities(i)`: the containing + /// entity of a located point, the nearest entity of an outside point, or -1 + /// for NO_INTERSECT. + [[nodiscard]] KOKKOS_INLINE_FUNCTION constexpr LO EntityId(LO i) const + { + if (dimensionalities(i) == Dimensionality::NO_INTERSECT) { + return -1; + } + return element_ids(i) < 0 ? -element_ids(i) - 1 : element_ids(i); + } + + /// Id of the entity of dimension `dimensionalities(i)`: the containing + /// entity of a located point, the nearest entity of an outside point, or -1 + /// for NO_INTERSECT. + [[nodiscard]] KOKKOS_INLINE_FUNCTION static constexpr LO EntityId( + LO id, Dimensionality dim) + { + if (dim == Dimensionality::NO_INTERSECT) { + return -1; + } + return id < 0 ? -id - 1 : id; + } + }; + + using PointSearchTolerances = Kokkos::View; + + explicit PointSearch(const PointSearchTolerances& tolerances) + : tolerances_(tolerances) + { + assert(tolerances_.is_allocated()); + } + + ~PointSearch() = default; + + virtual Results Apply(const CoordinateView& coords) const = 0; + /** + * This function provides a temporary solution to retrieve the original + * behavior of the point search, which previously returned the face ID + * regardless of which entity the search result belonged to. Many parts of + * the codebase still rely on this legacy behavior. After updating the point + * search to return the exact entity, this function can be called to retrieve + * the old behavior and ensure correctness. Long term, the implementation + * should be updated to properly handle the new result. + */ + [[nodiscard]] virtual LO GetOwningElementId(const Results& results, + int i) = 0; + [[nodiscard]] virtual Kokkos::View GetOwningElementIds( + const Results& results) const = 0; + virtual LO Dim() const = 0; + +protected: + PointSearchTolerances tolerances_; +}; + +} // namespace pcms +#endif // PCMS_COUPLING_POINT_SEARCH_H diff --git a/src/pcms/localization/localization_factory.h b/src/pcms/localization/localization_factory.h index f5671fa69..889d9c64f 100644 --- a/src/pcms/localization/localization_factory.h +++ b/src/pcms/localization/localization_factory.h @@ -2,7 +2,7 @@ #define PCMS_LOCALIZATION_LOCALIZATION_FACTORY_H #include "pcms/localization/adj_search.hpp" -#include "pcms/field/coordinate_system.h" +#include "pcms/field/coordinate_view.hpp" #include "pcms/utility/memory_spaces.h" namespace pcms diff --git a/src/pcms/localization/mesh_localization.cpp b/src/pcms/localization/mesh_localization.cpp index 27821430b..d5d18f030 100644 --- a/src/pcms/localization/mesh_localization.cpp +++ b/src/pcms/localization/mesh_localization.cpp @@ -1,13 +1,14 @@ #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/field/coordinate_view.hpp" #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 +#include namespace pcms { @@ -15,9 +16,13 @@ namespace pcms SupportResults AdjacencyLocalizationFactory::Build( CoordinateView target_coords) const { - if (target_coords.GetCoordinateSystem() != CoordinateSystem::Cartesian) { + // Euclidean search structures are valid wherever the + // metric is the identity. + if (!HasIdentityMetric(*target_coords.GetCoordinateSystem())) { throw pcms_error( - "AdjacencyLocalizationFactory: only Cartesian coordinates are supported"); + "AdjacencyLocalizationFactory: requires a coordinate system whose metric " + "is the identity (orthogonal basis and unit scale factors); got '" + + std::string(target_coords.GetCoordinateSystem()->Kind()) + "'"); } const auto tgt_view = target_coords.GetValues(); diff --git a/src/pcms/localization/point_cloud_localization.h b/src/pcms/localization/point_cloud_localization.h index 401b17e78..6605380bd 100644 --- a/src/pcms/localization/point_cloud_localization.h +++ b/src/pcms/localization/point_cloud_localization.h @@ -10,6 +10,7 @@ #include #include +#include namespace pcms { @@ -30,10 +31,14 @@ class PointCloudLocalizationFactory : public LocalizationFactory SupportResults Build( CoordinateView target_coords) const override { - if (target_coords.GetCoordinateSystem() != CoordinateSystem::Cartesian) { + // Euclidean search structures are valid wherever + // the metric is the identity. + if (!HasIdentityMetric(*target_coords.GetCoordinateSystem())) { throw pcms_error( - "PointCloudLocalizationFactory: only Cartesian coordinates are " - "supported"); + "PointCloudLocalizationFactory: requires a coordinate system whose " + "metric is the identity; " + "got '" + + std::string(target_coords.GetCoordinateSystem()->Kind()) + "'"); } const auto src_view = layout_->GetCoordinates(); diff --git a/src/pcms/localization/point_search.cpp b/src/pcms/localization/point_search.cpp deleted file mode 100644 index f71481074..000000000 --- a/src/pcms/localization/point_search.cpp +++ /dev/null @@ -1,826 +0,0 @@ -#include "point_search.h" -#include -#include -#include - -// From -// https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Vector_formulation -template -KOKKOS_INLINE_FUNCTION Omega_h::Real distance_from_line( - const Omega_h::Vector& a, const Omega_h::Vector& b, - const Omega_h::Vector& p) -{ - Omega_h::Vector n = Omega_h::normalize(b - a); - Omega_h::Vector ap = a - p; - return Omega_h::norm(ap - (ap * n) * n); -} - -template -KOKKOS_INLINE_FUNCTION bool normal_intersects_segment( - const Omega_h::Vector a, const Omega_h::Vector b, - const Omega_h::Vector p) -{ - auto ab = b - a; - auto ba = a - b; - auto ap = p - a; - auto bp = p - b; - return (ap * ab) * (bp * ba) >= 0; -} - -namespace pcms -{ - -LO GetOwningElementId(Omega_h::Mesh& mesh, int mesh_dim, int entity_dim, - LO element_id) -{ - if (element_id < 0) - return -1; - - const int target_dim = mesh_dim; // faces for 2D, regions for 3D - - // If entity is already at target dimension, return it directly - if (entity_dim == target_dim) - return element_id; - - // Get the upward adjacency from entity_dim to target_dim - auto upward_adj = mesh.ask_up(entity_dim, target_dim); - auto a2ab_h = Omega_h::HostRead(upward_adj.a2ab); - auto ab2b_h = Omega_h::HostRead(upward_adj.ab2b); - - const auto begin = a2ab_h[element_id]; - const auto end = a2ab_h[element_id + 1]; - if (begin >= end) - return -1; - - // Find the smallest owning element ID - LO owner = ab2b_h[begin]; - for (auto i = begin + 1; i < end; ++i) { - const LO candidate = ab2b_h[i]; - if (candidate < owner) - owner = candidate; - } - return owner; -} - -KOKKOS_INLINE_FUNCTION -AABBox<2> triangle_bbox(const Omega_h::Matrix<2, 3>& coords) -{ - std::array max{coords(0, 0), coords(1, 0)}; - std::array min{coords(0, 0), coords(1, 0)}; - for (int i = 1; i < 3; ++i) { - max[0] = std::fmax(max[0], coords(0, i)); - max[1] = std::fmax(max[1], coords(1, i)); - min[0] = std::fmin(min[0], coords(0, i)); - min[1] = std::fmin(min[1], coords(1, i)); - } - return {.center = {(max[0] + min[0]) / 2.0, (max[1] + min[1]) / 2.0}, - .half_width = {(max[0] - min[0]) / 2.0, (max[1] - min[1]) / 2.0}}; -} - -template -KOKKOS_INLINE_FUNCTION AABBox simplex_bbox( - const Omega_h::Matrix& coords) -{ - Kokkos::Array max; - Kokkos::Array min; - for (int j = 0; j < dim; ++j) { - max[j] = coords(j, 0); - min[j] = coords(j, 0); - } - for (int i = 1; i < dim + 1; ++i) { - for (int j = 0; j < dim; ++j) { - max[j] = std::fmax(max[j], coords(j, i)); - min[j] = std::fmin(min[j], coords(j, i)); - } - } - - Kokkos::Array center; - Kokkos::Array half_width; - - for (int j = 0; j < dim; ++j) { - center[j] = (max[j] + min[j]) / 2.0; - half_width[j] = (max[j] - min[j]) / 2.0; - } - - return {.center = center, .half_width = half_width}; -} - -// Liang, You-Dong, and B. A. Barsky. “A New Concept and Method for Line -// Clipping.” ACM Transactions on Graphics 3, no. 1 (January 1984): 1–22. -// https://doi.org/10.1145/357332.357333. -KOKKOS_INLINE_FUNCTION -bool clipt(Real p, Real q, Real& t0, Real& t1) -{ - if (p < 0) { - auto r = q / p; - if (r > t1) { - return false; - } - if (r > t0) { - t0 = r; - } - } else if (p > 0) { - auto r = q / p; - if (r < t0) { - return false; - } - if (r < t1) { - t1 = r; - } - } - // p==0 - else { - if (q < 0) { - return false; - } - } - return true; -} -// Liang, You-Dong, and B. A. Barsky. “A New Concept and Method for Line -// Clipping.” ACM Transactions on Graphics 3, no. 1 (January 1984): 1–22. -// https://doi.org/10.1145/357332.357333. -KOKKOS_INLINE_FUNCTION -bool line_intersects_bbox(const Omega_h::Vector<2>& p0, - const Omega_h::Vector<2>& p1, const AABBox<2>& bbox) -{ - Real t0 = 0; - Real t1 = 1; - auto xleft = bbox.center[0] - bbox.half_width[0]; - auto deltax = p1[0] - p0[0]; - if (clipt(-deltax, p0[0] - xleft, t0, t1)) { - auto xright = bbox.center[0] + bbox.half_width[0]; - if (clipt(deltax, xright - p0[0], t0, t1)) { - auto deltay = p1[1] - p0[1]; - auto ybottom = bbox.center[1] - bbox.half_width[1]; - if (clipt(-deltay, p0[1] - ybottom, t0, t1)) { - auto ytop = bbox.center[1] + bbox.half_width[1]; - if (clipt(deltay, ytop - p0[1], t0, t1)) { - // full liang-barksy algorithm computes new x-y coordinates here, but - // we just need to check intersections - return true; - } - } - } - } - return false; -} - -template -[[nodiscard]] KOKKOS_INLINE_FUNCTION bool within_bbox( - const Omega_h::Vector coord, const AABBox& bbox) noexcept -{ - for (int i = 0; i < dim; ++i) { - if (coord[i] < bbox.center[i] - bbox.half_width[i]) - return false; - if (coord[i] > bbox.center[i] + bbox.half_width[i]) - return false; - } - return true; -} - -template -[[nodiscard]] KOKKOS_INLINE_FUNCTION bool bbox_verts_within_simplex( - const AABBox& bbox, const Omega_h::Matrix& coords) -{ - // each dimension has a pair of opposing "walls" - // 2D: { [left, right], [top, bottom] } -> { left, right, top, bottom } - // 3D: { [left, right], [top, bottom], [front, back] } -> { left, ..., back } - std::array bbox_walls{}; - for (int i = 0; i < dim; i++) { - bbox_walls[i * 2] = bbox.center[i] - bbox.half_width[i]; - bbox_walls[i * 2 + 1] = bbox.center[i] + bbox.half_width[i]; - } - - // 1 << dim == 2 ** dim == num vertices in ndim bounding box / hypercube - constexpr unsigned num_verts = 1 << dim; - - // Each vertex is a just a unique combination of walls - // eg [left, bottom] (2D) or [right, top, front] (3) - for (unsigned i = 0; i < num_verts; ++i) { - // conveniently, i acts a bit field representing the current combination - Omega_h::Vector vert; - for (unsigned j = 0; j < dim; ++j) { - // eg 110 = 6 = [left, top, back] - // eg 01 = 1 = [right, top] - vert[j] = (i >> j) & 1 ? bbox_walls[j * 2] : bbox_walls[j * 2 + 1]; - } - auto xi = Omega_h::barycentric_from_global(vert, coords); - if (Omega_h::is_barycentric_inside(xi)) { - return true; - } - } - return false; -} - -/** - * Check if a triangle element represented by 3 coordinates in two dimensions - * intersects with a bounding box - */ -[[nodiscard]] KOKKOS_FUNCTION bool triangle_intersects_bbox( - const Omega_h::Matrix<2, 3>& coords, const AABBox<2>& bbox) -{ - // triangle and grid cell bounding box intersect - if (intersects(triangle_bbox(coords), bbox)) { - // if any of the triangle verts inside of bbox - if (within_bbox<2>(coords[0], bbox) || within_bbox<2>(coords[1], bbox) || - within_bbox<2>(coords[2], bbox)) { - return true; - } - // if any of the bbox verts are within the triangle - if (bbox_verts_within_simplex(bbox, coords)) { - return true; - } - // if any of the triangle's edges intersect with the bounding box - if (line_intersects_bbox(coords[0], coords[1], bbox) || - line_intersects_bbox(coords[1], coords[2], bbox) || - line_intersects_bbox(coords[2], coords[0], bbox)) { - return true; - } - } - return false; -} - -template -[[nodiscard]] KOKKOS_FUNCTION bool simplex_intersects_bbox( - const Omega_h::Matrix& coords, const AABBox& bbox) -{ - return intersects(simplex_bbox(coords), bbox); - // TODO: Add refined cases from triangle_intersects_bbox -} - -namespace detail -{ -/** - * Functor for constructing the mapping from grid cells to intersecting - * triangles. Each row of the resulting CSR structure represents a grid cell and - * each row entry corresponds to an ID of an element that intersects that grid - * cell. - * \Warning since this works on Omega_h meshes, we currently assume each element - * is a 2D simplex (triangle) - * \Warning since this uses Omega_h data which is only available in the - * "Default" Execution space, the should not be used in an alternative EXE space - */ -struct GridTriIntersectionFunctor2D -{ - GridTriIntersectionFunctor2D(Omega_h::Mesh& mesh, - Kokkos::View grid) - : mesh_(mesh), - tris2verts_(mesh_.ask_elem_verts()), - coords_(mesh_.coords()), - grid_(grid), - nelems_(mesh_.nelems()) - { - if (mesh_.dim() != 2) { - std::cerr << "GridTriIntersection2D currently only developed for 2D " - "triangular meshes\n"; - std::terminate(); - } - } - /// Two-pass functor. On the first pass we set the number of grid/triangle - /// intersections. On the second pass we fill the CSR array with the indexes - /// to the triangles intersecting with the current grid cell (row) - KOKKOS_INLINE_FUNCTION - LO operator()(LO row, LO* fill) const - { - auto grid_cell_bbox = grid_(0).GetCellBBOX(row); - LO num_intersections = 0; - // hierarchical parallel may make be very beneficial here... - for (LO elem_idx = 0; elem_idx < nelems_; ++elem_idx) { - const auto elem_tri2verts = - Omega_h::gather_verts<3>(tris2verts_, elem_idx); - // 2d mesh with 2d coords, but 3 triangles - const auto vertex_coords = - Omega_h::gather_vectors<3, 2>(coords_, elem_tri2verts); - if (triangle_intersects_bbox(vertex_coords, grid_cell_bbox)) { - if (fill) { - fill[num_intersections] = elem_idx; - } - ++num_intersections; - } - } - return num_intersections; - } - -private: - Omega_h::Mesh& mesh_; - Omega_h::LOs tris2verts_; - Omega_h::Reals coords_; - Kokkos::View grid_; - -public: - LO nelems_; -}; - -struct GridTriIntersectionFunctor3D -{ - GridTriIntersectionFunctor3D(Omega_h::Mesh& mesh, - Kokkos::View grid) - : mesh_(mesh), - tets2verts_(mesh_.ask_elem_verts()), - coords_(mesh_.coords()), - grid_(grid), - nelems_(mesh_.nelems()) - { - if (mesh_.dim() != 3) { - std::cerr << "GridTriIntersection3D currently only developed for 3D " - "tetrahedral meshes\n"; - std::terminate(); - } - } - /// Two-pass functor. On the first pass we set the number of grid/triangle - /// intersections. On the second pass we fill the CSR array with the indexes - /// to the triangles intersecting with the current grid cell (row) - KOKKOS_INLINE_FUNCTION - LO operator()(LO row, LO* fill) const - { - auto grid_cell_bbox = grid_(0).GetCellBBOX(row); - LO num_intersections = 0; - // hierarchical parallel may make be very beneficial here... - for (LO elem_idx = 0; elem_idx < nelems_; ++elem_idx) { - const auto elem_tet2verts = - Omega_h::gather_verts<4>(tets2verts_, elem_idx); - const auto vertex_coords = - Omega_h::gather_vectors<4, 3>(coords_, elem_tet2verts); - if (simplex_intersects_bbox<3>(vertex_coords, grid_cell_bbox)) { - if (fill) { - fill[num_intersections] = elem_idx; - } - ++num_intersections; - } - } - return num_intersections; - } - -private: - Omega_h::Mesh& mesh_; - Omega_h::LOs tets2verts_; - Omega_h::Reals coords_; - Kokkos::View grid_; - -public: - LO nelems_; -}; - -// num_grid_cells should be result of grid.GetNumCells(), take as argument to -// avoid extra copy of grid from gpu to cpu -Kokkos::Crs -construct_intersection_map_2d(Omega_h::Mesh& mesh, - Kokkos::View grid, - int num_grid_cells) -{ - Kokkos::Crs intersection_map{}; - auto f = detail::GridTriIntersectionFunctor2D{mesh, grid}; - Kokkos::count_and_fill_crs(intersection_map, num_grid_cells, f); - return intersection_map; -} - -Kokkos::Crs -construct_intersection_map_3d(Omega_h::Mesh& mesh, - Kokkos::View grid, - int num_grid_cells) -{ - Kokkos::Crs intersection_map{}; - auto f = detail::GridTriIntersectionFunctor3D{mesh, grid}; - Kokkos::count_and_fill_crs(intersection_map, num_grid_cells, f); - return intersection_map; -} -} // namespace detail - -template -OMEGA_H_INLINE double myreduce(const Omega_h::Vector& x, - Op op) OMEGA_H_NOEXCEPT -{ - auto out = x[0]; - for (int i = 1; i < n; ++i) - out = op(out, x[i]); - return out; -} - -Kokkos::View GridPointSearch2D::operator()( - Kokkos::View points) const -{ - Kokkos::View results("point search result", - points.extent(0)); - auto num_rows = candidate_map_.numRows(); - // needed so that we don't capture this ptr which will be memory error on cuda - auto grid = grid_; - auto candidate_map = candidate_map_; - auto tris2verts = tris2verts_; - auto tris2verts_adj = tris2verts_adj_; - auto tris2edges_adj = tris2edges_adj_; - auto edges2verts_adj = edges2verts_adj_; - auto coords = coords_; - auto tolerances = tolerances_; - Kokkos::parallel_for( - points.extent(0), KOKKOS_LAMBDA(int p) { - Omega_h::Vector<2> point( - std::initializer_list{points(p, 0), points(p, 1)}); - auto cell_id = grid(0).ClosestCellID(point); - assert(cell_id < num_rows && cell_id >= 0); - auto candidates_begin = candidate_map.row_map(cell_id); - auto candidates_end = candidate_map.row_map(cell_id + 1); - - // Track best entities across all candidates to ensure order invariance - Omega_h::Real best_vertex_dist = INFINITY; - LO best_vertex_id = -1; - int best_vertex_tid = -1; // triangle providing barycentric coords - Omega_h::Vector<3> best_vertex_bary{0.0, 0.0, 0.0}; - - Omega_h::Real best_edge_dist = INFINITY; - LO best_edge_id = -1; - int best_edge_tid_min = -1; - int best_edge_tid_max = -1; - Omega_h::Vector<3> best_edge_bary_min{0.0, 0.0, 0.0}; - Omega_h::Vector<3> best_edge_bary_max{0.0, 0.0, 0.0}; - - bool found_inside = false; - LO inside_face_id = -1; - Omega_h::Vector<3> inside_face_bary{0.0, 0.0, 0.0}; - - auto begin = candidate_map.row_map(cell_id); - auto end = candidate_map.row_map(cell_id + 1); - for (auto ii = begin; ii < end; ++ii) { - const int triangleID = candidate_map.entries(ii); - const auto elem_tri2verts = - Omega_h::gather_verts<3>(tris2verts, triangleID); - auto vertex_coords = - Omega_h::gather_vectors<3, 2>(coords, elem_tri2verts); - auto parametric_coords = - Omega_h::barycentric_from_global<2, 2>(point, vertex_coords); - - // Check vertices (hierarchy level 1): compute Euclidean distance - for (int j = 0; j < 3; ++j) { - const int vertexID = tris2verts_adj.ab2b[triangleID * 3 + j]; - const auto v = Omega_h::get_vector<2>(coords, vertexID); - const auto dv = Omega_h::norm(point - v); - if ((dv < best_vertex_dist) || - ((dv == best_vertex_dist) && (vertexID < best_vertex_id))) { - best_vertex_dist = dv; - best_vertex_id = vertexID; - best_vertex_tid = triangleID; - best_vertex_bary = parametric_coords; - } - } - - // Check edges (hierarchy level 2): only if projection falls within - - for (int j = 0; j < 3; ++j) { - const int edgeID = tris2edges_adj.ab2b[triangleID * 3 + j]; - - const int va_id = edges2verts_adj.ab2b[edgeID * 2 + 0]; - const int vb_id = edges2verts_adj.ab2b[edgeID * 2 + 1]; - const auto va = Omega_h::get_vector<2>(coords, va_id); - const auto vb = Omega_h::get_vector<2>(coords, vb_id); - - if (!normal_intersects_segment(va, vb, point)) - continue; - - const auto de = distance_from_line(va, vb, point); - if ((de < best_edge_dist) || - ((de == best_edge_dist) && (edgeID < best_edge_id)) || - ((de == best_edge_dist) && (edgeID == best_edge_id) && - (triangleID < best_edge_tid_min))) { - best_edge_dist = de; - best_edge_id = edgeID; - best_edge_tid_min = triangleID; - best_edge_tid_max = triangleID; - best_edge_bary_min = parametric_coords; - best_edge_bary_max = parametric_coords; - } else if ((de == best_edge_dist) && (edgeID == best_edge_id)) { - // Track the extents of face IDs sharing this nearest edge - if (triangleID < best_edge_tid_min) { - best_edge_tid_min = triangleID; - best_edge_bary_min = parametric_coords; - } - if (triangleID > best_edge_tid_max) { - best_edge_tid_max = triangleID; - best_edge_bary_max = parametric_coords; - } - } - } - - // Check face interior (hierarchy level 3) - if (Omega_h::is_barycentric_inside(parametric_coords)) { - if (!found_inside || (triangleID < inside_face_id)) { - found_inside = true; - inside_face_id = triangleID; - inside_face_bary = parametric_coords; - } - } - } - - const auto vtol = tolerances(0); - const auto etol = tolerances(1); - - // If we found an inside face, compute its edge distance using - // barycentric-only helper to check for edge classification while - // preserving the containing face ID. - Real inside_edge_dist = INFINITY; - int inside_edge_argmin = -1; - Omega_h::Matrix<2, 3> vcoords_in; - // Track Euclidean-nearest edge of the containing face (if any) - LO inside_edge_id = -1; - if (found_inside) { - const auto elem_tri2verts_in = - Omega_h::gather_verts<3>(tris2verts, inside_face_id); - vcoords_in = Omega_h::gather_vectors<3, 2>(coords, elem_tri2verts_in); - // Euclidean distance to the 3 edges of the containing triangle - inside_edge_argmin = -1; - inside_edge_dist = INFINITY; - inside_edge_id = -1; - for (int j = 0; j < 3; ++j) { - const int edgeID = tris2edges_adj.ab2b[inside_face_id * 3 + j]; - const int va_id = edges2verts_adj.ab2b[edgeID * 2 + 0]; - const int vb_id = edges2verts_adj.ab2b[edgeID * 2 + 1]; - const auto va = Omega_h::get_vector<2>(coords, va_id); - const auto vb = Omega_h::get_vector<2>(coords, vb_id); - if (!normal_intersects_segment(va, vb, point)) - continue; - const auto de = distance_from_line(va, vb, point); - if (de < inside_edge_dist) { - inside_edge_dist = de; - inside_edge_argmin = j; - inside_edge_id = edgeID; - } - } - } - - GridPointSearch2D::Result::Dimensionality dim_out = - GridPointSearch2D::Result::Dimensionality::REGION; - LO element_id_out = -1; - Omega_h::Vector<3> bary_out{0.0, 0.0, 0.0}; - - // Apply hierarchy with tolerances - // Points within tolerance are considered "inside" with positive IDs - // Only points with no candidates at all get negative IDs - if (best_vertex_id >= 0 && best_vertex_dist <= vtol) { - // Point within vertex tolerance - still inside the mesh - dim_out = GridPointSearch2D::Result::Dimensionality::VERTEX; - element_id_out = best_vertex_id; - bary_out = best_vertex_bary; - } else if ((best_edge_id >= 0 && best_edge_dist <= etol) || - (found_inside && inside_edge_dist <= etol)) { - // Point within edge tolerance - still inside the mesh - dim_out = GridPointSearch2D::Result::Dimensionality::EDGE; - if (found_inside && inside_edge_dist <= etol && - inside_edge_argmin >= 0) { - element_id_out = inside_edge_id; - bary_out = inside_face_bary; - } else { - element_id_out = best_edge_id; - bary_out = best_edge_bary_min; - } - } else if (found_inside) { - // Point inside face - dim_out = GridPointSearch2D::Result::Dimensionality::FACE; - element_id_out = inside_face_id; - bary_out = inside_face_bary; - } else { - // Outside mesh - beyond tolerance of any entity - // Only negate if we truly have no candidates at all. - if (best_vertex_id >= 0 && - (best_vertex_dist <= best_edge_dist || best_edge_id < 0)) { - dim_out = GridPointSearch2D::Result::Dimensionality::VERTEX; - element_id_out = best_vertex_id; - bary_out = best_vertex_bary; - element_id_out = -element_id_out; - } else if (best_edge_id >= 0) { - dim_out = GridPointSearch2D::Result::Dimensionality::EDGE; - element_id_out = best_edge_id; - bary_out = best_edge_bary_min; - element_id_out = -element_id_out; - } else { - // No candidates at all: both IDs stay -1 - } - } - - results(p) = GridPointSearch2D::Result{dim_out, element_id_out, bary_out}; - }); - - return results; -} - -GridPointSearch2D::GridPointSearch2D(Omega_h::Mesh& mesh, LO Nx, LO Ny) - : GridPointSearch2D(mesh, Nx, Ny, - PointSearchTolerances{"point search 2d tolerances"}) -{ - Kokkos::deep_copy(tolerances_, 1E-12); -} - -GridPointSearch2D::GridPointSearch2D(Omega_h::Mesh& mesh, LO Nx, LO Ny, - const PointSearchTolerances& tolerances) - : PointLocalizationSearch(tolerances), mesh_(mesh) -{ - auto mesh_bbox = Omega_h::get_bounding_box<2>(&mesh); - auto grid_h = Kokkos::create_mirror_view(grid_); - grid_h(0) = - Uniform2DGrid{.edge_length = {mesh_bbox.max[0] - mesh_bbox.min[0], - mesh_bbox.max[1] - mesh_bbox.min[1]}, - .bot_left = {mesh_bbox.min[0], mesh_bbox.min[1]}, - .divisions = {Nx, Ny}}; - Kokkos::deep_copy(grid_, grid_h); - // Determine inflation radius from tolerances (max of vertex/edge tol) - auto tol_h = Kokkos::create_mirror_view(tolerances_); - Kokkos::deep_copy(tol_h, tolerances_); - candidate_map_ = - detail::construct_intersection_map_2d(mesh, grid_, grid_h(0).GetNumCells()); - coords_ = mesh.coords(); - tris2verts_ = mesh.ask_elem_verts(); - tris2edges_adj_ = mesh.ask_down(Omega_h::FACE, Omega_h::EDGE); - tris2verts_adj_ = mesh.ask_down(Omega_h::FACE, Omega_h::VERT); - edges2verts_adj_ = mesh.ask_down(Omega_h::EDGE, Omega_h::VERT); - edges2faces_up_ = mesh.ask_up(Omega_h::EDGE, Omega_h::FACE); - verts2faces_up_ = mesh.ask_up(Omega_h::VERT, Omega_h::FACE); -} - -LO GridPointSearch2D::GetOwningElementId(const Result& result) -{ - const LO query_id = - (result.element_id < 0) ? -result.element_id : result.element_id; - return pcms::GetOwningElementId( - mesh_, 2, static_cast(result.dimensionality), query_id); -} - -Kokkos::View GridPointSearch2D::GetOwningElementIds( - Kokkos::View results) -{ - Kokkos::View owners("point search owning face ids", results.extent(0)); - auto edges2faces_up = edges2faces_up_; - auto verts2faces_up = verts2faces_up_; - constexpr int mesh_dim = 2; - Kokkos::parallel_for( - results.extent(0), KOKKOS_LAMBDA(const LO i) { - const auto result = results(i); - LO element_id = result.element_id; - if (element_id < 0) - element_id = -element_id; - - LO owner = -1; - if (element_id >= 0) { - if (result.dimensionality == Result::Dimensionality::FACE) { - owner = GetOwningElementIdFromAdj( - edges2faces_up, Result::Dimensionality::FACE, - Result::Dimensionality::FACE, element_id); - } else if (result.dimensionality == Result::Dimensionality::EDGE) { - owner = GetOwningElementIdFromAdj( - edges2faces_up, Result::Dimensionality::EDGE, - Result::Dimensionality::FACE, element_id); - } else if (result.dimensionality == Result::Dimensionality::VERTEX) { - owner = GetOwningElementIdFromAdj( - verts2faces_up, Result::Dimensionality::VERTEX, - Result::Dimensionality::FACE, element_id); - } - } - owners(i) = owner; - }); - return owners; -} - -Kokkos::View GridPointSearch3D::operator()( - Kokkos::View points) const -{ - Kokkos::View results("point search result", - points.extent(0)); - auto num_rows = candidate_map_.numRows(); - // needed so that we don't capture this ptr which will be memory error on cuda - auto grid = grid_; - auto candidate_map = candidate_map_; - auto tris2verts = tris2verts_; - auto tris2verts_adj = tris2verts_adj_; - auto tris2edges_adj = tris2edges_adj_; - auto edges2verts_adj = edges2verts_adj_; - auto coords = coords_; - Kokkos::parallel_for( - points.extent(0), KOKKOS_LAMBDA(int p) { - Omega_h::Vector point; - for (int i = 0; i < DIM; ++i) { - point[i] = points(p, i); - } - - auto cell_id = grid(0).ClosestCellID(point); - assert(cell_id < num_rows && cell_id >= 0); - auto candidates_begin = candidate_map.row_map(cell_id); - auto candidates_end = candidate_map.row_map(cell_id + 1); - bool found = false; - - auto nearest_triangle = candidates_begin; - auto dimensionality = GridPointSearch3D::Result::Dimensionality::EDGE; - Omega_h::Vector parametric_coords_to_nearest; - // create array that's size of number of candidates x num coords to store - // parametric inversion - for (auto i = candidates_begin; i < candidates_end; ++i) { - const int triangleID = candidate_map.entries(i); - const auto elem_tri2verts = - Omega_h::gather_verts(tris2verts, triangleID); - auto vertex_coords = - Omega_h::gather_vectors(coords, elem_tri2verts); - auto parametric_coords = - Omega_h::barycentric_from_global(point, vertex_coords); - - if (Omega_h::is_barycentric_inside(parametric_coords)) { - results(p) = GridPointSearch3D::Result{ - GridPointSearch3D::Result::Dimensionality::REGION, triangleID, - parametric_coords}; - found = true; - break; - } - - // TODO: Get nearest element if no tetrahedron found - } - if (!found) { - LO nearest_elem = candidate_map.entries(nearest_triangle); - results(p) = GridPointSearch3D::Result{dimensionality, -nearest_elem, - parametric_coords_to_nearest}; - } - }); - - return results; -} - -GridPointSearch3D::GridPointSearch3D(Omega_h::Mesh& mesh, LO Nx, LO Ny, LO Nz) - : GridPointSearch3D(mesh, Nx, Ny, Nz, - PointSearchTolerances{"point search 3d tolerances"}) -{ - Kokkos::deep_copy(tolerances_, 0); -} - -GridPointSearch3D::GridPointSearch3D(Omega_h::Mesh& mesh, LO Nx, LO Ny, LO Nz, - const PointSearchTolerances& tolerances) - : PointLocalizationSearch(tolerances), mesh_(mesh) -{ - auto mesh_bbox = Omega_h::get_bounding_box<3>(&mesh); - auto grid_h = Kokkos::create_mirror_view(grid_); - - std::array edge_lengths{}; - std::array bot_left{}; - - for (int i = 0; i < DIM; ++i) { - edge_lengths[i] = mesh_bbox.max[i] - mesh_bbox.min[i]; - bot_left[i] = mesh_bbox.min[i]; - } - - grid_h(0) = Uniform3DGrid{.edge_length = edge_lengths, - .bot_left = bot_left, - .divisions = {Nx, Ny, Nz}}; - - Kokkos::deep_copy(grid_, grid_h); - candidate_map_ = - detail::construct_intersection_map_3d(mesh, grid_, grid_h(0).GetNumCells()); - coords_ = mesh.coords(); - tris2verts_ = mesh.ask_elem_verts(); - tris2edges_adj_ = mesh.ask_down(Omega_h::FACE, Omega_h::EDGE); - tris2verts_adj_ = mesh.ask_down(Omega_h::FACE, Omega_h::VERT); - edges2verts_adj_ = mesh.ask_down(Omega_h::EDGE, Omega_h::VERT); - verts2regions_up_ = mesh.ask_up(Omega_h::VERT, Omega_h::REGION); - edges2regions_up_ = mesh.ask_up(Omega_h::EDGE, Omega_h::REGION); - faces2regions_up_ = mesh.ask_up(Omega_h::FACE, Omega_h::REGION); -} - -LO GridPointSearch3D::GetOwningElementId(const Result& result) -{ - const LO query_id = - (result.element_id < 0) ? -result.element_id : result.element_id; - return pcms::GetOwningElementId( - mesh_, 3, static_cast(result.dimensionality), query_id); -} - -Kokkos::View GridPointSearch3D::GetOwningElementIds( - Kokkos::View results) -{ - Kokkos::View owners("point search owning region ids", results.extent(0)); - auto verts2regions_up = verts2regions_up_; - auto edges2regions_up = edges2regions_up_; - auto faces2regions_up = faces2regions_up_; - constexpr int mesh_dim = 3; - Kokkos::parallel_for( - results.extent(0), KOKKOS_LAMBDA(const LO i) { - const auto result = results(i); - LO element_id = result.element_id; - if (element_id < 0) - element_id = -element_id; - - LO owner = -1; - if (element_id >= 0) { - if (result.dimensionality == Result::Dimensionality::REGION) { - owner = GetOwningElementIdFromAdj( - faces2regions_up, Result::Dimensionality::REGION, - Result::Dimensionality::REGION, element_id); - } else if (result.dimensionality == Result::Dimensionality::FACE) { - owner = GetOwningElementIdFromAdj( - faces2regions_up, Result::Dimensionality::FACE, - Result::Dimensionality::REGION, element_id); - } else if (result.dimensionality == Result::Dimensionality::EDGE) { - owner = GetOwningElementIdFromAdj( - edges2regions_up, Result::Dimensionality::EDGE, - Result::Dimensionality::REGION, element_id); - } else if (result.dimensionality == Result::Dimensionality::VERTEX) { - owner = GetOwningElementIdFromAdj( - verts2regions_up, Result::Dimensionality::VERTEX, - Result::Dimensionality::REGION, element_id); - } - } - owners(i) = owner; - }); - return owners; -} -} // namespace pcms diff --git a/src/pcms/localization/point_search.h b/src/pcms/localization/point_search.h deleted file mode 100644 index b1e0a9ef0..000000000 --- a/src/pcms/localization/point_search.h +++ /dev/null @@ -1,201 +0,0 @@ -#ifndef PCMS_COUPLING_POINT_SEARCH_H -#define PCMS_COUPLING_POINT_SEARCH_H -#include - -#include -#include -#include -#include - -#include "pcms/utility/types.h" -#include "pcms/utility/uniform_grid.h" -#include "pcms/utility/bounding_box.h" - -namespace pcms -{ - -/** - * Get the owning element (face for 2D, region for 3D) for a given entity. - * - * Given an entity of dimension `entity_dim` with ID `element_id`, returns the - * smallest ID of the owning element of dimension `mesh_dim` (the mesh's - * top-level dimension). - */ -LO GetOwningElementId(Omega_h::Mesh& mesh, int mesh_dim, int entity_dim, - LO element_id); - -template -KOKKOS_INLINE_FUNCTION LO -GetOwningElementIdFromAdj(const Omega_h::Adj& upward_adj, DimEnum entity_dim, - DimEnum target_dim, LO element_id) -{ - if (element_id < 0) - return -1; - - // If entity is already at target dimension, return it directly - if (static_cast(entity_dim) == static_cast(target_dim)) - return element_id; - - const auto begin = upward_adj.a2ab[element_id]; - const auto end = upward_adj.a2ab[element_id + 1]; - if (begin >= end) - return -1; - - // Find the smallest owning element ID - LO owner = upward_adj.ab2b[begin]; - for (auto i = begin + 1; i < end; ++i) { - const LO candidate = upward_adj.ab2b[i]; - if (candidate < owner) - owner = candidate; - } - return owner; -} - -// -// TODO take a bounding box as we may want a bbox that's bigger than the mesh! -// this function is in the public header for testing, but should not be directly -// used -namespace detail -{ -Kokkos::Crs -construct_intersection_map_2d(Omega_h::Mesh& mesh, - Kokkos::View grid, - int num_grid_cells); -} - -[[nodiscard]] KOKKOS_FUNCTION bool triangle_intersects_bbox( - const Omega_h::Matrix<2, 3>& coords, const AABBox<2>& bbox); - -template -class PointLocalizationSearch -{ -public: - struct Result - { - enum class Dimensionality - { - VERTEX = 0, - EDGE = 1, - FACE = 2, - REGION = 3 - }; - - Dimensionality dimensionality; - LO element_id; // ID of the actual entity (vertex/edge/face/region) - Omega_h::Vector parametric_coords; - }; - - static constexpr auto DIM = dim; - - using PointSearchTolerances = Kokkos::View; - - explicit PointLocalizationSearch(const PointSearchTolerances& tolerances) - : tolerances_(tolerances) - { - assert(tolerances_.is_allocated()); - } - - virtual Kokkos::View operator()( - Kokkos::View point) const = 0; - - /** - * This function provides a temporary solution to retrieve the original - * behavior of the point search, which previously returned the face ID - * regardless of which entity the search result belonged to. Many parts of - * the codebase still rely on this legacy behavior. After updating the point - * search to return the exact entity, this function can be called to retrieve - * the old behavior and ensure correctness. Long term, the implementation - * should be updated to properly handle the new result. - */ - [[nodiscard]] virtual LO GetOwningElementId(const Result& result) = 0; - [[nodiscard]] virtual Kokkos::View GetOwningElementIds( - Kokkos::View results) = 0; - virtual ~PointLocalizationSearch() = default; - -protected: - PointSearchTolerances tolerances_; -}; - -using PointLocalizationSearch2D = PointLocalizationSearch<2>; -using PointLocalizationSearch3D = PointLocalizationSearch<3>; - -class GridPointSearch2D : public PointLocalizationSearch2D -{ - using CandidateMapT = - Kokkos::Crs; - -public: - using Result = PointLocalizationSearch2D::Result; - - GridPointSearch2D(Omega_h::Mesh& mesh, LO Nx, LO Ny); - GridPointSearch2D(Omega_h::Mesh& mesh, LO Nx, LO Ny, - const PointSearchTolerances& tolerances); - - /** - * given a point in global coordinates give the id of the triangle that the - * point lies within and the parametric coordinate of the point within the - * triangle. If the point does not lie within any triangle element. Then the - * id will be a negative number and (TODO) will return a negative id of the - * closest element - */ - Kokkos::View operator()( - Kokkos::View point) const override; - [[nodiscard]] LO GetOwningElementId(const Result& result) override; - [[nodiscard]] Kokkos::View GetOwningElementIds( - Kokkos::View results) override; - -private: - Omega_h::Mesh mesh_; - Omega_h::Adj tris2edges_adj_; - Omega_h::Adj tris2verts_adj_; - Omega_h::Adj edges2verts_adj_; - Omega_h::Adj edges2faces_up_; - Omega_h::Adj verts2faces_up_; - Kokkos::View grid_{"uniform grid"}; - CandidateMapT candidate_map_; - Omega_h::LOs tris2verts_; - Omega_h::Reals coords_; -}; - -class GridPointSearch3D : public PointLocalizationSearch3D -{ - using CandidateMapT = - Kokkos::Crs; - -public: - using Result = PointLocalizationSearch3D::Result; - - GridPointSearch3D(Omega_h::Mesh& mesh, LO Nx, LO Ny, LO Nz); - GridPointSearch3D(Omega_h::Mesh& mesh, LO Nx, LO Ny, LO Nz, - const PointSearchTolerances& tolerances); - - /** - * Given a point in global coordinates, returns the id of the tetrahedron (3D - * element) that the point lies within and the parametric coordinate of the - * point within the tetrahedron. If the point does not lie within any - * tetrahedron element, then the id will be a negative number and (TODO) will - * return a negative id of the closest element. - */ - Kokkos::View operator()( - Kokkos::View point) const override; - [[nodiscard]] LO GetOwningElementId(const Result& result) override; - [[nodiscard]] Kokkos::View GetOwningElementIds( - Kokkos::View results) override; - -private: - Omega_h::Mesh mesh_; - Omega_h::Adj tris2edges_adj_; - Omega_h::Adj tris2verts_adj_; - Omega_h::Adj edges2verts_adj_; - Omega_h::Adj verts2regions_up_; - Omega_h::Adj edges2regions_up_; - Omega_h::Adj faces2regions_up_; - Kokkos::View[1]> grid_{"uniform grid"}; - CandidateMapT candidate_map_; - Omega_h::LOs tris2verts_; - Omega_h::Reals coords_; - Real fuzz_; -}; - -} // namespace pcms -#endif // PCMS_COUPLING_POINT_SEARCH_H diff --git a/src/pcms/localization/queue_visited.hpp b/src/pcms/localization/queue_visited.hpp index 8d768eec3..bab16e69c 100644 --- a/src/pcms/localization/queue_visited.hpp +++ b/src/pcms/localization/queue_visited.hpp @@ -7,8 +7,7 @@ #include #include #include -#define MAX_SIZE_QUEUE 500 -#define MAX_SIZE_TRACK 800 +#include "pcms/configuration.h" namespace pcms { @@ -16,7 +15,7 @@ namespace pcms class Queue { private: - Omega_h::LO queue_array[MAX_SIZE_QUEUE]; + Omega_h::LO queue_array[PCMS_INTERSECTION_QUEUE_SIZE]; int first = 0, last = -1, count = 0; public: @@ -45,7 +44,7 @@ class Queue class Track { private: - Omega_h::LO tracking_array[MAX_SIZE_TRACK]; + Omega_h::LO tracking_array[PCMS_INTERSECTION_TRACK_SIZE]; int first = 0, last = -1, count = 0; public: @@ -68,11 +67,11 @@ class Track OMEGA_H_INLINE void Queue::push_back(const int& item) { - if (count == MAX_SIZE_QUEUE) { + if (count == PCMS_INTERSECTION_QUEUE_SIZE) { printf("queue is full %d\n", count); return; } - last = (last + 1) % MAX_SIZE_QUEUE; + last = (last + 1) % PCMS_INTERSECTION_QUEUE_SIZE; queue_array[last] = item; count++; } @@ -84,7 +83,7 @@ void Queue::pop_front() printf("queue is empty\n"); return; } - first = (first + 1) % MAX_SIZE_QUEUE; + first = (first + 1) % PCMS_INTERSECTION_QUEUE_SIZE; count--; } @@ -103,19 +102,19 @@ bool Queue::isEmpty() const OMEGA_H_INLINE bool Queue::isFull() const { - return count == MAX_SIZE_QUEUE; + return count == PCMS_INTERSECTION_QUEUE_SIZE; } OMEGA_H_INLINE bool Track::push_back(const int& item) { - if (count == MAX_SIZE_TRACK) { + if (count == PCMS_INTERSECTION_TRACK_SIZE) { // Visited buffer is full. Report failure so callers can stop expanding the // search; otherwise new vertices can never be marked visited and the BFS // re-queues them forever (infinite loop). return false; } - last = (last + 1) % MAX_SIZE_TRACK; + last = (last + 1) % PCMS_INTERSECTION_TRACK_SIZE; tracking_array[last] = item; count++; return true; @@ -126,7 +125,7 @@ bool Track::notVisited(const int& item) { int id; for (int i = 0; i < count; ++i) { - id = (first + i) % MAX_SIZE_TRACK; + id = (first + i) % PCMS_INTERSECTION_TRACK_SIZE; if (tracking_array[id] == item) { return false; } diff --git a/src/pcms/localization/uniform_grid_localization.cpp b/src/pcms/localization/uniform_grid_localization.cpp new file mode 100644 index 000000000..f22912704 --- /dev/null +++ b/src/pcms/localization/uniform_grid_localization.cpp @@ -0,0 +1,1267 @@ +#include "uniform_grid_localization.hpp" +#include +#include +#include + +// From +// https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Vector_formulation +template +KOKKOS_INLINE_FUNCTION Omega_h::Real distance_from_line( + const Omega_h::Vector& a, const Omega_h::Vector& b, + const Omega_h::Vector& p) +{ + Omega_h::Vector n = Omega_h::normalize(b - a); + Omega_h::Vector ap = a - p; + return Omega_h::norm(ap - (ap * n) * n); +} + +template +KOKKOS_INLINE_FUNCTION bool normal_intersects_segment( + const Omega_h::Vector a, const Omega_h::Vector b, + const Omega_h::Vector p) +{ + auto ab = b - a; + auto ba = a - b; + auto ap = p - a; + auto bp = p - b; + return (ap * ab) * (bp * ba) >= 0; +} + +/** + * Barycentric coordinates of a point in a simplex together with the factors + * that convert a Cartesian distance from facet i into a band on coordinate i: + * |lambda[i]| <= tau * grad_norm[i] iff the point is within distance tau of + * the plane of the facet opposite vertex i. + */ +template +struct BarycentricWithScales +{ + Omega_h::Vector lambda; + Omega_h::Vector grad_norm; + + /** + * Signed Cartesian distance to the most violated facet plane: non-negative + * inside the simplex, -tau on the boundary of the tolerance band. + */ + KOKKOS_INLINE_FUNCTION Omega_h::Real SignedPlaneDistance() const + { + auto d = lambda[0] / grad_norm[0]; + for (int i = 1; i <= Dim; ++i) { + d = Kokkos::fmin(d, lambda[i] / grad_norm[i]); + } + return d; + } +}; + +template +KOKKOS_INLINE_FUNCTION BarycentricWithScales barycentric_with_scales( + const Omega_h::Few, Dim + 1>& vertex_coords, + const Omega_h::Vector& point) +{ + // rows of the inverse basis are the gradients of lambda_1..lambda_Dim; + // Omega_h matrices are stored by column, so row j is inv[k][j] over k + const auto inv = + Omega_h::pseudo_invert(Omega_h::simplex_basis(vertex_coords)); + BarycentricWithScales result; + result.lambda = Omega_h::form_barycentric(inv * (point - vertex_coords[0])); + auto grad0 = Omega_h::zero_vector(); + for (int j = 0; j < Dim; ++j) { + Omega_h::Vector row; + for (int k = 0; k < Dim; ++k) { + row[k] = inv[k][j]; + } + result.grad_norm[j + 1] = Omega_h::norm(row); + grad0 = grad0 - row; + } + result.grad_norm[0] = Omega_h::norm(grad0); + return result; +} + +/** + * Barycentric coordinates of a point in a simplex together with the factors + * that convert a Cartesian distance from facet i into a band on coordinate i: + * |lambda[i]| <= tau * grad_norm[i] iff the point is within distance tau of + * the plane of the facet opposite vertex i. + */ +template +struct BarycentricWithScales +{ + Omega_h::Vector lambda; + Omega_h::Vector grad_norm; + + /** + * Signed Cartesian distance to the most violated facet plane: non-negative + * inside the simplex, -tau on the boundary of the tolerance band. + */ + KOKKOS_INLINE_FUNCTION Omega_h::Real SignedPlaneDistance() const + { + auto d = lambda[0] / grad_norm[0]; + for (int i = 1; i <= Dim; ++i) { + d = Kokkos::fmin(d, lambda[i] / grad_norm[i]); + } + return d; + } +}; + +template +KOKKOS_INLINE_FUNCTION BarycentricWithScales barycentric_with_scales( + const Omega_h::Few, Dim + 1>& vertex_coords, + const Omega_h::Vector& point) +{ + // rows of the inverse basis are the gradients of lambda_1..lambda_Dim; + // Omega_h matrices are stored by column, so row j is inv[k][j] over k + const auto inv = + Omega_h::pseudo_invert(Omega_h::simplex_basis(vertex_coords)); + BarycentricWithScales result; + result.lambda = Omega_h::form_barycentric(inv * (point - vertex_coords[0])); + auto grad0 = Omega_h::zero_vector(); + for (int j = 0; j < Dim; ++j) { + Omega_h::Vector row; + for (int k = 0; k < Dim; ++k) { + row[k] = inv[k][j]; + } + result.grad_norm[j + 1] = Omega_h::norm(row); + grad0 = grad0 - row; + } + result.grad_norm[0] = Omega_h::norm(grad0); + return result; +} + +namespace pcms +{ + +KOKKOS_INLINE_FUNCTION +AABBox<2> triangle_bbox(const Omega_h::Matrix<2, 3>& coords) +{ + Kokkos::Array max{coords(0, 0), coords(1, 0)}; + Kokkos::Array min{coords(0, 0), coords(1, 0)}; + for (int i = 1; i < 3; ++i) { + max[0] = std::fmax(max[0], coords(0, i)); + max[1] = std::fmax(max[1], coords(1, i)); + min[0] = std::fmin(min[0], coords(0, i)); + min[1] = std::fmin(min[1], coords(1, i)); + } + return {.center = {(max[0] + min[0]) / 2.0, (max[1] + min[1]) / 2.0}, + .half_width = {(max[0] - min[0]) / 2.0, (max[1] - min[1]) / 2.0}}; +} + +template +KOKKOS_INLINE_FUNCTION AABBox simplex_bbox( + const Omega_h::Matrix& coords) +{ + Kokkos::Array max; + Kokkos::Array min; + for (int j = 0; j < dim; ++j) { + max[j] = coords(j, 0); + min[j] = coords(j, 0); + } + for (int i = 1; i < dim + 1; ++i) { + for (int j = 0; j < dim; ++j) { + max[j] = std::fmax(max[j], coords(j, i)); + min[j] = std::fmin(min[j], coords(j, i)); + } + } + + Kokkos::Array center; + Kokkos::Array half_width; + + for (int j = 0; j < dim; ++j) { + center[j] = (max[j] + min[j]) / 2.0; + half_width[j] = (max[j] - min[j]) / 2.0; + } + + return {.center = center, .half_width = half_width}; +} + +// Liang, You-Dong, and B. A. Barsky. “A New Concept and Method for Line +// Clipping.” ACM Transactions on Graphics 3, no. 1 (January 1984): 1–22. +// https://doi.org/10.1145/357332.357333. +KOKKOS_INLINE_FUNCTION +bool clipt(Real p, Real q, Real& t0, Real& t1) +{ + if (p < 0) { + auto r = q / p; + if (r > t1) { + return false; + } + if (r > t0) { + t0 = r; + } + } else if (p > 0) { + auto r = q / p; + if (r < t0) { + return false; + } + if (r < t1) { + t1 = r; + } + } + // p==0 + else { + if (q < 0) { + return false; + } + } + return true; +} +// Liang, You-Dong, and B. A. Barsky. “A New Concept and Method for Line +// Clipping.” ACM Transactions on Graphics 3, no. 1 (January 1984): 1–22. +// https://doi.org/10.1145/357332.357333. +KOKKOS_INLINE_FUNCTION +bool line_intersects_bbox(const Omega_h::Vector<2>& p0, + const Omega_h::Vector<2>& p1, const AABBox<2>& bbox) +{ + Real t0 = 0; + Real t1 = 1; + auto xleft = bbox.center[0] - bbox.half_width[0]; + auto deltax = p1[0] - p0[0]; + if (clipt(-deltax, p0[0] - xleft, t0, t1)) { + auto xright = bbox.center[0] + bbox.half_width[0]; + if (clipt(deltax, xright - p0[0], t0, t1)) { + auto deltay = p1[1] - p0[1]; + auto ybottom = bbox.center[1] - bbox.half_width[1]; + if (clipt(-deltay, p0[1] - ybottom, t0, t1)) { + auto ytop = bbox.center[1] + bbox.half_width[1]; + if (clipt(deltay, ytop - p0[1], t0, t1)) { + // full liang-barksy algorithm computes new x-y coordinates here, but + // we just need to check intersections + return true; + } + } + } + } + return false; +} + +template +[[nodiscard]] KOKKOS_INLINE_FUNCTION bool within_bbox( + const Omega_h::Vector coord, const AABBox& bbox) noexcept +{ + for (int i = 0; i < dim; ++i) { + if (coord[i] < bbox.center[i] - bbox.half_width[i]) + return false; + if (coord[i] > bbox.center[i] + bbox.half_width[i]) + return false; + } + return true; +} + +template +[[nodiscard]] KOKKOS_INLINE_FUNCTION bool bbox_verts_within_simplex( + const AABBox& bbox, const Omega_h::Matrix& coords) +{ + // each dimension has a pair of opposing "walls" + // 2D: { [left, right], [top, bottom] } -> { left, right, top, bottom } + // 3D: { [left, right], [top, bottom], [front, back] } -> { left, ..., back } + Kokkos::Array bbox_walls{}; + for (int i = 0; i < dim; i++) { + bbox_walls[i * 2] = bbox.center[i] - bbox.half_width[i]; + bbox_walls[i * 2 + 1] = bbox.center[i] + bbox.half_width[i]; + } + + // 1 << dim == 2 ** dim == num vertices in ndim bounding box / hypercube + constexpr unsigned num_verts = 1 << dim; + + // Each vertex is a just a unique combination of walls + // eg [left, bottom] (2D) or [right, top, front] (3) + for (unsigned i = 0; i < num_verts; ++i) { + // conveniently, i acts a bit field representing the current combination + Omega_h::Vector vert; + for (unsigned j = 0; j < dim; ++j) { + // eg 110 = 6 = [left, top, back] + // eg 01 = 1 = [right, top] + vert[j] = (i >> j) & 1 ? bbox_walls[j * 2] : bbox_walls[j * 2 + 1]; + } + auto xi = Omega_h::barycentric_from_global(vert, coords); + if (Omega_h::is_barycentric_inside(xi)) { + return true; + } + } + return false; +} + +/** + * Check if a triangle element represented by 3 coordinates in two dimensions + * intersects with a bounding box + */ +[[nodiscard]] KOKKOS_FUNCTION bool triangle_intersects_bbox( + const Omega_h::Matrix<2, 3>& coords, const AABBox<2>& bbox) +{ + // triangle and grid cell bounding box intersect + if (intersects(triangle_bbox(coords), bbox)) { + // if any of the triangle verts inside of bbox + if (within_bbox<2>(coords[0], bbox) || within_bbox<2>(coords[1], bbox) || + within_bbox<2>(coords[2], bbox)) { + return true; + } + // if any of the bbox verts are within the triangle + if (bbox_verts_within_simplex(bbox, coords)) { + return true; + } + // if any of the triangle's edges intersect with the bounding box + if (line_intersects_bbox(coords[0], coords[1], bbox) || + line_intersects_bbox(coords[1], coords[2], bbox) || + line_intersects_bbox(coords[2], coords[0], bbox)) { + return true; + } + } + return false; +} + +template +[[nodiscard]] KOKKOS_FUNCTION bool simplex_intersects_bbox( + const Omega_h::Matrix& coords, const AABBox& bbox) +{ + return intersects(simplex_bbox(coords), bbox); + // TODO: Add refined cases from triangle_intersects_bbox +} + +namespace detail +{ +/** + * Functor for constructing the mapping from grid cells to intersecting + * triangles. Each row of the resulting CSR structure represents a grid cell and + * each row entry corresponds to an ID of an element that intersects that grid + * cell. + * \Warning since this works on Omega_h meshes, we currently assume each element + * is a 2D simplex (triangle) + * \Warning since this uses Omega_h data which is only available in the + * "Default" Execution space, the should not be used in an alternative EXE space + */ +/// The bbox-overlap predicate for a simplex against one grid cell. 2D applies +/// the refined edge tests, 3D is a plain bbox overlap; both match what the +/// previous cell-major functors used, so candidate lists are unchanged. +template +[[nodiscard]] KOKKOS_INLINE_FUNCTION bool simplex_overlaps_cell( + const Omega_h::Matrix& coords, const AABBox& bbox) +{ + if constexpr (Dim == 2) { + return triangle_intersects_bbox(coords, bbox); + } else { + return simplex_intersects_bbox(coords, bbox); + } +} + +/** + * Functor for the element -> grid cell map, the transpose of what the search + * needs. Row `elem` lists the cells that element overlaps. + * + * This is the element-major inverse of the old cell-major functor: rather than + * testing every element against a cell, each element only visits the cells its + * own bounding box spans. That makes construction O(nelems * cells_per_elem) + * instead of O(num_cells * nelems). + */ +template +struct ElemCellOverlapFunctor +{ + ElemCellOverlapFunctor(Omega_h::Mesh& mesh, + Kokkos::View[1]> grid) + : elems2verts_(mesh.ask_elem_verts()), coords_(mesh.coords()), grid_(grid) + { + if (mesh.dim() != Dim) { + std::cerr << "ElemCellOverlapFunctor: mesh dimension does not match the " + "functor dimension\n"; + std::terminate(); + } + } + + /// Two-pass functor. On the first pass we count the cells this element + /// overlaps; on the second we fill the CSR row with their IDs. + KOKKOS_INLINE_FUNCTION + LO operator()(LO elem, LO* fill) const + { + const auto elem_verts = Omega_h::gather_verts(elems2verts_, elem); + const auto vertex_coords = + Omega_h::gather_vectors(coords_, elem_verts); + const auto& grid = grid_(0); + + // Per-axis inclusive range of cells the element's bbox spans, widened by + // one cell on each side. The overlap predicate treats a shared boundary + // plane as an intersection, so a cell that only touches the bbox counts as + // a candidate and would be missed by the unwidened range -- which happens + // constantly on a structured mesh whose vertices land on grid lines. One + // cell is enough: a cell two or more away cannot touch the bbox at all. + // The predicate below rejects whatever the widening over-includes. + Kokkos::Array lo, hi, idx; + for (int d = 0; d < Dim; ++d) { + Real min = vertex_coords[0][d]; + Real max = vertex_coords[0][d]; + for (int v = 1; v < Dim + 1; ++v) { + min = Kokkos::min(min, vertex_coords[v][d]); + max = Kokkos::max(max, vertex_coords[v][d]); + } + lo[d] = Kokkos::max(grid.AxisCellIndex(d, min) - 1, 0); + hi[d] = + Kokkos::min(grid.AxisCellIndex(d, max) + 1, grid.divisions[d] - 1); + idx[d] = lo[d]; + } + + LO num_overlaps = 0; + while (true) { + const LO cell = grid.CellIndexFromAxisIndices(idx); + if (simplex_overlaps_cell(vertex_coords, grid.GetCellBBOX(cell))) { + if (fill) { + fill[num_overlaps] = cell; + } + ++num_overlaps; + } + // odometer over the inclusive range [lo, hi] + int d = 0; + for (; d < Dim; ++d) { + if (++idx[d] <= hi[d]) { + break; + } + idx[d] = lo[d]; + } + if (d == Dim) { + break; + } + } + return num_overlaps; + } + +private: + Omega_h::LOs elems2verts_; + Omega_h::Reals coords_; + Kokkos::View[1]> grid_; +}; + +/** + * Build the grid cell -> element candidate map. + * + * Constructed as the transpose of the cheap element -> cell map. Kokkos' + * transpose_crs cannot be used here: it sizes the output row count from + * in.numRows() (the element count), which both over-allocates the row map and + * writes out of bounds once num_grid_cells exceeds the element count. + */ +template +Kokkos::Crs +construct_intersection_map(Omega_h::Mesh& mesh, + Kokkos::View[1]> grid, + int num_grid_cells) +{ + using CrsT = Kokkos::Crs; + const auto nelems = mesh.nelems(); + + CrsT elem2cells{}; + Kokkos::count_and_fill_crs(elem2cells, nelems, + ElemCellOverlapFunctor{mesh, grid}); + + Kokkos::View counts("candidate_counts", num_grid_cells); + Kokkos::parallel_for( + "count_cell_candidates", elem2cells.entries.size(), KOKKOS_LAMBDA(int i) { + Kokkos::atomic_inc(&counts(elem2cells.entries(i))); + }); + + CrsT cell2elems{}; + Kokkos::get_crs_row_map_from_counts(cell2elems.row_map, counts, + "candidate_row_map"); + cell2elems.entries = + CrsT::entries_type("candidate_entries", elem2cells.entries.size()); + + Kokkos::View filled("candidate_fill_offsets", num_grid_cells); + Kokkos::parallel_for( + "fill_cell_candidates", nelems, KOKKOS_LAMBDA(LO elem) { + for (auto j = elem2cells.row_map(elem); j < elem2cells.row_map(elem + 1); + ++j) { + const LO cell = elem2cells.entries(j); + const auto slot = Kokkos::atomic_fetch_add(&filled(cell), 1); + cell2elems.entries(cell2elems.row_map(cell) + slot) = elem; + } + }); + + // The atomic scatter leaves each row in arbitrary order, where the old + // cell-major loop emitted ascending element IDs. Restore that ordering: the + // query takes the first candidate that contains the point, so ordering + // decides which element wins when a point sits on a shared face, and the + // out-of-bounds path reports the row's first candidate. Without this the + // search results would vary run to run. Rows are short, so insertion sort. + Kokkos::parallel_for( + "sort_cell_candidates", num_grid_cells, KOKKOS_LAMBDA(LO cell) { + const auto begin = cell2elems.row_map(cell); + const auto end = cell2elems.row_map(cell + 1); + for (auto i = begin + 1; i < end; ++i) { + const LO value = cell2elems.entries(i); + auto j = i; + for (; j > begin && cell2elems.entries(j - 1) > value; --j) { + cell2elems.entries(j) = cell2elems.entries(j - 1); + } + cell2elems.entries(j) = value; + } + }); + Kokkos::fence(); + return cell2elems; +} + +/** + * Cell-major brute-force build: for every grid cell, test every element. + * + * This is the implementation `construct_intersection_map` replaced. It is + * O(num_cells * nelems) and must not be used in production, but it is the + * ground truth for what the candidate map should contain, so it is kept and + * exposed as an oracle for the equivalence test. + */ +template +struct ReferenceCellMajorFunctor +{ + ReferenceCellMajorFunctor(Omega_h::Mesh& mesh, + Kokkos::View[1]> grid) + : elems2verts_(mesh.ask_elem_verts()), + coords_(mesh.coords()), + grid_(grid), + nelems_(mesh.nelems()) + { + } + + KOKKOS_INLINE_FUNCTION + LO operator()(LO row, LO* fill) const + { + const auto cell_bbox = grid_(0).GetCellBBOX(row); + LO num_intersections = 0; + for (LO elem = 0; elem < nelems_; ++elem) { + const auto elem_verts = + Omega_h::gather_verts(elems2verts_, elem); + const auto vertex_coords = + Omega_h::gather_vectors(coords_, elem_verts); + if (simplex_overlaps_cell(vertex_coords, cell_bbox)) { + if (fill) { + fill[num_intersections] = elem; + } + ++num_intersections; + } + } + return num_intersections; + } + +private: + Omega_h::LOs elems2verts_; + Omega_h::Reals coords_; + Kokkos::View[1]> grid_; + LO nelems_; +}; + +template +Kokkos::Crs +construct_intersection_map_reference(Omega_h::Mesh& mesh, + Kokkos::View[1]> grid, + int num_grid_cells) +{ + Kokkos::Crs intersection_map{}; + Kokkos::count_and_fill_crs(intersection_map, num_grid_cells, + ReferenceCellMajorFunctor{mesh, grid}); + return intersection_map; +} + +Kokkos::Crs +construct_intersection_map_reference_2d(Omega_h::Mesh& mesh, + Kokkos::View grid, + int num_grid_cells) +{ + return construct_intersection_map_reference<2>(mesh, grid, num_grid_cells); +} + +Kokkos::Crs +construct_intersection_map_reference_3d(Omega_h::Mesh& mesh, + Kokkos::View grid, + int num_grid_cells) +{ + return construct_intersection_map_reference<3>(mesh, grid, num_grid_cells); +} + +// num_grid_cells should be result of grid.GetNumCells(), take as argument to +// avoid extra copy of grid from gpu to cpu +Kokkos::Crs +construct_intersection_map_2d(Omega_h::Mesh& mesh, + Kokkos::View grid, + int num_grid_cells) +{ + return construct_intersection_map<2>(mesh, grid, num_grid_cells); +} + +Kokkos::Crs +construct_intersection_map_3d(Omega_h::Mesh& mesh, + Kokkos::View grid, + int num_grid_cells) +{ + return construct_intersection_map<3>(mesh, grid, num_grid_cells); +} +} // namespace detail + +template +OMEGA_H_INLINE double myreduce(const Omega_h::Vector& x, + Op op) OMEGA_H_NOEXCEPT +{ + auto out = x[0]; + for (int i = 1; i < n; ++i) + out = op(out, x[i]); + return out; +} + +GridPointSearch2D::Results GridPointSearch2D::Apply( + const CoordinateView& points) const +{ + Results results{ + Kokkos::View("dimensionalities", + points.GetValues().extent(0)), + Kokkos::View("element IDs", points.GetValues().extent(0)), + Kokkos::View("parametric coordinates", points.GetValues().extent(0), + points.GetValues().extent(1) + 1)}; + + auto num_rows = candidate_map_.numRows(); + // needed so that we don't capture this ptr which will be memory error on cuda + auto grid = grid_; + auto candidate_map = candidate_map_; + auto tris2verts = tris2verts_; + auto tris2verts_adj = tris2verts_adj_; + auto tris2edges_adj = tris2edges_adj_; + auto edges2verts_adj = edges2verts_adj_; + auto coords = coords_; + auto tolerances = tolerances_; + auto point_coords = points.GetValues(); + Kokkos::parallel_for( + point_coords.extent(0), KOKKOS_LAMBDA(int p) { + const auto vtol = tolerances(0); + const auto etol = tolerances(1); + + Omega_h::Vector<2> point( + std::initializer_list{point_coords(p, 0), point_coords(p, 1)}); + auto cell_id = grid(0).ClosestCellID(point); + assert(cell_id < num_rows && cell_id >= 0); + auto candidates_begin = candidate_map.row_map(cell_id); + auto candidates_end = candidate_map.row_map(cell_id + 1); + + // Track best entities across all candidates to ensure order invariance + Omega_h::Real best_vertex_dist = INFINITY; + LO best_vertex_id = -1; + int best_vertex_tid = -1; // triangle providing barycentric coords + Omega_h::Vector<3> best_vertex_bary{0.0, 0.0, 0.0}; + + Omega_h::Real best_edge_dist = INFINITY; + LO best_edge_id = -1; + int best_edge_tid_min = -1; + int best_edge_tid_max = -1; + Omega_h::Vector<3> best_edge_bary_min{0.0, 0.0, 0.0}; + Omega_h::Vector<3> best_edge_bary_max{0.0, 0.0, 0.0}; + + bool found_inside = false; + LO inside_face_id = -1; + Omega_h::Vector<3> inside_face_bary{0.0, 0.0, 0.0}; + + auto begin = candidate_map.row_map(cell_id); + auto end = candidate_map.row_map(cell_id + 1); + for (auto ii = begin; ii < end; ++ii) { + const int triangleID = candidate_map.entries(ii); + const auto elem_tri2verts = + Omega_h::gather_verts<3>(tris2verts, triangleID); + auto vertex_coords = + Omega_h::gather_vectors<3, 2>(coords, elem_tri2verts); + auto parametric_coords = + Omega_h::barycentric_from_global<2, 2>(point, vertex_coords); + + // Check vertices (hierarchy level 1): compute Euclidean distance + for (int j = 0; j < 3; ++j) { + const int vertexID = tris2verts_adj.ab2b[triangleID * 3 + j]; + const auto v = Omega_h::get_vector<2>(coords, vertexID); + const auto dv = Omega_h::norm(point - v); + if ((dv < best_vertex_dist) || + ((dv == best_vertex_dist) && (vertexID < best_vertex_id))) { + best_vertex_dist = dv; + best_vertex_id = vertexID; + best_vertex_tid = triangleID; + best_vertex_bary = parametric_coords; + } + } + + // Check edges (hierarchy level 2): only if projection falls within + + for (int j = 0; j < 3; ++j) { + const int edgeID = tris2edges_adj.ab2b[triangleID * 3 + j]; + + const int va_id = edges2verts_adj.ab2b[edgeID * 2 + 0]; + const int vb_id = edges2verts_adj.ab2b[edgeID * 2 + 1]; + const auto va = Omega_h::get_vector<2>(coords, va_id); + const auto vb = Omega_h::get_vector<2>(coords, vb_id); + if (!normal_intersects_segment(va, vb, point)) + continue; + + const auto de = distance_from_line(va, vb, point); + if ((de < best_edge_dist) || + ((fabs(de - best_edge_dist) < etol) && (edgeID < best_edge_id)) || + ((fabs(de - best_edge_dist) < etol) && (edgeID == best_edge_id) && + (triangleID < best_edge_tid_min))) { + best_edge_dist = de; + best_edge_id = edgeID; + best_edge_tid_min = triangleID; + best_edge_tid_max = triangleID; + best_edge_bary_min = parametric_coords; + best_edge_bary_max = parametric_coords; + } else if ((fabs(de - best_edge_dist) < etol) && + (edgeID == best_edge_id)) { + // Track the extents of face IDs sharing this nearest edge + if (triangleID < best_edge_tid_min) { + best_edge_tid_min = triangleID; + best_edge_bary_min = parametric_coords; + } + if (triangleID > best_edge_tid_max) { + best_edge_tid_max = triangleID; + best_edge_bary_max = parametric_coords; + } + } + } + + // Check face interior (hierarchy level 3) + if (Omega_h::is_barycentric_inside(parametric_coords)) { + if (!found_inside || (triangleID < inside_face_id)) { + found_inside = true; + inside_face_id = triangleID; + inside_face_bary = parametric_coords; + } + } + } + // If we found an inside face, compute its edge distance using + // barycentric-only helper to check for edge classification while + // preserving the containing face ID. + Real inside_edge_dist = INFINITY; + int inside_edge_argmin = -1; + Omega_h::Matrix<2, 3> vcoords_in; + // Track Euclidean-nearest edge of the containing face (if any) + LO inside_edge_id = -1; + if (found_inside) { + const auto elem_tri2verts_in = + Omega_h::gather_verts<3>(tris2verts, inside_face_id); + vcoords_in = Omega_h::gather_vectors<3, 2>(coords, elem_tri2verts_in); + // Euclidean distance to the 3 edges of the containing triangle + inside_edge_argmin = -1; + inside_edge_dist = INFINITY; + inside_edge_id = -1; + for (int j = 0; j < 3; ++j) { + const int edgeID = tris2edges_adj.ab2b[inside_face_id * 3 + j]; + const int va_id = edges2verts_adj.ab2b[edgeID * 2 + 0]; + const int vb_id = edges2verts_adj.ab2b[edgeID * 2 + 1]; + const auto va = Omega_h::get_vector<2>(coords, va_id); + const auto vb = Omega_h::get_vector<2>(coords, vb_id); + if (!normal_intersects_segment(va, vb, point)) + continue; + const auto de = distance_from_line(va, vb, point); + if (de < inside_edge_dist) { + inside_edge_dist = de; + inside_edge_argmin = j; + inside_edge_id = edgeID; + } + } + } + + GridPointSearch2D::Results::Dimensionality dim_out = + GridPointSearch2D::Results::Dimensionality::NO_INTERSECT; + LO element_id_out = -1; + Omega_h::Vector<3> bary_out{0.0, 0.0, 0.0}; + + // Apply hierarchy with tolerances + // Points within tolerance are considered "inside" with positive IDs + // Only points with no candidates at all get negative IDs + if (best_vertex_id >= 0 && best_vertex_dist <= vtol) { + // Point within vertex tolerance - still inside the mesh + dim_out = GridPointSearch2D::Results::Dimensionality::VERTEX; + element_id_out = best_vertex_id; + bary_out = best_vertex_bary; + } else if ((best_edge_id >= 0 && best_edge_dist <= etol) || + (found_inside && inside_edge_dist <= etol)) { + // Point within edge tolerance - still inside the mesh + dim_out = GridPointSearch2D::Results::Dimensionality::EDGE; + if (found_inside && inside_edge_dist <= etol && + inside_edge_argmin >= 0) { + element_id_out = inside_edge_id; + bary_out = inside_face_bary; + } else { + element_id_out = best_edge_id; + bary_out = best_edge_bary_min; + } + } else if (found_inside) { + // Point inside face + dim_out = GridPointSearch2D::Results::Dimensionality::FACE; + element_id_out = inside_face_id; + bary_out = inside_face_bary; + } else { + // Outside mesh - beyond tolerance of any entity: name the nearest + // entity, encoded so the id stays negative even for entity 0. + if (best_vertex_id >= 0 && + (best_vertex_dist <= best_edge_dist || best_edge_id < 0)) { + dim_out = GridPointSearch2D::Results::Dimensionality::VERTEX; + element_id_out = + GridPointSearch2D::Results::OutsideId(best_vertex_id); + bary_out = best_vertex_bary; + } else if (best_edge_id >= 0) { + dim_out = GridPointSearch2D::Results::Dimensionality::EDGE; + element_id_out = GridPointSearch2D::Results::OutsideId(best_edge_id); + bary_out = best_edge_bary_min; + } else { + // No candidates at all: NO_INTERSECT with id -1 + } + } + + results.dimensionalities(p) = dim_out; + results.element_ids(p) = element_id_out; + for (int j = 0; j < 3; j++) + results.parametric_coords(p, j) = bary_out(j); + }); + + return results; +} + +GridPointSearch2D::GridPointSearch2D(Omega_h::Mesh& mesh, LO Nx, LO Ny) + : GridPointSearch2D( + mesh, Nx, Ny, + PointSearchTolerances("point search 2d tolerances", mesh.dim())) +{ + Kokkos::deep_copy(tolerances_, 1E-12); +} + +GridPointSearch2D::GridPointSearch2D(Omega_h::Mesh& mesh, LO Nx, LO Ny, + const PointSearchTolerances& tolerances) + : PointSearch(tolerances), mesh_(mesh) +{ + auto mesh_bbox = Omega_h::get_bounding_box<2>(&mesh); + auto grid_h = Kokkos::create_mirror_view(grid_); + grid_h(0) = + Uniform2DGrid{.edge_length = {mesh_bbox.max[0] - mesh_bbox.min[0], + mesh_bbox.max[1] - mesh_bbox.min[1]}, + .bot_left = {mesh_bbox.min[0], mesh_bbox.min[1]}, + .divisions = {Nx, Ny}}; + Kokkos::deep_copy(grid_, grid_h); + // Determine inflation radius from tolerances (max of vertex/edge tol) + auto tol_h = Kokkos::create_mirror_view(tolerances_); + Kokkos::deep_copy(tol_h, tolerances_); + candidate_map_ = + detail::construct_intersection_map_2d(mesh, grid_, grid_h(0).GetNumCells()); + coords_ = mesh.coords(); + tris2verts_ = mesh.ask_elem_verts(); + tris2edges_adj_ = mesh.ask_down(Omega_h::FACE, Omega_h::EDGE); + tris2verts_adj_ = mesh.ask_down(Omega_h::FACE, Omega_h::VERT); + edges2verts_adj_ = mesh.ask_down(Omega_h::EDGE, Omega_h::VERT); + edges2faces_up_ = mesh.ask_up(Omega_h::EDGE, Omega_h::FACE); + verts2faces_up_ = mesh.ask_up(Omega_h::VERT, Omega_h::FACE); +} + +LO GridPointSearch2D::GetOwningElementId(const Results& results, int i) +{ + const Kokkos::View query_id{""}; + const Kokkos::View dim{""}; + Kokkos::parallel_for( + 1, KOKKOS_LAMBDA(const int) { + query_id(0) = (results.element_ids(i) < 0) ? -results.element_ids(i) + : results.element_ids(i); + dim(0) = results.dimensionalities(i); + }); + auto query_id_h = Kokkos::create_mirror_view(query_id); + auto dim_h = Kokkos::create_mirror_view(dim); + Kokkos::deep_copy(query_id_h, query_id); + Kokkos::deep_copy(dim_h, dim); + return pcms::GetOwningElementId(mesh_, 2, static_cast(dim_h(0)), + query_id_h(0)); +} + +Kokkos::View GridPointSearch2D::GetOwningElementIds( + Results const& results) const +{ + Kokkos::View owners("point search owning face ids", + results.dimensionalities.extent(0)); + auto edges2faces_up = edges2faces_up_; + auto verts2faces_up = verts2faces_up_; + constexpr int mesh_dim = 2; + Kokkos::parallel_for( + results.dimensionalities.extent(0), KOKKOS_LAMBDA(const LO i) { + const LO element_id = results.EntityId(i); + + LO owner = -1; + if (element_id >= 0) { + if (results.dimensionalities(i) == Results::Dimensionality::FACE) { + owner = GetOwningElementIdFromAdj( + edges2faces_up, Results::Dimensionality::FACE, + Results::Dimensionality::FACE, element_id); + } else if (results.dimensionalities(i) == + Results::Dimensionality::EDGE) { + owner = GetOwningElementIdFromAdj( + edges2faces_up, Results::Dimensionality::EDGE, + Results::Dimensionality::FACE, element_id); + } else if (results.dimensionalities(i) == + Results::Dimensionality::VERTEX) { + owner = GetOwningElementIdFromAdj( + verts2faces_up, Results::Dimensionality::VERTEX, + Results::Dimensionality::FACE, element_id); + } + } + owners(i) = owner; + }); + return owners; +} + +GridPointSearch3D::Results GridPointSearch3D::Apply( + const CoordinateView& points) const +{ + Results results{ + Kokkos::View("dimensionalities", + points.GetValues().extent(0)), + Kokkos::View("element IDs", points.GetValues().extent(0)), + Kokkos::View("parametric coordinates", points.GetValues().extent(0), + points.GetValues().extent(1) + 1)}; + Kokkos::deep_copy(results.dimensionalities, Dimensionality::NO_INTERSECT); + Kokkos::deep_copy(results.element_ids, -1); + Kokkos::deep_copy(results.parametric_coords, 0.); + auto num_rows = candidate_map_.numRows(); + // needed so that we don't capture this ptr which will be memory error on cuda + auto grid = grid_; + auto candidate_map = candidate_map_; + auto tets2verts = tets2verts_; + auto tets2verts_adj = tets2verts_adj_; + auto tets2edges_adj = tets2edges_adj_; + auto tets2faces_adj = tets2faces_adj_; + auto edges2verts_adj = edges2verts_adj_; + auto tolerances = tolerances_; + auto coords = coords_; + auto point_coords = points.GetValues(); + auto get_face = KOKKOS_LAMBDA(int i) + { + // inverse face offsets as determined by the cannonical ordering: + // https://user-images.githubusercontent.com/56453280/74203616-39d27a80-4c3e-11ea-885d-b0260490e184.png + static const char OFFSETS = 1 << 6 | 2 << 2 | 3; + return (OFFSETS & 3 << (i * 2)) >> (i * 2); + }; + Kokkos::parallel_for( + point_coords.extent(0), KOKKOS_LAMBDA(int p) { + using Dimensionality = GridPointSearch3D::Results::Dimensionality; + const auto vtol = tolerances(0); + const auto etol = tolerances(1); + const auto ftol = tolerances(2); + Omega_h::Vector point; + for (int i = 0; i < DIM; ++i) { + point[i] = point_coords(p, i); + } + + auto cell_id = grid(0).ClosestCellID(point); + assert(cell_id < num_rows && cell_id >= 0); + const auto candidates_begin = candidate_map.row_map(cell_id); + const auto candidates_end = candidate_map.row_map(cell_id + 1); + const auto tau = tolerances(DIM - 1); + + LO inside_elem = -1; + auto inside_lambda = Omega_h::zero_vector(); + LO nearest_elem = -1; + Real nearest_dist = -INFINITY; + auto nearest_lambda = Omega_h::zero_vector(); + for (auto i = candidates_begin; i < candidates_end; ++i) { + const LO elem = candidate_map.entries(i); + const auto elem_verts = + Omega_h::gather_verts(tets2verts, elem); + const auto vertex_coords = + Omega_h::gather_vectors(coords, elem_tri2verts); + const Omega_h::Vector parametric_coords = + Omega_h::barycentric_from_global(point, vertex_coords); + + LO vertexID = -1; + Real best_vdist_sq = INFINITY; + for (int k = 0; k < 4; k++) { + Real vdist_sq = Omega_h::norm_squared(vertex_coords[k] - point); + if (vdist_sq < vtol * vtol && vdist_sq < best_vdist_sq) { + vertexID = elem_tri2verts[k]; + best_vdist_sq = vdist_sq; + } + } + if (vertexID > -1) { + results.dimensionalities(p) = Dimensionality::VERTEX; + results.element_ids(p) = vertexID; + for (int j = 0; j < 4; j++) + results.parametric_coords(p, j) = parametric_coords[j]; + found = true; + } + + if (results.dimensionalities(p) == Dimensionality::VERTEX) + continue; + + LO edgeID = -1; + Real best_edist = INFINITY; + for (int j = 0; j < 6; ++j) { + const int curr_edgeID = tets2edges_adj.ab2b[tetrahedronID * 6 + j]; + const int va_id = edges2verts_adj.ab2b[curr_edgeID * 2 + 0]; + const int vb_id = edges2verts_adj.ab2b[curr_edgeID * 2 + 1]; + const auto va = Omega_h::get_vector<3>(coords, va_id); + const auto vb = Omega_h::get_vector<3>(coords, vb_id); + if (!normal_intersects_segment(va, vb, point)) + continue; + const auto de = distance_from_line(va, vb, point); + if (de < etol && de < best_edist) { + best_edist = de; + edgeID = curr_edgeID; + } + } + if (edgeID > -1) { + results.dimensionalities(p) = Dimensionality::EDGE; + results.element_ids(p) = edgeID; + for (int j = 0; j < 4; j++) + results.parametric_coords(p, j) = parametric_coords[j]; + found = true; + } + + if (results.dimensionalities(p) == Dimensionality::EDGE) + continue; + + LO faceID = -1; + Real best_fdist_sq = INFINITY; + Omega_h::Matrix tet; + tet[0] = vertex_coords[0] - vertex_coords[3]; + tet[1] = vertex_coords[1] - vertex_coords[3]; + tet[2] = vertex_coords[2] - vertex_coords[3]; + Real tetrahedron_volume = fabs(Omega_h::determinant(tet)) / 6.; + + for (int k = 0; k < 4; k++) { + LO bary_offset = get_face(k); + Real face_area_sq = Omega_h::norm_squared(Omega_h::cross( + vertex_coords[(bary_offset + 1) % 4] - + vertex_coords[(bary_offset + 3) % 4], + vertex_coords[(bary_offset + 2) % 4] - + vertex_coords[(bary_offset + 3) % 4])) / + 4.; + + Real fdist_sq = + parametric_coords[bary_offset] * 3 * tetrahedron_volume; + fdist_sq *= fdist_sq; + fdist_sq /= face_area_sq; + + bool inside = true; + for (int j = 0; j < 4; j++) { + if (j == bary_offset) + continue; + if (parametric_coords[j] < 0) { + inside = false; + break; + } + } + + if (fdist_sq < ftol * ftol && fdist_sq < best_fdist_sq && inside) { + faceID = tets2faces_adj.ab2b[tetrahedronID * 4 + k]; + best_fdist_sq = fdist_sq; + } + } + if (faceID > -1) { + results.dimensionalities(p) = Dimensionality::FACE; + results.element_ids(p) = faceID; + for (int j = 0; j < 4; j++) + results.parametric_coords(p, j) = parametric_coords[j]; + found = true; + } + + if (results.dimensionalities(p) == Dimensionality::FACE) + continue; + + LO faceID = -1; + Real best_fdist_sq = INFINITY; + Omega_h::Matrix tet; + tet[0] = vertex_coords[0] - vertex_coords[3]; + tet[1] = vertex_coords[1] - vertex_coords[3]; + tet[2] = vertex_coords[2] - vertex_coords[3]; + Real tetrahedron_volume = fabs(Omega_h::determinant(tet)) / 6.; + + for (int k = 0; k < 4; k++) { + LO bary_offset = get_face(k); + Real face_area_sq = Omega_h::norm_squared(Omega_h::cross( + vertex_coords[(bary_offset + 1) % 4] - + vertex_coords[(bary_offset + 3) % 4], + vertex_coords[(bary_offset + 2) % 4] - + vertex_coords[(bary_offset + 3) % 4])) / + 4.; + + Real fdist_sq = + parametric_coords[bary_offset] * 3 * tetrahedron_volume; + fdist_sq *= fdist_sq; + fdist_sq /= face_area_sq; + + bool inside = true; + for (int j = 0; j < 4; j++) { + if (j == bary_offset) + continue; + if (parametric_coords[j] < 0) { + inside = false; + break; + } + } + + if (fdist_sq < ftol * ftol && fdist_sq < best_fdist_sq && inside) { + faceID = tets2faces_adj.ab2b[tetrahedronID * 4 + k]; + best_fdist_sq = fdist_sq; + } + } + if (faceID > -1) { + results.dimensionalities(p) = Dimensionality::FACE; + results.element_ids(p) = faceID; + for (int j = 0; j < 4; j++) + results.parametric_coords(p, j) = parametric_coords[j]; + found = true; + } + + if (results.dimensionalities(p) == Dimensionality::FACE) + continue; + + LO faceID = -1; + Real best_fdist_sq = INFINITY; + Omega_h::Matrix tet; + tet[0] = vertex_coords[0] - vertex_coords[3]; + tet[1] = vertex_coords[1] - vertex_coords[3]; + tet[2] = vertex_coords[2] - vertex_coords[3]; + Real tetrahedron_volume = fabs(Omega_h::determinant(tet)) / 6.; + + for (int k = 0; k < 4; k++) { + LO bary_offset = get_face(k); + Real face_area_sq = Omega_h::norm_squared(Omega_h::cross( + vertex_coords[(bary_offset + 1) % 4] - + vertex_coords[(bary_offset + 3) % 4], + vertex_coords[(bary_offset + 2) % 4] - + vertex_coords[(bary_offset + 3) % 4])) / + 4.; + + Real fdist_sq = + parametric_coords[bary_offset] * 3 * tetrahedron_volume; + fdist_sq *= fdist_sq; + fdist_sq /= face_area_sq; + + bool inside = true; + for (int j = 0; j < 4; j++) { + if (j == bary_offset) + continue; + if (parametric_coords[j] < 0) { + inside = false; + break; + } + } + + if (fdist_sq < ftol * ftol && fdist_sq < best_fdist_sq && inside) { + faceID = tets2faces_adj.ab2b[tetrahedronID * 4 + k]; + best_fdist_sq = fdist_sq; + } + } + if (faceID > -1) { + results.dimensionalities(p) = Dimensionality::FACE; + results.element_ids(p) = faceID; + for (int j = 0; j < 4; j++) + results.parametric_coords(p, j) = parametric_coords[j]; + found = true; + } + + if (results.dimensionalities(p) == Dimensionality::FACE) + continue; + + if (Omega_h::is_barycentric_inside(parametric_coords)) { + results.dimensionalities(p) = Dimensionality::REGION; + results.element_ids(p) = tetrahedronID; + for (int j = 0; j < 4; j++) + results.parametric_coords(p, j) = parametric_coords[j]; + found = true; + } + } + if (!found) { + LO nearest_elem = candidate_map.entries(nearest_tetrahedron); + results.dimensionalities(p) = dimensionality; + results.element_ids(p) = -nearest_elem; + for (int j = 0; j < 4; j++) + results.parametric_coords(p, j) = parametric_coords_to_nearest[j]; + } + }); + + return results; +} + +GridPointSearch3D::GridPointSearch3D(Omega_h::Mesh& mesh, LO Nx, LO Ny, LO Nz) + : GridPointSearch3D(mesh, Nx, Ny, Nz, + PointSearchTolerances("point search 3d tolerances", 3)) +{ + Kokkos::deep_copy(tolerances_, 1e-12); +} + +GridPointSearch3D::GridPointSearch3D(Omega_h::Mesh& mesh, LO Nx, LO Ny, LO Nz, + const PointSearchTolerances& tolerances) + : PointSearch(tolerances), mesh_(mesh) +{ + auto mesh_bbox = Omega_h::get_bounding_box<3>(&mesh); + auto grid_h = Kokkos::create_mirror_view(grid_); + + Kokkos::Array edge_lengths{}; + Kokkos::Array bot_left{}; + + for (int i = 0; i < DIM; ++i) { + edge_lengths[i] = mesh_bbox.max[i] - mesh_bbox.min[i]; + bot_left[i] = mesh_bbox.min[i]; + } + + grid_h(0) = Uniform3DGrid{.edge_length = edge_lengths, + .bot_left = bot_left, + .divisions = {Nx, Ny, Nz}}; + + Kokkos::deep_copy(grid_, grid_h); + candidate_map_ = + detail::construct_intersection_map_3d(mesh, grid_, grid_h(0).GetNumCells()); + coords_ = mesh.coords(); + tets2verts_ = mesh.ask_elem_verts(); + tets2faces_adj_ = mesh.ask_down(Omega_h::REGION, Omega_h::FACE); + tets2edges_adj_ = mesh.ask_down(Omega_h::REGION, Omega_h::EDGE); + tets2verts_adj_ = mesh.ask_down(Omega_h::REGION, Omega_h::VERT); + edges2verts_adj_ = mesh.ask_down(Omega_h::EDGE, Omega_h::VERT); + verts2regions_up_ = mesh.ask_up(Omega_h::VERT, Omega_h::REGION); + edges2regions_up_ = mesh.ask_up(Omega_h::EDGE, Omega_h::REGION); + faces2regions_up_ = mesh.ask_up(Omega_h::FACE, Omega_h::REGION); +} + +LO GridPointSearch3D::GetOwningElementId(const Results& results, int i) +{ + Kokkos::View dim("",1); + Kokkos::View id("",1); + auto dims = results.dimensionalities; + Kokkos::parallel_for(1, KOKKOS_LAMBDA(const LO){ + dim(0) = dims(i); + id(0) = results.EntityId(i); + }); + auto dim_h = Kokkos::create_mirror_view_and_copy(dim); + auto id_h = Kokkos::create_mirror_view_and_copy(id); + + return pcms::GetOwningElementId(mesh_, 3, + static_cast(dim_h[0]), + id_h[0]); +} + +Kokkos::View GridPointSearch3D::GetOwningElementIds( + const Results& results) const +{ + Kokkos::View owners("point search owning region ids", + results.dimensionalities.extent(0)); + auto verts2regions_up = verts2regions_up_; + auto edges2regions_up = edges2regions_up_; + auto faces2regions_up = faces2regions_up_; + Kokkos::parallel_for( + owners.extent(0), KOKKOS_LAMBDA(const LO i) { + const LO element_id = results.EntityId(i); + + LO owner = -1; + if (element_id >= 0) { + if (results.dimensionalities(i) == Results::Dimensionality::REGION) { + owner = GetOwningElementIdFromAdj( + faces2regions_up, Results::Dimensionality::REGION, + Results::Dimensionality::REGION, element_id); + } else if (results.dimensionalities(i) == + Results::Dimensionality::FACE) { + owner = GetOwningElementIdFromAdj( + faces2regions_up, Results::Dimensionality::FACE, + Results::Dimensionality::REGION, element_id); + } else if (results.dimensionalities(i) == + Results::Dimensionality::EDGE) { + owner = GetOwningElementIdFromAdj( + edges2regions_up, Results::Dimensionality::EDGE, + Results::Dimensionality::REGION, element_id); + } else if (results.dimensionalities(i) == + Results::Dimensionality::VERTEX) { + owner = GetOwningElementIdFromAdj( + verts2regions_up, Results::Dimensionality::VERTEX, + Results::Dimensionality::REGION, element_id); + } + } + owners(i) = owner; + }); + return owners; +} +} // namespace pcms diff --git a/src/pcms/localization/uniform_grid_localization.hpp b/src/pcms/localization/uniform_grid_localization.hpp new file mode 100644 index 000000000..8a19de306 --- /dev/null +++ b/src/pcms/localization/uniform_grid_localization.hpp @@ -0,0 +1,103 @@ +#ifndef PCMS_COUPLING_UNIFORM_GRID_LOCALIZATION_HPP +#define PCMS_COUPLING_UNIFORM_GRID_LOCALIZATION_HPP + +#include +#include +#include +#include + +#include "pcms/utility/types.h" +#include "pcms/utility/uniform_grid.h" +#include "pcms/utility/bounding_box.h" +#include "pcms/field/coordinate_system.h" +#include "pcms/localization/localization.h" + +namespace pcms +{ +[[nodiscard]] KOKKOS_FUNCTION bool triangle_intersects_bbox( + const Omega_h::Matrix<2, 3>& coords, const AABBox<2>& bbox); + +class GridPointSearch2D : public PointSearch +{ + using CandidateMapT = + Kokkos::Crs; + +public: + static constexpr int DIM = 2; + + using Results = PointSearch::Results; + using Dimensionality = Results::Dimensionality; + + GridPointSearch2D(Omega_h::Mesh& mesh, LO Nx, LO Ny); + GridPointSearch2D(Omega_h::Mesh& mesh, LO Nx, LO Ny, + const PointSearchTolerances& tolerances); + + /** + * given a point in global coordinates give the id of the triangle that the + * point lies within and the parametric coordinate of the point within the + * triangle. If the point does not lie within any triangle element. Then the + * id will be a negative number and (TODO) will return a negative id of the + * closest element + */ + Results Apply(const CoordinateView& coords) const override; + [[nodiscard]] LO GetOwningElementId(const Results& result, int i) override; + [[nodiscard]] Kokkos::View GetOwningElementIds( + const Results& results) override; + +private: + Omega_h::Mesh mesh_; + Omega_h::Adj tris2edges_adj_; + Omega_h::Adj tris2verts_adj_; + Omega_h::Adj edges2verts_adj_; + Omega_h::Adj edges2faces_up_; + Omega_h::Adj verts2faces_up_; + Kokkos::View grid_{"uniform grid"}; + CandidateMapT candidate_map_; + Omega_h::LOs tris2verts_; + Omega_h::Reals coords_; +}; + +class GridPointSearch3D : public PointSearch +{ + using CandidateMapT = + Kokkos::Crs; + +public: + static constexpr int DIM = 3; + + using Results = PointSearch::Results; + using Dimensionality = Results::Dimensionality; + + GridPointSearch3D(Omega_h::Mesh& mesh, LO Nx, LO Ny, LO Nz); + GridPointSearch3D(Omega_h::Mesh& mesh, LO Nx, LO Ny, LO Nz, + const PointSearchTolerances& tolerances); + + /** + * Given a point in global coordinates, returns the id of the tetrahedron (3D + * element) that the point lies within and the parametric coordinate of the + * point within the tetrahedron. If the point does not lie within any + * tetrahedron element, then the id will be a negative number and (TODO) will + * return a negative id of the closest element. + */ + Results Apply(const CoordinateView& coords) const override; + [[nodiscard]] LO GetOwningElementId(const Results& result, int i) override; + [[nodiscard]] Kokkos::View GetOwningElementIds( + const Results& results) override; + +private: + Omega_h::Mesh mesh_; + Omega_h::Adj tets2faces_adj_; + Omega_h::Adj tets2edges_adj_; + Omega_h::Adj tets2verts_adj_; + Omega_h::Adj edges2verts_adj_; + Omega_h::Adj verts2regions_up_; + Omega_h::Adj edges2regions_up_; + Omega_h::Adj faces2regions_up_; + Kokkos::View[1]> grid_{"uniform grid"}; + CandidateMapT candidate_map_; + Omega_h::LOs tets2verts_; + Omega_h::Reals coords_; + Real fuzz_; +}; +} // namespace pcms +#endif diff --git a/src/pcms/pythonapi/CMakeLists.txt b/src/pcms/pythonapi/CMakeLists.txt index f2c569cd2..2e0758f89 100644 --- a/src/pcms/pythonapi/CMakeLists.txt +++ b/src/pcms/pythonapi/CMakeLists.txt @@ -2,17 +2,18 @@ find_package(Python COMPONENTS Interpreter Development REQUIRED) find_package(pybind11 REQUIRED) message(STATUS "Python_EXECUTABLE ${Python_EXECUTABLE}") -pybind11_add_module(pcms +pybind11_add_module(pcms pythonapi.cpp - bind_field_base.cpp - bind_field_layout.cpp - bind_omega_h_field.cpp - bind_omega_h_field_layout.cpp - bind_uniform_grid_field.cpp + bind_runtime.cpp + bind_coordinate_system.cpp + bind_value_view.cpp + bind_omega_h_field.cpp bind_uniform_grid_field_layout.cpp - bind_transfer_field.cpp - bind_mls_interpolation.cpp - bind_mesh_utilities.cpp + bind_field_base.cpp + bind_transfer_field.cpp + bind_coordinate_map.cpp + bind_mls_interpolation.cpp + bind_mesh_utilities.cpp ) target_compile_features(pcms PUBLIC cxx_std_20) target_link_libraries(pcms PRIVATE Kokkos::kokkos pybind11::module MPI::MPI_C pcms::core pcms::transfer) @@ -31,12 +32,18 @@ if(SKBUILD) set(PCMS_PYTHON_INSTALL_LOCATION "${SKBUILD_PLATLIB_DIR}") else() # this is useful for direct CMake installation into python. - # This enables spack environment installation to directly load the PyOmega_h module - set(PCMS_PYTHON_INSTALL_LOCATION "${CMAKE_INSTALL_LIBDIR}/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages") - # Ensure the extension can resolve pcms shared libraries from a standard - # prefix install layout (e.g. Spack), where the module lives under - # //pythonX.Y/site-packages and core libs live in . - set_target_properties(pcms PROPERTIES INSTALL_RPATH "\$ORIGIN;\$ORIGIN/../../../") + # This enables spack environment installation to directly load the pcms module + # Defaults to the standard CPython prefix layout (lib/) + set(PCMS_PYTHON_INSTALL_LOCATION + "lib/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages" + CACHE STRING "Install dir for the pcms python module") + # Ensure the extension can resolve pcms shared libraries in / + # from wherever the module is installed (PCMS_PYTHON_INSTALL_LOCATION). + get_filename_component(_pcms_py_dir "${PCMS_PYTHON_INSTALL_LOCATION}" ABSOLUTE + BASE_DIR "${CMAKE_INSTALL_PREFIX}") + file(RELATIVE_PATH _pcms_py_to_lib "${_pcms_py_dir}" + "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + set_target_properties(pcms PROPERTIES INSTALL_RPATH "\$ORIGIN;\$ORIGIN/${_pcms_py_to_lib}") endif() diff --git a/src/pcms/pythonapi/bind_coordinate_map.cpp b/src/pcms/pythonapi/bind_coordinate_map.cpp new file mode 100644 index 000000000..523d06a3e --- /dev/null +++ b/src/pcms/pythonapi/bind_coordinate_map.cpp @@ -0,0 +1,91 @@ +#include + +#include +#include + +#include "pcms/field/coordinate_map.hpp" +#include "pcms/field/point_evaluator_factory.hpp" +#include "pcms/field/transformed_source.hpp" +#include "pcms/field/value_view.hpp" +#include "py_value_types.h" + +namespace py = pybind11; + +namespace pcms +{ + +void bind_coordinate_map_module(py::module& m) +{ + py::class_>( + m, "BoundCoordinateMap", "A CoordinateMap bound to one set of query points") + .def( + "mapped_points", + [](py::object self) { + return ToPyCoordinateView( + self.cast().MappedPoints(), self); + }, + "The query points in the map's target coordinate system, read-only") + .def("num_points", &BoundCoordinateMap::NumPoints) + .def("output_basis", &BoundCoordinateMap::OutputBasis, py::arg("stored"), + "The basis values stored in `stored` are re-expressed in") + .def( + "transform_values", + [](const BoundCoordinateMap& self, PyValueView values) { + ValueBasis transformed; + WithDeviceOutput(values.values, [&](auto out) { + transformed = self + .TransformValues(ValueView( + values.basis, out)) + .GetBasis(); + }); + return PyValueView{transformed, values.values}; + }, + py::arg("values"), + "Re-express `values` (one row per query point, over a writeable " + "C-contiguous float64 array) in place, and return a ValueView of the " + "same array tagged with the new basis"); + + py::class_>( + m, "CoordinateMap", + "Maps points from a source to a target coordinate system; values are " + "re-expressed the other way") + .def("get_source_coordinate_system", + [](const CoordinateMap& self) { + return ToPyHolder(self.GetSourceCoordinateSystem()); + }) + .def("get_target_coordinate_system", + [](const CoordinateMap& self) { + return ToPyHolder(self.GetTargetCoordinateSystem()); + }) + .def( + "bind", + [](const CoordinateMap& self, const PyCoordinateView& query_points) { + return self.Bind(CopyCoordinatesToDevice(query_points).view); + }, + py::arg("query_points"), + "Do every per-point computation for points in the source coordinate " + "system"); + + py::class_>(m, + "CartesianToCylindrical") + .def(py::init<>()); + py::class_>(m, + "CylindricalToCartesian") + .def(py::init<>()); + + py::class_>( + m, "TransformedSource", + "Presents a PointEvaluatorFactory as one in another coordinate system") + .def(py::init([](std::shared_ptr source, + std::shared_ptr to_source) { + return std::make_shared(std::move(source), + std::move(to_source)); + }), + py::arg("source"), py::arg("to_source"), + "`to_source` maps this object's coordinate system to the source's"); +} + +} // namespace pcms diff --git a/src/pcms/pythonapi/bind_coordinate_system.cpp b/src/pcms/pythonapi/bind_coordinate_system.cpp new file mode 100644 index 000000000..e50a36b30 --- /dev/null +++ b/src/pcms/pythonapi/bind_coordinate_system.cpp @@ -0,0 +1,131 @@ +#include +#include +#include + +#include +#include + +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/field/coordinate_systems/cylindrical.hpp" +#include "pcms/field/coordinate_view.hpp" +#include "numpy_view.h" +#include "py_value_types.h" + +namespace py = pybind11; + +namespace pcms +{ + +namespace +{ + +using SystemHolder = std::shared_ptr; + +std::string Repr(const CoordinateSystem& system) +{ + return ""; +} + +PyCoordinateView MakeCoordinateView(const SystemHolder& system, + const NumpyInput& coordinates) +{ + const auto input = NumpyToHostView<2, Real>(coordinates); + // pcms may keep the coordinates, so the view owns a copy + auto values = ToNumpy(input); + detail::SetReadOnly(values); + return PyCoordinateView{ + ResolveCoordinateSystem(CartesianIfNone(system), input.extent(1)), values}; +} + +} // namespace + +void bind_coordinate_system_module(py::module& m) +{ + // Concrete systems are registered as subclasses so Python sees the dynamic + // type of any system pcms returns (isinstance(cs, pcms.csys.Cartesian)). + py::class_( + m, "CoordinateSystem", + "What coordinate numbers mean. Create one through the classes in " + "pcms.csys.") + .def( + "kind", + [](const CoordinateSystem& self) { return std::string(self.Kind()); }, + "Family label, e.g. 'cartesian'") + .def( + "dimension", + [](const CoordinateSystem& self) { return self.Dimension(); }, + "Number of coordinate columns") + .def("has_orthogonal_basis", + [](const CoordinateSystem& self) { return self.HasOrthogonalBasis(); }) + .def( + "has_unit_scale_factors", + [](const CoordinateSystem& self) { return self.HasUnitScaleFactors(); }) + .def( + "__eq__", + [](const SystemHolder& a, const SystemHolder& b) { + return SameCoordinateSystem(a, b); + }, + py::is_operator()) + .def("__hash__", + [](const CoordinateSystem& self) { + return std::hash{}(std::string(self.Kind()) + "/" + + std::to_string(self.Dimension())); + }) + .def("__repr__", [](const CoordinateSystem& self) { return Repr(self); }); + + auto csys = m.def_submodule("csys", "Concrete coordinate systems"); + py::class_>(csys, "Cartesian") + .def_static( + "create", + [](int dim) { return ToPyHolder(csys::Cartesian::Create(dim)); }, + py::arg("dim"), "The Cartesian coordinate system of dimension 1..3"); + py::class_>(csys, + "CylindricalRThetaZ") + .def_static( + "create", []() { return ToPyHolder(csys::CylindricalRThetaZ::Create()); }, + "Cylindrical (r, theta, z) with theta = atan2(y, x); dimension 3"); + py::class_>(csys, "CylindricalRZ") + .def_static( + "create", []() { return ToPyHolder(csys::CylindricalRZ::Create()); }, + "(R, Z) poloidal section; dimension 2"); + + m.def( + "same_coordinate_system", + [](const SystemHolder& a, const SystemHolder& b) { + return SameCoordinateSystem(a, b); + }, + py::arg("a"), py::arg("b")); + m.def( + "has_identity_metric", + [](const CoordinateSystem& system) { return HasIdentityMetric(system); }, + py::arg("system")); + py::class_(m, "CoordinateView", + "Coordinates tagged with their coordinate " + "system") + .def(py::init(&MakeCoordinateView), py::arg("coordinate_system"), + py::arg("coordinates"), + "Coordinates from a 2D array (num_points × dim), which is copied. A " + "coordinate_system of None is Cartesian in the array's dimension.") + .def(py::init([](const NumpyInput& coordinates) { + return MakeCoordinateView(nullptr, coordinates); + }), + py::arg("coordinates"), + "Cartesian coordinates from a 2D array (num_points × dim), which is " + "copied.") + .def("get_coordinate_system", + [](const PyCoordinateView& self) { return ToPyHolder(self.system); }) + .def( + "get_values", [](const PyCoordinateView& self) { return self.values; }, + "The coordinates as a read-only 2D array (num_points × dim)") + .def("__repr__", [](const PyCoordinateView& self) { + return ""; + }); +} + +} // namespace pcms diff --git a/src/pcms/pythonapi/bind_field_base.cpp b/src/pcms/pythonapi/bind_field_base.cpp index 77be98715..b38c8fbfc 100644 --- a/src/pcms/pythonapi/bind_field_base.cpp +++ b/src/pcms/pythonapi/bind_field_base.cpp @@ -1,23 +1,30 @@ +#include +#include +#include + +#include #include #include -#include -#include "pcms/field/coordinate_system.h" -#include "pcms/field/coordinate.h" + +#include "pcms/discretization/discretization/omega_h.hpp" +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/field/coordinate_view.hpp" +#include "pcms/field/evaluation_request.h" +#include "pcms/field/evaluator/mls_options.h" #include "pcms/field/field.h" -#include "pcms/field/field_evaluator_factory.h" +#include "pcms/field/field_data.h" #include "pcms/field/field_layout.h" -#include "pcms/field/field_metadata.h" #include "pcms/field/function_space.h" -#include "pcms/discretization/discretization/omega_h.hpp" -#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/field/point_evaluator_factory.hpp" +#include "pcms/field/uniform_grid_binary_field.h" +#include "pcms/field/value_view.hpp" #include "pcms/utility/arrays.h" +#include "pcms/utility/uniform_grid.h" #include "numpy_array_transform.h" +#include "py_value_types.h" namespace py = pybind11; @@ -27,279 +34,178 @@ namespace pcms namespace { -struct PythonEvaluationRequest +struct PyEvaluationRequest { EvaluationRequest request; - py::object owner; - // Keep the device view alive to prevent dangling pointer in mdspan Kokkos::View device_coords; }; -} // namespace +using SystemHolder = std::shared_ptr; -void bind_coordinate_system_module(py::module& m) +void SetDOFHolderData(Field& self, const PyValueView& v) { - // Bind CoordinateSystem enum - py::enum_(m, "CoordinateSystem") - .value("Cartesian", CoordinateSystem::Cartesian) - .value("Cylindrical", CoordinateSystem::Cylindrical) - .value("XGC", CoordinateSystem::XGC) - .value("GNET", CoordinateSystem::GNET) - .value("BEAMS3D", CoordinateSystem::BEAMS3D) - .export_values(); - - // Bind CoordinateView for HostMemorySpace - py::class_>(m, "CoordinateView") - .def(py::init([](CoordinateSystem cs, py::array_t coords) { - // Convert 2D numpy array to Rank2View - auto buf = coords.request(); - if (buf.ndim != 2) { - throw std::runtime_error("Coordinates must be a 2D array"); - } - // Create a view from the numpy array - 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); - }), - py::arg("coordinate_system"), py::arg("coordinates"), - "Constructor for CoordinateView") - - .def("get_coordinate_system", - &CoordinateView::GetCoordinateSystem, - "Get the coordinate system") + auto array = NumpyInput::ensure(v.values); + if (!array) { + throw py::type_error("set_dof_holder_data: values must be float64"); + } + self.SetDOFHolderDataHost(ValueView( + v.basis, NumpyToHostView<2, Real>(array))); +} - .def("set_coordinate_system", - &CoordinateView::SetCoordinateSystem, py::arg("cs"), - "Set the coordinate system") +} // namespace +void bind_create_field_module(py::module& m) +{ + py::class_>( + m, "FieldLayout", "How a field's DOF holders are laid out") + .def("get_num_components", &FieldLayout::GetNumComponents) + .def("get_num_owned_dof_holder", &FieldLayout::GetNumOwnedDofHolder) + .def("get_num_global_dof_holder", &FieldLayout::GetNumGlobalDofHolder) + .def("get_dimension", &FieldLayout::GetDimension) + .def("is_distributed", &FieldLayout::IsDistributed) .def( - "get_coordinates", - [](const CoordinateView& self) { - auto coords = self.GetValues(); - // Convert to numpy array - py::array_t result({static_cast(coords.extent(0)), - static_cast(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_coordinate_system", + [](const FieldLayout& self) { + return ToPyHolder(self.GetCoordinateSystem()); }, - "Get the coordinates as numpy array"); - - // Bind CoordinateTransformation abstract base class - py::class_>( - m, "CoordinateTransformation") - .def("evaluate", &CoordinateTransformation::Evaluate, py::arg("from"), - py::arg("to"), "Evaluate the coordinate transformation"); -} - -void bind_coordinate_module(py::module& m) -{ - // Bind Coordinate template for common cases - // 3D Cartesian coordinates with Real type - py::class_>(m, "Coordinate3D") - .def(py::init(), py::arg("x"), py::arg("y"), py::arg("z"), - "Constructor for 3D coordinate") - + "What this layout's DOF-holder coordinates mean") .def( - "values", - [](const Coordinate& self) { - auto vals = self.Values(); - py::array_t result(3); - auto buf = result.request(); - Real* ptr = static_cast(buf.ptr); - ptr[0] = vals[0]; - ptr[1] = vals[1]; - ptr[2] = vals[2]; - return result; + "get_dof_holder_coordinates", + [](py::object self) { + return ToPyCoordinateView( + self.cast().GetDOFHolderCoordinates(), self); }, - "Get coordinate values as numpy array") - - .def("__getitem__", &Coordinate::operator[], - py::arg("index"), "Get coordinate value by index"); - - // 2D coordinates - py::class_>(m, "Coordinate2D") - .def(py::init(), py::arg("x"), py::arg("y"), - "Constructor for 2D coordinate") - + "The DOF-holder coordinates (num_dof_holders × dim), read-only"); + + py::class_>(m, "FieldData", "A field's stored values") + .def("get_value_type", &FieldData::GetValueType) + .def("get_value_basis", &FieldData::GetValueBasis, + "The basis the stored components are in"); + + py::class_>( + m, "Field", "DOF-holder values together with the layout that orders them") + .def("get_name", &Field::GetName) + .def("get_layout", &Field::GetLayout, + py::return_value_policy::reference_internal) + .def( + "get_data", + [](Field& self) -> FieldData& { return self.GetData(); }, + py::return_value_policy::reference_internal) .def( - "values", - [](const Coordinate& self) { - auto vals = self.Values(); - py::array_t result(2); - auto buf = result.request(); - Real* ptr = static_cast(buf.ptr); - ptr[0] = vals[0]; - ptr[1] = vals[1]; - return result; + "get_dof_holder_data", + [](py::object self) { + return ToPyValueView(self.cast&>().GetDOFHolderData(), + self); }, - "Get coordinate values as numpy array") - - .def("__getitem__", &Coordinate::operator[], - py::arg("index"), "Get coordinate value by index"); - - // Bind CoordinateElement for common types - py::class_>(m, "CoordinateElement") - .def(py::init(), py::arg("data"), "Constructor for CoordinateElement") - - .def("underlying", - py::overload_cast<>( - &CoordinateElement::underlying, py::const_), - "Get the underlying value"); -} - -void bind_create_field_module(py::module& m) -{ - py::class_(m, "EvaluationRequest") + "The DOF-holder values (num_dof_holders × num_components) as a " + "ValueView whose array is read-only. When the field's data is in host " + "memory the array shares it, so it reflects later writes; otherwise it " + "is a copy.") + .def("set_dof_holder_data", &SetDOFHolderData, py::arg("data"), + "Set the DOF-holder values from a ValueView (num_dof_holders × " + "num_components) whose basis must match the field's") + .def( + "set_dof_holder_data", + [](Field& self, const NumpyInput& data) { + RequireScalarForPlainArray("set_dof_holder_data", + self.GetData().GetValueBasis()); + self.SetDOFHolderDataHost(ValueView( + ValueBasis{}, + NumpyToHostComponentView(data, PerEntityComponents(data)))); + }, + py::arg("data"), + "Set a scalar field's DOF-holder values from a plain 2D array " + "(num_dof_holders × num_components). A 1D array (num_dof_holders,) is " + "accepted when the field has one component.") + .def( + "set_dof_holder_data_unchecked", + [](Field& self, const NumpyInput& data) { + const auto& layout = self.GetLayout(); + const LO num_dof_holders = layout.GetNumOwnedDofHolder(); + auto view = + NumpyToHostComponentView(data, layout.GetNumComponents()); + if (view.extent(0) != static_cast(num_dof_holders)) { + throw std::invalid_argument( + "set_dof_holder_data_unchecked: data must have " + + std::to_string(num_dof_holders) + " rows, got " + + std::to_string(view.extent(0))); + } + self.SetDOFHolderDataUncheckedHost(view); + }, + py::arg("data"), + "Set the DOF-holder values without checking their basis (the shape is " + "still checked). A 1D array (num_dof_holders,) is accepted when the " + "field has one component."); + + py::class_, Field>( + m, "Function", "A Field that knows its FunctionSpace") + .def("get_space", [](const Function& self) { + return ToPyHolder(self.GetSpacePtr()); + }); + + 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! + [](const PyCoordinateView& coords, OutOfBoundsPolicy policy) { + auto device = CopyCoordinatesToDevice(coords); + return PyEvaluationRequest{ + EvaluationRequest::FromCoordinates(device.view, policy), + device.storage}; }, - py::arg("coordinates"), - py::arg("coordinate_system") = CoordinateSystem::Cartesian, - py::arg("policy") = OutOfBoundsPolicy{}, - "Create an EvaluationRequest from an explicit coordinate array.") + py::arg("coordinates"), py::arg("policy") = OutOfBoundsPolicy{}, + "Request evaluation at the given CoordinateView's points") .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 + return PyEvaluationRequest{ + EvaluationRequest::FromFunctionSpace(function_space, policy), {}}; }, 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(); - // Flatten the [dof][comp] data into node-major order for the 1D numpy - // array (Python-facing DOF data stays 1D). - const auto num_dof = data.extent(0); - const auto num_comp = data.extent(1); - py::array_t result(static_cast(data.size())); - auto buf = result.request(); - Real* ptr = static_cast(buf.ptr); - for (size_t i = 0; i < num_dof; ++i) - for (size_t c = 0; c < num_comp; ++c) - ptr[i * num_comp + c] = data(i, c); - return result; - }, - "Get the DOF holder data as a 1D numpy array") - - .def( - "set_dof_holder_data", - // c_style|forcecast makes pybind materialize a contiguous copy of the - // input, so non-contiguous arrays (e.g. a column slice like data[:, i]) - // are read correctly rather than by walking flat memory with the wrong - // stride. - [](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"); - } - // Reshape the flat 1D input into the field's [dof][comp] layout. - const int nc = self.GetLayout().GetNumComponents(); - const auto total = static_cast(buf.shape[0]); - self.SetDOFHolderDataHost(Rank2View( - static_cast(buf.ptr), static_cast(total / nc), nc)); - }, - py::arg("data"), "Set the DOF holder data from a 1D numpy array") - + "Request evaluation at a FunctionSpace's DOF-holder coordinates") + .def_property_readonly( + "coords", + [](py::object self) { + return ToPyCoordinateView( + self.cast().request.coords, self); + }) + .def_property_readonly("policy", [](const PyEvaluationRequest& self) { + return self.request.policy; + }); + + py::class_>( + m, "PointEvaluatorFactory", "Creates evaluators bound to query points") .def( - "get_num_dof_holders", - [](const Field& self) { - return self.GetLayout().GetNumOwnedDofHolder(); + "get_coordinate_system", + [](const PointEvaluatorFactory& self) { + return ToPyHolder(self.GetCoordinateSystem()); }, - "Number of owned DOF holders (nodes/elements)") - + "The coordinate system query points must be expressed in") .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.GetValues(); - 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; + "create_point_evaluator", + [](const PointEvaluatorFactory& self, + const PyEvaluationRequest& request) { + return self.CreatePointEvaluator(request.request); }, - "DOF holder coordinates as a 2D numpy array (num_dof_holders × dim)"); + py::arg("request"), py::keep_alive<0, 1>(), + "Create a reusable point evaluator for the request's points"); - py::class_(m, "FunctionSpace") + 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); + "create_function", + [](const FunctionSpace& self, std::string name, + VarianceSignature variance, const SystemHolder& system, + std::optional scaling) { + return self.CreateFunction(std::move(name), variance, system, + scaling); }, - 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") + py::arg("name") = "", py::arg("variance") = values::Scalar, + py::arg("system") = py::none(), py::arg("scaling") = py::none(), + "Create a Function in this space. Vector and tensor values are in " + "`system`'s basis (default: this space's coordinate system), with " + "`scaling` required when that system admits both scalings.") + .def("get_layout", + [](const FunctionSpace& self) { return ToPyHolder(self.GetLayout()); }) .def( "mesh", [](const FunctionSpace& self) -> Omega_h::Mesh& { @@ -316,9 +222,9 @@ void bind_create_field_module(py::module& m) "The Omega_h mesh this function space is defined on (Omega_h backend " "only)."); - // Bind LagrangeFunctionSpace as a concrete FunctionSpace subtype. - py::class_ lagrange_space( - m, "LagrangeFunctionSpace"); + py::class_> + lagrange_space(m, "LagrangeFunctionSpace"); // Backend selecting the underlying field layout. The conservative-projection // transfer operators require the native Omega_h backend. @@ -330,49 +236,48 @@ void bind_create_field_module(py::module& m) "Monte Carlo projection transfer operators)") .export_values(); + // The mesh-backed spaces refer to the mesh, so it is kept alive with them. lagrange_space .def_static( "from_mesh", [](Omega_h::Mesh& mesh, int order, int num_components, - CoordinateSystem coordinate_system, std::string global_id_name, + const SystemHolder& mesh_coordinate_system, std::string global_id_name, LagrangeFunctionSpace::Backend backend) { return LagrangeFunctionSpace::FromMesh( - mesh, order, num_components, coordinate_system, + mesh, order, num_components, CartesianIfNone(mesh_coordinate_system), std::move(global_id_name), backend); }, - py::arg("mesh"), py::arg("order"), py::arg("num_components") = 1, - py::arg("coordinate_system") = CoordinateSystem::Cartesian, + py::arg("mesh"), py::arg("order"), py::arg("num_components"), + py::arg("mesh_coordinate_system") = py::none(), py::arg("global_id_name") = "global", py::arg("backend") = LagrangeFunctionSpace::DefaultBackend, - "Create a LagrangeFunctionSpace from an Omega_h mesh") - + py::keep_alive<0, 1>(), + "Create a LagrangeFunctionSpace from an Omega_h mesh. The coordinate " + "system defaults to Cartesian.") .def_static( "from_uniform_grid", - [](const UniformGrid<2>& grid, int num_components, CoordinateSystem cs, - int order) { - return LagrangeFunctionSpace::FromUniformGrid(grid, num_components, cs, - order); + [](const UniformGrid<2>& grid, int num_components, + const SystemHolder& mesh_coordinate_system, int order) { + return LagrangeFunctionSpace::FromUniformGrid( + grid, num_components, CartesianIfNone(mesh_coordinate_system), 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") - + py::arg("grid"), py::arg("num_components"), + py::arg("mesh_coordinate_system") = py::none(), py::arg("order") = 1, + "Create a LagrangeFunctionSpace from a 2D uniform grid. The coordinate " + "system defaults to Cartesian.") .def_static( "from_uniform_grid", - [](const UniformGrid<3>& grid, int num_components, CoordinateSystem cs, - int order) { - return LagrangeFunctionSpace::FromUniformGrid(grid, num_components, cs, - order); + [](const UniformGrid<3>& grid, int num_components, + const SystemHolder& mesh_coordinate_system, int order) { + return LagrangeFunctionSpace::FromUniformGrid( + grid, num_components, CartesianIfNone(mesh_coordinate_system), 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"); + py::arg("grid"), py::arg("num_components"), + py::arg("mesh_coordinate_system") = py::none(), py::arg("order") = 1, + "Create a LagrangeFunctionSpace from a 3D uniform grid. The coordinate " + "system defaults to Cartesian."); - // Bind MLSOptions: configuration struct for - // PolynomialReconstructionFunctionSpace MLS - // evaluation. + // Configuration for PolynomialReconstructionFunctionSpace MLS evaluation. py::class_(m, "MLSOptions") .def(py::init<>(), "Default MLSOptions") .def_readwrite("radius", &MLSOptions::radius) @@ -384,35 +289,34 @@ void bind_create_field_module(py::module& m) .def_readwrite("decay_factor", &MLSOptions::decay_factor) .def_readwrite("basis", &MLSOptions::basis); - // Bind PolynomialReconstructionFunctionSpace as a concrete FunctionSpace - // subtype. - py::class_( + 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); + "create", + [](const PyCoordinateView& coords, MLSOptions options) { + auto values = NumpyInput::ensure(coords.values); + return PolynomialReconstructionFunctionSpace::Create( + CoordinateView(coords.system, + NumpyToHostView<2, Real>(values)), + options); }, - 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).") + py::arg("coords"), py::arg("options") = MLSOptions{}, + "Create a PolynomialReconstructionFunctionSpace over the source points " + "of a CoordinateView") .def_static( "from_mesh", - [](Omega_h::Mesh& mesh, int source_entity_dim, CoordinateSystem cs, - MLSOptions opts) { + [](Omega_h::Mesh& mesh, int source_entity_dim, + const SystemHolder& coordinate_system, MLSOptions options) { return PolynomialReconstructionFunctionSpace::FromMesh( - mesh, source_entity_dim, cs, opts); + mesh, source_entity_dim, CartesianIfNone(coordinate_system), options); }, py::arg("mesh"), py::arg("source_entity_dim"), - py::arg("coordinate_system") = CoordinateSystem::Cartesian, - py::arg("options") = MLSOptions{}, + py::arg("coordinate_system") = py::none(), + py::arg("options") = MLSOptions{}, py::keep_alive<0, 1>(), "Create a PolynomialReconstructionFunctionSpace from mesh entity " "coordinates."); - // Bind CreateUniformGridFromMesh for 2D m.def( "create_uniform_grid_from_mesh", [](Omega_h::Mesh& mesh, const std::array& divisions) { @@ -421,7 +325,6 @@ void bind_create_field_module(py::module& m) py::arg("mesh"), py::arg("divisions"), "Create a 2D uniform grid from an Omega_h mesh"); - // Bind CreateUniformGridFromMesh for 3D m.def( "create_uniform_grid_from_mesh", [](Omega_h::Mesh& mesh, const std::array& divisions) { @@ -433,16 +336,10 @@ void bind_create_field_module(py::module& m) m.def( "create_uniform_grid_binary_field", [](Omega_h::Mesh& mesh, const std::array& divisions) { - auto [layout, field] = CreateUniformGridBinaryField<2>(mesh, divisions); - static_cast(layout); - return field; + return CreateUniformGridBinaryField<2>(mesh, divisions).second; }, py::arg("mesh"), py::arg("divisions"), "Create a 2D vertex mask field indicating inside/outside mesh"); } -// 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 diff --git a/src/pcms/pythonapi/bind_field_layout.cpp b/src/pcms/pythonapi/bind_field_layout.cpp deleted file mode 100644 index 4625a09d5..000000000 --- a/src/pcms/pythonapi/bind_field_layout.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include -#include -#include -#include "pcms/field/field_layout.h" -#include "numpy_array_transform.h" - -namespace py = pybind11; - -namespace pcms -{ - -void bind_field_layout_module(py::module& m) -{ - static_cast(m); -} - -} // namespace pcms diff --git a/src/pcms/pythonapi/bind_mesh_utilities.cpp b/src/pcms/pythonapi/bind_mesh_utilities.cpp index f83af08cc..0f03c38a1 100644 --- a/src/pcms/pythonapi/bind_mesh_utilities.cpp +++ b/src/pcms/pythonapi/bind_mesh_utilities.cpp @@ -12,11 +12,45 @@ namespace py = pybind11; namespace pcms { +namespace +{ + +// Number of components of per-entity values shaped (nentities,) or +// (nentities, ncomps). A 1D array has one component. +LO EntityComponents(const py::array& values, LO nentities) +{ + if (values.ndim() == 1 && values.shape(0) == nentities) { + return 1; + } + if (values.ndim() == 2 && values.shape(0) == nentities && + values.shape(1) > 0) { + return static_cast(values.shape(1)); + } + throw std::invalid_argument("entity_field_values must have shape (" + + std::to_string(nentities) + ",) or (" + + std::to_string(nentities) + ", ncomps), got " + + detail::ShapeString(values)); +} + +// Vertex values with the same rank as the entity values they came from. +py::array ShapeLike(py::array_t vertex_values, + const py::array& entity_values, LO ncomps) +{ + if (entity_values.ndim() == 1) { + return vertex_values; + } + return vertex_values.reshape( + std::vector{vertex_values.size() / ncomps, ncomps}); +} + +} // namespace + // Helper function to compute entity centroids -py::array_t compute_entity_centroids(Omega_h::Mesh& mesh, - Omega_h::Int entity_dim) +py::array compute_entity_centroids(Omega_h::Mesh& mesh, Omega_h::Int entity_dim) { - return omega_h_read_to_numpy(pcms::get_entity_centroids(mesh, entity_dim)); + const py::ssize_t dim = mesh.dim(); + return ToNumpy(pcms::get_entity_centroids(mesh, entity_dim)) + .reshape(std::vector{mesh.nents(entity_dim), dim}); } // Helper function to determine patch size based on interpolation degree @@ -32,13 +66,14 @@ Omega_h::LO patch_size_for_degree(Omega_h::LO interp_degree) } // Map entity field to vertices using simple averaging -py::array_t map_entity_field_to_vertices_average( - Omega_h::Mesh& mesh, py::array_t entity_field_values, +py::array map_entity_field_to_vertices_average( + Omega_h::Mesh& mesh, const NumpyInput& entity_field_values, Omega_h::Int entity_dim) { const Omega_h::LO nverts = mesh.nverts(); const Omega_h::LO nentities = mesh.nents(entity_dim); + const Omega_h::LO ncomps = EntityComponents(entity_field_values, nentities); // Get entity to vertices adjacency auto entity_to_verts_omega_h = mesh.ask_verts_of(entity_dim); @@ -46,22 +81,23 @@ py::array_t map_entity_field_to_vertices_average( entity_to_verts_omega_h.size() / nentities; // Convert numpy array to Omega_h array - auto entity_field_omega_h = - numpy_to_omega_h_read(entity_field_values); + auto entity_field_omega_h = NumpyToOmegaHRead(entity_field_values); // Initialize vertex field and count arrays - Omega_h::Write vertex_field_values(nverts, 0.0); + Omega_h::Write vertex_field_values(nverts * ncomps, 0.0); Omega_h::Write vertex_counts(nverts, 0.0); // Parallel accumulation Kokkos::parallel_for( "accumulate_to_vertices", nentities, OMEGA_H_LAMBDA(const Omega_h::LO entity_id) { - const Omega_h::Real entity_value = entity_field_omega_h[entity_id]; for (Omega_h::LO v_local = 0; v_local < verts_per_entity; ++v_local) { const Omega_h::LO vert_id = entity_to_verts_omega_h[entity_id * verts_per_entity + v_local]; - Kokkos::atomic_add(&vertex_field_values[vert_id], entity_value); + for (Omega_h::LO c = 0; c < ncomps; ++c) { + Kokkos::atomic_add(&vertex_field_values[vert_id * ncomps + c], + entity_field_omega_h[entity_id * ncomps + c]); + } Kokkos::atomic_add(&vertex_counts[vert_id], 1.0); } }); @@ -70,26 +106,29 @@ py::array_t map_entity_field_to_vertices_average( Kokkos::parallel_for( "average_vertices", nverts, OMEGA_H_LAMBDA(const Omega_h::LO vert_id) { if (vertex_counts[vert_id] > 0.0) { - vertex_field_values[vert_id] /= vertex_counts[vert_id]; + for (Omega_h::LO c = 0; c < ncomps; ++c) { + vertex_field_values[vert_id * ncomps + c] /= vertex_counts[vert_id]; + } } }); - return omega_h_read_to_numpy(Omega_h::read(vertex_field_values)); + return ShapeLike(ToNumpy(Omega_h::read(vertex_field_values)), + entity_field_values, ncomps); } // Map entity field to vertices using MLS interpolation -py::array_t map_entity_field_to_vertices_mls( - Omega_h::Mesh& mesh, py::array_t entity_field_values, +py::array map_entity_field_to_vertices_mls( + Omega_h::Mesh& mesh, const NumpyInput& entity_field_values, Omega_h::Int entity_dim, Omega_h::LO interp_degree, Omega_h::LO min_support, double lambda_reg) { const Omega_h::Int dim = mesh.dim(); + const Omega_h::LO nentities = mesh.nents(entity_dim); + const Omega_h::LO ncomps = EntityComponents(entity_field_values, nentities); - // Compute source coordinates (entity centroids) and flatten - auto centroids_flat = compute_entity_centroids(mesh, entity_dim); - auto source_coordinates = - numpy_to_omega_h_read(centroids_flat); + // Source coordinates are the entity centroids + auto source_coordinates = pcms::get_entity_centroids(mesh, entity_dim); // Get target coordinates (vertex coordinates) auto target_coordinates = mesh.coords(); @@ -112,18 +151,32 @@ py::array_t map_entity_field_to_vertices_mls( SupportResults support{supports_ptr, supports_idx, radii2_write}; // Convert entity field values to Omega_h array - auto entity_field_omega_h = - numpy_to_omega_h_read(entity_field_values); - - // Call MLS interpolation - auto interpolated_values = pcms::mls_interpolation( - entity_field_omega_h, source_coordinates, target_coordinates, support, dim, - interp_degree, pcms::RadialBasisFunction::NO_OP, lambda_reg, - 1e-6, // tol - 5.0 // decay_factor - ); - - return omega_h_read_to_numpy(Omega_h::Reals(interpolated_values)); + auto entity_field_omega_h = NumpyToOmegaHRead(entity_field_values); + + // mls_interpolation handles one component at a time + Omega_h::Write vertex_field_values(num_vertices * ncomps); + for (Omega_h::LO c = 0; c < ncomps; ++c) { + Omega_h::Write component_values(nentities); + Kokkos::parallel_for( + "extract_component", nentities, OMEGA_H_LAMBDA(const Omega_h::LO i) { + component_values[i] = entity_field_omega_h[i * ncomps + c]; + }); + + auto interpolated_values = pcms::mls_interpolation( + Omega_h::read(component_values), source_coordinates, target_coordinates, + support, dim, interp_degree, pcms::RadialBasisFunction::NO_OP, lambda_reg, + 1e-6, // tol + 5.0 // decay_factor + ); + + Kokkos::parallel_for( + "insert_component", num_vertices, OMEGA_H_LAMBDA(const Omega_h::LO v) { + vertex_field_values[v * ncomps + c] = interpolated_values[v]; + }); + } + + return ShapeLike(ToNumpy(Omega_h::read(vertex_field_values)), + entity_field_values, ncomps); } void bind_mesh_utilities_module(py::module& m) @@ -131,13 +184,13 @@ void bind_mesh_utilities_module(py::module& m) m.def("compute_entity_centroids", &compute_entity_centroids, py::arg("mesh"), py::arg("entity_dim"), "Compute centroids of mesh entities (faces or elements). " - "Returns flattened array of shape (nentities * dim,)."); + "Returns an array of shape (nentities, dim)."); // Backward compatibility alias m.def("compute_entity_centroids_few", &compute_entity_centroids, py::arg("mesh"), py::arg("entity_dim"), "Compute entity centroids using gather_verts/gather_vectors/average. " - "Returns flattened array of shape (nentities * dim,). " + "Returns an array of shape (nentities, dim). " "Note: This is an alias for compute_entity_centroids."); m.def( @@ -155,10 +208,11 @@ void bind_mesh_utilities_module(py::module& m) "Parameters:\n" " mesh: OmegaHMesh object\n" " entity_field_values: numpy array of field values at entities (shape: " - "nentities,)\n" + "(nentities,) or (nentities, ncomps))\n" " entity_dim: dimension of entities (2 for faces, 3 for elements)\n\n" "Returns:\n" - " numpy array of field values at vertices (shape: nverts,)"); + " numpy array of field values at vertices (shape: (nverts,) or " + "(nverts, ncomps), matching entity_field_values)"); m.def( "map_entity_field_to_vertices_mls", &map_entity_field_to_vertices_mls, @@ -171,13 +225,14 @@ void bind_mesh_utilities_module(py::module& m) "Parameters:\n" " mesh: OmegaHMesh object\n" " entity_field_values: numpy array of field values at entities (shape: " - "nentities,)\n" + "(nentities,) or (nentities, ncomps))\n" " entity_dim: dimension of entities (2 for faces, 3 for elements)\n" " interp_degree: polynomial degree for MLS (default: 1)\n" " min_support: minimum patch size, auto-selected if < 0 (default: -1)\n" " lambda_reg: regularization parameter (default: 1e-5)\n\n" "Returns:\n" - " numpy array of field values at vertices (shape: nverts,)"); + " numpy array of field values at vertices (shape: (nverts,) or " + "(nverts, ncomps), matching entity_field_values)"); } } // namespace pcms diff --git a/src/pcms/pythonapi/bind_omega_h_field_layout.cpp b/src/pcms/pythonapi/bind_omega_h_field_layout.cpp deleted file mode 100644 index 9574b4238..000000000 --- a/src/pcms/pythonapi/bind_omega_h_field_layout.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include -#include -#include -#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; - -namespace pcms -{ - -void bind_omega_h_field_layout_module(py::module& m) -{ - static_cast(m); -} - -} // namespace pcms diff --git a/src/pcms/pythonapi/bind_runtime.cpp b/src/pcms/pythonapi/bind_runtime.cpp new file mode 100644 index 000000000..4c72de686 --- /dev/null +++ b/src/pcms/pythonapi/bind_runtime.cpp @@ -0,0 +1,72 @@ +#include "pcms/configuration.h" +#include +#include "pcms/utility/assert.h" +#include +#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) +#include +#endif + +namespace pcms +{ + +namespace +{ +bool g_py_owns_mpi = false; +bool g_py_owns_kokkos = false; +bool g_py_owns_petsc = false; +} // namespace + +// Initializes MPI/Kokkos/PETSc only if they are not already running (e.g. +// started by an embedding application or another module such as PyOmega_h), and +// records ownership so finalize() only tears down what this module started. +void initialize() +{ + int is_initialized = 0; + MPI_Initialized(&is_initialized); + if (!is_initialized) { + if (MPI_Init(nullptr, nullptr) != MPI_SUCCESS) { + throw pcms_error("pcms: MPI_Init failed"); + } + g_py_owns_mpi = true; + } + if (!Kokkos::is_initialized()) { + Kokkos::initialize(); + g_py_owns_kokkos = true; + } +#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) + PetscBool petsc_initialized = PETSC_FALSE; + PetscInitialized(&petsc_initialized); + if (!petsc_initialized) { + if (PetscInitializeNoArguments() != 0) { + throw pcms_error("pcms: PetscInitialize failed"); + } + g_py_owns_petsc = true; + } +#endif +} + +void finalize() +{ +#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) + if (g_py_owns_petsc) { + PetscBool petsc_finalized = PETSC_FALSE; + PetscFinalized(&petsc_finalized); + if (!petsc_finalized) { + PetscFinalize(); + } + g_py_owns_petsc = false; + } +#endif + if (g_py_owns_kokkos && !Kokkos::is_finalized()) { + Kokkos::finalize(); + g_py_owns_kokkos = false; + } + int is_finalized = 0; + MPI_Finalized(&is_finalized); + if (g_py_owns_mpi && !is_finalized) { + MPI_Finalize(); + g_py_owns_mpi = false; + } +} + +} // namespace pcms diff --git a/src/pcms/pythonapi/bind_transfer_field.cpp b/src/pcms/pythonapi/bind_transfer_field.cpp index cc599bb09..cc55b28f5 100644 --- a/src/pcms/pythonapi/bind_transfer_field.cpp +++ b/src/pcms/pythonapi/bind_transfer_field.cpp @@ -9,8 +9,12 @@ #include "../transfer/interpolator.h" #include "pcms/field/out_of_bounds_policy.h" #include "numpy_array_transform.h" +#include "py_value_types.h" +#include "pcms/field/point_evaluator_factory.hpp" +#include "pcms/transfer/transfer_operator.hpp" #include "pcms/utility/types.h" #if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) +#include "pcms/transfer/mass_matrix_type.hpp" #include "pcms/transfer/omega_h_conservative_projection.hpp" #include "pcms/transfer/omega_h_control_variate_projection.hpp" #include "pcms/transfer/omega_h_mc_rhs_integrator.hpp" @@ -25,80 +29,111 @@ namespace pcms void bind_transfer_field_module(py::module& m) { py::class_, std::unique_ptr>>( - m, "PointEvaluator") + m, "PointEvaluator", + "Evaluates fields at the query points it was created for") .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); + PyValueView output) { + WithDeviceOutput(output.values, [&](auto out) { + self.Evaluate(field, + ValueView(output.basis, out)); + }); }, 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)."); + "Evaluate the field into `output`, a ValueView over a writeable " + "C-contiguous float64 array (num_points × num_components) whose basis " + "must be output_basis() of the field's stored basis") + .def( + "evaluate", + [](const PointEvaluator& self, const Field& field, + py::array output) { + const ValueBasis basis = + self.OutputBasis(field.GetData().GetValueBasis()); + RequireScalarForPlainArray("evaluate", basis); + WithDeviceOutput(output, [&](auto out) { + self.Evaluate(field, ValueView(basis, out)); + }); + }, + py::arg("field"), py::arg("output").noconvert(), + "Evaluate a scalar field into a writeable C-contiguous float64 array " + "(num_points × num_components). A 1D array (num_points,) is accepted " + "when the output has one component.") + .def("num_points", &PointEvaluator::NumPoints) + .def("output_basis", &PointEvaluator::OutputBasis, py::arg("stored"), + "The basis this evaluator writes for a field stored in `stored`") + .def( + "filled_points", + [](py::object self) { + const auto filled = + self.cast&>().FilledPoints(); + if (filled.extent(0) == 0) { + return py::array_t(0); + } + return ViewToNumpy(Rank1View( + filled.data(), filled.extent(0)), + self); + }, + "Indices of the query points that get the out-of-bounds fill value, " + "read-only"); + + py::class_>(m, "TransferOperator") + .def("apply", &TransferOperator::Apply, py::arg("source"), + py::arg("target"), "Transfer the source field onto the target"); - // 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") + // Interpolator: construct once per source × target pair (localization + // happens at construction), then apply() repeatedly for different field + // states at zero additional localization cost. + py::class_, TransferOperator>(m, "Interpolator") .def( - py::init([](const FunctionSpace& source_space, + py::init([](const PointEvaluatorFactory& source, const FunctionSpace& target_space, OutOfBoundsPolicy policy) { - return Interpolator(source_space, target_space, policy); + return std::make_unique>(source, 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::arg("source"), py::arg("target_space"), + py::arg("policy") = OutOfBoundsPolicy{}, py::keep_alive<1, 2>(), + py::keep_alive<1, 3>(), + "Construct an interpolator from any PointEvaluatorFactory (a " + "FunctionSpace or a TransformedSource) to the target space's DOF " + "holders. Localization is performed here and cached."); - 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)."); + py::class_, TransferOperator>(m, "Copy").def( + py::init( + [](const FunctionSpace& source_space, const FunctionSpace& target_space) { + return std::make_unique>(source_space, target_space); + }), + py::arg("source_space"), py::arg("target_space"), + "Construct a copy operator for function spaces sharing a layout."); #if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) + // Mass-matrix formulation for the Galerkin projections. Bound before the + // projection classes so it can be used as a default argument value. + py::enum_(m, "MassMatrixType") + .value("Consistent", MassMatrixType::Consistent, + "Full Galerkin mass matrix") + .value("Lumped", MassMatrixType::Lumped, + "Row-sum lumped diagonal mass matrix (exact diagonal solve; " + "conserves the integral but adds lumping error)") + .export_values(); + // Conservative L2 (Galerkin) projection between order-1 Lagrange spaces on // Omega_h 2D simplex meshes. The RHS is integrated exactly over the // intersection of the source and target meshes. - py::class_(m, "OmegaHConservativeProjection") + py::class_>( + m, "OmegaHConservativeProjection") .def(py::init([](const FunctionSpace& source_space, - const FunctionSpace& target_space) { - return std::make_unique(source_space, - target_space); + const FunctionSpace& target_space, + MassMatrixType mass_matrix_type) { + return std::make_unique( + source_space, target_space, mass_matrix_type); }), py::arg("source_space"), py::arg("target_space"), + py::arg("mass_matrix_type") = MassMatrixType::Consistent, + py::keep_alive<1, 2>(), py::keep_alive<1, 3>(), "Construct a mesh-intersection conservative projection. Mesh " "intersection, quadrature setup, and mass-matrix factorization happen " - "here and are cached; call apply() repeatedly.") - .def( - "apply", - [](const OmegaHConservativeProjection& self, const Field& source, - Field& target) { self.Apply(source, target); }, - py::arg("source"), py::arg("target"), - "Conservatively project the source field onto the target space using " - "exact mesh-intersection quadrature."); + "here and are cached; call apply() repeatedly."); // Sampling strategy for the Monte Carlo RHS integrator. py::enum_(m, "MonteCarloSampling") @@ -110,28 +145,25 @@ void bind_transfer_field_module(py::module& m) // first interpolated onto the target space as a control variate; only the // residual is integrated stochastically, so sampling noise is small (and // exactly zero when the source already lives in the target space). - py::class_(m, - "OmegaHControlVariateProjection") + py::class_>( + m, "OmegaHControlVariateProjection") .def(py::init([](const FunctionSpace& source_space, const FunctionSpace& target_space, int samples_per_element, - MonteCarloSampling sampling, std::uint64_t seed) { + MonteCarloSampling sampling, std::uint64_t seed, + MassMatrixType mass_matrix_type) { return std::make_unique( - source_space, target_space, samples_per_element, sampling, seed); + source_space, target_space, samples_per_element, sampling, seed, + mass_matrix_type); }), py::arg("source_space"), py::arg("target_space"), py::arg("samples_per_element"), py::arg("sampling") = MonteCarloSampling::UniformRandom, py::arg("seed") = std::uint64_t(8675309), + py::arg("mass_matrix_type") = MassMatrixType::Consistent, + py::keep_alive<1, 2>(), py::keep_alive<1, 3>(), "Construct a Monte Carlo (control-variate) conservative projection. " "Sample-point generation, the control-variate interpolator, and the " - "mass-matrix factorization are cached; call apply() repeatedly.") - .def( - "apply", - [](const OmegaHControlVariateProjection& self, const Field& source, - Field& target) { self.Apply(source, target); }, - py::arg("source"), py::arg("target"), - "Project the source field onto the target space using Monte Carlo " - "integration with control-variate variance reduction."); + "mass-matrix factorization are cached; call apply() repeatedly."); #endif } diff --git a/src/pcms/pythonapi/bind_uniform_grid_field.cpp b/src/pcms/pythonapi/bind_uniform_grid_field.cpp deleted file mode 100644 index bc211edb0..000000000 --- a/src/pcms/pythonapi/bind_uniform_grid_field.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include - -namespace py = pybind11; - -namespace pcms -{ - -// 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 96d0286ab..e4210c450 100644 --- a/src/pcms/pythonapi/bind_uniform_grid_field_layout.cpp +++ b/src/pcms/pythonapi/bind_uniform_grid_field_layout.cpp @@ -1,6 +1,7 @@ #include #include #include +#include "numpy_array_transform.h" #include "pcms/field/layout/uniform_grid.h" #include "pcms/utility/uniform_grid.h" @@ -26,13 +27,13 @@ void bind_uniform_grid_field_layout_module(py::module& m) "Get the total number of cells in the grid") .def( "closest_cell_id", - [](const UniformGrid<2>& self, py::array_t point) { - if (point.size() != 2) { - throw std::runtime_error("Point must have 2 coordinates for 2D grid"); + [](const UniformGrid<2>& self, const NumpyInput& point_arr) { + auto point = NumpyToHostView<1, Real>(point_arr); + if (point.extent(0) != 2) { + throw std::invalid_argument( + "Point must have 2 coordinates for 2D grid"); } - auto buf = point.request(); - Real* ptr = static_cast(buf.ptr); - Omega_h::Vector<2> omega_point({ptr[0], ptr[1]}); + Omega_h::Vector<2> omega_point({point(0), point(1)}); return self.ClosestCellID(omega_point); }, py::arg("point"), @@ -53,13 +54,13 @@ void bind_uniform_grid_field_layout_module(py::module& m) "Get the total number of cells in the grid") .def( "closest_cell_id", - [](const UniformGrid<3>& self, py::array_t point) { - if (point.size() != 3) { - throw std::runtime_error("Point must have 3 coordinates for 3D grid"); + [](const UniformGrid<3>& self, const NumpyInput& point_arr) { + auto point = NumpyToHostView<1, Real>(point_arr); + if (point.extent(0) != 3) { + throw std::invalid_argument( + "Point must have 3 coordinates for 3D grid"); } - auto buf = point.request(); - Real* ptr = static_cast(buf.ptr); - Omega_h::Vector<3> omega_point({ptr[0], ptr[1], ptr[2]}); + Omega_h::Vector<3> omega_point({point(0), point(1), point(2)}); return self.ClosestCellID(omega_point); }, py::arg("point"), diff --git a/src/pcms/pythonapi/bind_value_view.cpp b/src/pcms/pythonapi/bind_value_view.cpp new file mode 100644 index 000000000..7ab4531c1 --- /dev/null +++ b/src/pcms/pythonapi/bind_value_view.cpp @@ -0,0 +1,201 @@ +#include +#include +#include + +#include +#include +#include + +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/value_view.hpp" +#include "py_value_types.h" + +namespace py = pybind11; + +namespace pcms +{ + +namespace +{ + +VarianceSignature MakeVarianceSignature(const std::vector& v) +{ + switch (v.size()) { + case 0: return VarianceSignature{}; + case 1: return VarianceSignature{v[0]}; + case 2: return VarianceSignature{v[0], v[1]}; + case 3: return VarianceSignature{v[0], v[1], v[2]}; + case 4: return VarianceSignature{v[0], v[1], v[2], v[3]}; + default: break; + } + throw py::value_error("VarianceSignature: rank must be 0.." + + std::to_string(VarianceSignature::max_rank) + ", got " + + std::to_string(v.size())); +} + +std::string Repr(const VarianceSignature& v) +{ + std::string s = "["; + for (int i = 0; i < v.Rank(); ++i) { + s += (i > 0 ? ", " : ""); + s += v[i] == Variance::Contravariant ? "Contravariant" : "Covariant"; + } + return s + "]"; +} + +std::string Repr(const ValueBasis& b) +{ + if (b.Rank() == 0) { + return ""; + } + return "Kind()) : std::string("None")) + + " " + + (b.scaling == ComponentScaling::Physical ? "Physical" : "Natural") + + " " + Repr(b.variance) + ">"; +} + +} // namespace + +void bind_value_view_module(py::module& m) +{ + py::enum_(m, "Variance", + "Index variance: Contravariant components are on the " + "basis dx/dq^i, Covariant on the dual grad q^i") + .value("Contravariant", Variance::Contravariant) + .value("Covariant", Variance::Covariant); + + py::enum_( + m, "ComponentScaling", + "How components are scaled relative to the coordinate-induced basis") + .value("Physical", ComponentScaling::Physical, + "Components on the unit basis; all have the same units") + .value("Natural", ComponentScaling::Natural, + "Components on the unscaled coordinate basis"); + + py::enum_(m, "FieldValueType") + .value("Scalar", FieldValueType::Scalar) + .value("Vector", FieldValueType::Vector) + .value("Tensor", FieldValueType::Tensor); + + py::class_(m, "VarianceSignature", + "The variance of each index of a value") + .def(py::init<>(), "Rank 0 (scalar)") + .def(py::init(&MakeVarianceSignature), py::arg("variance"), + "From a list of Variance, one per index") + .def("rank", &VarianceSignature::Rank) + .def("__len__", &VarianceSignature::Rank) + .def("__getitem__", + [](const VarianceSignature& self, int i) { + if (i < 0) { + i += self.Rank(); + } + if (i < 0 || i >= self.Rank()) { + throw py::index_error("VarianceSignature index out of range"); + } + return self[i]; + }) + .def( + "__eq__", + [](const VarianceSignature& a, const VarianceSignature& b) { + return a == b; + }, + py::is_operator()) + .def("__repr__", [](const VarianceSignature& self) { + return ""; + }); + + auto values = m.def_submodule("values", "Common variance signatures"); + values.attr("Scalar") = values::Scalar; + values.attr("Vector") = values::Vector; + values.attr("Covector") = values::Covector; + values.attr("Tensor") = values::Tensor; + values.def("Of", &MakeVarianceSignature, py::arg("variance"), + "Variance signature with the given variance per index"); + + py::class_( + m, "ValueBasis", + "The basis a value's components are expressed in. The default is the " + "scalar basis.") + .def(py::init([](const std::shared_ptr& system, + ComponentScaling scaling, VarianceSignature variance) { + return ValueBasis{system, scaling, variance}; + }), + py::arg("system") = py::none(), + py::arg("scaling") = ComponentScaling::Physical, + py::arg("variance") = VarianceSignature{}) + .def_property( + "system", [](const ValueBasis& self) { return ToPyHolder(self.system); }, + [](ValueBasis& self, const std::shared_ptr& system) { + self.system = system; + }) + .def_readwrite("scaling", &ValueBasis::scaling) + .def_readwrite("variance", &ValueBasis::variance) + .def("rank", &ValueBasis::Rank) + .def( + "__eq__", + [](const ValueBasis& a, const ValueBasis& b) { + return SameValueBasis(a, b); + }, + py::is_operator(), + "Whether the two bases describe the same components (SameValueBasis)") + .def("__repr__", [](const ValueBasis& self) { return Repr(self); }); + + m.def("same_value_basis", &SameValueBasis, py::arg("a"), py::arg("b")); + + py::class_(m, "ValueView", + "Values (num_points × num_components) tagged with " + "the basis their components are in") + .def(py::init([](const ValueBasis& basis, const py::object& values) { + py::array array = py::array::ensure(values); + if (!array) { + throw py::type_error("ValueView: values must be array-like"); + } + if (array.ndim() != 2) { + throw py::value_error("ValueView: expected a 2D array, got a " + + std::to_string(array.ndim()) + "D array"); + } + detail::ValidateValueSemantics(basis, + static_cast(array.shape(1))); + return PyValueView{basis, array}; + }), + py::arg("basis"), py::arg("values"), + "Tag a 2D array with a basis. The array is not copied: a ValueView " + "passed as an output is written in place.") + .def("get_basis", [](const PyValueView& self) { return self.basis; }) + .def("get_values", [](const PyValueView& self) { return self.values; }) + .def("rank", [](const PyValueView& self) { return self.basis.Rank(); }) + .def("get_value_type", + [](const PyValueView& self) { + return ValueTypeOfRank(self.basis.Rank()); + }) + .def( + "extent", + [](const PyValueView& self, int r) { + if (r < 0 || r > 1) { + throw py::index_error("ValueView.extent: rank must be 0 or 1"); + } + return self.values.shape(r); + }, + py::arg("r")) + .def("size", [](const PyValueView& self) { return self.values.size(); }) + .def( + "__array__", + [](const PyValueView& self, const py::object& dtype, + const py::object& copy) { + auto np = py::module_::import("numpy"); + if (!copy.is_none() && copy.cast()) { + return np.attr("array")(self.values, dtype); + } + return np.attr("asarray")(self.values, dtype); + }, + py::arg("dtype") = py::none(), py::arg("copy") = py::none(), + "The values, so numpy functions accept a ValueView") + .def("__repr__", [](const PyValueView& self) { + return ""; + }); +} + +} // namespace pcms diff --git a/src/pcms/pythonapi/numpy_array_transform.h b/src/pcms/pythonapi/numpy_array_transform.h index ccf498fae..407d0c36c 100644 --- a/src/pcms/pythonapi/numpy_array_transform.h +++ b/src/pcms/pythonapi/numpy_array_transform.h @@ -4,257 +4,89 @@ #include #include "pcms/utility/arrays.h" #include "pcms/utility/types.h" +#include "numpy_view.h" namespace py = pybind11; -namespace pcms -{ - -// Helper function to convert numpy array to Rank1 View -template -Rank1View numpy_to_view(py::array_t arr) -{ - py::buffer_info buf = arr.request(); - if (buf.ndim != 1) { - throw std::runtime_error("Number of dimensions must be 1"); - } - T* data_ptr = reinterpret_cast(buf.ptr); - Rank1View view(data_ptr, buf.shape[0]); - return view; -} - -// Helper function to convert numpy array to Kokkos View -template -Kokkos::View numpy_to_kokkos_view(py::array_t arr) -{ - py::buffer_info buf = arr.request(); - if (buf.ndim != 1) { - throw std::runtime_error("Number of dimensions must be 1"); - } - Kokkos::View view(reinterpret_cast(buf.ptr), - buf.shape[0]); - return view; -} - -// Helper function to convert Rank1 View to array (creates a reference, -// zero-copy) Note: Caller must use pybind11::keep_alive to manage the source -// object's lifetime -template -py::array_t::type> view_to_numpy( - Rank1View view) -{ - using NonConstT = typename std::remove_const::type; - return py::array_t( - {static_cast(view.extent(0))}, // shape - {sizeof(NonConstT)}, // strides - const_cast(view.data_handle()) // data pointer - // No base object - lifetime managed by keep_alive<> policy at binding site - ); -} - -// Overload: Helper function to convert Rank2 View to array (creates a -// reference, zero-copy) Note: Caller must use pybind11::keep_alive to manage -// the source object's lifetime -template -py::array_t::type> view_to_numpy( - Rank2View view) -{ - using NonConstT = typename std::remove_const::type; - return py::array_t( - {static_cast(view.extent(0)), - static_cast(view.extent(1))}, // shape - {sizeof(NonConstT) * view.extent(1), - sizeof(NonConstT)}, // strides (row-major) - const_cast(view.data_handle()) // data pointer - // No base object - lifetime managed by keep_alive<> policy at binding site - ); -} - -// Helper function to convert Kokkos View to array (creates a reference) -template -py::array_t kokkos_view_to_numpy(Kokkos::View view) -{ - return py::array_t({static_cast(view.extent(0))}, // shape - {sizeof(T)}, // strides - view.data(), // data pointer - py::cast(view) // base object to manage lifetime - ); -} - -// Helper to convert 2D numpy array to Kokkos View -template -Kokkos::View numpy_to_kokkos_view_2d(py::array_t arr) -{ - py::buffer_info buf = arr.request(); - if (buf.ndim != 2) { - throw std::runtime_error("Number of dimensions must be 2"); - } - Kokkos::View view(reinterpret_cast(buf.ptr), - buf.shape[0], buf.shape[1]); - return view; -} - -// Helper to convert 3D numpy array to Kokkos View -template -Kokkos::View numpy_to_kokkos_view_3d(py::array_t arr) -{ - py::buffer_info buf = arr.request(); - if (buf.ndim != 3) { - throw std::runtime_error("Number of dimensions must be 3"); +// Mirror pybind11's built-in std::array type caster for Kokkos::Array so that +// def_readwrite / def_property members work with Python sequences. +namespace pybind11::detail +{ +template +struct type_caster> +{ + using ArrayType = Kokkos::Array; + using value_conv = make_caster; + + bool load(handle src, bool convert) + { + if (!isinstance(src)) + return false; + auto seq = reinterpret_borrow(src); + if (seq.size() != Size) + return false; + size_t i = 0; + for (auto it : seq) { + value_conv conv; + if (!conv.load(it, convert)) + return false; + value[i++] = cast_op(conv); + } + return true; } - Kokkos::View view( - reinterpret_cast(buf.ptr), buf.shape[0], buf.shape[1], buf.shape[2]); - return view; -} - -// Overload: Helper to convert 2D Kokkos View to numpy array (creates a -// reference) -template -py::array_t kokkos_view_to_numpy(Kokkos::View view) -{ - return py::array_t( - {static_cast(view.extent(0)), - static_cast(view.extent(1))}, // shape - {sizeof(T) * view.extent(1), sizeof(T)}, // strides (row-major) - view.data(), // data pointer - py::cast(view) // base object to manage lifetime - ); -} - -// Overload: Helper to convert 3D Kokkos View to numpy array (creates a -// reference) -template -py::array_t kokkos_view_to_numpy(Kokkos::View view) -{ - return py::array_t( - {static_cast(view.extent(0)), - static_cast(view.extent(1)), - static_cast(view.extent(2))}, // shape - {sizeof(T) * view.extent(1) * view.extent(2), sizeof(T) * view.extent(2), - sizeof(T)}, // strides (row-major) - view.data(), // data pointer - py::cast(view) // base object to manage lifetime - ); -} -// Helper to convert 1D numpy array to Omega_h::Read -template -Omega_h::Read numpy_to_omega_h_read(py::array_t arr) -{ - py::buffer_info buf = arr.request(); - if (buf.ndim != 1) { - throw std::runtime_error("Number of dimensions must be 1"); + template + static handle cast(T&& src, return_value_policy policy, handle parent) + { + list l(Size); + size_t i = 0; + for (auto& v : src) + l[i++] = make_caster::cast(v, policy, parent); + return l.release(); } - Kokkos::View> - view(reinterpret_cast(buf.ptr), buf.shape[0]); - Omega_h::Write write_view(view); - Omega_h::Read read_view(write_view); - return read_view; -} -// Helper to convert Omega_h::Read to numpy array (creates a copy) -template -py::array_t omega_h_read_to_numpy(Omega_h::Read read_view) -{ - auto read_view_host = Omega_h::HostRead(read_view); - py::array_t result(read_view_host.size()); - py::buffer_info buf = result.request(); - T* ptr = static_cast(buf.ptr); - for (Omega_h::LO i = 0; i < read_view_host.size(); ++i) { - ptr[i] = read_view_host[i]; - } - return result; -} + PYBIND11_TYPE_CASTER(ArrayType, const_name("List[") + + make_caster::name + const_name("]")); +}; +} // namespace pybind11::detail -// Helper to convert 1D numpy array to Omega_h::Write -template -Omega_h::Write numpy_to_omega_h_write(py::array_t arr) +namespace pcms { - py::buffer_info buf = arr.request(); - if (buf.ndim != 1) { - throw std::runtime_error("Number of dimensions must be 1"); - } - // Get host mirror and copy data - auto write_view_host = Omega_h::HostWrite(buf.shape[0]); - T* ptr = static_cast(buf.ptr); - for (Omega_h::LO i = 0; i < buf.shape[0]; ++i) { - write_view_host[i] = ptr[i]; - } - auto write_view = Omega_h::Write(write_view_host); - return write_view; -} -// Helper to convert Omega_h::Write to numpy array (creates a reference) -template -py::array_t omega_h_write_to_numpy(Omega_h::Write write_view) -{ - return py::array_t({static_cast(write_view.size())}, // shape - {sizeof(T)}, // strides - write_view.data(), // data pointer - py::cast(write_view) // base object to manage lifetime - ); -} +// Omega_h arrays (Read/Write) have storage in the default (device) memory +// space. -// Helper function to convert numpy array to Rank2 View template -Rank2View numpy_to_view_2d(py::array_t arr) +Omega_h::Read NumpyToOmegaHRead(const NumpyInput& arr) { - py::buffer_info buf = arr.request(); - if (buf.ndim != 2) { - throw std::runtime_error("Number of dimensions must be 2"); + Rank1View host( + arr.data(), static_cast(arr.size())); + if constexpr (is_host_accessible_v) { + return Omega_h::Read(Omega_h::Write( + Kokkos::View(const_cast(host.data_handle()), host.extent(0)))); + } else { + Kokkos::View device( + Kokkos::view_alloc(Kokkos::WithoutInitializing, "numpy_to_omega_h"), + host.extent(0)); + DeepCopyMismatchLayouts(device, MakeUnmanagedKokkosView(host)); + return Omega_h::Read(Omega_h::Write(device)); } - Rank2View view(reinterpret_cast(buf.ptr), - buf.shape[0], buf.shape[1]); - return view; } -// Helper function to convert numpy array to Rank3 View template -Rank3View numpy_to_view_3d(py::array_t arr) +py::array_t ToNumpy(Omega_h::Read arr) { - py::buffer_info buf = arr.request(); - if (buf.ndim != 3) { - throw std::runtime_error("Number of dimensions must be 3"); + Rank1View view(arr.data(), + arr.size()); + // Only the host branch dereferences owner; on device builds ViewToNumpy + // copies before the Read goes out of scope. + py::object owner = py::none(); + if constexpr (is_host_accessible_v) { + owner = py::capsule(new Omega_h::Read(std::move(arr)), [](void* p) { + delete static_cast*>(p); + }); } - Rank3View view(reinterpret_cast(buf.ptr), - buf.shape[0], buf.shape[1], buf.shape[2]); - return view; -} - -// Helper function to convert mdspan View to array (creates a reference, -// zero-copy) Note: Caller must use pybind11::keep_alive to manage the source -// object's lifetime -template -py::array_t::type> mdspan_view_to_numpy( - Rank2View view) -{ - using NonConstT = typename std::remove_const::type; - return py::array_t( - {static_cast(view.extent(0)), - static_cast(view.extent(1))}, // shape - {sizeof(NonConstT) * view.extent(1), - sizeof(NonConstT)}, // strides (row-major) - const_cast(view.data_handle()) // data pointer - // No base object - lifetime managed by keep_alive<> policy at binding site - ); -} - -template -py::array_t::type> mdspan_view_to_numpy( - Rank3View view) -{ - using NonConstT = typename std::remove_const::type; - return py::array_t( - {static_cast(view.extent(0)), - static_cast(view.extent(1)), - static_cast(view.extent(2))}, // shape - {sizeof(NonConstT) * view.extent(1) * view.extent(2), - sizeof(NonConstT) * view.extent(2), - sizeof(NonConstT)}, // strides (row-major) - const_cast(view.data_handle()) // data pointer - // No base object - lifetime managed by keep_alive<> policy at binding site - ); + return ViewToNumpy(view, owner); } -} // namespace pcms \ No newline at end of file +} // namespace pcms diff --git a/src/pcms/pythonapi/numpy_view.h b/src/pcms/pythonapi/numpy_view.h new file mode 100644 index 000000000..760188112 --- /dev/null +++ b/src/pcms/pythonapi/numpy_view.h @@ -0,0 +1,327 @@ +#ifndef PCMS_PYTHONAPI_NUMPY_VIEW_H +#define PCMS_PYTHONAPI_NUMPY_VIEW_H + +// Conversions between numpy arrays and pcms mdspan views / Kokkos views. +// +// Array semantics for python conversions +// +// Arrays returned for pcms-owned data (ViewToNumpy, ToNumpy of Omega_h data): +// - are host numpy arrays shaped like the C++ view, and read-only +// - are valid until the object they came from is modified +// - never dangle: they keep their owner alive or are copies. On host builds +// they share memory with pcms (zero-copy); otherwise they are copies +// Array inputs (NumpyInput): +// - accept any array-like convertible to the element type +// - may be read in place when only used during the call (NumpyToHostView) +// - are copied when pcms keeps the data (CopyNumpyToKokkosView), so later +// changes to the caller's array have no effect +// Output buffers (NumpyOutputToHostView): +// - must already be writeable, C-contiguous arrays of the element type +// Per-entity data (field DOF values, evaluation results): +// - is shaped (n, num_components) +// - returned arrays always have the component axis, so their rank does not +// depend on the number of components +// - inputs and output buffers may omit the component axis when +// num_components is 1 +// +// TODO: zero-copy for device memory via DLPack, with the same semantics. + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/types.h" + +namespace pcms +{ + +namespace py = pybind11; + +// Parameter type for numpy inputs. pybind11 converts the caller's array to a +// C-contiguous array of T, copying only when the array is strided (e.g. a +// column slice or a transpose) or has a different dtype. Inside the binding the +// parameter refers to the converted array, so it keeps the memory seen by +// NumpyToHostView alive for the duration of the call. +template +using NumpyInput = py::array_t; + +// True when memory in MemorySpace can be read directly by host code. +template +inline constexpr bool is_host_accessible_v = + Kokkos::SpaceAccessibility::accessible; + +namespace detail +{ + +template +struct KokkosDataType +{ + using type = typename KokkosDataType::type*; +}; +template +struct KokkosDataType +{ + using type = T; +}; +// KokkosDataType::type is T** +template +using kokkos_data_type_t = typename KokkosDataType::type; + +template +struct KokkosLayout; +template <> +struct KokkosLayout +{ + using type = Kokkos::LayoutRight; +}; +template <> +struct KokkosLayout +{ + using type = Kokkos::LayoutLeft; +}; + +template +std::array NumpyExtents(const py::array& arr) +{ + if (arr.ndim() != static_cast(Rank)) { + throw std::invalid_argument("expected a " + std::to_string(Rank) + + "D array, got a " + std::to_string(arr.ndim()) + + "D array"); + } + std::array extents{}; + for (std::size_t i = 0; i < Rank; ++i) { + extents[i] = static_cast(arr.shape(i)); + } + return extents; +} + +template +ViewType MakeKokkosViewImpl(Pointer ptr, const Extents& extents, + std::index_sequence) +{ + return ViewType(ptr, extents[Is]...); +} + +template +ViewType AllocateKokkosViewImpl(const std::string& label, + const Extents& extents, + std::index_sequence) +{ + return ViewType(Kokkos::view_alloc(Kokkos::WithoutInitializing, label), + extents[Is]...); +} + +template +std::vector NumpyShape( + const Kokkos::mdspan& + view) +{ + std::vector shape(Extents::rank()); + for (std::size_t i = 0; i < Extents::rank(); ++i) { + shape[i] = static_cast(view.extent(i)); + } + return shape; +} + +inline void SetReadOnly(py::array& arr) +{ + py::setattr(arr.attr("flags"), "writeable", py::bool_(false)); +} + +inline std::string ShapeString(const py::array& arr) +{ + std::string result = "("; + for (py::ssize_t i = 0; i < arr.ndim(); ++i) { + result += (i > 0 ? ", " : "") + std::to_string(arr.shape(i)); + } + return result + (arr.ndim() == 1 ? ",)" : ")"); +} + +// Extents of per-entity data shaped (n, num_components). A 1D array of length +// n is read as (n, 1) when num_components is 1. +inline std::array ComponentExtents(const py::array& arr, + LO num_components) +{ + if (arr.ndim() == 1 && num_components == 1) { + return {static_cast(arr.shape(0)), 1}; + } + if (arr.ndim() == 2 && arr.shape(1) == num_components) { + return {static_cast(arr.shape(0)), num_components}; + } + throw std::invalid_argument( + "expected an array of shape (n, " + std::to_string(num_components) + ")" + + (num_components == 1 ? " or (n,)" : "") + ", got " + ShapeString(arr)); +} + +} // namespace detail + +// Unmanaged Kokkos::View over the same memory as an mdspan, so Kokkos +// deep_copy and mirror utilities can be used on it. +template +auto MakeUnmanagedKokkosView( + const Kokkos::mdspan& + view) +{ + constexpr std::size_t rank = Extents::rank(); + using MemorySpace = detail::memory_space_selector_t; + using KokkosViewType = + Kokkos::View, + typename detail::KokkosLayout::type, MemorySpace, + Kokkos::MemoryTraits>; + std::array extents{}; + for (std::size_t i = 0; i < rank; ++i) { + extents[i] = view.extent(i); + } + return detail::MakeKokkosViewImpl( + view.data_handle(), extents, std::make_index_sequence{}); +} + +// Read-only host view of a numpy input, without copying. The view aliases +// `arr`, so `arr` must outlive it. +template +View NumpyToHostView( + const NumpyInput& arr) +{ + return View( + arr.data(), detail::NumpyExtents(arr)); +} + +// Writable host view of a caller-provided numpy output buffer. Results written +// to a converted copy would be lost, so instead of converting, require the +// buffer to already be a writeable, C-contiguous array of T. +namespace detail +{ +template +T* OutputBufferData(py::array& arr) +{ + if (!py::isinstance>(arr)) { + throw std::invalid_argument( + "output array must be C-contiguous with dtype " + + std::string(py::str(py::dtype::of()))); + } + if (!arr.writeable()) { + throw std::invalid_argument("output array must be writeable"); + } + return static_cast(arr.mutable_data()); +} +} // namespace detail + +template +View NumpyOutputToHostView( + py::array& arr) +{ + return View( + detail::OutputBufferData(arr), detail::NumpyExtents(arr)); +} + +// NumpyToHostView for per-entity data of shape (n, num_components), where the +// component axis may be omitted when num_components is 1. +template +Rank2View +NumpyToHostComponentView(const NumpyInput& arr, LO num_components) +{ + return Rank2View( + arr.data(), detail::ComponentExtents(arr, num_components)); +} + +// NumpyOutputToHostView for per-entity data of shape (n, num_components), +// where the component axis may be omitted when num_components is 1. +template +Rank2View +NumpyOutputToHostComponentView(py::array& arr, LO num_components) +{ + return Rank2View( + detail::OutputBufferData(arr), + detail::ComponentExtents(arr, num_components)); +} + +// Copy a numpy input into a new, owning Kokkos::View in MemorySpace (with that +// space's default layout). +template +Kokkos::View, MemorySpace> +CopyNumpyToKokkosView(const NumpyInput& arr, + const std::string& label = "numpy_copy") +{ + using KokkosViewType = + Kokkos::View, MemorySpace>; + const auto src = MakeUnmanagedKokkosView(NumpyToHostView(arr)); + auto dest = detail::AllocateKokkosViewImpl( + label, detail::NumpyExtents(arr), std::make_index_sequence{}); + DeepCopyMismatchLayouts(dest, src); + return dest; +} + +// Copy an mdspan (in any memory space, layout_left or layout_right) into a +// new C-ordered numpy array of the same shape. +template +py::array_t> ToNumpy( + const Kokkos::mdspan& + view) +{ + using T = std::remove_const_t; + constexpr std::size_t rank = Extents::rank(); + static_assert(rank > 0, "ToNumpy requires a view of rank >= 1"); + py::array_t result(detail::NumpyShape(view)); + if (result.size() == 0) { + return result; + } + auto dest = MakeUnmanagedKokkosView( + View( + result.mutable_data(), detail::NumpyExtents(result))); + DeepCopyMismatchLayouts(dest, MakeUnmanagedKokkosView(view)); + return result; +} + +// numpy array for an mdspan. When the view's memory is host-accessible the +// array aliases it (zero-copy, with the view's strides) and holds a reference +// to `owner`, which must keep the memory alive. Otherwise the data is copied. +// Either way the array is read-only when ElementType is const, so callers see +// the same behavior on host and device builds. +template +py::array_t> ViewToNumpy( + const Kokkos::mdspan& + view, + py::handle owner) +{ + using T = std::remove_const_t; + using MemorySpace = detail::memory_space_selector_t; + if constexpr (is_host_accessible_v) { + constexpr std::size_t rank = Extents::rank(); + std::vector strides(rank); + for (std::size_t i = 0; i < rank; ++i) { + strides[i] = static_cast(view.stride(i) * sizeof(T)); + } + py::array_t result(detail::NumpyShape(view), strides, view.data_handle(), + owner); + if constexpr (std::is_const_v) { + detail::SetReadOnly(result); + } + return result; + } else { + auto result = ToNumpy(view); + if constexpr (std::is_const_v) { + detail::SetReadOnly(result); + } + return result; + } +} + +} // namespace pcms + +#endif // PCMS_PYTHONAPI_NUMPY_VIEW_H diff --git a/src/pcms/pythonapi/py_value_types.h b/src/pcms/pythonapi/py_value_types.h new file mode 100644 index 000000000..002cbf828 --- /dev/null +++ b/src/pcms/pythonapi/py_value_types.h @@ -0,0 +1,156 @@ +#ifndef PCMS_PYTHONAPI_PY_VALUE_TYPES_H +#define PCMS_PYTHONAPI_PY_VALUE_TYPES_H + +// Python-side counterparts of the non-owning C++ view types. A C++ +// CoordinateView or ValueView borrows its memory, so the Python objects pair +// the same tag (coordinate system or value basis) with a numpy array that owns +// or keeps alive that memory, and are converted to the C++ view at each call. + +#include +#include +#include + +#include +#include +#include + +#include "numpy_view.h" +#include "pcms/configuration.h" +#include "pcms/field/coordinate_system.hpp" +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/field/coordinate_view.hpp" +#include "pcms/field/value_view.hpp" +#include "pcms/utility/arrays.h" +#include "pcms/utility/memory_spaces.h" +#include "pcms/utility/types.h" + +namespace pcms +{ + +namespace py = pybind11; + +// pybind11 holders cannot be shared_ptr. Python only reaches the +// const interface of these objects, so they are held as shared_ptr and +// converted at the boundary. +template +std::shared_ptr ToPyHolder(const std::shared_ptr& p) +{ + return std::const_pointer_cast(p); +} + +// Coordinate system arguments default to None in Python, meaning Cartesian in +// the dimension of the data they are used with. C++ resolves that dimension +// from a deferred Cartesian system, which Python never sees. +inline std::shared_ptr CartesianIfNone( + std::shared_ptr system) +{ + return system ? std::move(system) : csys::Cartesian::Deferred(); +} + +// Coordinates tagged with the coordinate system that gives them meaning. +// `values` is a read-only 2D float64 array (num_points × dim) that pcms owns +// or keeps alive. +struct PyCoordinateView +{ + std::shared_ptr system; + py::array_t values; +}; + +// Values tagged with the basis their components are expressed in. `values` is +// a 2D array (num_points × num_components): the caller's own array, or a +// read-only one returned by pcms. Calls that write it require it to be a +// writeable C-contiguous float64 array. +struct PyValueView +{ + ValueBasis basis; + py::array values; +}; + +// Python view of C++ coordinates, sharing memory with them when they are host +// accessible; `owner` must keep that memory alive. +template +PyCoordinateView ToPyCoordinateView( + const CoordinateView& view, py::handle owner) +{ + return PyCoordinateView{view.GetCoordinateSystem(), + ViewToNumpy(view.GetValues(), owner)}; +} + +// Python view of C++ values, sharing memory with them when they are host +// accessible; `owner` must keep that memory alive. +template +PyValueView ToPyValueView(const ValueView& view, + py::handle owner) +{ + return PyValueView{view.GetBasis(), ViewToNumpy(view.GetValues(), owner)}; +} + +// Device copy of the coordinates together with the CoordinateView over it. +// The view is valid while `storage` is alive. +struct DeviceCoordinates +{ + Kokkos::View storage; + CoordinateView view; +}; + +inline DeviceCoordinates CopyCoordinatesToDevice(const PyCoordinateView& coords) +{ + auto storage = CopyNumpyToKokkosView<2, Real, DeviceMemorySpace>( + NumpyInput::ensure(coords.values), "coordinates"); + return DeviceCoordinates{storage, CoordinateView( + coords.system, MakeRank2View(storage))}; +} + +// Number of components of per-entity data shaped (n, num_components), where a +// 1D array of length n has one component. The call the data is passed to +// checks the extents against what it expects. +inline LO PerEntityComponents(const py::array& arr) +{ + if (arr.ndim() == 1) { + return 1; + } + if (arr.ndim() == 2) { + return static_cast(arr.shape(1)); + } + throw std::invalid_argument( + "expected an array of shape (n, num_components) or (n,), got " + + detail::ShapeString(arr)); +} + +// Calls f with a device view over the caller's writeable output buffer and +// makes what f writes visible in that buffer. On host builds f writes the +// buffer in place; otherwise it writes a device copy that is copied back. +template +void WithDeviceOutput(py::array& output, F&& f) +{ + auto output_host = + NumpyOutputToHostComponentView(output, PerEntityComponents(output)); +#ifdef PCMS_HAS_DISTINCT_DEVICE_MEMORY_SPACE + // TODO: write in place on the device via DLPack. + auto output_view = MakeUnmanagedKokkosView(output_host); + auto output_device = Kokkos::View( + "output_device", output_view.extent(0), output_view.extent(1)); + DeepCopyMismatchLayouts(output_device, output_view); + std::forward(f)(MakeRank2View(output_device)); + DeepCopyMismatchLayouts(output_view, output_device); +#else + std::forward(f)(output_host); +#endif +} + +// Scalar values carry no basis, so for rank-0 values a plain array stands in +// for a ValueView. Values of higher rank must declare their basis. +inline void RequireScalarForPlainArray(const char* context, + const ValueBasis& basis) +{ + if (basis.Rank() != 0) { + throw py::type_error(std::string(context) + ": rank-" + + std::to_string(basis.Rank()) + + " values need a ValueView declaring their basis; a " + "plain array is accepted only for scalar values"); + } +} + +} // namespace pcms + +#endif // PCMS_PYTHONAPI_PY_VALUE_TYPES_H diff --git a/src/pcms/pythonapi/pythonapi.cpp b/src/pcms/pythonapi/pythonapi.cpp index e34913776..125f39823 100644 --- a/src/pcms/pythonapi/pythonapi.cpp +++ b/src/pcms/pythonapi/pythonapi.cpp @@ -1,87 +1,71 @@ - #include #include "pcms/configuration.h" -#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) -#include -#endif +#include "numpy_view.h" +#include namespace py = pybind11; namespace pcms { -void bind_omega_h_field(py::module& m); - -void bind_transfer_field_module(py::module& m); +// Defined in bind_runtime.cpp. initialize() brings up MPI/Kokkos/PETSc only if +// they are not already running (e.g. an embedding application or another +// pybind11 module such as PyOmega_h already owns them); finalize() tears +// down only the pieces this module itself started. +void initialize(); +void finalize(); void bind_coordinate_system_module(py::module& m); -void bind_coordinate_module(py::module& m); - -void bind_create_field_module(py::module& m); +void bind_value_view_module(py::module& m); -void bind_field_layout_module(py::module& m); +void bind_omega_h_field(py::module& m); -void bind_field_module(py::module& m); +void bind_uniform_grid_field_layout_module(py::module& m); -void bind_omega_h_field_layout_module(py::module& m); +void bind_create_field_module(py::module& m); -void bind_uniform_grid_field_layout_module(py::module& m); +void bind_transfer_field_module(py::module& m); -void bind_uniform_grid_field_module(py::module& m); +void bind_coordinate_map_module(py::module& m); void bind_mls_interpolation_module(py::module& m); void bind_mesh_utilities_module(py::module& m); } // namespace pcms + +namespace +{ +void py_atexit_finalize() +{ + pcms::finalize(); +} +} // namespace + PYBIND11_MODULE(pcms, m) { -#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) - // The conservative/Monte Carlo projection solvers build PETSc objects on - // PETSC_COMM_WORLD, so PETSc must be initialized before any of them are - // constructed. Do it once at import time (PetscInitialize brings up MPI if it - // is not already running) so callers never manage PETSc state by hand. - // - // PETSc is intentionally NOT finalized via atexit: PETSc's Kokkos-backed - // objects must be torn down before Kokkos::finalize, but Kokkos is owned by - // OmegaHLibrary and an atexit hook would run after that library is destroyed. - // Leaving PETSc initialized until the process exits avoids that ordering trap - // and is harmless (the OS reclaims everything on exit). - { - PetscBool petsc_initialized = PETSC_FALSE; - PetscInitialized(&petsc_initialized); - if (!petsc_initialized) { - PetscInitializeNoArguments(); - } + // Bring up MPI/Kokkos/PETSc if nothing else (e.g. PyOmega_h) has already done + // so, and finalize whatever we own when the interpreter shuts down. + pcms::initialize(); + if (Py_AtExit(&py_atexit_finalize) != 0) { + throw std::runtime_error("pcms: failed to register Py_AtExit finalizer"); } -#endif - // Bind fundamental types first (coordinate systems, etc.) - pcms::bind_coordinate_system_module(m); - pcms::bind_coordinate_module(m); + // Whether arrays shared with numpy can alias pcms memory. + m.attr("device_memory_is_host_accessible") = + pcms::is_host_accessible_v; - // 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_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. + // Types used as default arguments are registered before their users: + // coordinate systems and value bases, then OutOfBoundsPolicy, then the + // function spaces, evaluators, and transfer operators that take them. + pcms::bind_coordinate_system_module(m); + pcms::bind_value_view_module(m); pcms::bind_omega_h_field(m); - // bind_create_field_module registers FunctionSpace, - // LagrangeFunctionSpace, and Field. + pcms::bind_uniform_grid_field_layout_module(m); pcms::bind_create_field_module(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 such as Interpolator and Copy. pcms::bind_transfer_field_module(m); - - // Bind interpolator operations + pcms::bind_coordinate_map_module(m); pcms::bind_mls_interpolation_module(m); - - // Bind mesh utility functions pcms::bind_mesh_utilities_module(m); } diff --git a/src/pcms/pythonapi/test_field_copy.py b/src/pcms/pythonapi/test_field_copy.py deleted file mode 100644 index 8609ea4f1..000000000 --- a/src/pcms/pythonapi/test_field_copy.py +++ /dev/null @@ -1,83 +0,0 @@ -import pcms -import PyOmega_h as omega_h -import numpy as np - -def test_copy(world, dim, order, num_components): - """Test copying omega_h field data.""" - nx = 100 - ny = 100 if dim > 1 else 0 - nz = 100 if dim > 2 else 0 - print(f"\nStarting test: dim={dim}, order={order}, num_components={num_components}") - - # Build mesh - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, - nx, ny, nz, - False - ) - print(f" Built {dim}D mesh with {nx}x{ny}x{nz} elements") - print(f" Mesh type: {type(mesh)}") - print(f" Mesh object: {mesh}") - - # Create factory and layout - print(" About to create factory...") - factory = pcms.LagrangeFunctionSpace.from_mesh( - mesh, order, num_components, pcms.CoordinateSystem.Cartesian - ) - print(f"Testing dim={dim}, order={order}, num_components={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}") - original.set_dof_holder_data(ids) - print(" Set data in original field") - - # Create copied field and copy data - copied = factory.create_field() - copier = pcms.Copy(factory, factory) - copier.apply(original, copied) - print(" Copied data to new field") - - # Get copied data - copied_array = copied.get_dof_holder_data() - - # Verify the copy - assert len(copied_array) == ndata, f"Expected {ndata} elements, got {len(copied_array)}" - - # Check that all values match - differences = np.abs(ids - copied_array) - num_matches = np.sum(differences < 1e-12) - - assert num_matches == ndata, f"Only {num_matches}/{ndata} elements matched" - - print(f"✓ Test passed: dim={dim}, order={order}, num_components={num_components}") - -def main(): - """Run all test cases""" - print("Testing copy omega_h field data...") - - # Initialize Omega_h library - lib = omega_h.OmegaHLibrary() - world = lib.world() - print("Initialized Omega_h library and world") - - # Run test cases - test_copy(world, 2, 1, 1) - print("first passed") - test_copy(world, 2, 2, 1) - - print("\nAll tests passed!") - - del world - del lib - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/pcms/pythonapi/test_file_io.py b/src/pcms/pythonapi/test_file_io.py deleted file mode 100644 index 7366af04b..000000000 --- a/src/pcms/pythonapi/test_file_io.py +++ /dev/null @@ -1,442 +0,0 @@ -#!/usr/bin/env python3 -""" -Test Omega_h file I/O Python bindings -""" - -import pcms -import PyOmega_h as omega_h -import numpy as np -import os -import shutil -import gc -import tempfile - -def test_binary_io(lib, world): - """Test binary file format I/O""" - print("\n=== Testing Binary Format I/O ===") - - # Build a simple 3D box mesh - print("Creating test mesh...") - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, # x, y, z dimensions - 4, 4, 4, # nx, ny, nz divisions - False # symmetric - ) - - # Get initial mesh properties - nverts_orig = mesh.nverts() - nelems_orig = mesh.nelems() - dim_orig = mesh.dim() - print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - - # 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") - print(f"Writing to binary file: {binary_file}") - omega_h.write_mesh_binary(binary_file, mesh) - - print(f"Reading from binary file: {binary_file}") - mesh_read = omega_h.read_mesh_binary(binary_file, lib) - - # Verify mesh properties - assert mesh_read.dim() == dim_orig, f"Dimension mismatch: {mesh_read.dim()} != {dim_orig}" - assert mesh_read.nverts() == nverts_orig, f"Vertex count mismatch: {mesh_read.nverts()} != {nverts_orig}" - assert mesh_read.nelems() == nelems_orig, f"Element count mismatch: {mesh_read.nelems()} != {nelems_orig}" - - 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 - shutil.rmtree(test_dir, ignore_errors=True) - print(f"Cleaned up test directory: {test_dir}") - - -def test_gmsh_io(lib, world): - """Test Gmsh file format I/O""" - print("\n=== Testing Gmsh Format I/O ===") - - # Build a simple 2D box mesh - print("Creating test mesh...") - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 2.0, 1.0, 0.0, # x, y, z dimensions (z=0 for 2D) - 5, 3, 0, # nx, ny, nz divisions (nz=0 for 2D) - False # symmetric - ) - - # Get initial mesh properties - nverts_orig = mesh.nverts() - nelems_orig = mesh.nelems() - dim_orig = mesh.dim() - print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - - # 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") - print(f"Writing to Gmsh file: {gmsh_file}") - omega_h.write_mesh_gmsh(gmsh_file, mesh) - - print(f"Reading from Gmsh file: {gmsh_file}") - mesh_read = omega_h.read_mesh_gmsh(gmsh_file, world) - - # Verify mesh properties - assert mesh_read.dim() == dim_orig, f"Dimension mismatch: {mesh_read.dim()} != {dim_orig}" - assert mesh_read.nverts() == nverts_orig, f"Vertex count mismatch: {mesh_read.nverts()} != {nverts_orig}" - assert mesh_read.nelems() == nelems_orig, f"Element count mismatch: {mesh_read.nelems()} != {nelems_orig}" - - 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 - shutil.rmtree(test_dir, ignore_errors=True) - print(f"Cleaned up test directory: {test_dir}") - - -def test_vtk_io(lib, world): - """Test VTK file format I/O""" - print("\n=== Testing VTK Format I/O ===") - - # Build a simple 3D box mesh - print("Creating test mesh...") - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.5, 1.0, 0.5, # x, y, z dimensions - 3, 3, 2, # nx, ny, nz divisions - False # symmetric - ) - - # Get initial mesh properties - nverts_orig = mesh.nverts() - nelems_orig = mesh.nelems() - dim_orig = mesh.dim() - coords_orig = mesh.coords() - print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - print(f"Coordinates shape: {coords_orig.shape}") - - # 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") - print(f"Writing to VTU file: {vtu_file}") - omega_h.write_mesh_vtu(vtu_file, mesh, compress=False) - - # Verify file was created - assert os.path.exists(vtu_file), f"VTU file was not created: {vtu_file}" - file_size = os.path.getsize(vtu_file) - print(f"VTU file created successfully (size: {file_size} bytes)") - - # Test compressed VTU write - vtu_compressed = os.path.join(test_dir, "test_mesh_compressed.vtu") - print(f"Writing compressed VTU file: {vtu_compressed}") - omega_h.write_mesh_vtu(vtu_compressed, mesh, compress=True) - - assert os.path.exists(vtu_compressed), f"Compressed VTU file was not created" - compressed_size = os.path.getsize(vtu_compressed) - print(f"Compressed VTU file created (size: {compressed_size} bytes)") - - print("✓ VTK I/O test passed") - - # Explicitly delete mesh objects before cleanup - del mesh - gc.collect() # Force garbage collection - - finally: - # Clean up temporary files - shutil.rmtree(test_dir, ignore_errors=True) - print(f"Cleaned up test directory: {test_dir}") - - -def test_meshb_io(lib, world): - """Test MESHB file format I/O""" - print("\n=== Testing MESHB Format I/O ===") - - # Check if MESHB support is available - if not hasattr(omega_h, 'write_mesh_meshb'): - print("⊘ MESHB support not available (OMEGA_H_USE_LIBMESHB not enabled)") - return - - # Build a simple 3D box mesh - print("Creating test mesh...") - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, # x, y, z dimensions - 3, 3, 3, # nx, ny, nz divisions - False # symmetric - ) - - # Get initial mesh properties - nverts_orig = mesh.nverts() - nelems_orig = mesh.nelems() - dim_orig = mesh.dim() - print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - - # 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") - print(f"Writing to MESHB file: {meshb_file}") - omega_h.write_mesh_meshb(mesh, meshb_file, version=2) - - print(f"Reading from MESHB file: {meshb_file}") - # MESHB read requires pre-created mesh object - mesh_read = omega_h.Mesh(lib) - mesh_read.set_comm(world) - omega_h.read_mesh_meshb(mesh_read, meshb_file) - - # Verify mesh properties - assert mesh_read.dim() == dim_orig, f"Dimension mismatch: {mesh_read.dim()} != {dim_orig}" - assert mesh_read.nverts() == nverts_orig, f"Vertex count mismatch: {mesh_read.nverts()} != {nverts_orig}" - assert mesh_read.nelems() == nelems_orig, f"Element count mismatch: {mesh_read.nelems()} != {nelems_orig}" - - 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 - shutil.rmtree(test_dir, ignore_errors=True) - print(f"Cleaned up test directory: {test_dir}") - - -def test_exodus_io(lib, world): - """Test Exodus file format I/O""" - print("\n=== Testing Exodus Format I/O ===") - - # Check if Exodus support is available - if not hasattr(omega_h, 'write_mesh_exodus'): - print("⊘ Exodus support not available (OMEGA_H_USE_SEACASEXODUS not enabled)") - return - - # Build a simple 3D box mesh - print("Creating test mesh...") - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, # x, y, z dimensions - 3, 3, 3, # nx, ny, nz divisions - False # symmetric - ) - - # Get initial mesh properties - nverts_orig = mesh.nverts() - nelems_orig = mesh.nelems() - dim_orig = mesh.dim() - print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - - # 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") - print(f"Writing to Exodus file: {exodus_file}") - omega_h.write_mesh_exodus(exodus_file, mesh, verbose=False) - - # Test Exodus file handle API - if hasattr(omega_h, 'exodus_open'): - print(f"Testing Exodus file handle API with: {exodus_file}") - - # Open the file - exo_handle = omega_h.exodus_open(exodus_file, verbose=False) - print(f"Opened Exodus file with handle: {exo_handle}") - - # Get number of time steps - num_steps = omega_h.exodus_get_num_time_steps(exo_handle) - print(f"Number of time steps: {num_steps}") - - # Read mesh using file handle - mesh_from_handle = omega_h.OmegaHMesh(lib) - mesh_from_handle.set_comm(world) - omega_h.read_mesh_exodus(exo_handle, mesh_from_handle, verbose=False) - - # Verify mesh properties - assert mesh_from_handle.dim() == dim_orig, f"Dimension mismatch: {mesh_from_handle.dim()} != {dim_orig}" - assert mesh_from_handle.nverts() == nverts_orig, f"Vertex count mismatch: {mesh_from_handle.nverts()} != {nverts_orig}" - assert mesh_from_handle.nelems() == nelems_orig, f"Element count mismatch: {mesh_from_handle.nelems()} != {nelems_orig}" - - # Close the file - omega_h.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 - shutil.rmtree(test_dir, ignore_errors=True) - print(f"Cleaned up test directory: {test_dir}") - - -def test_adios2_io(lib, world): - """Test ADIOS2 file format I/O""" - print("\n=== Testing ADIOS2 Format I/O ===") - - # Check if ADIOS2 support is available - if not hasattr(omega_h, 'write_mesh_adios2'): - print("⊘ ADIOS2 support not available (OMEGA_H_USE_ADIOS2 not enabled)") - return - - # Build a simple 3D box mesh - print("Creating test mesh...") - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, # x, y, z dimensions - 3, 3, 3, # nx, ny, nz divisions - False # symmetric - ) - - # Get initial mesh properties - nverts_orig = mesh.nverts() - nelems_orig = mesh.nelems() - dim_orig = mesh.dim() - print(f"Original mesh: dim={dim_orig}, nverts={nverts_orig}, nelems={nelems_orig}") - - # 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") - print(f"Writing to ADIOS2 file: {adios2_file}") - omega_h.write_mesh_adios2(adios2_file, mesh, prefix="") - - print(f"Reading from ADIOS2 file: {adios2_file}") - mesh_read = omega_h.read_mesh_adios2(adios2_file, lib, prefix="") - - # Verify mesh properties - assert mesh_read.dim() == dim_orig, f"Dimension mismatch: {mesh_read.dim()} != {dim_orig}" - assert mesh_read.nverts() == nverts_orig, f"Vertex count mismatch: {mesh_read.nverts()} != {nverts_orig}" - assert mesh_read.nelems() == nelems_orig, f"Element count mismatch: {mesh_read.nelems()} != {nelems_orig}" - - 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 - shutil.rmtree(test_dir, ignore_errors=True) - print(f"Cleaned up test directory: {test_dir}") - - -def test_read_mesh_file_auto_detect(lib, world): - """Test automatic format detection with read_mesh_file""" - print("\n=== Testing Automatic Format Detection ===") - - # Build a test mesh - print("Creating test mesh...") - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, - 3, 3, 3, - False - ) - - nverts_orig = mesh.nverts() - nelems_orig = mesh.nelems() - - # 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") - gmsh_file = os.path.join(test_dir, "mesh.msh") - - omega_h.write_mesh_binary(binary_file, mesh) - omega_h.write_mesh_gmsh(gmsh_file, mesh) - - # Test auto-detection for binary format - print(f"Auto-detecting binary file: {binary_file}") - mesh_binary = omega_h.read_mesh_file(binary_file, world) - assert mesh_binary.nverts() == nverts_orig - assert mesh_binary.nelems() == nelems_orig - print("✓ Binary auto-detection passed") - - # Test auto-detection for Gmsh format - print(f"Auto-detecting Gmsh file: {gmsh_file}") - mesh_gmsh = omega_h.read_mesh_file(gmsh_file, world) - assert mesh_gmsh.nverts() == nverts_orig - 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 - shutil.rmtree(test_dir, ignore_errors=True) - print(f"Cleaned up test directory: {test_dir}") - -if __name__ == "__main__": - print("=" * 60) - print("Omega_h File I/O Python Binding Tests") - print("=" * 60) - - # Create library and world communicator once for all tests - lib = omega_h.OmegaHLibrary() - world = lib.world() - - try: - test_binary_io(lib, world) - test_gmsh_io(lib, world) - test_vtk_io(lib, world) - test_meshb_io(lib, world) - test_exodus_io(lib, world) - test_adios2_io(lib, world) - test_read_mesh_file_auto_detect(lib, world) - - print("\n" + "=" * 60) - print("✓ All tests passed!") - print("=" * 60) - - except Exception as e: - print("\n" + "=" * 60) - print(f"✗ Test failed with error: {e}") - print("=" * 60) - import traceback - traceback.print_exc() - exit(1) - finally: - # Explicitly delete objects to avoid an MPI finalizing issue - del world - del lib \ No newline at end of file diff --git a/src/pcms/pythonapi/test_mls_interpolation.py b/src/pcms/pythonapi/test_mls_interpolation.py deleted file mode 100644 index df3f116b9..000000000 --- a/src/pcms/pythonapi/test_mls_interpolation.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 -""" -Tests for MLS-backed PolynomialReconstructionFunctionSpace using the -Field-based API. -""" - -import numpy as np -import pcms -import PyOmega_h as omega_h - - -def poly_value(x, y, degree): - if degree == 0: - return 3.0 - if degree == 1: - return x + y - if degree == 2: - return x**2 + y**2 - if degree == 3: - return x**3 + y**3 - raise ValueError(f"Unsupported polynomial degree: {degree}") - - -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) - source_xy = np.array([(x, y) for x in grid_vals for y in grid_vals], - dtype=np.float64) - target_xy = np.array( - [ - [0.11, 0.19], - [0.23, 0.77], - [0.51, 0.49], - [0.87, 0.26], - [0.69, 0.91], - [0.35, 0.62], - ], - dtype=np.float64, - ) - num_targets = target_xy.shape[0] - - 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], - dtype=np.float64, - ) - exact_target_values = np.array( - [poly_value(x, y, func_degree) for (x, y) in target_xy], - dtype=np.float64, - ) - - 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, ( - f"MLS failed for interp_degree={interp_degree}, " - f"func_degree={func_degree}: max_abs_err={max_abs_err}" - ) - - print("MLS polynomial reproduction test passed.") - - -if __name__ == "__main__": - lib = omega_h.OmegaHLibrary() - world = lib.world() - - try: - test_mls_interpolation_polynomial_reproduction() - print("MLS interpolation test passed") - except Exception as e: - print(f"MLS interpolation test failed: {e}") - import traceback - traceback.print_exc() - exit(1) - finally: - del world - del lib \ No newline at end of file diff --git a/src/pcms/pythonapi/test_omega_h_field.py b/src/pcms/pythonapi/test_omega_h_field.py deleted file mode 100644 index 4ef1db706..000000000 --- a/src/pcms/pythonapi/test_omega_h_field.py +++ /dev/null @@ -1,339 +0,0 @@ -#!/usr/bin/env python3 -""" -Field-centric Python examples for Omega_h-backed function spaces. -""" - -import numpy as np -import pcms -import PyOmega_h as omega_h - - -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 - - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, - nx, ny, nz, - False - ) - - factory = pcms.LagrangeFunctionSpace.from_mesh( - mesh, order, num_components, pcms.CoordinateSystem.Cartesian - ) - field = factory.create_field() - - 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 = omega_h.build_box( - world, - omega_h.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() - - 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) - - 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) - - -def test_field_evaluation(world, dim, order, num_components): - """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}, " - f"num_components={num_components}" - ) - - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, - nx, ny, nz, - False - ) - - factory = pcms.LagrangeFunctionSpace.from_mesh( - mesh, order, num_components, pcms.CoordinateSystem.Cartesian - ) - field = factory.create_field() - - print(" Setting up field data...") - mesh_coords = mesh.coords() - num_verts = mesh.nverts() - print(f" Mesh has {num_verts} vertices") - - num_owned = field.get_num_dof_holders() - ndata = num_owned * num_components - print(f" Setting up field data for {ndata} DOFs") - - def test_func(x, y, z, component): - return np.sin(5.0 * x * y) + float(component) - - test_data = np.zeros(ndata, dtype=np.float64) - 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 - z = mesh_coords[i * dim + 2] if dim >= 3 else 0.0 - for c in range(num_components): - idx = i * num_components + c - test_data[idx] = test_func(x, y, z, c) - - 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 - test_data[idx] = float(c) + 0.5 - - field.set_dof_holder_data(test_data) - print(" Set field data with test function") - - 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], - ], dtype=np.float64) - 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], - ], dtype=np.float64) - - 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") - - eval_values = np.zeros((eval_coords.shape[0], num_components), - dtype=np.float64) - evaluator.evaluate(field, eval_values) - print(" Field evaluated successfully") - - 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, c]:.4f}" for c in range(num_components)] - ) - print(f" Point {i} ({coords_str}): values = [{values_str}]") - for c in range(num_components): - val = eval_values[i, c] - assert not np.isnan(val) - assert not np.isinf(val) - - -def test_tag_operations(world, dim): - """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 - - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, - nx, ny, nz, - False - ) - rng = np.random.default_rng(0) - - 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") - assert mesh.has_tag(0, "test_int32") - assert mesh.has_tag(0, "test_int64") - - nverts = mesh.nverts() - temp_data = np.linspace(0.0, 100.0, nverts, dtype=np.float64) - 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) - mesh.add_tag(0, "velocity", dim, velocity_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) - np.testing.assert_allclose(mesh.get_tag(0, "temperature"), new_temp) - - if dim == 3: - 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=omega_h.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") - - mesh.remove_tag(0, "test_float") - assert not mesh.has_tag(0, "test_float") - assert mesh.has_tag(0, "temperature") - assert mesh.ntags(0) > 0 - - nelems = mesh.nelems() - elem_quality = rng.random(nelems).astype(np.float64) - mesh.add_tag(dim, "quality", 1, elem_quality) - np.testing.assert_allclose(mesh.get_tag(dim, "quality"), elem_quality) - - if dim >= 2: - 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() - global_ids = mesh.globals(0) - verts_of_elems = mesh.ask_verts_of(dim) - owned_verts = mesh.owned(0) - - 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): - """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 - - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 1.0, - nx, ny, nz, - False - ) - - vertex_coords = mesh.coords() - assert len(vertex_coords) == mesh.nverts() * dim - - if dim >= 2: - edge_coords = omega_h.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: - elem_coords = omega_h.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: - face_coords = omega_h.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 = omega_h.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) - averaged_field = omega_h.average_field(mesh, dim, 1, vertex_field) - assert averaged_field.shape[0] == mesh.nents(dim) - - -def main(): - """Run all test cases""" - print("Testing Omega_h Field Python bindings...") - lib = omega_h.OmegaHLibrary() - world = lib.world() - print("Initialized Omega_h library and world") - - test_field_methods(world, 2, 1, 1) - test_field_methods(world, 2, 2, 1) - - print("\n" + "=" * 60) - print("Testing field evaluation...") - print("=" * 60) - test_field_evaluation(world, 2, 1, 1) - test_field_evaluation(world, 2, 2, 1) - - 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) - test_tag_operations(world, 2) - test_tag_operations(world, 3) - - print("\n" + "=" * 60) - print("Testing entity coordinate computation...") - print("=" * 60) - test_entity_coordinates(world, 2) - test_entity_coordinates(world, 3) - - print("\n✓ All tests passed!") - del world - del lib - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/pcms/pythonapi/test_uniform_grid_field.py b/src/pcms/pythonapi/test_uniform_grid_field.py deleted file mode 100644 index fe0745f3b..000000000 --- a/src/pcms/pythonapi/test_uniform_grid_field.py +++ /dev/null @@ -1,344 +0,0 @@ -""" -Field-centric Python examples for uniform-grid-backed function spaces. -""" -import numpy as np -import pcms -import PyOmega_h as omega_h - - -def test_uniform_grid_field_creation(): - """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] - - # Build the function space from the grid, then create a Field from it. - factory = pcms.LagrangeFunctionSpace.from_uniform_grid( - grid, 1, pcms.CoordinateSystem.Cartesian - ) - 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}") - - -def test_uniform_grid_field_data_operations(): - """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] - - factory = pcms.LagrangeFunctionSpace.from_uniform_grid( - grid, 1, pcms.CoordinateSystem.Cartesian - ) - 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}") - field.set_dof_holder_data(data) - retrieved_data = field.get_dof_holder_data() - print(f"Retrieved data: {retrieved_data}") - np.testing.assert_allclose(retrieved_data, data) - print("Data successfully set and retrieved!") - - -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] - - factory = pcms.LagrangeFunctionSpace.from_uniform_grid( - grid, 1, pcms.CoordinateSystem.Cartesian - ) - field = factory.create_field() - - coords = field.get_dof_holder_coordinates() - expected_vertices = (grid.divisions[0] + 1) * (grid.divisions[1] + 1) - - 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(): - """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] - - 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_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, 1, 3] - - factory = pcms.LagrangeFunctionSpace.from_uniform_grid( - grid, 1, pcms.CoordinateSystem.Cartesian - ) - field = factory.create_field() - - coords = field.get_dof_holder_coordinates() - expected_vertices = ((grid.divisions[0] + 1) * - (grid.divisions[1] + 1) * - (grid.divisions[2] + 1)) - - 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!") - - # 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!") - - -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] - - 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): - """ - Test complete UniformGrid workflow with Omega_h mesh and field interpolation. - - This test: - 1. Creates an Omega_h mesh - 2. Creates a uniform grid from the mesh - 3. Creates a binary mask field - 4. Creates and initializes an Omega_h field with f(x,y) = x + 2*y - 5. Transfers the field to uniform grid via interpolation - 6. Verifies the transferred values - """ - # Create a simple 2D box mesh: 1.0 x 1.0 domain with 4x4 elements - mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 0.0, - 4, 4, 0, - False - ) - - # 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") - 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") - - # 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 {omega_h_field.get_num_dof_holders()} nodes") - - ug_factory = pcms.LagrangeFunctionSpace.from_uniform_grid( - grid, 1, pcms.CoordinateSystem.Cartesian - ) - 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") - - ug_field_data = ug_field.get_dof_holder_data() - 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 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!") - - -def test_omega_h_to_omega_h_transfer_workflow(world): - """ - Transfer a field from one Omega_h mesh to another Omega_h mesh. - - 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). - src_mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 0.0, - 4, 4, 0, - False - ) - - # Target mesh (finer). - tgt_mesh = omega_h.build_box( - world, - omega_h.Family.SIMPLEX, - 1.0, 1.0, 0.0, - 8, 8, 0, - False - ) - - # 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 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") - - tgt_coords = tgt_field.get_dof_holder_coordinates() - tgt_data = tgt_field.get_dof_holder_data() - errors = 0 - 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 error < 1e-10 - print("Omega_h to Omega_h transfer verification completed") - - -def main(): - lib = omega_h.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 Coordinates (2D)") - print("=" * 60) - test_uniform_grid_field_coordinates_2d() - - print("\n" + "=" * 60) - print("Testing UniformGrid Field Coordinates (3D)") - print("=" * 60) - 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 UniformGrid Field Evaluation") - print("=" * 60) - test_uniform_grid_field_evaluation() - - print("\n" + "=" * 60) - print("Testing UniformGrid Workflow") - print("=" * 60) - test_uniform_grid_workflow(world) - - print("\n" + "=" * 60) - 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() \ No newline at end of file diff --git a/src/pcms/pythonapi/uniform_grid_workflow_guide.md b/src/pcms/pythonapi/uniform_grid_workflow_guide.md index 8e93682ee..aca7bc2a1 100644 --- a/src/pcms/pythonapi/uniform_grid_workflow_guide.md +++ b/src/pcms/pythonapi/uniform_grid_workflow_guide.md @@ -115,15 +115,15 @@ 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 mask_field = pcms.create_uniform_grid_binary_field(mesh, [4, 4]) -print(f"Created mask field with {mask_field.get_num_dof_holders()} vertices") +print(f"Created mask field with {mask_field.get_layout().get_num_owned_dof_holder()} vertices") ``` **Accessing Values**: ```python # Access mask value for vertex (i, j) vertex_id = j * (grid.divisions[0] + 1) + i -mask_data = mask_field.get_dof_holder_data() -mask_value = mask_data[vertex_id] # Returns 0.0 or 1.0 +mask_data = mask_field.get_dof_holder_data().get_values() # shape (num_vertices, 1) +mask_value = mask_data[vertex_id, 0] # Returns 0.0 or 1.0 ``` --- @@ -136,16 +136,15 @@ mask_value = mask_data[vertex_id] # Returns 0.0 or 1.0 # Create a concrete FunctionSpace with linear (order=1) elements and # 1 component (scalar) omega_h_factory = pcms.LagrangeFunctionSpace.from_mesh( - mesh, + mesh, 1, 1, - pcms.CoordinateSystem.Cartesian ) -omega_h_field = omega_h_factory.create_field() +omega_h_field = omega_h_factory.create_function() # Initialize field with f(x,y) = x + 2*y -coords = omega_h_field.get_dof_holder_coordinates() -num_nodes = omega_h_field.get_num_dof_holders() +coords = omega_h_field.get_layout().get_dof_holder_coordinates().get_values() +num_nodes = omega_h_field.get_layout().get_num_owned_dof_holder() omega_h_data = np.zeros(num_nodes) for i in range(num_nodes): @@ -173,9 +172,8 @@ vertex_field = pcms.map_entity_field_to_vertices_average(mesh, face_field, face_ # 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() + mesh, 1, 1) +omega_h_field = omega_h_factory.create_function() omega_h_field.set_dof_holder_data(vertex_field) ``` @@ -189,11 +187,10 @@ Set up the data structure for storing field values on the uniform grid vertices. ```python # Create a uniform-grid FunctionSpace ug_factory = pcms.LagrangeFunctionSpace.from_uniform_grid( - grid, + grid, 1, # Number of components - pcms.CoordinateSystem.Cartesian ) -ug_field = ug_factory.create_field() +ug_field = ug_factory.create_function() ``` --- @@ -216,25 +213,25 @@ Retrieve interpolated values and verify correctness. ```python # Get field data and coordinates -ug_field_data = ug_field.get_dof_holder_data() -ug_coords = ug_field.get_dof_holder_coordinates() +ug_field_data = ug_field.get_dof_holder_data().get_values() +ug_coords = ug_field.get_layout().get_dof_holder_coordinates().get_values() # Access data at vertex (i, j) vertex_id = j * (grid.divisions[0] + 1) + i -value = ug_field_data[vertex_id] +value = ug_field_data[vertex_id, 0] x, y = ug_coords[vertex_id, 0], ug_coords[vertex_id, 1] ``` --- -### Step 9: Export Flat Field Data +### Step 9: Export Field Data -Retrieve the flat DOF arrays for downstream analyses. +Retrieve the DOF arrays for downstream analyses. ```python -# Get flat DOF arrays -grid_values = ug_field.get_dof_holder_data() -mask_values = mask_field.get_dof_holder_data() +# Get DOF arrays, shape (num_dof_holders, num_components) +grid_values = ug_field.get_dof_holder_data().get_values() +mask_values = mask_field.get_dof_holder_data().get_values() # Save to file (example) np.save('field_data.npy', grid_values) @@ -248,26 +245,52 @@ 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 +The Python names mirror the C++ API: classes keep their C++ names, C++ +namespaces are submodules (`pcms.csys`, `pcms.values`), and methods are the +snake_case form of the C++ method (`GetDOFHolderData` is +`get_dof_holder_data`). + +### Coordinate Systems and Value Bases +- `csys.Cartesian.create(dim)` - Cartesian system. Coordinate system arguments default to `None`, meaning Cartesian in the dimension of the mesh, grid or coordinates they are used with +- `csys.CylindricalRThetaZ.create()`, `csys.CylindricalRZ.create()` - cylindrical systems +- `CoordinateView(system, coords)`, `CoordinateView(coords)` - coordinates `(num_points, dim)` tagged with their system (Cartesian when omitted); the array is copied +- `view.get_coordinate_system()`, `view.get_values()` - the system and a read-only coordinate array +- `ValueBasis(system, scaling, variance)` - the basis a value's components are in; `ValueBasis()` is the scalar basis +- `values.Scalar`, `values.Vector`, `values.Covector`, `values.Tensor`, `values.Of([...])` - variance signatures +- `ComponentScaling.Physical`, `ComponentScaling.Natural`, `Variance.Contravariant`, `Variance.Covariant` +- `ValueView(basis, values)` - values `(num_points, num_components)` tagged with their basis; the array is not copied + ### 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 +- `FunctionSpace` - abstract base class for field spaces; a `PointEvaluatorFactory` +- `LagrangeFunctionSpace.from_mesh(mesh, order, num_components, mesh_coordinate_system)` - create an Omega_h-backed FunctionSpace +- `LagrangeFunctionSpace.from_uniform_grid(grid, num_components, mesh_coordinate_system, order=1)` - create a uniform-grid-backed FunctionSpace +- `PolynomialReconstructionFunctionSpace.create(coordinate_view, options)` - MLS reconstruction over a point cloud ### Field Operations -- `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 +- `space.create_function(name="", variance=values.Scalar, system=None, scaling=None)` - Create a real-valued `Function`; vector and tensor values are in `system`'s basis (default: the space's) +- `field.get_layout()` - the field's `FieldLayout` +- `field.get_data().get_value_basis()` - the basis the field's values are stored in +- `field.get_layout().get_num_owned_dof_holder()` - Number of owned DOF holders (nodes/elements) +- `field.get_layout().get_num_components()` - Number of field components per DOF holder +- `field.get_layout().get_dof_holder_coordinates()` - DOF holder coordinates as a `CoordinateView` +- `field.get_dof_holder_data()` - Field values as a `ValueView` whose array (`.get_values()`, shape `(num_dof_holders, num_components)`) is read-only and valid until the field is next modified (use `.copy()` to keep values) +- `field.set_dof_holder_data(value_view)` - Set field values; the basis must match the field's. Scalar fields also accept a plain array, of shape `(num_dof_holders,)` when the field has one component +- `field.set_dof_holder_data_unchecked(data)` - Set field values without checking their basis +- `EvaluationRequest.from_coordinates(coordinate_view, 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 -- `Interpolator(source_space, target_space)` - Create an interpolator between FunctionSpaces (cached localization) -- `interpolator.apply(source_field, target_field)` - Interpolate between fields +- `factory.create_point_evaluator(request)` - Create a reusable point evaluator; the request's points must be in `factory.get_coordinate_system()` +- `evaluator.evaluate(field, output)` - Evaluate into a `ValueView` tagged `evaluator.output_basis(field.get_data().get_value_basis())` (a plain array for scalar fields, of shape `(num_points,)` when the output has one component); the output array must be writeable, C-contiguous float64 +- `Interpolator(source, target_space)` - Create an interpolator from a `PointEvaluatorFactory` (cached localization) +- `operator.apply(source_field, target_field)` - Apply any `TransferOperator` (`Interpolator`, `Copy`, projections) - `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 +### Coordinate Transformations +- `CartesianToCylindrical()`, `CylindricalToCartesian()` - `CoordinateMap`s; points map source to target, values are re-expressed the other way +- `bound = coordinate_map.bind(coordinate_view)` - per-point work for points in the map's source system +- `bound.mapped_points()`, `bound.output_basis(basis)`, `bound.transform_values(value_view)` - mapped points, and in-place re-expression of values +- `TransformedSource(source, to_source)` - presents a `PointEvaluatorFactory` in another coordinate system; use it as an `Interpolator` source + ### Out-of-Bounds Modes - `OutOfBoundsMode.ERROR` - Raise error when points are outside mesh - `OutOfBoundsMode.FILL` - Fill with specified value when points are outside mesh @@ -309,8 +332,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 -- `field.get_num_dof_holders()` - Total number of field DOF holders -- `field.get_dof_holder_coordinates()` - DOF holder coordinates +- `field.get_layout().get_num_owned_dof_holder()` - Total number of field DOF holders +- `field.get_layout().get_dof_holder_coordinates().get_values()` - DOF holder coordinates --- diff --git a/src/pcms/transfer/CMakeLists.txt b/src/pcms/transfer/CMakeLists.txt index c3983e0bd..ad62383bd 100644 --- a/src/pcms/transfer/CMakeLists.txt +++ b/src/pcms/transfer/CMakeLists.txt @@ -7,6 +7,8 @@ set(PCMS_FIELD_TRANSFER_HEADERS interpolator.h copy.h transfer_operator.hpp + mass_matrix_type.hpp + monte_carlo_sampling.hpp ) diff --git a/src/pcms/transfer/bilinear_form_integrator.hpp b/src/pcms/transfer/bilinear_form_integrator.hpp index 060667bd2..df8ace693 100644 --- a/src/pcms/transfer/bilinear_form_integrator.hpp +++ b/src/pcms/transfer/bilinear_form_integrator.hpp @@ -14,6 +14,10 @@ class BilinearFormIntegrator // counting keeps the matrix alive until the KSP is destroyed). virtual Mat GetMatrix() const noexcept = 0; + // True when the assembled matrix is diagonal; lets solvers pick an exact + // one-application diagonal solve (e.g. preonly + Jacobi). + virtual bool IsDiagonal() const noexcept { return false; } + virtual ~BilinearFormIntegrator() noexcept = default; }; diff --git a/src/pcms/transfer/conservative_projection_solver.cpp b/src/pcms/transfer/conservative_projection_solver.cpp index e6955580a..11a47d7e7 100644 --- a/src/pcms/transfer/conservative_projection_solver.cpp +++ b/src/pcms/transfer/conservative_projection_solver.cpp @@ -1,5 +1,6 @@ #include +#include "pcms/configuration.h" #include "pcms/transfer/conservative_projection_solver.hpp" #include "pcms/utility/arrays.h" #include "pcms/transfer/petsc_utils.hpp" @@ -12,11 +13,11 @@ static Omega_h::Reals vecToOmegaHReals(Vec vec) { PetscInt n = 0; PetscErrorCode ierr = VecGetSize(vec, &n); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); const PetscScalar* array = nullptr; ierr = VecGetArrayRead(vec, &array); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); auto values_host = Omega_h::HostWrite(n); for (PetscInt i = 0; i < n; ++i) { @@ -24,7 +25,7 @@ static Omega_h::Reals vecToOmegaHReals(Vec vec) } ierr = VecRestoreArrayRead(vec, &array); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); return Omega_h::Reals(values_host); } @@ -42,7 +43,7 @@ GalerkinProjectionSolver::GalerkinProjectionSolver( PetscInt m = 0, n = 0; PetscErrorCode ierr = MatGetSize(A, &m, &n); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); nverts_ = m; const std::size_t num_pts = @@ -50,14 +51,29 @@ GalerkinProjectionSolver::GalerkinProjectionSolver( sampled_values_ = Kokkos::View("rhs_sampled", num_pts, 1); - ierr = KSPCreate(PETSC_COMM_WORLD, &ksp_); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + ierr = KSPCreate(PETSC_COMM_SELF, &ksp_); + CHKERRABORT(PETSC_COMM_SELF, ierr); ierr = KSPSetOperators(ksp_, A, A); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); + if (mass_integrator.IsDiagonal()) { + // Jacobi preconditioning applies inv(diag(A)) + // with preonly this is the true inverse + ierr = KSPSetType(ksp_, KSPPREONLY); + CHKERRABORT(PETSC_COMM_SELF, ierr); + PC pc = nullptr; + ierr = KSPGetPC(ksp_, &pc); + CHKERRABORT(PETSC_COMM_SELF, ierr); + ierr = PCSetType(pc, PCJACOBI); + CHKERRABORT(PETSC_COMM_SELF, ierr); + } + ierr = + KSPSetTolerances(ksp_, PCMS_PROJECTION_KSP_RTOL, PCMS_PROJECTION_KSP_ATOL, + PETSC_DEFAULT, PETSC_DEFAULT); + CHKERRABORT(PETSC_COMM_SELF, ierr); ierr = KSPSetFromOptions(ksp_); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); ierr = KSPSetUp(ksp_); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); } GalerkinProjectionSolver::~GalerkinProjectionSolver() @@ -71,7 +87,9 @@ GalerkinProjectionSolver::~GalerkinProjectionSolver() Omega_h::Reals GalerkinProjectionSolver::Solve( const PointEvaluator& evaluator, const Field& source_field) const { - evaluator.Evaluate(source_field, MakeRank2View(sampled_values_)); + evaluator.Evaluate(source_field, ValueView( + source_field.GetData().GetValueBasis(), + MakeRank2View(sampled_values_))); return Solve(MakeConstRank2View(sampled_values_)); } @@ -82,16 +100,16 @@ Omega_h::Reals GalerkinProjectionSolver::Solve( Vec rhs_vector = rhs_integrator_->GetVector(); Vec solution = nullptr; - PetscErrorCode ierr = createSeqVec(PETSC_COMM_WORLD, nverts_, &solution); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + PetscErrorCode ierr = createSeqVec(PETSC_COMM_SELF, nverts_, &solution); + CHKERRABORT(PETSC_COMM_SELF, ierr); ierr = KSPSolve(ksp_, rhs_vector, solution); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); auto result = vecToOmegaHReals(solution); ierr = VecDestroy(&solution); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); return result; } diff --git a/src/pcms/transfer/copy.h b/src/pcms/transfer/copy.h index f650fed51..73d63f32f 100644 --- a/src/pcms/transfer/copy.h +++ b/src/pcms/transfer/copy.h @@ -5,7 +5,10 @@ #include "pcms/field/function_space.h" #include "pcms/utility/assert.h" #include "pcms/utility/profile.h" +#include "pcms/utility/types.h" #include "pcms/transfer/transfer_operator.hpp" +#include +#include namespace pcms { @@ -13,22 +16,17 @@ 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"); + const auto& sd = source.GetData(); + const auto& td = target.GetData(); + if (sd.GetValueType() != td.GetValueType() || + !SameValueBasis(sd.GetValueBasis(), td.GetValueBasis())) { + throw pcms_error("Copy: source and target value type/basis differ"); } } @@ -52,7 +50,7 @@ class Copy : public TransferOperator { PCMS_FUNCTION_TIMER; detail::CheckCopyCompatible(source, target); - target.SetDOFHolderDataHost(source.GetDOFHolderDataHost()); + target.SetDOFHolderData(source.GetDOFHolderData()); } }; diff --git a/src/pcms/transfer/interpolator.h b/src/pcms/transfer/interpolator.h index 25ef3e1ee..959f5be39 100644 --- a/src/pcms/transfer/interpolator.h +++ b/src/pcms/transfer/interpolator.h @@ -4,6 +4,7 @@ #include "pcms/field/field.h" #include "pcms/field/field_data.h" #include "pcms/field/function_space.h" +#include "pcms/field/point_evaluator_factory.hpp" #include "pcms/field/out_of_bounds_policy.h" #include "pcms/field/point_evaluator.h" #include "pcms/utility/arrays.h" @@ -34,45 +35,45 @@ class Interpolator : public TransferOperator { public: // Expensive: localizes target DOF coords into source mesh. Called once. - Interpolator(const FunctionSpace& source_space, + /// @param source factory evaluated at the target's DOF-holder coordinates + /// @param target_space space whose DOF holders receive the values + /// @param policy out-of-bounds policy for the evaluation + Interpolator(const PointEvaluatorFactory& source, const FunctionSpace& target_space, OutOfBoundsPolicy policy = {}) : num_points_(static_cast( target_space.GetLayout()->GetDOFHolderCoordinates().GetValues().extent( 0))), n_comp_(target_space.GetLayout()->GetNumComponents()), - evaluator_(source_space.CreatePointEvaluator( - EvaluationRequest::FromFunctionSpace(target_space, policy))) + evaluator_(source.CreatePointEvaluator( + EvaluationRequest::FromFunctionSpace(target_space, policy))), + target_values_("interp_output", num_points_, n_comp_) { } - // 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; - 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( - Rank2View(flat.data(), num_points, n_comp)); + const auto& sd = source.GetData(); + const auto& td = target.GetData(); + if (sd.GetValueType() != td.GetValueType()) { + throw pcms_error("Interpolator: source and target value types differ"); + } + const ValueBasis written = evaluator_->OutputBasis(sd.GetValueBasis()); + if (!SameValueBasis(written, td.GetValueBasis())) { + throw pcms_error( + "Interpolator: the basis written for the source's stored basis " + "differs from the target's declared basis"); + } + evaluator_->Evaluate(source, ValueView( + written, MakeRank2View(target_values_))); + target.SetDOFHolderDataUnchecked(MakeConstRank2View(target_values_)); } private: LO num_points_; int n_comp_; std::unique_ptr> evaluator_; + mutable Kokkos::View target_values_; }; } // namespace pcms diff --git a/src/pcms/transfer/linear_form_integrator.hpp b/src/pcms/transfer/linear_form_integrator.hpp index 8930502d4..05bacbb0e 100644 --- a/src/pcms/transfer/linear_form_integrator.hpp +++ b/src/pcms/transfer/linear_form_integrator.hpp @@ -1,7 +1,7 @@ #ifndef PCMS_TRANSFER_LINEAR_FORM_INTEGRATOR_H #define PCMS_TRANSFER_LINEAR_FORM_INTEGRATOR_H -#include "pcms/field/coordinate_system.h" +#include "pcms/field/coordinate_view.hpp" #include "pcms/utility/arrays.h" #include diff --git a/src/pcms/transfer/linear_interpolant.hpp b/src/pcms/transfer/linear_interpolant.hpp index c058d48f9..bf0afe355 100644 --- a/src/pcms/transfer/linear_interpolant.hpp +++ b/src/pcms/transfer/linear_interpolant.hpp @@ -68,7 +68,8 @@ void basis_function(const RealVecView& parametric_coord, KOKKOS_INLINE_FUNCTION double linear_interpolant(const IntVecView& dimensions, const double* linear_basis_each_dir, - const IntVecView& indices, const RealVecView& values) + const IntVecView& indices, const RealMatView& values, + const int component) { int dim = dimensions.extent(0); double sum = 0; @@ -91,50 +92,60 @@ double linear_interpolant(const IntVecView& dimensions, } int idx = calculateIndex(dimensions, ids); - double corner_values = values(idx); + double corner_values = values(idx, component); sum += temp * corner_values; } return sum; } +// Multilinear interpolation over a regular grid. Supports one or more +// components per grid point; the scalar case is num_components == 1. +// `values` is [num_grid_points][num_components] and the result is +// [num_query_points][num_components]. class RegularGridInterpolator { private: const RealMatView parametric_coords; - const RealVecView values; + const RealMatView values; // 2D: [num_grid_points][num_components] const IntMatView indices; const IntVecView dimensions; + const int num_components; public: RegularGridInterpolator(const RealMatView& parametric_coords_, - const RealVecView& values_, + const RealMatView& values_, const IntMatView& indices_, const IntVecView& dimensions_) : parametric_coords(parametric_coords_), values(values_), indices(indices_), - dimensions(dimensions_){}; + dimensions(dimensions_), + num_components(values_.extent(1)) + { + } - RealVecView linear_interpolation() + RealMatView linear_interpolation() { - int dim = dimensions.extent(0); int N = parametric_coords.extent(0); - RealVecView interpolated_values("approximated values", N); + RealMatView interpolated_values("approximated values", N, num_components); auto parametric_coords_ = parametric_coords; auto dimensions_ = dimensions; auto values_ = values; auto indices_ = indices; + int n_comp = num_components; + Kokkos::parallel_for( - "linear interpolation function", N, KOKKOS_LAMBDA(int j) { + "linear interpolation", N, KOKKOS_LAMBDA(int j) { double linear_basis_each_dir[MAX_DIM] = {0.0}; auto parametric_coord_each_point = Kokkos::subview(parametric_coords_, j, Kokkos::ALL()); auto index_each_point = Kokkos::subview(indices_, j, Kokkos::ALL()); basis_function(parametric_coord_each_point, linear_basis_each_dir); - auto approx_value = linear_interpolant( - dimensions_, linear_basis_each_dir, index_each_point, values_); - interpolated_values(j) = approx_value; + for (int c = 0; c < n_comp; ++c) { + interpolated_values(j, c) = linear_interpolant( + dimensions_, linear_basis_each_dir, index_each_point, values_, c); + } }); return interpolated_values; diff --git a/src/pcms/transfer/mass_matrix_integrator.hpp b/src/pcms/transfer/mass_matrix_integrator.hpp index 7d36c9759..472a540fc 100644 --- a/src/pcms/transfer/mass_matrix_integrator.hpp +++ b/src/pcms/transfer/mass_matrix_integrator.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -17,16 +18,19 @@ template class MassMatrixIntegrator : public MeshField::Integrator { public: + // Linear simplex: numNodes = spatial dim + 1 (3 for triangles, 4 for tets). + static constexpr int numNodes = FieldElement::MeshEntDim + 1; + 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), + subMatrixSize(numNodes * numNodes), + elmMassMatrix("elmMassMatrix", mesh_in.nelems() * numNodes * numNodes), Integrator(order) { Kokkos::deep_copy(elmMassMatrix, 0); - assert(mesh.dim() == 2); // TODO support 1d,2d,3d + assert(mesh.dim() == 2 || mesh.dim() == 3); assert(mesh.family() == OMEGA_H_SIMPLEX); } void atPoints(Kokkos::View p, @@ -38,7 +42,7 @@ class MassMatrixIntegrator : public MeshField::Integrator // std::cerr << " Number points per Elem : " << numPtsPerElem << "\n"; assert(numPtsPerElem >= 1); const size_t ptDim = p.extent(1); - assert(ptDim == fe.MeshEntDim + 1); + assert(ptDim == fe.MeshEntDim); // 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(); @@ -51,19 +55,24 @@ class MassMatrixIntegrator : public MeshField::Integrator 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; + 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); + // Use the unsigned volume element: MeshField returns a signed + // Jacobian determinant, which is negative for tetrahedra whose vertex + // ordering has negative orientation. A mass matrix integrates against + // the positive volume measure, so take the magnitude (a no-op in 2D + // where the differential area is already positive). + const auto dVPt = Kokkos::fabs(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; + massMatrix(elm * subMat + i * numNodes + j) += + N[i] * N[j] * wPt * dVPt; } } } diff --git a/src/pcms/transfer/mass_matrix_type.hpp b/src/pcms/transfer/mass_matrix_type.hpp new file mode 100644 index 000000000..158988139 --- /dev/null +++ b/src/pcms/transfer/mass_matrix_type.hpp @@ -0,0 +1,16 @@ +#ifndef PCMS_TRANSFER_MASS_MATRIX_TYPE_HPP +#define PCMS_TRANSFER_MASS_MATRIX_TYPE_HPP + +namespace pcms +{ + +// Mass-matrix formulation used by Galerkin projections. +enum class MassMatrixType +{ + Consistent, // full Galerkin mass matrix + Lumped, // row-sum lumped diagonal mass matrix +}; + +} // namespace pcms + +#endif // PCMS_TRANSFER_MASS_MATRIX_TYPE_HPP diff --git a/src/pcms/transfer/mesh_intersection.cpp b/src/pcms/transfer/mesh_intersection.cpp index 564ae3507..859ee9c74 100644 --- a/src/pcms/transfer/mesh_intersection.cpp +++ b/src/pcms/transfer/mesh_intersection.cpp @@ -1,23 +1,58 @@ #include "pcms/transfer/mesh_intersection.hpp" +#include "pcms/utility/assert.h" #include "pcms/utility/mesh_geometry.h" #include "pcms/utility/omega_h_array_utils.h" namespace pcms { +namespace +{ +// Construct the source-mesh containing-element search appropriate to the +// spatial dimension: a background grid sized so cells hold ~1 element. +template +auto MakeGridPointSearch(Omega_h::Mesh& source_mesh) +{ + const auto n = pcms::DivisionsPerAxisForMesh(source_mesh.nelems()); + if constexpr (Dim == 3) { + return pcms::GridPointSearch3D(source_mesh, n, n, n); + } else { + return pcms::GridPointSearch2D(source_mesh, n, n); + } +} + +// For each target element, the source element containing its centroid: where +// the BFS over the source dual graph starts. +template +Kokkos::View LocateTargetCentroids( + Omega_h::Mesh& target_mesh, const PointSearch& source_search) +{ + const auto flat_centroids = pcms::get_entity_centroids(target_mesh, Dim); + // Convert layout_right 1D Omega_h array to 2D Kokkos view with correct layout + auto centroids = ConvertCoordsTo2D(flat_centroids, target_mesh.nelems(), Dim); + auto coords = CoordinateView( + CoordinateSystem::Cartesian, + MakeConstRank2View(centroids)); + auto results = source_search.apply(coords); + return source_search.GetOwningElementIds(results); +} +} // namespace + +template void FindIntersections::adjBasedIntersectSearch( + const Kokkos::View& start_elements, const Omega_h::LOs& tgt2src_offsets, Omega_h::Write& nIntersections, - Omega_h::Write& tgt2src_indices, bool is_count_only) + Omega_h::Write& tgt2src_indices, bool is_count_only, + bool use_prefilter) { - + // Element entity dimension equals the spatial dimension (FACE for 2D, REGION + // for 3D); measures are triangle areas (2D) or tet volumes (3D). 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& tgt_elems2nodes = target_mesh_.ask_down(Dim, Omega_h::VERT).ab2b; + const auto& src_elems2nodes = source_mesh_.ask_down(Dim, Omega_h::VERT).ab2b; + const auto& src_elem_measures = measure_elements_real(&source_mesh_); + const auto& tgt_elem_measures = measure_elements_real(&target_mesh_); const auto& t2t = source_mesh_.ask_dual(); // gives connected element neighbors const auto& t2tt = t2t.a2ab; @@ -29,26 +64,29 @@ void FindIntersections::adjBasedIntersectSearch( auto centroids = ConvertCoordsTo2D(flat_centroids, target_mesh_.nfaces(), 2); pcms::GridPointSearch2D search_cell(source_mesh_, 20, 20); - auto results = search_cell(centroids); + auto results = search_cell.Apply( + CoordinateView(CoordinateSystem::Cartesian, MakeConstRank2View(centroids))); auto owning_cell_ids = search_cell.GetOwningElementIds(results); auto nfaces_target = target_mesh_.nfaces(); Omega_h::parallel_for( - nfaces_target, + nelems_target, OMEGA_H_LAMBDA(const Omega_h::LO id) { Queue queue; Track visited; - auto current_cell_id = owning_cell_ids(id); - auto current_tgt_elm_area = tgt_elem_areas[id]; + auto current_cell_id = start_elements(id); + auto current_tgt_elm_measure = tgt_elem_measures[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)); + // No source element is a candidate for the centroid's grid cell, so the + // target lies outside the source mesh and overlaps nothing. + if (current_cell_id < 0) { + nIntersections[id] = 0; + return; + } auto tgt_elm_vert_coords = - get_vert_coords_of_elem(tgt_coords, tgt_faces2nodes, id); + get_vert_coords_of_elem(tgt_coords, tgt_elems2nodes, id); Omega_h::LO start_counter; if (!is_count_only) { @@ -76,22 +114,53 @@ void FindIntersections::adjBasedIntersectSearch( 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; + // If the visited buffer is full, skip this neighbor so the BFS + // terminates. Without this, an unrecorded neighbor stays "not + // visited" and is re-queued forever (infinite loop). Mirrors the + // guard in adj_search.cpp. Raise PCMS_INTERSECTION_TRACK_SIZE if + // this fires. + if (!visited.push_back(neighborElmId)) { + printf("ERROR: visited buffer full " + "(PCMS_INTERSECTION_TRACK_SIZE=%d) for " + "target %d; some intersections may be missed\n", + PCMS_INTERSECTION_TRACK_SIZE, id); + continue; + } + auto elm_vert_coords = get_vert_coords_of_elem( + src_coords, src_elems2nodes, neighborElmId); + + // use plane to check if element is already outside clipped region + // this avoids construction of unnecessary clip + if (use_prefilter && simplices_have_degenerate_overlap( + tgt_elm_vert_coords, elm_vert_coords)) { + continue; + } + + r3d::Polytope 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]; + // Take the magnitude: r3d::measure is signed by the orientation of + // the target simplex used to initialize the polytope, which can be + // negative for tetrahedra. This mirrors the fabs applied in the + // sub-simplex decomposition and mass assembly; without it a + // negatively-oriented target element would reject all of its real + // overlaps and break conservation. + auto intersected_measure = Kokkos::fabs(r3d::measure(intersection)); + auto current_src_elm_measure = src_elem_measures[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) { + Kokkos::fmax(current_tgt_elm_measure, current_src_elm_measure); + auto eps = Kokkos::fmax(PCMS_INTERSECTION_ABS_TOL, + PCMS_INTERSECTION_REL_TOL * scale); + // A valid intersection is a non-degenerate simplex-simplex overlap: + // at least Dim+1 vertices (a polygon in 2D, a polyhedron in 3D). + if (intersection.nverts >= Dim + 1 && intersected_measure >= eps) { count++; OMEGA_H_CHECK_PRINTF( - count < 500, "WARNING: count exceeds 500 for target %d", id); + count < PCMS_INTERSECTION_QUEUE_SIZE, + "intersection count for target %d reached the cap %d; raise " + "PCMS_INTERSECTION_QUEUE_SIZE", + id, PCMS_INTERSECTION_QUEUE_SIZE); queue.push_back(neighborElmId); @@ -113,20 +182,37 @@ void FindIntersections::adjBasedIntersectSearch( }, // end of lambda "count the number of intersections for each target element"); } -IntersectionResults intersectTargets(Omega_h::Mesh& source_mesh, - Omega_h::Mesh& target_mesh) + +// Explicit instantiations for the supported spatial dimensions. +template void FindIntersections::adjBasedIntersectSearch<2>( + const Kokkos::View&, const Omega_h::LOs&, + Omega_h::Write&, Omega_h::Write&, bool, bool); +template void FindIntersections::adjBasedIntersectSearch<3>( + const Kokkos::View&, const Omega_h::LOs&, + Omega_h::Write&, Omega_h::Write&, bool, bool); + +namespace +{ +template +IntersectionResults intersectTargetsImpl( + Omega_h::Mesh& source_mesh, Omega_h::Mesh& target_mesh, + const PointSearch& source_search, bool use_prefilter) { FindIntersections intersect(source_mesh, target_mesh); - auto nfaces_target = target_mesh.nfaces(); + auto nelems_target = target_mesh.nelems(); + + const auto start_elements = + LocateTargetCentroids(target_mesh, source_search); Omega_h::Write nIntersections( - nfaces_target, 0, "number of intersections in each target vertex"); + nelems_target, 0, "number of intersections in each target element"); Omega_h::Write tgt2src_indices; - intersect.adjBasedIntersectSearch(Omega_h::LOs(), nIntersections, - tgt2src_indices, true); + intersect.adjBasedIntersectSearch(start_elements, Omega_h::LOs(), + nIntersections, tgt2src_indices, true, + use_prefilter); Kokkos::fence(); auto tgt2src_offsets = Omega_h::offset_scan(Omega_h::Read(nIntersections), @@ -139,9 +225,52 @@ IntersectionResults intersectTargets(Omega_h::Mesh& source_mesh, ntotal_intersections, 0, "indices of the source elements that intersect the given target element"); - intersect.adjBasedIntersectSearch(tgt2src_offsets, nIntersections, - tgt2src_indices, false); + intersect.adjBasedIntersectSearch(start_elements, tgt2src_offsets, + nIntersections, tgt2src_indices, false, + use_prefilter); return {.tgt2src_offsets = tgt2src_offsets, .tgt2src_indices = Omega_h::read(tgt2src_indices)}; } + +template +const PointSearch& RequireSearchDimension( + const PointSearch& source_search) +{ + if (source_search.Dim() != Dim) { + throw pcms_error("intersectTargets: the supplied source search has a " + "different spatial dimension than the meshes"); + } + return source_search; +} +} // namespace + +IntersectionResults intersectTargets(Omega_h::Mesh& source_mesh, + Omega_h::Mesh& target_mesh, + bool use_prefilter) +{ + OMEGA_H_CHECK(source_mesh.dim() == target_mesh.dim()); + if (source_mesh.dim() == 3) { + const auto search = MakeGridPointSearch<3>(source_mesh); + return intersectTargetsImpl<3>(source_mesh, target_mesh, search, + use_prefilter); + } + const auto search = MakeGridPointSearch<2>(source_mesh); + return intersectTargetsImpl<2>(source_mesh, target_mesh, search, + use_prefilter); +} + +IntersectionResults intersectTargets( + Omega_h::Mesh& source_mesh, Omega_h::Mesh& target_mesh, + const PointSearch& source_search, bool use_prefilter) +{ + OMEGA_H_CHECK(source_mesh.dim() == target_mesh.dim()); + if (source_mesh.dim() == 3) { + return intersectTargetsImpl<3>(source_mesh, target_mesh, + RequireSearchDimension<3>(source_search), + use_prefilter); + } + return intersectTargetsImpl<2>(source_mesh, target_mesh, + RequireSearchDimension<2>(source_search), + use_prefilter); +} } // namespace pcms diff --git a/src/pcms/transfer/mesh_intersection.hpp b/src/pcms/transfer/mesh_intersection.hpp index 84a52114c..83e8b0f54 100644 --- a/src/pcms/transfer/mesh_intersection.hpp +++ b/src/pcms/transfer/mesh_intersection.hpp @@ -1,7 +1,8 @@ #ifndef PCMS_TRANSFER_MESH_INTERSECTION_HPP #define PCMS_TRANSFER_MESH_INTERSECTION_HPP -#include +#include +#include #include #include #include @@ -12,27 +13,105 @@ 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> +// Gather the vertex coordinates of a simplex element (triangle for Dim==2, +// tetrahedron for Dim==3) into an r3d simplex, ready for +// r3d::intersect_simplices. +template +[[nodiscard]] OMEGA_H_INLINE r3d::Few, Dim + 1> get_vert_coords_of_elem(const Omega_h::Reals& coords, - const Omega_h::LOs& faces2nodes, const int id) + const Omega_h::LOs& elems2nodes, const int id) { - const auto elm_verts = Omega_h::gather_verts<3>(faces2nodes, id); + const auto elm_verts = Omega_h::gather_verts(elems2nodes, id); - const Omega_h::Matrix<2, 3> elm_vert_coords = - Omega_h::gather_vectors<3, 2>(coords, elm_verts); + const Omega_h::Matrix elm_vert_coords = + Omega_h::gather_vectors(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]; + r3d::Few, Dim + 1> r3d_vector; + for (int i = 0; i < Dim + 1; ++i) { + for (int d = 0; d < Dim; ++d) { + r3d_vector[i][d] = elm_vert_coords[i][d]; + } } return r3d_vector; } +/** + * @brief Conservative test for a zero-volume overlap between two simplices. + * + * If verticies of one simplex lies in the closed outer half-space of some face + * plane of the other, their intersection is contained in that plane and so has + * no volume. + * + * @return true when the overlap is provably degenerate and the clip can be + * skipped + */ +template +[[nodiscard]] OMEGA_H_INLINE bool simplices_have_degenerate_overlap( + const r3d::Few, Dim + 1>& a, + const r3d::Few, Dim + 1>& b) +{ + // Each iteration treats one simplex as the half-space owner. Both are + // needed: a face of `a` may separate the pair while no face of `b` does. + for (int side = 0; side < 2; ++side) { + const auto& owner = (side == 0) ? a : b; + const auto& other = (side == 0) ? b : a; + + // Face `f` is the face opposite vertex `f`. + for (int f = 0; f < Dim + 1; ++f) { + // Vertices spanning the face, in index order with `f` skipped. + int face_verts[Dim]; + int n_face_verts = 0; + for (int v = 0; v < Dim + 1; ++v) { + if (v != f) { + face_verts[n_face_verts++] = v; + } + } + const auto& origin = owner[face_verts[0]]; + + // Face normal, oriented to point away from the opposite vertex so the + // owner's interior lies on the negative side. + r3d::Vector normal; + if constexpr (Dim == 2) { + const auto edge = owner[face_verts[1]] - origin; + normal[0] = edge[1]; + normal[1] = -edge[0]; + } else { + const auto e1 = owner[face_verts[1]] - origin; + const auto e2 = owner[face_verts[2]] - origin; + normal[0] = e1[1] * e2[2] - e1[2] * e2[1]; + normal[1] = e1[2] * e2[0] - e1[0] * e2[2]; + normal[2] = e1[0] * e2[1] - e1[1] * e2[0]; + } + + Omega_h::Real opposite_side = 0.0; + Omega_h::Real normal_sq = 0.0; + for (int d = 0; d < Dim; ++d) { + opposite_side += normal[d] * (owner[f][d] - origin[d]); + normal_sq += normal[d] * normal[d]; + } + // A degenerate face gives no usable axis. + if (normal_sq == 0.0) { + continue; + } + const Omega_h::Real orientation = (opposite_side > 0.0) ? -1.0 : 1.0; + + bool all_outside = true; + for (int v = 0; v < Dim + 1 && all_outside; ++v) { + Omega_h::Real signed_distance = 0.0; + for (int d = 0; d < Dim; ++d) { + signed_distance += normal[d] * (other[v][d] - origin[d]); + } + all_outside = (orientation * signed_distance) >= 0.0; + } + if (all_outside) { + return true; + } + } + } + return false; +} + /** * @brief Stores results of mesh element intersections for conservative * transfer. @@ -71,22 +150,28 @@ class FindIntersections * source elements. If an element intersects the target triangle (based on * area tolerance), it is included. * + * @param start_elements For each target element, the source element + * containing its centroid (from a point search over the source mesh) * @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. + * @param use_prefilter if true, skip degenerate clips * - * @note This method assumes 2D linear triangles and uses - * `r3d::intersect_simplices` for geometric intersection. + * @note Templated on spatial dimension `Dim`: linear triangles (Dim==2) or + * linear tetrahedra (Dim==3), using `r3d::intersect_simplices` for geometric + * intersection. * * @see r3d::intersect_simplices, intersectTargets */ - void adjBasedIntersectSearch(const Omega_h::LOs& tgt2src_offsets, + template + void adjBasedIntersectSearch(const Kokkos::View& start_elements, + const Omega_h::LOs& tgt2src_offsets, Omega_h::Write& nIntersections, Omega_h::Write& tgt2src_indices, - bool is_count_only); + bool is_count_only, bool use_prefilter = true); }; /** @@ -108,8 +193,14 @@ class FindIntersections * * @see FindIntersections::adjBasedIntersectSearch */ - IntersectionResults intersectTargets(Omega_h::Mesh& source_mesh, - Omega_h::Mesh& target_mesh); + Omega_h::Mesh& target_mesh, + bool use_prefilter = true); + +/** + */ +IntersectionResults intersectTargets( + Omega_h::Mesh& source_mesh, Omega_h::Mesh& target_mesh, + const PointSearch& source_search, bool use_prefilter = true); } // namespace pcms #endif // PCMS_TRANSFER_MESH_INTERSECTION_HPP diff --git a/src/pcms/transfer/omega_h_conservative_projection.cpp b/src/pcms/transfer/omega_h_conservative_projection.cpp index 8230da22c..45a0c1b64 100644 --- a/src/pcms/transfer/omega_h_conservative_projection.cpp +++ b/src/pcms/transfer/omega_h_conservative_projection.cpp @@ -25,31 +25,27 @@ void CheckApplyCompatible(const Field& source, const Field& target, "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) { + if (source.GetData().GetValueType() != FieldValueType::Scalar || + target.GetData().GetValueType() != 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"); - } } } // namespace OmegaHConservativeProjection::OmegaHConservativeProjection( - const FunctionSpace& source_space, const FunctionSpace& target_space) + const FunctionSpace& source_space, const FunctionSpace& target_space, + MassMatrixType mass_matrix_type) : source_layout_(std::dynamic_pointer_cast( source_space.GetLayout())), target_layout_(std::dynamic_pointer_cast( target_space.GetLayout())) { + // The space-based constructor lets the integrator reuse the source space's + // point search for the intersection instead of building another. rhs_integrator_ = std::make_unique( - source_layout_, source_space.GetCoordinateSystem(), target_layout_, - target_space.GetCoordinateSystem()); + source_space, target_space); evaluator_ = source_space.CreatePointEvaluator(EvaluationRequest::FromCoordinates( @@ -57,8 +53,8 @@ OmegaHConservativeProjection::OmegaHConservativeProjection( // Mass integrator is only needed to build the solver; PETSc reference-counts // the matrix so it remains alive inside the KSP after this scope ends. - OmegaHMassIntegrator mass_integrator(target_layout_, - target_space.GetCoordinateSystem()); + OmegaHMassIntegrator mass_integrator( + target_layout_, target_space.GetCoordinateSystem(), mass_matrix_type); solver_ = std::make_unique(mass_integrator, *rhs_integrator_); target_values_ = Kokkos::View( @@ -74,23 +70,29 @@ void OmegaHConservativeProjection::Apply(const Field& source, Field& target) const { CheckApplyCompatible(source, target, *source_layout_, *target_layout_); - - const auto solution = solver_->Solve(*evaluator_, source); - const auto global_to_local = target_layout_->GetGlobalToLocalPermutation(); + if (&source.GetLayout() != source_layout_.get()) { + throw pcms_error( + "OmegaHConservativeProjection::Apply: source field layout mismatch"); + } + if (source.GetData().GetValueType() != FieldValueType::Scalar) { + throw pcms_error( + "OmegaHConservativeProjection::Apply: only scalar fields are supported"); + } const int num_dof_holders = target_layout_->GetNumOwnedDofHolder(); const int num_components = target_layout_->GetNumComponents(); - auto target_values = target_values_; + const auto values = MakeRank2View(target_values_); + const auto solution = solver_->Solve(*evaluator_, source); + const auto global_to_local = target_layout_->GetGlobalToLocalPermutation(); Kokkos::parallel_for( "conservative_projection_scatter_solution", Kokkos::RangePolicy(0, num_dof_holders), KOKKOS_LAMBDA(int i) { for (int c = 0; c < num_components; ++c) { - target_values(i, c) = solution[global_to_local(i) * num_components + c]; + values(i, c) = solution[global_to_local(i) * num_components + c]; } }); Kokkos::fence(); - - target.SetDOFHolderData(MakeConstRank2View(target_values_)); + target.SetDOFHolderDataUnchecked(MakeConstRank2View(target_values_)); } } // namespace pcms diff --git a/src/pcms/transfer/omega_h_conservative_projection.hpp b/src/pcms/transfer/omega_h_conservative_projection.hpp index 18b7a2cbd..2b2abf3d4 100644 --- a/src/pcms/transfer/omega_h_conservative_projection.hpp +++ b/src/pcms/transfer/omega_h_conservative_projection.hpp @@ -4,6 +4,7 @@ #include "pcms/field/function_space.h" #include "pcms/field/layout/omega_h_lagrange.h" #include "pcms/field/point_evaluator.h" +#include "pcms/transfer/mass_matrix_type.hpp" #include "pcms/transfer/transfer_operator.hpp" #include #include @@ -21,7 +22,10 @@ class OmegaHIntersectionRHSIntegrator; // (OmegaHIntersectionRHSIntegrator). // - Localizes integration points in the source mesh (PointEvaluator). // - Assembles the target mass matrix and factors it via KSP -// (GalerkinProjectionSolver). +// (GalerkinProjectionSolver). With MassMatrixType::Lumped the mass +// matrix is row-sum lumped and the KSP degenerates to an exact diagonal +// scaling; conservation is unaffected, but linear fields are no longer +// reproduced exactly on order-1 targets. // // Apply() (per-call cost): // - Evaluates the source field at the fixed integration points. @@ -31,8 +35,9 @@ class OmegaHIntersectionRHSIntegrator; class OmegaHConservativeProjection : public TransferOperator { public: - OmegaHConservativeProjection(const FunctionSpace& source_space, - const FunctionSpace& target_space); + OmegaHConservativeProjection( + const FunctionSpace& source_space, const FunctionSpace& target_space, + MassMatrixType mass_matrix_type = MassMatrixType::Consistent); // Defined in the .cpp where GalerkinProjectionSolver is a complete type. ~OmegaHConservativeProjection() override; diff --git a/src/pcms/transfer/omega_h_control_variate_projection.cpp b/src/pcms/transfer/omega_h_control_variate_projection.cpp index 6916c7155..d8c4e1aa1 100644 --- a/src/pcms/transfer/omega_h_control_variate_projection.cpp +++ b/src/pcms/transfer/omega_h_control_variate_projection.cpp @@ -10,28 +10,38 @@ namespace pcms { OmegaHControlVariateProjection::OmegaHControlVariateProjection( - const FunctionSpace& source_space, const FunctionSpace& target_space, - int samples_per_element, MonteCarloSampling sampling, uint64_t seed) + const PointEvaluatorFactory& source, const FunctionSpace& target_space, + int samples_per_element, MonteCarloSampling sampling, uint64_t seed, + MassMatrixType mass_matrix_type) : target_layout_(std::dynamic_pointer_cast( target_space.GetLayout())), rhs_integrator_(std::make_unique( target_layout_, target_space.GetCoordinateSystem(), samples_per_element, sampling, seed)), - interpolator_(source_space, target_space), - control_variate_(target_space.CreateField()) + interpolator_(source, target_space), + control_variate_(target_space.CreateFunction()) { const auto sample_coords = rhs_integrator_->GetIntegrationPoints(); - source_at_samples_ = source_space.CreatePointEvaluator( + source_at_samples_ = source.CreatePointEvaluator( EvaluationRequest::FromCoordinates(sample_coords)); control_variate_at_samples_ = target_space.CreatePointEvaluator( EvaluationRequest::FromCoordinates(sample_coords)); // Mass integrator is only needed to build the solver; PETSc reference-counts // the matrix so it remains alive inside the KSP after this scope ends. - OmegaHMassIntegrator mass_integrator(target_layout_, - target_space.GetCoordinateSystem()); + OmegaHMassIntegrator mass_integrator( + target_layout_, target_space.GetCoordinateSystem(), mass_matrix_type); solver_ = std::make_unique(mass_integrator, *rhs_integrator_); + + target_values_ = Kokkos::View( + "cv_target_values", target_layout_->GetNumOwnedDofHolder(), + target_layout_->GetNumComponents()); + const auto num_samples = sample_coords.GetValues().extent(0); + f_samples_ = + Kokkos::View("cv_f_samples", num_samples, 1); + residual_ = + Kokkos::View("cv_residual", num_samples, 1); } // Defined here so that GalerkinProjectionSolver (forward-declared in the @@ -41,20 +51,25 @@ OmegaHControlVariateProjection::~OmegaHControlVariateProjection() = default; void OmegaHControlVariateProjection::Apply(const Field& source, Field& target) const { + const int num_dof_holders = target_layout_->GetNumOwnedDofHolder(); + const int num_components = target_layout_->GetNumComponents(); + // 1. Control variate: interpolate the source field onto the target space. interpolator_.Apply(source, control_variate_); // 2. Sample the source field and the control variate at the fixed Monte // Carlo sample points; the stochastic RHS integrates the residual. - const std::size_t num_samples = - rhs_integrator_->GetIntegrationPoints().GetValues().extent(0); - Kokkos::View f_samples("cv_f_samples", num_samples, - 1); - Kokkos::View residual("cv_residual", num_samples, - 1); - source_at_samples_->Evaluate(source, MakeRank2View(f_samples)); - control_variate_at_samples_->Evaluate(control_variate_, - MakeRank2View(residual)); + const std::size_t num_samples = f_samples_.extent(0); + // Shallow copies so the device lambda captures the views, not `this`. + auto f_samples = f_samples_; + auto residual = residual_; + source_at_samples_->Evaluate( + source, ValueView(source.GetData().GetValueBasis(), + MakeRank2View(f_samples))); + control_variate_at_samples_->Evaluate( + control_variate_, + ValueView( + control_variate_.GetData().GetValueBasis(), MakeRank2View(residual))); Kokkos::parallel_for( "cv_residual", Kokkos::RangePolicy(0, num_samples), KOKKOS_LAMBDA(int i) { @@ -66,19 +81,27 @@ void OmegaHControlVariateProjection::Apply(const Field& source, // 4. x = g + delta. delta is indexed by active PETSc row, while the field is // indexed by local DOF holder. - const auto g_nodal = control_variate_.GetDOFHolderData(); + const auto g_nodal = control_variate_.GetDOFHolderData().GetValues(); const auto global_to_local = target_layout_->GetGlobalToLocalPermutation(); - const int nverts = static_cast(g_nodal.extent(0)); - Kokkos::View result("cv_result", nverts, 1); + if (static_cast(g_nodal.extent(0)) != num_dof_holders || + static_cast(g_nodal.extent(1)) != num_components) { + throw pcms_error("OmegaHControlVariateProjection::Apply: control variate " + "extents do not match the target layout"); + } + + const auto values = MakeRank2View(target_values_); Kokkos::parallel_for( - "cv_add_correction", Kokkos::RangePolicy(0, nverts), + "cv_add_correction", + Kokkos::RangePolicy(0, num_dof_holders), KOKKOS_LAMBDA(int i) { - result(i, 0) = g_nodal(i, 0) + delta[global_to_local(i)]; + for (int c = 0; c < num_components; ++c) { + values(i, c) = + g_nodal(i, c) + delta[global_to_local(i) * num_components + c]; + } }); Kokkos::fence(); - - target.SetDOFHolderData(MakeConstRank2View(result)); + target.SetDOFHolderDataUnchecked(MakeConstRank2View(target_values_)); } } // namespace pcms diff --git a/src/pcms/transfer/omega_h_control_variate_projection.hpp b/src/pcms/transfer/omega_h_control_variate_projection.hpp index 4196ea5d0..29e5c6d03 100644 --- a/src/pcms/transfer/omega_h_control_variate_projection.hpp +++ b/src/pcms/transfer/omega_h_control_variate_projection.hpp @@ -5,9 +5,12 @@ #include "pcms/field/function_space.h" #include "pcms/field/layout/omega_h_lagrange.h" #include "pcms/field/point_evaluator.h" +#include "pcms/field/point_evaluator_factory.hpp" #include "pcms/transfer/interpolator.h" +#include "pcms/transfer/mass_matrix_type.hpp" #include "pcms/transfer/monte_carlo_sampling.hpp" #include "pcms/transfer/transfer_operator.hpp" +#include #include #include @@ -37,11 +40,11 @@ class OmegaHMonteCarloRHSIntegrator; class OmegaHControlVariateProjection : public TransferOperator { public: - OmegaHControlVariateProjection(const FunctionSpace& source_space, - const FunctionSpace& target_space, - int samples_per_element, - MonteCarloSampling sampling, - uint64_t seed = 8675309); + OmegaHControlVariateProjection( + const PointEvaluatorFactory& source, const FunctionSpace& target_space, + int samples_per_element, MonteCarloSampling sampling, + uint64_t seed = 8675309, + MassMatrixType mass_matrix_type = MassMatrixType::Consistent); // Defined in the .cpp where GalerkinProjectionSolver is a complete type. ~OmegaHControlVariateProjection() override; @@ -58,6 +61,10 @@ class OmegaHControlVariateProjection : public TransferOperator std::unique_ptr> source_at_samples_; std::unique_ptr> control_variate_at_samples_; std::unique_ptr solver_; + + mutable Kokkos::View target_values_; + mutable Kokkos::View f_samples_; + mutable Kokkos::View residual_; }; } // namespace pcms diff --git a/src/pcms/transfer/omega_h_form_integrator_utils.hpp b/src/pcms/transfer/omega_h_form_integrator_utils.hpp index 23f5e35d2..0f167bde1 100644 --- a/src/pcms/transfer/omega_h_form_integrator_utils.hpp +++ b/src/pcms/transfer/omega_h_form_integrator_utils.hpp @@ -3,20 +3,23 @@ #include "pcms/field/function_space.h" #include "pcms/field/layout/omega_h_lagrange.h" +#include "pcms/field/coordinate_systems/cartesian.hpp" #include "pcms/transfer/mesh_intersection.hpp" #include "pcms/utility/assert.h" #include #include +#include #include #include namespace pcms::detail { -// Shared checks for a scalar Cartesian Lagrange space on a 2D simplex mesh, -// independent of order. Order is validated separately by the callers below. -inline void CheckOmegaHScalarSimplex2DLayout( - CoordinateSystem coordinate_system, +// Shared checks for a scalar Cartesian Lagrange space on a simplex mesh +// (triangles in 2D, tetrahedra in 3D), independent of order. Order is validated +// separately by the callers below. +inline void CheckOmegaHScalarSimplexLayout( + std::shared_ptr coordinate_system, const std::shared_ptr& layout, const char* context, const char* role) { @@ -28,27 +31,30 @@ inline void CheckOmegaHScalarSimplex2DLayout( throw pcms_error(std::string(context) + ": " + role + " space must have exactly one component"); } - if (coordinate_system != CoordinateSystem::Cartesian) { + const Omega_h::Mesh& mesh = layout->GetMesh(); + if (mesh.dim() != 2 && mesh.dim() != 3) { throw pcms_error(std::string(context) + ": " + role + - " space must use Cartesian coordinates"); + " mesh must be 2D or 3D"); } - const Omega_h::Mesh& mesh = layout->GetMesh(); - if (mesh.dim() != 2) { - throw pcms_error(std::string(context) + ": " + role + " mesh must be 2D"); + if (!SameCoordinateSystem(coordinate_system, + csys::Cartesian::Create(mesh.dim()))) { + throw pcms_error(std::string(context) + ": " + role + + " space must use the Cartesian coordinate system of its " + "mesh dimension; map other systems explicitly"); } if (mesh.family() != OMEGA_H_SIMPLEX) { throw pcms_error(std::string(context) + ": " + role + - " mesh must be a simplex (triangle) mesh"); + " mesh must be a simplex (triangle/tetrahedron) mesh"); } } // Strict order-1 check (used where only P1 is supported, e.g. Monte-Carlo RHS). inline void CheckOmegaHScalarP1Layout( - CoordinateSystem coordinate_system, + std::shared_ptr coordinate_system, const std::shared_ptr& layout, const char* context, const char* role) { - CheckOmegaHScalarSimplex2DLayout(coordinate_system, layout, context, role); + CheckOmegaHScalarSimplexLayout(coordinate_system, layout, context, role); if (layout->GetOrder() != 1) { throw pcms_error(std::string(context) + ": " + role + " space must be order-1"); @@ -59,11 +65,11 @@ inline void CheckOmegaHScalarP1Layout( // intersection integrator handles source and target orders independently, so // this replaces the strict P1 requirement on those paths. inline void CheckOmegaHScalarLagrangeLayout( - CoordinateSystem coordinate_system, + std::shared_ptr coordinate_system, const std::shared_ptr& layout, const char* context, const char* role) { - CheckOmegaHScalarSimplex2DLayout(coordinate_system, layout, context, role); + CheckOmegaHScalarSimplexLayout(coordinate_system, layout, context, role); const int order = layout->GetOrder(); if (order != 0 && order != 1) { throw pcms_error(std::string(context) + ": " + role + @@ -71,200 +77,263 @@ inline void CheckOmegaHScalarLagrangeLayout( } } -[[nodiscard]] OMEGA_H_INLINE Omega_h::Vector<2> GlobalFromBarycentric( - const MeshField::Vector3& barycentric_coord, - const Omega_h::Few, 3>& verts_coord) +// Map barycentric coordinates on a simplex (Dim+1 barycentric components) to +// the global Cartesian point, given the simplex's Dim+1 vertex coordinates. +template +[[nodiscard]] OMEGA_H_INLINE Omega_h::Vector GlobalFromBarycentric( + const Omega_h::Vector& barycentric_coord, + const Omega_h::Few, Dim + 1>& 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]; + Omega_h::Vector real_coords; + for (int d = 0; d < Dim; ++d) { + real_coords[d] = 0.0; } - return real_coords; -} - -[[nodiscard]] OMEGA_H_INLINE Omega_h::Vector<3> EvaluateBarycentric( - 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]; + for (int i = 0; i < Dim + 1; ++i) { + for (int d = 0; d < Dim; ++d) { + real_coords[d] += barycentric_coord[i] * verts_coord[i][d]; + } } - return Omega_h::barycentric_from_global<2, 2>(point, omegah_vector); + return real_coords; } -[[nodiscard]] OMEGA_H_INLINE int RemoveDuplicateVerticesAndFixLinks( - r3d::Polytope<2>& poly, const double tol = 1e-12) +// Walk each face of a clipped r3d polyhedron exactly once and fan it into +// triangles, invoking op(v0, v1, v2) for each triangle (v0 is the face's anchor +// vertex, so a face with k vertices yields k-2 triangles). Every vertex of an +// r3d clipped Polytope<3> has exactly three face-neighbors (pnbrs); marking +// each directed edge as it is consumed guarantees every face is emitted once. +// This mirrors the edge-marking traversal buried inside r3d::reduce, which r3d +// does not expose for reuse, so the traversal is reproduced here once and +// shared. Vertices arrive as r3d::Vector<3> (indexable [0..2]). +template +OMEGA_H_INLINE void ForEachPolytopeFaceTriangle(const r3d::Polytope<3>& poly, + TriangleOp&& op) { - const int old_n = poly.nverts; - int new_n = 0; - - 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; + // emarks[v][p] == 1 once the directed edge (v, pnbr p) has been consumed. + int emarks[r3d::Polytope<3>::max_verts][3] = {{}}; + for (int vstart = 0; vstart < poly.nverts; ++vstart) { + for (int pstart = 0; pstart < 3; ++pstart) { + if (emarks[vstart][pstart]) { + continue; } - } - if (!dup) { - poly.verts[new_n] = poly.verts[i]; - ++new_n; - } - } - poly.nverts = new_n; - - if (new_n >= 3) { - 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; - - 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; + int pnext = pstart; + int vcur = vstart; + emarks[vcur][pnext] = 1; + int vnext = poly.verts[vcur].pnbrs[pnext]; + const auto face_v0 = poly.verts[vcur].pos; + + // Move to the second edge of this face. + int np = 0; + for (np = 0; np < 3; ++np) { + if (poly.verts[vnext].pnbrs[np] == vcur) { + break; + } + } + vcur = vnext; + pnext = (np + 1) % 3; + emarks[vcur][pnext] = 1; + vnext = poly.verts[vcur].pnbrs[pnext]; + + // Fan the face into triangles anchored at face_v0. + while (vnext != vstart) { + op(face_v0, poly.verts[vnext].pos, poly.verts[vcur].pos); + + // Advance around the face. + for (np = 0; np < 3; ++np) { + if (poly.verts[vnext].pnbrs[np] == vcur) { + break; + } } + vcur = vnext; + pnext = (np + 1) % 3; + emarks[vcur][pnext] = 1; + vnext = poly.verts[vcur].pnbrs[pnext]; } } + } +} - r3d::Vertex<2> tmp[r3d::MaxVerts<2>::value]; - for (int i = 0; i < new_n; ++i) { - tmp[i] = poly.verts[order[i]]; - } +// --------------------------------------------------------------------------- +// Star decomposition of a clipped r3d polytope into quadrature simplices. +// --------------------------------------------------------------------------- - for (int i = 0; i < new_n; ++i) { - poly.verts[i] = tmp[i]; - } +// Boundary (Dim-1)-simplices of a clipped r3d polytope as its vertex graph +// encodes them; op(facet) receives the facet's Dim vertices in walk order. +// +// This is the one dimension-specific step of the decomposition, because r3d's +// graph has no dimension-generic boundary iterator (r3d::reduce is itself +// written once per dimension). In 2D, pnbrs[0] is the next vertex around the +// single cycle and every edge is a facet. In 3D, the three pnbrs are the +// cyclically ordered neighbors and faces are recovered by the edge-marking walk +template +OMEGA_H_INLINE void ForEachPolytopeBoundarySimplex(const r3d::Polytope<2>& poly, + FacetOp&& op) +{ + for (int v = 0; v < poly.nverts; ++v) { + r3d::Few, 2> facet; + facet[0] = poly.verts[v].pos; + facet[1] = poly.verts[poly.verts[v].pnbrs[0]].pos; + op(facet); + } +} - 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; - } +template +OMEGA_H_INLINE void ForEachPolytopeBoundarySimplex(const r3d::Polytope<3>& poly, + FacetOp&& op) +{ + ForEachPolytopeFaceTriangle(poly, [&](const r3d::Vector<3>& a, + const r3d::Vector<3>& b, + const r3d::Vector<3>& c) { + r3d::Few, 3> facet; + facet[0] = a; + facet[1] = b; + facet[2] = c; + op(facet); + }); +} - double area = r3d::measure(poly); - if (area < 0.0) { - 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 { - for (int i = 0; i < new_n; ++i) { - poly.verts[i].pnbrs[0] = -1; - poly.verts[i].pnbrs[1] = -1; +// Signed measure of the simplex (apex, facet[0], ..., facet[Dim-1]) in the +// order the boundary walk emits it. +template +[[nodiscard]] OMEGA_H_INLINE Omega_h::Real StarSimplexSignedMeasure( + const Omega_h::Vector& apex, + const r3d::Few, Dim>& facet) +{ + Omega_h::Few, Dim> basis; + for (int i = 0; i < Dim; ++i) { + for (int d = 0; d < Dim; ++d) { + basis[i][d] = facet[i][d] - apex[d]; } } + return Omega_h::simplex_size_from_basis(basis); +} + +// Star-decompose a clipped r3d polytope into simplices, invoking +// op(simplex, measure) for every piece with |measure| > eps. +template +OMEGA_H_INLINE void ForEachPolytopeStarSimplex(const r3d::Polytope& poly, + const double eps, SimplexOp&& op) +{ + Omega_h::Vector apex; + for (int d = 0; d < Dim; ++d) { + apex[d] = poly.verts[0].pos[d]; + } - return new_n; + Omega_h::Real signed_total = 0.0; + ForEachPolytopeBoundarySimplex( + poly, [&](const r3d::Few, Dim>& facet) { + signed_total += StarSimplexSignedMeasure(apex, facet); + }); + const Omega_h::Real orientation = (signed_total < 0.0) ? -1.0 : 1.0; + + ForEachPolytopeBoundarySimplex( + poly, [&](const r3d::Few, Dim>& facet) { + const Omega_h::Real measure = + orientation * StarSimplexSignedMeasure(apex, facet); + if (Kokkos::fabs(measure) > eps) { + Omega_h::Few, Dim + 1> simplex; + simplex[0] = apex; + for (int i = 0; i < Dim; ++i) { + for (int d = 0; d < Dim; ++d) { + simplex[i + 1][d] = facet[i][d]; + } + } + op(simplex, measure); + } + }); } -template -OMEGA_H_INLINE void ForEachIntersectionSubtriangle( +// loop over the sub-simplices (triangles in 2D, tets in 3D) that partition each +// target element's intersection with the source mesh +template +OMEGA_H_INLINE void ForEachIntersectionSubsimplex( 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) + const Omega_h::LOs& tgt_elems2nodes, const Omega_h::LOs& src_elems2nodes, + SimplexOp&& op) { auto tgt_elm_vert_coords = - get_vert_coords_of_elem(tgt_coords, tgt_faces2nodes, elm); + get_vert_coords_of_elem(tgt_coords, tgt_elems2nodes, 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; + auto src_elm_vert_coords = get_vert_coords_of_elem( + src_coords, src_elems2nodes, current_src_elm); + r3d::Polytope poly; r3d::intersect_simplices(poly, tgt_elm_vert_coords, src_elm_vert_coords); - auto nverts = RemoveDuplicateVerticesAndFixLinks(poly, 1e-12); - auto poly_area = r3d::measure(poly); - - for (int j = 1; j < nverts - 1; ++j) { - 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; - } - - op(tri_coords, tgt_elm_vert_coords, src_elm_vert_coords, current_src_elm, - area); + if (poly.nverts < Dim + 1) { + continue; } + const double poly_measure = Kokkos::fabs(r3d::measure(poly)); + const double eps = + PCMS_INTERSECTION_ABS_TOL + PCMS_INTERSECTION_REL_TOL * poly_measure; + + ForEachPolytopeStarSimplex( + poly, eps, + [&](const Omega_h::Few, Dim + 1>& simplex, + Omega_h::Real measure) { op(simplex, current_src_elm, measure); }); } } -// Barycentric integration points and weights for a reference triangle, taken -// from MeshField's predefined triangle quadrature rules and staged on device -// for use in element integration kernels. +// Maps spatial dimension to the MeshField simplex topology whose reference +// quadrature rules we use (triangle in 2D, tetrahedron in 3D). +template +struct SimplexTopology; +template <> +struct SimplexTopology<2> +{ + static constexpr MeshField::Mesh_Topology value = MeshField::Triangle; +}; +template <> +struct SimplexTopology<3> +{ + static constexpr MeshField::Mesh_Topology value = MeshField::Tetrahedron; +}; + +// Barycentric integration points and weights for a reference simplex (triangle +// in 2D, tetrahedron in 3D), taken from MeshField's predefined quadrature rules +// and staged on device for use in element integration kernels. // // MeshField::getIntegrationPoints returns a host std::vector, which cannot be // dereferenced inside a device kernel, so the (tiny) rule is copied into device -// Kokkos views once at construction. +// Kokkos views once at construction. Each barycentric point has Dim+1 +// components. // // The quadrature order is a runtime argument because the required polynomial // accuracy depends on the source and target element orders (degree = // source_order + target_order), which are only known at construction. +template struct IntegrationData { - Kokkos::View bary_coords; // barycentric coordinates - Kokkos::View weights; // quadrature weights + Kokkos::View + bary_coords; // barycentric coordinates + Kokkos::View weights; // quadrature weights explicit IntegrationData(int order) { - auto ip_vec = MeshField::getIntegrationPoints(order); + auto ip_vec = + MeshField::getIntegrationPoints::value>(order); const std::size_t num_ip = ip_vec.size(); - bary_coords = Kokkos::View("bary_coords", num_ip); + 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; + // MeshField returns points in reduced parametric coordinates: only the + // first Dim barycentric components are stored, with the last implied by + // the partition of unity. Expand to the full Dim+1 barycentric form + // consumed by GlobalFromBarycentric. + Omega_h::Real last = 1.0; + for (int d = 0; d < Dim; ++d) { + const Omega_h::Real xi = ip_vec[i].param[d]; + bary_coords_host(i, d) = xi; + last -= xi; + } + bary_coords_host(i, Dim) = last; weights_host(i) = ip_vec[i].weight; } Kokkos::deep_copy(bary_coords, bary_coords_host); @@ -274,9 +343,10 @@ struct IntegrationData int size() const { return bary_coords.extent(0); } }; -// Target Lagrange basis on a triangle, parameterized by element order, for the +// Target Lagrange basis on a simplex (triangle in 2D, tetrahedron in 3D), +// parameterized by spatial dimension and element order, for the // conservative-projection RHS assembly. Order 0 is a single element-constant -// DOF; order 1 is the three vertex (barycentric) DOFs. Higher orders slot in as +// DOF; order 1 is the Dim+1 vertex (barycentric) DOFs. Higher orders slot in as // additional specializations, kept in lock-step with element_dispatch.h. // // Each specialization provides, for a target element `elm` with local vertex @@ -284,40 +354,39 @@ struct IntegrationData // ndof number of local target DOFs // Index(...) active PETSc row for local dof k // Values(pt, ...) basis values at the (global) integration point pt -template -struct TargetTriBasis; +template +struct TargetSimplexBasis; -template <> -struct TargetTriBasis<0> +template +struct TargetSimplexBasis { static constexpr int ndof = 1; template - KOKKOS_INLINE_FUNCTION static LO Index(const Permutation& permutation, - int elm, - const Omega_h::Few&, - int /*k*/) + KOKKOS_INLINE_FUNCTION static LO Index( + const Permutation& permutation, int elm, + const Omega_h::Few&, int /*k*/) { return permutation(elm); } KOKKOS_INLINE_FUNCTION static void Values( - const Omega_h::Vector<2>&, const Omega_h::Few, 3>&, - Omega_h::Real out[ndof]) + const Omega_h::Vector&, + const Omega_h::Few, Dim + 1>&, Omega_h::Real out[ndof]) { out[0] = 1.0; } }; -template <> -struct TargetTriBasis<1> +template +struct TargetSimplexBasis { - static constexpr int ndof = 3; + static constexpr int ndof = Dim + 1; template KOKKOS_INLINE_FUNCTION static LO Index( const Permutation& permutation, int /*elm*/, - const Omega_h::Few& verts, int k) + const Omega_h::Few& verts, int k) { return permutation(verts[k]); } @@ -325,14 +394,14 @@ struct TargetTriBasis<1> // P1 basis functions are the barycentric coordinates of the target element // evaluated at the (global) integration point. KOKKOS_INLINE_FUNCTION static void Values( - const Omega_h::Vector<2>& pt, - const Omega_h::Few, 3>& tgt_verts, + const Omega_h::Vector& pt, + const Omega_h::Few, Dim + 1>& tgt_verts, Omega_h::Real out[ndof]) { - const auto bary = Omega_h::barycentric_from_global<2, 2>(pt, tgt_verts); - out[0] = bary[0]; - out[1] = bary[1]; - out[2] = bary[2]; + const auto bary = Omega_h::barycentric_from_global(pt, tgt_verts); + for (int i = 0; i < ndof; ++i) { + out[i] = bary[i]; + } } }; diff --git a/src/pcms/transfer/omega_h_intersection_rhs_integrator.cpp b/src/pcms/transfer/omega_h_intersection_rhs_integrator.cpp index ae9b293f2..974f3d0ea 100644 --- a/src/pcms/transfer/omega_h_intersection_rhs_integrator.cpp +++ b/src/pcms/transfer/omega_h_intersection_rhs_integrator.cpp @@ -1,5 +1,7 @@ #include "pcms/transfer/omega_h_intersection_rhs_integrator.hpp" #include "pcms/field/element_dispatch.h" +#include "pcms/field/evaluator/omega_h_lagrange.h" +#include "pcms/field/function_space/lagrange.h" #include "pcms/transfer/omega_h_form_integrator_utils.hpp" #include "pcms/transfer/petsc_utils.hpp" #include "pcms/utility/assert.h" @@ -7,6 +9,7 @@ #include #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" namespace pcms { @@ -25,14 +28,30 @@ struct Data PetscInt num_target_dofs = 0; }; -template +template Data BuildDataImpl(const OmegaHLagrangeLayout& source_layout, - const OmegaHLagrangeLayout& target_layout, int quad_order); + const OmegaHLagrangeLayout& target_layout, int quad_order, + const GridPointSearchVariant* source_search); + +// The source space's point search, if the space is an Omega_h Lagrange space +// and so owns one the intersection can reuse; null otherwise. +const GridPointSearchVariant* SourceSearchFromSpace(const FunctionSpace& space) +{ + const auto* lagrange = dynamic_cast(&space); + if (lagrange == nullptr) { + return nullptr; + } + const auto* factory = + dynamic_cast*>( + lagrange->GetEvaluatorFactory().get()); + return factory ? &factory->GetSearch() : nullptr; +} Data BuildData(const std::shared_ptr& source_layout, - CoordinateSystem source_coordinate_system, + std::shared_ptr source_coordinate_system, const std::shared_ptr& target_layout, - CoordinateSystem target_coordinate_system) + std::shared_ptr target_coordinate_system, + const GridPointSearchVariant* source_search) { detail::CheckOmegaHScalarLagrangeLayout( source_coordinate_system, source_layout, "OmegaHIntersectionRHSIntegrator", @@ -41,38 +60,54 @@ Data BuildData(const std::shared_ptr& source_layout, target_coordinate_system, target_layout, "OmegaHIntersectionRHSIntegrator", "target"); + const int dim = target_layout->GetMesh().dim(); + if (source_layout->GetMesh().dim() != dim) { + throw pcms_error("OmegaHIntersectionRHSIntegrator: source and target mesh " + "dimensions differ"); + } + // The integrand f_src * phi_target has polynomial degree source_order + - // target_order on each intersection subtriangle; integrate it exactly (with a + // target_order on each intersection sub-simplex; integrate it exactly (with a // 1-point floor so a P0->P0 pair still gets a valid rule). const int quad_order = std::max(1, source_layout->GetOrder() + target_layout->GetOrder()); return detail::DispatchByOrder(target_layout->GetOrder(), [&](auto order_c) { constexpr int TgtOrder = decltype(order_c)::value; - return BuildDataImpl(*source_layout, *target_layout, quad_order); + if (dim == 3) { + return BuildDataImpl<3, TgtOrder>(*source_layout, *target_layout, + quad_order, source_search); + } + return BuildDataImpl<2, TgtOrder>(*source_layout, *target_layout, + quad_order, source_search); }); } -template +template Data BuildDataImpl(const OmegaHLagrangeLayout& source_layout, - const OmegaHLagrangeLayout& target_layout, int quad_order) + const OmegaHLagrangeLayout& target_layout, int quad_order, + const GridPointSearchVariant* source_search) { - using Basis = detail::TargetTriBasis; + using Basis = detail::TargetSimplexBasis; constexpr int ndof = Basis::ndof; + // Reference-to-physical Jacobian factor for a simplex: the reference simplex + // measure is 1/Dim! (1/2 in 2D, 1/6 in 3D), so a physical sub-simplex of + // measure `m` scales the reference quadrature weights by Dim! * m. + constexpr Omega_h::Real ref_factor = (Dim == 3) ? 6.0 : 2.0; Omega_h::Mesh& source_mesh = source_layout.GetMesh(); Omega_h::Mesh& target_mesh = target_layout.GetMesh(); - const auto intersections = intersectTargets(source_mesh, target_mesh); + const auto intersections = + source_search ? intersectTargets(source_mesh, target_mesh, *source_search) + : intersectTargets(source_mesh, target_mesh); const auto& tgt_coords = target_mesh.coords(); - const auto& tgt_faces2nodes = - target_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto& tgt_elems2nodes = target_mesh.ask_down(Dim, Omega_h::VERT).ab2b; const auto& src_coords = source_mesh.coords(); - const auto& src_faces2nodes = - source_mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto& src_elems2nodes = source_mesh.ask_down(Dim, Omega_h::VERT).ab2b; - detail::IntegrationData ip_data(quad_order); + detail::IntegrationData ip_data(quad_order); const int npts = ip_data.size(); auto bary_coords = ip_data.bary_coords; // device view auto weights = ip_data.weights; // device view @@ -88,12 +123,10 @@ Data BuildDataImpl(const OmegaHLagrangeLayout& source_layout, Kokkos::parallel_for( "rhs_count", nelems, KOKKOS_LAMBDA(int elm) { int count = 0; - detail::ForEachIntersectionSubtriangle( + detail::ForEachIntersectionSubsimplex( elm, {tgt2src_offsets, tgt2src_indices}, tgt_coords, src_coords, - tgt_faces2nodes, src_faces2nodes, - [&](const Omega_h::Few, 3>&, - const r3d::Few, 3>&, - const r3d::Few, 3>&, int, + tgt_elems2nodes, src_elems2nodes, + [&](const Omega_h::Few, Dim + 1>&, int, Omega_h::Real) { count += npts; }); ip_counts[elm] = count; }); @@ -105,7 +138,7 @@ Data BuildDataImpl(const OmegaHLagrangeLayout& source_layout, // Pass 2: fill coords, node_gids, and coeffs on device. node_gids/coeffs hold // ndof (target DOFs per element) entries per integration point. - Kokkos::View coords("rhs_coords", num_pts, 2); + Kokkos::View coords("rhs_coords", num_pts, Dim); Kokkos::View node_gids( "rhs_node_gids", static_cast(num_pts) * ndof); Kokkos::View coeffs( @@ -113,37 +146,45 @@ Data BuildDataImpl(const OmegaHLagrangeLayout& source_layout, Kokkos::parallel_for( "rhs_fill", nelems, KOKKOS_LAMBDA(int elm) { - const auto tgt_verts = Omega_h::gather_verts<3>(tgt_faces2nodes, elm); - const Omega_h::Matrix<2, 3> tgt_vert_mat = - Omega_h::gather_vectors<3, 2>(tgt_coords, tgt_verts); - Omega_h::Few, 3> tgt_omh; - for (int i = 0; i < 3; ++i) - tgt_omh[i] = {tgt_vert_mat[i][0], tgt_vert_mat[i][1]}; + const auto tgt_verts = + Omega_h::gather_verts(tgt_elems2nodes, elm); + const Omega_h::Matrix tgt_vert_mat = + Omega_h::gather_vectors(tgt_coords, tgt_verts); + Omega_h::Few, Dim + 1> tgt_omh; + for (int i = 0; i < Dim + 1; ++i) { + for (int d = 0; d < Dim; ++d) { + tgt_omh[i][d] = tgt_vert_mat[i][d]; + } + } int ip_local = 0; const int offset = ip_offsets[elm]; - detail::ForEachIntersectionSubtriangle( + detail::ForEachIntersectionSubsimplex( elm, {tgt2src_offsets, tgt2src_indices}, tgt_coords, src_coords, - tgt_faces2nodes, src_faces2nodes, - [&](const Omega_h::Few, 3>& tri, - const r3d::Few, 3>&, - const r3d::Few, 3>&, int, Omega_h::Real area) { + tgt_elems2nodes, src_elems2nodes, + [&](const Omega_h::Few, Dim + 1>& sub, int, + Omega_h::Real measure) { for (int ip_idx = 0; ip_idx < npts; ++ip_idx) { - const auto bary = bary_coords(ip_idx); + Omega_h::Vector bary; + for (int d = 0; d < Dim + 1; ++d) { + bary[d] = bary_coords(ip_idx, d); + } const double w = weights(ip_idx); - const auto pt = detail::GlobalFromBarycentric(bary, tri); + const auto pt = detail::GlobalFromBarycentric(bary, sub); Omega_h::Real basis[ndof]; Basis::Values(pt, tgt_omh, basis); const int global_ip = offset + ip_local; - coords(global_ip, 0) = pt[0]; - coords(global_ip, 1) = pt[1]; + for (int d = 0; d < Dim; ++d) { + coords(global_ip, d) = pt[d]; + } for (int k = 0; k < ndof; ++k) { node_gids(global_ip * ndof + k) = static_cast( Basis::Index(global_to_local, elm, tgt_verts, k)); - coeffs(global_ip * ndof + k) = basis[k] * w * 2.0 * area; + coeffs(global_ip * ndof + k) = + basis[k] * w * ref_factor * measure; } ++ip_local; } @@ -175,18 +216,30 @@ OmegaHIntersectionRHSIntegrator::OmegaHIntersectionRHSIntegrator( source_space.GetCoordinateSystem(), std::dynamic_pointer_cast( target_space.GetLayout()), - target_space.GetCoordinateSystem()) + target_space.GetCoordinateSystem(), SourceSearchFromSpace(source_space)) +{ +} + +OmegaHIntersectionRHSIntegrator::OmegaHIntersectionRHSIntegrator( + std::shared_ptr source_layout, + std::shared_ptr source_coordinate_system, + std::shared_ptr target_layout, + std::shared_ptr target_coordinate_system) + : OmegaHIntersectionRHSIntegrator( + std::move(source_layout), std::move(source_coordinate_system), + std::move(target_layout), std::move(target_coordinate_system), nullptr) { } OmegaHIntersectionRHSIntegrator::OmegaHIntersectionRHSIntegrator( std::shared_ptr source_layout, - CoordinateSystem source_coordinate_system, + std::shared_ptr source_coordinate_system, std::shared_ptr target_layout, - CoordinateSystem target_coordinate_system) + std::shared_ptr target_coordinate_system, + const GridPointSearchVariant* source_search) { Data data = BuildData(source_layout, source_coordinate_system, target_layout, - target_coordinate_system); + target_coordinate_system, source_search); coords_ = std::move(data.coords); node_gids_ = std::move(data.node_gids); coeffs_ = std::move(data.coeffs); @@ -194,15 +247,15 @@ OmegaHIntersectionRHSIntegrator::OmegaHIntersectionRHSIntegrator( const PetscInt nnz = static_cast(node_gids_.extent(0)); PetscErrorCode ierr = - createSeqVec(PETSC_COMM_WORLD, data.num_target_dofs, &vec_); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + createSeqVec(PETSC_COMM_SELF, data.num_target_dofs, &vec_); + CHKERRABORT(PETSC_COMM_SELF, ierr); // VecSetPreallocationCOO takes the COO indices on the host // TODO: ask Todd/PETSc folks if there is a better way to do this for GPU // support auto node_gids_host = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, node_gids_); ierr = VecSetPreallocationCOO(vec_, nnz, node_gids_host.data()); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); } OmegaHIntersectionRHSIntegrator::~OmegaHIntersectionRHSIntegrator() @@ -219,8 +272,9 @@ OmegaHIntersectionRHSIntegrator::~OmegaHIntersectionRHSIntegrator() CoordinateView OmegaHIntersectionRHSIntegrator::GetIntegrationPoints() const noexcept { - return CoordinateView(CoordinateSystem::Cartesian, - MakeConstRank2View(coords_)); + return CoordinateView( + csys::Cartesian::Create(static_cast(coords_.extent(1))), + MakeConstRank2View(coords_)); } Vec OmegaHIntersectionRHSIntegrator::GetVector() const noexcept @@ -239,12 +293,9 @@ void OmegaHIntersectionRHSIntegrator::Assemble( PCMS_ALWAYS_ASSERT(sampled_values.extent(1) >= 1); PetscErrorCode ierr = VecZeroEntries(vec_); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); - auto sv = Kokkos::View>( - sampled_values.data_handle(), sampled_values.extent(0), - sampled_values.extent(1)); + auto sv = sampled_values; Kokkos::View coo_vals("rhs_coo_vals", num_pts * ndof); auto coeffs = coeffs_; @@ -258,7 +309,7 @@ void OmegaHIntersectionRHSIntegrator::Assemble( }); ierr = VecSetValuesCOO(vec_, coo_vals.data(), ADD_VALUES); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); } // --------------------------------------------------------------------------- diff --git a/src/pcms/transfer/omega_h_intersection_rhs_integrator.hpp b/src/pcms/transfer/omega_h_intersection_rhs_integrator.hpp index 8c798f325..808ba272f 100644 --- a/src/pcms/transfer/omega_h_intersection_rhs_integrator.hpp +++ b/src/pcms/transfer/omega_h_intersection_rhs_integrator.hpp @@ -1,7 +1,7 @@ #ifndef PCMS_TRANSFER_OMEGA_H_INTERSECTION_RHS_INTEGRATOR_HPP #define PCMS_TRANSFER_OMEGA_H_INTERSECTION_RHS_INTEGRATOR_HPP -#include "pcms/field/coordinate_system.h" +#include "pcms/field/coordinate_view.hpp" #include "pcms/field/function_space.h" #include "pcms/field/layout/omega_h_lagrange.h" #include "pcms/transfer/linear_form_integrator.hpp" @@ -20,7 +20,10 @@ namespace pcms // // Construction validates both spaces, computes mesh intersections, and runs a // two-pass device kernel (count then fill) to build the quadrature data and -// allocate the owned RHS vector sized to the target DOF count. +// allocate the owned RHS vector sized to the target DOF count. When built from +// function spaces and the source is an Omega_h Lagrange space, the intersection +// reuses the point search that space already owns; the layout constructor has +// no space to borrow from and builds its own. // // Assemble(sampled_values): // Zeros the owned vector, then for each integration point i and local target @@ -35,9 +38,9 @@ class OmegaHIntersectionRHSIntegrator : public LinearFormIntegrator const FunctionSpace& target_space); OmegaHIntersectionRHSIntegrator( std::shared_ptr source_layout, - CoordinateSystem source_coordinate_system, + std::shared_ptr source_coordinate_system, std::shared_ptr target_layout, - CoordinateSystem target_coordinate_system); + std::shared_ptr target_coordinate_system); ~OmegaHIntersectionRHSIntegrator(); Vec GetVector() const noexcept override; @@ -49,6 +52,15 @@ class OmegaHIntersectionRHSIntegrator : public LinearFormIntegrator Rank2View sampled_values) override; private: + // Both public constructors delegate here; `source_search` may be null, in + // which case the intersection builds its own search over the source mesh. + OmegaHIntersectionRHSIntegrator( + std::shared_ptr source_layout, + std::shared_ptr source_coordinate_system, + std::shared_ptr target_layout, + std::shared_ptr target_coordinate_system, + const GridPointSearchVariant* source_search); + Vec vec_ = nullptr; Kokkos::View coords_; // [num_pts][dim] integration point coordinates diff --git a/src/pcms/transfer/omega_h_mass_integrator.cpp b/src/pcms/transfer/omega_h_mass_integrator.cpp index 29cd90e2e..c34dd529b 100644 --- a/src/pcms/transfer/omega_h_mass_integrator.cpp +++ b/src/pcms/transfer/omega_h_mass_integrator.cpp @@ -17,10 +17,24 @@ namespace pcms namespace { +// Measure (area in 2D, volume in 3D) of a simplex from its vertex-difference +// basis. +template +KOKKOS_INLINE_FUNCTION Omega_h::Real SimplexMeasure( + const Omega_h::Few, Dim>& basis) +{ + if constexpr (Dim == 3) { + return Kokkos::fabs(Omega_h::tet_volume_from_basis(basis)); + } else { + return Kokkos::fabs(Omega_h::triangle_area_from_basis(basis)); + } +} + // Fills the diagonal COO entries of a P0 (piecewise-constant) mass matrix: one -// entry per element on its own DOF, valued at the element area. +// entry per element on its own DOF, valued at the element measure. +template void FillP0MassCoo( - int nelems, const Omega_h::Reals& coords, const Omega_h::LOs& faces2nodes, + int nelems, const Omega_h::Reals& coords, const Omega_h::LOs& elems2nodes, const Kokkos::View& global_to_local, const Kokkos::View& coo_rows, const Kokkos::View& coo_cols, @@ -28,128 +42,164 @@ void FillP0MassCoo( { Kokkos::parallel_for( "mass_p0_diag", nelems, KOKKOS_LAMBDA(int e) { - const auto verts = Omega_h::gather_verts<3>(faces2nodes, e); - const Omega_h::Matrix<2, 3> vm = - Omega_h::gather_vectors<3, 2>(coords, verts); - Omega_h::Few, 2> basis; - basis[0] = vm[1] - vm[0]; - basis[1] = vm[2] - vm[0]; - const Omega_h::Real area = - Kokkos::fabs(Omega_h::triangle_area_from_basis(basis)); + const auto verts = Omega_h::gather_verts(elems2nodes, e); + const Omega_h::Matrix vm = + Omega_h::gather_vectors(coords, verts); + Omega_h::Few, Dim> basis; + for (int d = 0; d < Dim; ++d) { + basis[d] = vm[d + 1] - vm[0]; + } + const Omega_h::Real measure = SimplexMeasure(basis); const PetscInt g = static_cast(global_to_local(e)); coo_rows(e) = g; coo_cols(e) = g; - vals(e) = static_cast(area); + vals(e) = static_cast(measure); }); } -// Fills the 3x3-block COO sparsity pattern of a P1 (linear) mass matrix: each -// element contributes a dense block coupling its three vertex DOFs. +// Fills the (Dim+1)x(Dim+1)-block COO sparsity pattern of a P1 (linear) mass +// matrix: each element contributes a dense block coupling its vertex DOFs. When +// lumped, every block entry is mapped onto the row's diagonal instead; PETSc's +// COO assembly sums repeated indices, so the unmodified element values +// accumulate into the row-sum lumped diagonal. +template void FillP1MassCooPattern( - int nelems, const Omega_h::LOs& faces2nodes, + int nelems, const Omega_h::LOs& elems2nodes, const Kokkos::View& global_to_local, - const Kokkos::View& coo_rows, + bool lumped, const Kokkos::View& coo_rows, const Kokkos::View& coo_cols) { + constexpr int nv = Dim + 1; Kokkos::parallel_for( "mass_coo_pattern", nelems, KOKKOS_LAMBDA(int e) { - const auto verts = Omega_h::gather_verts<3>(faces2nodes, e); - for (int i = 0; i < 3; ++i) { - for (int j = 0; j < 3; ++j) { - const int idx = e * 9 + i * 3 + j; - coo_rows(idx) = static_cast(global_to_local(verts[i])); - coo_cols(idx) = static_cast(global_to_local(verts[j])); + const auto verts = Omega_h::gather_verts(elems2nodes, e); + for (int i = 0; i < nv; ++i) { + for (int j = 0; j < nv; ++j) { + const int idx = e * (nv * nv) + i * nv + j; + const auto row = static_cast(global_to_local(verts[i])); + coo_rows(idx) = row; + coo_cols(idx) = + lumped ? row : static_cast(global_to_local(verts[j])); } } }); } -} // namespace - -OmegaHMassIntegrator::OmegaHMassIntegrator(const FunctionSpace& target_space) - : OmegaHMassIntegrator(std::dynamic_pointer_cast( - target_space.GetLayout()), - target_space.GetCoordinateSystem()) -{ -} - -OmegaHMassIntegrator::OmegaHMassIntegrator( - std::shared_ptr target_layout, - CoordinateSystem coordinate_system) +// Assembles the target-space mass matrix for spatial dimension Dim (triangles +// for Dim==2, tetrahedra for Dim==3) and returns the owned PETSc matrix. +template +Mat BuildOmegaHMassMatrixImpl(Omega_h::Mesh& mesh, + const OmegaHLagrangeLayout& target_layout, + MassMatrixType mass_type) { - detail::CheckOmegaHScalarLagrangeLayout(coordinate_system, target_layout, - "OmegaHMassIntegrator", "target"); - - Omega_h::Mesh& mesh = target_layout->GetMesh(); - const auto global_to_local = target_layout->GetGlobalToLocalPermutation(); + const auto global_to_local = target_layout.GetGlobalToLocalPermutation(); const PetscInt num_dofs = - static_cast(target_layout->GetNumOwnedDofHolder()); + static_cast(target_layout.GetNumOwnedDofHolder()); const int nelems = mesh.nelems(); + Mat mat = nullptr; - if (target_layout->GetOrder() == 0) { + if (target_layout.GetOrder() == 0) { // P0 target: piecewise-constant basis functions have disjoint support, so - // the mass matrix is diagonal with M_ee = area(e). One COO entry per + // the mass matrix is diagonal with M_ee = measure(e). One COO entry per // element on its own (element-id) diagonal. const auto& coords = mesh.coords(); - const auto& faces2nodes = mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto& elems2nodes = mesh.ask_down(Dim, Omega_h::VERT).ab2b; const PetscInt nnz = static_cast(nelems); Kokkos::View coo_rows("mass_coo_rows", nnz); Kokkos::View coo_cols("mass_coo_cols", nnz); Kokkos::View vals("mass_vals", nnz); - FillP0MassCoo(nelems, coords, faces2nodes, global_to_local, coo_rows, - coo_cols, vals); + FillP0MassCoo(nelems, coords, elems2nodes, global_to_local, coo_rows, + coo_cols, vals); PetscErrorCode ierr = - createSeqAIJMat(PETSC_COMM_WORLD, num_dofs, num_dofs, 0, nullptr, &mat_); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + createSeqAIJMat(PETSC_COMM_SELF, num_dofs, num_dofs, 0, nullptr, &mat); + CHKERRABORT(PETSC_COMM_SELF, ierr); auto coo_rows_host = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coo_rows); auto coo_cols_host = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coo_cols); - ierr = MatSetPreallocationCOO(mat_, nnz, coo_rows_host.data(), + ierr = MatSetPreallocationCOO(mat, nnz, coo_rows_host.data(), coo_cols_host.data()); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - ierr = MatSetValuesCOO(mat_, vals.data(), INSERT_VALUES); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - return; + CHKERRABORT(PETSC_COMM_SELF, ierr); + ierr = MatSetValuesCOO(mat, vals.data(), INSERT_VALUES); + CHKERRABORT(PETSC_COMM_SELF, ierr); + return mat; } - // P1 target: consistent mass matrix assembled from MeshField per-element 3x3 - // blocks. (Higher MeshField orders extend this branch via - // getTriangleElement.) - MeshField::OmegahMeshField omf(mesh); auto coordField = omf.getCoordField(); - const auto [shp, map] = MeshField::Omegah::getTriangleElement<1>(mesh); - MeshField::FieldElement coordFe(mesh.nelems(), coordField, shp, map); - auto elm_mass_dev = buildElementMassMatrix(mesh, coordFe); + Kokkos::View elm_mass_dev; + if constexpr (Dim == 3) { + const auto [shp, map] = MeshField::Omegah::getTetrahedronElement<1>(mesh); + MeshField::FieldElement coordFe(mesh.nelems(), coordField.field, shp, map); + elm_mass_dev = buildElementMassMatrix(mesh, coordFe); + } else { + const auto [shp, map] = MeshField::Omegah::getTriangleElement<1>(mesh); + MeshField::FieldElement coordFe(mesh.nelems(), coordField.field, shp, map); + elm_mass_dev = buildElementMassMatrix(mesh, coordFe); + } - // Build COO sparsity pattern on device: each element contributes a 3x3 block. - const auto& faces2nodes = mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + // Build COO sparsity pattern on device: each element contributes a + // (Dim+1)x(Dim+1) block. + const auto& elems2nodes = mesh.ask_down(Dim, Omega_h::VERT).ab2b; - const PetscInt nnz = static_cast(nelems) * 9; + constexpr int nv = Dim + 1; + const PetscInt nnz = static_cast(nelems) * (nv * nv); Kokkos::View coo_rows("mass_coo_rows", nnz); Kokkos::View coo_cols("mass_coo_cols", nnz); - FillP1MassCooPattern(nelems, faces2nodes, global_to_local, coo_rows, - coo_cols); + FillP1MassCooPattern(nelems, elems2nodes, global_to_local, + mass_type == MassMatrixType::Lumped, coo_rows, + coo_cols); // Create sparse matrix, preallocate with COO pattern, then bulk-set values. // elm_mass_dev is in the same element-major order as coo_rows/coo_cols, so // it can be passed directly to MatSetValuesCOO — no host copy needed. PetscErrorCode ierr = - createSeqAIJMat(PETSC_COMM_WORLD, num_dofs, num_dofs, 0, nullptr, &mat_); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + createSeqAIJMat(PETSC_COMM_SELF, num_dofs, num_dofs, 0, nullptr, &mat); + CHKERRABORT(PETSC_COMM_SELF, ierr); auto coo_rows_host = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coo_rows); auto coo_cols_host = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coo_cols); - ierr = MatSetPreallocationCOO(mat_, nnz, coo_rows_host.data(), + ierr = MatSetPreallocationCOO(mat, nnz, coo_rows_host.data(), coo_cols_host.data()); - CHKERRABORT(PETSC_COMM_WORLD, ierr); - ierr = MatSetValuesCOO(mat_, elm_mass_dev.data(), INSERT_VALUES); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); + ierr = MatSetValuesCOO(mat, elm_mass_dev.data(), INSERT_VALUES); + CHKERRABORT(PETSC_COMM_SELF, ierr); + return mat; +} + +} // namespace + +OmegaHMassIntegrator::OmegaHMassIntegrator(const FunctionSpace& target_space, + MassMatrixType mass_type) + : OmegaHMassIntegrator(std::dynamic_pointer_cast( + target_space.GetLayout()), + target_space.GetCoordinateSystem(), mass_type) +{ +} + +OmegaHMassIntegrator::OmegaHMassIntegrator( + std::shared_ptr target_layout, + std::shared_ptr coordinate_system, + MassMatrixType mass_type) +{ + detail::CheckOmegaHScalarLagrangeLayout(coordinate_system, target_layout, + "OmegaHMassIntegrator", "target"); + + Omega_h::Mesh& mesh = target_layout->GetMesh(); + diagonal_ = + target_layout->GetOrder() == 0 || mass_type == MassMatrixType::Lumped; + if (mesh.dim() == 3) { + mat_ = BuildOmegaHMassMatrixImpl<3>(mesh, *target_layout, mass_type); + } else { + mat_ = BuildOmegaHMassMatrixImpl<2>(mesh, *target_layout, mass_type); + } } OmegaHMassIntegrator::~OmegaHMassIntegrator() @@ -164,10 +214,15 @@ Mat OmegaHMassIntegrator::GetMatrix() const noexcept return mat_; } +bool OmegaHMassIntegrator::IsDiagonal() const noexcept +{ + return diagonal_; +} + std::unique_ptr BuildOmegaHMassIntegrator( - const FunctionSpace& target_space) + const FunctionSpace& target_space, MassMatrixType mass_type) { - return std::make_unique(target_space); + return std::make_unique(target_space, mass_type); } } // namespace pcms diff --git a/src/pcms/transfer/omega_h_mass_integrator.hpp b/src/pcms/transfer/omega_h_mass_integrator.hpp index 54cc7a663..8da8c47e9 100644 --- a/src/pcms/transfer/omega_h_mass_integrator.hpp +++ b/src/pcms/transfer/omega_h_mass_integrator.hpp @@ -4,6 +4,7 @@ #include "pcms/field/function_space.h" #include "pcms/field/layout/omega_h_lagrange.h" #include "pcms/transfer/bilinear_form_integrator.hpp" +#include "pcms/transfer/mass_matrix_type.hpp" #include #include @@ -12,7 +13,9 @@ namespace pcms // Mass-matrix bilinear form integrator for Lagrange spaces on Omega_h 2D // simplex meshes. Supports order-0 (diagonal, M_ee = area(e)) and order-1 -// (consistent 3x3 element blocks) target spaces. +// (consistent 3x3 element blocks) target spaces. With +// MassMatrixType::Lumped the order-1 matrix is row-sum lumped into a +// diagonal matrix (order-0 is already diagonal, so lumping is a no-op). // // The matrix is assembled once at construction into a PETSc sparse AIJ matrix. // Sparsity is derived directly from the element node GIDs — no separate COO @@ -22,23 +25,29 @@ namespace pcms class OmegaHMassIntegrator : public BilinearFormIntegrator { public: - explicit OmegaHMassIntegrator(const FunctionSpace& target_space); + explicit OmegaHMassIntegrator( + const FunctionSpace& target_space, + MassMatrixType mass_type = MassMatrixType::Consistent); OmegaHMassIntegrator( std::shared_ptr target_layout, - CoordinateSystem coordinate_system); + std::shared_ptr coordinate_system, + MassMatrixType mass_type = MassMatrixType::Consistent); ~OmegaHMassIntegrator(); Mat GetMatrix() const noexcept override; + bool IsDiagonal() const noexcept override; private: Mat mat_ = nullptr; + bool diagonal_ = false; }; // Builds an OmegaHMassIntegrator for the given target space. // target_space must use OmegaHLagrangeLayout, scalar, Cartesian, order-0 or // order-1. std::unique_ptr BuildOmegaHMassIntegrator( - const FunctionSpace& target_space); + const FunctionSpace& target_space, + MassMatrixType mass_type = MassMatrixType::Consistent); } // namespace pcms diff --git a/src/pcms/transfer/omega_h_mc_rhs_integrator.cpp b/src/pcms/transfer/omega_h_mc_rhs_integrator.cpp index c57ce9f1f..497efe9ea 100644 --- a/src/pcms/transfer/omega_h_mc_rhs_integrator.cpp +++ b/src/pcms/transfer/omega_h_mc_rhs_integrator.cpp @@ -5,6 +5,7 @@ #include #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" namespace pcms { @@ -12,9 +13,9 @@ namespace pcms namespace { -// Maps a uniform point on the unit square to uniform barycentric coordinates -// Shape Distributions (ACM Transactions on Graphics, Vol. 21, No. 4, October -// 2002.) page 814 Eq 1 +// Maps a uniform point on the unit square to uniform barycentric coordinates on +// a triangle. Shape Distributions (ACM Transactions on Graphics, Vol. 21, +// No. 4, October 2002.) page 814 Eq 1. KOKKOS_INLINE_FUNCTION Omega_h::Vector<3> UniformTriangleBarycentric(Real u, Real v) { @@ -22,51 +23,96 @@ KOKKOS_INLINE_FUNCTION Omega_h::Vector<3> UniformTriangleBarycentric(Real u, return {1.0 - s, s * (1.0 - v), s * v}; } +// Maps three uniform draws on the unit cube to uniform barycentric coordinates +// on a tetrahedron via the cut-and-fold method (Rocchini & Cignoni, +// "Generating Random Points in a Tetrahedron", J. Graphics Tools 2000). +KOKKOS_INLINE_FUNCTION Omega_h::Vector<4> UniformTetBarycentric(Real s, Real t, + Real u) +{ + if (s + t > 1.0) { // fold the cube into a prism + s = 1.0 - s; + t = 1.0 - t; + } + if (t + u > 1.0) { // fold the prism into a tetrahedron + const Real tmp = u; + u = 1.0 - s - t; + t = 1.0 - tmp; + } else if (s + t + u > 1.0) { + const Real tmp = u; + u = s + t + u - 1.0; + s = 1.0 - t - tmp; + } + const Real a = 1.0 - s - t - u; + return {a, s, t, u}; +} + +template +KOKKOS_INLINE_FUNCTION Omega_h::Vector UniformSimplexBarycentric( + const Real r[Dim]) +{ + if constexpr (Dim == 3) { + return UniformTetBarycentric(r[0], r[1], r[2]); + } else { + return UniformTriangleBarycentric(r[0], r[1]); + } +} + // Fills coords, node_gids, and coeffs for all samples of one target element. -// unit_sample_at(s, u, v) provides the s-th unit-square sample. -template +// unit_sample_at(s, r) fills the s-th sample's Dim unit-hypercube draws. +template KOKKOS_INLINE_FUNCTION void FillElementSamples( const int elm, const int samples_per_element, - const Omega_h::Reals& mesh_coords, const Omega_h::LOs& faces2nodes, + const Omega_h::Reals& mesh_coords, const Omega_h::LOs& elems2nodes, const Kokkos::View& global_to_local, const Kokkos::View& coords, const Kokkos::View& node_gids, const Kokkos::View& coeffs, const UnitSampleAt& unit_sample_at) { - const auto verts = Omega_h::gather_verts<3>(faces2nodes, elm); - const auto vert_coords = Omega_h::gather_vectors<3, 2>(mesh_coords, verts); - Omega_h::Few, 2> basis; - basis[0] = vert_coords[1] - vert_coords[0]; - basis[1] = vert_coords[2] - vert_coords[0]; - const Real area = Kokkos::fabs(Omega_h::triangle_area_from_basis(basis)); - const Real weight = area / samples_per_element; + constexpr int nv = Dim + 1; + const auto verts = Omega_h::gather_verts(elems2nodes, elm); + const auto vert_coords = Omega_h::gather_vectors(mesh_coords, verts); + Omega_h::Few, Dim> basis; + for (int d = 0; d < Dim; ++d) { + basis[d] = vert_coords[d + 1] - vert_coords[0]; + } + Real measure; + if constexpr (Dim == 3) { + measure = Kokkos::fabs(Omega_h::tet_volume_from_basis(basis)); + } else { + measure = Kokkos::fabs(Omega_h::triangle_area_from_basis(basis)); + } + const Real weight = measure / samples_per_element; for (int s = 0; s < samples_per_element; ++s) { - Real u = 0.0; - Real v = 0.0; - unit_sample_at(s, u, v); - const auto bary = UniformTriangleBarycentric(u, v); + Real r[Dim]; + unit_sample_at(s, r); + const auto bary = UniformSimplexBarycentric(r); const int i = elm * samples_per_element + s; - Real x = 0.0; - Real y = 0.0; - for (int k = 0; k < 3; ++k) { - x += bary[k] * vert_coords[k][0]; - y += bary[k] * vert_coords[k][1]; - node_gids(i * 3 + k) = static_cast(global_to_local(verts[k])); - coeffs(i * 3 + k) = bary[k] * weight; + Omega_h::Vector x; + for (int d = 0; d < Dim; ++d) { + x[d] = 0.0; + } + for (int k = 0; k < nv; ++k) { + for (int d = 0; d < Dim; ++d) { + x[d] += bary[k] * vert_coords[k][d]; + } + node_gids(i * nv + k) = static_cast(global_to_local(verts[k])); + coeffs(i * nv + k) = bary[k] * weight; + } + for (int d = 0; d < Dim; ++d) { + coords(i, d) = x[d]; } - coords(i, 0) = x; - coords(i, 1) = y; } } -// Samples samples_per_element from a uniform random distribution over the +// Samples samples_per_element from a uniform random distribution over each // target element, writing their coordinates, node GIDs, and coefficients. +template void FillElementSamples( int nelems, int samples_per_element, const Omega_h::Reals& mesh_coords, - const Omega_h::LOs& faces2nodes, + const Omega_h::LOs& elems2nodes, const Kokkos::View& global_to_local, const Kokkos::View& coords, const Kokkos::View& node_gids, @@ -77,12 +123,13 @@ void FillElementSamples( "mc_rhs_fill_random", Kokkos::RangePolicy(0, nelems), KOKKOS_LAMBDA(int elm) { auto gen = pool.get_state(); - FillElementSamples(elm, samples_per_element, mesh_coords, faces2nodes, - global_to_local, coords, node_gids, coeffs, - [&](int /*s*/, Real& u, Real& v) { - u = gen.drand(); - v = gen.drand(); - }); + FillElementSamples(elm, samples_per_element, mesh_coords, + elems2nodes, global_to_local, coords, node_gids, + coeffs, [&](int /*s*/, Real r[Dim]) { + for (int d = 0; d < Dim; ++d) { + r[d] = gen.drand(); + } + }); pool.free_state(gen); }); Kokkos::fence(); @@ -102,8 +149,8 @@ OmegaHMonteCarloRHSIntegrator::OmegaHMonteCarloRHSIntegrator( OmegaHMonteCarloRHSIntegrator::OmegaHMonteCarloRHSIntegrator( std::shared_ptr target_layout, - CoordinateSystem target_coordinate_system, int samples_per_element, - MonteCarloSampling /*sampling*/, uint64_t seed) + std::shared_ptr target_coordinate_system, + int samples_per_element, MonteCarloSampling /*sampling*/, uint64_t seed) { detail::CheckOmegaHScalarP1Layout(target_coordinate_system, target_layout, "OmegaHMonteCarloRHSIntegrator", "target"); @@ -113,22 +160,29 @@ OmegaHMonteCarloRHSIntegrator::OmegaHMonteCarloRHSIntegrator( } Omega_h::Mesh& mesh = target_layout->GetMesh(); + const int dim = mesh.dim(); + nbary_ = dim + 1; const int nelems = mesh.nelems(); const int num_samples = nelems * samples_per_element; const auto mesh_coords = mesh.coords(); - const auto faces2nodes = mesh.ask_down(Omega_h::FACE, Omega_h::VERT).ab2b; + const auto elems2nodes = mesh.ask_down(dim, Omega_h::VERT).ab2b; const auto global_to_local = target_layout->GetGlobalToLocalPermutation(); Kokkos::View coords("mc_rhs_coords", num_samples, - 2); + dim); Kokkos::View node_gids( - "mc_rhs_node_gids", static_cast(num_samples) * 3); + "mc_rhs_node_gids", static_cast(num_samples) * nbary_); Kokkos::View coeffs( - "mc_rhs_coeffs", static_cast(num_samples) * 3); + "mc_rhs_coeffs", static_cast(num_samples) * nbary_); - FillElementSamples(nelems, samples_per_element, mesh_coords, faces2nodes, - global_to_local, coords, node_gids, coeffs, seed); + if (dim == 3) { + FillElementSamples<3>(nelems, samples_per_element, mesh_coords, elems2nodes, + global_to_local, coords, node_gids, coeffs, seed); + } else { + FillElementSamples<2>(nelems, samples_per_element, mesh_coords, elems2nodes, + global_to_local, coords, node_gids, coeffs, seed); + } coords_ = std::move(coords); node_gids_ = std::move(node_gids); @@ -136,15 +190,15 @@ OmegaHMonteCarloRHSIntegrator::OmegaHMonteCarloRHSIntegrator( const PetscInt nnz = static_cast(node_gids_.extent(0)); PetscErrorCode ierr = - createSeqVec(PETSC_COMM_WORLD, static_cast(mesh.nverts()), &vec_); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + createSeqVec(PETSC_COMM_SELF, static_cast(mesh.nverts()), &vec_); + CHKERRABORT(PETSC_COMM_SELF, ierr); // VecSetPreallocationCOO takes the COO indices on the host // TODO: ask Todd/PETSc folks if there is a better way to do this for GPU // support auto node_gids_host = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, node_gids_); ierr = VecSetPreallocationCOO(vec_, nnz, node_gids_host.data()); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); } OmegaHMonteCarloRHSIntegrator::~OmegaHMonteCarloRHSIntegrator() @@ -161,8 +215,9 @@ OmegaHMonteCarloRHSIntegrator::~OmegaHMonteCarloRHSIntegrator() CoordinateView OmegaHMonteCarloRHSIntegrator::GetIntegrationPoints() const noexcept { - return CoordinateView(CoordinateSystem::Cartesian, - MakeConstRank2View(coords_)); + return CoordinateView( + csys::Cartesian::Create(static_cast(coords_.extent(1))), + MakeConstRank2View(coords_)); } Vec OmegaHMonteCarloRHSIntegrator::GetVector() const noexcept @@ -173,32 +228,31 @@ Vec OmegaHMonteCarloRHSIntegrator::GetVector() const noexcept void OmegaHMonteCarloRHSIntegrator::Assemble( Rank2View sampled_values) { + const int nbary = nbary_; const std::size_t num_samples = - static_cast(node_gids_.extent(0) / 3); + static_cast(node_gids_.extent(0) / nbary); PCMS_ALWAYS_ASSERT(static_cast(sampled_values.extent(0)) == num_samples); PCMS_ALWAYS_ASSERT(sampled_values.extent(1) >= 1); PetscErrorCode ierr = VecZeroEntries(vec_); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); - auto sv = Kokkos::View>( - sampled_values.data_handle(), sampled_values.extent(0), - sampled_values.extent(1)); + auto sv = sampled_values; Kokkos::View coo_vals("mc_rhs_coo_vals", - num_samples * 3); + num_samples * nbary); auto coeffs = coeffs_; Kokkos::parallel_for( "mc_rhs_coo_vals", static_cast(num_samples), KOKKOS_LAMBDA(int i) { const PetscScalar f = static_cast(sv(i, 0)); - for (int j = 0; j < 3; ++j) { - coo_vals(i * 3 + j) = static_cast(coeffs(i * 3 + j)) * f; + for (int j = 0; j < nbary; ++j) { + coo_vals(i * nbary + j) = + static_cast(coeffs(i * nbary + j)) * f; } }); ierr = VecSetValuesCOO(vec_, coo_vals.data(), ADD_VALUES); - CHKERRABORT(PETSC_COMM_WORLD, ierr); + CHKERRABORT(PETSC_COMM_SELF, ierr); } } // namespace pcms diff --git a/src/pcms/transfer/omega_h_mc_rhs_integrator.hpp b/src/pcms/transfer/omega_h_mc_rhs_integrator.hpp index c9b533b4f..f652e380a 100644 --- a/src/pcms/transfer/omega_h_mc_rhs_integrator.hpp +++ b/src/pcms/transfer/omega_h_mc_rhs_integrator.hpp @@ -1,7 +1,7 @@ #ifndef PCMS_TRANSFER_OMEGA_H_MC_RHS_INTEGRATOR_HPP #define PCMS_TRANSFER_OMEGA_H_MC_RHS_INTEGRATOR_HPP -#include "pcms/field/coordinate_system.h" +#include "pcms/field/coordinate_view.hpp" #include "pcms/field/function_space.h" #include "pcms/field/layout/omega_h_lagrange.h" #include "pcms/transfer/linear_form_integrator.hpp" @@ -14,7 +14,8 @@ namespace pcms { // Monte Carlo RHS integrator for conservative L2 projection onto order-1 -// Lagrange spaces on Omega_h 2D simplex meshes. +// Lagrange spaces on Omega_h simplex meshes (triangles in 2D, tetrahedra in +// 3D). // // Instead of intersection-based quadrature, each load-vector entry // b_j = \int phi_j f dx @@ -35,8 +36,9 @@ class OmegaHMonteCarloRHSIntegrator : public LinearFormIntegrator uint64_t seed = 8675309); OmegaHMonteCarloRHSIntegrator( std::shared_ptr target_layout, - CoordinateSystem target_coordinate_system, int samples_per_element, - MonteCarloSampling sampling, uint64_t seed = 8675309); + std::shared_ptr target_coordinate_system, + int samples_per_element, MonteCarloSampling sampling, + uint64_t seed = 8675309); ~OmegaHMonteCarloRHSIntegrator(); OmegaHMonteCarloRHSIntegrator(const OmegaHMonteCarloRHSIntegrator&) = delete; @@ -57,6 +59,7 @@ class OmegaHMonteCarloRHSIntegrator : public LinearFormIntegrator coords_; // [num_pts][dim] sample coordinates Kokkos::View node_gids_; // COO indices Kokkos::View coeffs_; // basis * |T| / N + int nbary_ = 3; // barycentric DOFs per sample (Dim+1): 3 in 2D, 4 in 3D }; } // namespace pcms diff --git a/src/pcms/transfer/transfer_operator.hpp b/src/pcms/transfer/transfer_operator.hpp index adf36c1df..be78a9cb1 100644 --- a/src/pcms/transfer/transfer_operator.hpp +++ b/src/pcms/transfer/transfer_operator.hpp @@ -11,7 +11,10 @@ template class TransferOperator { public: + using value_type = T; + virtual void Apply(const Field& source, Field& target) const = 0; + virtual ~TransferOperator() noexcept = default; }; diff --git a/src/pcms/transient/CMakeLists.txt b/src/pcms/transient/CMakeLists.txt new file mode 100644 index 000000000..c64c9e36d --- /dev/null +++ b/src/pcms/transient/CMakeLists.txt @@ -0,0 +1,33 @@ +set(PCMS_TRANSIENT_HEADERS timestepper.hpp) +set(PCMS_TRANSIENT_SOURCES timestepper.cpp) + +add_library(pcms_transient ${PCMS_TRANSIENT_SOURCES}) +set_target_properties(pcms_transient PROPERTIES EXPORT_NAME transient) +add_library(pcms::transient ALIAS pcms_transient) + +target_sources( + pcms_transient + PUBLIC FILE_SET transient + TYPE HEADERS + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/.. + FILES ${PCMS_TRANSIENT_HEADERS}) +target_compile_features(pcms_transient PUBLIC cxx_std_20) +target_link_libraries(pcms_transient PUBLIC pcms::utility) +target_include_directories( + pcms_transient + PUBLIC $ + $) + +install( + TARGETS pcms_transient + EXPORT pcms_transient-targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms/transient + FILE_SET transient DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/pcms) + +install( + EXPORT pcms_transient-targets + NAMESPACE pcms:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pcms) diff --git a/src/pcms/transient/timestepper.cpp b/src/pcms/transient/timestepper.cpp new file mode 100644 index 000000000..3ef63d5b7 --- /dev/null +++ b/src/pcms/transient/timestepper.cpp @@ -0,0 +1,18 @@ +#include "pcms/transient/timestepper.hpp" + +namespace pcms +{ + +FixedTimestepper::FixedTimestepper(Real dt) : dt_(dt) {} + +Real FixedTimestepper::InitialStep() const +{ + return dt_; +} + +Step FixedTimestepper::UpdateStep(Real, Real) +{ + return {true, dt_}; +} + +} // namespace pcms diff --git a/src/pcms/transient/timestepper.hpp b/src/pcms/transient/timestepper.hpp new file mode 100644 index 000000000..00337c36b --- /dev/null +++ b/src/pcms/transient/timestepper.hpp @@ -0,0 +1,54 @@ +#ifndef PCMS_TRANSIENT_TIMESTEPPER_HPP +#define PCMS_TRANSIENT_TIMESTEPPER_HPP + +#include "pcms/utility/types.h" + +namespace pcms +{ + +/// Result of Timestepper::UpdateStep. +struct Step +{ + /// Whether the completed time window is accepted. + bool accepted; + /// Positive step size to try next. + Real next_step; +}; + +/// Decides whether to accept a completed time window and selects the next step. +class Timestepper +{ +public: + /// \return The positive step size used for the first coupling window. + [[nodiscard]] virtual Real InitialStep() const = 0; + + /// Decide whether to accept a completed time window and pick the next step. + /// \param dt The time step that was actually completed. + /// \param err Normalized error of the completed step. + /// \return Whether the step is accepted and the positive step size to try + /// next. + [[nodiscard]] virtual Step UpdateStep(Real dt, Real err) = 0; + + virtual ~Timestepper() = default; +}; + +/// Always accepts and keeps a constant time step. +class FixedTimestepper : public Timestepper +{ +public: + /// \param dt Positive step size used for every window. + explicit FixedTimestepper(Real dt); + + /// \return The configured fixed step. + Real InitialStep() const override; + + /// Ignores \p dt and \p err; always accepts and returns the fixed step. + Step UpdateStep(Real dt, Real err) override; + +private: + Real dt_; +}; + +} // namespace pcms + +#endif // PCMS_TRANSIENT_TIMESTEPPER_HPP diff --git a/src/pcms/utility/arrays.h b/src/pcms/utility/arrays.h index 71aa6d19c..ca7f64904 100644 --- a/src/pcms/utility/arrays.h +++ b/src/pcms/utility/arrays.h @@ -70,6 +70,12 @@ struct arr_trait> using type = T; }; +template +struct arr_trait> +{ + using type = T; +}; + template using element_type_t = typename arr_trait::value>::type; @@ -432,6 +438,29 @@ void CopyHostRank2ViewToDeviceView(Kokkos::View dest, Kokkos::deep_copy(dest, src_mirror); } +// Element-wise copy between device Rank2Views. Indexing through (i, c) makes +// the copy correct whatever layouts the two views carry, unlike a flat copy +// through data_handle(), which assumes both element sequences agree. +template +void CopyDeviceRank2ViewToRank2View(Rank2View dest, + Rank2View src) +{ + const size_t num_dof = src.extent(0); + const size_t num_comp = src.extent(1); + if (dest.extent(0) != num_dof || dest.extent(1) != num_comp) { + throw pcms_error("CopyDeviceRank2ViewToRank2View: extent mismatch"); + } + Kokkos::parallel_for( + "CopyDeviceRank2ViewToRank2View", + Kokkos::RangePolicy( + 0, static_cast(num_dof)), + KOKKOS_LAMBDA(LO i) { + for (size_t c = 0; c < num_comp; ++c) { + dest(i, c) = src(i, c); + } + }); +} + // utility function to copy from Rank1View to Rank1View template void CopyRank1ViewToHost(Rank1View dest, diff --git a/src/pcms/utility/mesh_geometry.h b/src/pcms/utility/mesh_geometry.h index ef40719a7..4c74595f4 100644 --- a/src/pcms/utility/mesh_geometry.h +++ b/src/pcms/utility/mesh_geometry.h @@ -2,6 +2,7 @@ #define PCMS_UTILITY_MESH_GEOMETRY_H #include +#include #include #include @@ -93,6 +94,35 @@ inline Omega_h::Reals get_entity_centroids(Omega_h::Mesh& mesh, return Omega_h::Reals(centroids); } +// Some meshes (e.g. certain XGC meshes) are stored without any geometric +// classification (no class_id/class_dim tags). Layouts read these tags on +// every mesh dimension, so derive a default classification when it is +// missing. classify_elements + finalize_classification populate a +// geometrically meaningful class_dim on all dimensions but do not create +// class_id (that normally comes from a geometric model we don't have here), +// so fill any missing class_id with 0. +// Classification is only consulted for coupling DOF matching +// (field_exchange_planner); standalone field transfer never reads it, so a +// default id is sufficient to keep such meshes usable. +inline void EnsureClassification(Omega_h::Mesh& mesh) +{ + if (mesh.has_tag(mesh.dim(), "class_id") && + mesh.has_tag(mesh.dim(), "class_dim")) { + return; + } + if (!mesh.has_tag(mesh.dim(), "class_dim")) { + Omega_h::classify_elements(&mesh); + Omega_h::finalize_classification(&mesh); + } + for (int d = 0; d <= mesh.dim(); ++d) { + if (!mesh.has_tag(d, "class_id")) { + mesh.add_tag( + d, "class_id", 1, + Omega_h::Read(mesh.nents(d), 0, "class_id")); + } + } +} + } // namespace pcms #endif // PCMS_UTILITY_MESH_GEOMETRY_H diff --git a/src/pcms/utility/uniform_grid.h b/src/pcms/utility/uniform_grid.h index 1a478fec3..e6730fcec 100644 --- a/src/pcms/utility/uniform_grid.h +++ b/src/pcms/utility/uniform_grid.h @@ -4,7 +4,10 @@ #include "Omega_h_vector.hpp" #include "Omega_h_bbox.hpp" #include "Omega_h_mesh.hpp" -#include +#include +#include +#include +#include namespace pcms { @@ -12,15 +15,17 @@ template struct UniformGrid { // Make private? - std::array edge_length; - std::array bot_left; - std::array divisions; + Kokkos::Array edge_length; + Kokkos::Array bot_left; + Kokkos::Array divisions; public: [[nodiscard]] LO GetNumCells() const { - return std::accumulate(divisions.begin(), divisions.end(), 1, - std::multiplies{}); + LO total = 1; + for (std::size_t i = 0; i < dim; ++i) + total *= divisions[i]; + return total; } /// return the grid cell ID that the input point is inside or closest to if /// the point lies outside @@ -30,23 +35,30 @@ struct UniformGrid [[nodiscard]] KOKKOS_INLINE_FUNCTION LO ClosestCellID(const Omega_h::Vector& point) const { - std::array distance_within_grid; + Kokkos::Array indexes; for (size_t i = 0; i < dim; ++i) { - distance_within_grid[i] = point[i] - bot_left[i]; + indexes[i] = AxisCellIndex(i, point[i]); } + return CellIndexFromAxisIndices(indexes); + } - std::array indexes; - - for (auto& index : indexes) { - index = -1; - } + /// Index of the cell containing `coord` along `axis`, clamped to the grid. + /// The single definition of the grid's binning rule: keeping the point query + /// and the cell-range query on this one formula is what guarantees a point + /// always lands in a cell that its own element was binned into. + [[nodiscard]] KOKKOS_INLINE_FUNCTION LO AxisCellIndex(size_t axis, + Real coord) const + { + const auto index = static_cast(std::floor( + (coord - bot_left[axis]) * divisions[axis] / edge_length[axis])); + return Kokkos::clamp(index, 0, divisions[axis] - 1); + } - for (int i = 0; i < dim; ++i) { - auto index = static_cast( - std::floor(distance_within_grid[i] * divisions[i] / edge_length[i])); - indexes[i] = Kokkos::clamp(index, 0, divisions[i] - 1); - } + /// Linear cell ID from per-axis indices given in coordinate order (x,y,z). + [[nodiscard]] KOKKOS_INLINE_FUNCTION LO + CellIndexFromAxisIndices(Kokkos::Array indexes) const + { // note that the indexes refer to row/columns which have the opposite order // of the coordinates i.e. x,y reverse(indexes); @@ -86,14 +98,14 @@ struct UniformGrid return true; } - [[nodiscard]] KOKKOS_INLINE_FUNCTION std::array GetDimensionedIndex( - LO idx) const + [[nodiscard]] KOKKOS_INLINE_FUNCTION Kokkos::Array + GetDimensionedIndex(LO idx) const { LO stride = 1; - for (std::size_t i = 0; i < divisions.size() - 1; ++i) { + for (std::size_t i = 0; i < dim - 1; ++i) { stride *= divisions[i]; } - std::array result; + Kokkos::Array result; for (size_t i = 0; i < dim; ++i) { result[i] = idx / stride; @@ -105,7 +117,7 @@ struct UniformGrid } [[nodiscard]] KOKKOS_INLINE_FUNCTION LO - GetCellIndex(std::array dimensionedIndex) const + GetCellIndex(Kokkos::Array dimensionedIndex) const { // note that the indexes refer to row/columns which have the opposite order // of the coordinates i.e. x,y @@ -124,9 +136,9 @@ struct UniformGrid private: template - KOKKOS_INLINE_FUNCTION static void reverse(std::array& arr) + KOKKOS_INLINE_FUNCTION static void reverse(Kokkos::Array& arr) { - for (size_t i = 0, j = arr.size() - 1; i < j; ++i, --j) { + for (size_t i = 0, j = N - 1; i < j; ++i, --j) { auto temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; @@ -137,6 +149,18 @@ struct UniformGrid using Uniform2DGrid = UniformGrid<2>; using Uniform3DGrid = UniformGrid<3>; +/** + * \brief Divisions per axis giving roughly one element per grid cell. + * + */ +template +LO DivisionsPerAxisForMesh(LO nelems, LO min_divisions = 10) +{ + const auto per_axis = static_cast(std::ceil( + std::pow(static_cast(nelems), 1.0 / static_cast(dim)))); + return std::max(per_axis, min_divisions); +} + /** * \brief Create a uniform grid layout from an Omega_h mesh as a bounding box * @@ -158,16 +182,18 @@ UniformGrid CreateUniformGridFromMesh(Omega_h::Mesh& mesh, auto bbox = Omega_h::get_bounding_box(&mesh); // Calculate edge lengths and bottom-left corner - std::array edge_length; - std::array bot_left; + Kokkos::Array edge_length; + Kokkos::Array bot_left; + Kokkos::Array divs; for (unsigned i = 0; i < dim; ++i) { bot_left[i] = bbox.min[i]; edge_length[i] = bbox.max[i] - bbox.min[i]; + divs[i] = divisions[i]; } return UniformGrid{ - .edge_length = edge_length, .bot_left = bot_left, .divisions = divisions}; + .edge_length = edge_length, .bot_left = bot_left, .divisions = divs}; } /** @@ -187,7 +213,8 @@ UniformGrid CreateUniformGridFromMesh(Omega_h::Mesh& mesh, LO cells_per_dim) { std::array divisions; - divisions.fill(cells_per_dim); + for (unsigned i = 0; i < dim; ++i) + divisions[i] = cells_per_dim; return CreateUniformGridFromMesh(mesh, divisions); } diff --git a/src/pcms/version.h.in b/src/pcms/version.h.in index f76334b57..c4442d538 100644 --- a/src/pcms/version.h.in +++ b/src/pcms/version.h.in @@ -1,11 +1,13 @@ #pragma once -#define PCMS_VERSION_MAJOR @CMAKE_PROJECT_VERSION_MAJOR@ -#define PCMS_VERSION_MINOR @CMAKE_PROJECT_VERSION_MINOR@ -#define PCMS_VERSION_PATCH @CMAKE_PROJECT_VERSION_PATCH@ +#define PCMS_VERSION_MAJOR @CMAKE_PROJECT_VERSION_MAJOR @ +#define PCMS_VERSION_MINOR @CMAKE_PROJECT_VERSION_MINOR @ +#define PCMS_VERSION_PATCH @CMAKE_PROJECT_VERSION_PATCH @ -namespace pcms { - const char* pcms_version() { - return "@CMAKE_PROJECT_VERSION@"; - } -} \ No newline at end of file +namespace pcms +{ +const char* pcms_version() +{ + return "@CMAKE_PROJECT_VERSION@"; +} +} // namespace pcms \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5efdc81e8..559f9c86e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -18,7 +18,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 rdv EXE1 - ./test_ohClassPtn + test_ohClassPtn PROCS1 2 ARGS1 @@ -27,7 +27,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 app EXE2 - ./test_ohClassPtn + test_ohClassPtn PROCS2 1 ARGS2 @@ -45,7 +45,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 rdv EXE1 - ./test_ohClassPtn_appRibPtn + test_ohClassPtn_appRibPtn PROCS1 2 ARGS1 @@ -54,7 +54,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 app EXE2 - ./test_ohClassPtn_appRibPtn + test_ohClassPtn_appRibPtn PROCS2 2 ARGS2 @@ -78,7 +78,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 rdv EXE1 - ./test_ohOverlap + test_ohOverlap PROCS1 4 ARGS1 @@ -88,7 +88,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 app EXE2 - ./test_ohOverlap + test_ohOverlap PROCS2 16 ARGS2 @@ -96,6 +96,37 @@ if(PCMS_ENABLE_OMEGA_H) ${d3d16p} ignored) endif() + add_exe(test_GDI) + tri_mpi_test( + TESTNAME + test_GDI + TIMEOUT + 20 + NAME1 + app + EXE1 + test_GDI + PROCS1 + 1 + ARGS1 + 1 + NAME2 + rdv + EXE2 + test_GDI + PROCS2 + 1 + ARGS2 + -1 + NAME3 + app + EXE3 + test_GDI + PROCS3 + 1 + ARGS3 + 0 + ) set(d3d8p ${PCMS_TEST_DATA_DIR}/d3d/d3d-full_9k_sfc_p8.osh/) add_exe(test_twoClientOverlap) @@ -108,7 +139,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 rdv EXE1 - ./test_twoClientOverlap + test_twoClientOverlap PROCS1 4 ARGS1 @@ -118,7 +149,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 client0 EXE2 - ./test_twoClientOverlap + test_twoClientOverlap PROCS2 16 ARGS2 @@ -128,7 +159,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME3 client1 EXE3 - ./test_twoClientOverlap + test_twoClientOverlap PROCS3 8 ARGS3 @@ -147,7 +178,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 server EXE1 - ./field_communication + field_communication PROCS1 2 ARGS1 @@ -157,7 +188,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 client1 EXE2 - ./field_communication + field_communication PROCS2 4 ARGS2 @@ -167,7 +198,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME3 client2 EXE3 - ./field_communication + field_communication PROCS3 4 ARGS3 @@ -186,7 +217,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 rdv EXE1 - ./proxy_coupling + proxy_coupling PROCS1 2 ARGS1 @@ -196,7 +227,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 client0 EXE2 - ./proxy_coupling + proxy_coupling PROCS2 1 ARGS2 @@ -206,7 +237,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME3 client1 EXE3 - ./proxy_coupling + proxy_coupling PROCS3 1 ARGS3 @@ -223,7 +254,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 rdv EXE1 - ./proxy_coupling + proxy_coupling PROCS1 2 ARGS1 @@ -233,7 +264,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 client0 EXE2 - ./proxy_coupling + proxy_coupling PROCS2 16 ARGS2 @@ -243,7 +274,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME3 client1 EXE3 - ./proxy_coupling + proxy_coupling PROCS3 8 ARGS3 @@ -260,7 +291,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 rdv EXE1 - ./proxy_coupling + proxy_coupling PROCS1 4 ARGS1 @@ -270,7 +301,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 client0 EXE2 - ./proxy_coupling + proxy_coupling PROCS2 16 ARGS2 @@ -280,7 +311,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME3 client1 EXE3 - ./proxy_coupling + proxy_coupling PROCS3 8 ARGS3 @@ -297,7 +328,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 rdv EXE1 - ./proxy_coupling + proxy_coupling PROCS1 8 ARGS1 @@ -307,7 +338,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 client0 EXE2 - ./proxy_coupling + proxy_coupling PROCS2 16 ARGS2 @@ -317,7 +348,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME3 client1 EXE3 - ./proxy_coupling + proxy_coupling PROCS3 8 ARGS3 @@ -334,7 +365,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME1 rdv EXE1 - ./proxy_coupling + proxy_coupling PROCS1 16 ARGS1 @@ -344,7 +375,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME2 client0 EXE2 - ./proxy_coupling + proxy_coupling PROCS2 16 ARGS2 @@ -354,7 +385,7 @@ if(PCMS_ENABLE_OMEGA_H) NAME3 client1 EXE3 - ./proxy_coupling + proxy_coupling PROCS3 8 ARGS3 @@ -368,8 +399,9 @@ find_package(Catch2 3) if(Catch2_FOUND) message( STATUS "Found Catch2: ${Catch2_DIR} (found version ${Catch2_VERSION})") - set(PCMS_UNIT_TEST_SOURCES unit_test_main.cpp test_coordinate_transform.cpp - test_coordinate.cpp test_bounding_box.cpp) + set(PCMS_UNIT_TEST_SOURCES unit_test_main.cpp + test_coordinate_transformation.cpp + test_bounding_box.cpp test_field_serializer.cpp) if(PCMS_ENABLE_XGC) list(APPEND PCMS_UNIT_TEST_SOURCES test_xgc_reverse_classification.cpp test_xgc_field_data.cpp) @@ -380,7 +412,7 @@ if(Catch2_FOUND) APPEND PCMS_UNIT_TEST_SOURCES test_error_handling.cpp - test_eqdsk.cpp + test_eqdsk.cpp test_uniform_grid.cpp test_field_evaluation.cpp test_field_interpolation.cpp @@ -400,7 +432,22 @@ if(Catch2_FOUND) test_localization_factory.cpp test_omega_h_field2_outofbounds.cpp test_omega_h_lagrange_field.cpp - test_point_evaluator.cpp) + test_point_evaluator.cpp + test_tagged_outputs.cpp + test_transformed_source.cpp + test_evaluation_only_space.cpp + ) + if (PCMS_ENABLE_ARBORX) + list( + APPEND + PCMS_UNIT_TEST_SOURCES + test_tree_localization.cpp + ) + endif() + endif() + + if(PCMS_ENABLE_MFEM) + list(APPEND PCMS_UNIT_TEST_SOURCES test_mfem_adapter.cpp) endif() if(PCMS_ENABLE_MESHFIELDS) @@ -413,7 +460,12 @@ if(Catch2_FOUND) test_omega_h_intersection_rhs_integrator.cpp test_omega_h_mass_integrator.cpp test_omega_h_mc_rhs_integrator.cpp - test_mesh_intersection_field_transfer.cpp) + test_mesh_intersection_field_transfer.cpp + test_mesh_intersection_prefilter.cpp + test_omega_h_3d_conservative_projection.cpp + test_omega_h_3d_analytic_mass.cpp + test_omega_h_intersection_coverage.cpp + test_omega_h_same_mesh_p0_to_p1.cpp) endif() add_executable(unit_tests ${PCMS_UNIT_TEST_SOURCES}) @@ -422,6 +474,10 @@ if(Catch2_FOUND) 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::transfer Kokkos::kokkoskernels) target_include_directories(unit_tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) @@ -430,15 +486,6 @@ if(Catch2_FOUND) 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 @@ -453,10 +500,44 @@ if(Catch2_FOUND) include(Catch) Catch_discover_tests(unit_tests) + if(PCMS_ENABLE_TRANSIENT) + add_executable(test_transient_timestepper test_transient_timestepper.cpp) + target_link_libraries(test_transient_timestepper + PRIVATE Catch2::Catch2WithMain pcms::transient) + Catch_discover_tests(test_transient_timestepper) + endif() 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) @@ -483,7 +564,7 @@ if(PCMS_ENABLE_C) NAME1 cinterface EXE1 - $ + test_proxy_couple_xgc_c_interface PROCS1 2 ARGS1 @@ -491,7 +572,7 @@ if(PCMS_ENABLE_C) NAME2 cppinterface EXE2 - $ + test_proxy_couple_xgc_cpp_interface PROCS2 1 ARGS2 @@ -507,7 +588,7 @@ if(PCMS_ENABLE_C) NAME1 cinterface EXE1 - $ + test_proxy_couple_xgc_c_interface PROCS1 2 ARGS1 @@ -515,7 +596,7 @@ if(PCMS_ENABLE_C) NAME2 cppinterface_overlap EXE2 - $ + test_proxy_couple_xgc_overlap_interface PROCS2 1 ARGS2 @@ -532,7 +613,7 @@ if(PCMS_ENABLE_C) NAME1 cinterface EXE1 - $ + test_proxy_couple_xgc_c_interface PROCS1 4 ARGS1 @@ -540,7 +621,7 @@ if(PCMS_ENABLE_C) NAME2 cppinterface EXE2 - $ + test_proxy_couple_xgc_cpp_interface PROCS2 2 ARGS2 @@ -556,7 +637,7 @@ if(PCMS_ENABLE_C) NAME1 cinterface EXE1 - $ + test_proxy_couple_xgc_c_interface PROCS1 4 ARGS1 @@ -564,7 +645,7 @@ if(PCMS_ENABLE_C) NAME2 cppinterface_overlap EXE2 - $ + test_proxy_couple_xgc_overlap_interface PROCS2 2 ARGS2 @@ -604,7 +685,7 @@ if(PCMS_ENABLE_Fortran) NAME1 finterface EXE1 - $ + test_proxy_coupling_xgc_client_fortran PROCS1 2 ARGS1 @@ -612,7 +693,7 @@ if(PCMS_ENABLE_Fortran) NAME2 cppinterface EXE2 - $ + test_proxy_couple_xgc_cpp_interface PROCS2 2 ARGS2 diff --git a/test/data/cyclone23/README.md b/test/data/cyclone23/README.md new file mode 100644 index 000000000..44c10f283 --- /dev/null +++ b/test/data/cyclone23/README.md @@ -0,0 +1,10 @@ +# cyclone23 + +`mesh.osh` - 23 triangle Omega_h mesh of the cyclone circular geometric model. +Used by the install `smoke_test` so it runs without a copy of +[pcms_testcases](https://github.com/SCOREC/pcms_testcases). + +Copied from `cyclone/23elements/mesh.osh` in pcms_testcases (20a6906), which +was generated by TOMMS from the `Cyclone_ITG/Cyclone_ITG_deltaf_23mesh` +directory of the SCOREC `xgc1_data` repo +(https://github.com/SCOREC/xgc1_data.git cbdac20aee894c3d01889b5c1a8a26ea56012287). diff --git a/test/data/cyclone23/mesh.osh/0.osh b/test/data/cyclone23/mesh.osh/0.osh new file mode 100644 index 000000000..350dbf18b Binary files /dev/null and b/test/data/cyclone23/mesh.osh/0.osh differ diff --git a/test/data/cyclone23/mesh.osh/nparts b/test/data/cyclone23/mesh.osh/nparts new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/test/data/cyclone23/mesh.osh/nparts @@ -0,0 +1 @@ +1 diff --git a/test/data/cyclone23/mesh.osh/version b/test/data/cyclone23/mesh.osh/version new file mode 100644 index 000000000..ec635144f --- /dev/null +++ b/test/data/cyclone23/mesh.osh/version @@ -0,0 +1 @@ +9 diff --git a/test/field_test_utils.h b/test/field_test_utils.h index 3fbb624f9..fd81add9b 100644 --- a/test/field_test_utils.h +++ b/test/field_test_utils.h @@ -3,20 +3,34 @@ #include #include +#include "pcms/configuration.h" #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/field/coordinate_view.hpp" #include "pcms/utility/arrays.h" #include "pcms/utility/memory_spaces.h" +#ifdef PCMS_ENABLE_OMEGA_H +#include +#include +#include +#include "pcms/field/function_space/lagrange.h" +#include "pcms/localization/adj_search.hpp" +#endif +#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) +#include "pcms/transfer/linear_form_integrator.hpp" +#endif #include +#include +#include +#include #include +#include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" // Shared utilities for field tests that apply equally to MeshFields-backed // and native OmegaH-backed field implementations. @@ -24,18 +38,133 @@ namespace pcms::test { +// tolerances used for testing. +// Currently we just added for the tests we were working on. +// We need to update all tests to use consistent tolerances +// Those tolerances should either come from these constexpr variables +// or toleranes in the config file +inline constexpr double ExactTol = 1e-12; +inline constexpr double EvalTol = 1e-10; + // Affine test function — exactly representable on linear elements. KOKKOS_INLINE_FUNCTION Real linear_f(Real x, Real y) { return x + 2.0 * y; } +KOKKOS_INLINE_FUNCTION Real linear_f_3d(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}; } +// Points strictly outside a unit [0,1]^2 box mesh. +inline std::vector StandardOutsideCoords2D() +{ + return {-0.5, 0.5, 1.5, 0.5, 0.5, -0.5, 0.5, 1.5}; +} + +#ifdef PCMS_ENABLE_OMEGA_H +// Builds a unit-square 2D simplex mesh from the given element connectivity and +// adds the geometric classification tags required to build an Omega_h-backed +// Lagrange function space. +// + +// Geometric classification tags required by OmegaHLagrangeLayout. +inline void AddDefaultClassification(Omega_h::Mesh& mesh) +{ + for (Omega_h::Int dim = 0; dim <= mesh.dim(); ++dim) { + mesh.add_tag( + dim, "class_dim", 1, + Omega_h::Read(mesh.nents(dim), Omega_h::I8(dim))); + mesh.add_tag( + dim, "class_id", 1, + Omega_h::Read(mesh.nents(dim), Omega_h::ClassId(0))); + } +} + +inline Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib, + const Omega_h::LOs& ev2v) +{ + const Omega_h::Reals coords({ + 0.0, 0.0, // v0 + 1.0, 0.0, // v1 + 1.0, 1.0, // v2 + 0.0, 1.0 // v3 + }); + Omega_h::Mesh mesh(&lib); + Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 2, ev2v, coords); + AddDefaultClassification(mesh); + return mesh; +} + +// Convenience overload: diagonal=0 splits the square along vertices (1,3), +// diagonal=1 along (0,2). +inline Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib, int diagonal) +{ + return BuildUnitSquare(lib, (diagonal == 0) + ? Omega_h::LOs({0, 1, 3, 1, 2, 3}) + : Omega_h::LOs({0, 1, 2, 0, 2, 3})); +} + +// Tetrahedral mesh of [0,1]^3. build_box already adds classification. +inline Omega_h::Mesh BuildUnitCube(Omega_h::Library& lib, int n) +{ + return Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1.0, 1.0, 1.0, n, n, + n); +} + +inline Omega_h::Mesh BuildTet(Omega_h::Library& lib, + const Omega_h::Reals& coords) +{ + + Omega_h::Mesh mesh(&lib); + const Omega_h::LOs ev2v = Omega_h::LOs({0, 1, 2, 3}); + Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 3, ev2v, coords); + AddDefaultClassification(mesh); + return mesh; +} + +// Reference tet: (0,0,0), (1,0,0), (0,1,0), (0,0,1). Volume 1/6. +// reversed=true swaps the last two verts so the signed volume is negative. +inline Omega_h::Mesh BuildReferenceTet(Omega_h::Library& lib) +{ + return BuildTet(lib, Omega_h::Reals({ + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + })); +} + +inline std::shared_ptr MakeP1Space( + Omega_h::Mesh& mesh, const std::string& global_id_name = "global") +{ + return LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, csys::Cartesian::Deferred(), global_id_name, + LagrangeFunctionSpace::Backend::OmegaH); +} + +inline std::shared_ptr MakeP0Space(Omega_h::Mesh& mesh) +{ + return LagrangeFunctionSpace::FromMesh( + mesh, 0, 1, csys::Cartesian::Deferred(), "global", + LagrangeFunctionSpace::Backend::OmegaH); +} + inline bool AreArraysEqualUnordered( const Omega_h::HostRead& array1, const Omega_h::HostRead& array2, int start, int end) @@ -76,24 +205,86 @@ inline std::vector CopyOmegaHRealsToVector(const Omega_h::Reals& coords) coords_read.data() + coords_read.size()); } +inline double IntegrateP0Field(Omega_h::Mesh& mesh, const Field& field) +{ + const auto values = + FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); + const auto measures = Omega_h::measure_elements_real(&mesh); + const auto measures_h = Omega_h::HostRead(measures); + + double integral = 0.0; + for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { + integral += measures_h[e] * values[e]; + } + return integral; +} + +inline double IntegrateP1Field(Omega_h::Mesh& mesh, const Field& field) +{ + const auto values = + FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); + const auto measures = Omega_h::measure_elements_real(&mesh); + const auto measures_h = Omega_h::HostRead(measures); + const auto elem_verts_h = + Omega_h::HostRead(mesh.ask_elem_verts()); + const int verts_per_elem = mesh.dim() + 1; + + double integral = 0.0; + for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { + double avg = 0.0; + for (int k = 0; k < verts_per_elem; ++k) { + avg += values[elem_verts_h[verts_per_elem * e + k]]; + } + integral += measures_h[e] * (avg / verts_per_elem); + } + return integral; +} +#endif // PCMS_ENABLE_OMEGA_H + +// Min/max of an order-1 nodal field. For P1, nodal values are min/max +inline std::pair P1FieldRange(const Field& field) +{ + const auto values = FlattenToRank1View(field.GetDOFHolderData().GetValues()); + using MinMaxReducer = Kokkos::MinMax; + using MinMaxValue = MinMaxReducer::value_type; + + MinMaxValue range; + Kokkos::parallel_reduce( + static_cast(values.size()), + KOKKOS_LAMBDA(const int i, MinMaxValue& update) { + const double value = static_cast(values[i]); + if (value < update.min_val) + update.min_val = value; + if (value > update.max_val) + update.max_val = value; + }, + MinMaxReducer(range)); + return {range.min_val, range.max_val}; +} + +// Require that the target field stays within the range of the source field. +inline void RequireBoundedBy(const Field& target, + const Field& source, double tol = ExactTol) +{ + const auto src_range = P1FieldRange(source); + const auto tgt_range = P1FieldRange(target); + CAPTURE(src_range.first, src_range.second, tgt_range.first, tgt_range.second); + REQUIRE(tgt_range.first >= src_range.first - tol); + REQUIRE(tgt_range.second <= src_range.second + tol); +} + // 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) + const CoordView& coords_device) { - auto coords_view = - Kokkos::View("coords_view", nents, dim); + auto coords_view = Kokkos::View( + "coords_view", coords_device.extent(0), coords_device.extent(1)); 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::create_mirror_view(DeviceMemorySpace(), coords_view); + ConvertMismatchLayoutView2D(coords_view_device, coords_device); Kokkos::deep_copy(coords_view, coords_view_device); return coords_view; } @@ -122,95 +313,88 @@ inline std::vector EvaluateReferenceFunction(const std::vector& pts, expected_host.data() + expected_host.extent(0)); } -template +template struct SetFieldFunctor { - Kokkos::View data; - Kokkos::View coords; + DataView data; + CoordsView coords; Func f; - SetFieldFunctor(Kokkos::View data_, - Kokkos::View coords_, Func f_) + SetFieldFunctor(DataView data_, CoordsView 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)); } + void operator()(int i) const + { + if constexpr (std::is_invocable_v) { + data(i) = f(coords(i, 0), coords(i, 1), coords(i, 2)); + } else { + data(i) = f(coords(i, 0), coords(i, 1)); + } + } }; -// Set scalar DOF data by sampling func at each DOF-holder coordinate. +// Set scalar DOF data by sampling func at each DOF-holder coordinate. The +// arity of func selects the spatial dimension: func(x, y) for 2D layouts, +// func(x, y, z) for 3D. template -inline void SetField(const FieldLayout& layout, FieldData& field, - Func func) +inline void SetField(Field& field, Func func) { using MemorySpace = typename ExecutionSpace::memory_space; - auto dof_coords = layout.GetDOFHolderCoordinates().GetValues(); + static_assert(std::is_invocable_v || + std::is_invocable_v, + "SetField requires func(x, y) or func(x, y, z)"); + + auto dof_coords = field.GetLayout().GetDOFHolderCoordinates().GetValues(); 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}); + SetFieldFunctor{data_device, dof_coords, func}); auto data_host = Kokkos::create_mirror_view_and_copy(HostMemorySpace(), data_device); field.SetDOFHolderDataHost( - Rank2View(data_host.data(), n, 1)); + pcms::ValueView( + pcms::ValueBasis{}, + Rank2View(data_host.data(), n, 1))); } -template -inline void SetField(Field& field, Func func) +// Set multi-component DOF data by sampling func at each DOF-holder +// coordinate, writing the components through the out pointer. The arity of +// func selects the spatial dimension: func(x, y, out) for 2D layouts, +// func(x, y, z, out) for 3D. Host-side; the component count comes from the +// field's layout. +template +inline void SetFieldComponents(Field& field, Func func) { - SetField(field.GetLayout(), field.GetData(), func); -} + const auto coords = field.GetLayout().GetDOFHolderCoordinates().GetValues(); + const int n = static_cast(coords.extent(0)); + const int num_components = field.GetLayout().GetNumComponents(); -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 = FlattenToRank1View(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); + static_assert(std::is_invocable_v || + std::is_invocable_v, + "SetFieldComponents requires func(x, y, out) or " + "func(x, y, z, out)"); - auto data_after = FlattenToRank1View(field.GetDOFHolderDataHost()); - REQUIRE(data_after.size() == data_before.size()); + const auto coords_host = CopyCoordinatesToHost(coords); + std::vector data(static_cast(n) * num_components); for (int i = 0; i < n; ++i) { - REQUIRE(data_after[i] == Catch::Approx(data_before[i])); + Real* out = &data[static_cast(i) * num_components]; + if constexpr (std::is_invocable_v) { + func(coords_host(i, 0), coords_host(i, 1), coords_host(i, 2), out); + } else { + func(coords_host(i, 0), coords_host(i, 1), out); + } } -} - -inline void CheckSerializeDeserialize(Field& field) -{ - CheckSerializeDeserialize(field.GetLayout(), field.GetData()); + field.SetDOFHolderDataHost( + pcms::ValueView( + field.GetData().GetValueBasis(), + Rank2View(data.data(), n, num_components))); } // Helper structure to hold device coordinates with proper lifetime management @@ -220,28 +404,64 @@ struct DeviceCoordinates CoordinateView coordinate_view; }; +// Binds an already-built device buffer to a coordinate system, without the +// allocation and placeholder resolution CreateDeviceCoordinateView performs. +inline CoordinateView MakeCoords( + const Kokkos::View& data, + std::shared_ptr coordinate_system) +{ + return CoordinateView(std::move(coordinate_system), + MakeConstRank2View(data)); +} + +/// Tags a device output buffer with a field's stored value basis, which is +/// what evaluators and the component-moving transfer operators write. +template +inline ValueView TagLike( + const Field& field, Kokkos::View values) +{ + return ValueView(field.GetData().GetValueBasis(), + MakeRank2View(values)); +} + +template +inline Kokkos::View CreateDeviceRank1View( + const std::vector& values) +{ + Kokkos::View device("device", values.size()); + CopyHostRank1ViewToDeviceView(device, make_const_array_view(values)); + return device; +} + +inline Kokkos::View CreateDeviceRank2View( + const std::vector& values, int num_columns) +{ + const int n = static_cast(values.size()) / num_columns; + Kokkos::View host("host", n, num_columns); + for (int i = 0; i < n; ++i) { + for (int c = 0; c < num_columns; ++c) { + host(i, c) = values[static_cast(i) * num_columns + c]; + } + } + Kokkos::View device("device", n, num_columns); + DeepCopyMismatchLayouts(device, host); + return device; +} + // 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) + const std::vector& pts, + std::shared_ptr coordinate_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}}; + auto coords_device = CreateDeviceRank2View(pts, dim); + return DeviceCoordinates{ + coords_device, + CoordinateView{ + pcms::ResolveCoordinateSystem(std::move(coordinate_system), dim), + pcms::MakeRank2View(coords_device)}}; } // Evaluate field at explicit test points using a PointEvaluator and check @@ -249,16 +469,12 @@ inline DeviceCoordinates CreateDeviceCoordinateView( template void CheckEvaluation(const PointEvaluator& evaluator, const Field& field, const std::vector& pts, - Func func, double abs_tol = 1e-10) + Func func, double abs_tol = EvalTol) { 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); + Kokkos::View out_device("out_device", n, 1); + evaluator.Evaluate(field, TagLike(field, out_device)); auto out_host = Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); @@ -267,8 +483,8 @@ void CheckEvaluation(const PointEvaluator& evaluator, 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)); + << " got=" << out_host(i, 0) << " expected=" << expected[i]); + REQUIRE(out_host(i, 0) == Catch::Approx(expected[i]).margin(abs_tol)); } } @@ -279,33 +495,48 @@ template void CheckEvaluation(const Factory& factory, const Field& field, const std::vector& pts, Func func, - double abs_tol = 1e-10) + double abs_tol = EvalTol) { auto device_coords = - CreateDeviceCoordinateView(pts, factory.GetCoordinateSystem()); - auto evaluator = factory.template CreatePointEvaluator( + 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. +// Checks that the evaluator reports exactly the rows marked in `is_filled`. +inline void CheckFilledPoints(const PointEvaluator& evaluator, + const std::vector& is_filled) +{ + auto filled = Kokkos::create_mirror_view_and_copy(HostMemorySpace(), + evaluator.FilledPoints()); + std::vector reported(is_filled.size(), false); + for (size_t k = 0; k < filled.extent(0); ++k) { + const LO i = filled(k); + REQUIRE(i >= 0); + REQUIRE(static_cast(i) < is_filled.size()); + REQUIRE_FALSE(reported[i]); + reported[i] = true; + } + REQUIRE(reported == is_filled); +} + 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); + Kokkos::View out_device("out_device", n, 1); + evaluator.Evaluate(field, TagLike(field, out_device)); 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); + REQUIRE(out_host(i, 0) == fill_value); } + CheckFilledPoints(evaluator, std::vector(n, true)); } // Overload that creates the evaluator from any factory with FILL policy. @@ -314,9 +545,9 @@ void CheckFillMode(const Factory& factory, const Field& field, Real fill_value, const std::vector& outside_pts) { auto device_coords = - CreateDeviceCoordinateView(outside_pts, factory.GetCoordinateSystem()); + CreateDeviceCoordinateView(outside_pts, factory->GetCoordinateSystem()); OutOfBoundsPolicy policy{OutOfBoundsMode::FILL, fill_value}; - auto evaluator = factory.template CreatePointEvaluator( + auto evaluator = factory->template CreatePointEvaluator( EvaluationRequest::FromCoordinates(device_coords.coordinate_view, policy)); CheckFillMode(*evaluator, field, fill_value, outside_pts); } @@ -328,22 +559,19 @@ template & field, const std::vector& pts, const std::vector& is_inside, Func func, - Real fill_value, double abs_tol = 1e-10) + Real fill_value, double abs_tol = EvalTol) { int n = static_cast(pts.size()) / 2; REQUIRE(static_cast(is_inside.size()) == n); auto device_coords = - CreateDeviceCoordinateView(pts, factory.GetCoordinateSystem()); + CreateDeviceCoordinateView(pts, factory->GetCoordinateSystem()); OutOfBoundsPolicy policy{OutOfBoundsMode::FILL, fill_value}; - auto evaluator = factory.template CreatePointEvaluator( + 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); + Kokkos::View out_device("out_device", n, 1); + evaluator->Evaluate(field, TagLike(field, out_device)); auto out_host = Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); @@ -352,15 +580,38 @@ void CheckEvaluationWithFill(const Factory& factory, const Field& field, 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)); + << " got=" << out_host(i, 0)); if (is_inside[i]) { INFO(" expected=" << expected[i]); - REQUIRE(out_host(i) == Catch::Approx(expected[i]).margin(abs_tol)); + REQUIRE(out_host(i, 0) == Catch::Approx(expected[i]).margin(abs_tol)); } else { - REQUIRE(out_host(i) == fill_value); + REQUIRE(out_host(i, 0) == fill_value); } } + std::vector is_filled(is_inside.size()); + for (size_t i = 0; i < is_inside.size(); ++i) { + is_filled[i] = !is_inside[i]; + } + CheckFilledPoints(*evaluator, is_filled); +} + +#if defined(PCMS_ENABLE_PETSC) && defined(PCMS_ENABLE_MESHFIELDS) +// Evaluates source_field at the integrator's sample points and assembles the +// load vector. +inline void EvaluateAndAssemble( + LinearFormIntegrator& integrator, + const std::shared_ptr& source_space, + const Field& source_field) +{ + const auto& pts = integrator.GetIntegrationPoints(); + const std::size_t npts = pts.GetValues().extent(0); + auto evaluator = source_space->CreatePointEvaluator( + EvaluationRequest::FromCoordinates(pts)); + Kokkos::View sampled("sampled", npts, 1); + evaluator->Evaluate(source_field, TagLike(source_field, sampled)); + integrator.Assemble(MakeConstRank2View(sampled)); } +#endif // PCMS_ENABLE_PETSC && PCMS_ENABLE_MESHFIELDS } // namespace pcms::test diff --git a/test/python/__init__.py b/test/python/__init__.py new file mode 100644 index 000000000..ae78246ec --- /dev/null +++ b/test/python/__init__.py @@ -0,0 +1 @@ +# tests/__init__.py diff --git a/test/python/conftest.py b/test/python/conftest.py new file mode 100644 index 000000000..08e73385b --- /dev/null +++ b/test/python/conftest.py @@ -0,0 +1,27 @@ +""" +Shared pytest fixtures for pcms Python API tests. +""" + +import pytest +import PyOmega_h as omega_h + + +@pytest.fixture(scope="session") +def omega_h_lib(): + """Create Omega_h library instance. + + Session-scoped so the (potentially expensive) Omega_h initialization + happens exactly once per test run. + """ + lib = omega_h.OmegaHLibrary() + return lib + + +@pytest.fixture(scope="session") +def world(omega_h_lib): + """Create Omega_h world communicator. + + Session-scoped; the world is obtained from the session-scoped library + and shared by all tests that need mesh-building capabilities. + """ + return omega_h_lib.world() diff --git a/test/python/test_coordinate_transformations.py b/test/python/test_coordinate_transformations.py new file mode 100644 index 000000000..1db5c0944 --- /dev/null +++ b/test/python/test_coordinate_transformations.py @@ -0,0 +1,427 @@ +""" +Tests for coordinate systems, value bases, and coordinate transformations. +""" + +import gc + +import numpy as np +import pytest +import pcms +import PyOmega_h as omega_h + +# This just checks if we can get data without copying it. Once +# https://github.com/SCOREC/pcms/issues/361 is resolved, we should +# be able to have zero copy on both host and device side +ZERO_COPY = pcms.device_memory_is_host_accessible + + +def _cylindrical_vector(): + return pcms.ValueBasis(pcms.csys.CylindricalRThetaZ.create(), + pcms.ComponentScaling.Physical, pcms.values.Vector) + + +def _cartesian_vector(): + return pcms.ValueBasis(pcms.csys.Cartesian.create(3), + pcms.ComponentScaling.Physical, pcms.values.Vector) + + +def _coordinates(field): + return field.get_layout().get_dof_holder_coordinates().get_values() + + +@pytest.fixture +def cylindrical_space(world): + """P1 vector space on a box whose coordinates are (r, theta, z).""" + mesh = omega_h.build_box(world, omega_h.Family.SIMPLEX, 2.0, 1.6, 1.1, + 6, 6, 4, False) + return pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 3, pcms.csys.CylindricalRThetaZ.create(), + backend=pcms.LagrangeFunctionSpace.Backend.OmegaH) + + +@pytest.fixture +def cartesian_space(world): + """P1 vector space on the Cartesian unit cube.""" + mesh = omega_h.build_box(world, omega_h.Family.SIMPLEX, 1.0, 1.0, 1.0, + 3, 3, 3, False) + return pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 3, pcms.csys.Cartesian.create(3), + backend=pcms.LagrangeFunctionSpace.Backend.OmegaH) + + +def _coordinates_of(space): + return space.get_layout().get_dof_holder_coordinates().get_values() + + +def _radial_field(space): + """B = r e_r + z e_z in physical cylindrical components, which is + (x, y, z) in Cartesian ones.""" + b = space.create_function("b", pcms.values.Vector, + scaling=pcms.ComponentScaling.Physical) + p = _coordinates(b) + b.set_dof_holder_data(pcms.ValueView( + b.get_data().get_value_basis(), + np.stack([p[:, 0], np.zeros(len(p)), p[:, 2]], axis=1))) + return b + + +class TestCoordinateSystem: + def test_concrete_types(self): + cases = [ + (pcms.csys.Cartesian.create(2), pcms.csys.Cartesian, 2), + (pcms.csys.CylindricalRThetaZ.create(), + pcms.csys.CylindricalRThetaZ, 3), + (pcms.csys.CylindricalRZ.create(), pcms.csys.CylindricalRZ, 2), + ] + for system, cls, dim in cases: + assert isinstance(system, cls) + assert isinstance(system, pcms.CoordinateSystem) + assert system.dimension() == dim + + def test_properties(self): + cyl = pcms.csys.CylindricalRThetaZ.create() + assert cyl.kind() == "cylindrical_rthetaz" + assert cyl.has_orthogonal_basis() + assert not cyl.has_unit_scale_factors() + assert not pcms.has_identity_metric(cyl) + assert pcms.has_identity_metric(pcms.csys.Cartesian.create(3)) + + def test_equality_is_by_value(self): + a = pcms.csys.Cartesian.create(3) + assert a == pcms.csys.Cartesian.create(3) + assert a != pcms.csys.Cartesian.create(2) + assert a != pcms.csys.CylindricalRThetaZ.create() + assert pcms.same_coordinate_system(a, pcms.csys.Cartesian.create(3)) + assert len({a, pcms.csys.Cartesian.create(3)}) == 1 + assert a != "cartesian" + + def test_space_defaults_to_cartesian_of_mesh_dimension(self, world): + mesh = omega_h.build_box(world, omega_h.Family.SIMPLEX, 1.0, 1.0, 0.0, + 4, 4, 0, False) + space = pcms.LagrangeFunctionSpace.from_mesh(mesh, 1, 1) + assert (space.get_layout().get_coordinate_system() + == pcms.csys.Cartesian.create(2)) + + def test_create_rejects_bad_dimension(self): + with pytest.raises(RuntimeError): + pcms.csys.Cartesian.create(4) + + +class TestCoordinateView: + def test_defaults_to_cartesian_of_array_dimension(self): + view = pcms.CoordinateView(np.zeros((4, 2))) + assert view.get_coordinate_system() == pcms.csys.Cartesian.create(2) + + def test_rejects_wrong_dimension(self): + with pytest.raises(RuntimeError, match="coordinate columns"): + pcms.CoordinateView(pcms.csys.CylindricalRThetaZ.create(), + np.zeros((4, 2))) + + def test_layout_coordinates_carry_the_space_system(self, + cylindrical_space): + coords = cylindrical_space.get_layout().get_dof_holder_coordinates() + assert isinstance(coords.get_coordinate_system(), + pcms.csys.CylindricalRThetaZ) + assert not coords.get_values().flags.writeable + assert cylindrical_space.get_coordinate_system() == \ + pcms.csys.CylindricalRThetaZ.create() + + +class TestValueBasis: + def test_variance_signatures(self): + assert pcms.values.Scalar.rank() == 0 + assert list(pcms.values.Vector) == [pcms.Variance.Contravariant] + assert list(pcms.values.Covector) == [pcms.Variance.Covariant] + assert pcms.values.Tensor.rank() == 2 + assert pcms.values.Of([pcms.Variance.Covariant]) == \ + pcms.values.Covector + assert pcms.VarianceSignature( + [pcms.Variance.Contravariant]) == pcms.values.Vector + with pytest.raises(ValueError): + pcms.values.Of([pcms.Variance.Covariant] * 5) + + def test_equality_is_same_value_basis(self): + # Every scalar basis is the same, whatever its members say. + assert pcms.ValueBasis() == pcms.ValueBasis( + pcms.csys.Cartesian.create(3)) + assert _cylindrical_vector() == _cylindrical_vector() + assert _cylindrical_vector() != _cartesian_vector() + assert pcms.same_value_basis(_cartesian_vector(), _cartesian_vector()) + + def test_members(self): + basis = _cylindrical_vector() + assert basis.rank() == 1 + assert isinstance(basis.system, pcms.csys.CylindricalRThetaZ) + assert basis.scaling == pcms.ComponentScaling.Physical + assert basis.variance == pcms.values.Vector + + +class TestValueView: + def test_checks_component_count(self): + with pytest.raises(RuntimeError, match="require 3 components"): + pcms.ValueView(_cylindrical_vector(), np.zeros((4, 2))) + + def test_rejects_non_2d(self): + with pytest.raises(ValueError): + pcms.ValueView(pcms.ValueBasis(), np.zeros(4)) + + def test_aliases_the_array(self): + values = np.zeros((4, 3)) + view = pcms.ValueView(_cylindrical_vector(), values) + assert view.get_values() is values + assert view.rank() == 1 + assert view.get_value_type() == pcms.FieldValueType.Vector + assert (view.extent(0), view.extent(1), view.size()) == (4, 3, 12) + np.testing.assert_array_equal(np.asarray(view), values) + + +class TestFunction: + def test_value_basis_defaults_to_the_space_system(self, cartesian_space): + b = cartesian_space.create_function("b", pcms.values.Vector) + assert b.get_name() == "b" + assert b.get_data().get_value_basis() == _cartesian_vector() + assert b.get_data().get_value_type() == pcms.FieldValueType.Vector + + def test_borrowed_basis(self, cylindrical_space): + b = cylindrical_space.create_function( + "b", pcms.values.Vector, system=pcms.csys.Cartesian.create(3)) + assert b.get_data().get_value_basis() == _cartesian_vector() + + def test_ambiguous_scaling_must_be_stated(self, cylindrical_space): + with pytest.raises(RuntimeError, match="scaling must be stated"): + cylindrical_space.create_function("b", pcms.values.Vector) + + def test_get_space_returns_the_space(self, cartesian_space): + f = cartesian_space.create_function() + assert f.get_space() is cartesian_space + assert isinstance(f, pcms.Field) + + def test_dof_holder_data_is_tagged(self, cylindrical_space): + b = _radial_field(cylindrical_space) + data = b.get_dof_holder_data() + assert data.get_basis() == _cylindrical_vector() + assert not data.get_values().flags.writeable + np.testing.assert_array_equal(data.get_values()[:, 0], + _coordinates(b)[:, 0]) + + def test_set_checks_the_basis(self, cylindrical_space): + b = _radial_field(cylindrical_space) + n = b.get_layout().get_num_owned_dof_holder() + with pytest.raises(RuntimeError, match="does not match"): + b.set_dof_holder_data( + pcms.ValueView(_cartesian_vector(), np.zeros((n, 3)))) + + def test_set_checks_the_shape(self, cylindrical_space): + b = _radial_field(cylindrical_space) + n = b.get_layout().get_num_owned_dof_holder() + with pytest.raises(ValueError): + b.set_dof_holder_data( + pcms.ValueView(_cylindrical_vector(), np.zeros((n - 1, 3)))) + + def test_plain_arrays_only_for_scalars(self, cylindrical_space): + b = _radial_field(cylindrical_space) + n = b.get_layout().get_num_owned_dof_holder() + with pytest.raises(TypeError, match="need a ValueView"): + b.set_dof_holder_data(np.zeros((n, 3))) + b.set_dof_holder_data_unchecked(np.ones((n, 3))) + np.testing.assert_array_equal(b.get_dof_holder_data().get_values(), + 1.0) + with pytest.raises(ValueError): + b.set_dof_holder_data_unchecked(np.ones((n, 2))) + + +class TestCoordinateMap: + def test_bind_maps_points_and_rotates_values(self): + cart = pcms.CoordinateView(pcms.csys.Cartesian.create(3), + [[1.0, 0.0, 0.5], [0.0, 2.0, -1.0], + [1.0, 1.0, 0.0]]) + m = pcms.CartesianToCylindrical() + assert m.get_source_coordinate_system() == \ + pcms.csys.Cartesian.create(3) + assert m.get_target_coordinate_system() == \ + pcms.csys.CylindricalRThetaZ.create() + bound = m.bind(cart) + assert bound.num_points() == 3 + mapped = bound.mapped_points() + assert mapped.get_coordinate_system() == \ + pcms.csys.CylindricalRThetaZ.create() + rtz = mapped.get_values() + np.testing.assert_allclose(rtz[:, 0], [1.0, 2.0, np.sqrt(2.0)]) + np.testing.assert_allclose(rtz[:, 1], [0.0, np.pi / 2, np.pi / 4]) + + assert bound.output_basis(_cylindrical_vector()) == \ + _cartesian_vector() + e_theta = np.tile([0.0, 1.0, 0.0], (3, 1)) + out = bound.transform_values( + pcms.ValueView(_cylindrical_vector(), e_theta)) + assert out.get_basis() == _cartesian_vector() + assert out.get_values() is e_theta # transformed in place + np.testing.assert_allclose( + e_theta, np.stack([-np.sin(rtz[:, 1]), np.cos(rtz[:, 1]), + np.zeros(3)], axis=1), atol=1e-15) + + def test_round_trip(self): + rtz = pcms.CoordinateView(pcms.csys.CylindricalRThetaZ.create(), + [[1.0, 0.3, 0.5], [2.0, 1.2, -1.0]]) + xyz = pcms.CylindricalToCartesian().bind(rtz).mapped_points() + back = pcms.CartesianToCylindrical().bind(xyz).mapped_points() + np.testing.assert_allclose(back.get_values(), rtz.get_values()) + + def test_bind_checks_the_source_system(self): + rtz = pcms.CoordinateView(pcms.csys.CylindricalRThetaZ.create(), + np.ones((2, 3))) + with pytest.raises(RuntimeError): + pcms.CartesianToCylindrical().bind(rtz) + + def test_transform_values_requires_a_writeable_buffer(self): + cart = pcms.CoordinateView(pcms.csys.Cartesian.create(3), + np.ones((2, 3))) + bound = pcms.CartesianToCylindrical().bind(cart) + values = np.zeros((2, 3)) + values.flags.writeable = False + with pytest.raises(ValueError, match="writeable"): + bound.transform_values( + pcms.ValueView(_cylindrical_vector(), values)) + + def test_mapped_points_outlive_the_bound_map(self): + cart = pcms.CoordinateView(pcms.csys.Cartesian.create(3), + [[0.0, 3.0, 1.0]]) + points = pcms.CartesianToCylindrical().bind(cart).mapped_points() + gc.collect() + np.testing.assert_allclose(points.get_values(), + [[3.0, np.pi / 2, 1.0]]) + + +class TestTransformedSource: + def _evaluator(self, cylindrical_space, cartesian_space): + source = pcms.TransformedSource(cylindrical_space, + pcms.CartesianToCylindrical()) + assert isinstance(source, pcms.PointEvaluatorFactory) + assert source.get_coordinate_system() == \ + pcms.csys.Cartesian.create(3) + request = pcms.EvaluationRequest.from_function_space(cartesian_space) + return source.create_point_evaluator(request) + + def test_evaluate_rotates_into_the_query_basis(self, cylindrical_space, + cartesian_space): + b = _radial_field(cylindrical_space) + evaluator = self._evaluator(cylindrical_space, cartesian_space) + basis = evaluator.output_basis(b.get_data().get_value_basis()) + assert basis == _cartesian_vector() + n = evaluator.num_points() + out = np.zeros((n, 3)) + evaluator.evaluate(b, pcms.ValueView(basis, out)) + np.testing.assert_allclose(out, _coordinates_of(cartesian_space), + atol=1e-12) + assert len(evaluator.filled_points()) == 0 + + def test_evaluate_checks_its_output(self, cylindrical_space, + cartesian_space): + b = _radial_field(cylindrical_space) + evaluator = self._evaluator(cylindrical_space, cartesian_space) + n = evaluator.num_points() + with pytest.raises(TypeError, match="need a ValueView"): + evaluator.evaluate(b, np.zeros((n, 3))) + with pytest.raises(RuntimeError, match="does not match"): + evaluator.evaluate( + b, pcms.ValueView(_cylindrical_vector(), np.zeros((n, 3)))) + with pytest.raises(ValueError, match="must be"): + evaluator.evaluate( + b, pcms.ValueView(_cartesian_vector(), np.zeros((n + 1, 3)))) + + def test_query_points_must_be_in_the_factory_system(self, + cylindrical_space): + request = pcms.EvaluationRequest.from_coordinates( + pcms.CoordinateView(pcms.csys.Cartesian.create(3), + np.full((2, 3), 0.5))) + with pytest.raises(RuntimeError, match="TransformedSource"): + cylindrical_space.create_point_evaluator(request) + + def test_filled_points(self, cylindrical_space): + source = pcms.TransformedSource(cylindrical_space, + pcms.CartesianToCylindrical()) + # The second point has theta = pi, outside the source's [0, 1.6]. + request = pcms.EvaluationRequest.from_coordinates( + pcms.CoordinateView(pcms.csys.Cartesian.create(3), + [[0.5, 0.5, 0.5], [-0.5, 0.0, 0.5]]), + pcms.OutOfBoundsPolicy(pcms.OutOfBoundsMode.FILL, -7.0)) + evaluator = source.create_point_evaluator(request) + filled = evaluator.filled_points() + np.testing.assert_array_equal(filled, [1]) + assert not filled.flags.writeable + b = _radial_field(cylindrical_space) + out = np.zeros((2, 3)) + evaluator.evaluate(b, pcms.ValueView(_cartesian_vector(), out)) + np.testing.assert_allclose(out[0], [0.5, 0.5, 0.5], atol=1e-12) + np.testing.assert_array_equal(out[1], -7.0) + + def test_interpolator_transfers_across_systems(self, cylindrical_space, + cartesian_space): + b_src = _radial_field(cylindrical_space) + b_tgt = cartesian_space.create_function("b", pcms.values.Vector) + op = pcms.Interpolator( + pcms.TransformedSource(cylindrical_space, + pcms.CartesianToCylindrical()), + cartesian_space) + assert isinstance(op, pcms.TransferOperator) + op.apply(b_src, b_tgt) + np.testing.assert_allclose(b_tgt.get_dof_holder_data().get_values(), + _coordinates(b_tgt), atol=1e-12) + + def test_interpolator_checks_the_target_basis(self, cylindrical_space, + cartesian_space): + b_src = _radial_field(cylindrical_space) + wrong = cartesian_space.create_function( + "b", pcms.values.Vector, + system=pcms.csys.CylindricalRThetaZ.create(), + scaling=pcms.ComponentScaling.Physical) + op = pcms.Interpolator( + pcms.TransformedSource(cylindrical_space, + pcms.CartesianToCylindrical()), + cartesian_space) + with pytest.raises(RuntimeError, match="target's declared basis"): + op.apply(b_src, wrong) + + def test_plain_interpolator_refuses_to_bridge_systems( + self, cylindrical_space, cartesian_space): + with pytest.raises(RuntimeError, match="TransformedSource"): + pcms.Interpolator(cylindrical_space, cartesian_space) + + +class TestLifetimes: + def test_space_keeps_its_mesh_alive(self, world): + mesh = omega_h.build_box(world, omega_h.Family.SIMPLEX, 1.0, 1.0, 0.0, + 4, 4, 0, False) + space = pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 1, backend=pcms.LagrangeFunctionSpace.Backend.OmegaH) + del mesh + gc.collect() + f = space.create_function() + coords = _coordinates(f) + f.set_dof_holder_data(coords[:, :1]) + evaluator = space.create_point_evaluator( + pcms.EvaluationRequest.from_coordinates( + pcms.CoordinateView([[0.3, 0.6]]))) + out = np.zeros((1, 1)) + evaluator.evaluate(f, out) + np.testing.assert_allclose(out, [[0.3]], atol=1e-12) + + def test_operators_keep_their_sources_alive(self, cylindrical_space, + cartesian_space): + b_src = _radial_field(cylindrical_space) + b_tgt = cartesian_space.create_function("b", pcms.values.Vector) + op = pcms.Interpolator( + pcms.TransformedSource(cylindrical_space, + pcms.CartesianToCylindrical()), + cartesian_space) + source = pcms.TransformedSource(cylindrical_space, + pcms.CartesianToCylindrical()) + evaluator = source.create_point_evaluator( + pcms.EvaluationRequest.from_function_space(cartesian_space)) + del source + gc.collect() + op.apply(b_src, b_tgt) + out = np.zeros((evaluator.num_points(), 3)) + evaluator.evaluate(b_src, pcms.ValueView(_cartesian_vector(), out)) + np.testing.assert_allclose(out, b_tgt.get_dof_holder_data(), + atol=1e-12) diff --git a/test/python/test_examples.py b/test/python/test_examples.py new file mode 100644 index 000000000..6cb40c8e2 --- /dev/null +++ b/test/python/test_examples.py @@ -0,0 +1,72 @@ +""" +Run the Python API examples + +An example whose optional dependencies are missing +is skipped. Set PCMS_REQUIRE_EXAMPLES=1 to require +those examples to run. +""" + +import os +import pathlib +import subprocess +import sys + +import pytest +import pcms +import PyOmega_h as omega_h + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] +EXAMPLES_DIR = REPO_ROOT / "examples" / "python-api-example" +EXAMPLE_TIMEOUT_SECONDS = 600 + + +def _test_data_dir(): + return pathlib.Path( + os.environ.get("PCMS_TEST_DATA_DIR", REPO_ROOT / "pcms_testcases") + ) + + +def _skip_or_fail(reason): + if os.environ.get("PCMS_REQUIRE_EXAMPLES", "0") == "1": + pytest.fail(f"{reason} (PCMS_REQUIRE_EXAMPLES=1)") + pytest.skip(reason) + + +def _run_example(script, *args): + result = subprocess.run( + [sys.executable, str(EXAMPLES_DIR / script), *map(str, args)], + capture_output=True, + text=True, + timeout=EXAMPLE_TIMEOUT_SECONDS, + ) + assert result.returncode == 0, ( + f"{script} exited with {result.returncode}\n" + f"--- stdout ---\n{result.stdout}\n--- stderr ---\n{result.stderr}" + ) + return result.stdout + + +def test_conservative_transfer_example(): + if not hasattr(pcms, "OmegaHConservativeProjection"): + _skip_or_fail("pcms was built without PETSc/MeshFields") + stdout = _run_example("example_conservative_transfer.py") + assert "Field transfer completed" in stdout + + +def test_flow_example(): + if not hasattr(omega_h, "exodus_open"): + _skip_or_fail("PyOmega_h was built without Exodus support") + exo_path = _test_data_dir() / "exodus" / "flow" / "flow.exo" + if not exo_path.exists(): + _skip_or_fail(f"test data not found: {exo_path}") + with open(exo_path, "rb") as f: + if f.read(64).startswith(b"version https://git-lfs"): + _skip_or_fail(f"{exo_path} is a Git LFS pointer; fetch it with " + "git lfs pull") + stdout = _run_example("flow_example.py", exo_path) + assert "SUCCESS" in stdout + + +def test_coordinate_transformations_example(): + stdout = _run_example("coordinate_transformations.py") + assert "SUCCESS" in stdout diff --git a/test/python/test_field_copy.py b/test/python/test_field_copy.py new file mode 100644 index 000000000..fa616aa28 --- /dev/null +++ b/test/python/test_field_copy.py @@ -0,0 +1,61 @@ +"""Test copying Omega_h field data.""" + +import numpy as np +import pytest +import pcms +import PyOmega_h as omega_h + + +class TestFieldCopy: + """Tests for field data copying between Omega_h-backed fields.""" + + @pytest.mark.parametrize("dim, order, num_components", [ + (2, 1, 1), + (2, 2, 1), + ]) + def test_copy(self, world, dim, order, num_components): + """Test copying omega_h field data.""" + nx = 100 + ny = 100 if dim > 1 else 0 + nz = 100 if dim > 2 else 0 + + # Build mesh + mesh = omega_h.build_box( + world, + omega_h.Family.SIMPLEX, + 1.0, 1.0, 1.0, + nx, ny, nz, + False + ) + + # Create factory and layout + factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components) + + # Create original field and set data + original = factory.create_function() + layout = original.get_layout() + ndata = layout.get_num_owned_dof_holder() * layout.get_num_components() + + # Create sequential array of IDs + ids = np.arange(ndata, dtype=np.float64).reshape( + -1, original.get_layout().get_num_components()) + original.set_dof_holder_data(ids) + + # Create copied field and copy data + copied = factory.create_function() + copier = pcms.Copy(factory, factory) + copier.apply(original, copied) + + # Get copied data + copied_array = copied.get_dof_holder_data().get_values() + + # Verify the copy + assert copied_array.shape == ids.shape, \ + f"Expected shape {ids.shape}, got {copied_array.shape}" + + # Check that all values match + differences = np.abs(ids - copied_array) + num_matches = np.sum(differences < 1e-12) + assert num_matches == ndata, \ + f"Only {num_matches}/{ndata} elements matched" diff --git a/test/python/test_mls_interpolation.py b/test/python/test_mls_interpolation.py new file mode 100644 index 000000000..c09451e3a --- /dev/null +++ b/test/python/test_mls_interpolation.py @@ -0,0 +1,91 @@ +""" +Tests for MLS-backed PolynomialReconstructionFunctionSpace. +""" + +import numpy as np +import pcms + + +def poly_value(x, y, degree): + """Evaluate a simple polynomial at (x, y).""" + if degree == 0: + return 3.0 + if degree == 1: + return x + y + if degree == 2: + return x**2 + y**2 + if degree == 3: + return x**3 + y**3 + raise ValueError(f"Unsupported polynomial degree: {degree}") + + +class TestMLSInterpolation: + """Tests for MLS polynomial reproduction.""" + + def test_polynomial_reproduction(self): + """ + 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) + source_xy = np.array( + [(x, y) for x in grid_vals for y in grid_vals], + dtype=np.float64, + ) + target_xy = np.array( + [ + [0.11, 0.19], + [0.23, 0.77], + [0.51, 0.49], + [0.87, 0.26], + [0.69, 0.91], + [0.35, 0.62], + ], + dtype=np.float64, + ) + num_targets = target_xy.shape[0] + + for interp_degree in range(1, 4): + opts = pcms.MLSOptions() + opts.degree = interp_degree + opts.radius = 1.5 + opts.adapt_radius = False + opts.basis = pcms.RadialBasisFunction.NO_OP + + factory = pcms.PolynomialReconstructionFunctionSpace.create( + pcms.CoordinateView(source_xy), + opts, + ) + field = factory.create_function() + request = pcms.EvaluationRequest.from_coordinates( + pcms.CoordinateView(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], + dtype=np.float64, + ) + exact_target_values = np.array( + [poly_value(x, y, func_degree) for (x, y) in target_xy], + dtype=np.float64, + ) + + 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, ( + f"MLS failed for interp_degree={interp_degree}, " + f"func_degree={func_degree}: max_abs_err={max_abs_err}" + ) diff --git a/test/python/test_numpy_conversion.py b/test/python/test_numpy_conversion.py new file mode 100644 index 000000000..4d65a75d2 --- /dev/null +++ b/test/python/test_numpy_conversion.py @@ -0,0 +1,318 @@ +""" +Tests for numpy <-> pcms array conversions. +""" + +import gc + +import numpy as np +import pytest +import pcms +import PyOmega_h as omega_h + +ZERO_COPY = pcms.device_memory_is_host_accessible + + +def _linear(x, y): + return 1.0 + 2.0 * x + 3.0 * y + + +@pytest.fixture +def mesh(world): + return omega_h.build_box( + world, omega_h.Family.SIMPLEX, 1.0, 1.0, 0.0, 8, 8, 0, False + ) + + +@pytest.fixture +def p1_space(mesh): + return pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 1) + + +def _coordinates(field): + return field.get_layout().get_dof_holder_coordinates().get_values() + + +def _num_dof_holders(field): + return field.get_layout().get_num_owned_dof_holder() + + +def _linear_field(space): + field = space.create_function() + coords = _coordinates(field) + field.set_dof_holder_data(_linear(coords[:, 0], coords[:, 1])) + return field + + +class TestFieldData: + def test_shape_matches_layout(self, mesh, world): + space = pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 2) + field = space.create_function() + n = _num_dof_holders(field) + data = np.arange(2 * n, dtype=np.float64).reshape(n, 2) + field.set_dof_holder_data(data) + result = field.get_dof_holder_data().get_values() + assert result.shape == (n, 2) + np.testing.assert_array_equal(result, data) + + def test_dof_holder_coordinates_match_mesh(self, mesh, p1_space): + field = p1_space.create_function() + coords = _coordinates(field) + assert coords.shape == (mesh.nverts(), 2) + np.testing.assert_array_equal(coords, mesh.coords().reshape(-1, 2)) + + def test_set_from_strided_array(self, p1_space): + field = p1_space.create_function() + n = _num_dof_holders(field) + wide = np.arange(2 * n, dtype=np.float64).reshape(n, 2) + field.set_dof_holder_data(wide[:, 1:]) # stride of two doubles + np.testing.assert_array_equal(field.get_dof_holder_data(), wide[:, 1:]) + + def test_set_from_float32_array(self, p1_space): + field = p1_space.create_function() + n = _num_dof_holders(field) + data = np.arange(n, dtype=np.float32).reshape(n, 1) + field.set_dof_holder_data(data) + np.testing.assert_array_equal(field.get_dof_holder_data(), + data.astype(np.float64)) + + def test_scalar_field_accepts_1d(self, p1_space): + field = p1_space.create_function() + n = _num_dof_holders(field) + data = np.arange(n, dtype=np.float64) + field.set_dof_holder_data(data) + result = field.get_dof_holder_data().get_values() + assert result.shape == (n, 1) + np.testing.assert_array_equal(result[:, 0], data) + + @pytest.mark.parametrize("shape", [ + lambda n: (n, 2), lambda n: (n + 1, 1), lambda n: (n + 1,), + lambda n: (n, 1, 1), + ], ids=["wrong-components", "wrong-dof-holders", "1d-wrong-dof-holders", + "3d"]) + def test_set_rejects_wrong_shape(self, p1_space, shape): + field = p1_space.create_function() + with pytest.raises(ValueError): + field.set_dof_holder_data(np.zeros(shape(_num_dof_holders(field)))) + + def test_vector_field_rejects_1d(self, mesh): + space = pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 2) + field = space.create_function() + n = _num_dof_holders(field) + for shape in [(n,), (2 * n,)]: + with pytest.raises(ValueError): + field.set_dof_holder_data(np.zeros(shape)) + + def test_returned_arrays_are_read_only(self, p1_space): + field = _linear_field(p1_space) + for arr in (field.get_dof_holder_data().get_values(), + _coordinates(field)): + assert not arr.flags.writeable + with pytest.raises(ValueError): + arr[0, 0] = 0.0 + + def test_data_is_zero_copy_on_host_builds(self, p1_space): + field = _linear_field(p1_space) + assert np.shares_memory(field.get_dof_holder_data().get_values(), + field.get_dof_holder_data().get_values()) == \ + ZERO_COPY + + def test_data_reflects_set(self, p1_space): + # Only arrays obtained after the modification are guaranteed current. + field = _linear_field(p1_space) + new = field.get_dof_holder_data().get_values() + 1.0 + field.set_dof_holder_data(new) + np.testing.assert_array_equal(field.get_dof_holder_data(), new) + + def test_coordinates_are_zero_copy_on_host_builds(self, p1_space): + field = p1_space.create_function() + assert np.shares_memory(_coordinates(field), _coordinates(field)) == \ + ZERO_COPY + + def test_shared_arrays_keep_field_alive(self, p1_space): + field = _linear_field(p1_space) + data = field.get_dof_holder_data().get_values() + coords = _coordinates(field) + expected_data, expected_coords = data.copy(), coords.copy() + del field + gc.collect() + np.testing.assert_array_equal(data, expected_data) + np.testing.assert_array_equal(coords, expected_coords) + + +class TestPointEvaluation: + QUERY = np.array([[0.5, 0.5], [0.25, 0.75], [0.9, 0.1]]) + + def _evaluator(self, space, coords): + request = pcms.EvaluationRequest.from_coordinates( + pcms.CoordinateView(coords)) + return space.create_point_evaluator(request) + + def test_evaluate_linear_field(self, p1_space): + field = _linear_field(p1_space) + out = np.zeros((len(self.QUERY), 1)) + self._evaluator(p1_space, self.QUERY).evaluate(field, out) + np.testing.assert_allclose( + out[:, 0], _linear(self.QUERY[:, 0], self.QUERY[:, 1]), + atol=1e-12) + + def test_evaluate_into_1d_output(self, p1_space): + field = _linear_field(p1_space) + out = np.zeros(len(self.QUERY)) + self._evaluator(p1_space, self.QUERY).evaluate(field, out) + np.testing.assert_allclose( + out, _linear(self.QUERY[:, 0], self.QUERY[:, 1]), atol=1e-12) + + def test_evaluate_rejects_wrong_components(self, p1_space): + field = _linear_field(p1_space) + out = np.zeros((len(self.QUERY), 2)) + with pytest.raises(ValueError): + self._evaluator(p1_space, self.QUERY).evaluate(field, out) + + def test_request_from_transposed_coordinates(self, p1_space): + field = _linear_field(p1_space) + coords = np.ascontiguousarray(self.QUERY.T).T # same values, F-order + assert not coords.flags.c_contiguous + out = np.zeros((len(coords), 1)) + self._evaluator(p1_space, coords).evaluate(field, out) + np.testing.assert_allclose( + out[:, 0], _linear(self.QUERY[:, 0], self.QUERY[:, 1]), + atol=1e-12) + + def test_request_copies_coordinates(self, p1_space): + field = _linear_field(p1_space) + coords = self.QUERY.copy() + evaluator = self._evaluator(p1_space, coords) + coords[:] = -1.0 # must not affect the request + del coords + gc.collect() + out = np.zeros((len(self.QUERY), 1)) + evaluator.evaluate(field, out) + np.testing.assert_allclose( + out[:, 0], _linear(self.QUERY[:, 0], self.QUERY[:, 1]), + atol=1e-12) + + @pytest.mark.parametrize("make_output", [ + lambda n: np.zeros((n, 1), dtype=np.float32), + lambda n: np.zeros((n, 2))[:, :1], + ], ids=["float32", "strided"]) + def test_evaluate_rejects_unwritable_output_layout(self, p1_space, + make_output): + field = _linear_field(p1_space) + out = make_output(len(self.QUERY)) + with pytest.raises(ValueError): + self._evaluator(p1_space, self.QUERY).evaluate(field, out) + + def test_evaluate_rejects_read_only_output(self, p1_space): + field = _linear_field(p1_space) + out = np.zeros((len(self.QUERY), 1)) + out.flags.writeable = False + with pytest.raises(ValueError): + self._evaluator(p1_space, self.QUERY).evaluate(field, out) + + +class TestCoordinateView: + def test_copies_input(self): + coords = np.arange(12, dtype=np.float64).reshape(6, 2) + expected = coords.copy() + view = pcms.CoordinateView(coords) + assert not np.shares_memory(view.get_values(), coords) + coords[:] = -1.0 + del coords + gc.collect() + np.testing.assert_array_equal(view.get_values(), expected) + + def test_strided_input(self): + coords = np.arange(12, dtype=np.float64).reshape(6, 2) + view = pcms.CoordinateView(np.asfortranarray(coords)) + gc.collect() + np.testing.assert_array_equal(view.get_values(), coords) + assert not view.get_values().flags.writeable + + def test_coordinates_keep_view_alive(self): + coords = np.arange(12, dtype=np.float64).reshape(6, 2) + values = pcms.CoordinateView(coords.copy()).get_values() + gc.collect() + np.testing.assert_array_equal(values, coords) + + def test_default_system_is_cartesian_of_array_dimension(self): + coords = np.zeros((4, 2)) + cartesian = pcms.csys.Cartesian.create(2) + assert pcms.CoordinateView(coords).get_coordinate_system() == cartesian + assert (pcms.CoordinateView(None, coords).get_coordinate_system() + == cartesian) + + def test_system_must_match_array_dimension(self): + with pytest.raises(Exception): + pcms.CoordinateView(pcms.csys.CylindricalRThetaZ.create(), + np.zeros((4, 2))) + + +class TestMeshUtilities: + def test_average_from_strided_input(self, mesh): + nfaces = mesh.nfaces() + values = np.arange(nfaces, dtype=np.float64) + wide = np.stack([values, -values], axis=1) + contiguous = pcms.map_entity_field_to_vertices_average(mesh, values, 2) + strided = pcms.map_entity_field_to_vertices_average(mesh, wide[:, 0], + 2) + np.testing.assert_array_equal(strided, contiguous) + + def test_average_of_constant_is_constant(self, mesh): + values = np.full(mesh.nfaces(), 3.5) + result = pcms.map_entity_field_to_vertices_average(mesh, values, 2) + assert result.shape == (mesh.nverts(),) + assert not result.flags.writeable + np.testing.assert_allclose(result, 3.5) + + @pytest.mark.parametrize("mapping", [ + lambda mesh, v: pcms.map_entity_field_to_vertices_average(mesh, v, 2), + lambda mesh, v: pcms.map_entity_field_to_vertices_mls(mesh, v, 2), + ], ids=["average", "mls"]) + def test_multicomponent_matches_per_component(self, mesh, mapping): + centroids = pcms.compute_entity_centroids(mesh, 2) + values = np.stack([_linear(centroids[:, 0], centroids[:, 1]), + centroids[:, 0] - centroids[:, 1]], axis=1) + result = mapping(mesh, values) + assert result.shape == (mesh.nverts(), 2) + for c in range(2): + np.testing.assert_allclose( + result[:, c], mapping(mesh, values[:, c]), atol=1e-12) + + def test_single_component_2d_keeps_rank(self, mesh): + values = np.full((mesh.nfaces(), 1), 3.5) + result = pcms.map_entity_field_to_vertices_average(mesh, values, 2) + assert result.shape == (mesh.nverts(), 1) + np.testing.assert_allclose(result, 3.5) + + @pytest.mark.parametrize("shape", [ + lambda n: (n + 1,), lambda n: (n + 1, 2), lambda n: (n, 0), + lambda n: (n, 1, 1), + ], ids=["1d-wrong-entities", "2d-wrong-entities", "no-components", "3d"]) + def test_rejects_wrong_shape(self, mesh, shape): + with pytest.raises(ValueError): + pcms.map_entity_field_to_vertices_average( + mesh, np.zeros(shape(mesh.nfaces())), 2) + + def test_centroids_survive_garbage_collection(self, mesh): + centroids = pcms.compute_entity_centroids(mesh, 2) + expected = centroids.copy() + gc.collect() + np.testing.assert_array_equal(centroids, expected) + assert centroids.shape == (mesh.nfaces(), 2) + + +class TestUniformGrid: + def test_closest_cell_id_accepts_sequences(self, mesh): + grid = pcms.create_uniform_grid_from_mesh(mesh, [4, 4]) + expected = grid.closest_cell_id(np.array([0.3, 0.6])) + assert grid.closest_cell_id([0.3, 0.6]) == expected + assert grid.closest_cell_id(np.array([0.3, 0.6], np.float32)) == \ + expected + + def test_closest_cell_id_rejects_wrong_dimension(self, mesh): + grid = pcms.create_uniform_grid_from_mesh(mesh, [4, 4]) + with pytest.raises(ValueError): + grid.closest_cell_id(np.array([0.3, 0.6, 0.1])) diff --git a/test/python/test_omega_h_field.py b/test/python/test_omega_h_field.py new file mode 100644 index 000000000..09d432aae --- /dev/null +++ b/test/python/test_omega_h_field.py @@ -0,0 +1,154 @@ +""" +Field-centric tests for Omega_h-backed function spaces. +""" + +import numpy as np +import pytest +import pcms +import PyOmega_h as omega_h + +class TestOmegaHField: + """Tests for Omega_h-backed Field operations.""" + + @staticmethod + def _build_mesh(world, dim, nx=10): + """Build a box mesh of the requested dimension.""" + ny = nx if dim > 1 else 0 + nz = nx if dim > 2 else 0 + return omega_h.build_box( + world, omega_h.Family.SIMPLEX, 1.0, 1.0, 1.0, nx, ny, nz, False, + ) + + @pytest.mark.parametrize("dim, order, num_components", [ + (2, 1, 1), (2, 2, 1), + ]) + def test_field_methods(self, world, dim, order, num_components): + """Create an Omega_h-backed Field and exercise the public Field API.""" + mesh = self._build_mesh(world, dim) + factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components) + field = factory.create_function() + + assert field.get_layout().get_num_components() == num_components + assert field.get_layout().get_num_owned_dof_holder() > 0 + + coords = field.get_layout().get_dof_holder_coordinates().get_values() + assert coords.shape[0] == field.get_layout().get_num_owned_dof_holder() + assert coords.shape[1] == dim + + ndata = field.get_layout().get_num_owned_dof_holder() * num_components + test_data = np.arange(ndata, dtype=np.float64).reshape( + -1, num_components) + expected_data = test_data.copy() + field.set_dof_holder_data(test_data) + test_data.fill(-999.0) + np.testing.assert_allclose(field.get_dof_holder_data().get_values(), + expected_data) + + @pytest.mark.parametrize("dim, order, num_components", [ + (2, 1, 1), (2, 2, 1), + ]) + def test_field_transfer(self, world, dim, order, num_components): + """Transfer data between identical Omega_h-backed function spaces.""" + if num_components != 1: + pytest.skip("Multi-component transfer not yet supported") + + mesh = self._build_mesh(world, dim) + source_space = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components) + target_space = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components) + source = source_space.create_function() + target = target_space.create_function() + + coords = source.get_layout().get_dof_holder_coordinates().get_values() + num_owned = source.get_layout().get_num_owned_dof_holder() + source_data = np.zeros((num_owned, 1), dtype=np.float64) + for i in range(num_owned): + source_data[i, 0] = np.sum(coords[i, :dim]) + source.set_dof_holder_data(source_data) + + interp = pcms.Interpolator(source_space, target_space) + interp.apply(source, target) + np.testing.assert_allclose( + target.get_dof_holder_data().get_values(), source_data, atol=1e-14 + ) + + @pytest.mark.parametrize("dim, order, num_components", [ + (2, 1, 1), (2, 2, 1), + ]) + def test_field_evaluation(self, world, dim, order, num_components): + """Evaluate an Omega_h-backed field at explicit query points.""" + mesh = self._build_mesh(world, dim) + factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, order, num_components) + field = factory.create_function() + + mesh_coords = mesh.coords() + num_verts = mesh.nverts() + num_owned = field.get_layout().get_num_owned_dof_holder() + ndata = num_owned * num_components + + def test_func(x, y, z, component): + return np.sin(5.0 * x * y) + float(component) + + test_data = np.zeros(ndata, dtype=np.float64) + 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 + z = mesh_coords[i * dim + 2] if dim >= 3 else 0.0 + for c in range(num_components): + idx = i * num_components + c + test_data[idx] = test_func(x, y, z, c) + + 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 + test_data[idx] = float(c) + 0.5 + + field.set_dof_holder_data(test_data.reshape(num_owned, num_components)) + + 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], + ], dtype=np.float64) + 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], + ], dtype=np.float64) + + request = pcms.EvaluationRequest.from_coordinates( + pcms.CoordinateView(eval_coords)) + evaluator = factory.create_point_evaluator(request) + eval_values = np.zeros((eval_coords.shape[0], num_components), + dtype=np.float64) + evaluator.evaluate(field, eval_values) + + for i in range(eval_coords.shape[0]): + for c in range(num_components): + val = eval_values[i, c] + assert not np.isnan(val) + assert not np.isinf(val) + + @pytest.mark.parametrize("shape", [(4, 1), (6, 1), (5, 2)]) + def test_evaluate_rejects_wrong_output_shape(self, world, shape): + """A wrongly shaped output buffer raises instead of aborting.""" + mesh = self._build_mesh(world, 2) + factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 1) + field = factory.create_function() + eval_coords = np.full((5, 2), 0.5) + evaluator = factory.create_point_evaluator( + pcms.EvaluationRequest.from_coordinates( + pcms.CoordinateView(eval_coords))) + with pytest.raises(ValueError, match="must be \\[5\\]\\[1\\]"): + evaluator.evaluate(field, np.zeros(shape)) diff --git a/test/python/test_uniform_grid_field.py b/test/python/test_uniform_grid_field.py new file mode 100644 index 000000000..286297be0 --- /dev/null +++ b/test/python/test_uniform_grid_field.py @@ -0,0 +1,218 @@ +""" +Field-centric tests for uniform-grid-backed function spaces. +""" + +import numpy as np +import pytest +import pcms +import PyOmega_h as omega_h + + +def _coordinates(field): + return field.get_layout().get_dof_holder_coordinates().get_values() + + +def _num_dof_holders(field): + return field.get_layout().get_num_owned_dof_holder() + + +class TestUniformGridField: + """Tests for UniformGrid-backed Field operations (no Omega_h needed).""" + + def test_field_creation(self): + """Create a 2D field from a uniform-grid function space.""" + grid = pcms.UniformGrid2D() + grid.bot_left = [0.0, 0.0] + grid.edge_length = [10.0, 10.0] + grid.divisions = [4, 4] + + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( + grid, 1) + field = factory.create_function() + expected = (grid.divisions[0] + 1) * (grid.divisions[1] + 1) + assert field.get_layout().get_num_components() == 1 + assert _num_dof_holders(field) == expected + + def test_data_operations(self): + """Set and get flat DOF data through Field.""" + grid = pcms.UniformGrid2D() + grid.bot_left = [0.0, 0.0] + grid.edge_length = [10.0, 10.0] + grid.divisions = [2, 2] + + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( + grid, 1) + field = factory.create_function() + data = np.arange(_num_dof_holders(field), + dtype=np.float64).reshape(-1, 1) + expected_data = data.copy() + field.set_dof_holder_data(data) + data.fill(-999.0) + np.testing.assert_allclose(field.get_dof_holder_data().get_values(), + expected_data) + + def test_coordinates_2d(self): + """Expose 2D DOF-holder coordinates through Field.""" + grid = pcms.UniformGrid2D() + grid.bot_left = [0.0, 0.0] + grid.edge_length = [10.0, 10.0] + grid.divisions = [2, 3] + + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( + grid, 1) + field = factory.create_function() + coords = _coordinates(field) + expected = (grid.divisions[0] + 1) * (grid.divisions[1] + 1) + + assert isinstance(coords, np.ndarray) + assert coords.shape == (expected, 2) + np.testing.assert_allclose(coords[0], [0.0, 0.0]) + np.testing.assert_allclose(coords[-1], [10.0, 10.0]) + + def test_closest_cell(self): + """UniformGrid helpers remain available for grid setup.""" + grid = pcms.UniformGrid2D() + grid.bot_left = [0.0, 0.0] + grid.edge_length = [10.0, 10.0] + grid.divisions = [4, 4] + + 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])) + assert isinstance(cell_id, (int, np.integer)) + assert isinstance(cell_id_outside, (int, np.integer)) + + def test_coordinates_3d(self): + """Expose 3D DOF-holder coordinates and data through Field.""" + 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] + + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( + grid, 1) + field = factory.create_function() + coords = _coordinates(field) + expected = ( + (grid.divisions[0] + 1) + * (grid.divisions[1] + 1) + * (grid.divisions[2] + 1) + ) + assert coords.shape == (expected, 3) + + data = np.arange(expected, dtype=np.float64).reshape(-1, 1) + expected_data = data.copy() + field.set_dof_holder_data(data) + data.fill(-999.0) + np.testing.assert_allclose(field.get_dof_holder_data().get_values(), + expected_data) + + def test_field_evaluation(self): + """Evaluate a uniform-grid field at explicit query points.""" + grid = pcms.UniformGrid2D() + grid.bot_left = [0.0, 0.0] + grid.edge_length = [1.0, 1.0] + grid.divisions = [10, 10] + + factory = pcms.LagrangeFunctionSpace.from_uniform_grid( + grid, 1) + field = factory.create_function() + coords = _coordinates(field) + num_dofs = _num_dof_holders(field) + data = np.zeros(num_dofs, dtype=np.float64) + for i in range(num_dofs): + data[i] = coords[i, 0] + 2.0 * coords[i, 1] + field.set_dof_holder_data(data) + + query_pts = np.array( + [[0.1, 0.2], [0.5, 0.5], [0.9, 0.8]], dtype=np.float64 + ) + request = pcms.EvaluationRequest.from_coordinates( + pcms.CoordinateView(query_pts)) + evaluator = factory.create_point_evaluator(request) + results = np.zeros((len(query_pts), 1), dtype=np.float64) + evaluator.evaluate(field, results) + + for i, pt in enumerate(query_pts): + expected = pt[0] + 2.0 * pt[1] + assert np.abs(results[i, 0] - expected) < 1e-6 + + +class TestUniformGridOmegaHWorkflow: + """Tests combining UniformGrid with Omega_h meshes (needs world).""" + + def test_uniform_grid_to_omega_h_workflow(self, world): + """ + Test complete UniformGrid workflow with Omega_h mesh and field + interpolation (Omega_h → UniformGrid). + """ + # Create a simple 2D box mesh: 1.0 x 1.0 domain with 4x4 elements. + mesh = omega_h.build_box( + world, omega_h.Family.SIMPLEX, 1.0, 1.0, 0.0, 4, 4, 0, False, + ) + + grid = pcms.create_uniform_grid_from_mesh(mesh, [4, 4]) + mask_field = pcms.create_uniform_grid_binary_field(mesh, [4, 4]) + mask_data = mask_field.get_dof_holder_data().get_values() + + # Build a mesh-backed source field: f(x,y) = x + 2y. + omega_h_factory = pcms.LagrangeFunctionSpace.from_mesh( + mesh, 1, 1) + omega_h_field = omega_h_factory.create_function() + coords = _coordinates(omega_h_field) + omega_h_data = np.zeros(_num_dof_holders(omega_h_field), + dtype=np.float64) + for i in range(_num_dof_holders(omega_h_field)): + omega_h_data[i] = coords[i, 0] + 2.0 * coords[i, 1] + omega_h_field.set_dof_holder_data(omega_h_data) + + ug_factory = pcms.LagrangeFunctionSpace.from_uniform_grid( + grid, 1) + ug_field = ug_factory.create_function() + + # Transfer from unstructured mesh to uniform grid. + interp = pcms.Interpolator(omega_h_factory, ug_factory) + interp.apply(omega_h_field, ug_field) + + ug_field_data = ug_field.get_dof_holder_data().get_values() + ug_coords = _coordinates(ug_field) + + assert mask_data.shape == (25, 1) + assert ug_field_data.shape == (25, 1) + + for vertex_id in range(len(ug_field_data)): + x, y = ug_coords[vertex_id, 0], ug_coords[vertex_id, 1] + expected = x + 2.0 * y + actual = ug_field_data[vertex_id, 0] + assert abs(actual - expected) < 1e-10 + + def test_omega_h_to_omega_h_transfer(self, world): + """Transfer a field from one Omega_h mesh to another Omega_h mesh.""" + src_mesh = omega_h.build_box( + world, omega_h.Family.SIMPLEX, 1.0, 1.0, 0.0, 4, 4, 0, False, + ) + tgt_mesh = omega_h.build_box( + world, omega_h.Family.SIMPLEX, 1.0, 1.0, 0.0, 8, 8, 0, False, + ) + + src_factory = pcms.LagrangeFunctionSpace.from_mesh( + src_mesh, 1, 1) + tgt_factory = pcms.LagrangeFunctionSpace.from_mesh( + tgt_mesh, 1, 1) + src_field = src_factory.create_function() + tgt_field = tgt_factory.create_function() + + src_coords = _coordinates(src_field) + src_data = np.zeros(_num_dof_holders(src_field)) + 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) + + interp = pcms.Interpolator(src_factory, tgt_factory) + interp.apply(src_field, tgt_field) + + tgt_coords = _coordinates(tgt_field) + tgt_data = tgt_field.get_dof_holder_data().get_values() + for i in range(_num_dof_holders(tgt_field)): + expected = tgt_coords[i, 0] + 2.0 * tgt_coords[i, 1] + assert abs(expected - tgt_data[i, 0]) < 1e-10 + diff --git a/test/test_GDI.cpp b/test/test_GDI.cpp new file mode 100644 index 000000000..c2621501e --- /dev/null +++ b/test/test_GDI.cpp @@ -0,0 +1,138 @@ +#include +#include +#include "test_support.h" +#include "pcms/coupler/coupler.hpp" +#include +#include + +static constexpr bool done = true; +static constexpr int COMM_ROUNDS = 1; + +void xgc_delta_f(MPI_Comm comm) +{ + pcms::CouplerComm coupler("proxy_couple", comm, false, {}); + pcms::ApplicationComm* app = + coupler.AddApplication("proxy_couple_xgc_delta_f"); + + std::vector mean_storage(1); + auto mean = pcms::make_array_view(mean_storage); + + app->AddData("mean", mean, comm); + + mean[0] = 16; + + do { + for (int i = 0; i < COMM_ROUNDS; ++i) { + app->BeginSendPhase(); + app->SendData("mean"); + app->EndSendPhase(); + printf("delta Sent mean:%ld\n", mean[0]); + app->BeginReceivePhase(); + app->ReceiveData("mean"); + app->EndReceivePhase(); + mean[0] = mean[0] / 2; + } + } while (!done); + printf("final Mean = %ld\n", mean[0]); + assert(std::fabs(mean[0] - 1.0) < 1e-12); + printf("GDI test successful.\n"); +} +void xgc_total_f(MPI_Comm comm) +{ + pcms::CouplerComm coupler("proxy_couple", comm, false, {}); + pcms::ApplicationComm* app = + coupler.AddApplication("proxy_couple_xgc_total_f"); + + std::vector mean_storage(1); + auto mean = pcms::make_array_view(mean_storage); + app->AddData("mean", mean, comm); + + do { + for (int i = 0; i < COMM_ROUNDS; ++i) { + app->BeginReceivePhase(); + app->ReceiveData("mean"); + app->EndReceivePhase(); + printf("total Recieved mean:%ld\n", mean[0]); + mean[0] = mean[0] / 2; + app->BeginSendPhase(); + app->SendData("mean"); + app->EndSendPhase(); + printf("total Sent mean:%ld\n", mean[0]); + } + } while (!done); +} +void xgc_coupler(MPI_Comm comm) +{ + // Define Partition + redev::LO dim = 3; + redev::LOs ranks(1); + std::iota(ranks.begin(), ranks.end(), 0); + redev::Reals cuts = {0}; + auto partition = redev::Partition{redev::RCBPtn{dim, ranks, cuts}}; + + pcms::CouplerComm cpl("proxy_couple", comm, true, partition); + auto* total_f = cpl.AddApplication("proxy_couple_xgc_total_f"); + auto* delta_f = cpl.AddApplication("proxy_couple_xgc_delta_f"); + + std::vector mean_storage(1); + auto mean = pcms::make_array_view(mean_storage); + total_f->AddData("mean", mean, comm); + delta_f->AddData("mean", mean, comm); + + do { + for (int i = 0; i < COMM_ROUNDS; ++i) { + delta_f->BeginReceivePhase(); + delta_f->ReceiveData("mean"); + delta_f->EndReceivePhase(); + printf("delta Received mean:%ld\n", mean[0]); + mean[0] = mean[0] / 2; + total_f->BeginSendPhase(); + total_f->SendData("mean"); + total_f->EndSendPhase(); + printf("total sent mean:%ld\n", mean[0]); + total_f->BeginReceivePhase(); + total_f->ReceiveData("mean"); + total_f->EndReceivePhase(); + printf("delta Received mean:%ld\n", mean[0]); + mean[0] = mean[0] / 2; + delta_f->BeginSendPhase(); + delta_f->SendData("mean"); + delta_f->EndSendPhase(); + printf("delta sent mean:%ld\n", mean[0]); + } + } while (!done); +} + +int main(int argc, char** argv) +{ + try { + MPI_Init(&argc, &argv); + Kokkos::initialize(argc, argv); + OMEGA_H_CHECK(argc == 2); + + const auto clientId = std::atoi(argv[1]); + REDEV_ALWAYS_ASSERT(clientId >= -1 && clientId <= 1); + + MPI_Comm comm = MPI_COMM_WORLD; + + switch (clientId) { + case -1: xgc_coupler(comm); break; + + case 0: xgc_delta_f(comm); break; + + case 1: xgc_total_f(comm); break; + default: + std::cerr << "Unhandled client id; expected -1, 0, or 1\n"; + MPI_Abort(MPI_COMM_WORLD, EXIT_FAILURE); + } + + MPI_Finalize(); + 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\n"; + return 1; + } +} diff --git a/test/test_coordinate.cpp b/test/test_coordinate.cpp deleted file mode 100644 index 849ab1c6a..000000000 --- a/test/test_coordinate.cpp +++ /dev/null @@ -1,78 +0,0 @@ -#include -#include -#include -#include -#include "mdspan/mdspan.hpp" -#include - -using pcms::Cartesian; -using pcms::Coordinate; -using pcms::CoordinateElement; -using pcms::Cylindrical; -using pcms::Real; - -#if __cplusplus >= 202002L -// coordinates from different coordinate systems should not be equality -// comparable -static_assert(!std::equality_comparable_with, - Coordinate>); -#endif - -TEST_CASE("Coordinate Strong type works", "[coordinate]") -{ - Coordinate cart{1.0, 2.0, 3.0}; - Coordinate cyl{4.0, 5.0, 6.0}; - SECTION("Values()") - { - auto values = cart.Values(); - REQUIRE(values[0] == Catch::Approx(1.0)); - REQUIRE(values[1] == Catch::Approx(2.0)); - REQUIRE(values[2] == Catch::Approx(3.0)); - values = cyl.Values(); - REQUIRE(values[0] == Catch::Approx(4.0)); - REQUIRE(values[1] == Catch::Approx(5.0)); - REQUIRE(values[2] == Catch::Approx(6.0)); - } - SECTION("operator[]") - { - REQUIRE(cart[0] == Catch::Approx(1.0)); - REQUIRE(cart[1] == Catch::Approx(2.0)); - REQUIRE(cart[2] == Catch::Approx(3.0)); - REQUIRE(cyl[0] == Catch::Approx(4.0)); - REQUIRE(cyl[1] == Catch::Approx(5.0)); - REQUIRE(cyl[2] == Catch::Approx(6.0)); - } -} -/* -TEST_CASE( "Coordinate value type works", "[coordinate]" ) { - using CartVal = CoordinateElement ; - namespace stdex = std::experimental; - pcms::CoordinateMDArray a(2); - - Kokkos::mdarray< - double, - Kokkos::extents, - Kokkos::layout_right, Omega_h::Write > bla(2); - - double count = 1.0; - for(int i=0; i -#include - -TEST_CASE("cartesian to spherical") {} diff --git a/test/test_coordinate_transformation.cpp b/test/test_coordinate_transformation.cpp new file mode 100644 index 000000000..c1fb509e0 --- /dev/null +++ b/test/test_coordinate_transformation.cpp @@ -0,0 +1,403 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/field/coordinate_systems/cylindrical.hpp" +#include "field_test_utils.h" + +using Catch::Matchers::ContainsSubstring; +using Catch::Matchers::WithinAbs; +using pcms::BoundCoordinateMap; +using pcms::ComponentScaling; +using pcms::CoordinateView; +using pcms::DeviceMemorySpace; +using pcms::FieldValueType; +using pcms::HostMemorySpace; +using pcms::Real; +using pcms::SameCoordinateSystem; +using pcms::ValueBasis; +using pcms::ValueView; +using pcms::Variance; + +namespace +{ + +ValueBasis CylindricalVector() +{ + return ValueBasis{pcms::csys::CylindricalRThetaZ::Create(), + ComponentScaling::Physical, pcms::values::Vector}; +} + +ValueBasis CartesianVector() +{ + return ValueBasis{pcms::csys::Cartesian::Create(3), + ComponentScaling::Physical, pcms::values::Vector}; +} + +ValueBasis NaturalCylindricalVector() +{ + return ValueBasis{pcms::csys::CylindricalRThetaZ::Create(), + ComponentScaling::Natural, pcms::values::Vector}; +} + +} // namespace + +TEST_CASE("coordinate systems: structural equality") +{ + // Independently constructed systems of the same family and dimension are + // the same system: equality is structural, never pointer identity. + REQUIRE(SameCoordinateSystem(pcms::csys::Cartesian::Create(3), + pcms::csys::Cartesian::Create(3))); + REQUIRE(pcms::csys::Cartesian::Create(3) != pcms::csys::Cartesian::Create(3)); + REQUIRE(SameCoordinateSystem(pcms::csys::CylindricalRThetaZ::Create(), + pcms::csys::CylindricalRThetaZ::Create())); + // Distinct dimensions and families are distinct systems. + REQUIRE_FALSE(SameCoordinateSystem(pcms::csys::Cartesian::Create(2), + pcms::csys::Cartesian::Create(3))); + REQUIRE_FALSE(SameCoordinateSystem(pcms::csys::CylindricalRZ::Create(), + pcms::csys::CylindricalRThetaZ::Create())); + REQUIRE(pcms::csys::Cartesian::Create(2)->Dimension() == 2); + REQUIRE(pcms::csys::CylindricalRZ::Create()->Dimension() == 2); + REQUIRE(pcms::csys::CylindricalRThetaZ::Create()->Dimension() == 3); + + // The analytic orthogonal coordinate systems are orthogonal + REQUIRE(pcms::csys::Cartesian::Create(3)->HasOrthogonalBasis()); + REQUIRE(pcms::csys::CylindricalRThetaZ::Create()->HasOrthogonalBasis()); +} + +TEST_CASE("coordinate systems: ResolveCoordinateSystem resolves the deferred " + "Cartesian placeholder from the data dimension") +{ + const auto placeholder = pcms::csys::Cartesian::Deferred(); + REQUIRE(SameCoordinateSystem(pcms::ResolveCoordinateSystem(placeholder, 2), + pcms::csys::Cartesian::Create(2))); + REQUIRE(SameCoordinateSystem(pcms::ResolveCoordinateSystem(placeholder, 3), + pcms::csys::Cartesian::Create(3))); + // A concrete coordinate system validates against the data dimension instead. + const auto concrete = pcms::csys::CylindricalRThetaZ::Create(); + REQUIRE(SameCoordinateSystem(pcms::ResolveCoordinateSystem(concrete, 3), + pcms::csys::CylindricalRThetaZ::Create())); + REQUIRE_THROWS_WITH(pcms::ResolveCoordinateSystem(concrete, 2), + ContainsSubstring("coordinate columns")); + REQUIRE_THROWS_WITH(pcms::ResolveCoordinateSystem(nullptr, 2), + ContainsSubstring("null")); + // A Cartesian that already states its dimension is validated like any + // other system, not silently rewritten to the data's dimension. + REQUIRE_THROWS_WITH( + pcms::ResolveCoordinateSystem(pcms::csys::Cartesian::Create(3), 2), + ContainsSubstring("coordinate columns")); + // The unresolved placeholder's Kind self-explains in error messages. + REQUIRE(placeholder->Kind() == "cartesian (dimension deferred)"); + // Deferred() is the only route to a dimension-0 Cartesian. + REQUIRE_THROWS_WITH(pcms::csys::Cartesian::Create(0), + ContainsSubstring("1..3")); + REQUIRE_THROWS_WITH(pcms::csys::Cartesian::Create(4), + ContainsSubstring("1..3")); +} + +TEST_CASE("CoordinateView validates the dimension; family form derives it") +{ + auto pts2 = pcms::test::CreateDeviceCoordinateView( + {0.0, 1.0, 2.0, 3.0}, pcms::csys::Cartesian::Deferred(), 2); + // Family form: dimension from extent(1). + CoordinateView v(pcms::csys::Cartesian::Deferred(), + pcms::MakeConstRank2View(pts2.view)); + REQUIRE(SameCoordinateSystem(v.GetCoordinateSystem(), + pcms::csys::Cartesian::Create(2))); + // Mis-declared buffer fails at its entry point. + REQUIRE_THROWS_WITH( + CoordinateView(pcms::csys::CylindricalRThetaZ::Create(), + pcms::MakeConstRank2View(pts2.view)), + ContainsSubstring("coordinate columns")); +} + +TEST_CASE("CartesianToCylindrical: Bind produces known values") +{ + const std::vector xyz = {1.0, 0.0, 0.5, // + 0.0, 2.0, -1.0, // + -3.0, 0.0, 2.0, // + 1.0, 1.0, 0.0}; + auto data = pcms::test::CreateDeviceRank2View(xyz, 3); + const pcms::CartesianToCylindrical map; + REQUIRE(SameCoordinateSystem(map.GetSourceCoordinateSystem(), + pcms::csys::Cartesian::Create(3))); + REQUIRE(SameCoordinateSystem(map.GetTargetCoordinateSystem(), + pcms::csys::CylindricalRThetaZ::Create())); + + const auto bound = + map.Bind(pcms::test::MakeCoords(data, pcms::csys::Cartesian::Create(3))); + REQUIRE(SameCoordinateSystem(bound->MappedPoints().GetCoordinateSystem(), + pcms::csys::CylindricalRThetaZ::Create())); + REQUIRE(bound->NumPoints() == 4); + + auto out = + pcms::test::CopyCoordinatesToHost(bound->MappedPoints().GetValues()); + for (size_t i = 0; i < 4; ++i) { + const Real x = xyz[3 * i], y = xyz[3 * i + 1], z = xyz[3 * i + 2]; + CAPTURE(i); + REQUIRE_THAT(out(i, 0), + WithinAbs(std::sqrt(x * x + y * y), pcms::test::ExactTol)); + REQUIRE_THAT(out(i, 1), WithinAbs(std::atan2(y, x), pcms::test::ExactTol)); + REQUIRE_THAT(out(i, 2), WithinAbs(z, pcms::test::ExactTol)); + } + + // Values run opposite to points: a cylindrical field becomes Cartesian. + REQUIRE(pcms::SameValueBasis(bound->OutputBasis(CylindricalVector()), + CartesianVector())); +} + +TEST_CASE("round trip via sequential Bind calls") +{ + const std::vector xyz = {0.3, -0.4, 1.0, 2.0, 1.0, -0.5, 0.0, 0.0, 3.0}; + auto data = pcms::test::CreateDeviceRank2View(xyz, 3); + const auto stage1 = pcms::CartesianToCylindrical{}.Bind( + pcms::test::MakeCoords(data, pcms::csys::Cartesian::Create(3))); + const auto stage2 = + pcms::CylindricalToCartesian{}.Bind(stage1->MappedPoints()); + auto out = + pcms::test::CopyCoordinatesToHost(stage2->MappedPoints().GetValues()); + for (size_t i = 0; i < xyz.size() / 3; ++i) { + for (int d = 0; d < 3; ++d) { + REQUIRE_THAT(out(i, d), WithinAbs(xyz[3 * i + d], pcms::test::ExactTol)); + } + } +} + +TEST_CASE("Bind returns owned output and completes before returning") +{ + const std::vector xyz = {1.0, 0.0, 3.0}; + auto data = pcms::test::CreateDeviceRank2View(xyz, 3); + const auto bound = pcms::CartesianToCylindrical{}.Bind( + pcms::test::MakeCoords(data, pcms::csys::Cartesian::Create(3))); + // Clobber the caller's buffer after Bind; the owned output stands. + Kokkos::deep_copy(data, -99.0); + auto out = + pcms::test::CopyCoordinatesToHost(bound->MappedPoints().GetValues()); + REQUIRE_THAT(out(0, 0), WithinAbs(1.0, pcms::test::ExactTol)); + REQUIRE_THAT(out(0, 1), WithinAbs(0.0, pcms::test::ExactTol)); + REQUIRE_THAT(out(0, 2), WithinAbs(3.0, pcms::test::ExactTol)); +} + +TEST_CASE("one map instance serves multiple point sets") +{ + const pcms::CartesianToCylindrical map; + auto a = pcms::test::CreateDeviceRank2View({1.0, 0.0, 0.0}, 3); + auto b = + pcms::test::CreateDeviceRank2View({0.0, 2.0, 1.0, -3.0, 0.0, 2.0}, 3); + const auto bound_a = + map.Bind(pcms::test::MakeCoords(a, pcms::csys::Cartesian::Create(3))); + const auto bound_b = + map.Bind(pcms::test::MakeCoords(b, pcms::csys::Cartesian::Create(3))); + REQUIRE(bound_a->NumPoints() == 1); + REQUIRE(bound_b->NumPoints() == 2); + auto out_a = + pcms::test::CopyCoordinatesToHost(bound_a->MappedPoints().GetValues()); + auto out_b = + pcms::test::CopyCoordinatesToHost(bound_b->MappedPoints().GetValues()); + REQUIRE_THAT(out_a(0, 1), WithinAbs(0.0, pcms::test::ExactTol)); + REQUIRE_THAT(out_b(0, 1), WithinAbs(M_PI / 2.0, pcms::test::ExactTol)); + REQUIRE_THAT(out_b(1, 1), WithinAbs(M_PI, pcms::test::ExactTol)); +} + +TEST_CASE("Bind rejects a source system that is not the map's own") +{ + const std::vector pts = {1.0, 0.0, 0.0}; + auto data = pcms::test::CreateDeviceRank2View(pts, 3); + REQUIRE_THROWS_WITH( + pcms::CartesianToCylindrical{}.Bind( + pcms::test::MakeCoords(data, pcms::csys::CylindricalRThetaZ::Create())), + ContainsSubstring("source coordinate system")); +} + +TEST_CASE("bound map: manufactured pushforward rotates vector components") +{ + // Radial unit vectors in cylindrical components must become + // (cos theta, sin theta, 0) in Cartesian components. + const std::vector rtz = {1.0, 0.0, 0.0, // + 2.0, M_PI / 2.0, 1.0, // + 0.5, M_PI / 4.0, -1.0}; + std::vector xyz(rtz.size()); + for (size_t i = 0; i < 3; ++i) { + xyz[3 * i] = rtz[3 * i] * std::cos(rtz[3 * i + 1]); + xyz[3 * i + 1] = rtz[3 * i] * std::sin(rtz[3 * i + 1]); + xyz[3 * i + 2] = rtz[3 * i + 2]; + } + auto data = pcms::test::CreateDeviceRank2View(xyz, 3); + const auto bound = pcms::CartesianToCylindrical{}.Bind( + pcms::test::MakeCoords(data, pcms::csys::Cartesian::Create(3))); + REQUIRE(bound->NumPoints() == 3); + + auto values = pcms::test::CreateDeviceRank2View( + {1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0}, 3); + const auto result = bound->TransformValues(ValueView( + CylindricalVector(), pcms::MakeRank2View(values))); + REQUIRE(pcms::SameValueBasis(result.GetBasis(), CartesianVector())); + REQUIRE(result.GetValues().data_handle() == values.data()); + auto out = + pcms::test::CopyCoordinatesToHost(pcms::MakeConstRank2View(values)); + for (size_t i = 0; i < 3; ++i) { + const Real theta = rtz[3 * i + 1]; + CAPTURE(i); + REQUIRE_THAT(out(i, 0), WithinAbs(std::cos(theta), pcms::test::ExactTol)); + REQUIRE_THAT(out(i, 1), WithinAbs(std::sin(theta), pcms::test::ExactTol)); + REQUIRE_THAT(out(i, 2), WithinAbs(0.0, pcms::test::ExactTol)); + } +} + +TEST_CASE("bound map: points on the axis rotate by the angle they map to") +{ + auto data = pcms::test::CreateDeviceRank2View({0.0, 0.0, 2.0}, 3); + const auto bound = pcms::CartesianToCylindrical{}.Bind( + pcms::test::MakeCoords(data, pcms::csys::Cartesian::Create(3))); + auto mapped = + pcms::test::CopyCoordinatesToHost(bound->MappedPoints().GetValues()); + REQUIRE_THAT(mapped(0, 0), WithinAbs(0.0, pcms::test::ExactTol)); + const Real theta = mapped(0, 1); + + const std::vector c = {0.3, -1.2, 0.7}; + auto values = pcms::test::CreateDeviceRank2View( + {c[0] * std::cos(theta) + c[1] * std::sin(theta), + -c[0] * std::sin(theta) + c[1] * std::cos(theta), c[2]}, + 3); + const auto result = bound->TransformValues(ValueView( + CylindricalVector(), pcms::MakeRank2View(values))); + REQUIRE(pcms::SameValueBasis(result.GetBasis(), CartesianVector())); + auto out = + pcms::test::CopyCoordinatesToHost(pcms::MakeConstRank2View(values)); + for (size_t d = 0; d < 3; ++d) { + REQUIRE_THAT(out(0, d), WithinAbs(c[d], pcms::test::ExactTol)); + } +} + +TEST_CASE("bound map: the two cylindrical maps invert each other") +{ + const Real theta = 0.7; + const std::vector cart = {2.0 * std::cos(theta), 2.0 * std::sin(theta), + 0.3}; + const std::vector cyl = {2.0, theta, 0.3}; + auto cart_data = pcms::test::CreateDeviceRank2View(cart, 3); + auto cyl_data = pcms::test::CreateDeviceRank2View(cyl, 3); + const auto to_cart = pcms::CartesianToCylindrical{}.Bind( + pcms::test::MakeCoords(cart_data, pcms::csys::Cartesian::Create(3))); + const auto to_cyl = pcms::CylindricalToCartesian{}.Bind( + pcms::test::MakeCoords(cyl_data, pcms::csys::CylindricalRThetaZ::Create())); + + const std::vector v = {0.0, 1.0, 0.0}; // theta-direction unit vector + auto values = pcms::test::CreateDeviceRank2View(v, 3); + const auto as_cart = + to_cart->TransformValues(ValueView( + CylindricalVector(), pcms::MakeRank2View(values))); + auto a = pcms::test::CopyCoordinatesToHost(pcms::MakeConstRank2View(values)); + REQUIRE_THAT(a(0, 0), WithinAbs(-std::sin(theta), pcms::test::ExactTol)); + REQUIRE_THAT(a(0, 1), WithinAbs(std::cos(theta), pcms::test::ExactTol)); + + const auto round_trip = to_cyl->TransformValues(as_cart); + REQUIRE(pcms::SameValueBasis(round_trip.GetBasis(), CylindricalVector())); + auto b = pcms::test::CopyCoordinatesToHost(pcms::MakeConstRank2View(values)); + for (int d = 0; d < 3; ++d) { + REQUIRE_THAT(b(0, d), WithinAbs(v[d], pcms::test::ExactTol)); + } +} + +TEST_CASE("bound map: TransformValues validates the values it is given") +{ + const std::vector xyz = {1.0, 0.5, 0.0}; + auto data = pcms::test::CreateDeviceRank2View(xyz, 3); + const auto bound = pcms::CartesianToCylindrical{}.Bind( + pcms::test::MakeCoords(data, pcms::csys::Cartesian::Create(3))); + + SECTION("a basis the map cannot re-express") + { + auto values = pcms::test::CreateDeviceRank2View({1.0, 0.0, 0.0}, 3); + REQUIRE_THROWS_WITH( + bound->TransformValues(ValueView( + NaturalCylindricalVector(), pcms::MakeRank2View(values))), + ContainsSubstring("cannot re-express")); + REQUIRE_THROWS_WITH(bound->OutputBasis(NaturalCylindricalVector()), + ContainsSubstring("cannot re-express")); + } + SECTION("values already in the output basis pass through unchanged") + { + auto values = pcms::test::CreateDeviceRank2View({0.3, -0.4, 0.5}, 3); + REQUIRE(pcms::SameValueBasis(bound->OutputBasis(CartesianVector()), + CartesianVector())); + const auto result = + bound->TransformValues(ValueView( + CartesianVector(), pcms::MakeRank2View(values))); + REQUIRE(pcms::SameValueBasis(result.GetBasis(), CartesianVector())); + auto out = + pcms::test::CopyCoordinatesToHost(pcms::MakeConstRank2View(values)); + REQUIRE(out(0, 0) == 0.3); + REQUIRE(out(0, 1) == -0.4); + REQUIRE(out(0, 2) == 0.5); + } + SECTION("a value count that is not the bound point count") + { + auto values = + pcms::test::CreateDeviceRank2View({1.0, 0.0, 0.0, 0.0, 1.0, 0.0}, 3); + REQUIRE_THROWS_WITH( + bound->TransformValues(ValueView( + CylindricalVector(), pcms::MakeRank2View(values))), + ContainsSubstring("value count does not match")); + } +} + +TEST_CASE("a points-only bound map cannot re-express component values") +{ + auto data = pcms::test::CreateDeviceRank2View({1.0, 2.0, 3.0}, 3); + Kokkos::View mapped("mapped", 1, 3); + const BoundCoordinateMap bound(pcms::csys::Cartesian::Create(3), mapped); + REQUIRE(bound.NumPoints() == 1); + REQUIRE_THROWS_WITH(bound.OutputBasis(CartesianVector()), + ContainsSubstring("maps points only")); +} + +TEST_CASE("ValueView validates component counts and basis") +{ + auto data = pcms::test::CreateDeviceRank2View({1.0, 2.0}, 2); + // A rank-1 value on a 3-dimensional system needs 3 components. + REQUIRE_THROWS_WITH( + (ValueView( + ValueBasis{pcms::csys::Cartesian::Create(3), ComponentScaling::Physical, + pcms::values::Vector}, + pcms::MakeConstRank2View(data))), + ContainsSubstring("require 3 components")); + // Component count comes from the system's dimension: a 2-dimensional system + // takes 2. + REQUIRE_NOTHROW(ValueView( + ValueBasis{pcms::csys::CylindricalRZ::Create(), ComponentScaling::Physical, + pcms::values::Vector}, + pcms::MakeConstRank2View(data))); + // Non-scalar values require a basis coordinate system. + auto data3 = pcms::test::CreateDeviceRank2View({1.0, 2.0, 3.0}, 3); + REQUIRE_THROWS_WITH( + (ValueView( + ValueBasis{nullptr, ComponentScaling::Physical, pcms::values::Vector}, + pcms::MakeConstRank2View(data3))), + ContainsSubstring("basis coordinate system")); + // Physical components are undefined on a non-orthogonal basis, and rank 0 + // (an empty signature) ignores the basis at any component count. + REQUIRE_NOTHROW(ValueView( + ValueBasis{}, pcms::MakeConstRank2View(data))); +} + +TEST_CASE("value declarations reject rank > 2") +{ + std::vector flat(27, 0.0); + auto data = pcms::test::CreateDeviceRank2View(flat, 27); + REQUIRE_THROWS_WITH( + (ValueView( + ValueBasis{ + pcms::csys::Cartesian::Create(3), ComponentScaling::Physical, + pcms::values::Of({Variance::Contravariant, Variance::Contravariant, + Variance::Contravariant})}, + pcms::MakeConstRank2View(data))), + ContainsSubstring("rank-3")); +} diff --git a/test/test_eqdsk.cpp b/test/test_eqdsk.cpp index 401505744..7096c9096 100644 --- a/test/test_eqdsk.cpp +++ b/test/test_eqdsk.cpp @@ -2,13 +2,15 @@ #include #include #include -#include +#include #include #include #include #include +#include "field_test_utils.h" #include #include +#include "pcms/field/coordinate_systems/cylindrical.hpp" using Catch::Approx; using pcms::CoordinateSystem; @@ -182,10 +184,10 @@ TEST_CASE("EQDSKData with SplineFunctionSpace") { // auto spline_space = pcms::SplineFunctionSpace::FromEQDSK(eqdsk_data); - // auto psi_field = spline_space.CreateField(); + // auto psi_field = spline_space->CreateFunction(); auto [spline_space, psi_field] = pcms::MakeEQDSKField(eqdsk_data); - auto layout = spline_space.GetLayout(); + auto layout = spline_space->GetLayout(); const int layout_size = layout->OwnedSize(); const int psizr_size = static_cast(eqdsk_data.PSIZR.extent(0)); @@ -212,42 +214,25 @@ TEST_CASE("EQDSKData with SplineFunctionSpace") const int num_eval_points = 3; - auto eval_coords_host = Kokkos::View( - "eval_coords_host", num_eval_points, 2); - for (size_t i = 0; i < num_eval_points; ++i) { - eval_coords_host(i, 0) = eval_coords[2 * i]; // R - eval_coords_host(i, 1) = eval_coords[2 * i + 1]; // Z - } - - auto eval_coords_device = - Kokkos::View("eval_coords_device", - num_eval_points, 2); - pcms::DeepCopyMismatchLayouts(eval_coords_device, eval_coords_host); - - auto coords_view = pcms::MakeRank2View(eval_coords_device); - auto coord_view = pcms::CoordinateView{ - CoordinateSystem::Cartesian, coords_view}; - auto eval_request = pcms::EvaluationRequest::FromCoordinates(coord_view); + auto device_coords = pcms::test::CreateDeviceCoordinateView( + eval_coords, pcms::csys::CylindricalRZ::Create()); + auto eval_request = + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view); auto evaluator = - spline_space.CreatePointEvaluator(eval_request); - - auto eval_results_1d = Kokkos::View( - "eval_results", num_eval_points); - - using LayoutPolicy = - pcms::detail::default_layout_for_memory_space_t; - pcms::Rank2View - eval_results(eval_results_1d.data(), num_eval_points, 1); + spline_space->CreatePointEvaluator(eval_request); - evaluator->Evaluate(psi_field, eval_results); + Kokkos::View eval_results( + "eval_results", num_eval_points, 1); + evaluator->Evaluate(psi_field, + pcms::test::TagLike(psi_field, eval_results)); auto results_host = Kokkos::create_mirror_view_and_copy( - pcms::HostMemorySpace(), eval_results_1d); + pcms::HostMemorySpace(), eval_results); // Verify that all results are finite for (int i = 0; i < num_eval_points; ++i) { - REQUIRE(std::isfinite(results_host(i))); + REQUIRE(std::isfinite(results_host(i, 0))); } } } diff --git a/test/test_evaluation_only_space.cpp b/test/test_evaluation_only_space.cpp new file mode 100644 index 000000000..5440ee5da --- /dev/null +++ b/test/test_evaluation_only_space.cpp @@ -0,0 +1,189 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "field_test_utils.h" + +using Catch::Matchers::ContainsSubstring; +using Catch::Matchers::WithinAbs; +using pcms::DeviceMemorySpace; +using pcms::LO; +using pcms::Real; +using pcms::ValueBasis; + +// An evaluation-only source, the pattern external codes (NIMROD, Fusion-IO) +// follow: a FunctionSpace on an EmptyFieldLayout whose field data names a +// quantity but provides no DOFAccess. Here the quantity is analytic: +// component c of f at (R, theta, Z) is 2R + 3 theta - Z + c. +namespace +{ + +KOKKOS_INLINE_FUNCTION Real MockValue(Real r, Real theta, Real z, int c) +{ + return 2.0 * r + 3.0 * theta - z + c; +} + +class MockData final : public pcms::FieldData +{ +public: + pcms::FieldValueType GetValueType() const override + { + return pcms::FieldValueType::Scalar; + } + const ValueBasis& GetValueBasis() const override { return basis_; } + +private: + ValueBasis basis_; +}; + +class MockEvaluator final : public pcms::PointEvaluator +{ +public: + explicit MockEvaluator(const pcms::EvaluationRequest& request) + : points_("mock_points", request.coords.GetValues().extent(0), + request.coords.GetValues().extent(1)) + { + pcms::CopyDeviceRank2ViewToRank2View(pcms::MakeRank2View(points_), + request.coords.GetValues()); + } + + void Evaluate(const pcms::Field& field, + pcms::ValueView values) const override + { + CheckEvaluateWriteTag("MockEvaluator", field, values); + if (dynamic_cast(&field.GetData()) == nullptr) { + throw pcms::pcms_error("MockEvaluator: not a field of this source"); + } + const auto out = values.GetValues(); + const auto points = points_; + Kokkos::parallel_for( + Kokkos::RangePolicy( + 0, static_cast(points.extent(0))), + KOKKOS_LAMBDA(const LO i) { + for (size_t c = 0; c < out.extent(1); ++c) { + out(i, c) = + MockValue(points(i, 0), points(i, 1), points(i, 2), int(c)); + } + }); + } + + Kokkos::View FilledPoints() const override + { + return {}; + } + +private: + Kokkos::View points_; +}; + +class MockSpace final : public pcms::FunctionSpace +{ +public: + static std::shared_ptr Create(int num_components) + { + return std::shared_ptr(new MockSpace(num_components)); + } + + std::shared_ptr GetLayout() const noexcept override + { + return layout_; + } + +protected: + pcms::FieldVariant CreateFieldImpl(pcms::Type, ValueBasis) const override + { + throw pcms::pcms_error("MockSpace: pass the data naming the quantity"); + } + + pcms::FieldVariant CreateFieldImpl(pcms::FieldDataVariant data) const override + { + auto& real = std::get>>(data); + if (dynamic_cast(real.get()) == nullptr) { + throw pcms::pcms_error("MockSpace: requires MockData"); + } + return WrapField(layout_, std::move(real)); + } + + pcms::PointEvaluatorVariant CreatePointEvaluatorImpl( + pcms::Type, const pcms::EvaluationRequest& request) const override + { + return std::unique_ptr>( + std::make_unique(request)); + } + +private: + explicit MockSpace(int num_components) + : layout_(std::make_shared( + pcms::csys::CylindricalRThetaZ::Create(), num_components)) + { + } + + std::shared_ptr layout_; +}; + +std::shared_ptr MakePoloidalTarget( + Omega_h::Mesh& mesh, int num_components) +{ + return pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, num_components, pcms::csys::CylindricalRZ::Create(), "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); +} + +} // namespace + +TEST_CASE("Evaluation-only space interpolates onto a poloidal plane") +{ + auto lib = Omega_h::Library{}; + auto mesh = pcms::test::BuildUnitSquare(lib, 1); + const int num_components = GENERATE(1, 2); + auto source = MockSpace::Create(num_components); + auto target = MakePoloidalTarget(mesh, num_components); + const Real theta0 = -0.3; + pcms::TransformedSource on_plane( + source, std::make_shared(theta0)); + pcms::Interpolator interpolator(on_plane, *target); + + auto f = source->CreateFunction("f", std::make_unique()); + auto g = target->CreateFunction("g"); + interpolator.Apply(f, g); + + const auto coords = pcms::test::CopyCoordinatesToHost( + target->GetLayout()->GetDOFHolderCoordinates().GetValues()); + const auto result = g.GetDOFHolderDataHost().GetValues(); + for (size_t i = 0; i < coords.extent(0); ++i) { + for (int c = 0; c < num_components; ++c) { + CAPTURE(i, c); + REQUIRE_THAT(result(i, c), + WithinAbs(MockValue(coords(i, 0), theta0, coords(i, 1), c), + pcms::test::ExactTol)); + } + } + + // The embedding maps points only, so vector values cannot pass through it. + auto evaluator = on_plane.CreatePointEvaluator( + pcms::EvaluationRequest::FromFunctionSpace(*target)); + REQUIRE_THROWS_WITH( + evaluator->OutputBasis(ValueBasis{pcms::csys::CylindricalRThetaZ::Create(), + pcms::ComponentScaling::Physical, + pcms::values::Vector}), + ContainsSubstring("cannot re-express")); +} + +TEST_CASE("Functions of an evaluation-only space hold no values") +{ + auto source = MockSpace::Create(1); + auto f = source->CreateFunction("f", std::make_unique()); + REQUIRE(f.GetLayout().GetNumOwnedDofHolder() == 0); + REQUIRE_THROWS_WITH(f.GetDOFHolderData(), ContainsSubstring("no DOFAccess")); + REQUIRE_THROWS_WITH(source->CreateFunction("default"), + ContainsSubstring("naming the quantity")); +} diff --git a/test/test_field_communication.cpp b/test/test_field_communication.cpp index 83b8bc5b8..2a74e0602 100644 --- a/test/test_field_communication.cpp +++ b/test/test_field_communication.cpp @@ -11,9 +11,10 @@ #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/value_view.hpp" #include "pcms/field/data/simple.h" #include "test_support.h" +#include "pcms/field/coordinate_systems/cartesian.hpp" namespace ts = test_support; using pcms::Real; @@ -74,19 +75,16 @@ static void test_shared_layout(Omega_h::Library& lib, if (is_server) { auto partition = setupServerPartition(mesh, cpn_file); - pcms::Coupler cpl("shared_layout_server", mpi_comm, true, - redev::Partition{partition}); + pcms::CouplerComm 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); + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "shared"); + auto f1 = app->AddField(factory->CreateFunction("field_a")); 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 + auto f2 = app->AddField(factory->CreateFunction("field_b")); + PCMS_ALWAYS_ASSERT(app->GetLayoutCommunicatorCount() == 1); // shared layout app->ReceivePhase([&]() { f1.Receive(); f2.Receive(); @@ -96,15 +94,14 @@ static void test_shared_layout(Omega_h::Library& lib, f2.Send(); }); } else { - pcms::Coupler cpl("shared_layout_client", mpi_comm, false, - redev::Partition{}); + pcms::CouplerComm 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()); + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "shared"); + auto f1 = app->AddField(factory->CreateFunction("field_a")); + auto f2 = app->AddField(factory->CreateFunction("field_b")); app->SendPhase([&]() { f1.Send(); f2.Send(); @@ -124,8 +121,8 @@ void client1(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, rdv.CreateAdiosChannel("field2_chan1", params, redev::TransportType::BP4); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, order, 1, pcms::CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); + mesh, order, 1, pcms::csys::Cartesian::Deferred()); + auto layout = factory->GetLayout(); auto gids = layout->GetGidsHost(); const auto n = layout->GetNumOwnedDofHolder(); Omega_h::HostWrite ids(n); @@ -134,8 +131,8 @@ 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 = factory.CreateField(pcms::FieldMetadata{}); - field.SetDOFHolderDataHost( + auto field = factory->CreateFunction(); + field.SetDOFHolderDataUncheckedHost( pcms::Rank2View(ids.data(), n, 1)); pcms::FieldLayoutCommunicator layout_comm(comm_name + "1", comm, rdv, channel, @@ -156,12 +153,12 @@ void client2(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, rdv.CreateAdiosChannel("field2_chan2", params, redev::TransportType::BP4); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, order, 1, pcms::CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); + mesh, order, 1, pcms::csys::Cartesian::Deferred()); + auto layout = factory->GetLayout(); auto gids = layout->GetGidsHost(); const auto n = layout->GetNumOwnedDofHolder(); - auto field = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::FieldLayoutCommunicator layout_comm(comm_name + "2", comm, rdv, channel, *layout); pcms::FieldCommunicator field_comm(layout_comm.GetName(), @@ -171,7 +168,8 @@ void client2(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, field_comm.Receive(); channel.EndReceiveCommunicationPhase(); - auto copied_array = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto copied_array = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); auto owned = layout->GetOwnedHost(); PCMS_ALWAYS_ASSERT(copied_array.size() == gids.size()); @@ -216,15 +214,15 @@ void server(MPI_Comm comm, Omega_h::Mesh& mesh, std::string comm_name, rdv.CreateAdiosChannel("field2_chan2", params, redev::TransportType::BP4); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, order, 1, pcms::CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); + mesh, order, 1, pcms::csys::Cartesian::Deferred()); + 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 = factory.CreateField(pcms::FieldMetadata{}); + auto field = factory->CreateFunction(); pcms::FieldLayoutCommunicator layout_comm1(comm_name + "1", comm, rdv, channel1, *layout); pcms::FieldLayoutCommunicator layout_comm2(comm_name + "2", comm, rdv, @@ -261,7 +259,9 @@ int main(int argc, char** argv) const auto classPartitionFile = argv[3]; Omega_h::Mesh mesh = Omega_h::binary::read(meshFile, world); - adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; + adios2::Params params{{"Streaming", "On"}, + {"OpenTimeoutSecs", "60"}, + {"BeginStepPollingFrequencySecs", "0.05"}}; MPI_Comm mpi_comm = lib.world()->get_impl(); switch (clientId) { diff --git a/test/test_field_copy.cpp b/test/test_field_copy.cpp index 9906bbbcb..754371c61 100644 --- a/test/test_field_copy.cpp +++ b/test/test_field_copy.cpp @@ -5,9 +5,10 @@ #include #include "pcms/transfer/copy.h" #include "pcms/field/function_space/lagrange.h" -#include "pcms/field/field_metadata.h" +#include "pcms/field/value_view.hpp" #include "pcms/utility/assert.h" #include +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::Real; @@ -20,24 +21,25 @@ 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 factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, order, num_components, pcms::CoordinateSystem::Cartesian, "global", + mesh, order, num_components, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto layout = factory.GetLayout(); + 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 = factory.CreateField(pcms::FieldMetadata{}); - original.SetDOFHolderDataHost( + auto original = factory->CreateFunction(); + original.SetDOFHolderDataUncheckedHost( pcms::Rank2View( ids.data(), layout->GetNumOwnedDofHolder(), num_components)); - auto copied = factory.CreateField(pcms::FieldMetadata{}); - pcms::Copy copy(factory, factory); + auto copied = factory->CreateFunction(); + pcms::Copy copy(*factory, *factory); copy.Apply(original, copied); - auto copied_array = pcms::FlattenToRank1View(copied.GetDOFHolderDataHost()); + auto copied_array = + pcms::FlattenToRank1View(copied.GetDOFHolderDataHost().GetValues()); REQUIRE(copied_array.size() == ndata); int sum = 0; @@ -58,7 +60,7 @@ TEST_CASE("copy omega_h_field2 data") 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", + mesh, 2, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH), pcms::pcms_error); } diff --git a/test/test_field_evaluation.cpp b/test/test_field_evaluation.cpp index c4a293781..85a05a96e 100644 --- a/test/test_field_evaluation.cpp +++ b/test/test_field_evaluation.cpp @@ -3,10 +3,13 @@ #include #include #include "pcms/field/function_space/lagrange.h" -#include "pcms/field/field_metadata.h" +#include "pcms/field/value_view.hpp" #include "pcms/utility/assert.h" #include "field_test_utils.h" +#include #include +#include +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::Real; @@ -29,15 +32,15 @@ TEST_CASE("evaluate linear 2d omega_h_field") 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{}); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); pcms::test::CheckEvaluation( factory, field, pcms::test::StandardEvalCoords2D(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); } #ifdef PCMS_ENABLE_MESHFIELDS @@ -47,38 +50,14 @@ TEST_CASE("evaluate quadratic 2d meshfields_field") 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", + mesh, 2, 1, pcms::csys::Cartesian::Deferred(), "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); - - Omega_h::Write test_f(nverts + nedges); - Omega_h::parallel_for( - nverts, OMEGA_H_LAMBDA(int i) { - 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 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 = factory.CreateField(pcms::FieldMetadata{}); - field.GetData().SetDOFHolderDataHost( - pcms::Rank2View(test_f_host.data(), - test_f_host.size(), 1)); + // Quadratic DOF holders span vertices and edge midpoints; the layout's DOF + // coordinates cover both, so SetField samples sin_f at every holder. + auto field = factory->CreateFunction(); + pcms::test::SetField( + field, OMEGA_H_LAMBDA(Real x, Real y) { return sin_f(x, y); }); pcms::test::CheckEvaluation( factory, field, kEvalCoords, @@ -87,6 +66,101 @@ TEST_CASE("evaluate quadratic 2d meshfields_field") } #endif +#ifdef PCMS_ENABLE_MESHFIELDS +// Recomputes the expected DOF-holder coordinates from mesh geometry exactly as +// the layout's isoparametric map does for affine simplex elements, accumulated +// per entity dimension in the same block order the layout stores: +// dim 0 (vertex) holders -> the vertex's own coordinate, +// dim 1 (edge) holders -> the midpoint of the edge's two endpoint vertices. +// Returns a flat node-major vector matching GetDOFHolderCoordinates() layout. +static std::vector ComputeManualHolderCoords( + Omega_h::Mesh& mesh, const std::array& nodes_per_dim) +{ + const int dim = mesh.dim(); + auto mesh_coords_h = Omega_h::HostRead(mesh.coords()); + auto edge2vtx_h = Omega_h::HostRead(mesh.ask_verts_of(1)); + + std::vector expected; + for (int e_dim = 0; e_dim <= dim; ++e_dim) { + if (nodes_per_dim[static_cast(e_dim)] != 1) + continue; + const int num_ents = mesh.nents(e_dim); + for (int ent = 0; ent < num_ents; ++ent) { + if (e_dim == 0) { + for (int d = 0; d < dim; ++d) + expected.push_back(mesh_coords_h[ent * dim + d]); + } else { // e_dim == 1: edge midpoint + const int a = edge2vtx_h[2 * ent + 0]; + const int b = edge2vtx_h[2 * ent + 1]; + for (int d = 0; d < dim; ++d) + expected.push_back( + 0.5 * (mesh_coords_h[a * dim + d] + mesh_coords_h[b * dim + d])); + } + } + } + return expected; +} + +// Builds a MeshFields layout of the given order on a small known box and checks +// that GetDOFHolderCoordinates() matches the manual reference computation. +static void CheckMeshFieldsDofHolderCoordsAgainstManual(Omega_h::Library& lib, + int order) +{ + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 2, 3, 0, false); + + std::array nodes_per_dim{}; + if (order == 1) + nodes_per_dim = {1, 0, 0, 0}; + else if (order == 2) + nodes_per_dim = {1, 1, 0, 0}; + + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, order, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); + auto layout = factory->GetLayout(); + + // Sanity-check the block layout: vertices then edges. + if (order == 2) { + const auto offsets = layout->GetEntOffsets(); + const int nverts = mesh.nents(0); + REQUIRE(offsets[0] == 0); + REQUIRE(offsets[1] == static_cast(nverts)); + } + + const std::vector expected = + ComputeManualHolderCoords(mesh, nodes_per_dim); + + auto coords = layout->GetDOFHolderCoordinates().GetValues(); + const int dim = mesh.dim(); + REQUIRE(coords.extent(1) == static_cast(dim)); + REQUIRE(coords.extent(0) == expected.size() / static_cast(dim)); + + auto coords_h = pcms::test::CopyCoordinatesToHost(coords); + + size_t idx = 0; + for (size_t r = 0; r < coords_h.extent(0); ++r) { + for (int d = 0; d < dim; ++d) { + INFO("row " << r << " dim " << d); + REQUIRE(coords_h(r, d) == Catch::Approx(expected[idx])); + ++idx; + } + } +} + +TEST_CASE("MeshFields: linear dof-holder coordinates match manual reference") +{ + auto lib = Omega_h::Library{}; + CheckMeshFieldsDofHolderCoordsAgainstManual(lib, 1); +} + +TEST_CASE("MeshFields: quadratic dof-holder coordinates match manual reference") +{ + auto lib = Omega_h::Library{}; + CheckMeshFieldsDofHolderCoordsAgainstManual(lib, 2); +} +#endif + TEST_CASE("evaluate quadratic 2d omega_h_field throws") { auto lib = Omega_h::Library{}; @@ -94,7 +168,7 @@ TEST_CASE("evaluate quadratic 2d omega_h_field throws") 100, 0, false); REQUIRE_THROWS_AS(pcms::LagrangeFunctionSpace::FromMesh( - mesh, 2, 1, pcms::CoordinateSystem::Cartesian, "global", + mesh, 2, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH), pcms::pcms_error); } diff --git a/test/test_field_exchange_planner.cpp b/test/test_field_exchange_planner.cpp index 61b5bc54c..1ebdd6fff 100644 --- a/test/test_field_exchange_planner.cpp +++ b/test/test_field_exchange_planner.cpp @@ -2,12 +2,13 @@ #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" TEST_CASE("Field exchange plans exclude GID message headers", "[field_exchange]") { Kokkos::View coords("coords", 4, 2); - pcms::PointCloudLayout layout(2, coords, pcms::CoordinateSystem::Cartesian); + pcms::PointCloudLayout layout(2, coords, pcms::csys::Cartesian::Deferred()); // Two incoming messages. Each contains a five-entry entity-offset header // followed by two vertex GIDs. @@ -39,7 +40,7 @@ TEST_CASE("GID messages insert headers around compact field payloads", "[field_exchange]") { Kokkos::View coords("coords", 4, 2); - pcms::PointCloudLayout layout(2, coords, pcms::CoordinateSystem::Cartesian); + pcms::PointCloudLayout layout(2, coords, pcms::csys::Cartesian::Deferred()); pcms::ExchangePlan plan; plan.dest_ranks = {0, 1}; @@ -51,9 +52,7 @@ TEST_CASE("GID messages insert headers around compact field payloads", "gid_message", plan.msg_size + 2 * static_cast(pcms::ent_offsets_len)); pcms::GenericFieldExchangePlanner planner; - planner.FillGidMessage(layout, plan, - pcms::Rank1View( - message.data(), message.size())); + planner.FillGidMessage(layout, plan, pcms::MakeRank1View(message)); const pcms::GO expected[] = { 0, 2, 2, 2, 2, 0, 2, 0, 2, 2, 2, 2, 1, 3, @@ -75,7 +74,7 @@ TEST_CASE("ExchangePlan permutation is negative for non-participating holders", // the received message, so holders 4 and 5 should have negative // permutation entries. Kokkos::View coords("coords", 6, 2); - pcms::PointCloudLayout layout(2, coords, pcms::CoordinateSystem::Cartesian); + pcms::PointCloudLayout layout(2, coords, pcms::csys::Cartesian::Deferred()); const std::size_t header_len = static_cast(pcms::ent_offsets_len); diff --git a/test/test_field_interpolation.cpp b/test/test_field_interpolation.cpp index fdada7270..0a94e0594 100644 --- a/test/test_field_interpolation.cpp +++ b/test/test_field_interpolation.cpp @@ -6,11 +6,12 @@ #include #include "pcms/transfer/interpolator.h" #include "pcms/field/function_space/lagrange.h" -#include "pcms/field/field_metadata.h" +#include "pcms/field/value_view.hpp" #include "pcms/utility/assert.h" #include "field_test_utils.h" #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::Real; @@ -26,17 +27,18 @@ TEST_CASE("interpolate linear 2d omega_h_field") 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 field = factory.CreateField(pcms::FieldMetadata{}); - auto interpolated = factory.CreateField(pcms::FieldMetadata{}); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); + auto interpolated = factory->CreateFunction(); pcms::test::SetField( field, OMEGA_H_LAMBDA(Real x, Real y) { return -0.3 * x + 0.5 * y; }); - pcms::Interpolator interp(factory, factory); + pcms::Interpolator interp(*factory, *factory); interp.Apply(field, interpolated); auto interpolated_dof = - pcms::FlattenToRank1View(interpolated.GetDOFHolderDataHost()); - auto original_dof = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + pcms::FlattenToRank1View(interpolated.GetDOFHolderDataHost().GetValues()); + auto original_dof = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(interpolated_dof.size() == original_dof.size()); for (int i = 0; i < static_cast(interpolated_dof.size()); ++i) { REQUIRE_THAT(interpolated_dof[i], @@ -53,44 +55,21 @@ TEST_CASE("interpolate quadratic 2d meshfields_field") auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 100, 100, 0, false); auto factory2 = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 2, 1, pcms::CoordinateSystem::Cartesian, "global", + mesh, 2, 1, pcms::csys::Cartesian::Deferred(), "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); - 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] = interpolation_linear_f(x, y); - }); - 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] = interpolation_linear_f(cx, cy); - }); - - Omega_h::HostWrite test_f_host(test_f); - auto field = factory2.CreateField(pcms::FieldMetadata{}); - auto interpolated = factory2.CreateField(pcms::FieldMetadata{}); - field.SetDOFHolderDataHost(pcms::Rank2View( - test_f_host.data(), test_f_host.size(), 1)); - - pcms::Interpolator interp(factory2, factory2); + auto field = factory2->CreateFunction(); + auto interpolated = factory2->CreateFunction(); + pcms::test::SetField( + field, + OMEGA_H_LAMBDA(Real x, Real y) { return interpolation_linear_f(x, y); }); + + pcms::Interpolator interp(*factory2, *factory2); interp.Apply(field, interpolated); auto interpolated_dof = - pcms::FlattenToRank1View(interpolated.GetDOFHolderDataHost()); - auto original_dof = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + pcms::FlattenToRank1View(interpolated.GetDOFHolderDataHost().GetValues()); + auto original_dof = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(interpolated_dof.size() == original_dof.size()); for (int i = 0; i < static_cast(interpolated_dof.size()); ++i) { REQUIRE_THAT(interpolated_dof[i], @@ -108,7 +87,7 @@ TEST_CASE("interpolate quadratic 2d omega_h_field throws") 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", + mesh, 2, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH), pcms::pcms_error); } @@ -124,15 +103,15 @@ TEST_CASE("Interpolator: construct once, apply twice with different data") 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); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); - auto source = factory.CreateField(pcms::FieldMetadata{}); - auto target = factory.CreateField(pcms::FieldMetadata{}); + auto source = factory->CreateFunction(); + auto target = factory->CreateFunction(); // 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); + pcms::Interpolator interp(*factory, *factory); // First application: linear function f(x,y) = -0.3*x + 0.5*y pcms::test::SetField( @@ -140,8 +119,10 @@ TEST_CASE("Interpolator: construct once, apply twice with different data") interp.Apply(source, target); { - auto src_dof = pcms::FlattenToRank1View(source.GetDOFHolderDataHost()); - auto tgt_dof = pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + auto src_dof = + pcms::FlattenToRank1View(source.GetDOFHolderDataHost().GetValues()); + auto tgt_dof = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); 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) || @@ -156,7 +137,8 @@ TEST_CASE("Interpolator: construct once, apply twice with different data") interp.Apply(source, target); { - auto tgt_dof = pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + auto tgt_dof = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); 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_serializer.cpp b/test/test_field_serializer.cpp new file mode 100644 index 000000000..267564922 --- /dev/null +++ b/test/test_field_serializer.cpp @@ -0,0 +1,273 @@ +#include +#include + +#include "pcms/configuration.h" +#include "pcms/coupler/field_serializer.h" +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/utility/arrays.h" + +#include +#include +#include + +#ifdef PCMS_ENABLE_OMEGA_H +#include "field_test_utils.h" +#include "pcms/field/function_space/lagrange.h" +#include "pcms/field/function_space/polynomial_reconstruction.hpp" +#include "pcms/utility/uniform_grid.h" +#include +#endif + +#ifdef PCMS_ENABLE_XGC +#include "pcms/coupler/overlap_mask.h" +#include "pcms/field/data/xgc.h" +#include "pcms/field/function_space/xgc.h" +#endif + +namespace +{ + +#ifdef PCMS_ENABLE_OMEGA_H +void CheckSerializeDeserialize(pcms::Field& field) +{ + auto data_before = field.GetDOFHolderDataHost().GetValues(); + const auto num_dof = static_cast(data_before.extent(0)); + const auto num_components = static_cast(data_before.extent(1)); + const auto num_values = static_cast(data_before.size()); + + std::vector expected(num_values); + for (pcms::LO i = 0; i < num_dof; ++i) { + for (pcms::LO c = 0; c < num_components; ++c) { + expected[static_cast(i) * num_components + c] = data_before(i, c); + } + } + + std::vector buffer(num_values); + std::vector permutation(num_dof); + std::iota(permutation.begin(), permutation.end(), pcms::LO{0}); + + pcms::FieldSerializer serializer; + REQUIRE(serializer.Serialize(field, pcms::make_array_view(buffer), + pcms::make_const_array_view(permutation)) == + static_cast(num_values)); + + std::vector cleared(num_values, -1.0); + field.SetDOFHolderDataUncheckedHost( + pcms::Rank2View( + cleared.data(), num_dof, num_components)); + serializer.Deserialize(field, pcms::make_const_array_view(buffer), + pcms::make_const_array_view(permutation)); + + auto data_after = field.GetDOFHolderDataHost().GetValues(); + REQUIRE(data_after.size() == expected.size()); + for (pcms::LO i = 0; i < num_dof; ++i) { + for (pcms::LO c = 0; c < num_components; ++c) { + REQUIRE( + data_after(i, c) == + Catch::Approx(expected[static_cast(i) * num_components + c])); + } + } +} +#endif + +#ifdef PCMS_ENABLE_XGC +pcms::ReverseClassificationVertex CreateDummyReverseClassification(int size) +{ + pcms::ReverseClassificationVertex rc; + for (int i = 0; i < size; ++i) { + rc.Insert(i % 4 == 0 ? pcms::DimID{0, 0} : pcms::DimID{0, 1}, i); + } + return rc; +} + +bool InXGCOverlap(int, int id) +{ + return id == 0; +} +#endif + +} // namespace + +#ifdef PCMS_ENABLE_XGC +TEST_CASE("Serializing an XGC field preserves inactive field entries") +{ + static constexpr int data_size = 16; + auto rc = CreateDummyReverseClassification(data_size); + pcms::XGCFieldFactory factory(rc, data_size); + + std::vector data(data_size); + std::iota(data.begin(), data.end(), 0.0); + const auto original = data; + auto field = factory.CreateField( + "", std::make_unique>( + factory.GetXGCLayout(), pcms::ValueBasis{}, + pcms::make_array_view(data), MPI_COMM_SELF)); + pcms::FieldSerializer serializer; + + // Entries outside the overlap region are not exchanged. + const pcms::OverlapMask overlap_mask(InXGCOverlap); + auto active = overlap_mask.GetMask(field.GetLayout()); + std::vector permutation(data_size, -1); + int entry = 0; + for (int i = 0; i < data_size; ++i) { + if (active[i]) { + permutation[i] = entry++; + } + } + const int num_active = entry; + REQUIRE(num_active == 4); + + std::vector buffer(num_active, -1.0); + REQUIRE(serializer.Serialize(field, pcms::make_array_view(buffer), + pcms::make_const_array_view(permutation)) == + num_active); + + for (int i = 0; i < data_size; ++i) { + if (active[i]) { + REQUIRE(buffer[permutation[i]] == data[i]); + buffer[permutation[i]] += 100.0; + } else { + REQUIRE(permutation[i] == -1); + } + } + + serializer.Deserialize(field, pcms::make_const_array_view(buffer), + pcms::make_const_array_view(permutation)); + + auto after = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); + for (int i = 0; i < data_size; ++i) { + if (active[i]) { + REQUIRE(after[i] == Catch::Approx(original[i] + 100.0)); + } else { + REQUIRE(after[i] == Catch::Approx(original[i])); + } + } +} +#endif + +#ifdef PCMS_ENABLE_OMEGA_H +TEST_CASE("FieldSerializer round-trips a polynomial-reconstruction field") +{ + std::vector coords{0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0}; + pcms::Rank2View coords_view(coords.data(), + 4, 2); + auto space = pcms::PolynomialReconstructionFunctionSpace::Create( + pcms::CoordinateView( + pcms::csys::Cartesian::Deferred(), coords_view)); + auto field = space->CreateFunction(); + + std::vector data{5.0, 6.0, 7.0, 8.0}; + field.SetDOFHolderDataUncheckedHost( + pcms::Rank2View(data.data(), 4, + 1)); + + CheckSerializeDeserialize(field); +} + +TEST_CASE("FieldSerializer round-trips a uniform-grid field") +{ + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {10.0, 10.0}; + grid.divisions = {3, 3}; + auto space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::csys::Cartesian::Deferred()); + auto field = space->CreateFunction(); + + std::vector data(16); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast(i * 10); + } + field.SetDOFHolderDataUncheckedHost( + pcms::Rank2View(data.data(), 16, + 1)); + + CheckSerializeDeserialize(field); +} + +TEST_CASE("FieldSerializer round-trips an order-1 Omega_h field") +{ + auto lib = Omega_h::Library{}; + auto mesh = pcms::test::BuildUnitSquare(lib, 0); + auto space = pcms::test::MakeP1Space(mesh); + auto field = space->CreateFunction(); + + pcms::test::SetField( + field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { + return pcms::test::linear_f(x, y); + }); + + CheckSerializeDeserialize(field); +} + +TEST_CASE("FieldSerializer round-trips a multi-component Omega_h field") +{ + auto lib = Omega_h::Library{}; + auto mesh = pcms::test::BuildUnitSquare(lib, 0); + constexpr int num_components = 3; + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, num_components, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto field = space->CreateFunction(); + + const int num_dof = field.GetLayout().GetNumOwnedDofHolder(); + std::vector data(static_cast(num_dof) * num_components); + for (int i = 0; i < num_dof; ++i) { + for (int c = 0; c < num_components; ++c) { + data[static_cast(i) * num_components + c] = i + 0.25 * c; + } + } + field.SetDOFHolderDataUncheckedHost( + pcms::Rank2View( + data.data(), num_dof, num_components)); + + CheckSerializeDeserialize(field); +} + +TEST_CASE("FieldSerializer round-trips an order-0 Omega_h field") +{ + auto lib = Omega_h::Library{}; + auto mesh = pcms::test::BuildUnitSquare(lib, 0); + auto space = pcms::test::MakeP0Space(mesh); + auto field = space->CreateFunction(); + + const int num_dof = mesh.nelems(); + std::vector data(num_dof); + std::iota(data.begin(), data.end(), pcms::Real{0}); + field.SetDOFHolderDataUncheckedHost( + pcms::Rank2View(data.data(), + num_dof, 1)); + + CheckSerializeDeserialize(field); +} +TEST_CASE("FieldSerializer preserves holders outside the exchange on receive") +{ + std::vector coords{0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0}; + pcms::Rank2View coords_view(coords.data(), + 4, 2); + auto space = pcms::PolynomialReconstructionFunctionSpace::Create( + pcms::CoordinateView( + pcms::csys::Cartesian::Deferred(), coords_view)); + auto field = space->CreateFunction(); + + std::vector data{5.0, 6.0, 7.0, 8.0}; + field.SetDOFHolderDataUncheckedHost( + pcms::Rank2View(data.data(), 4, + 1)); + + // Holders 1 and 3 have no slot in the received buffer (e.g. they lie outside + // the sender's overlap mask); they must keep their current values. + std::vector permutation{0, -1, 1, -1}; + std::vector buffer{50.0, 70.0}; + pcms::FieldSerializer serializer; + serializer.Deserialize(field, pcms::make_const_array_view(buffer), + pcms::make_const_array_view(permutation)); + + auto after = field.GetDOFHolderDataHost().GetValues(); + REQUIRE(after(0, 0) == 50.0); + REQUIRE(after(1, 0) == 6.0); + REQUIRE(after(2, 0) == 70.0); + REQUIRE(after(3, 0) == 8.0); +} +#endif diff --git a/test/test_interpolation_class.cpp b/test/test_interpolation_class.cpp index 7a34af4da..878f6288d 100644 --- a/test/test_interpolation_class.cpp +++ b/test/test_interpolation_class.cpp @@ -10,36 +10,10 @@ #include #include #include +#include "field_test_utils.h" #include #include -#include - -bool areArraysEqualUnordered(const Omega_h::HostRead& array1, - const Omega_h::HostRead& array2, - int start, int end) -{ - // Ensure the indices are valid - assert(start >= 0 && end <= array1.size() && start <= end); - assert(start >= 0 && end <= array2.size() && start <= end); - - // Use frequency maps to count occurrences of each value - std::unordered_map freq1, freq2; - - for (int i = start; i < end; ++i) { - freq1[array1[i]]++; - freq2[array2[i]]++; - } - - // Compare the frequency maps - if (freq1 != freq2) { - pcms::printError("[ERROR] Arrays differ in the range [%d, %d)\n", start, - end); - return false; - } - - return true; -} void translate_mesh(Omega_h::Mesh* mesh, Omega_h::Vector<2> translation_vector) { @@ -135,28 +109,13 @@ TEST_CASE("Test MLSMeshInterpolation") auto mls_single = pcms::MLSMeshInterpolation(source_mesh, 0.12, 15, 3, true, 0.0, 5.0); - 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 = - Omega_h::HostWrite(source_points_host.size()); - for (int i = 0; i < source_points_host.size(); i++) { - source_points_host_write[i] = source_points_host[i]; - } - auto source_points_view = pcms::Rank1View( - source_points_host_write.data(), source_points_host_write.size()); - - auto target_points_reals = source_mesh.coords(); - auto target_points_host = - Omega_h::HostRead(target_points_reals); - auto target_points_host_write = - Omega_h::HostWrite(target_points_host.size()); - for (int i = 0; i < target_points_host.size(); i++) { - target_points_host_write[i] = target_points_host[i]; - } - auto target_points_view = pcms::Rank1View( - target_points_host_write.data(), target_points_host_write.size()); + auto source_points_vec = pcms::test::CopyOmegaHRealsToVector( + pcms::get_entity_centroids(source_mesh, Omega_h::FACE)); + auto source_points_view = pcms::make_array_view(source_points_vec); + + auto target_points_vec = + pcms::test::CopyOmegaHRealsToVector(source_mesh.coords()); + auto target_points_view = pcms::make_array_view(target_points_vec); REQUIRE(source_mesh.dim() == 2); pcms::printInfo("Point cloud based search...\n"); auto point_mls = pcms::MLSPointCloudInterpolation( @@ -173,14 +132,11 @@ TEST_CASE("Test MLSMeshInterpolation") source_mesh.nverts()); Omega_h::HostWrite exact_values_at_nodes(source_sinxcosy_node); - pcms::Rank1View sourceArrayView( - source_data_host_write.data(), source_data_host_write.size()); - pcms::Rank1View interpolatedArrayView( - interpolated_data_hwrite.data(), interpolated_data_hwrite.size()); - pcms::Rank1View - point_cloud_interpolatedArrayView( - point_cloud_interpolated_data_hwrite.data(), - point_cloud_interpolated_data_hwrite.size()); + auto sourceArrayView = pcms::make_array_view(source_data_host_write); + auto interpolatedArrayView = + pcms::make_array_view(interpolated_data_hwrite); + auto point_cloud_interpolatedArrayView = + pcms::make_array_view(point_cloud_interpolated_data_hwrite); OMEGA_H_CHECK_PRINTF(sourceArrayView.size() == mls_single.getSourceSize(), "Source size mismatch: %zu vs %zu\n", @@ -222,33 +178,8 @@ TEST_CASE("Test MLSMeshInterpolation") ///*****************************// auto mesh_based_supports = mls_single.getSupports(); auto point_cloud_based_supports = point_mls.getSupports(); - auto mesh_based_support_ptr_host = - Omega_h::HostRead(mesh_based_supports.supports_ptr); - auto mesh_based_support_idx_host = - Omega_h::HostRead(mesh_based_supports.supports_idx); - auto point_cloud_based_support_ptr_host = - Omega_h::HostRead(point_cloud_based_supports.supports_ptr); - auto point_cloud_based_support_idx_host = - Omega_h::HostRead(point_cloud_based_supports.supports_idx); - - REQUIRE(point_cloud_based_support_idx_host.size() == - mesh_based_support_idx_host.size()); - REQUIRE(point_cloud_based_support_ptr_host.size() == - mesh_based_support_ptr_host.size()); - for (int i = 0; i < mesh_based_support_ptr_host.size(); i++) { - REQUIRE(point_cloud_based_support_ptr_host[i] == - mesh_based_support_ptr_host[i]); - } - - for (int i = 0; i < mesh_based_support_ptr_host.size() - 1; i++) { - auto start = mesh_based_support_ptr_host[i]; - auto end = mesh_based_support_ptr_host[i + 1]; - - bool isEqual = - areArraysEqualUnordered(mesh_based_support_idx_host, - point_cloud_based_support_idx_host, start, end); - REQUIRE(isEqual); - } + pcms::test::CheckSupportResultsEquivalent(point_cloud_based_supports, + mesh_based_supports); // Check if the point cloud interpolation is same as the MLS interpolation pcms::printDebugInfo("Interpolated data size: %d\n", @@ -289,10 +220,9 @@ TEST_CASE("Test MLSMeshInterpolation") Omega_h::HostWrite interpolated_data_hwrite( mls_double.getTargetSize()); - pcms::Rank1View sourceArrayView( - source_data_host_write.data(), source_data_host_write.size()); - pcms::Rank1View interpolatedArrayView( - interpolated_data_hwrite.data(), interpolated_data_hwrite.size()); + auto sourceArrayView = pcms::make_array_view(source_data_host_write); + auto interpolatedArrayView = + pcms::make_array_view(interpolated_data_hwrite); mls_double.eval(sourceArrayView, interpolatedArrayView); @@ -326,10 +256,8 @@ TEST_CASE("MLSPointCloudInterpolation honors provided dimension in eval") } 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()); + auto source_points_view = pcms::make_array_view(source_points); + auto target_points_view = pcms::make_array_view(target_points); // Degree-1 polynomial that depends on z to catch accidental 2D behavior. auto source_values = Omega_h::HostWrite(27); @@ -341,10 +269,8 @@ TEST_CASE("MLSPointCloudInterpolation honors provided dimension in eval") } 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 source_values_view = pcms::make_array_view(source_values); + auto output_values_view = pcms::make_array_view(output_values); auto mls = pcms::MLSPointCloudInterpolation( source_points_view, target_points_view, 3, 2.5, 10, 1, true, 0.0, 5.0); diff --git a/test/test_interpolation_on_ltx_mesh.cpp b/test/test_interpolation_on_ltx_mesh.cpp index a348acae9..141ebfa86 100644 --- a/test/test_interpolation_on_ltx_mesh.cpp +++ b/test/test_interpolation_on_ltx_mesh.cpp @@ -83,16 +83,13 @@ TEST_CASE("Test Interpolation on LTX Mesh", "[interpolation]") 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 = - pcms::Rank1View( - degas2_mesh_centroids_host.data(), degas2_mesh_centroids_host.size()); + pcms::make_array_view(degas2_mesh_centroids_host); auto xgc_mesh_points = read_xgc_mesh_nodes(ltx_mesh_base_filename + ".node"); const int xgc_num_nodes = xgc_mesh_points.size() / 2; printf("[INFO] XGC Mesh loaded from %s with %d points\n", ltx_mesh_base_filename.c_str(), xgc_num_nodes); - const auto xgc_mesh_points_view = - pcms::Rank1View( - xgc_mesh_points.data(), xgc_mesh_points.size()); + const auto xgc_mesh_points_view = pcms::make_array_view(xgc_mesh_points); auto xgc_to_degas2_interpolator = pcms::MLSPointCloudInterpolation( xgc_mesh_points_view, degas2_mesh_centroids_view, 2, 0.000001, 10, 1, true, @@ -120,18 +117,13 @@ TEST_CASE("Test Interpolation on LTX Mesh", "[interpolation]") // ------------------ First Interpolation ------------------ // const auto density_at_xgc_nodes_view = - pcms::Rank1View(density_at_xgc_nodes.data(), - density_at_xgc_nodes.size()); - const auto temp_at_xgc_nodes_view = - pcms::Rank1View(temp_at_xgc_nodes.data(), - temp_at_xgc_nodes.size()); + pcms::make_array_view(density_at_xgc_nodes); + const auto temp_at_xgc_nodes_view = pcms::make_array_view(temp_at_xgc_nodes); const auto density_at_degas2_centroids_view = - pcms::Rank1View( - density_at_degas2_centroids.data(), density_at_degas2_centroids.size()); + pcms::make_array_view(density_at_degas2_centroids); const auto temp_at_degas2_centroids_view = - pcms::Rank1View( - temp_at_degas2_centroids.data(), temp_at_degas2_centroids.size()); + pcms::make_array_view(temp_at_degas2_centroids); Omega_h::HostWrite interpolated_xgc_density(degas2_num_elems); Omega_h::HostWrite interpolated_xgc_temp(degas2_num_elems); @@ -139,17 +131,13 @@ TEST_CASE("Test Interpolation on LTX Mesh", "[interpolation]") Omega_h::HostWrite interpolated_degas2_temp(xgc_num_nodes); const auto interpolated_xgc_density_view = - pcms::Rank1View( - interpolated_xgc_density.data(), interpolated_xgc_density.size()); + pcms::make_array_view(interpolated_xgc_density); const auto interpolated_xgc_temp_view = - pcms::Rank1View( - interpolated_xgc_temp.data(), interpolated_xgc_temp.size()); + pcms::make_array_view(interpolated_xgc_temp); const auto interpolated_degas2_density_view = - pcms::Rank1View( - interpolated_degas2_density.data(), interpolated_degas2_density.size()); + pcms::make_array_view(interpolated_degas2_density); const auto interpolated_degas2_temp_view = - pcms::Rank1View( - interpolated_degas2_temp.data(), interpolated_degas2_temp.size()); + pcms::make_array_view(interpolated_degas2_temp); xgc_to_degas2_interpolator.eval(density_at_xgc_nodes_view, interpolated_xgc_density_view); @@ -174,21 +162,13 @@ TEST_CASE("Test Interpolation on LTX Mesh", "[interpolation]") degas2_num_elems); const auto interpolated_back_density_at_xgc_nodes_view = - pcms::Rank1View( - interpolated_back_density_at_xgc_nodes.data(), - interpolated_back_density_at_xgc_nodes.size()); + pcms::make_array_view(interpolated_back_density_at_xgc_nodes); const auto interpolated_back_temp_at_xgc_nodes_view = - pcms::Rank1View( - interpolated_back_temp_at_xgc_nodes.data(), - interpolated_back_temp_at_xgc_nodes.size()); + pcms::make_array_view(interpolated_back_temp_at_xgc_nodes); const auto interpolated_back_density_at_degas2_centroids_view = - pcms::Rank1View( - interpolated_back_density_at_degas2_centroids.data(), - interpolated_back_density_at_degas2_centroids.size()); + pcms::make_array_view(interpolated_back_density_at_degas2_centroids); const auto interpolated_back_temp_at_degas2_centroids_view = - pcms::Rank1View( - interpolated_back_temp_at_degas2_centroids.data(), - interpolated_back_temp_at_degas2_centroids.size()); + pcms::make_array_view(interpolated_back_temp_at_degas2_centroids); degas2_to_xgc_interpolator.eval(interpolated_xgc_density_view, interpolated_back_density_at_xgc_nodes_view); diff --git a/test/test_intersections.cpp b/test/test_intersections.cpp index 02598f0a3..36ccb766e 100644 --- a/test/test_intersections.cpp +++ b/test/test_intersections.cpp @@ -111,7 +111,7 @@ TEST_CASE("Mesh intersection test with source and target", "[intersection]") Omega_h::parallel_for( ntgt, OMEGA_H_LAMBDA(int t) { auto tgt_vert_coords = - get_vert_coords_of_elem(tgt_coords, tgt_faces2verts, t); + get_vert_coords_of_elem<2>(tgt_coords, tgt_faces2verts, t); int start = intersection.tgt2src_offsets[t]; int end = intersection.tgt2src_offsets[t + 1]; @@ -120,7 +120,7 @@ TEST_CASE("Mesh intersection test with source and target", "[intersection]") 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); + get_vert_coords_of_elem<2>(src_coords, src_faces2verts, sid); r3d::Polytope<2> poly; r3d::intersect_simplices(poly, tgt_vert_coords, src_vert_coords); diff --git a/test/test_localization_factory.cpp b/test/test_localization_factory.cpp index 571ac1751..c5de024f9 100644 --- a/test/test_localization_factory.cpp +++ b/test/test_localization_factory.cpp @@ -14,7 +14,9 @@ #include "pcms/localization/point_cloud_localization.h" #include "pcms/discretization/discretization/omega_h.hpp" #include "pcms/utility/arrays.h" +#include "pcms/utility/omega_h_array_utils.h" #include "field_test_utils.h" +#include "pcms/field/coordinate_systems/cartesian.hpp" namespace { @@ -23,7 +25,7 @@ std::shared_ptr MakeMeshEntityLayout( Omega_h::Mesh& mesh, int entity_dim) { return std::make_shared( - mesh, entity_dim, 1, pcms::CoordinateSystem::Cartesian); + mesh, entity_dim, 1, pcms::csys::Cartesian::Deferred()); } } // namespace @@ -44,22 +46,12 @@ TEST_CASE( 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); + target_coords, pcms::csys::Cartesian::Deferred(), 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 coords_dev = pcms::ConvertCoordsTo2D(source_coords, mesh.nverts(), dim); auto layout = std::make_shared( - dim, coords_dev, pcms::CoordinateSystem::Cartesian); + dim, coords_dev, pcms::csys::Cartesian::Deferred()); pcms::PointCloudLocalizationFactory factory(layout, options); auto actual = factory.Build(target_device.coordinate_view); @@ -90,7 +82,7 @@ TEST_CASE("LocalizationFactory: vertex adjacency Build matches two-mesh " auto target_coords = pcms::test::CopyOmegaHRealsToVector(target_mesh.coords()); auto target_device = pcms::test::CreateDeviceCoordinateView( - target_coords, pcms::CoordinateSystem::Cartesian, target_mesh.dim()); + target_coords, pcms::csys::Cartesian::Deferred(), target_mesh.dim()); auto actual = factory.Build(target_device.coordinate_view); @@ -112,7 +104,7 @@ TEST_CASE("Localization path selection: vertex source uses adjacency for " 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); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); REQUIRE(pcms::detail::SelectLocalizationPath(*source_layout) == pcms::detail::LocalizationPath::VertexAdjacencySearch); @@ -127,7 +119,7 @@ TEST_CASE( auto source_layout = MakeMeshEntityLayout(mesh, pcms::Face); auto target_layout = std::make_shared( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); REQUIRE( pcms::detail::SelectLocalizationPath(*source_layout, target_layout.get()) == @@ -192,7 +184,7 @@ TEST_CASE("LocalizationFactory: correct with different meshes") auto target_coords = pcms::test::CopyOmegaHRealsToVector(target_mesh.coords()); auto target_device = pcms::test::CreateDeviceCoordinateView( - target_coords, pcms::CoordinateSystem::Cartesian, target_mesh.dim()); + target_coords, pcms::csys::Cartesian::Deferred(), target_mesh.dim()); // Should not throw — falls back to point-cloud N^2 path. auto result = factory.Build(target_device.coordinate_view); diff --git a/test/test_mesh_intersection_field_transfer.cpp b/test/test_mesh_intersection_field_transfer.cpp index 54b160389..84ee7706b 100644 --- a/test/test_mesh_intersection_field_transfer.cpp +++ b/test/test_mesh_intersection_field_transfer.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -7,53 +8,15 @@ #include #include +#include #include "field_test_utils.h" -#include -#include +#include +#include namespace { -// Builds a unit-square 2D simplex mesh from the given element connectivity and -// adds the geometric classification tags required to build an Omega_h-backed -// Lagrange function space. -Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib, const Omega_h::LOs& ev2v) -{ - const Omega_h::Reals coords({ - 0.0, 0.0, // v0 - 1.0, 0.0, // v1 - 1.0, 1.0, // v2 - 0.0, 1.0 // v3 - }); - Omega_h::Mesh mesh(&lib); - Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 2, ev2v, coords); - for (Omega_h::Int dim = 0; dim <= 2; ++dim) { - mesh.add_tag( - dim, "class_dim", 1, - Omega_h::Read(mesh.nents(dim), Omega_h::I8(dim))); - mesh.add_tag( - dim, "class_id", 1, - Omega_h::Read(mesh.nents(dim), Omega_h::ClassId(0))); - } - return mesh; -} - -pcms::LagrangeFunctionSpace MakeP1Space( - Omega_h::Mesh& mesh, const std::string& global_id_name = "global") -{ - return pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian, global_id_name, - pcms::LagrangeFunctionSpace::Backend::OmegaH); -} - -pcms::LagrangeFunctionSpace MakeP0Space(Omega_h::Mesh& mesh) -{ - return pcms::LagrangeFunctionSpace::FromMesh( - mesh, 0, 1, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); -} - void AddReorderedVertexGlobalIds(Omega_h::Mesh& mesh) { mesh.add_tag( @@ -68,70 +31,11 @@ void AddSparseVertexGlobalIds(Omega_h::Mesh& mesh) Omega_h::Read({102, 7, 41, 19}, "sparse_global")); } -// Integral over the mesh of an order-0 (piecewise-constant) field whose values -// are stored in element order. Exact by construction. -double IntegrateP0Field(Omega_h::Mesh& mesh, - const pcms::Field& field) -{ - const auto values = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); - const auto elem_areas = Omega_h::measure_elements_real(&mesh); - const auto elem_areas_h = Omega_h::HostRead(elem_areas); - - double integral = 0.0; - for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { - integral += elem_areas_h[e] * values[e]; - } - return integral; -} - -// Per-element centroid coordinates in element order. -std::vector> ElementCentroids(Omega_h::Mesh& mesh) -{ - const auto coords_h = Omega_h::HostRead(mesh.coords()); - const auto elem_verts_h = - Omega_h::HostRead(mesh.ask_elem_verts()); - std::vector> centroids(mesh.nelems()); - for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { - double cx = 0.0, cy = 0.0; - for (int k = 0; k < 3; ++k) { - const Omega_h::LO v = elem_verts_h[3 * e + k]; - cx += coords_h[2 * v + 0]; - cy += coords_h[2 * v + 1]; - } - centroids[e] = {cx / 3.0, cy / 3.0}; - } - return centroids; -} - -// Integral over the mesh of an order-1 nodal field whose values are stored in -// vertex order (as produced by Field::GetDOFHolderDataHost for the -// Omega_h backend), computed exactly via per-element vertex averaging. -double IntegrateP1Field(Omega_h::Mesh& mesh, - const pcms::Field& field) -{ - const auto values = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); - const auto elem_areas = Omega_h::measure_elements_real(&mesh); - const auto elem_verts = mesh.ask_elem_verts(); - const auto elem_areas_h = Omega_h::HostRead(elem_areas); - const auto elem_verts_h = Omega_h::HostRead(elem_verts); - - 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 avg = (values[v0] + values[v1] + values[v2]) / 3.0; - integral += elem_areas_h[e] * avg; - } - return integral; -} - } // namespace -// Fields that already live in the target order-1 space (constants and affine -// functions) must be reproduced exactly by the L2 projection, and the integral -// must be conserved. These properties hold without any reference solver, so -// they pin down correctness on their own. +// Fields that are in the target order-1 space (constants and affine +// functions) should be reproduced exactly by the L2 projection, and the +// integral must be conserved. TEST_CASE("OmegaHConservativeProjection reproduces constant and linear fields", "[transfer][mesh_intersection]") { @@ -139,17 +43,17 @@ TEST_CASE("OmegaHConservativeProjection reproduces constant and linear fields", // Source and target triangulate the same square along opposite diagonals. Omega_h::Mesh source_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); Omega_h::Mesh target_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); - auto source_space = MakeP1Space(source_mesh); - auto target_space = MakeP1Space(target_mesh); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); - auto source = source_space.CreateField(); - auto target = target_space.CreateField(); + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); - pcms::OmegaHConservativeProjection projection(source_space, target_space); + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); SECTION("constant field is preserved and conserved") { @@ -160,14 +64,15 @@ TEST_CASE("OmegaHConservativeProjection reproduces constant and linear fields", projection.Apply(source, target); const auto target_values = - pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); 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(c).margin(1e-10)); } - REQUIRE(IntegrateP1Field(target_mesh, target) == - Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-10)); + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-10)); } SECTION("linear field is reproduced on target vertices and conserved") @@ -178,48 +83,50 @@ TEST_CASE("OmegaHConservativeProjection reproduces constant and linear fields", projection.Apply(source, target); const auto target_values = - pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); - const auto tgt_coords_h = - Omega_h::HostRead(target_mesh.coords()); + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + const auto tgt_coords_h = pcms::test::CopyCoordinatesToHost( + pcms::MakeConstRank2View(target_mesh.coords(), 2)); 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]; + const double expected = tgt_coords_h(i, 0) + tgt_coords_h(i, 1); REQUIRE(target_values[i] == Catch::Approx(expected).margin(1e-9)); } - REQUIRE(IntegrateP1Field(target_mesh, target) == - Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-9)); + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-9)); } } -// For a field that does not live in the target space the projection is not an -// interpolation, but conservation of the integral is the defining property of -// the conservative (Galerkin) projection and must still hold exactly. This -// also exercises a second Apply with a different source field to confirm the -// cached integrators/evaluator/factorization are reused without stale state. TEST_CASE("OmegaHConservativeProjection conserves the integral", "[transfer][mesh_intersection]") { Omega_h::Library lib; Omega_h::Mesh source_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); Omega_h::Mesh target_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); - auto source_space = MakeP1Space(source_mesh); - auto target_space = MakeP1Space(target_mesh); + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); - auto source = source_space.CreateField(); - auto target = target_space.CreateField(); + const auto mass_type = + GENERATE(pcms::MassMatrixType::Consistent, pcms::MassMatrixType::Lumped); + CAPTURE(static_cast(mass_type)); - pcms::OmegaHConservativeProjection projection(source_space, target_space); + pcms::OmegaHConservativeProjection projection(*source_space, *target_space, + mass_type); pcms::test::SetField( source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x * x + x * y + 0.5 * y * y; }); projection.Apply(source, target); - REQUIRE(IntegrateP1Field(target_mesh, target) == - Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-12)); + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-12)); // Second Apply with a different source field — verifies cached state is // reused correctly and is not stale. @@ -227,8 +134,99 @@ TEST_CASE("OmegaHConservativeProjection conserves the integral", source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return 2.0 * x - y + 0.5; }); projection.Apply(source, target); - REQUIRE(IntegrateP1Field(target_mesh, target) == - Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-12)); + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-12)); +} + +// Row-sum lumping preserves constants and integral. It also bounds the values. +TEST_CASE("OmegaHConservativeProjection with lumped mass reproduces constants, " + "not linears, and stays bounded", + "[transfer][mesh_intersection]") +{ + Omega_h::Library lib; + + Omega_h::Mesh source_mesh = + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + Omega_h::Mesh target_mesh = + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + + pcms::OmegaHConservativeProjection projection(*source_space, *target_space, + pcms::MassMatrixType::Lumped); + + SECTION("constant field is preserved and conserved") + { + const double c = 2.0; + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real, pcms::Real) { return c; }); + + projection.Apply(source, target); + + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + 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(c).margin(1e-10)); + } + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-10)); + } + + SECTION("linear field conserves the integral but is not nodally exact") + { + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + y; }); + + projection.Apply(source, target); + + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-9)); + + // On this mesh the lumped solution at the corner vertices is the + // phi-weighted patch average of x + y, which is off by 0.5 — far above + // solver tolerance. A consistent mass matrix reproduces the field to + // 1e-9 (see the exact-reproduction test above), so a large error here + // proves the lumped path was actually taken. + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + const auto tgt_coords_h = + Omega_h::HostRead(target_mesh.coords()); + double max_err = 0.0; + 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]; + max_err = std::max(max_err, std::abs(target_values[i] - expected)); + } + CHECK(max_err > 1e-3); + + pcms::test::RequireBoundedBy(target, source); + } + + SECTION("hat function stays inside source field range") + { + // A hat centered on v0: nodal values (1, 0, 0, 0). + // This function can cause overshoot/undershoot with + // L2 projection + pcms::test::SetField( + source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { + return (1.0 - x) * (1.0 - y); + }); + + projection.Apply(source, target); + + pcms::test::RequireBoundedBy(target, source); + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-9)); + } } TEST_CASE("OmegaHConservativeProjection writes reordered target GIDs in local " @@ -238,28 +236,27 @@ TEST_CASE("OmegaHConservativeProjection writes reordered target GIDs in local " Omega_h::Library lib; Omega_h::Mesh source_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); Omega_h::Mesh target_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); AddReorderedVertexGlobalIds(target_mesh); - auto source_space = MakeP1Space(source_mesh); - auto target_space = MakeP1Space(target_mesh, "reordered_global"); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh, "reordered_global"); - auto source = source_space.CreateField(); - auto target = target_space.CreateField(); + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); pcms::test::SetField( source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); - pcms::OmegaHConservativeProjection projection(source_space, target_space); + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); projection.Apply(source, target); - const auto target_values = target.GetDOFHolderDataHost(); + const auto target_values = target.GetDOFHolderDataHost().GetValues(); const auto target_coords = - target_space.GetLayout()->GetDOFHolderCoordinates().GetValues(); - const auto target_coords_h = pcms::test::CopyCoordinatesToHost( - target_coords, target_mesh.nverts(), target_mesh.dim()); + target_space->GetLayout()->GetDOFHolderCoordinates().GetValues(); + const auto target_coords_h = pcms::test::CopyCoordinatesToHost(target_coords); REQUIRE(static_cast(target_values.extent(0)) == target_mesh.nverts()); REQUIRE(target_values.extent(1) == 1); @@ -277,28 +274,27 @@ TEST_CASE("OmegaHConservativeProjection maps sparse target GIDs to active " Omega_h::Library lib; Omega_h::Mesh source_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); Omega_h::Mesh target_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); AddSparseVertexGlobalIds(target_mesh); - auto source_space = MakeP1Space(source_mesh); - auto target_space = MakeP1Space(target_mesh, "sparse_global"); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh, "sparse_global"); - auto source = source_space.CreateField(); - auto target = target_space.CreateField(); + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); pcms::test::SetField( source, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); - pcms::OmegaHConservativeProjection projection(source_space, target_space); + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); projection.Apply(source, target); - const auto target_values = target.GetDOFHolderDataHost(); + const auto target_values = target.GetDOFHolderDataHost().GetValues(); const auto target_coords = - target_space.GetLayout()->GetDOFHolderCoordinates().GetValues(); - const auto target_coords_h = pcms::test::CopyCoordinatesToHost( - target_coords, target_mesh.nverts(), target_mesh.dim()); + target_space->GetLayout()->GetDOFHolderCoordinates().GetValues(); + const auto target_coords_h = pcms::test::CopyCoordinatesToHost(target_coords); REQUIRE(static_cast(target_values.extent(0)) == target_mesh.nverts()); REQUIRE(target_values.extent(1) == 1); @@ -318,17 +314,17 @@ TEST_CASE("OmegaHConservativeProjection P0 source to P1 target", Omega_h::Library lib; Omega_h::Mesh source_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); Omega_h::Mesh target_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); - auto source_space = MakeP0Space(source_mesh); - auto target_space = MakeP1Space(target_mesh); + auto source_space = pcms::test::MakeP0Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); - auto source = source_space.CreateField(); - auto target = target_space.CreateField(); + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); - pcms::OmegaHConservativeProjection projection(source_space, target_space); + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); SECTION("constant field is preserved and conserved") { @@ -339,14 +335,15 @@ TEST_CASE("OmegaHConservativeProjection P0 source to P1 target", projection.Apply(source, target); const auto target_values = - pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); 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(c).margin(1e-10)); } - REQUIRE(IntegrateP1Field(target_mesh, target) == - Catch::Approx(IntegrateP0Field(source_mesh, source)).margin(1e-10)); + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP0Field(source_mesh, source)) + .margin(1e-10)); } SECTION("non-constant P0 source conserves the integral") @@ -357,8 +354,9 @@ TEST_CASE("OmegaHConservativeProjection P0 source to P1 target", projection.Apply(source, target); - REQUIRE(IntegrateP1Field(target_mesh, target) == - Catch::Approx(IntegrateP0Field(source_mesh, source)).margin(1e-10)); + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP0Field(source_mesh, source)) + .margin(1e-10)); } } @@ -372,17 +370,17 @@ TEST_CASE("OmegaHConservativeProjection P1 source to P0 target", Omega_h::Library lib; Omega_h::Mesh source_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 2, 0, 2, 3})); Omega_h::Mesh target_mesh = - BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); + pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3, 1, 2, 3})); - auto source_space = MakeP1Space(source_mesh); - auto target_space = MakeP0Space(target_mesh); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP0Space(target_mesh); - auto source = source_space.CreateField(); - auto target = target_space.CreateField(); + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); - pcms::OmegaHConservativeProjection projection(source_space, target_space); + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); SECTION("constant field is preserved and conserved") { @@ -393,14 +391,15 @@ TEST_CASE("OmegaHConservativeProjection P1 source to P0 target", projection.Apply(source, target); const auto target_values = - pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); REQUIRE(static_cast(target_values.size()) == target_mesh.nelems()); for (Omega_h::LO e = 0; e < target_mesh.nelems(); ++e) { REQUIRE(target_values[e] == Catch::Approx(c).margin(1e-10)); } - REQUIRE(IntegrateP0Field(target_mesh, target) == - Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-10)); + REQUIRE(pcms::test::IntegrateP0Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-10)); } SECTION("linear field projects to cell average and conserves the integral") @@ -413,14 +412,17 @@ TEST_CASE("OmegaHConservativeProjection P1 source to P0 target", projection.Apply(source, target); const auto target_values = - pcms::FlattenToRank1View(target.GetDOFHolderDataHost()); - const auto centroids = ElementCentroids(target_mesh); - REQUIRE(static_cast(target_values.size()) == centroids.size()); - for (std::size_t e = 0; e < centroids.size(); ++e) { - const double expected = f(centroids[e][0], centroids[e][1]); + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + const auto centroids_h = Omega_h::HostRead( + pcms::get_entity_centroids(target_mesh, target_mesh.dim())); + REQUIRE(static_cast(target_values.size()) == + target_mesh.nelems()); + for (Omega_h::LO e = 0; e < target_mesh.nelems(); ++e) { + const double expected = f(centroids_h[2 * e + 0], centroids_h[2 * e + 1]); REQUIRE(target_values[e] == Catch::Approx(expected).margin(1e-9)); } - REQUIRE(IntegrateP0Field(target_mesh, target) == - Catch::Approx(IntegrateP1Field(source_mesh, source)).margin(1e-9)); + REQUIRE(pcms::test::IntegrateP0Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-9)); } } diff --git a/test/test_mesh_intersection_prefilter.cpp b/test/test_mesh_intersection_prefilter.cpp new file mode 100644 index 000000000..2a15b8e4b --- /dev/null +++ b/test/test_mesh_intersection_prefilter.cpp @@ -0,0 +1,216 @@ +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace +{ +/// Assert the two intersection maps are identical: same per-target counts and +/// the same source elements in the same order. +void require_intersections_identical(const pcms::IntersectionResults& filtered, + const pcms::IntersectionResults& exact) +{ + const auto filtered_offsets = Omega_h::HostRead(filtered.tgt2src_offsets); + const auto exact_offsets = Omega_h::HostRead(exact.tgt2src_offsets); + REQUIRE(filtered_offsets.size() == exact_offsets.size()); + for (int i = 0; i < filtered_offsets.size(); ++i) { + REQUIRE(filtered_offsets[i] == exact_offsets[i]); + } + + const auto filtered_indices = Omega_h::HostRead(filtered.tgt2src_indices); + const auto exact_indices = Omega_h::HostRead(exact.tgt2src_indices); + REQUIRE(filtered_indices.size() == exact_indices.size()); + for (int i = 0; i < filtered_indices.size(); ++i) { + REQUIRE(filtered_indices[i] == exact_indices[i]); + } +} + +/// Assert the two intersection maps hold the same source elements per target, +/// ignoring order. The order of a row is seed-then-BFS, and the seed is +/// whichever containing element the point search reports first; a centroid +/// lying on a shared face can legitimately be located in either neighbor. +void require_intersections_equivalent(const pcms::IntersectionResults& a, + const pcms::IntersectionResults& b) +{ + const auto a_offsets = Omega_h::HostRead(a.tgt2src_offsets); + const auto b_offsets = Omega_h::HostRead(b.tgt2src_offsets); + REQUIRE(a_offsets.size() == b_offsets.size()); + const auto a_indices = Omega_h::HostRead(a.tgt2src_indices); + const auto b_indices = Omega_h::HostRead(b.tgt2src_indices); + for (int row = 0; row + 1 < a_offsets.size(); ++row) { + REQUIRE(a_offsets[row] == b_offsets[row]); + std::vector a_row(a_indices.data() + a_offsets[row], + a_indices.data() + a_offsets[row + 1]); + std::vector b_row(b_indices.data() + b_offsets[row], + b_indices.data() + b_offsets[row + 1]); + std::sort(a_row.begin(), a_row.end()); + std::sort(b_row.begin(), b_row.end()); + REQUIRE(a_row == b_row); + } +} +} // namespace + +// A pre-filter that never fires would satisfy the equivalence test below +// while saving nothing, so pin down that it actually rejects the case it +// exists for, and that it does not reject a real overlap. +TEST_CASE("intersection prefilter rejects degenerate overlaps", + "[intersection]") +{ + SECTION("3D, face-adjacent tets are rejected") + { + // Two tets sharing the face x=0; separated exactly by that plane. + const r3d::Few, 4> left{ + {{0, 0, 0}, {0, 1, 0}, {0, 0, 1}, {-1, 0, 0}}}; + const r3d::Few, 4> right{ + {{0, 0, 0}, {0, 1, 0}, {0, 0, 1}, {1, 0, 0}}}; + REQUIRE(pcms::simplices_have_degenerate_overlap<3>(left, right)); + } + SECTION("3D, overlapping tets are not rejected") + { + const r3d::Few, 4> a{ + {{0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {0, 0, 1}}}; + // Same tet shifted slightly; the overlap has positive volume. + const r3d::Few, 4> b{ + {{0.1, 0.1, 0.1}, {1.1, 0.1, 0.1}, {0.1, 1.1, 0.1}, {0.1, 0.1, 1.1}}}; + REQUIRE_FALSE(pcms::simplices_have_degenerate_overlap<3>(a, b)); + } + SECTION("2D, edge-adjacent triangles are rejected") + { + const r3d::Few, 3> left{{{0, 0}, {0, 1}, {-1, 0}}}; + const r3d::Few, 3> right{{{0, 0}, {0, 1}, {1, 0}}}; + REQUIRE(pcms::simplices_have_degenerate_overlap<2>(left, right)); + } + SECTION("2D, overlapping triangles are not rejected") + { + const r3d::Few, 3> a{{{0, 0}, {1, 0}, {0, 1}}}; + const r3d::Few, 3> b{{{0.1, 0.1}, {1.1, 0.1}, {0.1, 1.1}}}; + REQUIRE_FALSE(pcms::simplices_have_degenerate_overlap<2>(a, b)); + } +} + +// The separating-plane pre-filter must not change which source elements are +// found, only how quickly the degenerate pairs are discarded. The unfiltered +// path is the oracle. +TEST_CASE("intersection prefilter preserves the intersection map", + "[intersection]") +{ + Omega_h::Library lib; + auto world = lib.world(); + + SECTION("2D, identical meshes") + { + auto source = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 8, 8, 0, false); + auto target = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 8, 8, 0, false); + require_intersections_identical( + pcms::intersectTargets(source, target, true), + pcms::intersectTargets(source, target, false)); + } + SECTION("2D, nested resolutions") + { + auto source = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 12, 12, 0, false); + auto target = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 4, 4, 0, false); + require_intersections_identical( + pcms::intersectTargets(source, target, true), + pcms::intersectTargets(source, target, false)); + } + SECTION("2D, non-nested resolutions") + { + auto source = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 7, 11, 0, false); + auto target = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 5, 3, 0, false); + require_intersections_identical( + pcms::intersectTargets(source, target, true), + pcms::intersectTargets(source, target, false)); + } + SECTION("3D, identical meshes") + { + auto source = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 4, 4, 4, false); + auto target = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 4, 4, 4, false); + require_intersections_identical( + pcms::intersectTargets(source, target, true), + pcms::intersectTargets(source, target, false)); + } + SECTION("3D, nested resolutions") + { + auto source = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 6, 6, 6, false); + auto target = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 3, 3, 3, false); + require_intersections_identical( + pcms::intersectTargets(source, target, true), + pcms::intersectTargets(source, target, false)); + } + SECTION("3D, non-nested resolutions") + { + auto source = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 5, 3, 4, false); + auto target = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 2, 4, 3, false); + require_intersections_identical( + pcms::intersectTargets(source, target, true), + pcms::intersectTargets(source, target, false)); + } +} + +// The conservative transfer hands the intersection the source space's own +// point search rather than building a second one. The set of source elements +// per target is a property of the two meshes: which search located the target +// centroids, and at what grid resolution, must not show in it. Row order can +// (see require_intersections_equivalent), so this compares rows as sets. +TEST_CASE("intersection map is independent of the supplied source search", + "[intersection]") +{ + Omega_h::Library lib; + auto world = lib.world(); + + SECTION("2D, coarse caller-supplied grid") + { + auto source = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 7, 11, 0, false); + auto target = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 5, 3, 0, false); + const pcms::GridPointSearchVariant search{ + std::in_place_type, source, 3, 3}; + require_intersections_equivalent( + pcms::intersectTargets(source, target, search), + pcms::intersectTargets(source, target)); + } + SECTION("3D, coarse caller-supplied grid") + { + auto source = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 5, 3, 4, false); + auto target = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 2, 4, 3, false); + const pcms::GridPointSearchVariant search{ + std::in_place_type, source, 2, 2, 2}; + require_intersections_equivalent( + pcms::intersectTargets(source, target, search), + pcms::intersectTargets(source, target)); + } + SECTION("a search of the wrong dimension is rejected") + { + auto source = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 2, 2, 2, false); + auto target = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 2, 2, 2, false); + auto flat = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 2, 2, 0, false); + const pcms::GridPointSearchVariant search{ + std::in_place_type, flat, 2, 2}; + REQUIRE_THROWS_AS(pcms::intersectTargets(source, target, search), + pcms::pcms_error); + } +} diff --git a/test/test_mfem_adapter.cpp b/test/test_mfem_adapter.cpp new file mode 100644 index 000000000..ba5b869a0 --- /dev/null +++ b/test/test_mfem_adapter.cpp @@ -0,0 +1,144 @@ +#include + +#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", "[mfem]") +{ + 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::csys::Cartesian::Deferred()); + + 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().GetValues(); + REQUIRE(static_cast(coords.extent(0)) == pmesh.GetNV()); + REQUIRE(static_cast(coords.extent(1)) == 2); + + // Owned mask covers 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::MFEMFieldFactory factory(pmesh, pfes, gf, + pcms::csys::Cartesian::Deferred(), "mfem"); + auto field = factory.CreateField("data"); + const auto& layout = field.GetLayout(); + + auto host = field.GetDOFHolderDataHost().GetValues(); + REQUIRE(static_cast(host.extent(0)) == pmesh.GetNV()); + REQUIRE(host.extent(1) == 1); + + std::vector captured(host.extent(0)); + for (size_t v = 0; v < captured.size(); ++v) { + captured[v] = host(v, 0); + } + + // Write the captured values back and confirm the grid function is restored. + gf = 0.0; + Kokkos::View in("in", captured.size(), + 1); + for (size_t v = 0; v < captured.size(); ++v) { + in(v, 0) = captured[v]; + } + field.SetDOFHolderDataHost( + pcms::ValueView( + pcms::ValueBasis{}, pcms::MakeConstRank2View(in))); + + auto host2 = field.GetDOFHolderDataHost().GetValues(); + auto owned = layout.GetOwnedHost(); + for (size_t v = 0; v < host2.extent(0); ++v) { + if (owned[v]) { + REQUIRE(host2(v, 0) == 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); + + pcms::MFEMFieldFactory factory(pmesh, pfes, gf, + pcms::csys::Cartesian::Deferred(), "mfem"); + auto field = factory.CreateField("source"); + 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, pcms::make_array_view(buffer), + pcms::make_const_array_view(perm)); + + // Deserialize into a second field bound to a fresh grid function. + mfem::ParGridFunction gf2(&pfes); + gf2 = 0.0; + auto field2 = factory.CreateField( + "target", std::make_unique(pfes, gf2)); + serializer.Deserialize(field2, pcms::make_const_array_view(buffer), + pcms::make_const_array_view(perm)); + + auto restored = field2.GetDOFHolderDataHost().GetValues(); + auto original = field.GetDOFHolderDataHost().GetValues(); + for (size_t v = 0; v < restored.extent(0); ++v) { + REQUIRE(restored(v, 0) == original(v, 0)); + } + } +} diff --git a/test/test_mfem_coupling.cpp b/test/test_mfem_coupling.cpp new file mode 100644 index 000000000..495495af0 --- /dev/null +++ b/test/test_mfem_coupling.cpp @@ -0,0 +1,200 @@ +// Two-application coupling test for the MFEM field adapter. + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +using pcms::Real; + +namespace +{ + +constexpr int TargetAttribute = 2; +constexpr const char* LayoutName = "mfem_vertex"; +constexpr const char* FieldName = "field"; + +constexpr Real OutsideOverlapSentinel = -7.0; + +// Build a 4x4 quad mesh and tag the left half (centroid x < 0.5) with the +// target attribute, everything else has tag 1. +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); +} + +void FillField(pcms::Field& field, const std::vector& values) +{ + Kokkos::View view("values", values.size(), 1); + for (size_t v = 0; v < values.size(); ++v) { + view(v, 0) = values[v]; + } + field.SetDOFHolderDataUncheckedHost(pcms::MakeConstRank2View(view)); +} + +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::CouplerComm cpl("mfem_overlap_coupler", comm, false, + redev::Partition{}); + auto* app = cpl.AddApplication("mfem_app"); + + pcms::MFEMFieldFactory factory(pmesh, pfes, gf, + pcms::csys::Cartesian::Deferred(), LayoutName); + auto layout = factory.GetLayout(); + + auto overlap_view = + pcms::MFEMLayout::OverlapMaskFromAttribute(pmesh, TargetAttribute); + app->SetLayoutOverlapMask( + LayoutName, + std::make_unique( + static_cast(layout->GetNumOwnedDofHolder()), overlap_view)); + + auto handle = app->AddField(factory.CreateField(FieldName)); + + // Seed the field so each vertex holds (gid + 1). + auto gids = layout->GetGidsHost(); + std::vector values(static_cast(layout->GetNumOwnedDofHolder())); + for (size_t v = 0; v < values.size(); ++v) { + values[v] = ExpectedValue(gids[v]); + } + FillField(handle.GetField(), 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::CouplerComm cpl("mfem_overlap_coupler", comm, true, + MakeRCBPartition(pmesh.SpaceDimension())); + auto* app = cpl.AddApplication("mfem_app"); + + pcms::MFEMFieldFactory factory(pmesh, pfes, gf, + pcms::csys::Cartesian::Deferred(), LayoutName); + auto layout = factory.GetLayout(); + auto handle = app->AddField(factory.CreateField(FieldName)); + + FillField( + handle.GetField(), + std::vector(static_cast(layout->GetNumOwnedDofHolder()), + OutsideOverlapSentinel)); + + app->ReceivePhase([&]() { handle.Receive(); }); + + auto overlap = + pcms::MFEMLayout::OverlapMaskFromAttribute(pmesh, TargetAttribute); + auto gids = layout->GetGidsHost(); + auto received = handle.GetField().GetDOFHolderDataHost().GetValues(); + + int overlap_count = 0; + int mismatches = 0; + int corrupted_outside = 0; + for (size_t v = 0; v < received.extent(0); ++v) { + if (overlap(v)) { + ++overlap_count; + const Real expected = ExpectedValue(gids[v]); + if (received(v, 0) != expected) { + ++mismatches; + std::cerr << "Mismatch at vertex " << v << ": expected " << expected + << " got " << received(v, 0) << "\n"; + } + } else if (received(v, 0) != 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_ohClassPtn.cpp b/test/test_ohClassPtn.cpp index 94c9f355c..e27b494ed 100644 --- a/test/test_ohClassPtn.cpp +++ b/test/test_ohClassPtn.cpp @@ -41,7 +41,9 @@ int main(int argc, char** argv) redev::Redev rdv(MPI_COMM_WORLD, redev::Partition{std::move(partition)}, static_cast(isRdv)); const std::string name = "meshVtxIds"; - adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; + adios2::Params params{{"Streaming", "On"}, + {"OpenTimeoutSecs", "60"}, + {"BeginStepPollingFrequencySecs", "0.05"}}; auto channel = rdv.CreateAdiosChannel(name, params, redev::TransportType::BP4); auto commPair = channel.CreateComm(name, rdv.GetMPIComm()); diff --git a/test/test_ohClassPtn_appRibPtn.cpp b/test/test_ohClassPtn_appRibPtn.cpp index 7132f11ed..a6680ce19 100644 --- a/test/test_ohClassPtn_appRibPtn.cpp +++ b/test/test_ohClassPtn_appRibPtn.cpp @@ -53,7 +53,9 @@ int main(int argc, char** argv) const std::string name = "meshVtxIds"; - adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; + adios2::Params params{{"Streaming", "On"}, + {"OpenTimeoutSecs", "60"}, + {"BeginStepPollingFrequencySecs", "0.05"}}; auto channel = rdv.CreateAdiosChannel(name, params, redev::TransportType::BP4); auto commPair = channel.CreateComm(name, rdv.GetMPIComm()); diff --git a/test/test_ohOverlap.cpp b/test/test_ohOverlap.cpp index c9691b08d..c75614b38 100644 --- a/test/test_ohOverlap.cpp +++ b/test/test_ohOverlap.cpp @@ -120,7 +120,9 @@ int main(int argc, char** argv) auto partition = setupServerPartition(mesh, cpnFileName); auto rdv = redev::Redev(MPI_COMM_WORLD, redev::Partition{partition}, static_cast(isRdv)); - adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; + adios2::Params params{{"Streaming", "On"}, + {"OpenTimeoutSecs", "60"}, + {"BeginStepPollingFrequencySecs", "0.05"}}; auto channel = rdv.CreateAdiosChannel(std::string(name), params, redev::TransportType::BP4); auto comm = @@ -210,7 +212,9 @@ int main(int argc, char** argv) auto rdv = redev::Redev(MPI_COMM_WORLD, redev::Partition{std::move(partition)}, static_cast(isRdv)); - adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; + adios2::Params params{{"Streaming", "On"}, + {"OpenTimeoutSecs", "60"}, + {"BeginStepPollingFrequencySecs", "0.05"}}; auto channel = rdv.CreateAdiosChannel(std::string(name), params, redev::TransportType::BP4); auto comm = diff --git a/test/test_omega_h_3d_analytic_mass.cpp b/test/test_omega_h_3d_analytic_mass.cpp new file mode 100644 index 000000000..4a80a634c --- /dev/null +++ b/test/test_omega_h_3d_analytic_mass.cpp @@ -0,0 +1,136 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "field_test_utils.h" +#include + +// Reference corner tet: +// v0=(0,0,0), v1=(1,0,0), v2=(0,1,0), v3=(0,0,1) +// Volume V = 1/6. +// +// P1 consistent mass on a tet: +// M_ij = V/20 * (1 + delta_ij) +// = 1/60 on diagonal, 1/120 off-diagonal. + +TEST_CASE("OmegaHMassIntegrator (3D): P1 mass on reference tet matches " + "analytic V/20*(1+delta)", + "[mass_integrator][3d][analytic]") +{ + Omega_h::Library lib; + auto mesh = pcms::test::BuildReferenceTet(lib); + REQUIRE(mesh.dim() == 3); + REQUIRE(mesh.nverts() == 4); + REQUIRE(mesh.nelems() == 1); + + constexpr pcms::Real V = 1.0 / 6.0; + constexpr pcms::Real M_diag = V / 10.0; // 1/60 + constexpr pcms::Real M_off = V / 20.0; // 1/120 + + auto space = pcms::test::MakeP1Space(mesh); + auto integrator = pcms::BuildOmegaHMassIntegrator(*space); + Mat mat = integrator->GetMatrix(); + + const auto layout = + std::dynamic_pointer_cast( + space->GetLayout()); + REQUIRE(layout != nullptr); + + // PETSc rows are active indices = global_to_local(local_vertex). + const auto perm = layout->GetGlobalToLocalPermutationHost(); + REQUIRE(static_cast(perm.extent(0)) == 4); + + pcms::Real grand_total = 0.0; + for (int i = 0; i < 4; ++i) { + pcms::Real row_sum = 0.0; + for (int j = 0; j < 4; ++j) { + PetscInt r = static_cast(perm(i)); + PetscInt c = static_cast(perm(j)); + PetscScalar got = 0.0; + MatGetValues(mat, 1, &r, 1, &c, &got); + const pcms::Real expected = (i == j) ? M_diag : M_off; + CAPTURE(i, j, r, c, expected, got); + CHECK(static_cast(got) == + Catch::Approx(expected).epsilon(1e-12)); + row_sum += static_cast(got); + } + // Row sum =\int \lamda_i = V/4 + CHECK(row_sum == Catch::Approx(V / 4.0).epsilon(1e-12)); + grand_total += row_sum; + } + // Sum of row sums = volume + CHECK(grand_total == Catch::Approx(V).epsilon(1e-12)); +} + +TEST_CASE("OmegaHMassIntegrator (3D): P0 mass on reference tet equals volume", + "[mass_integrator][3d][analytic]") +{ + Omega_h::Library lib; + auto mesh = pcms::test::BuildReferenceTet(lib); + + constexpr pcms::Real V = 1.0 / 6.0; + + auto space = pcms::test::MakeP0Space(mesh); + auto integrator = pcms::BuildOmegaHMassIntegrator(*space); + Mat mat = integrator->GetMatrix(); + + PetscInt r = 0; + PetscInt c = 0; + PetscScalar got = 0.0; + MatGetValues(mat, 1, &r, 1, &c, &got); + CHECK(static_cast(got) == Catch::Approx(V).epsilon(1e-12)); +} + +TEST_CASE("OmegaHConservativeProjection (3D): same-mesh reference tet is " + "exact for constant and linear fields", + "[transfer][mesh_intersection][3d][analytic]") +{ + // Purpose: source == target == one tet. Intersection is trivial; P1 must + // reproduce constants/linears at vertices. If this fails, bug is in + // RHS/mass/Apply/KSP — not multi-tet clipping. + Omega_h::Library lib; + Omega_h::Mesh mesh = pcms::test::BuildReferenceTet(lib); + REQUIRE(mesh.nverts() == 4); + REQUIRE(mesh.nelems() == 1); + auto space = pcms::test::MakeP1Space(mesh); + auto source = space->CreateFunction(); + auto target = space->CreateFunction(); + pcms::OmegaHConservativeProjection projection(*space, *space); + SECTION("constant field") + { + const double c = 2.5; + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real, pcms::Real, pcms::Real) { return c; }); + projection.Apply(source, target); + const auto values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + REQUIRE(static_cast(values.size()) == 4); + for (Omega_h::LO i = 0; i < 4; ++i) { + CAPTURE(i, values[i]); + REQUIRE(values[i] == Catch::Approx(c).margin(1e-12)); + } + } + SECTION("linear field") + { + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real x, pcms::Real y, pcms::Real z) { + return 1.0 + x + 2.0 * y + 3.0 * z; + }); + projection.Apply(source, target); + const auto values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + const auto coords_h = pcms::test::CopyCoordinatesToHost( + pcms::MakeConstRank2View(mesh.coords(), 3)); + for (Omega_h::LO i = 0; i < 4; ++i) { + const double expected = + 1.0 + coords_h(i, 0) + 2.0 * coords_h(i, 1) + 3.0 * coords_h(i, 2); + CAPTURE(i, expected, values[i]); + REQUIRE(values[i] == Catch::Approx(expected).margin(1e-12)); + } + } +} diff --git a/test/test_omega_h_3d_conservative_projection.cpp b/test/test_omega_h_3d_conservative_projection.cpp new file mode 100644 index 000000000..807c69701 --- /dev/null +++ b/test/test_omega_h_3d_conservative_projection.cpp @@ -0,0 +1,214 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include "field_test_utils.h" + +#include + +TEST_CASE("OmegaHConservativeProjection (3D tets) reproduces constant and " + "linear fields", + "[transfer][mesh_intersection][3d]") +{ + Omega_h::Library lib; + + // Two independent tessellations of the same unit cube. + Omega_h::Mesh source_mesh = pcms::test::BuildUnitCube(lib, 1); + Omega_h::Mesh target_mesh = pcms::test::BuildUnitCube(lib, 2); + + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); + + SECTION("constant field is preserved and conserved") + { + const double c = 2.0; + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real, pcms::Real, pcms::Real) { return c; }); + + projection.Apply(source, target); + + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + 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(c).margin(1e-9)); + } + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-9)); + } + + SECTION("linear field is reproduced on target vertices and conserved") + { + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real x, pcms::Real y, pcms::Real z) { + return 1.0 + x + 2.0 * y + 3.0 * z; + }); + + projection.Apply(source, target); + + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + const auto tgt_coords_h = pcms::test::CopyCoordinatesToHost( + pcms::MakeConstRank2View(target_mesh.coords(), 3)); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + const double expected = 1.0 + tgt_coords_h(i, 0) + + 2.0 * tgt_coords_h(i, 1) + + 3.0 * tgt_coords_h(i, 2); + REQUIRE(target_values[i] == Catch::Approx(expected).margin(1e-8)); + } + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-8)); + } +} + +TEST_CASE("OmegaHConservativeProjection (3D tets) conserves the integral for a " + "P0 target", + "[transfer][mesh_intersection][3d]") +{ + Omega_h::Library lib; + + Omega_h::Mesh source_mesh = pcms::test::BuildUnitCube(lib, 2); + Omega_h::Mesh target_mesh = pcms::test::BuildUnitCube(lib, 1); + + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP0Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + + pcms::OmegaHConservativeProjection projection(*source_space, *target_space); + + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real x, pcms::Real y, pcms::Real z) { + return 1.0 + x + 2.0 * y + 3.0 * z; + }); + + projection.Apply(source, target); + + REQUIRE(pcms::test::IntegrateP0Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-8)); +} + +TEST_CASE("Copy transfer (3D tets) reproduces the source field", + "[transfer][copy][3d]") +{ + Omega_h::Library lib; + Omega_h::Mesh mesh = pcms::test::BuildUnitCube(lib, 2); + auto space = pcms::test::MakeP1Space(mesh); + + auto source = space->CreateFunction(); + auto target = space->CreateFunction(); + + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real x, pcms::Real y, pcms::Real z) { + return 1.0 + x + 2.0 * y + 3.0 * z; + }); + + pcms::Copy copy(*space, *space); + copy.Apply(source, target); + + const auto sv = + pcms::FlattenToRank1View(source.GetDOFHolderDataHost().GetValues()); + const auto tv = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + REQUIRE(tv.size() == sv.size()); + for (std::size_t i = 0; i < tv.size(); ++i) { + REQUIRE(tv[i] == Catch::Approx(sv[i])); + } +} + +// Point interpolation evaluates the source field at each target DOF site. A +// linear field is reproduced exactly at the target vertices in 3D. +TEST_CASE("Interpolation transfer (3D tets) reproduces a linear field", + "[transfer][interpolation][3d]") +{ + Omega_h::Library lib; + Omega_h::Mesh source_mesh = pcms::test::BuildUnitCube(lib, 2); + Omega_h::Mesh target_mesh = pcms::test::BuildUnitCube(lib, 3); + + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real x, pcms::Real y, pcms::Real z) { + return 1.0 + x + 2.0 * y + 3.0 * z; + }); + + pcms::Interpolator interp(*source_space, *target_space); + interp.Apply(source, target); + + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + const auto tgt_coords_h = pcms::test::CopyCoordinatesToHost( + pcms::MakeConstRank2View(target_mesh.coords(), 3)); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + const double expected = 1.0 + tgt_coords_h(i, 0) + + 2.0 * tgt_coords_h(i, 1) + 3.0 * tgt_coords_h(i, 2); + REQUIRE(target_values[i] == Catch::Approx(expected).margin(1e-8)); + } +} + +// The Monte-Carlo/control-variate projection uses the source field interpolated +// onto the target space as a control variate, so a field already representable +// in the target P1 space (an affine function) is reproduced exactly and its +// integral conserved, even with very few stochastic samples. +TEST_CASE("OmegaHControlVariateProjection (3D tets) is exact for target-space " + "fields", + "[transfer][monte_carlo][3d]") +{ + Omega_h::Library lib; + + Omega_h::Mesh source_mesh = pcms::test::BuildUnitCube(lib, 1); + Omega_h::Mesh target_mesh = pcms::test::BuildUnitCube(lib, 2); + + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real x, pcms::Real y, pcms::Real z) { + return 1.0 + x + 2.0 * y + 3.0 * z; + }); + + pcms::OmegaHControlVariateProjection projection( + *source_space, *target_space, /*samples_per_element=*/8, + pcms::MonteCarloSampling::UniformRandom, /*seed=*/12345); + projection.Apply(source, target); + + const auto target_values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + const auto tgt_coords_h = pcms::test::CopyCoordinatesToHost( + pcms::MakeConstRank2View(target_mesh.coords(), 3)); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + const double expected = 1.0 + tgt_coords_h(i, 0) + + 2.0 * tgt_coords_h(i, 1) + 3.0 * tgt_coords_h(i, 2); + REQUIRE(target_values[i] == Catch::Approx(expected).margin(1e-8)); + } + REQUIRE(pcms::test::IntegrateP1Field(target_mesh, target) == + Catch::Approx(pcms::test::IntegrateP1Field(source_mesh, source)) + .margin(1e-8)); +} diff --git a/test/test_omega_h_field2_outofbounds.cpp b/test/test_omega_h_field2_outofbounds.cpp index 5e4c3813d..5a909e527 100644 --- a/test/test_omega_h_field2_outofbounds.cpp +++ b/test/test_omega_h_field2_outofbounds.cpp @@ -5,11 +5,12 @@ #include #include "pcms/field/function_space/lagrange.h" #include "pcms/field/function_space/spline.h" -#include "pcms/field/field_metadata.h" +#include "pcms/field/value_view.hpp" #include "pcms/utility/uniform_grid.h" #include "field_test_utils.h" #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::Real; @@ -20,11 +21,9 @@ TEST_CASE("omega_h_field2 out of bounds FILL mode") auto mesh = Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 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 + y; }); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); + pcms::test::SetField(field, OMEGA_H_LAMBDA(Real x, Real y) { return x + y; }); Real fill_value = -999.0; @@ -52,11 +51,9 @@ TEST_CASE("uniform_grid_field out of bounds FILL mode") grid.divisions = {N, N}; auto factory = pcms::LagrangeFunctionSpace::FromUniformGrid( - grid, 1, pcms::CoordinateSystem::Cartesian, 1); - auto field = factory.CreateField(pcms::FieldMetadata{}); - pcms::test::SetField( - field.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + y; }); + grid, 1, pcms::csys::Cartesian::Deferred(), 1); + auto field = factory->CreateFunction(); + pcms::test::SetField(field, OMEGA_H_LAMBDA(Real x, Real y) { return x + y; }); Real fill_value = -999.0; @@ -84,11 +81,9 @@ TEST_CASE("spline_field out of bounds FILL mode") grid.divisions = {N, N}; auto factory = pcms::SplineFunctionSpace::FromUniformGrid( - grid, 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; }); + grid, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); + pcms::test::SetField(field, OMEGA_H_LAMBDA(Real x, Real y) { return x + y; }); Real fill_value = -999.0; diff --git a/test/test_omega_h_form_integrator_utils.cpp b/test/test_omega_h_form_integrator_utils.cpp index 91cedac70..6f3865cc6 100644 --- a/test/test_omega_h_form_integrator_utils.cpp +++ b/test/test_omega_h_form_integrator_utils.cpp @@ -2,79 +2,145 @@ #include #include +#include +#include +#include + +// --------------------------------------------------------------------------- +// ForEachPolytopeFaceTriangle: the 3D face-walk that ForEachIntersectionSubtet +// relies on to star-decompose a clipped intersection polyhedron. +// --------------------------------------------------------------------------- + namespace { -r3d::Polytope<2> MakePoly(int nverts, - std::initializer_list> pts) +double TetVolume(const r3d::Vector<3>& a, const r3d::Vector<3>& b, + const r3d::Vector<3>& c, const r3d::Vector<3>& d) { - r3d::Polytope<2> poly; - poly.nverts = nverts; + const double bx = b[0] - a[0], by = b[1] - a[1], bz = b[2] - a[2]; + const double cx = c[0] - a[0], cy = c[1] - a[1], cz = c[2] - a[2]; + const double dx = d[0] - a[0], dy = d[1] - a[1], dz = d[2] - a[2]; + const double triple = bx * (cy * dz - cz * dy) - by * (cx * dz - cz * dx) + + bz * (cx * dy - cy * dx); + return std::abs(triple) / 6.0; +} + +// Star-decompose `poly` from its centroid exactly as ForEachIntersectionSubtet +// does, summing the sub-tet volumes and counting the emitted face triangles. +std::pair DecomposeFromCentroid(const r3d::Polytope<3>& poly) +{ + r3d::Vector<3> apex; + apex[0] = apex[1] = apex[2] = 0.0; + for (int v = 0; v < poly.nverts; ++v) { + apex[0] += poly.verts[v].pos[0]; + apex[1] += poly.verts[v].pos[1]; + apex[2] += poly.verts[v].pos[2]; + } + apex[0] /= poly.nverts; + apex[1] /= poly.nverts; + apex[2] /= poly.nverts; + + double total_vol = 0.0; + int ntri = 0; + pcms::detail::ForEachPolytopeFaceTriangle(poly, [&](const r3d::Vector<3>& a, + const r3d::Vector<3>& b, + const r3d::Vector<3>& c) { + total_vol += TetVolume(apex, a, b, c); + ++ntri; + }); + return {total_vol, ntri}; +} + +r3d::Few, 4> MakeTetVerts( + std::initializer_list> pts) +{ + r3d::Few, 4> verts; int i = 0; - for (auto [x, y] : pts) { - poly.verts[i].pos[0] = x; - poly.verts[i].pos[1] = y; - poly.verts[i].pnbrs[0] = -1; - poly.verts[i].pnbrs[1] = -1; + for (const auto& p : pts) { + verts[i][0] = p[0]; + verts[i][1] = p[1]; + verts[i][2] = p[2]; ++i; } - return poly; + return verts; } } // namespace -TEST_CASE( - "RemoveDuplicateVerticesAndFixLinks: no duplicates leaves triangle intact", - "[form_integrator_utils]") -{ - // A clean CCW triangle — nothing to remove, area should equal 0.5. - auto poly = MakePoly(3, {{0.0, 0.0}, {1.0, 0.0}, {0.0, 1.0}}); - const int n = pcms::detail::RemoveDuplicateVerticesAndFixLinks(poly); - REQUIRE(n == 3); - REQUIRE(poly.nverts == 3); - // The function sets correct pnbrs, so r3d::measure is valid after the call. - const double area = r3d::measure(poly); - REQUIRE(area == Catch::Approx(0.5).epsilon(1e-12)); -} - -TEST_CASE("RemoveDuplicateVerticesAndFixLinks: one duplicate vertex is removed", +TEST_CASE("ForEachPolytopeFaceTriangle: a tetrahedron yields 4 face triangles " + "that tile its volume", "[form_integrator_utils]") { - // 4-vertex polygon; last vertex duplicates the first within the default tol. - // After dedup: 3 unique vertices, area of the underlying triangle preserved. - auto poly = MakePoly(4, {{0.0, 0.0}, {1.0, 0.0}, {0.0, 1.0}, {5e-13, 0.0}}); - const int n = pcms::detail::RemoveDuplicateVerticesAndFixLinks(poly); - REQUIRE(n == 3); - REQUIRE(poly.nverts == 3); - const double area = r3d::measure(poly); - REQUIRE(area == Catch::Approx(0.5).epsilon(1e-10)); + // A tetrahedron has 4 triangular faces, so the walk must emit exactly 4 + // triangles (= 2*(nverts-2) for nverts==4), and the centroid star-tiling must + // reproduce the tet's volume (1/6 for the reference tet). + r3d::Polytope<3> poly; + r3d::init(poly, MakeTetVerts({{{0.0, 0.0, 0.0}}, + {{1.0, 0.0, 0.0}}, + {{0.0, 1.0, 0.0}}, + {{0.0, 0.0, 1.0}}})); + + const auto [vol, ntri] = DecomposeFromCentroid(poly); + REQUIRE(ntri == 4); + REQUIRE(ntri == 2 * (poly.nverts - 2)); + REQUIRE(vol == Catch::Approx(1.0 / 6.0).epsilon(1e-12)); + REQUIRE(vol == Catch::Approx(std::abs(r3d::measure(poly))).epsilon(1e-12)); } TEST_CASE( - "RemoveDuplicateVerticesAndFixLinks: polygon becomes degenerate after dedup", + "ForEachPolytopeFaceTriangle: a clipped polyhedron with quad faces is " + "tiled exactly", "[form_integrator_utils]") { - // 3 vertices where two positions are identical — only 2 unique remain. - // The function must mark the polygon degenerate: all pnbrs set to -1. - auto poly = MakePoly(3, {{0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0}}); - const int n = pcms::detail::RemoveDuplicateVerticesAndFixLinks(poly); - REQUIRE(n == 2); - REQUIRE(poly.nverts == 2); - for (int i = 0; i < 2; ++i) { - CAPTURE(i); - CHECK(poly.verts[i].pnbrs[0] == -1); - CHECK(poly.verts[i].pnbrs[1] == -1); - } + // Clip the reference tet with the plane x <= 0.5. This truncates the corner + // at (1,0,0), producing a polyhedron with a quadrilateral face, so at least + // one face must fan into more than one triangle. The removed piece is a tet + // similar to the original at scale 0.5 (volume (1/6)*0.5^3 = 1/48), leaving + // 1/6 - 1/48 = 7/48. + r3d::Polytope<3> poly; + r3d::init(poly, MakeTetVerts({{{0.0, 0.0, 0.0}}, + {{1.0, 0.0, 0.0}}, + {{0.0, 1.0, 0.0}}, + {{0.0, 0.0, 1.0}}})); + r3d::Few, 1> planes; + planes[0].n[0] = -1.0; // keep -x + 0.5 >= 0, i.e. x <= 0.5 + planes[0].n[1] = 0.0; + planes[0].n[2] = 0.0; + planes[0].d = 0.5; + r3d::clip(poly, planes); + + REQUIRE(poly.nverts > 4); // truncation added vertices / a quad face + const double measure = std::abs(r3d::measure(poly)); + REQUIRE(measure == Catch::Approx(7.0 / 48.0).epsilon(1e-12)); + + const auto [vol, ntri] = DecomposeFromCentroid(poly); + // Every face walked exactly once <=> the genus-0 fan invariant holds. + REQUIRE(ntri == 2 * (poly.nverts - 2)); + REQUIRE(ntri > 4); // a quad face fans into 2+ triangles + REQUIRE(vol == Catch::Approx(measure).epsilon(1e-12)); } -TEST_CASE( - "RemoveDuplicateVerticesAndFixLinks: near-tolerance vertex is retained", - "[form_integrator_utils]") +// --------------------------------------------------------------------------- +// ForEachPolytopeBoundarySimplex: the 2D counterpart, one edge per vertex. +// --------------------------------------------------------------------------- + +TEST_CASE("ForEachPolytopeBoundarySimplex: a triangle yields its 3 edges once, " + "in cycle order", + "[form_integrator_utils]") { - // Vertex at (0, 2e-12) differs from (0, 0) by 2e-12 in y, which exceeds the - // default tol=1e-12, so it must NOT be treated as a duplicate. - auto poly = MakePoly(3, {{0.0, 0.0}, {1.0, 0.0}, {0.0, 2e-12}}); - const int n = pcms::detail::RemoveDuplicateVerticesAndFixLinks(poly); - REQUIRE(n == 3); - REQUIRE(poly.nverts == 3); + const r3d::Few, 3> verts{{{0.0, 0.0}, {1.0, 0.0}, {0.0, 1.0}}}; + r3d::Polytope<2> poly; + r3d::init(poly, verts); + + int nedges = 0; + double shoelace = 0.0; + pcms::detail::ForEachPolytopeBoundarySimplex( + poly, [&](const r3d::Few, 2>& e) { + shoelace += 0.5 * (e[0][0] * e[1][1] - e[1][0] * e[0][1]); + ++nedges; + }); + REQUIRE(nedges == 3); + // Consistent orientation around the cycle: the edges alone integrate to the + // (signed) area. + REQUIRE(shoelace == Catch::Approx(0.5)); } diff --git a/test/test_omega_h_intersection_coverage.cpp b/test/test_omega_h_intersection_coverage.cpp new file mode 100644 index 000000000..b631d325f --- /dev/null +++ b/test/test_omega_h_intersection_coverage.cpp @@ -0,0 +1,92 @@ + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "field_test_utils.h" + +namespace +{ + +// Per-target-element fraction of its volume that the intersection quadrature +// accounted for, obtained by projecting u == 1 onto a P0 target space. +std::vector CoveragePerTargetElement(Omega_h::Mesh& source_mesh, + Omega_h::Mesh& target_mesh) +{ + auto source_space = pcms::test::MakeP0Space(source_mesh); + auto target_space = pcms::test::MakeP0Space(target_mesh); + + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real, pcms::Real, pcms::Real) { return 1.0; }); + + // Lumped: for a P0 target the mass matrix is diagonal either way, so this + // is an exact division by the element volume and adds no error of its own. + pcms::OmegaHConservativeProjection projection(*source_space, *target_space, + pcms::MassMatrixType::Lumped); + projection.Apply(source, target); + + const auto values = + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues()); + std::vector coverage(values.size()); + for (std::size_t i = 0; i < coverage.size(); ++i) { + coverage[i] = values[i]; + } + return coverage; +} + +} // namespace + +TEST_CASE("mesh intersection covers every target element", + "[transfer][mesh_intersection][3d][regression]") +{ + Omega_h::Library lib; + + // 10 -> 5 is the smallest nested pair that reproduces it. 12 -> 6, 12 -> 7, + // 12 -> 9, 12 -> 10 and 12 -> 11 fail too, with 1 to 7 empty elements. + const int source_divisions = 10; + const int target_divisions = 5; + + Omega_h::Mesh source_mesh = pcms::test::BuildUnitCube(lib, source_divisions); + Omega_h::Mesh target_mesh = pcms::test::BuildUnitCube(lib, target_divisions); + + const auto coverage = CoveragePerTargetElement(source_mesh, target_mesh); + REQUIRE(static_cast(coverage.size()) == target_mesh.nelems()); + + const auto sizes_h = Omega_h::HostRead( + Omega_h::measure_elements_real(&target_mesh)); + + // Report every miss rather than just the first, so a change in how many + // elements are affected is visible in the failure output. + std::vector empty; + pcms::Real covered_volume = 0.0; + pcms::Real total_volume = 0.0; + for (Omega_h::LO e = 0; e < target_mesh.nelems(); ++e) { + covered_volume += coverage[e] * sizes_h[e]; + total_volume += sizes_h[e]; + if (coverage[e] <= 1e-12) { + empty.push_back(e); + } + } + CAPTURE(empty.size(), covered_volume, total_volume); + CHECK(empty.empty()); + + for (Omega_h::LO e = 0; e < target_mesh.nelems(); ++e) { + CAPTURE(e, coverage[e]); + REQUIRE(coverage[e] == Catch::Approx(1.0).margin(1e-9)); + } + REQUIRE(covered_volume == Catch::Approx(total_volume).margin(1e-9)); +} diff --git a/test/test_omega_h_intersection_rhs_integrator.cpp b/test/test_omega_h_intersection_rhs_integrator.cpp index 18a087c85..aa6488c1a 100644 --- a/test/test_omega_h_intersection_rhs_integrator.cpp +++ b/test/test_omega_h_intersection_rhs_integrator.cpp @@ -13,6 +13,8 @@ #include #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/field/coordinate_systems/cylindrical.hpp" // --------------------------------------------------------------------------- // Helpers @@ -21,37 +23,6 @@ namespace { -// Build a unit-square 2D simplex mesh. -// diagonal=0: T0=(0,1,3), T1=(1,2,3) -// diagonal=1: T0=(0,1,2), T1=(0,2,3) -Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib, int diagonal) -{ - const Omega_h::Reals coords({0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0}); - Omega_h::LOs ev2v = (diagonal == 0) ? Omega_h::LOs({0, 1, 3, 1, 2, 3}) - : Omega_h::LOs({0, 1, 2, 0, 2, 3}); - Omega_h::Mesh mesh(&lib); - Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 2, ev2v, coords); - mesh.add_tag( - 0, "class_dim", 1, - Omega_h::Read(mesh.nverts(), Omega_h::I8(0))); - mesh.add_tag( - 0, "class_id", 1, - Omega_h::Read(mesh.nverts(), Omega_h::ClassId(0))); - mesh.add_tag( - 1, "class_dim", 1, - Omega_h::Read(mesh.nedges(), Omega_h::I8(1))); - mesh.add_tag( - 1, "class_id", 1, - Omega_h::Read(mesh.nedges(), Omega_h::ClassId(0))); - mesh.add_tag( - 2, "class_dim", 1, - Omega_h::Read(mesh.nelems(), Omega_h::I8(2))); - mesh.add_tag( - 2, "class_id", 1, - Omega_h::Read(mesh.nelems(), Omega_h::ClassId(0))); - return mesh; -} - // Independent reference for the assembled conservative load vector // b_j = \int phi_j^target f dx // when f is exactly representable in the target order-1 space (constant or @@ -95,20 +66,12 @@ std::unordered_map ExpectedLoadByGid( // Assembles the integrator's load vector for source_field and compares it, // per global DOF id, against the supplied expected values. void CheckAssembledLoadMatches( - pcms::FunctionSpace& source_space, pcms::LinearFormIntegrator& integrator, + const std::shared_ptr& source_space, + pcms::LinearFormIntegrator& integrator, const pcms::Field& source_field, const std::unordered_map& expected) { - const auto pts = integrator.GetIntegrationPoints(); - const std::size_t npts = pts.GetValues().extent(0); - - auto evaluator = source_space.CreatePointEvaluator( - pcms::EvaluationRequest::FromCoordinates(pts)); - Kokkos::View sampled("sampled", npts, - 1); - evaluator->Evaluate(source_field, pcms::MakeRank2View(sampled)); - - integrator.Assemble(pcms::MakeConstRank2View(sampled)); + pcms::test::EvaluateAndAssemble(integrator, source_space, source_field); Vec vec = integrator.GetVector(); const PetscScalar* vec_array = nullptr; @@ -133,26 +96,17 @@ TEST_CASE("OmegaHIntersectionRHSIntegrator: integration points lie inside " "[rhs_integrator]") { Omega_h::Library lib; - auto source_mesh = BuildUnitSquare(lib, 1); - auto target_mesh = BuildUnitSquare(lib, 0); + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 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_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); auto integrator = - pcms::BuildOmegaHConservativeRHSIntegrator(source_space, target_space); + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); const auto raw_coords = integrator->GetIntegrationPoints().GetValues(); - auto raw_coords_view = Kokkos::View>( - raw_coords.data_handle(), raw_coords.extent(0), raw_coords.extent(1)); - auto raw_coords_host = - Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, raw_coords_view); + auto raw_coords_host = pcms::test::CopyCoordinatesToHost(raw_coords); const std::size_t n = raw_coords_host.extent(0); REQUIRE(n > 0); @@ -165,37 +119,61 @@ TEST_CASE("OmegaHIntersectionRHSIntegrator: integration points lie inside " } } +// The space constructor borrows the source space's point search for the +// intersection; the layout constructor builds its own. Both must produce the +// same quadrature data, point for point. +TEST_CASE("OmegaHIntersectionRHSIntegrator: space and layout constructors " + "build identical integration points", + "[rhs_integrator]") +{ + Omega_h::Library lib; + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 0); + + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + pcms::OmegaHIntersectionRHSIntegrator from_spaces(*source_space, + *target_space); + pcms::OmegaHIntersectionRHSIntegrator from_layouts( + std::dynamic_pointer_cast( + source_space->GetLayout()), + source_space->GetCoordinateSystem(), + std::dynamic_pointer_cast( + target_space->GetLayout()), + target_space->GetCoordinateSystem()); + + const auto a = from_spaces.GetIntegrationPoints().GetValues(); + const auto b = from_layouts.GetIntegrationPoints().GetValues(); + REQUIRE(a.extent(0) == b.extent(0)); + REQUIRE(a.extent(1) == b.extent(1)); + REQUIRE(a.extent(0) > 0); + auto a_host = pcms::test::CopyCoordinatesToHost(a); + auto b_host = pcms::test::CopyCoordinatesToHost(b); + for (std::size_t i = 0; i < a_host.extent(0); ++i) { + for (std::size_t d = 0; d < a_host.extent(1); ++d) { + REQUIRE(a_host(i, d) == b_host(i, d)); + } + } +} + TEST_CASE("OmegaHIntersectionRHSIntegrator: zero source field gives zero " "assembled vector", "[rhs_integrator]") { Omega_h::Library lib; - auto source_mesh = BuildUnitSquare(lib, 1); - auto target_mesh = BuildUnitSquare(lib, 0); + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 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_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); - auto source_field = source_space.CreateField(); + auto source_field = source_space->CreateFunction(); // Default-constructed field has zero data. auto integrator = - pcms::BuildOmegaHConservativeRHSIntegrator(source_space, target_space); - const auto pts = integrator->GetIntegrationPoints(); - const std::size_t npts = pts.GetValues().extent(0); - - auto evaluator = source_space.CreatePointEvaluator( - pcms::EvaluationRequest::FromCoordinates(pts)); - - Kokkos::View sampled("sampled", npts, - 1); - evaluator->Evaluate(source_field, pcms::MakeRank2View(sampled)); - - integrator->Assemble(pcms::MakeConstRank2View(sampled)); + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); + pcms::test::EvaluateAndAssemble(*integrator, source_space, source_field); Vec vec = integrator->GetVector(); PetscReal norm = 0.0; @@ -212,29 +190,25 @@ TEST_CASE("OmegaHIntersectionRHSIntegrator: constant field matches " // For a constant source field the assembled load vector must equal the // target consistent mass matrix applied to the constant nodal vector. Omega_h::Library lib; - auto source_mesh = BuildUnitSquare(lib, 1); - auto target_mesh = BuildUnitSquare(lib, 0); + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 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_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); const double c = 2.0; - auto source_field = source_space.CreateField(); + auto source_field = source_space->CreateFunction(); pcms::test::SetField( source_field, OMEGA_H_LAMBDA(pcms::Real, pcms::Real) { return c; }); const auto target_layout = std::dynamic_pointer_cast( - target_space.GetLayout()); + target_space->GetLayout()); const std::vector g(target_mesh.nverts(), c); const auto expected = ExpectedLoadByGid(target_mesh, *target_layout, g); auto integrator = - pcms::BuildOmegaHConservativeRHSIntegrator(source_space, target_space); + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); CheckAssembledLoadMatches(source_space, *integrator, source_field, expected); } @@ -249,33 +223,29 @@ TEST_CASE( // vector must equal the target consistent mass matrix applied to the nodal // values of x + y. Omega_h::Library lib; - auto source_mesh = BuildUnitSquare(lib, 1); - auto target_mesh = BuildUnitSquare(lib, 0); + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 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_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); - auto source_field = source_space.CreateField(); + auto source_field = source_space->CreateFunction(); pcms::test::SetField( source_field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + y; }); const auto target_layout = std::dynamic_pointer_cast( - target_space.GetLayout()); - const auto tgt_coords_h = - Omega_h::HostRead(target_mesh.coords()); + target_space->GetLayout()); + const auto tgt_coords_h = pcms::test::CopyCoordinatesToHost( + pcms::MakeConstRank2View(target_mesh.coords(), 2)); std::vector g(target_mesh.nverts()); for (int i = 0; i < target_mesh.nverts(); ++i) { - g[i] = tgt_coords_h[2 * i + 0] + tgt_coords_h[2 * i + 1]; + g[i] = tgt_coords_h(i, 0) + tgt_coords_h(i, 1); } const auto expected = ExpectedLoadByGid(target_mesh, *target_layout, g); auto integrator = - pcms::BuildOmegaHConservativeRHSIntegrator(source_space, target_space); + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); CheckAssembledLoadMatches(source_space, *integrator, source_field, expected); } @@ -283,54 +253,190 @@ TEST_CASE("OmegaHIntersectionRHSIntegrator: rejects invalid layouts", "[rhs_integrator]") { Omega_h::Library lib; - auto source_mesh = BuildUnitSquare(lib, 1); - auto target_mesh = BuildUnitSquare(lib, 0); + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 0); SECTION("multi-component source space throws") { auto source_space = pcms::LagrangeFunctionSpace::FromMesh( - source_mesh, 1, 2, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto target_space = pcms::LagrangeFunctionSpace::FromMesh( - target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + source_mesh, 1, 2, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::test::MakeP1Space(target_mesh); REQUIRE_THROWS( - pcms::BuildOmegaHConservativeRHSIntegrator(source_space, target_space)); + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space)); } SECTION("multi-component target space throws") { - auto source_space = pcms::LagrangeFunctionSpace::FromMesh( - source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto source_space = pcms::test::MakeP1Space(source_mesh); auto target_space = pcms::LagrangeFunctionSpace::FromMesh( - target_mesh, 1, 2, pcms::CoordinateSystem::Cartesian, "global", + target_mesh, 1, 2, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); REQUIRE_THROWS( - pcms::BuildOmegaHConservativeRHSIntegrator(source_space, target_space)); + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space)); } SECTION("non-Cartesian source coordinate system throws") { auto source_space = pcms::LagrangeFunctionSpace::FromMesh( - source_mesh, 1, 1, pcms::CoordinateSystem::Cylindrical, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto target_space = pcms::LagrangeFunctionSpace::FromMesh( - target_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", + source_mesh, 1, 1, pcms::csys::CylindricalRZ::Create(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto target_space = pcms::test::MakeP1Space(target_mesh); REQUIRE_THROWS( - pcms::BuildOmegaHConservativeRHSIntegrator(source_space, target_space)); + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space)); } SECTION("non-Cartesian target coordinate system throws") { - auto source_space = pcms::LagrangeFunctionSpace::FromMesh( - source_mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto source_space = pcms::test::MakeP1Space(source_mesh); auto target_space = pcms::LagrangeFunctionSpace::FromMesh( - target_mesh, 1, 1, pcms::CoordinateSystem::Cylindrical, "global", + target_mesh, 1, 1, pcms::csys::CylindricalRZ::Create(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); REQUIRE_THROWS( - pcms::BuildOmegaHConservativeRHSIntegrator(source_space, target_space)); + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space)); + } +} + +TEST_CASE("OmegaHIntersectionRHSIntegrator (3D): constant load sums to c times " + "known tet-tet overlap", + "[rhs_integrator][3d]") +{ + // Source is the reference tet (volume 1/6). Target is the similar tet at the + // (1,0,0) corner, cut by the plane x=0.5: + // (1,0,0), (0.5,0,0), (0.5,0.5,0), (0.5,0,0.5) + // which sits entirely inside the source (volume (1/6)*(1/2)^3 = 1/48). + // checks PCMS search + quadrature + P1 partition of unity: + // sum_j b_j = \int_overlap f dx = c * 1/48. + Omega_h::Library lib; + auto source_mesh = pcms::test::BuildReferenceTet(lib); + auto target_mesh = pcms::test::BuildTet(lib, Omega_h::Reals({ + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5, + 0.5, + 0.0, + 0.5, + 0.0, + 0.5, + })); + + REQUIRE(source_mesh.nelems() == 1); + REQUIRE(target_mesh.nelems() == 1); + + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + constexpr double c = 3.0; + constexpr double overlap = 1.0 / 48.0; + auto source_field = source_space->CreateFunction(); + pcms::test::SetField( + source_field, + KOKKOS_LAMBDA(pcms::Real, pcms::Real, pcms::Real) { return c; }); + + auto integrator = + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); + + const auto raw_coords = integrator->GetIntegrationPoints().GetValues(); + REQUIRE(raw_coords.extent(0) > 0); + REQUIRE(raw_coords.extent(1) == 3); + auto coords_h = pcms::test::CopyCoordinatesToHost(raw_coords); + for (std::size_t i = 0; i < coords_h.extent(0); ++i) { + const double x = coords_h(i, 0); + const double y = coords_h(i, 1); + const double z = coords_h(i, 2); + CAPTURE(i, x, y, z); + CHECK(x >= 0.5 - 1e-12); + CHECK(y >= -1e-12); + CHECK(z >= -1e-12); + CHECK(x + y + z <= 1.0 + 1e-12); + } + + pcms::test::EvaluateAndAssemble(*integrator, source_space, source_field); + PetscScalar sum = 0.0; + VecSum(integrator->GetVector(), &sum); + CHECK(static_cast(sum) == + Catch::Approx(c * overlap).margin(1e-12)); +} + +TEST_CASE("OmegaHIntersectionRHSIntegrator (3D): dual tets overlap is an " + "octahedron; load sums to integral over the intersection", + "[rhs_integrator][3d]") +{ + // Dual tets in the cube [-1,1]^3. Intersection = {|x|+|y|+|z| <= 1}, + // volume 4/3. + Omega_h::Library lib; + auto source_mesh = pcms::test::BuildTet(lib, Omega_h::Reals({ + 1.0, + 1.0, + 1.0, + 1.0, + -1.0, + -1.0, + -1.0, + -1.0, + 1.0, + -1.0, + 1.0, + -1.0, + })); + auto target_mesh = pcms::test::BuildTet(lib, Omega_h::Reals({ + -1.0, + -1.0, + -1.0, + -1.0, + 1.0, + 1.0, + 1.0, + -1.0, + 1.0, + 1.0, + 1.0, + -1.0, + })); + + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + auto integrator = + pcms::BuildOmegaHConservativeRHSIntegrator(*source_space, *target_space); + + const auto raw_coords = integrator->GetIntegrationPoints().GetValues(); + REQUIRE(raw_coords.extent(0) > 0); + auto coords_h = pcms::test::CopyCoordinatesToHost(raw_coords); + for (std::size_t i = 0; i < coords_h.extent(0); ++i) { + const double s = std::abs(coords_h(i, 0)) + std::abs(coords_h(i, 1)) + + std::abs(coords_h(i, 2)); + CAPTURE(i, coords_h(i, 0), coords_h(i, 1), coords_h(i, 2), s); + CHECK(s <= 1.0 + 1e-10); + } + + SECTION("constant f=c => sum(b) = c * 4/3") + { + constexpr double c = 3.0; + auto source_field = source_space->CreateFunction(); + pcms::test::SetField( + source_field, + KOKKOS_LAMBDA(pcms::Real, pcms::Real, pcms::Real) { return c; }); + pcms::test::EvaluateAndAssemble(*integrator, source_space, source_field); + PetscScalar sum = 0.0; + VecSum(integrator->GetVector(), &sum); + CHECK(static_cast(sum) == + Catch::Approx(c * 4.0 / 3.0).margin(1e-10)); + } + + SECTION("f=x+y+z => sum(b) = 0 by symmetry") + { + auto source_field = source_space->CreateFunction(); + pcms::test::SetField( + source_field, KOKKOS_LAMBDA(pcms::Real x, pcms::Real y, pcms::Real z) { + return x + y + z; + }); + pcms::test::EvaluateAndAssemble(*integrator, source_space, source_field); + PetscScalar sum = 0.0; + VecSum(integrator->GetVector(), &sum); + CHECK(static_cast(sum) == Catch::Approx(0.0).margin(1e-10)); } } diff --git a/test/test_omega_h_lagrange_field.cpp b/test/test_omega_h_lagrange_field.cpp index 259a01653..65871ac54 100644 --- a/test/test_omega_h_lagrange_field.cpp +++ b/test/test_omega_h_lagrange_field.cpp @@ -6,7 +6,7 @@ #include "pcms/field/layout/omega_h_lagrange.h" #include "pcms/field/function_space/lagrange.h" -#include "pcms/field/field_metadata.h" +#include "pcms/field/value_view.hpp" #include "pcms/utility/arrays.h" #include "pcms/utility/mesh_geometry.h" #include "field_test_utils.h" @@ -14,6 +14,7 @@ #include #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::LO; using pcms::Real; @@ -31,7 +32,7 @@ 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); + pcms::csys::Cartesian::Deferred()); REQUIRE(layout.GetOrder() == 1); REQUIRE(layout.GetNumComponents() == 2); @@ -42,8 +43,7 @@ TEST_CASE("OmegaHLagrangeLayout order-1 properties") // DOF holder coordinates should match vertex coordinates auto coords_device = layout.GetDOFHolderCoordinates().GetValues(); int nverts = mesh.nents(0); - auto coords_view = - pcms::test::CopyCoordinatesToHost(coords_device, nverts, mesh.dim()); + auto coords_view = pcms::test::CopyCoordinatesToHost(coords_device); 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()); @@ -66,7 +66,7 @@ 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); + pcms::csys::Cartesian::Deferred()); REQUIRE(layout.GetOrder() == 0); REQUIRE(layout.GetNumComponents() == 1); @@ -76,8 +76,7 @@ TEST_CASE("OmegaHLagrangeLayout order-0 properties") // DOF holder coordinates should match element centroids auto coords_device = layout.GetDOFHolderCoordinates().GetValues(); int nelems = mesh.nelems(); - auto coords_view = - pcms::test::CopyCoordinatesToHost(coords_device, nelems, mesh.dim()); + auto coords_view = pcms::test::CopyCoordinatesToHost(coords_device); auto centroids = Omega_h::HostRead(pcms::get_entity_centroids(mesh, mesh.dim())); REQUIRE(static_cast(coords_view.extent(0)) == nelems); @@ -101,10 +100,10 @@ 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), + pcms::OmegaHLagrangeLayout(mesh, 2, 1, pcms::csys::Cartesian::Deferred()), std::invalid_argument); REQUIRE_THROWS_AS( - pcms::OmegaHLagrangeLayout(mesh, -1, 1, pcms::CoordinateSystem::Cartesian), + pcms::OmegaHLagrangeLayout(mesh, -1, 1, pcms::csys::Cartesian::Deferred()), std::invalid_argument); } @@ -113,10 +112,10 @@ 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); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); - auto f1 = factory.CreateField(pcms::FieldMetadata{}); - auto f2 = factory.CreateField(pcms::FieldMetadata{}); + auto f1 = factory->CreateFunction(); + auto f2 = factory->CreateFunction(); REQUIRE(&f1.GetLayout() == &f2.GetLayout()); } @@ -128,37 +127,67 @@ 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{}); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); - int n = factory.GetLayout()->GetNumOwnedDofHolder(); + int n = factory->GetLayout()->GetNumOwnedDofHolder(); std::vector data(n); for (int i = 0; i < n; ++i) data[i] = static_cast(i); pcms::Rank2View view(data.data(), n, 1); - field.GetData().SetDOFHolderDataHost(view); + field.SetDOFHolderDataHost( + pcms::ValueView(pcms::ValueBasis{}, + view)); - auto got = pcms::FlattenToRank1View(field.GetData().GetDOFHolderDataHost()); + auto got = pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(static_cast(got.size()) == n); for (int i = 0; i < n; ++i) REQUIRE(got[i] == Catch::Approx(data[i])); } +TEST_CASE("Field checked writes validate the view shape, not just total size") +{ + auto lib = Omega_h::Library{}; + auto mesh = MakeBox2D(lib.world()); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); + + const int n = factory->GetLayout()->GetNumOwnedDofHolder(); + std::vector data(static_cast(n), 1.0); + const auto& basis = field.GetData().GetValueBasis(); + + // Same total size but the wrong shape: every scalar basis compares equal, + // so only the extent check can catch a [1][n] view aimed at an [n][1] + // field. + pcms::Rank2View reshaped(data.data(), 1, + n); + REQUIRE_THROWS_AS( + field.SetDOFHolderDataHost( + pcms::ValueView(basis, reshaped)), + std::invalid_argument); + + // The matching shape passes. + pcms::Rank2View ok(data.data(), n, 1); + REQUIRE_NOTHROW(field.SetDOFHolderDataHost( + pcms::ValueView(basis, ok))); +} + 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{}); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); pcms::test::CheckEvaluation( factory, field, pcms::test::StandardEvalCoords2D(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); } // The same linear evaluation test run on the MeshFields-backed order-1 field @@ -168,15 +197,15 @@ 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{}); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); pcms::test::CheckEvaluation( factory, field, pcms::test::StandardEvalCoords2D(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); } TEST_CASE("OmegaHLagrangeField order-1: out-of-bounds FILL mode") @@ -184,56 +213,18 @@ 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{}); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); pcms::test::SetField( - field.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, 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}; + auto outside = pcms::test::StandardOutsideCoords2D(); 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()); -} - -TEST_CASE( - "OmegaHLagrangeField order-1: multi-component serialize / deserialize " - "round-trip") -{ - auto lib = Omega_h::Library{}; - auto mesh = MakeBox2D(lib.world()); - const int nc = 3; - // The MeshFields backend is scalar-only; multi-component needs OmegaH. - auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, nc, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field = factory.CreateField(pcms::FieldMetadata{}); - - const int n = factory.GetLayout()->GetNumOwnedDofHolder(); - std::vector data(static_cast(n) * nc); - for (int i = 0; i < n; ++i) - for (int c = 0; c < nc; ++c) - data[static_cast(i) * nc + c] = i + 0.25 * c; - field.GetData().SetDOFHolderDataHost( - pcms::Rank2View(data.data(), n, nc)); - - pcms::test::CheckSerializeDeserialize(*factory.GetLayout(), field.GetData()); -} - // ---- Order-0 field tests ---------------------------------------------------- TEST_CASE("OmegaHLagrangeField order-0: set/get DOF data round-trip") @@ -241,15 +232,17 @@ 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{}); + mesh, 0, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); - int n = factory.GetLayout()->GetNumOwnedDofHolder(); + int n = factory->GetLayout()->GetNumOwnedDofHolder(); std::vector data(n, 3.14); pcms::Rank2View view(data.data(), n, 1); - field.GetData().SetDOFHolderDataHost(view); + field.SetDOFHolderDataHost( + pcms::ValueView(pcms::ValueBasis{}, + view)); - auto got = pcms::FlattenToRank1View(field.GetData().GetDOFHolderDataHost()); + auto got = pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(static_cast(got.size()) == n); for (int i = 0; i < n; ++i) REQUIRE(got[i] == Catch::Approx(3.14)); @@ -260,34 +253,21 @@ 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{}); + mesh, 0, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); const Real kValue = 42.0; int nelems = mesh.nelems(); std::vector data(nelems, kValue); pcms::Rank2View view(data.data(), nelems, 1); - 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); + field.SetDOFHolderDataHost( + pcms::ValueView(pcms::ValueBasis{}, + view)); - for (int i = 0; i < n; ++i) - REQUIRE(eval_host(i) == Catch::Approx(kValue)); + pcms::test::CheckEvaluation( + factory, field, pcms::test::StandardEvalCoords2D(), + OMEGA_H_LAMBDA(Real, Real) { return kValue; }); } TEST_CASE("OmegaHLagrangeField order-0: out-of-bounds FILL mode") @@ -295,39 +275,22 @@ 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{}); + mesh, 0, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); int nelems = mesh.nelems(); std::vector data(nelems, 1.0); pcms::Rank2View view(data.data(), nelems, 1); - field.GetData().SetDOFHolderDataHost(view); + field.SetDOFHolderDataHost( + pcms::ValueView(pcms::ValueBasis{}, + 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::Rank2View view(data.data(), nelems, - 1); - field.GetData().SetDOFHolderDataHost(view); - - pcms::test::CheckSerializeDeserialize(*factory.GetLayout(), field.GetData()); -} - // ---- Layout sharing communicator contract ----------------------------------- // // BEHAVIORAL CONTRACT (requires MPI/redev — exercised by @@ -352,12 +315,13 @@ TEST_CASE("OmegaHLagrangeField: field valid after layout destruction") std::optional> field; { auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - field.emplace(factory.CreateField(pcms::FieldMetadata{})); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + field.emplace(factory->CreateFunction()); } // 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; }); + *field, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); // Just verify data was set correctly (no evaluator needed for this lifetime // test) auto data = field->GetDOFHolderDataHost(); diff --git a/test/test_omega_h_mass_integrator.cpp b/test/test_omega_h_mass_integrator.cpp index 939283d1b..ca3b1dc78 100644 --- a/test/test_omega_h_mass_integrator.cpp +++ b/test/test_omega_h_mass_integrator.cpp @@ -4,17 +4,22 @@ #include #include #include +#include #include #include #include #include +#include "field_test_utils.h" #include #include +#include #include #include #include #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/field/coordinate_systems/cylindrical.hpp" // --------------------------------------------------------------------------- // Helpers @@ -23,32 +28,30 @@ namespace { -Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib) +// dummy coordinate system that's public interfaces gives +// same result as Cartesian should fail equality +class OtherCartesian2 final : public pcms::CoordinateSystem { - const Omega_h::Reals coords({0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0}); - Omega_h::LOs ev2v({0, 1, 3, 1, 2, 3}); - Omega_h::Mesh mesh(&lib); - Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 2, ev2v, coords); - mesh.add_tag( - 0, "class_dim", 1, - Omega_h::Read(mesh.nverts(), Omega_h::I8(0))); - mesh.add_tag( - 0, "class_id", 1, - Omega_h::Read(mesh.nverts(), Omega_h::ClassId(0))); - mesh.add_tag( - 1, "class_dim", 1, - Omega_h::Read(mesh.nedges(), Omega_h::I8(1))); - mesh.add_tag( - 1, "class_id", 1, - Omega_h::Read(mesh.nedges(), Omega_h::ClassId(0))); - mesh.add_tag( - 2, "class_dim", 1, - Omega_h::Read(mesh.nelems(), Omega_h::I8(2))); - mesh.add_tag( - 2, "class_id", 1, - Omega_h::Read(mesh.nelems(), Omega_h::ClassId(0))); - return mesh; -} +public: + [[nodiscard]] std::string_view Kind() const noexcept override + { + return "cartesian"; + } + [[nodiscard]] int Dimension() const noexcept override { return 2; } + [[nodiscard]] bool HasOrthogonalBasis() const noexcept override + { + return true; + } + [[nodiscard]] bool HasUnitScaleFactors() const noexcept override + { + return true; + } + [[nodiscard]] bool operator==( + const pcms::CoordinateSystem& other) const noexcept override + { + return dynamic_cast(&other) != nullptr; + } +}; std::map, pcms::Real> BuildReferenceMassMap( Omega_h::Mesh& mesh, const pcms::FunctionSpace& space) @@ -60,7 +63,11 @@ std::map, pcms::Real> BuildReferenceMassMap( omf(mesh); auto coordField = omf.getCoordField(); const auto [shp, map] = MeshField::Omegah::getTriangleElement<1>(mesh); +#if MeshFields_VERSION < 10000 MeshField::FieldElement coordFe(mesh.nelems(), coordField, shp, map); +#else + MeshField::FieldElement coordFe(mesh.nelems(), coordField.field, shp, map); +#endif auto elm_mass_dev = buildElementMassMatrix(mesh, coordFe); auto elm_mass_host = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, elm_mass_dev); @@ -98,15 +105,13 @@ TEST_CASE( "[mass_integrator]") { Omega_h::Library lib; - auto mesh = BuildUnitSquare(lib); + auto mesh = pcms::test::BuildUnitSquare(lib, 0); - auto space = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto space = pcms::test::MakeP1Space(mesh); - const auto ref = BuildReferenceMassMap(mesh, space); + const auto ref = BuildReferenceMassMap(mesh, *space); - auto integrator = pcms::BuildOmegaHMassIntegrator(space); + auto integrator = pcms::BuildOmegaHMassIntegrator(*space); Mat mat = integrator->GetMatrix(); for (const auto& [key, ref_val] : ref) { @@ -128,19 +133,17 @@ TEST_CASE("OmegaHMassIntegrator: row sums match lumped mass", // each node: row_sum(i) = integral(N_i dx). For a regular mesh of unit // area with uniform nodal distribution the sum over all nodes equals 1. Omega_h::Library lib; - auto mesh = BuildUnitSquare(lib); + auto mesh = pcms::test::BuildUnitSquare(lib, 0); - auto space = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); + auto space = pcms::test::MakeP1Space(mesh); const auto layout = std::dynamic_pointer_cast( - space.GetLayout()); + space->GetLayout()); const auto& gids = layout->GetGidsHost(); const int nverts = mesh.nverts(); - auto integrator = pcms::BuildOmegaHMassIntegrator(space); + auto integrator = pcms::BuildOmegaHMassIntegrator(*space); Mat mat = integrator->GetMatrix(); // Sum all row sums. @@ -166,24 +169,136 @@ TEST_CASE("OmegaHMassIntegrator: row sums match lumped mass", // mat is owned by integrator; no MatDestroy here. } +TEST_CASE("OmegaHMassIntegrator: lumped diagonal equals consistent row sums", + "[mass_integrator]") +{ + // Row-sum lumping: M_L(i,i) = sum_j M_C(i,j) = integral(N_i dx), all + // off-diagonal entries zero. The trace equals the domain area. + Omega_h::Library lib; + auto mesh = pcms::test::BuildUnitSquare(lib, 0); + + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + const auto layout = + std::dynamic_pointer_cast( + space->GetLayout()); + const auto& gids = layout->GetGidsHost(); + const int nverts = mesh.nverts(); + + auto consistent = pcms::BuildOmegaHMassIntegrator(*space); + auto lumped = + pcms::BuildOmegaHMassIntegrator(*space, pcms::MassMatrixType::Lumped); + CHECK_FALSE(consistent->IsDiagonal()); + CHECK(lumped->IsDiagonal()); + Mat consistent_mat = consistent->GetMatrix(); + Mat lumped_mat = lumped->GetMatrix(); + + pcms::Real trace = 0.0; + for (int v = 0; v < nverts; ++v) { + const PetscInt row = static_cast(gids[v]); + pcms::Real row_sum = 0.0; + for (int u = 0; u < nverts; ++u) { + PetscInt r = row; + PetscInt c = static_cast(gids[u]); + PetscScalar val = 0.0; + MatGetValues(consistent_mat, 1, &r, 1, &c, &val); + row_sum += static_cast(val); + + PetscScalar lumped_val = 0.0; + MatGetValues(lumped_mat, 1, &r, 1, &c, &lumped_val); + CAPTURE(v, u, row_sum, lumped_val); + if (r == c) { + trace += static_cast(lumped_val); + } else { + CHECK(static_cast(lumped_val) == + Catch::Approx(0.0).margin(1e-14)); + } + } + PetscInt r = row; + PetscScalar diag = 0.0; + MatGetValues(lumped_mat, 1, &r, 1, &r, &diag); + CAPTURE(v, row_sum, diag); + CHECK(static_cast(diag) == + Catch::Approx(row_sum).epsilon(1e-12)); + } + + // Trace == area of domain == 1. + CHECK(trace == Catch::Approx(1.0).epsilon(1e-10)); + // matrices are owned by the integrators; no MatDestroy here. +} + +TEST_CASE("OmegaHMassIntegrator: P0 lumped equals consistent", + "[mass_integrator]") +{ + // P0 basis functions have disjoint support, so the consistent mass matrix + // is already diagonal (M_ee = area(e)) and lumping is a no-op. + Omega_h::Library lib; + auto mesh = pcms::test::BuildUnitSquare(lib, 0); + + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 0, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + + auto consistent = pcms::BuildOmegaHMassIntegrator(*space); + auto lumped = + pcms::BuildOmegaHMassIntegrator(*space, pcms::MassMatrixType::Lumped); + CHECK(consistent->IsDiagonal()); + CHECK(lumped->IsDiagonal()); + Mat consistent_mat = consistent->GetMatrix(); + Mat lumped_mat = lumped->GetMatrix(); + + const auto elem_areas = Omega_h::measure_elements_real(&mesh); + const auto elem_areas_h = Omega_h::HostRead(elem_areas); + const auto layout = + std::dynamic_pointer_cast( + space->GetLayout()); + const auto& gids = layout->GetGidsHost(); + + for (int e = 0; e < mesh.nelems(); ++e) { + PetscInt r = static_cast(gids[e]); + PetscScalar consistent_val = 0.0; + PetscScalar lumped_val = 0.0; + MatGetValues(consistent_mat, 1, &r, 1, &r, &consistent_val); + MatGetValues(lumped_mat, 1, &r, 1, &r, &lumped_val); + CAPTURE(e, elem_areas_h[e]); + CHECK(static_cast(consistent_val) == + Catch::Approx(elem_areas_h[e]).epsilon(1e-12)); + CHECK(static_cast(lumped_val) == + Catch::Approx(elem_areas_h[e]).epsilon(1e-12)); + } + // matrices are owned by the integrators; no MatDestroy here. +} + TEST_CASE("OmegaHMassIntegrator: rejects invalid layouts", "[mass_integrator]") { Omega_h::Library lib; - auto mesh = BuildUnitSquare(lib); + auto mesh = pcms::test::BuildUnitSquare(lib, 0); SECTION("multi-component space throws") { auto space = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 2, pcms::CoordinateSystem::Cartesian, "global", + mesh, 1, 2, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - REQUIRE_THROWS(pcms::BuildOmegaHMassIntegrator(space)); + REQUIRE_THROWS(pcms::BuildOmegaHMassIntegrator(*space)); } SECTION("non-Cartesian coordinate system throws") { auto space = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cylindrical, "global", + mesh, 1, 1, pcms::csys::CylindricalRZ::Create(), "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + REQUIRE_THROWS(pcms::BuildOmegaHMassIntegrator(*space)); + } + + SECTION("distinct cartesian-kind system throws") + { + const std::shared_ptr other = + std::make_shared(); + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, other, "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - REQUIRE_THROWS(pcms::BuildOmegaHMassIntegrator(space)); + REQUIRE_THROWS(pcms::BuildOmegaHMassIntegrator(*space)); } } diff --git a/test/test_omega_h_mc_rhs_integrator.cpp b/test/test_omega_h_mc_rhs_integrator.cpp index 92726f6dd..1559a246d 100644 --- a/test/test_omega_h_mc_rhs_integrator.cpp +++ b/test/test_omega_h_mc_rhs_integrator.cpp @@ -1,11 +1,15 @@ #include #include +#include #include #include #include #include #include +#include +#include #include +#include #include #include #include @@ -15,68 +19,6 @@ #include #include -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -namespace -{ - -// Build a unit-square 2D simplex mesh. -// diagonal=0: T0=(0,1,3), T1=(1,2,3) -// diagonal=1: T0=(0,1,2), T1=(0,2,3) -Omega_h::Mesh BuildUnitSquare(Omega_h::Library& lib, int diagonal) -{ - const Omega_h::Reals coords({0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0}); - Omega_h::LOs ev2v = (diagonal == 0) ? Omega_h::LOs({0, 1, 3, 1, 2, 3}) - : Omega_h::LOs({0, 1, 2, 0, 2, 3}); - Omega_h::Mesh mesh(&lib); - Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, 2, ev2v, coords); - mesh.add_tag( - 0, "class_dim", 1, - Omega_h::Read(mesh.nverts(), Omega_h::I8(0))); - mesh.add_tag( - 0, "class_id", 1, - Omega_h::Read(mesh.nverts(), Omega_h::ClassId(0))); - mesh.add_tag( - 1, "class_dim", 1, - Omega_h::Read(mesh.nedges(), Omega_h::I8(1))); - mesh.add_tag( - 1, "class_id", 1, - Omega_h::Read(mesh.nedges(), Omega_h::ClassId(0))); - mesh.add_tag( - 2, "class_dim", 1, - Omega_h::Read(mesh.nelems(), Omega_h::I8(2))); - mesh.add_tag( - 2, "class_id", 1, - Omega_h::Read(mesh.nelems(), Omega_h::ClassId(0))); - return mesh; -} - -pcms::LagrangeFunctionSpace MakeP1Space(Omega_h::Mesh& mesh) -{ - return pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::OmegaH); -} - -// Evaluates source_field at the integrator's sample points and assembles. -void EvaluateAndAssemble(pcms::LinearFormIntegrator& integrator, - const pcms::FunctionSpace& source_space, - const pcms::Field& source_field) -{ - const auto pts = integrator.GetIntegrationPoints(); - const std::size_t npts = pts.GetValues().extent(0); - auto evaluator = source_space.CreatePointEvaluator( - pcms::EvaluationRequest::FromCoordinates(pts)); - Kokkos::View sampled("sampled", npts, - 1); - evaluator->Evaluate(source_field, pcms::MakeRank2View(sampled)); - integrator.Assemble(pcms::MakeConstRank2View(sampled)); -} - -} // namespace - // --------------------------------------------------------------------------- // Monte Carlo RHS integrator // --------------------------------------------------------------------------- @@ -85,20 +27,15 @@ TEST_CASE("OmegaHMonteCarloRHSIntegrator: sample points lie inside the domain", "[mc_rhs_integrator]") { Omega_h::Library lib; - auto target_mesh = BuildUnitSquare(lib, 0); - auto target_space = MakeP1Space(target_mesh); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 0); + auto target_space = pcms::test::MakeP1Space(target_mesh); const int samples_per_element = 16; for (const auto sampling : {pcms::MonteCarloSampling::UniformRandom}) { pcms::OmegaHMonteCarloRHSIntegrator integrator( - target_space, samples_per_element, sampling); + *target_space, samples_per_element, sampling); const auto raw_coords = integrator.GetIntegrationPoints().GetValues(); - auto coords_view = Kokkos::View>( - raw_coords.data_handle(), raw_coords.extent(0), raw_coords.extent(1)); - auto coords_h = - Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, coords_view); + auto coords_h = pcms::test::CopyCoordinatesToHost(raw_coords); REQUIRE( coords_h.extent(0) == @@ -119,18 +56,18 @@ TEST_CASE("OmegaHMonteCarloRHSIntegrator: constant field integrates exactly", // For f = c the estimator sums to c * |domain| for any sample placement, // because the P1 basis functions partition unity at every sample point. Omega_h::Library lib; - auto source_mesh = BuildUnitSquare(lib, 1); - auto target_mesh = BuildUnitSquare(lib, 0); - auto source_space = MakeP1Space(source_mesh); - auto target_space = MakeP1Space(target_mesh); + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 0); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); - auto source_field = source_space.CreateField(); + auto source_field = source_space->CreateFunction(); pcms::test::SetField( source_field, OMEGA_H_LAMBDA(pcms::Real, pcms::Real) { return 2.0; }); for (const auto sampling : {pcms::MonteCarloSampling::UniformRandom}) { - pcms::OmegaHMonteCarloRHSIntegrator integrator(target_space, 8, sampling); - EvaluateAndAssemble(integrator, source_space, source_field); + pcms::OmegaHMonteCarloRHSIntegrator integrator(*target_space, 8, sampling); + pcms::test::EvaluateAndAssemble(integrator, source_space, source_field); PetscScalar sum = 0.0; VecSum(integrator.GetVector(), &sum); @@ -150,30 +87,36 @@ TEST_CASE("OmegaHControlVariateProjection: exact for fields in the target " // For a globally linear field the control variate (target-space interpolant // of the source field) equals the source field, so the sampled residual is // identically zero and the projection is exact regardless of sample count. + // That holds for any invertible mass matrix (delta = M^-1 0 = 0), so the + // lumped variant must be exact here too. Omega_h::Library lib; - auto source_mesh = BuildUnitSquare(lib, 1); - auto target_mesh = BuildUnitSquare(lib, 0); - auto source_space = MakeP1Space(source_mesh); - auto target_space = MakeP1Space(target_mesh); + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 0); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); - auto source_field = source_space.CreateField(); - auto target_field = target_space.CreateField(); + auto source_field = source_space->CreateFunction(); + auto target_field = target_space->CreateFunction(); pcms::test::SetField( source_field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return 3.0 * x - y + 0.25; }); + const auto mass_type = + GENERATE(pcms::MassMatrixType::Consistent, pcms::MassMatrixType::Lumped); + CAPTURE(static_cast(mass_type)); + pcms::OmegaHControlVariateProjection projection( - source_space, target_space, /*samples_per_element=*/4, - pcms::MonteCarloSampling::UniformRandom); + *source_space, *target_space, /*samples_per_element=*/4, + pcms::MonteCarloSampling::UniformRandom, /*seed=*/8675309, mass_type); projection.Apply(source_field, target_field); const auto values = - pcms::FlattenToRank1View(target_field.GetDOFHolderDataHost()); - const auto coords_h = Omega_h::HostRead(target_mesh.coords()); + pcms::FlattenToRank1View(target_field.GetDOFHolderDataHost().GetValues()); + const auto coords_h = pcms::test::CopyCoordinatesToHost( + pcms::MakeConstRank2View(target_mesh.coords(), 2)); REQUIRE(static_cast(values.size()) == target_mesh.nverts()); for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { - const double expected = - 3.0 * coords_h[2 * i + 0] - coords_h[2 * i + 1] + 0.25; + const double expected = 3.0 * coords_h(i, 0) - coords_h(i, 1) + 0.25; CAPTURE(i, expected, values[i]); CHECK(values[i] == Catch::Approx(expected).margin(1e-9)); } @@ -187,48 +130,48 @@ TEST_CASE("OmegaHControlVariateProjection: reduces error vs plain Monte Carlo", // seeds the control-variate projection must be closer to the exact // (intersection-quadrature) projection than the plain Monte Carlo one. Omega_h::Library lib; - auto source_mesh = BuildUnitSquare(lib, 1); - auto target_mesh = BuildUnitSquare(lib, 0); - auto source_space = MakeP1Space(source_mesh); - auto target_space = MakeP1Space(target_mesh); + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 0); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); - auto source_field = source_space.CreateField(); + auto source_field = source_space->CreateFunction(); pcms::test::SetField( source_field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x * x + x * y + 0.5 * y * y; }); // Reference: exact mesh-intersection quadrature projection. - auto reference_field = target_space.CreateField(); - pcms::OmegaHConservativeProjection reference_projection(source_space, - target_space); + auto reference_field = target_space->CreateFunction(); + pcms::OmegaHConservativeProjection reference_projection(*source_space, + *target_space); reference_projection.Apply(source_field, reference_field); - const auto reference = - pcms::FlattenToRank1View(reference_field.GetDOFHolderDataHost()); + const auto reference = pcms::FlattenToRank1View( + reference_field.GetDOFHolderDataHost().GetValues()); const int samples_per_element = 64; const uint64_t seed = 20240611; // Plain Monte Carlo projection. pcms::OmegaHMonteCarloRHSIntegrator mc_integrator( - target_space, samples_per_element, pcms::MonteCarloSampling::UniformRandom, + *target_space, samples_per_element, pcms::MonteCarloSampling::UniformRandom, seed); - auto evaluator = source_space.CreatePointEvaluator( + auto evaluator = source_space->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates( mc_integrator.GetIntegrationPoints())); - pcms::OmegaHMassIntegrator mass_integrator(target_space); + pcms::OmegaHMassIntegrator mass_integrator(*target_space); pcms::GalerkinProjectionSolver solver(mass_integrator, mc_integrator); const auto mc_projected = solver.Solve(*evaluator, source_field); const auto mc_projected_h = Omega_h::HostRead(mc_projected); // Control-variate projection with the same sampling configuration. - auto cv_field = target_space.CreateField(); + auto cv_field = target_space->CreateFunction(); pcms::OmegaHControlVariateProjection cv_projection( - source_space, target_space, samples_per_element, + *source_space, *target_space, samples_per_element, pcms::MonteCarloSampling::UniformRandom, seed); cv_projection.Apply(source_field, cv_field); const auto cv_values = - pcms::FlattenToRank1View(cv_field.GetDOFHolderDataHost()); + pcms::FlattenToRank1View(cv_field.GetDOFHolderDataHost().GetValues()); double mc_error = 0.0; double cv_error = 0.0; @@ -239,3 +182,193 @@ TEST_CASE("OmegaHControlVariateProjection: reduces error vs plain Monte Carlo", CAPTURE(mc_error, cv_error); CHECK(cv_error < mc_error); } + +// --------------------------------------------------------------------------- +// 3D Monte Carlo RHS integrator +// --------------------------------------------------------------------------- + +TEST_CASE("OmegaHMonteCarloRHSIntegrator (3D): sample points lie inside the " + "unit cube", + "[mc_rhs_integrator][3d]") +{ + // for Dim=3 sample coordinates inside [0,1]^3 with correct count. + Omega_h::Library lib; + auto target_mesh = pcms::test::BuildUnitCube(lib, 1); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + const int samples_per_element = 16; + pcms::OmegaHMonteCarloRHSIntegrator integrator( + *target_space, samples_per_element, + pcms::MonteCarloSampling::UniformRandom); + + const auto raw_coords = integrator.GetIntegrationPoints().GetValues(); + REQUIRE(raw_coords.extent(1) == 3); + + auto coords_h = pcms::test::CopyCoordinatesToHost(raw_coords); + + REQUIRE(coords_h.extent(0) == + static_cast(target_mesh.nelems() * samples_per_element)); + + for (std::size_t i = 0; i < coords_h.extent(0); ++i) { + CAPTURE(i, coords_h(i, 0), coords_h(i, 1), coords_h(i, 2)); + CHECK(coords_h(i, 0) >= -1e-12); + CHECK(coords_h(i, 0) <= 1.0 + 1e-12); + CHECK(coords_h(i, 1) >= -1e-12); + CHECK(coords_h(i, 1) <= 1.0 + 1e-12); + CHECK(coords_h(i, 2) >= -1e-12); + CHECK(coords_h(i, 2) <= 1.0 + 1e-12); + } +} + +TEST_CASE("OmegaHMonteCarloRHSIntegrator (3D): constant field integrates " + "exactly", + "[mc_rhs_integrator][3d]") +{ + // if f=c => VecSum(b) = c * volume = c * 1 (P1 partition of unity). + Omega_h::Library lib; + auto source_mesh = pcms::test::BuildUnitCube(lib, 1); + auto target_mesh = pcms::test::BuildUnitCube(lib, 1); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + auto source_field = source_space->CreateFunction(); + pcms::test::SetField( + source_field, + KOKKOS_LAMBDA(pcms::Real, pcms::Real, pcms::Real) { return 2.0; }); + + pcms::OmegaHMonteCarloRHSIntegrator integrator( + *target_space, /*samples_per_element=*/8, + pcms::MonteCarloSampling::UniformRandom); + + pcms::test::EvaluateAndAssemble(integrator, source_space, source_field); + + PetscScalar sum = 0.0; + VecSum(integrator.GetVector(), &sum); + CHECK(static_cast(sum) == Catch::Approx(2.0).margin(1e-12)); +} + +TEST_CASE( + "OmegaHControlVariateProjection (3D): reduces error vs plain Monte Carlo", + "[mc_rhs_integrator][control_variate][3d]") +{ + Omega_h::Library lib; + auto source_mesh = pcms::test::BuildUnitCube(lib, 1); + auto target_mesh = pcms::test::BuildUnitCube(lib, 2); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + auto source = source_space->CreateFunction(); + pcms::test::SetField( + source, KOKKOS_LAMBDA(pcms::Real x, pcms::Real y, pcms::Real z) { + return x * x + y * y + z * z; + }); + + auto reference = target_space->CreateFunction(); + pcms::OmegaHConservativeProjection ref_proj(*source_space, *target_space); + ref_proj.Apply(source, reference); + const auto ref_vals = + pcms::FlattenToRank1View(reference.GetDOFHolderDataHost().GetValues()); + + const int nsample = 64; + const uint64_t seed = 20240611; + + pcms::OmegaHMonteCarloRHSIntegrator mc( + *target_space, nsample, pcms::MonteCarloSampling::UniformRandom, seed); + auto ev = source_space->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(mc.GetIntegrationPoints())); + pcms::OmegaHMassIntegrator mass(*target_space); + pcms::GalerkinProjectionSolver solver(mass, mc); + const auto mc_x = solver.Solve(*ev, source); + const auto mc_h = Omega_h::HostRead(mc_x); + + auto cv_field = target_space->CreateFunction(); + pcms::OmegaHControlVariateProjection cv( + *source_space, *target_space, nsample, + pcms::MonteCarloSampling::UniformRandom, seed); + cv.Apply(source, cv_field); + const auto cv_vals = + pcms::FlattenToRank1View(cv_field.GetDOFHolderDataHost().GetValues()); + + double mc_err = 0.0, cv_err = 0.0; + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + mc_err = std::max(mc_err, std::abs(mc_h[i] - ref_vals[i])); + cv_err = std::max(cv_err, std::abs(cv_vals[i] - ref_vals[i])); + } + CAPTURE(mc_err, cv_err); + CHECK(cv_err < mc_err); +} + +namespace +{ + +Kokkos::View SwapXYPoints( + const pcms::CoordinateView& query_points) +{ + const auto in = query_points.GetValues(); + Kokkos::View out("swapped", + in.extent(0), 2); + Kokkos::parallel_for( + "swap_xy", + Kokkos::RangePolicy( + 0, static_cast(in.extent(0))), + KOKKOS_LAMBDA(const pcms::LO i) { + out(i, 0) = in(i, 1); + out(i, 1) = in(i, 0); + }); + return out; +} + +class SwapXY final : public pcms::CoordinateMap +{ +public: + SwapXY() + : CoordinateMap(pcms::csys::Cartesian::Create(2), + pcms::csys::Cartesian::Create(2)) + { + } + +protected: + [[nodiscard]] std::unique_ptr BindImpl( + const pcms::CoordinateView& query_points) + const override + { + return std::make_unique( + GetTargetCoordinateSystem(), SwapXYPoints(query_points)); + } +}; + +} // namespace + +TEST_CASE("OmegaHControlVariateProjection over a TransformedSource maps the " + "sample points", + "[mc_rhs_integrator][control_variate]") +{ + Omega_h::Library lib; + auto source_mesh = pcms::test::BuildUnitSquare(lib, 1); + auto target_mesh = pcms::test::BuildUnitSquare(lib, 0); + auto source_space = pcms::test::MakeP1Space(source_mesh); + auto target_space = pcms::test::MakeP1Space(target_mesh); + + auto source_field = source_space->CreateFunction(); + auto target_field = target_space->CreateFunction(); + pcms::test::SetField( + source_field, + OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return 3.0 * x - y + 0.25; }); + + pcms::TransformedSource src_as_swapped(source_space, + std::make_shared()); + pcms::OmegaHControlVariateProjection projection( + src_as_swapped, *target_space, /*samples_per_element=*/4, + pcms::MonteCarloSampling::UniformRandom); + projection.Apply(source_field, target_field); + + const auto values = + pcms::FlattenToRank1View(target_field.GetDOFHolderDataHost().GetValues()); + const auto coords_h = pcms::test::CopyCoordinatesToHost( + pcms::MakeConstRank2View(target_mesh.coords(), 2)); + for (Omega_h::LO i = 0; i < target_mesh.nverts(); ++i) { + const double expected = 3.0 * coords_h(i, 1) - coords_h(i, 0) + 0.25; + CAPTURE(i, expected, values[i]); + CHECK(values[i] == Catch::Approx(expected).margin(1e-9)); + } +} diff --git a/test/test_omega_h_same_mesh_p0_to_p1.cpp b/test/test_omega_h_same_mesh_p0_to_p1.cpp new file mode 100644 index 000000000..1e9116a03 --- /dev/null +++ b/test/test_omega_h_same_mesh_p0_to_p1.cpp @@ -0,0 +1,605 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "field_test_utils.h" + +#include +#include +#include +#include +#include + +// Same-mesh P0->P1 projection has closed-form answers, which makes it a sharp +// test of the intersection quadrature: every target element is clipped against +// itself, the pieces must tile it exactly, and the lumped projection must equal +// the element-nodal average to roundoff. Both dimensions run through the same +// decomposition code except for the boundary walk, so a geometric slip in the +// shared path shows up in both. + +namespace +{ + +// Tets taken verbatim from the PUMI-Tally wistell-D mesh. Clipping each one +// against itself with r3d gives signed distances of ~1e-13 on every face plane, +// and r3d then interpolates new vertices at O(1) fractions along the edges +// between two on-plane vertices. The resulting polytope has the right volume +// but a folded face graph whose triangles come out in both orientations, so a +// star decomposition that takes |volume| per piece over-counts (by 28.6%, +// 1.7% and 9.0% respectively). Any element sharing a face plane with a source +// element can hit this; the same-mesh case hits it for every element. +const std::vector kFoldingTetCoords = { + 30.198030285537243, + -936.1389388516545, + 483.16848456836186, + 20.132020190358162, + -979.0909772444453, + 485.85648258043625, + 0.0, + -966.3369691371918, + 483.16848456836186, + 15.099015142768621, + -951.2379539944232, + 468.06946942559324, + + -452.97045428305864, + 392.57439371198416, + 241.58424228406392, + -457.87167295326776, + 423.1168047352564, + 234.21738729666106, + -482.6122426477242, + 412.69480663896707, + 261.65815839972544, + -460.4863470953233, + 422.81883403751397, + 272.4806339440611, + + -620.6927499466186, + 1017.0028114430277, + 82.31454644833995, + -634.1586359962821, + 1056.9310599938033, + 90.59409085637769, + -637.7105881441516, + 1022.8656314424136, + 102.40466297536494, + -603.9377425704466, + 1022.2805800406807, + 87.57395423730232, +}; + +template +constexpr int kVertsPerElem = Dim + 1; + +// Signed measure of the simplex whose Dim+1 vertices are packed in `c`. +template +double SignedSimplexMeasure(const double* c) +{ + if constexpr (Dim == 2) { + const double bx = c[2] - c[0], by = c[3] - c[1]; + const double dx = c[4] - c[0], dy = c[5] - c[1]; + return 0.5 * (bx * dy - by * dx); + } else { + const double b[3] = {c[3] - c[0], c[4] - c[1], c[5] - c[2]}; + const double d[3] = {c[6] - c[0], c[7] - c[1], c[8] - c[2]}; + const double e[3] = {c[9] - c[0], c[10] - c[1], c[11] - c[2]}; + return (b[0] * (d[1] * e[2] - d[2] * e[1]) - + b[1] * (d[0] * e[2] - d[2] * e[0]) + + b[2] * (d[0] * e[1] - d[1] * e[0])) / + 6.0; + } +} + +// Random, positively oriented, non-sliver simplices at the coordinate scale of +// a tokamak/stellarator mesh (~1e3), so shared-plane roundoff is +// representative. +template +std::vector RandomSimplexCoords(int n, unsigned seed) +{ + constexpr int nv = kVertsPerElem; + constexpr double min_measure = (Dim == 3) ? 1e3 : 1e2; + std::mt19937 gen(seed); + std::uniform_real_distribution base(-1900.0, 1900.0); + std::uniform_real_distribution offset(-50.0, 50.0); + std::vector coords; + coords.reserve(static_cast(Dim) * nv * n); + while (static_cast(coords.size()) < Dim * nv * n) { + double c[Dim * nv]; + for (int d = 0; d < Dim; ++d) { + c[d] = base(gen); + } + for (int v = 1; v < nv; ++v) { + for (int d = 0; d < Dim; ++d) { + c[Dim * v + d] = c[d] + offset(gen); + } + } + const double measure = SignedSimplexMeasure(c); + if (std::fabs(measure) < min_measure) { + continue; + } + if (measure < 0.0) { + for (int d = 0; d < Dim; ++d) { + std::swap(c[Dim * (nv - 2) + d], c[Dim * (nv - 1) + d]); + } + } + coords.insert(coords.end(), c, c + Dim * nv); + } + return coords; +} + +// One mesh holding mutually disjoint simplices. +template +Omega_h::Mesh BuildDisjointSimplices(Omega_h::Library& lib, + const std::vector& coords) +{ + constexpr int nv = kVertsPerElem; + const int nelems = static_cast(coords.size()) / (Dim * nv); + Omega_h::HostWrite ev2v(nv * nelems); + for (int i = 0; i < nv * nelems; ++i) { + ev2v[i] = i; + } + Omega_h::HostWrite x(static_cast(coords.size())); + for (std::size_t i = 0; i < coords.size(); ++i) { + x[static_cast(i)] = coords[i]; + } + Omega_h::Mesh mesh(&lib); + Omega_h::build_from_elems_and_coords(&mesh, OMEGA_H_SIMPLEX, Dim, + Omega_h::LOs(ev2v.write()), + Omega_h::Reals(x.write())); + pcms::test::AddDefaultClassification(mesh); + return mesh; +} + +// Unit box tessellation moved to non-dyadic coordinates of magnitude ~1e3. On +// the unit box every on-plane distance is exactly zero and r3d never folds; +// this is what a real mesh looks like to the clipper. +template +Omega_h::Mesh BuildScaledBox(Omega_h::Library& lib, int n) +{ + auto mesh = (Dim == 3) ? Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1.0, + 1.0, 1.0, n, n, n) + : Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1.0, + 1.0, 0.0, n, n, 0); + const auto coords = Omega_h::HostRead(mesh.coords()); + Omega_h::HostWrite scaled(coords.size()); + constexpr double scale = 937.31; + constexpr std::array shift = {-1894.78, 211.7, -598.52}; + for (Omega_h::LO v = 0; v < mesh.nverts(); ++v) { + for (int d = 0; d < Dim; ++d) { + scaled[Dim * v + d] = coords[Dim * v + d] * scale + shift[d]; + } + } + mesh.set_coords(Omega_h::Reals(scaled.write())); + return mesh; +} + +// Sum of the sub-simplex measures handed to the integrand for target element +// e when the source mesh is the target mesh itself and the intersection map is +// the identity. Exactly |e| for a correct decomposition. +template +Omega_h::HostRead SelfDecompositionMeasures(Omega_h::Mesh& mesh) +{ + const int nelems = mesh.nelems(); + const auto coords = mesh.coords(); + const auto e2n = mesh.ask_down(Dim, Omega_h::VERT).ab2b; + Omega_h::Write offsets(nelems + 1); + Omega_h::Write indices(nelems); + Omega_h::parallel_for(nelems + 1, OMEGA_H_LAMBDA(int i) { offsets[i] = i; }); + Omega_h::parallel_for(nelems, OMEGA_H_LAMBDA(int i) { indices[i] = i; }); + const pcms::IntersectionResults self{Omega_h::LOs(offsets), + Omega_h::LOs(indices)}; + + Omega_h::Write sums(nelems, 0.0); + Omega_h::parallel_for( + nelems, OMEGA_H_LAMBDA(int e) { + Omega_h::Real acc = 0.0; + pcms::detail::ForEachIntersectionSubsimplex( + e, self, coords, coords, e2n, e2n, + [&](const Omega_h::Few, Dim + 1>&, int, + Omega_h::Real measure) { acc += measure; }); + sums[e] = acc; + }); + return Omega_h::HostRead(sums); +} + +template +void RequireSelfDecompositionExact(Omega_h::Mesh& mesh) +{ + const auto sums = SelfDecompositionMeasures(mesh); + const auto sizes = + Omega_h::HostRead(Omega_h::measure_elements_real(&mesh)); + for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { + REQUIRE(sums[e] == Catch::Approx(std::fabs(sizes[e])).epsilon(1e-12)); + } +} + +// The lumped P1 mass matrix is diagonal with M_ii = sum_{e in i} |e|/(Dim+1) +// and the exact P0 load vector is b_i = sum_{e in i} |e|/(Dim+1) f_e, so the +// lumped projection of a P0 field onto P1 on the same mesh is the +// measure-weighted average of the incident element values. Nothing here is +// approximate; the operator must reproduce it to roundoff. +template +std::vector ElementNodalAverage(Omega_h::Mesh& mesh, + const std::vector& elem_values) +{ + constexpr int nv = kVertsPerElem; + const auto sizes = + Omega_h::HostRead(Omega_h::measure_elements_real(&mesh)); + const auto e2v = Omega_h::HostRead(mesh.ask_elem_verts()); + std::vector numerator(mesh.nverts(), 0.0); + std::vector denominator(mesh.nverts(), 0.0); + for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { + const double w = std::fabs(sizes[e]) / nv; + for (int k = 0; k < nv; ++k) { + const auto v = e2v[nv * e + k]; + numerator[v] += w * elem_values[e]; + denominator[v] += w; + } + } + for (Omega_h::LO v = 0; v < mesh.nverts(); ++v) { + numerator[v] /= denominator[v]; + } + return numerator; +} + +template +std::vector ToVector(const View& values) +{ + std::vector out(values.size()); + for (std::size_t i = 0; i < out.size(); ++i) { + out[i] = values[i]; + } + return out; +} + +// Zeroth and first moments (integrals of 1 and of each coordinate times the +// field) of a P0 element field and of a P1 vertex field, exact on simplices. +template +std::array P0Moments(Omega_h::Mesh& mesh, + const std::vector& f) +{ + constexpr int nv = kVertsPerElem; + const auto sizes = + Omega_h::HostRead(Omega_h::measure_elements_real(&mesh)); + const auto e2v = Omega_h::HostRead(mesh.ask_elem_verts()); + const auto x = Omega_h::HostRead(mesh.coords()); + std::array m{}; + for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { + const double measure = std::fabs(sizes[e]); + m[0] += measure * f[e]; + for (int d = 0; d < Dim; ++d) { + double centroid = 0.0; + for (int k = 0; k < nv; ++k) { + centroid += x[Dim * e2v[nv * e + k] + d]; + } + m[1 + d] += measure * f[e] * centroid / nv; + } + } + return m; +} + +template +std::array P1Moments(Omega_h::Mesh& mesh, + const std::vector& u) +{ + constexpr int nv = kVertsPerElem; + // int_T phi_i phi_j = |T| (1 + delta_ij) / ((Dim+1)(Dim+2)), so + // int_T x u_h = |T| / ((Dim+1)(Dim+2)) * ((sum_i u_i)(sum_j x_j) + + // sum_i u_i x_i). + constexpr double mass_factor = 1.0 / ((Dim + 1) * (Dim + 2)); + const auto sizes = + Omega_h::HostRead(Omega_h::measure_elements_real(&mesh)); + const auto e2v = Omega_h::HostRead(mesh.ask_elem_verts()); + const auto x = Omega_h::HostRead(mesh.coords()); + std::array m{}; + for (Omega_h::LO e = 0; e < mesh.nelems(); ++e) { + const double measure = std::fabs(sizes[e]); + double sum_u = 0.0; + for (int k = 0; k < nv; ++k) { + sum_u += u[e2v[nv * e + k]]; + } + m[0] += measure * sum_u / nv; + for (int d = 0; d < Dim; ++d) { + double sum_x = 0.0; + double sum_ux = 0.0; + for (int k = 0; k < nv; ++k) { + const double xk = x[Dim * e2v[nv * e + k] + d]; + sum_x += xk; + sum_ux += u[e2v[nv * e + k]] * xk; + } + m[1 + d] += measure * mass_factor * (sum_u * sum_x + sum_ux); + } + } + return m; +} + +// A P0 field that varies from element to element at the ~10% level, so a +// misattributed or double-counted piece of any element is visible. The arity +// of operator() selects the dimension in pcms::test::SetField. +template +struct SourceFunction; + +template <> +struct SourceFunction<2> +{ + KOKKOS_INLINE_FUNCTION pcms::Real operator()(pcms::Real x, pcms::Real y) const + { + return 10.0 + Kokkos::sin(0.011 * x) + 0.5 * Kokkos::cos(0.017 * y); + } +}; + +template <> +struct SourceFunction<3> +{ + KOKKOS_INLINE_FUNCTION pcms::Real operator()(pcms::Real x, pcms::Real y, + pcms::Real z) const + { + return 10.0 + Kokkos::sin(0.011 * x) + 0.5 * Kokkos::cos(0.017 * y) + + 0.001 * z; + } +}; + +template +std::vector ProjectSameMesh(Omega_h::Mesh& mesh, + pcms::MassMatrixType mass_type, + std::vector& source_values) +{ + auto source_space = pcms::test::MakeP0Space(mesh); + auto target_space = pcms::test::MakeP1Space(mesh); + auto source = source_space->CreateFunction(); + auto target = target_space->CreateFunction(); + pcms::test::SetField(source, SourceFunction{}); + + pcms::OmegaHConservativeProjection projection(*source_space, *target_space, + mass_type); + projection.Apply(source, target); + + source_values = ToVector( + pcms::FlattenToRank1View(source.GetDOFHolderDataHost().GetValues())); + return ToVector( + pcms::FlattenToRank1View(target.GetDOFHolderDataHost().GetValues())); +} + +template +void RequireLumpedEqualsNodalAverage(Omega_h::Mesh& mesh) +{ + std::vector f; + const auto u = ProjectSameMesh(mesh, pcms::MassMatrixType::Lumped, f); + REQUIRE(static_cast(u.size()) == mesh.nverts()); + const auto expected = ElementNodalAverage(mesh, f); + for (Omega_h::LO v = 0; v < mesh.nverts(); ++v) { + REQUIRE(u[v] == Catch::Approx(expected[v]).epsilon(1e-12)); + } + REQUIRE(P1Moments(mesh, u)[0] == + Catch::Approx(P0Moments(mesh, f)[0]).epsilon(1e-12)); +} + +template +void RequireConsistentPreservesMoments(Omega_h::Mesh& mesh) +{ + std::vector f; + const auto u = + ProjectSameMesh(mesh, pcms::MassMatrixType::Consistent, f); + const auto m0 = P0Moments(mesh, f); + const auto m1 = P1Moments(mesh, u); + for (int k = 0; k <= Dim; ++k) { + REQUIRE(m1[k] == Catch::Approx(m0[k]).epsilon(1e-9)); + } +} + +// The polytope r3d produced when clipping tet 38125 above against itself in +// an optimized build, captured verbatim (positions to 1e-6, graph exact). The +// four original vertices appear three or four times each and three new +// vertices sit at 1/2 and 2/3 along edges; two of the faces the walk emits are +// mirror images that must cancel. Whether a given build reproduces this fold +// from the coordinates depends on the compiler's floating-point contraction, +// so the polytope is pinned here rather than regenerated. +r3d::Polytope<3> FoldedSelfClipPolytope() +{ + constexpr int nverts = 14; + constexpr double pos[nverts][3] = { + {0.000000, -966.336969, 483.168485}, {20.132020, -979.090977, 485.856483}, + {7.549508, -958.787462, 475.618977}, {0.000000, -966.336969, 483.168485}, + {30.198030, -936.138939, 483.168485}, {15.099015, -951.237954, 468.069469}, + {22.648523, -943.688446, 475.618977}, {30.198030, -936.138939, 483.168485}, + {0.000000, -966.336969, 483.168485}, {20.132020, -979.090977, 485.856483}, + {20.132020, -979.090977, 485.856483}, {13.421347, -974.839641, 484.960483}, + {20.132020, -979.090977, 485.856483}, {30.198030, -936.138939, 483.168485}, + }; + constexpr int pnbrs[nverts][3] = { + {3, 2, 8}, {4, 9, 10}, {0, 5, 11}, {0, 4, 6}, {1, 7, 3}, + {2, 6, 12}, {3, 7, 5}, {4, 13, 6}, {0, 11, 9}, {1, 8, 10}, + {1, 9, 13}, {2, 12, 8}, {5, 13, 11}, {7, 10, 12}, + }; + r3d::Polytope<3> poly; + poly.nverts = nverts; + for (int v = 0; v < nverts; ++v) { + for (int d = 0; d < 3; ++d) { + poly.verts[v].pos[d] = pos[v][d]; + poly.verts[v].pnbrs[d] = pnbrs[v][d]; + } + } + return poly; +} + +// The 2D analogue, built by hand: the triangle A B C with the cycle detouring +// from B out to D (on edge BC) and straight back, A -> B -> D -> B -> C -> A. +// This is the pattern r3d leaves after clipping on a plane through B: a +// spliced edge vertex reached twice. The two spur edges B->D and D->B lift to +// mirrored triangles whose signed areas cancel, so the cycle still encloses +// the area of ABC; summing |area| per piece adds twice the spur triangle. +r3d::Polytope<2> FoldedPolygon() +{ + constexpr int nverts = 5; + constexpr double pos[nverts][2] = { + {0.0, 0.0}, {4.0, 0.0}, {2.0, 1.5}, {4.0, 0.0}, {0.0, 3.0}}; + r3d::Polytope<2> poly; + poly.nverts = nverts; + for (int v = 0; v < nverts; ++v) { + poly.verts[v].pos[0] = pos[v][0]; + poly.verts[v].pos[1] = pos[v][1]; + poly.verts[v].pnbrs[0] = (v + 1) % nverts; + poly.verts[v].pnbrs[1] = (v + nverts - 1) % nverts; + } + return poly; +} + +template +double StarDecompositionMeasure(const r3d::Polytope& poly) +{ + const double eps = PCMS_INTERSECTION_ABS_TOL + + PCMS_INTERSECTION_REL_TOL * std::fabs(r3d::measure(poly)); + double sum = 0.0; + pcms::detail::ForEachPolytopeStarSimplex( + poly, eps, + [&](const Omega_h::Few, Dim + 1>&, + Omega_h::Real measure) { sum += measure; }); + return sum; +} + +// A clean simplex star-decomposes from its first vertex into exactly one +// piece, itself: the Dim facets incident to the apex have zero measure and are +// dropped. One piece means one quadrature rule per same-mesh element, which is +// the cost floor for the RHS build. +template +void RequireCleanSimplexDecomposition(const double* coords) +{ + r3d::Few, Dim + 1> verts; + for (int v = 0; v < Dim + 1; ++v) { + for (int d = 0; d < Dim; ++d) { + verts[v][d] = coords[Dim * v + d]; + } + } + r3d::Polytope poly; + r3d::init(poly, verts); + const double measure = std::fabs(r3d::measure(poly)); + int pieces = 0; + pcms::detail::ForEachPolytopeStarSimplex( + poly, 0.0, + [&](const Omega_h::Few, Dim + 1>&, + Omega_h::Real piece) { + REQUIRE(piece == Catch::Approx(measure).epsilon(1e-12)); + ++pieces; + }); + REQUIRE(pieces == 1); +} + +} // namespace + +TEST_CASE("star decomposition of a folded r3d polytope matches its measure", + "[transfer][mesh_intersection]") +{ + SECTION("2D, clean triangle: one piece, itself") + { + constexpr double tri[6] = {0.0, 0.0, 4.0, 0.0, 0.0, 3.0}; + RequireCleanSimplexDecomposition<2>(tri); + } + SECTION("3D, clean tetrahedron: one piece, itself") + { + RequireCleanSimplexDecomposition<3>(kFoldingTetCoords.data()); + } + SECTION("2D, folded polygon") + { + const auto poly = FoldedPolygon(); + const double area = std::fabs(r3d::measure(poly)); + REQUIRE(area == Catch::Approx(6.0)); + REQUIRE(StarDecompositionMeasure<2>(poly) == + Catch::Approx(area).epsilon(1e-12)); + } + SECTION("3D, folded self-clip polytope") + { + const auto poly = FoldedSelfClipPolytope(); + const double volume = std::fabs(r3d::measure(poly)); + // Volume of the original tet (positions were rounded to 1e-6): r3d's own + // signed integration gets it right despite the fold. + REQUIRE(volume == Catch::Approx(2499.121742).epsilon(1e-6)); + REQUIRE(StarDecompositionMeasure<3>(poly) == + Catch::Approx(volume).epsilon(1e-12)); + } +} + +TEST_CASE("self-clip star decomposition sums to the element measure", + "[transfer][mesh_intersection]") +{ + Omega_h::Library lib; + + SECTION("3D, tets whose r3d self-clip is known to fold") + { + auto mesh = BuildDisjointSimplices<3>(lib, kFoldingTetCoords); + REQUIRE(mesh.nelems() == 3); + RequireSelfDecompositionExact<3>(mesh); + } + SECTION("2D, random triangles at realistic coordinate scale") + { + auto mesh = + BuildDisjointSimplices<2>(lib, RandomSimplexCoords<2>(400, 20260903u)); + RequireSelfDecompositionExact<2>(mesh); + } + SECTION("3D, random tets at realistic coordinate scale") + { + auto mesh = + BuildDisjointSimplices<3>(lib, RandomSimplexCoords<3>(400, 20260902u)); + RequireSelfDecompositionExact<3>(mesh); + } + SECTION("2D, scaled box mesh") + { + auto mesh = BuildScaledBox<2>(lib, 12); + RequireSelfDecompositionExact<2>(mesh); + } + SECTION("3D, scaled box mesh") + { + auto mesh = BuildScaledBox<3>(lib, 5); + RequireSelfDecompositionExact<3>(mesh); + } +} + +TEST_CASE("same-mesh P0->P1 lumped projection equals the element-nodal average", + "[transfer][mesh_intersection]") +{ + Omega_h::Library lib; + + SECTION("3D, tets whose r3d self-clip is known to fold") + { + auto mesh = BuildDisjointSimplices<3>(lib, kFoldingTetCoords); + RequireLumpedEqualsNodalAverage<3>(mesh); + } + SECTION("2D, scaled box mesh") + { + auto mesh = BuildScaledBox<2>(lib, 12); + RequireLumpedEqualsNodalAverage<2>(mesh); + } + SECTION("3D, scaled box mesh") + { + auto mesh = BuildScaledBox<3>(lib, 5); + RequireLumpedEqualsNodalAverage<3>(mesh); + } +} + +TEST_CASE("same-mesh P0->P1 consistent projection preserves zeroth and first " + "moments", + "[transfer][mesh_intersection]") +{ + // Galerkin orthogonality with v = 1 and each coordinate in P1: the + // consistent L2 projection matches every moment the target space represents. + Omega_h::Library lib; + + SECTION("2D, scaled box mesh") + { + auto mesh = BuildScaledBox<2>(lib, 12); + RequireConsistentPreservesMoments<2>(mesh); + } + SECTION("3D, scaled box mesh") + { + auto mesh = BuildScaledBox<3>(lib, 5); + RequireConsistentPreservesMoments<3>(mesh); + } +} diff --git a/test/test_point_evaluator.cpp b/test/test_point_evaluator.cpp index fa0fc2f57..02f2e20b3 100644 --- a/test/test_point_evaluator.cpp +++ b/test/test_point_evaluator.cpp @@ -9,14 +9,139 @@ #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/field/value_view.hpp" +#include "pcms/field/coordinate_view.hpp" #include "pcms/utility/arrays.h" #include "field_test_utils.h" +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::CoordinateSystem; using pcms::Real; +namespace +{ + +// Evaluate checks the output's shape and tag before evaluating, the same way +// for every evaluator. `field` must be scalar. +void CheckOutputValidation(const pcms::PointEvaluator& evaluator, + const pcms::Field& field, int num_points) +{ + using OutView = Kokkos::View; + REQUIRE(evaluator.NumPoints() == num_points); + REQUIRE(field.GetLayout().GetNumComponents() == 1); + auto scalar = [](OutView v) { + return pcms::ValueView( + pcms::ValueBasis{}, pcms::MakeRank2View(v)); + }; + const auto n = static_cast(num_points); + + REQUIRE_NOTHROW(evaluator.Evaluate(field, scalar(OutView("ok", n, 1)))); + REQUIRE_THROWS_AS( + evaluator.Evaluate(field, scalar(OutView("short", n - 1, 1))), + std::invalid_argument); + REQUIRE_THROWS_AS( + evaluator.Evaluate(field, scalar(OutView("long", n + 1, 1))), + std::invalid_argument); + REQUIRE_THROWS_AS(evaluator.Evaluate(field, scalar(OutView("wide", n, 2))), + std::invalid_argument); + // One component is also a valid 1D vector, so only the tag is wrong. + OutView out("tagged", n, 1); + REQUIRE_THROWS_AS( + evaluator.Evaluate(field, + pcms::ValueView( + pcms::ValueBasis{pcms::csys::Cartesian::Create(1), + pcms::ComponentScaling::Physical, + pcms::values::Vector}, + pcms::MakeRank2View(out))), + pcms::pcms_error); +} + +// Two points inside [0,1]^2 and two outside. +const std::vector kMixedPoints = {0.25, 0.25, -0.5, 0.5, + 0.7, 0.3, 1.5, 0.5}; + +std::unique_ptr> CreateFillEvaluator( + const pcms::FunctionSpace& space) +{ + auto coords = pcms::test::CreateDeviceCoordinateView( + kMixedPoints, pcms::csys::Cartesian::Deferred()); + return space.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates( + coords.coordinate_view, + pcms::OutOfBoundsPolicy{pcms::OutOfBoundsMode::FILL, -1.0})); +} + +} // namespace + +TEST_CASE("PointEvaluator::Evaluate checks the output for every evaluator") +{ + auto lib = Omega_h::Library{}; + const int n = static_cast(kMixedPoints.size()) / 2; + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {1.0, 1.0}; + grid.divisions = {10, 10}; + + SECTION("Omega_h Lagrange") + { + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 10, + 10, 0, false); + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::Backend::OmegaH); + CheckOutputValidation(*CreateFillEvaluator(*space), + space->CreateFunction(), n); + } +#ifdef PCMS_ENABLE_MESHFIELDS + SECTION("MeshFields Lagrange") + { + auto mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 10, + 10, 0, false); + auto space = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); + CheckOutputValidation(*CreateFillEvaluator(*space), + space->CreateFunction(), n); + } +#endif + SECTION("uniform grid Lagrange") + { + auto space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::csys::Cartesian::Deferred(), 1); + CheckOutputValidation(*CreateFillEvaluator(*space), + space->CreateFunction(), n); + } + SECTION("uniform grid spline") + { + auto space = pcms::SplineFunctionSpace::FromUniformGrid( + grid, pcms::csys::Cartesian::Deferred()); + CheckOutputValidation(*CreateFillEvaluator(*space), + space->CreateFunction(), n); + } + SECTION("MLS point cloud") + { + std::vector sources; + for (int j = 0; j < 5; ++j) { + for (int i = 0; i < 5; ++i) { + sources.push_back(0.25 * i); + sources.push_back(0.25 * j); + } + } + auto space = pcms::PolynomialReconstructionFunctionSpace::Create( + pcms::CoordinateView( + pcms::csys::Cartesian::Deferred(), + pcms::Rank2View(sources.data(), 25, + 2))); + const auto pts = pcms::test::StandardEvalCoords2D(); + auto coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + auto evaluator = space->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(coords.coordinate_view)); + CheckOutputValidation(*evaluator, space->CreateFunction(), + static_cast(pts.size()) / 2); + } +} + // ============================================================================ // OmegaH order-1 — basic evaluation via new API // ============================================================================ @@ -28,23 +153,23 @@ TEST_CASE("PointEvaluator: OmegaH order-1 linear evaluation") 100, 0, false); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, CoordinateSystem::Cartesian, "global", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field_data, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, 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( + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + 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; }); + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); } // ============================================================================ @@ -58,54 +183,32 @@ TEST_CASE("PointEvaluator: same evaluator reused for two FieldData objects") 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", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_a = factory.CreateField(); - auto field_b = factory.CreateField(); + auto field_a = factory->CreateFunction(); + auto field_b = factory->CreateFunction(); // 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; }); + field_a, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); pcms::test::SetField( - field_b.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real, Real) { return Real(42); }); + field_b, 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 device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); - // Create the PointEvaluator once - auto evaluator = factory.CreatePointEvaluator( + // Create the PointEvaluator once and reuse it for both fields. + 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)); - } + pcms::test::CheckEvaluation( + *evaluator, field_a, pts, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); + pcms::test::CheckEvaluation( + *evaluator, field_b, pts, OMEGA_H_LAMBDA(Real, Real) { return Real(42); }); } // ============================================================================ @@ -119,23 +222,22 @@ TEST_CASE("PointEvaluator: OmegaH order-1 out-of-bounds fill") 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", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field_data, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, 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}; + const auto outside_pts = pcms::test::StandardOutsideCoords2D(); auto device_coords = pcms::test::CreateDeviceCoordinateView( - outside_pts, CoordinateSystem::Cartesian); + outside_pts, pcms::csys::Cartesian::Deferred()); pcms::OutOfBoundsPolicy policy{pcms::OutOfBoundsMode::FILL, -999.0}; - auto evaluator = - factory.CreatePointEvaluator(pcms::EvaluationRequest::FromCoordinates( - device_coords.coordinate_view, policy)); + auto evaluator = factory->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view, + policy)); pcms::test::CheckFillMode(*evaluator, field_data, -999.0, outside_pts); } @@ -153,21 +255,22 @@ TEST_CASE("PointEvaluator: UniformGrid order-1 linear evaluation") grid.divisions = {N, N}; auto factory = pcms::LagrangeFunctionSpace::FromUniformGrid( - grid, 1, CoordinateSystem::Cartesian, 1); + grid, 1, pcms::csys::Cartesian::Deferred(), 1); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field_data, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, 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( + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + 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); + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }, + 1e-8); } TEST_CASE("PointEvaluator: SplineFunctionSpace uniform-grid evaluation") @@ -179,21 +282,67 @@ TEST_CASE("PointEvaluator: SplineFunctionSpace uniform-grid evaluation") grid.divisions = {N, N}; auto factory = pcms::SplineFunctionSpace::FromUniformGrid( - grid, CoordinateSystem::Cartesian); + grid, pcms::csys::Cartesian::Deferred()); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field_data, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, 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( + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + 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); + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }, + 1e-8); +} + +TEST_CASE("PointEvaluator: uniform grids report the points they fill") +{ + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {1.0, 1.0}; + grid.divisions = {10, 10}; + const std::vector pts = {0.25, 0.25, -0.5, 0.5, 0.7, 0.3, 1.5, 0.5}; + const std::vector is_inside = {true, false, true, false}; + auto linear = OMEGA_H_LAMBDA(Real x, Real y) + { + return pcms::test::linear_f(x, y); + }; + + SECTION("Lagrange") + { + auto factory = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::csys::Cartesian::Deferred(), 1); + auto field = factory->CreateFunction(); + pcms::test::SetField(field, linear); + pcms::test::CheckEvaluationWithFill(factory, field, pts, is_inside, linear, + -999.0, 1e-8); + } + SECTION("spline") + { + auto factory = pcms::SplineFunctionSpace::FromUniformGrid( + grid, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); + pcms::test::SetField(field, linear); + pcms::test::CheckEvaluationWithFill(factory, field, pts, is_inside, linear, + -999.0, 1e-8); + } + SECTION("no fill without the FILL policy") + { + auto factory = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, 1, pcms::csys::Cartesian::Deferred(), 1); + auto coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + auto evaluator = factory->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates( + coords.coordinate_view, + pcms::OutOfBoundsPolicy{pcms::OutOfBoundsMode::NEAREST_BOUNDARY})); + REQUIRE(evaluator->FilledPoints().extent(0) == 0); + } } // ============================================================================ @@ -207,12 +356,13 @@ TEST_CASE("FieldLayout: metadata queries") 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", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto layout = factory.GetLayout(); + auto layout = factory->GetLayout(); auto coords = layout->GetDOFHolderCoordinates(); - REQUIRE(coords.GetCoordinateSystem() == CoordinateSystem::Cartesian); + REQUIRE(pcms::SameCoordinateSystem(coords.GetCoordinateSystem(), + pcms::csys::Cartesian::Create(2))); REQUIRE(coords.GetValues().extent(0) > 0); REQUIRE(coords.GetValues().extent(1) == 2); } @@ -228,12 +378,13 @@ TEST_CASE("FieldData: layout metadata queries") 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", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); - auto coords = factory.GetLayout()->GetDOFHolderCoordinates(); - REQUIRE(coords.GetCoordinateSystem() == CoordinateSystem::Cartesian); + auto coords = factory->GetLayout()->GetDOFHolderCoordinates(); + REQUIRE(pcms::SameCoordinateSystem(coords.GetCoordinateSystem(), + pcms::csys::Cartesian::Create(2))); REQUIRE(coords.GetValues().extent(0) > 0); REQUIRE(coords.GetValues().extent(1) == 2); } @@ -249,11 +400,11 @@ TEST_CASE("SimpleFieldData: set and get DOF holder data round-trip") 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", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::OmegaH); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); - auto& layout = *factory.GetLayout(); + auto& layout = *factory->GetLayout(); int n = layout.GetNumOwnedDofHolder(); REQUIRE(n > 0); @@ -262,11 +413,13 @@ TEST_CASE("SimpleFieldData: set and get DOF holder data round-trip") for (int i = 0; i < n; ++i) data_in[i] = static_cast(i) * 0.5; - field_data.GetData().SetDOFHolderDataHost( - pcms::Rank2View(data_in.data(), n, 1)); + field_data.SetDOFHolderDataHost( + pcms::ValueView( + pcms::ValueBasis{}, pcms::Rank2View( + data_in.data(), n, 1))); auto data_out = - pcms::FlattenToRank1View(field_data.GetData().GetDOFHolderDataHost()); + pcms::FlattenToRank1View(field_data.GetDOFHolderDataHost().GetValues()); REQUIRE(static_cast(data_out.size()) == n); for (int i = 0; i < n; ++i) { REQUIRE(data_out[i] == Catch::Approx(data_in[i])); @@ -285,12 +438,13 @@ TEST_CASE("FieldLayout: MeshFields metadata queries") 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", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::MeshFields); - auto layout = factory.GetLayout(); + auto layout = factory->GetLayout(); auto coords = layout->GetDOFHolderCoordinates(); - REQUIRE(coords.GetCoordinateSystem() == CoordinateSystem::Cartesian); + REQUIRE(pcms::SameCoordinateSystem(coords.GetCoordinateSystem(), + pcms::csys::Cartesian::Create(2))); REQUIRE(coords.GetValues().extent(0) > 0); REQUIRE(coords.GetValues().extent(1) == 2); } @@ -302,23 +456,23 @@ TEST_CASE("PointEvaluator: MeshFields order-1 linear evaluation") 100, 0, false); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, CoordinateSystem::Cartesian, "global", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::MeshFields); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field_data, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, 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( + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + 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; }); + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); } TEST_CASE("PointEvaluator: MeshFields out-of-bounds fill") @@ -328,22 +482,21 @@ TEST_CASE("PointEvaluator: MeshFields out-of-bounds fill") 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", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::MeshFields); - auto field_data = factory.CreateField(); + auto field_data = factory->CreateFunction(); pcms::test::SetField( - field_data.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field_data, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); - const std::vector outside_pts = {-0.5, 0.5, 1.5, 0.5, - 0.5, -0.5, 0.5, 1.5}; + const auto outside_pts = pcms::test::StandardOutsideCoords2D(); auto device_coords = pcms::test::CreateDeviceCoordinateView( - outside_pts, CoordinateSystem::Cartesian); + outside_pts, pcms::csys::Cartesian::Deferred()); pcms::OutOfBoundsPolicy policy{pcms::OutOfBoundsMode::FILL, -999.0}; - auto evaluator = - factory.CreatePointEvaluator(pcms::EvaluationRequest::FromCoordinates( - device_coords.coordinate_view, policy)); + auto evaluator = factory->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view, + policy)); pcms::test::CheckFillMode(*evaluator, field_data, -999.0, outside_pts); } @@ -355,61 +508,141 @@ TEST_CASE( 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", + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", pcms::LagrangeFunctionSpace::Backend::MeshFields); - auto field_a = factory.CreateField(); - auto field_b = factory.CreateField(); + auto field_a = factory->CreateFunction(); + auto field_b = factory->CreateFunction(); pcms::test::SetField( - field_a.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field_a, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); pcms::test::SetField( - field_b.GetData(), *factory.GetLayout(), - OMEGA_H_LAMBDA(Real, Real) { return Real(42); }); + field_b, 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 device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); - auto evaluator = factory.CreatePointEvaluator( + // Create the PointEvaluator once and reuse it for both fields. + 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); + pcms::test::CheckEvaluation( + *evaluator, field_a, pts, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); + pcms::test::CheckEvaluation( + *evaluator, field_b, pts, OMEGA_H_LAMBDA(Real, Real) { return Real(42); }); +} + +TEST_CASE("PointEvaluator: MeshFields order-1 multi-component (2) evaluation") +{ + auto lib = Omega_h::Library{}; + auto mesh = + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 50, 50, 0, false); - evaluator->Evaluate(field_a, view_a); - evaluator->Evaluate(field_b, view_b); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 2, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); - 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); + auto field = factory->CreateFunction(); + auto layout = factory->GetLayout(); + REQUIRE(layout->GetNumComponents() == 2); + int num_dof = layout->GetNumOwnedDofHolder(); + + // Component 0: f(x,y)=x+y, Component 1: f(x,y)=2x-y + auto dof_coords_mdspan = layout->GetDOFHolderCoordinates().GetValues(); + Kokkos::View dof_dev("dof_dev", num_dof, 2); + pcms::ConvertMismatchLayoutView2D(dof_dev, dof_coords_mdspan); + auto coords_host = + Kokkos::create_mirror_view_and_copy(pcms::HostMemorySpace(), dof_dev); + + std::vector host_data(static_cast(num_dof * 2)); + for (int i = 0; i < num_dof; ++i) { + Real x = coords_host(i, 0); + Real y = coords_host(i, 1); + host_data[static_cast(i) * 2 + 0] = x + y; + host_data[static_cast(i) * 2 + 1] = 2.0 * x - y; + } + field.SetDOFHolderDataHost( + pcms::ValueView( + pcms::ValueBasis{}, pcms::Rank2View( + host_data.data(), num_dof, 2))); + + auto pts = pcms::test::StandardEvalCoords2D(); + int n = static_cast(pts.size()) / 2; + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + auto evaluator = factory->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View out("out", n, 2); + evaluator->Evaluate(field, pcms::test::TagLike(field, out)); + auto out_host = + Kokkos::create_mirror_view_and_copy(pcms::HostMemorySpace(), out); 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)); + Real x = pts[2 * static_cast(i)]; + Real y = pts[2 * static_cast(i) + 1]; + INFO("Point " << i << " (" << x << ", " << y << ")"); + REQUIRE(out_host(i, 0) == Catch::Approx(x + y).margin(1e-8)); + REQUIRE(out_host(i, 1) == Catch::Approx(2.0 * x - y).margin(1e-8)); } } -TEST_CASE("LagrangeFunctionSpace: MeshFields rejects multi-component fields") +TEST_CASE("PointEvaluator: MeshFields order-1 multi-component (3) evaluation") { auto lib = Omega_h::Library{}; auto mesh = - Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 0, false); + Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 1, 1, 0, 50, 50, 0, false); - REQUIRE_THROWS_AS(pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 2, CoordinateSystem::Cartesian, "global", - pcms::LagrangeFunctionSpace::Backend::MeshFields), - pcms::pcms_error); + auto factory = pcms::LagrangeFunctionSpace::FromMesh( + mesh, 1, 3, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::Backend::MeshFields); + + auto field = factory->CreateFunction(); + auto layout = factory->GetLayout(); + REQUIRE(layout->GetNumComponents() == 3); + int num_dof = layout->GetNumOwnedDofHolder(); + + // Component 0: x, Component 1: y, Component 2: x*y + auto dof_coords_mdspan = layout->GetDOFHolderCoordinates().GetValues(); + Kokkos::View dof_dev("dof_dev", num_dof, 2); + pcms::ConvertMismatchLayoutView2D(dof_dev, dof_coords_mdspan); + auto coords_host = + Kokkos::create_mirror_view_and_copy(pcms::HostMemorySpace(), dof_dev); + + std::vector host_data(static_cast(num_dof * 3)); + for (int i = 0; i < num_dof; ++i) { + Real x = coords_host(i, 0); + Real y = coords_host(i, 1); + host_data[static_cast(i) * 3 + 0] = x; + host_data[static_cast(i) * 3 + 1] = y; + host_data[static_cast(i) * 3 + 2] = x * y; + } + field.SetDOFHolderDataHost( + pcms::ValueView( + pcms::ValueBasis{}, pcms::Rank2View( + host_data.data(), num_dof, 3))); + + auto pts = pcms::test::StandardEvalCoords2D(); + int n = static_cast(pts.size()) / 2; + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + auto evaluator = factory->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View out("out", n, 3); + evaluator->Evaluate(field, pcms::test::TagLike(field, out)); + auto out_host = + Kokkos::create_mirror_view_and_copy(pcms::HostMemorySpace(), out); + + for (int i = 0; i < n; ++i) { + Real x = pts[2 * static_cast(i)]; + Real y = pts[2 * static_cast(i) + 1]; + INFO("Point " << i << " (" << x << ", " << y << ")"); + REQUIRE(out_host(i, 0) == Catch::Approx(x).margin(1e-8)); + REQUIRE(out_host(i, 1) == Catch::Approx(y).margin(1e-8)); + REQUIRE(out_host(i, 2) == Catch::Approx(x * y).margin(1e-8)); + } } #endif // PCMS_ENABLE_MESHFIELDS diff --git a/test/test_point_search.cpp b/test/test_point_search.cpp index 6bb9a1cc0..61e8685be 100644 --- a/test/test_point_search.cpp +++ b/test/test_point_search.cpp @@ -1,34 +1,35 @@ #include #include -#include +#include #include #include +#include using pcms::AABBox; using pcms::Uniform2DGrid; TEST_CASE("global to local") { - Omega_h::Matrix<2, 3> coords{{0.0}, {1, 0}, {0.5, 1}}; + Omega_h::Matrix<2, 3> coords{{0.0, 0.0}, {1, 0}, {0.5, 1}}; SECTION("check verts") { - for (int i = 0; i < coords.size(); ++i) { - auto xi = Omega_h::barycentric_from_global<2, 2>(coords[i], coords); - Omega_h::Vector<3> hand_xi{0, 0, 0}; - hand_xi[i] = 1; - printf("[%f,%f,%f] == [%f,%f,%f]\n", xi[0], xi[1], xi[2], hand_xi[0], - hand_xi[1], hand_xi[2]); - REQUIRE(Omega_h::are_close(xi, hand_xi)); - } + for (int i = 0; i < coords.size(); ++i) { + auto xi = Omega_h::barycentric_from_global<2, 2>(coords[i], coords); + Omega_h::Vector<3> hand_xi{0, 0, 0}; + hand_xi[i] = 1; + printf("[%f,%f,%f] == [%f,%f,%f]\n", xi[0], xi[1], xi[2], hand_xi[0], + hand_xi[1], hand_xi[2]); + REQUIRE(Omega_h::are_close(xi, hand_xi)); + } } SECTION("check point") { - Omega_h::Vector<2> point{0.5, 0.5}; - auto xi = Omega_h::barycentric_from_global<2, 2>(point, coords); - Omega_h::Vector<3> hand_xi{0.25, 0.25, 0.5}; - printf("[%f,%f,%f] == [%f,%f,%f]\n", xi[0], xi[1], xi[2], hand_xi[0], - hand_xi[1], hand_xi[2]); - REQUIRE(are_close(xi, hand_xi)); + Omega_h::Vector<2> point{0.5, 0.5}; + auto xi = Omega_h::barycentric_from_global<2, 2>(point, coords); + Omega_h::Vector<3> hand_xi{0.25, 0.25, 0.5}; + printf("[%f,%f,%f] == [%f,%f,%f]\n", xi[0], xi[1], xi[2], hand_xi[0], + hand_xi[1], hand_xi[2]); + REQUIRE(are_close(xi, hand_xi)); } } TEST_CASE("Triangle AABB intersection") @@ -37,91 +38,91 @@ TEST_CASE("Triangle AABB intersection") AABBox<2> unit_square{.center = {0, 0}, .half_width = {0.5, 0.5}}; SECTION("Triangle outside bbox") { - REQUIRE(triangle_intersects_bbox({{1, 1}, {2, 1}, {2, 2}}, unit_square) == - false); + REQUIRE(triangle_intersects_bbox({{1, 1}, {2, 1}, {2, 2}}, unit_square) == + false); } SECTION("BBOX inside Triangle") { - REQUIRE(triangle_intersects_bbox({{-10, -10}, {10, -10}, {10, 10}}, - unit_square) == true); + REQUIRE(triangle_intersects_bbox({{-10, -10}, {10, -10}, {10, 10}}, + unit_square) == true); } SECTION("All verts inside BBOX") { - REQUIRE(triangle_intersects_bbox({{-10, -10}, {10, -10}, {10, 10}}, - unit_square) == true); + REQUIRE(triangle_intersects_bbox({{-10, -10}, {10, -10}, {10, 10}}, + unit_square) == true); } SECTION("Single vert inside bbox") { - REQUIRE(triangle_intersects_bbox({{0, 0}, {1, 0}, {1, 1}}, unit_square) == - true); - REQUIRE(triangle_intersects_bbox({{1, 1}, {0, 0}, {1, 0}}, unit_square) == - true); - REQUIRE(triangle_intersects_bbox({{1, 0}, {1, 1}, {0, 0}}, unit_square) == - true); + REQUIRE(triangle_intersects_bbox({{0, 0}, {1, 0}, {1, 1}}, unit_square) == + true); + REQUIRE(triangle_intersects_bbox({{1, 1}, {0, 0}, {1, 0}}, unit_square) == + true); + REQUIRE(triangle_intersects_bbox({{1, 0}, {1, 1}, {0, 0}}, unit_square) == + true); } SECTION("edge crossed bbox") { - // Triangle 1 with all vert permutations - REQUIRE(triangle_intersects_bbox({{-0.6, 0}, {0, 0.6}, {-1, 1}}, - unit_square) == true); - REQUIRE(triangle_intersects_bbox({{-1, 1}, {-0.6, 0}, {0, 0.6}}, - unit_square) == true); - REQUIRE(triangle_intersects_bbox({{0, 0.6}, {-1, 1}, {-0.6, 0}}, - unit_square) == true); - // triangle 2 with all vert permutations - REQUIRE(triangle_intersects_bbox({{-0.6, 0}, {0.6, 0}, {0, -1}}, - unit_square) == true); - REQUIRE(triangle_intersects_bbox({{0, -1}, {-0.6, 0}, {0.6, 0}}, - unit_square) == true); - REQUIRE(triangle_intersects_bbox({{0.6, 0}, {0, -1}, {-0.6, 0}}, - unit_square) == true); + // Triangle 1 with all vert permutations + REQUIRE(triangle_intersects_bbox({{-0.6, 0}, {0, 0.6}, {-1, 1}}, + unit_square) == true); + REQUIRE(triangle_intersects_bbox({{-1, 1}, {-0.6, 0}, {0, 0.6}}, + unit_square) == true); + REQUIRE(triangle_intersects_bbox({{0, 0.6}, {-1, 1}, {-0.6, 0}}, + unit_square) == true); + // triangle 2 with all vert permutations + REQUIRE(triangle_intersects_bbox({{-0.6, 0}, {0.6, 0}, {0, -1}}, + unit_square) == true); + REQUIRE(triangle_intersects_bbox({{0, -1}, {-0.6, 0}, {0.6, 0}}, + unit_square) == true); + REQUIRE(triangle_intersects_bbox({{0.6, 0}, {0, -1}, {-0.6, 0}}, + unit_square) == true); } } TEST_CASE("Triangle BBox Intersection Regression") { using pcms::triangle_intersects_bbox; AABBox<2> bbox{.center = {0.10833333, 0.10833333}, - .half_width = {0.00833333, 0.00833333}}; + .half_width = {0.00833333, 0.00833333}}; REQUIRE(triangle_intersects_bbox({{0, 0}, {0.1, 0}, {0.1, 0.1}}, bbox) == - true); + true); REQUIRE(triangle_intersects_bbox({{0.1, 0}, {0.2, 0}, {0.2, 0.1}}, bbox) == - false); + false); REQUIRE(triangle_intersects_bbox({{0.1, 0.1}, {0, 0.1}, {0, 0}}, bbox) == - true); + true); REQUIRE(triangle_intersects_bbox({{0.1, 0.1}, {0.2, 0.1}, {0.2, 0.2}}, - bbox) == true); + bbox) == true); REQUIRE(triangle_intersects_bbox({{0, 0.1}, {0.1, 0.1}, {0.1, 0.2}}, bbox) == - true); + true); REQUIRE(triangle_intersects_bbox({{0.1, 0.2}, {0, 0.2}, {0, 0.1}}, bbox) == - false); + false); REQUIRE(triangle_intersects_bbox({{0.2, 0.2}, {0.1, 0.2}, {0.1, 0.1}}, - bbox) == true); + bbox) == true); REQUIRE(triangle_intersects_bbox({{0.2, 0.1}, {0.1, 0.1}, {0.1, 0}}, bbox) == - true); + true); } template bool num_candidates_within_range(const T& intersection_map, pcms::LO min, - pcms::LO max) + pcms::LO max) { using size_type = typename T::size_type; using MinMax = Kokkos::MinMax; using result_type = typename MinMax::value_type; result_type result; Kokkos::parallel_reduce( - intersection_map.numRows(), - KOKKOS_LAMBDA(const int i, result_type& update) { - auto num_candidates = - intersection_map.row_map(i + 1) - intersection_map.row_map(i); - if (num_candidates > update.max_val) - update.max_val = num_candidates; - if (num_candidates < update.min_val) - update.min_val = num_candidates; - }, - MinMax{result}); + intersection_map.numRows(), + KOKKOS_LAMBDA(const int i, result_type& update) { + auto num_candidates = + intersection_map.row_map(i + 1) - intersection_map.row_map(i); + if (num_candidates > update.max_val) + update.max_val = num_candidates; + if (num_candidates < update.min_val) + update.min_val = num_candidates; + }, + MinMax{result}); bool within_range = (result.min_val >= min) && (result.max_val <= max); if (!within_range) - std::cerr << result.min_val << ' ' << result.max_val << '\n'; + std::cerr << result.min_val << ' ' << result.max_val << '\n'; return within_range; } // extern Omega_h::Library omega_h_library; @@ -132,53 +133,133 @@ TEST_CASE("construct intersection map") auto lib = Omega_h::Library{}; auto world = lib.world(); auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 10, 10, 0, false); + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 10, 10, 0, false); REQUIRE(mesh.dim() == 2); SECTION("grid bbox overlap") { + Kokkos::View grid_d("uniform grid"); + auto grid_h = Kokkos::create_mirror_view(grid_d); + grid_h(0) = Uniform2DGrid{ + .edge_length{1, 1}, .bot_left = {0, 0}, .divisions = {10, 10}}; + Kokkos::deep_copy(grid_d, grid_h); + auto intersection_map = pcms::detail::construct_intersection_map_2d( + mesh, grid_d, grid_h(0).GetNumCells()); + // assert(cudaSuccess == cudaDeviceSynchronize()); + REQUIRE(intersection_map.numRows() == 100); + REQUIRE(num_candidates_within_range(intersection_map, 2, 16)); + } + SECTION("fine grid") + { + Kokkos::View grid_d("uniform grid"); + auto grid_h = Kokkos::create_mirror_view(grid_d); + grid_h(0) = Uniform2DGrid{ + .edge_length{1, 1}, .bot_left = {0, 0}, .divisions = {60, 60}}; + Kokkos::deep_copy(grid_d, grid_h); + // require number of candidates is >=1 and <=6 + auto intersection_map = pcms::detail::construct_intersection_map_2d( + mesh, grid_d, grid_h(0).GetNumCells()); + + REQUIRE(intersection_map.numRows() == 3600); + REQUIRE(num_candidates_within_range(intersection_map, 1, 6)); + } +} +namespace +{ +/// Assert two candidate maps are identical: same row offsets and the same +/// entries in the same order within every row. +template +void require_maps_identical(const T& fast, const T& reference) +{ + REQUIRE(fast.numRows() == reference.numRows()); + REQUIRE(fast.entries.size() == reference.entries.size()); + + auto fast_row_map = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, fast.row_map); + auto ref_row_map = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, reference.row_map); + auto fast_entries = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, fast.entries); + auto ref_entries = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, reference.entries); + + for (std::size_t i = 0; i < fast_row_map.extent(0); ++i) { + REQUIRE(fast_row_map(i) == ref_row_map(i)); + } + for (std::size_t i = 0; i < fast_entries.extent(0); ++i) { + REQUIRE(fast_entries(i) == ref_entries(i)); + } +} +} // namespace + +// The element-major candidate map must produce exactly what the brute-force +// cell-major build does. Entry order matters as well as membership: the search +// returns the first candidate containing the point, so ordering decides which +// element wins a shared-face tie and which is reported for an out-of-bounds +// query. +TEST_CASE("candidate map matches brute-force reference") +{ + auto lib = Omega_h::Library{}; + auto world = lib.world(); + + SECTION("2D") + { + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 0, 10, 10, 0, false); + REQUIRE(mesh.dim() == 2); + for (pcms::LO divisions : {3, 10, 37}) { Kokkos::View grid_d("uniform grid"); auto grid_h = Kokkos::create_mirror_view(grid_d); - grid_h(0) = Uniform2DGrid{ - .edge_length{1, 1}, .bot_left = {0, 0}, .divisions = {10, 10}}; + grid_h(0) = Uniform2DGrid{.edge_length{1, 1}, + .bot_left = {0, 0}, + .divisions = {divisions, divisions}}; Kokkos::deep_copy(grid_d, grid_h); - auto intersection_map = pcms::detail::construct_intersection_map_2d( - mesh, grid_d, grid_h(0).GetNumCells()); - // assert(cudaSuccess == cudaDeviceSynchronize()); - REQUIRE(intersection_map.numRows() == 100); - REQUIRE(num_candidates_within_range(intersection_map, 2, 16)); + const auto ncells = grid_h(0).GetNumCells(); + require_maps_identical( + pcms::detail::construct_intersection_map_2d(mesh, grid_d, ncells), + pcms::detail::construct_intersection_map_reference_2d(mesh, grid_d, + ncells)); } - SECTION("fine grid") + } + SECTION("3D") { - Kokkos::View grid_d("uniform grid"); + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 5, 5, 5, false); + REQUIRE(mesh.dim() == 3); + for (pcms::LO divisions : {2, 5, 11}) { + Kokkos::View grid_d("uniform grid"); auto grid_h = Kokkos::create_mirror_view(grid_d); - grid_h(0) = Uniform2DGrid{ - .edge_length{1, 1}, .bot_left = {0, 0}, .divisions = {60, 60}}; + grid_h(0) = + pcms::Uniform3DGrid{.edge_length{1, 1, 1}, + .bot_left = {0, 0, 0}, + .divisions = {divisions, divisions, divisions}}; Kokkos::deep_copy(grid_d, grid_h); - // require number of candidates is >=1 and <=6 - auto intersection_map = pcms::detail::construct_intersection_map_2d( - mesh, grid_d, grid_h(0).GetNumCells()); - - REQUIRE(intersection_map.numRows() == 3600); - REQUIRE(num_candidates_within_range(intersection_map, 1, 6)); + const auto ncells = grid_h(0).GetNumCells(); + require_maps_identical( + pcms::detail::construct_intersection_map_3d(mesh, grid_d, ncells), + pcms::detail::construct_intersection_map_reference_3d(mesh, grid_d, + ncells)); + } } } -TEST_CASE("uniform grid search") + +TEST_CASE("uniform grid search 2D") { - using pcms::GridPointSearch2D; + using pcms::PointSearch; auto lib = Omega_h::Library{}; auto world = lib.world(); auto mesh = - Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 10, 10, 0, false); + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 10, 10, 0, false); auto tolerances = - GridPointSearch2D::PointSearchTolerances{"point search 2d tolerances"}; + pcms::PointSearch::PointSearchTolerances("point search 2d tolerances", 2); auto tolerances_h = Kokkos::create_mirror_view(tolerances); tolerances_h(0) = 0.01; tolerances_h(1) = 0.01; Kokkos::deep_copy(tolerances, tolerances_h); - GridPointSearch2D search{mesh, 10, 10, tolerances}; + std::unique_ptr search = + std::make_unique(mesh, 10, 10, tolerances); - Kokkos::View points("test_points", 8); + Kokkos::View points("test_points", 8, 2); // Kokkos::View points("test_points", 1); auto points_h = Kokkos::create_mirror_view(points); points_h(0, 0) = 0; @@ -198,90 +279,589 @@ TEST_CASE("uniform grid search") points_h(7, 0) = 0.05; points_h(7, 1) = 0.02; Kokkos::deep_copy(points, points_h); - auto results = search(points); - auto results_h = Kokkos::create_mirror_view(results); - Kokkos::deep_copy(results_h, results); + auto results = search->Apply(pcms::CoordinateView( + pcms::CoordinateSystem::Cartesian, pcms::MakeConstRank2View(points))); + auto result_dims_h = Kokkos::create_mirror_view(results.dimensionalities); + Kokkos::deep_copy(result_dims_h, results.dimensionalities); + auto result_ids_h = Kokkos::create_mirror_view(results.element_ids); + Kokkos::deep_copy(result_ids_h, results.element_ids); + auto result_coords_h = Kokkos::create_mirror_view(results.parametric_coords); + Kokkos::deep_copy(result_coords_h, results.parametric_coords); SECTION("global coordinate within mesh") { { - auto [dim, idx, coords] = results_h(0); - const auto face_idx = search.GetOwningElementId(results_h(0)); + auto dim = result_dims_h(0); + auto idx = result_ids_h(0); + auto coords = Omega_h::Vector<3>{ + result_coords_h(0, 0), result_coords_h(0, 1), result_coords_h(0, 2)}; + const auto face_idx = search->GetOwningElementId(results, 0); - CAPTURE(idx); + CAPTURE(idx); - REQUIRE(dim == GridPointSearch2D::Result::Dimensionality::VERTEX); + REQUIRE(dim == PointSearch::Results::Dimensionality::VERTEX); REQUIRE(idx == 0); REQUIRE(face_idx >= 0); - REQUIRE(coords[0] == Catch::Approx(1)); - REQUIRE(coords[1] == Catch::Approx(0)); - REQUIRE(coords[2] == Catch::Approx(0)); + REQUIRE(coords[0] == Catch::Approx(1).margin(0.00001)); + REQUIRE(coords[1] == Catch::Approx(0).margin(0.00001)); + REQUIRE(coords[2] == Catch::Approx(0).margin(0.00001)); } { - auto [dim, idx, coords] = results_h(1); - const auto face_idx = search.GetOwningElementId(results_h(1)); - REQUIRE(dim == GridPointSearch2D::Result::Dimensionality::EDGE); + auto dim = result_dims_h(1); + auto idx = result_ids_h(1); + auto coords = Omega_h::Vector<3>{ + result_coords_h(1, 0), result_coords_h(1, 1), result_coords_h(1, 2)}; + const auto face_idx = search->GetOwningElementId(results, 1); + REQUIRE(dim == PointSearch::Results::Dimensionality::EDGE); REQUIRE(idx == 156); REQUIRE(face_idx >= 0); - REQUIRE(coords[0] == Catch::Approx(0.5)); - REQUIRE(coords[1] == Catch::Approx(0.1)); - REQUIRE(coords[2] == Catch::Approx(0.4)); + REQUIRE(coords[0] == Catch::Approx(0.5).margin(0.00001)); + REQUIRE(coords[1] == Catch::Approx(0.1).margin(0.00001)); + REQUIRE(coords[2] == Catch::Approx(0.4).margin(0.00001)); } { - auto [dim, idx, coords] = results_h(7); - const auto face_idx = search.GetOwningElementId(results_h(7)); - REQUIRE(dim == GridPointSearch2D::Result::Dimensionality::FACE); + auto dim = result_dims_h(7); + auto idx = result_ids_h(7); + const auto face_idx = search->GetOwningElementId(results, 7); + REQUIRE(dim == PointSearch::Results::Dimensionality::FACE); REQUIRE(idx == 0); REQUIRE(face_idx >= 0); } } // feature needs to be added - SECTION("Global coordinate outside mesh", "[!mayfail]") + SECTION("Global coordinate outside mesh") { - auto out_of_bounds = results_h(2); - auto top_right = results_h(3); - REQUIRE(out_of_bounds.dimensionality == - GridPointSearch2D::Result::Dimensionality::VERTEX); - REQUIRE(-1 * out_of_bounds.element_id == top_right.element_id); - REQUIRE(search.GetOwningElementId(out_of_bounds) >= 0); - - out_of_bounds = results_h(4); - auto bot_left = results_h(0); - REQUIRE(out_of_bounds.dimensionality == - GridPointSearch2D::Result::Dimensionality::VERTEX); - REQUIRE(-1 * out_of_bounds.element_id == bot_left.element_id); - REQUIRE(search.GetOwningElementId(out_of_bounds) >= 0); - - out_of_bounds = results_h(5); - REQUIRE(out_of_bounds.dimensionality == - GridPointSearch2D::Result::Dimensionality::EDGE); - REQUIRE(out_of_bounds.element_id == -219); - REQUIRE(search.GetOwningElementId(out_of_bounds) >= 0); - - out_of_bounds = results_h(6); - REQUIRE(out_of_bounds.dimensionality == - GridPointSearch2D::Result::Dimensionality::EDGE); - REQUIRE(-1 * out_of_bounds.element_id == bot_left.element_id); - REQUIRE(search.GetOwningElementId(out_of_bounds) >= 0); + auto out_of_bounds_dim = result_dims_h(2); + auto out_of_bounds_id = result_ids_h(2); + auto top_right_id = result_ids_h(3); + REQUIRE(out_of_bounds_dim == + PointSearch::Results::Dimensionality::VERTEX); + REQUIRE(pcms::PointSearch::Results::EntityId(out_of_bounds_id, out_of_bounds_dim) + == top_right_id); + REQUIRE(pcms::GetOwningElementId(mesh, mesh.dim(), static_cast(out_of_bounds_dim), -out_of_bounds_id) >= 0); + + + out_of_bounds_dim = result_dims_h(4); + out_of_bounds_id = result_ids_h(4); + auto bot_left_id = result_ids_h(0); + REQUIRE(out_of_bounds_dim == + PointSearch::Results::Dimensionality::VERTEX); + REQUIRE(PointSearch::Results::EntityId(out_of_bounds_id, out_of_bounds_dim) + == bot_left_id); + REQUIRE(pcms::GetOwningElementId(mesh, mesh.dim(), static_cast(out_of_bounds_dim), -out_of_bounds_id) >= 0); + + out_of_bounds_dim = result_dims_h(5); + out_of_bounds_id = result_ids_h(5); + REQUIRE(out_of_bounds_dim == + PointSearch::Results::Dimensionality::EDGE); + REQUIRE(-PointSearch::Results::EntityId(out_of_bounds_id, out_of_bounds_dim) + == -219); + REQUIRE(pcms::GetOwningElementId(mesh, mesh.dim(), static_cast(out_of_bounds_dim), -out_of_bounds_id) >= 0); + + // (0.05, -0.01) is within the edge tolerance of the bottom edge, so it is + // located on that edge rather than reported outside. + out_of_bounds_dim = result_dims_h(6); + out_of_bounds_id = result_ids_h(6); + REQUIRE(out_of_bounds_dim == + PointSearch::Results::Dimensionality::EDGE); + REQUIRE(PointSearch::Results::EntityId(out_of_bounds_id, out_of_bounds_dim) == bot_left_id); + REQUIRE(pcms::GetOwningElementId(mesh, mesh.dim(), static_cast(out_of_bounds_dim), -out_of_bounds_id) >= 0); } SECTION("point on extension of an edge") { - Kokkos::View ext_points("ext_test_points", 1); - auto ext_h = Kokkos::create_mirror_view(ext_points); - ext_h(0, 0) = 1.5; - ext_h(0, 1) = 0.0; - Kokkos::deep_copy(ext_points, ext_h); - auto ext_results = search(ext_points); - auto ext_results_h = Kokkos::create_mirror_view(ext_results); - Kokkos::deep_copy(ext_results_h, ext_results); - - auto res = ext_results_h(0); - // Must be out-of-bounds (negative element id) - REQUIRE(res.element_id < 0); - // Dimensionality must be VERTEX — the nearest entity is the - // rightmost bottom vertex (1.0, 0). - REQUIRE(res.dimensionality == - GridPointSearch2D::Result::Dimensionality::VERTEX); - // Owning element should still resolve to a valid face - REQUIRE(search.GetOwningElementId(res) >= 0); + Kokkos::View ext_points("ext_test_points", 1, 2); + auto ext_h = Kokkos::create_mirror_view(ext_points); + ext_h(0, 0) = 1.5; + ext_h(0, 1) = 0.0; + Kokkos::deep_copy(ext_points, ext_h); + auto ext_results = search->Apply(pcms::CoordinateView( + pcms::CoordinateSystem::Cartesian, pcms::MakeConstRank2View(ext_points))); + + auto result_dims_h = Kokkos::create_mirror_view(ext_results.dimensionalities); + Kokkos::deep_copy(result_dims_h, ext_results.dimensionalities); + auto result_ids_h = Kokkos::create_mirror_view(ext_results.element_ids); + Kokkos::deep_copy(result_ids_h, ext_results.element_ids); + // Must be out-of-bounds (negative element id) + REQUIRE(result_ids_h(0) < 0); + // Dimensionality must be VERTEX — the nearest entity is the + // rightmost bottom vertex (1.0, 0). + REQUIRE(result_dims_h(0) == + PointSearch::Results::Dimensionality::VERTEX); + // Owning element should still resolve to a valid face + REQUIRE(pcms::GetOwningElementId(mesh, mesh.dim(), 0, + pcms::PointSearch::Results::EntityId(result_ids_h(0), result_dims_h(0))) >= 0); + } +} + +TEST_CASE("point search locates points on shared faces", + "[point_search][3d][regression]") +{ + using pcms::GridPointSearch3D; + auto lib = Omega_h::Library{}; + auto world = lib.world(); + + const int source_divisions = 10; + const int query_divisions = 5; + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, source_divisions, + source_divisions, source_divisions, false); + auto query_mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, query_divisions, + query_divisions, query_divisions, false); + + // Same grid resolution the intersection search uses for this mesh. + const auto n = pcms::DivisionsPerAxisForMesh<3>(mesh.nelems()); + GridPointSearch3D search{mesh, n, n, n}; + + const auto centroids_h = + Omega_h::HostRead(pcms::get_entity_centroids(query_mesh, 3)); + const auto npoints = query_mesh.nelems(); + + Kokkos::View points("query points", npoints, 3); + auto points_h = Kokkos::create_mirror_view(points); + for (Omega_h::LO i = 0; i < npoints; ++i) { + for (int d = 0; d < 3; ++d) { + points_h(i, d) = centroids_h[i * 3 + d]; + } + } + Kokkos::deep_copy(points, points_h); + + pcms::CoordinateView> + points_cv( + pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(points)); + auto results = search.Apply(points_cv); + auto result_ids_h = Kokkos::create_mirror_view(results.element_ids); + Kokkos::deep_copy(result_ids_h, results.element_ids); + + std::vector not_located; + for (Omega_h::LO i = 0; i < npoints; ++i) { + if (result_ids_h(i) < 0) { + not_located.push_back(i); + } + } + CAPTURE(not_located.size(), npoints); + REQUIRE(not_located.empty()); +} + +TEST_CASE("point search encodes outside points unambiguously", + "[point_search][regression]") +{ + auto lib = Omega_h::Library{}; + + SECTION("3D: nearest tetrahedron 0") + { + using pcms::GridPointSearch3D; + using Results = GridPointSearch3D::Results; + using Dimensionality = Results::Dimensionality; + auto mesh = pcms::test::BuildReferenceTet(lib); + const auto n = pcms::DivisionsPerAxisForMesh<3>(mesh.nelems()); + GridPointSearch3D search{mesh, n, n, n}; + + Kokkos::View points("points", 2, 3); + auto points_h = Kokkos::create_mirror_view(points); + // inside the tet + points_h(0, 0) = 0.1; + points_h(0, 1) = 0.1; + points_h(0, 2) = 0.1; + // in the tet's bounding box but outside it: the nearest candidate is tet 0 + points_h(1, 0) = 0.5; + points_h(1, 1) = 0.5; + points_h(1, 2) = 0.5; + Kokkos::deep_copy(points, points_h); + + pcms::CoordinateView + points_cv( + pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(points)); + auto results = search.Apply(points_cv); + auto result_dims_h = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, results.dimensionalities); + auto result_ids_h = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, results.element_ids); + auto owners_h = Kokkos::create_mirror_view_and_copy( + Kokkos::HostSpace{}, search.GetOwningElementIds(results)); + + REQUIRE(result_dims_h(0) == Dimensionality::REGION); + REQUIRE_FALSE(results.IsOutside(result_ids_h(0))); + REQUIRE(result_ids_h(0) == 0); + REQUIRE(Results::EntityId(result_ids_h(0), result_dims_h(0)) == 0); + REQUIRE(owners_h(0) == 0); + + REQUIRE(result_dims_h(1) == Dimensionality::REGION); + REQUIRE(results.IsOutside(result_ids_h(1))); + REQUIRE(Results::EntityId(result_ids_h(1), result_dims_h(1)) == 0); + REQUIRE(search.GetOwningElementId(results, 1) == 0); + REQUIRE(owners_h(1) == 0); + + // The grid over a single tet has no empty cell, so decode the + // no-candidate result directly. + Kokkos::View dims("dims", 1); + Kokkos::View ids("ids", 1); + Kokkos::View coords("coords", 1, 3); + + Kokkos::deep_copy(dims, Dimensionality::NO_INTERSECT); + Kokkos::deep_copy(ids, -1); + Kokkos::deep_copy(coords, 0); + + Results none{dims, ids, coords}; + + auto none_owners_h = Kokkos::create_mirror_view_and_copy( + Kokkos::HostSpace{}, search.GetOwningElementIds(none)); + auto dims_h = Kokkos::create_mirror_view_and_copy(dims); + REQUIRE(none.IsOutside(none_owners_h(0))); + REQUIRE(Results::EntityId(none_owners_h(0), dims_h(0)) == -1); + REQUIRE(search.GetOwningElementId(none, 0) == -1); + REQUIRE(none_owners_h(0) == -1); + } + + SECTION("2D: nearest vertex 0 and an empty query cell") + { + using pcms::GridPointSearch2D; + using Results = GridPointSearch2D::Results; + using Dimensionality = Results::Dimensionality; + // Only the triangle below the diagonal, so the cells above it hold no + // candidates. + auto mesh = pcms::test::BuildUnitSquare(lib, Omega_h::LOs({0, 1, 3})); + const auto n = pcms::DivisionsPerAxisForMesh<2>(mesh.nelems()); + GridPointSearch2D search{mesh, n, n}; + + Kokkos::View points("points", 3, 2); + auto points_h = Kokkos::create_mirror_view(points); + // beyond vertex 0: the nearest entity is vertex 0 + points_h(0, 0) = -0.05; + points_h(0, 1) = -0.05; + // in a grid cell the triangle does not reach + points_h(1, 0) = 0.6; + points_h(1, 1) = 0.6; + // inside the triangle + points_h(2, 0) = 0.2; + points_h(2, 1) = 0.2; + Kokkos::deep_copy(points, points_h); + + pcms::CoordinateView + points_cv( + pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(points)); + + auto results = search.Apply(points_cv); + auto result_dims_h = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, results.dimensionalities); + auto result_ids_h = + Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, results.element_ids); + auto owners_h = Kokkos::create_mirror_view_and_copy( + Kokkos::HostSpace{}, search.GetOwningElementIds(results)); + + REQUIRE(result_dims_h(0) == Dimensionality::VERTEX); + REQUIRE(results.IsOutside(result_ids_h(0))); + REQUIRE(Results::EntityId(result_ids_h(0), result_dims_h(0)) == 0); + REQUIRE(search.GetOwningElementId(results, 0) == 0); + REQUIRE(owners_h(0) == 0); + + REQUIRE(result_dims_h(1) == Dimensionality::NO_INTERSECT); + REQUIRE(results.IsOutside(result_ids_h(1))); + REQUIRE(Results::EntityId(result_ids_h(1), result_dims_h(1)) == -1); + REQUIRE(search.GetOwningElementId(results, 1) == -1); + REQUIRE(owners_h(1) == -1); + + REQUIRE(result_dims_h(2) == Dimensionality::FACE); + REQUIRE_FALSE(results.IsOutside(result_ids_h(2))); + REQUIRE(result_ids_h(2) == 0); + REQUIRE(owners_h(2) == 0); + } +} +TEST_CASE("uniform grid search 3D") +{ + using pcms::GridPointSearch3D; + auto lib = Omega_h::Library{}; + auto world = lib.world(); + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 1, 1, 1, false); + auto tolerances = + GridPointSearch3D::PointSearchTolerances("point search 3d tolerances", 3); + auto tolerances_h = Kokkos::create_mirror_view(tolerances); + tolerances_h(0) = 0.01; + tolerances_h(1) = 0.01; + tolerances_h(2) = 0.01; + Kokkos::deep_copy(tolerances, tolerances_h); + + GridPointSearch3D search{mesh, 5, 5, 5, tolerances}; + + auto check_res = [](auto const& results, int dim) { + auto dims = Kokkos::create_mirror_view(results.dimensionalities); + auto ids = Kokkos::create_mirror_view(results.element_ids); + Kokkos::deep_copy(dims, results.dimensionalities); + Kokkos::deep_copy(ids, results.element_ids); + for (int i = 0; i < dims.size(); i++) { + CHECK(dims(i) == (pcms::PointSearch::Results::Dimensionality)dim); + CHECK(ids(i) == i); + } + }; + SECTION("Vertex intersection") + { + Kokkos::View coords("vertex coordinates", mesh.nverts(), 3); + auto coords_h = Kokkos::create_mirror_view(coords); + auto meshcoords = Omega_h::HostRead(mesh.coords()); + for (int i = 0; i < mesh.nverts(); i++) { + coords_h(i, 0) = meshcoords[i * 3]; + coords_h(i, 1) = meshcoords[i * 3 + 1]; + coords_h(i, 2) = meshcoords[i * 3 + 2]; + } + Kokkos::deep_copy(coords, coords_h); + pcms::CoordinateView> + vert_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::PointSearch::Results results = search.Apply(vert_cv); + + check_res(results, 0); + } + SECTION("Edge intersection") + { + Kokkos::View coords("intersection coords", mesh.nedges(), 3); + auto edge2vert = + Omega_h::HostRead(mesh.get_adj(Omega_h::EDGE, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + + for (int i = 0; i < mesh.nedges(); i++) { + Omega_h::Vector<3> v0{meshcoords[edge2vert[2 * i] * 3], + meshcoords[edge2vert[2 * i] * 3 + 1], + meshcoords[edge2vert[2 * i] * 3 + 2]}; + Omega_h::Vector<3> v1{meshcoords[edge2vert[2 * i + 1] * 3], + meshcoords[edge2vert[2 * i + 1] * 3 + 1], + meshcoords[edge2vert[2 * i + 1] * 3 + 2]}; + Omega_h::Vector<3> midpoint = (v0 + v1) / 2.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + coords_h(i, 2) = midpoint[2]; + } + + Kokkos::deep_copy(coords, coords_h); + + pcms::CoordinateView> + edge_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::PointSearch::Results results = search.Apply(edge_cv); + check_res(results, 1); + } + SECTION("Face intersection") + { + Kokkos::View coords("intersection coords", mesh.nfaces(), 3); + auto face2vert = + Omega_h::HostRead(mesh.get_adj(Omega_h::FACE, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + + for (int i = 0; i < mesh.nfaces(); i++) { + Omega_h::Vector<3> v0{meshcoords[face2vert[3 * i] * 3], + meshcoords[face2vert[3 * i] * 3 + 1], + meshcoords[face2vert[3 * i] * 3 + 2]}; + Omega_h::Vector<3> v1{meshcoords[face2vert[3 * i + 1] * 3], + meshcoords[face2vert[3 * i + 1] * 3 + 1], + meshcoords[face2vert[3 * i + 1] * 3 + 2]}; + Omega_h::Vector<3> v2{meshcoords[face2vert[3 * i + 2] * 3], + meshcoords[face2vert[3 * i + 2] * 3 + 1], + meshcoords[face2vert[3 * i + 2] * 3 + 2]}; + Omega_h::Vector<3> midpoint = (v0 + v1 + v2) / 3.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + coords_h(i, 2) = midpoint[2]; + } + Kokkos::deep_copy(coords, coords_h); + + pcms::CoordinateView> + face_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::GridPointSearch3D::Results results = search.Apply(face_cv); + check_res(results, 2); + } + SECTION("Region intersection") + { + Kokkos::View coords("intersection coords", mesh.nelems(), 3); + auto region2vert = + Omega_h::HostRead(mesh.get_adj(Omega_h::REGION, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + + for (unsigned i = 0; i < mesh.nelems(); i++) { + Omega_h::Vector<3> v0{meshcoords[region2vert[4 * i] * 3], + meshcoords[region2vert[4 * i] * 3 + 1], + meshcoords[region2vert[4 * i] * 3 + 2]}; + Omega_h::Vector<3> v1{meshcoords[region2vert[4 * i + 1] * 3], + meshcoords[region2vert[4 * i + 1] * 3 + 1], + meshcoords[region2vert[4 * i + 1] * 3 + 2]}; + Omega_h::Vector<3> v2{meshcoords[region2vert[4 * i + 2] * 3], + meshcoords[region2vert[4 * i + 2] * 3 + 1], + meshcoords[region2vert[4 * i + 2] * 3 + 2]}; + Omega_h::Vector<3> v3{meshcoords[region2vert[4 * i + 3] * 3], + meshcoords[region2vert[4 * i + 3] * 3 + 1], + meshcoords[region2vert[4 * i + 3] * 3 + 2]}; + Omega_h::Vector<3> midpoint = (v0 + v1 + v2 + v3) / 4.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + coords_h(i, 2) = midpoint[2]; + } + Kokkos::deep_copy(coords, coords_h); + + pcms::CoordinateView> + region_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::PointSearch::Results results = search.Apply(region_cv); + check_res(results, 3); + } +} +TEST_CASE("uniform grid search 3D") +{ + using pcms::GridPointSearch3D; + auto lib = Omega_h::Library{}; + auto world = lib.world(); + auto mesh = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 1, 1, 1, false); + auto tolerances = + GridPointSearch3D::PointSearchTolerances("point search 3d tolerances", 3); + auto tolerances_h = Kokkos::create_mirror_view(tolerances); + tolerances_h(0) = 0.01; + tolerances_h(1) = 0.01; + tolerances_h(2) = 0.01; + Kokkos::deep_copy(tolerances, tolerances_h); + + GridPointSearch3D search{mesh, 5, 5, 5, tolerances}; + + auto check_res = [](auto const& results, int dim) { + auto dims = Kokkos::create_mirror_view(results.dimensionalities); + auto ids = Kokkos::create_mirror_view(results.element_ids); + Kokkos::deep_copy(dims, results.dimensionalities); + Kokkos::deep_copy(ids, results.element_ids); + for (int i = 0; i < dims.size(); i++) { + CHECK(dims(i) == (pcms::PointSearch::Results::Dimensionality)dim); + CHECK(ids(i) == i); + } + }; + SECTION("Vertex intersection") + { + Kokkos::View coords("vertex coordinates", mesh.nverts(), 3); + auto coords_h = Kokkos::create_mirror_view(coords); + auto meshcoords = Omega_h::HostRead(mesh.coords()); + for (int i = 0; i < mesh.nverts(); i++) { + coords_h(i, 0) = meshcoords[i * 3]; + coords_h(i, 1) = meshcoords[i * 3 + 1]; + coords_h(i, 2) = meshcoords[i * 3 + 2]; + } + Kokkos::deep_copy(coords, coords_h); + pcms::CoordinateView> + vert_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::PointSearch::Results results = search.Apply(vert_cv); + + check_res(results, 0); + } + SECTION("Edge intersection") + { + Kokkos::View coords("intersection coords", mesh.nedges(), 3); + auto edge2vert = + Omega_h::HostRead(mesh.get_adj(Omega_h::EDGE, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + + for (int i = 0; i < mesh.nedges(); i++) { + Omega_h::Vector<3> v0{meshcoords[edge2vert[2 * i] * 3], + meshcoords[edge2vert[2 * i] * 3 + 1], + meshcoords[edge2vert[2 * i] * 3 + 2]}; + Omega_h::Vector<3> v1{meshcoords[edge2vert[2 * i + 1] * 3], + meshcoords[edge2vert[2 * i + 1] * 3 + 1], + meshcoords[edge2vert[2 * i + 1] * 3 + 2]}; + Omega_h::Vector<3> midpoint = (v0 + v1) / 2.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + coords_h(i, 2) = midpoint[2]; + } + + Kokkos::deep_copy(coords, coords_h); + + pcms::CoordinateView> + edge_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::PointSearch::Results results = search.Apply(edge_cv); + check_res(results, 1); + } + SECTION("Face intersection") + { + Kokkos::View coords("intersection coords", mesh.nfaces(), 3); + auto face2vert = + Omega_h::HostRead(mesh.get_adj(Omega_h::FACE, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + + for (int i = 0; i < mesh.nfaces(); i++) { + Omega_h::Vector<3> v0{meshcoords[face2vert[3 * i] * 3], + meshcoords[face2vert[3 * i] * 3 + 1], + meshcoords[face2vert[3 * i] * 3 + 2]}; + Omega_h::Vector<3> v1{meshcoords[face2vert[3 * i + 1] * 3], + meshcoords[face2vert[3 * i + 1] * 3 + 1], + meshcoords[face2vert[3 * i + 1] * 3 + 2]}; + Omega_h::Vector<3> v2{meshcoords[face2vert[3 * i + 2] * 3], + meshcoords[face2vert[3 * i + 2] * 3 + 1], + meshcoords[face2vert[3 * i + 2] * 3 + 2]}; + Omega_h::Vector<3> midpoint = (v0 + v1 + v2) / 3.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + coords_h(i, 2) = midpoint[2]; + } + Kokkos::deep_copy(coords, coords_h); + + pcms::CoordinateView> + face_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::GridPointSearch3D::Results results = search.Apply(face_cv); + check_res(results, 2); + } + SECTION("Region intersection") + { + Kokkos::View coords("intersection coords", mesh.nelems(), 3); + auto region2vert = + Omega_h::HostRead(mesh.get_adj(Omega_h::REGION, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + + for (unsigned i = 0; i < mesh.nelems(); i++) { + Omega_h::Vector<3> v0{meshcoords[region2vert[4 * i] * 3], + meshcoords[region2vert[4 * i] * 3 + 1], + meshcoords[region2vert[4 * i] * 3 + 2]}; + Omega_h::Vector<3> v1{meshcoords[region2vert[4 * i + 1] * 3], + meshcoords[region2vert[4 * i + 1] * 3 + 1], + meshcoords[region2vert[4 * i + 1] * 3 + 2]}; + Omega_h::Vector<3> v2{meshcoords[region2vert[4 * i + 2] * 3], + meshcoords[region2vert[4 * i + 2] * 3 + 1], + meshcoords[region2vert[4 * i + 2] * 3 + 2]}; + Omega_h::Vector<3> v3{meshcoords[region2vert[4 * i + 3] * 3], + meshcoords[region2vert[4 * i + 3] * 3 + 1], + meshcoords[region2vert[4 * i + 3] * 3 + 2]}; + Omega_h::Vector<3> midpoint = (v0 + v1 + v2 + v3) / 4.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + coords_h(i, 2) = midpoint[2]; + } + Kokkos::deep_copy(coords, coords_h); + + pcms::CoordinateView> + region_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::PointSearch::Results results = search.Apply(region_cv); + check_res(results, 3); } } diff --git a/test/test_polynomial_reconstruction_function_space.cpp b/test/test_polynomial_reconstruction_function_space.cpp index 1f3b675ae..c62865292 100644 --- a/test/test_polynomial_reconstruction_function_space.cpp +++ b/test/test_polynomial_reconstruction_function_space.cpp @@ -2,7 +2,7 @@ #include #include "pcms/field/function_space/polynomial_reconstruction.hpp" -#include "pcms/field/field_metadata.h" +#include "pcms/field/value_view.hpp" #include "pcms/discretization/discretization.h" #include "pcms/field/function_space/lagrange.h" #include "pcms/field/layout/point_cloud.h" @@ -11,6 +11,7 @@ #include #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::CoordinateSystem; using pcms::HostMemorySpace; @@ -36,8 +37,9 @@ TEST_CASE( Rank2View coords_view(coords.data(), 4, 2); auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( - coords_view, CoordinateSystem::Cartesian); - auto layout = factory.GetLayout(); + pcms::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view)); + auto layout = factory->GetLayout(); REQUIRE(layout->GetNumComponents() == 1); REQUIRE(layout->GetNumOwnedDofHolder() == 4); @@ -45,7 +47,7 @@ TEST_CASE( REQUIRE_FALSE(layout->IsDistributed()); auto dof_coords = layout->GetDOFHolderCoordinates().GetValues(); - auto dof_coords_host = pcms::test::CopyCoordinatesToHost(dof_coords, 4, 2); + auto dof_coords_host = pcms::test::CopyCoordinatesToHost(dof_coords); REQUIRE(static_cast(dof_coords_host.extent(0)) == 4); REQUIRE(static_cast(dof_coords_host.extent(1)) == 2); @@ -63,9 +65,10 @@ TEST_CASE("PolynomialReconstructionFunctionSpace fields share layout") 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{}); + pcms::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view)); + auto source = factory->CreateFunction(); + auto target = factory->CreateFunction(); REQUIRE(&source.GetLayout() == &target.GetLayout()); } @@ -77,39 +80,24 @@ TEST_CASE("PolynomialReconstructionFunctionSpace point-cloud field set/get DOF " Rank2View coords_view(coords.data(), 4, 2); auto field = pcms::PolynomialReconstructionFunctionSpace::Create( - coords_view, CoordinateSystem::Cartesian) - .CreateField(pcms::FieldMetadata{}); + pcms::CoordinateView( + pcms::csys::Cartesian::Deferred(), coords_view)) + ->CreateFunction(); std::vector data{1.0, 2.0, 3.0, 4.0}; Rank2View data_view( data.data(), static_cast(data.size()), 1); - field.GetData().SetDOFHolderDataHost(data_view); + field.SetDOFHolderDataHost( + pcms::ValueView(pcms::ValueBasis{}, + data_view)); - auto got = pcms::FlattenToRank1View(field.GetData().GetDOFHolderDataHost()); + auto got = pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); 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}; - Rank2View data_view( - data.data(), static_cast(data.size()), 1); - field.GetData().SetDOFHolderDataHost(data_view); - - pcms::test::CheckSerializeDeserialize(*factory.GetLayout(), field.GetData()); -} - TEST_CASE("PolynomialReconstructionFunctionSpace field keeps layout alive " "after temporary factory destruction") { @@ -118,8 +106,9 @@ TEST_CASE("PolynomialReconstructionFunctionSpace field keeps layout alive " auto field = [&]() { auto factory = pcms::PolynomialReconstructionFunctionSpace::Create( - coords_view, CoordinateSystem::Cartesian); - return factory.CreateField(pcms::FieldMetadata{}); + pcms::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view)); + return factory->CreateFunction(); }(); auto point_cloud_layout = @@ -130,9 +119,9 @@ TEST_CASE("PolynomialReconstructionFunctionSpace field keeps layout alive " std::vector data{9.0, 10.0, 11.0, 12.0}; Rank2View data_view( data.data(), static_cast(data.size()), 1); - field.SetDOFHolderDataHost(data_view); + field.SetDOFHolderDataUncheckedHost(data_view); - auto got = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto got = pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(got[0] == Catch::Approx(9.0)); REQUIRE(got[3] == Catch::Approx(12.0)); } @@ -144,19 +133,19 @@ TEST_CASE("Different layouts on the same mesh report SameEntities") 2, 0, false); auto nodal = pcms::PolynomialReconstructionFunctionSpace::FromMesh( - mesh, pcms::Face, CoordinateSystem::Cartesian); + mesh, pcms::Face, pcms::csys::Cartesian::Deferred()); auto lagrange = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, CoordinateSystem::Cartesian); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); - auto nodal_disc = nodal.GetLayout()->GetDiscretization(); - auto lagrange_disc = lagrange.GetLayout()->GetDiscretization(); + 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->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()); @@ -171,12 +160,12 @@ TEST_CASE("Layouts on different meshes do not report SameEntities") 3, 3, 0, false); auto nodal_a = pcms::PolynomialReconstructionFunctionSpace::FromMesh( - mesh_a, pcms::Vertex, CoordinateSystem::Cartesian); + mesh_a, pcms::Vertex, pcms::csys::Cartesian::Deferred()); auto nodal_b = pcms::PolynomialReconstructionFunctionSpace::FromMesh( - mesh_b, pcms::Vertex, CoordinateSystem::Cartesian); + mesh_b, pcms::Vertex, pcms::csys::Cartesian::Deferred()); - auto disc_a = nodal_a.GetLayout()->GetDiscretization(); - auto disc_b = nodal_b.GetLayout()->GetDiscretization(); + auto disc_a = nodal_a->GetLayout()->GetDiscretization(); + auto disc_b = nodal_b->GetLayout()->GetDiscretization(); REQUIRE_FALSE(disc_a->SameEntities(*disc_b)); } @@ -189,15 +178,16 @@ TEST_CASE( 2, 0, false); auto lagrange = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, CoordinateSystem::Cartesian); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); auto coords = MakeCoords2D(); Rank2View coords_view(coords.data(), 4, 2); auto standalone = pcms::PolynomialReconstructionFunctionSpace::Create( - coords_view, CoordinateSystem::Cartesian); + pcms::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view)); - auto mesh_disc = lagrange.GetLayout()->GetDiscretization(); - auto point_cloud_disc = standalone.GetLayout()->GetDiscretization(); + 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 index 8b979e68d..9f8784d66 100644 --- a/test/test_polynomial_reconstruction_mls_evaluation.cpp +++ b/test/test_polynomial_reconstruction_mls_evaluation.cpp @@ -3,8 +3,8 @@ #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/value_view.hpp" +#include "pcms/field/coordinate_view.hpp" #include "pcms/field/out_of_bounds_policy.h" #include "pcms/utility/arrays.h" #include "pcms/utility/memory_spaces.h" @@ -14,6 +14,8 @@ #include #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/field/coordinate_systems/cylindrical.hpp" using pcms::CoordinateSystem; using pcms::CoordinateView; @@ -77,12 +79,6 @@ pcms::MLSOptions DefaultTestOptions3D() 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) { @@ -118,14 +114,16 @@ void CheckPolynomialReproduction(unsigned degree, 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::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view), + SweepTestOptions(degree, basis)); + auto field = fs->CreateFunction(); + pcms::test::SetField(field, func); + + auto pts = pcms::test::StandardEvalCoords2D(); + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + auto evaluator = fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); pcms::test::CheckEvaluation(*evaluator, field, pts, func, abs_tol); } @@ -149,7 +147,9 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: reproduces " 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); + 1, basis, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }, + 5e-3); CheckPolynomialReproduction( 2, basis, OMEGA_H_LAMBDA(Real x, Real y) { return x * x + x * y + 2.0 * y * y; }, @@ -169,49 +169,32 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: same PointEvaluator " 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::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view), + DefaultTestOptions()); + auto field_a = fs->CreateFunction(); + auto field_b = fs->CreateFunction(); pcms::test::SetField( - field_a.GetData(), *fs.GetLayout(), - OMEGA_H_LAMBDA(Real x, Real y) { return x + 2.0 * y; }); + field_a, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }); const Real cval = 7.0; - pcms::test::SetField( - field_b.GetData(), *fs.GetLayout(), - OMEGA_H_LAMBDA(Real, Real) { return cval; }); + pcms::test::SetField(field_b, 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( + auto pts = pcms::test::StandardEvalCoords2D(); + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + // Create the PointEvaluator once and reuse it for both fields. + 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)); - } + pcms::test::CheckEvaluation( + *evaluator, field_a, pts, + OMEGA_H_LAMBDA(Real x, Real y) { return pcms::test::linear_f(x, y); }, + 5e-3); + pcms::test::CheckEvaluation( + *evaluator, field_b, pts, OMEGA_H_LAMBDA(Real, Real) { return cval; }, + 5e-3); } // ============================================================================ @@ -225,24 +208,21 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: Evaluate throws for " Rank2View coords_view(src.data(), 25, 2); auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( - coords_view, CoordinateSystem::Cartesian); - auto field = fs.CreateField(pcms::FieldMetadata{}); + pcms::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view)); + auto field = fs->CreateFunction(); - auto pts = QueryPoints(); + auto pts = pcms::test::StandardEvalCoords2D(); int n = static_cast(pts.size()) / 2; - auto device_coords = - pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = fs.CreatePointEvaluator( + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + 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)); + Kokkos::View out_device("out", n, 2); + REQUIRE_THROWS( + evaluator->Evaluate(field, pcms::test::TagLike(field, out_device))); } // ============================================================================ @@ -257,63 +237,108 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: default MLSOptions — " // 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); }); + pcms::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view)); + auto field = fs->CreateFunction(); + pcms::test::SetField(field, OMEGA_H_LAMBDA(Real, Real) { return Real(1.0); }); - auto pts = QueryPoints(); + auto pts = pcms::test::StandardEvalCoords2D(); int n = static_cast(pts.size()) / 2; - auto device_coords = - pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = fs.CreatePointEvaluator( + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + 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); + Kokkos::View out_device("out", n, 1); // Just verify it runs without error and returns finite values - REQUIRE_NOTHROW(evaluator->Evaluate(field, out_view)); + REQUIRE_NOTHROW( + evaluator->Evaluate(field, pcms::test::TagLike(field, out_device))); 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))); + REQUIRE(std::isfinite(out_host(i, 0))); } TEST_CASE("PolynomialReconstructionFunctionSpace MLS: CreatePointEvaluator " - "rejects coordinate " - "system mismatch") + "rejects a query system that is not the space's") { 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::CoordinateView(pcms::csys::CylindricalRZ::Create(), + coords_view), + DefaultTestOptions()); + + auto pts = pcms::test::StandardEvalCoords2D(); + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + // The hoisted domain-coordinate system identity check fires before any MLS + // logic. + REQUIRE_THROWS(fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view))); } -TEST_CASE("PolynomialReconstructionFunctionSpace MLS: CreatePointEvaluator " - "rejects non-Cartesian " - "point-cloud coordinates") +// ensure that MLS reconstruction works with CylindricalRZ which has identity +// metric +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: accepts the (R, Z) " + "poloidal section, whose metric is the identity") { - auto src = MakeGrid2D(5); - Rank2View coords_view(src.data(), 25, 2); + auto src = MakeGrid2D(7); + Rank2View coords_view( + src.data(), static_cast(src.size() / 2), 2); auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( - coords_view, CoordinateSystem::Cylindrical, DefaultTestOptions()); + pcms::CoordinateView(pcms::csys::CylindricalRZ::Create(), + coords_view), + DefaultTestOptions()); + + // A linear field in (R, Z), which a degree-1 MLS must reproduce. + auto linear_rz = [](Real r, Real z) { return 2.0 + 3.0 * r - 1.5 * z; }; + auto field = fs->CreateFunction(); + std::vector dof_values; + dof_values.reserve(src.size() / 2); + for (size_t i = 0; i < src.size() / 2; ++i) { + dof_values.push_back(linear_rz(src[2 * i], src[2 * i + 1])); + } + field.SetDOFHolderDataHost( + pcms::ValueView( + pcms::ValueBasis{}, + Rank2View( + dof_values.data(), static_cast(dof_values.size()), 1))); + + std::vector pts{0.3, 0.4, 0.6, 0.7}; + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::CylindricalRZ::Create()); + auto evaluator = fs->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View out_device("out", 2, 1); + evaluator->Evaluate(field, pcms::test::TagLike(field, out_device)); + auto out_host = + Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); + + REQUIRE(out_host(0, 0) == Catch::Approx(linear_rz(0.3, 0.4)).margin(5e-3)); + REQUIRE(out_host(1, 0) == Catch::Approx(linear_rz(0.6, 0.7)).margin(5e-3)); +} + +TEST_CASE("PolynomialReconstructionFunctionSpace MLS: CreatePointEvaluator " + "rejects coordinates whose metric is not the identity") +{ + auto src = MakeGrid3D(3); + Rank2View coords_view( + src.data(), static_cast(src.size() / 3), 3); - auto pts = QueryPoints(); - auto device_coords = - pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cylindrical); - REQUIRE_THROWS(fs.CreatePointEvaluator( + auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( + pcms::CoordinateView( + pcms::csys::CylindricalRThetaZ::Create(), coords_view), + DefaultTestOptions3D()); + + auto pts = std::vector{0.3, 0.4, 0.5}; + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::CylindricalRThetaZ::Create(), 3); + // h_theta = r, so summing squared coordinate differences is not a distance. + REQUIRE_THROWS(fs->CreatePointEvaluator( pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view))); } @@ -331,27 +356,28 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: radius option is " opts.basis = pcms::RadialBasisFunction::RBF_CONST; auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( - coords_view, CoordinateSystem::Cartesian, opts); - auto field = fs.CreateField(pcms::FieldMetadata{}); + pcms::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view), + opts); + auto field = fs->CreateFunction(); std::vector dof_values{1.0, 5.0}; - field.GetData().SetDOFHolderDataHost(Rank2View( - dof_values.data(), static_cast(dof_values.size()), 1)); + field.SetDOFHolderDataHost( + pcms::ValueView( + pcms::ValueBasis{}, + Rank2View( + dof_values.data(), static_cast(dof_values.size()), 1))); std::vector pts{0.0, 0.0}; - auto device_coords = - pcms::test::CreateDeviceCoordinateView(pts, CoordinateSystem::Cartesian); - auto evaluator = fs.CreatePointEvaluator( + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred()); + 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); + Kokkos::View out_device("out", 1, 1); + evaluator->Evaluate(field, pcms::test::TagLike(field, out_device)); auto out_host = Kokkos::create_mirror_view_and_copy(HostMemorySpace(), out_device); - REQUIRE(out_host(0) == Catch::Approx(1.0).margin(1e-8)); + REQUIRE(out_host(0, 0) == Catch::Approx(1.0).margin(1e-8)); } TEST_CASE("PolynomialReconstructionFunctionSpace MLS: 3D point clouds preserve " @@ -362,11 +388,12 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: 3D point clouds preserve " Rank2View coords_view(src.data(), 27, 3); auto fs = pcms::PolynomialReconstructionFunctionSpace::Create( - coords_view, CoordinateSystem::Cartesian, DefaultTestOptions3D()); + pcms::CoordinateView(pcms::csys::Cartesian::Deferred(), + coords_view), + DefaultTestOptions3D()); auto layout_coords_device = - fs.GetLayout()->GetDOFHolderCoordinates().GetValues(); - auto layout_coords = - pcms::test::CopyCoordinatesToHost(layout_coords_device, 27, 3); + fs->GetLayout()->GetDOFHolderCoordinates().GetValues(); + auto layout_coords = pcms::test::CopyCoordinatesToHost(layout_coords_device); 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])); @@ -374,35 +401,25 @@ TEST_CASE("PolynomialReconstructionFunctionSpace MLS: 3D point clouds preserve " 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(Rank2View( - dof_values.data(), static_cast(dof_values.size()), 1)); + auto field = fs->CreateFunction(); + pcms::test::SetField( + field, + KOKKOS_LAMBDA(Real x, Real y, Real z) { return x + 2.0 * y + 3.0 * z; }); 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( + auto device_coords = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::Cartesian::Deferred(), 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); + Kokkos::View out_device("out", 2, 1); + evaluator->Evaluate(field, pcms::test::TagLike(field, out_device)); 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)); + REQUIRE(out_host(0, 0) == Catch::Approx(2.25).margin(1e-2)); + REQUIRE(out_host(1, 0) == Catch::Approx(3.75).margin(1e-2)); + REQUIRE(out_host(1, 0) - out_host(0, 0) == Catch::Approx(1.5).margin(1e-2)); } diff --git a/test/test_proxy_coupling.cpp b/test/test_proxy_coupling.cpp index 455fc1837..0e8c04d47 100644 --- a/test/test_proxy_coupling.cpp +++ b/test/test_proxy_coupling.cpp @@ -8,9 +8,10 @@ #include "test_support.h" #include "pcms/coupler/coupler.hpp" #include "pcms/field/function_space/lagrange.h" -#include "pcms/field/field_metadata.h" +#include "pcms/field/value_view.hpp" #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::GO; using pcms::make_array_view; @@ -22,7 +23,7 @@ static constexpr int COMM_ROUNDS = 4; namespace ts = test_support; void initializeFieldWithGids(const pcms::FieldLayout& layout, - pcms::FieldData* field, + pcms::Field& field, pcms::Real multiplier = 1.0) { auto gids = layout.GetGidsHost(); @@ -35,17 +36,21 @@ void initializeFieldWithGids(const pcms::FieldLayout& layout, Kokkos::RangePolicy(0, n), [=](int i) { ids[i] = gids[i] * multiplier; }); - field->SetDOFHolderDataHost( - pcms::Rank2View(ids.data(), n, 1)); + field.SetDOFHolderDataHost( + pcms::ValueView( + pcms::ValueBasis{}, + pcms::Rank2View(ids.data(), n, + 1))); } bool validateField(const pcms::FieldLayout& layout, - pcms::FieldData* field, + const pcms::Field* field, const std::string& field_name, int rank, pcms::Real multiplier = 1.0) { auto gids = layout.GetGidsHost(); - auto copied_array = pcms::FlattenToRank1View(field->GetDOFHolderDataHost()); + auto copied_array = + pcms::FlattenToRank1View(field->GetDOFHolderDataHost().GetValues()); auto owned = layout.GetOwnedHost(); const auto n = layout.GetNumOwnedDofHolder(); @@ -86,24 +91,22 @@ void xgc_delta_f(MPI_Comm comm, Omega_h::Mesh& mesh) int rank; MPI_Comm_rank(comm, &rank); - pcms::Coupler coupler("proxy_couple", comm, false, {}); - pcms::Application* app = coupler.AddApplication("proxy_couple_xgc_delta_f"); + pcms::CouplerComm coupler("proxy_couple", comm, false, {}); + pcms::ApplicationComm* app = + coupler.AddApplication("proxy_couple_xgc_delta_f"); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - app->AddLayout("gids", factory.GetLayout()); + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "gids"); - auto gids_field = factory.CreateField(pcms::FieldMetadata{}); - auto gids2_field = factory.CreateField(pcms::FieldMetadata{}); + auto gids_field = factory->CreateFunction("gids"); + auto gids2_field = factory->CreateFunction("gids2"); - auto* gids_ptr = &gids_field.GetData(); - auto* gids2_ptr = &gids2_field.GetData(); + initializeFieldWithGids(*factory->GetLayout(), gids_field, 1.0); + initializeFieldWithGids(*factory->GetLayout(), gids2_field, 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)); + const auto* gids_ptr = &app->AddField(std::move(gids_field)).GetField(); + app->AddField(std::move(gids2_field)); do { for (int i = 0; i < COMM_ROUNDS; ++i) { @@ -114,7 +117,7 @@ void xgc_delta_f(MPI_Comm comm, Omega_h::Mesh& mesh) app->ReceiveField("gids"); //(Alt) df_gid_field->Receive(); app->EndReceivePhase(); - if (!validateField(*factory.GetLayout(), 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); @@ -127,20 +130,19 @@ void xgc_total_f(MPI_Comm comm, Omega_h::Mesh& mesh) int rank; MPI_Comm_rank(comm, &rank); - pcms::Coupler coupler("proxy_couple", comm, false, {}); - pcms::Application* app = coupler.AddApplication("proxy_couple_xgc_total_f"); + pcms::CouplerComm coupler("proxy_couple", comm, false, {}); + pcms::ApplicationComm* app = + coupler.AddApplication("proxy_couple_xgc_total_f"); auto factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - app->AddLayout("gids", factory.GetLayout()); - - auto gids_field = factory.CreateField(pcms::FieldMetadata{}); + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "gids"); - auto* gids_ptr = &gids_field.GetData(); + auto gids_field = factory->CreateFunction("gids"); - initializeFieldWithGids(*factory.GetLayout(), gids_ptr, 10.0); + initializeFieldWithGids(*factory->GetLayout(), gids_field, 10.0); - app->AddField("gids", std::move(gids_field)); + const auto* gids_ptr = &app->AddField(std::move(gids_field)).GetField(); do { for (int i = 0; i < COMM_ROUNDS; ++i) { @@ -151,7 +153,7 @@ void xgc_total_f(MPI_Comm comm, Omega_h::Mesh& mesh) app->ReceiveField("gids"); //(Alt) tf_gid_field->Receive(); app->EndReceivePhase(); - if (!validateField(*factory.GetLayout(), 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); @@ -167,33 +169,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::Coupler cpl("proxy_couple", comm, true, - redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); + pcms::CouplerComm 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 factory_total = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - total_f->AddLayout("gids", factory_total.GetLayout()); + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "gids"); auto factory_delta = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - delta_f->AddLayout("gids", factory_delta.GetLayout()); + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), "global", + pcms::LagrangeFunctionSpace::DefaultBackend, "gids"); // TODO, fields should have a transfer policy rather than parameters - 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.GetData(); - auto* delta_gids_ptr = &delta_gids_field.GetData(); - auto* delta_gids2_ptr = &delta_gids2_field.GetData(); + auto total_gids_field = factory_total->CreateFunction("gids"); + auto delta_gids_field = factory_delta->CreateFunction("gids"); + auto delta_gids2_field = factory_delta->CreateFunction("gids2"); - total_f->AddField("gids", std::move(total_gids_field)); - delta_f->AddField("gids", std::move(delta_gids_field)); - delta_f->AddField("gids2", std::move(delta_gids2_field)); + const auto* total_gids_ptr = + &total_f->AddField(std::move(total_gids_field)).GetField(); + const auto* delta_gids_ptr = + &delta_f->AddField(std::move(delta_gids_field)).GetField(); + delta_f->AddField(std::move(delta_gids2_field)); do { for (int i = 0; i < COMM_ROUNDS; ++i) { @@ -201,7 +199,7 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) total_f->ReceiveField("gids"); total_f->EndReceivePhase(); - if (!validateField(*factory_total.GetLayout(), total_gids_ptr, "gids", + 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; @@ -212,7 +210,7 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) delta_f->ReceiveField("gids"); delta_f->EndReceivePhase(); - if (!validateField(*factory_delta.GetLayout(), delta_gids_ptr, "gids", + 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; diff --git a/test/test_proxy_coupling_xgc_client.c b/test/test_proxy_coupling_xgc_client.c index 43546b2d3..91898ab57 100644 --- a/test/test_proxy_coupling_xgc_client.c +++ b/test/test_proxy_coupling_xgc_client.c @@ -67,10 +67,11 @@ int main(int argc, char** argv) int communicating_rank = (i == plane) && (plane_rank == 0); if (plane == i) { field_adapters[i] = pcms_create_xgc_field_adapter( - "adapter1", plane_comm, data, nverts, PCMS_LONG_INT, rc, &in_overlap); + "adapter1", plane_comm, data, nverts, PCMS_LONG_INT, rc); } else { field_adapters[i] = pcms_create_dummy_field_adapter(); } + pcms_set_overlap_mask(client, field_name, &in_overlap); field[i] = pcms_add_field(client, field_name, field_adapters[i], communicating_rank); } diff --git a/test/test_proxy_coupling_xgc_client_fortran.f90 b/test/test_proxy_coupling_xgc_client_fortran.f90 index ab4341410..594a377db 100644 --- a/test/test_proxy_coupling_xgc_client_fortran.f90 +++ b/test/test_proxy_coupling_xgc_client_fortran.f90 @@ -89,10 +89,11 @@ Program main name = 'xgc_gids_plane_' // trim(plane_str) if (plane .eq. ix) then adapters(ix+1) = pcms_create_xgc_field_adapter(trim(name), MPI_COMM_SELF, c_loc(data_pointer), & - size(data_pointer), PCMS_LONG_INT, reverse_classification, in_overlap) + size(data_pointer), PCMS_LONG_INT, reverse_classification) else adapters(ix+1) = pcms_create_dummy_field_adapter() end if + call pcms_set_overlap_mask(client, trim(name), in_overlap) if ((plane_rank .eq. 0) .and. (ix .eq. plane)) then ! field participates in the communication fields(ix+1) = pcms_add_field(client, trim(name), adapters(ix+1), 1) diff --git a/test/test_proxy_coupling_xgc_server.cpp b/test/test_proxy_coupling_xgc_server.cpp index 4a76b748e..b7d067547 100644 --- a/test/test_proxy_coupling_xgc_server.cpp +++ b/test/test_proxy_coupling_xgc_server.cpp @@ -8,10 +8,10 @@ #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/field/field_metadata.h" +#include "pcms/field/value_view.hpp" #include "pcms/field/function_space/lagrange.h" +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::ConstructRCFromOmegaHMesh; using pcms::GO; @@ -26,8 +26,9 @@ 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::Coupler cpl("proxy_couple_server", comm, true, - redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); + pcms::CouplerComm 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")) { @@ -48,20 +49,18 @@ void xgc_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, std::string_view cpn_file) data[i].resize(mesh.nverts()); std::stringstream ss; ss << "xgc_gids_plane_" << i; + application->SetLayoutOverlapMask( + ss.str(), std::make_unique(ts::IsModelEntInOverlap{})); // 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::XGCFunctionSpace( - rc, ts::IsModelEntInOverlap{}, static_cast(mesh.nverts())); + auto function_space = + pcms::XGCFieldFactory(rc, static_cast(mesh.nverts()), ss.str()); 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))); + ss.str(), std::make_unique>( + function_space.GetXGCLayout(), pcms::ValueBasis{}, + make_array_view(data[i]), comm)); + fields.push_back(application->AddField(std::move(field))); } do { @@ -144,8 +143,9 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, // 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::Coupler cpl("proxy_couple_server", comm, true, - redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); + pcms::CouplerComm 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; @@ -171,16 +171,12 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, // 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))); + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), numbering, + pcms::LagrangeFunctionSpace::Backend::OmegaH, ss.str()); + auto field = factory->CreateFunction( + ss.str(), std::make_unique>( + factory->GetLayout(), pcms::ValueBasis{})); + fields.push_back(application->AddField(std::move(field))); } do { application->ReceivePhase([&]() { diff --git a/test/test_proxy_coupling_xgc_server_overlap.cpp b/test/test_proxy_coupling_xgc_server_overlap.cpp index 0161ee4e9..4fd3eba1c 100644 --- a/test/test_proxy_coupling_xgc_server_overlap.cpp +++ b/test/test_proxy_coupling_xgc_server_overlap.cpp @@ -8,11 +8,11 @@ #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/value_view.hpp" #include "pcms/field/function_space/lagrange.h" +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::ConstructRCFromOmegaHMesh; using pcms::GO; @@ -25,8 +25,9 @@ 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)}); + pcms::CouplerComm cpl( + "proxy_couple_server", comm, true, + redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); const auto partition = std::get(cpl.GetPartition()); ReverseClassificationVertex rc; @@ -47,28 +48,22 @@ void xgc_coupler_with_overlap(MPI_Comm comm, Omega_h::Mesh& mesh, std::stringstream ss; ss << "xgc_gids_plane_" << i; - auto overlap_mask = std::make_unique( - mesh.nverts(), [](int dim, int id) -> int8_t { + auto overlap_mask = + std::make_unique([](int dim, int id) -> int8_t { return ts::IsModelEntInOverlap{}(dim, id); }); application->SetLayoutOverlapMask(ss.str(), std::move(overlap_mask)); - auto function_space = pcms::XGCFunctionSpace( - rc, ts::IsModelEntInOverlap{}, static_cast(mesh.nverts())); - - application->AddLayout(ss.str(), function_space.GetLayout()); + auto function_space = + pcms::XGCFieldFactory(rc, static_cast(mesh.nverts()), ss.str()); 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); + ss.str(), std::make_unique>( + function_space.GetXGCLayout(), pcms::ValueBasis{}, + make_array_view(data[i]), comm)); - fields.push_back( - application->AddField(ss.str(), std::move(field), std::move(serializer))); + fields.push_back(application->AddField(std::move(field))); } do { @@ -142,8 +137,9 @@ void xgc_coupler_with_overlap(MPI_Comm comm, Omega_h::Mesh& mesh, 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)}); + pcms::CouplerComm 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"); @@ -166,28 +162,22 @@ void omegah_coupler_with_overlap(MPI_Comm comm, Omega_h::Mesh& mesh, std::stringstream ss; ss << "xgc_gids_plane_" << i; - auto overlap_mask = std::make_unique( - mesh.nverts(), [](int dim, int id) -> int8_t { + auto overlap_mask = + std::make_unique([](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{})); + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), numbering, + pcms::LagrangeFunctionSpace::Backend::OmegaH, ss.str()); - std::unique_ptr> serializer = - std::make_unique>(); + auto field = factory->CreateFunction( + ss.str(), std::make_unique>( + factory->GetLayout(), pcms::ValueBasis{})); - fields.push_back( - application->AddField(ss.str(), std::move(field), std::move(serializer))); + fields.push_back(application->AddField(std::move(field))); } do { diff --git a/test/test_spline_interpolator.cpp b/test/test_spline_interpolator.cpp index 1052582fd..1bc682ef6 100644 --- a/test/test_spline_interpolator.cpp +++ b/test/test_spline_interpolator.cpp @@ -2,13 +2,24 @@ #include "pcms.h" #include #include +#include #include +#include +#include #include +#include +#include +#include +#include +#include #include "pcms/field/evaluator/spline_interpolator.hpp" +#include "field_test_utils.h" using namespace pcms; +using pcms::test::CreateDeviceRank1View; -using TestMemorySpace = Kokkos::DefaultExecutionSpace; +using TestMemorySpace = pcms::DeviceMemorySpace; +using TestExecutionSpace = TestMemorySpace::execution_space; bool are_equal(double a, double b, double tolerance = 1e-7) { @@ -38,7 +49,8 @@ void tset(int nth, Kokkos::View th, double thmax) { Kokkos::parallel_for( - Kokkos::RangePolicy(0, nth), KOKKOS_LAMBDA(const int ith) { + Kokkos::RangePolicy(0, nth), + KOKKOS_LAMBDA(const int ith) { th[ith] = thmin + static_cast(ith) * (thmax - thmin) / static_cast(nth - 1); sth[ith] = 2.0 + std::sin(th[ith]); @@ -53,7 +65,8 @@ void xset(int nx, Kokkos::View x, return; // avoid division by zero Kokkos::parallel_for( - Kokkos::RangePolicy(0, nx), KOKKOS_LAMBDA(const int ix) { + Kokkos::RangePolicy(0, nx), + KOKKOS_LAMBDA(const int ix) { x[ix] = xmin + static_cast(ix) * (xmax - xmin) / static_cast(nx - 1); ex[ix] = std::exp(2.0 * x[ix] - 1.0); @@ -62,13 +75,11 @@ void xset(int nx, Kokkos::View x, void ffset(int num, Kokkos::View xf, Kokkos::View tf, - Kokkos::View f) + Kokkos::View f) { Kokkos::parallel_for( Kokkos::MDRangePolicy>({0, 0}, {num, num}), - KOKKOS_LAMBDA(const int i, const int j) { - f(i * num + j) = xf[i] * tf[j]; - }); + KOKKOS_LAMBDA(const int i, const int j) { f(i, j) = xf[i] * tf[j]; }); } void bset(Rank1View fx, int nx, @@ -79,13 +90,15 @@ void bset(Rank1View fx, int nx, Rank1View bcth2) { Kokkos::parallel_for( - Kokkos::RangePolicy(0, nth), KOKKOS_LAMBDA(const int ith) { + Kokkos::RangePolicy(0, nth), + KOKKOS_LAMBDA(const int ith) { bcx1[ith] = 2.0 * fx(0) * fth(ith); // df/dx at x(1) bcx2[ith] = 2.0 * fx(nx - 1) * fth(ith); // df/dx at x(nx) }); Kokkos::parallel_for( - Kokkos::RangePolicy(0, nx), KOKKOS_LAMBDA(const int ix) { + Kokkos::RangePolicy(0, nx), + KOKKOS_LAMBDA(const int ix) { bcth1(ix) = fx(ix); // df/dth at th = 0 (th[0]) bcth2(ix) = fx(ix); // df/dth at th = 2π (th[nth - 1]) }); @@ -104,7 +117,7 @@ void generate_gt_2d(Rank1View gt_splinv, void compare(Rank2View splinv, Rank1View gt_splinv, - Rank1View res, std::string label) + Kokkos::View res, std::string label) { // Compare the output of splinv with gt_splinv double result_fdif = 0.0; @@ -127,7 +140,7 @@ void compare(Rank2View splinv, void compare_2d(Rank2View splinv, Rank1View gt_splinv, - Rank1View res, std::string label) + Kokkos::View res, std::string label) { // Compare the output of splinv with gt_splinv double result_fdif = 0.0; @@ -167,9 +180,8 @@ void dotest1(int ns, Rank1View x, using std::data; using std::size; - Kokkos::View splinv_view( - "splinv_view", static_cast(3) * nt); - auto splinv = Rank2View(splinv_view.data(), nt, 3); + Kokkos::View splinv_view("splinv_view", nt, 3); + auto splinv = MakeRank2View(splinv_view); reset_2dspan(splinv); // interpolator.cspline(x, ns, fspl, 1, 1, 1, 1, wk); @@ -178,8 +190,8 @@ void dotest1(int ns, Rank1View x, BoundaryCondition::FIRST_DERIVATIVE_MATCH, 1); explicit_interpolator.evaluate(xt, splinv); - auto res1 = Rank1View(res_1d.data(), 2); - compare(splinv, ft, res1, "cspline"); + compare(splinv, ft, Kokkos::subview(res_1d, Kokkos::make_pair(0, 2)), + "cspline"); reset_2dspan(splinv); ExplicitCubicSplineInterpolator @@ -187,8 +199,8 @@ void dotest1(int ns, Rank1View x, BoundaryCondition::PERIODIC, 0); explicit_interpolator_periodic.evaluate(xt, splinv); - auto res2 = Rank1View(res_1d.data() + 2, 2); - compare(splinv, ft, res2, "cspline_periodic"); + compare(splinv, ft, Kokkos::subview(res_1d, Kokkos::make_pair(2, 4)), + "cspline_periodic"); reset_2dspan(splinv); CompactCubicSplineInterpolator compact_interpolator( @@ -196,8 +208,8 @@ void dotest1(int ns, Rank1View x, BoundaryCondition::FIRST_DERIVATIVE_MATCH, 1); compact_interpolator.evaluate(xt, splinv); - auto res3 = Rank1View(res_1d.data() + 4, 2); - compare(splinv, ft, res3, "compact_cubic"); + compare(splinv, ft, Kokkos::subview(res_1d, Kokkos::make_pair(4, 6)), + "compact_cubic"); } void pspltest1(Kokkos::View res_1d) @@ -218,13 +230,12 @@ void pspltest1(Kokkos::View res_1d) 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 x = MakeRank1View(x_view); - auto z2sin = - Rank1View(z2sin_view.data(), z2sin_view.size()); + auto z2sin = MakeRank1View(z2sin_view); - auto xtest = Rank1View(xtest_view.data(), 1000); - auto ftest = Rank1View(ftest_view.data(), 1000); + auto xtest = MakeRank1View(xtest_view); + auto ftest = MakeRank1View(ftest_view); // Call test function dotest1(inum, x, z2sin, 1000, xtest, ftest, res_1d); @@ -246,11 +257,10 @@ void pspltest1_long_periodic(Kokkos::View res_1d) 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); + auto x = MakeRank1View(x_view); + auto z2sin = MakeRank1View(z2sin_view); + auto xtest = MakeRank1View(xtest_view); + auto ftest = MakeRank1View(ftest_view); dotest1(inum, x, z2sin, 1000, xtest, ftest, res_1d); } @@ -260,7 +270,7 @@ void dotest2(Rank1View x, Rank1View th, Rank1View fth, Rank1View dfth, int nth, - Rank4View f, + Kokkos::View f, Rank1View bcx1, Rank1View bcx2, Rank1View bcth1, @@ -277,15 +287,13 @@ void dotest2(Rank1View x, Kokkos::parallel_for( Kokkos::MDRangePolicy>({0, 0}, {nth, nx}), KOKKOS_LAMBDA(const int ith, const int ix) { - values_view(ix * nth + ith) = f(0, 0, ix, ith); + values_view(ix * nth + ith) = f(ix, ith); }); - auto values = - Rank1View(values_view.data(), nth * nx); + auto values = MakeRank1View(values_view); bset(fx, nx, fth, nth, bcx1, bcx2, bcth1, bcth2); - Kokkos::View splinv_view("splinv_view", 400000); - auto splinv = - Rank2View(splinv_view.data(), 40000, 10); + Kokkos::View splinv_view("splinv_view", 40000, 10); + auto splinv = MakeRank2View(splinv_view); reset_2dspan(splinv); Kokkos::View xtest_grid_view("xtest_grid_view", @@ -293,13 +301,10 @@ void dotest2(Rank1View x, Kokkos::View thtest_grid_view("thtest_grid_view", 40000); createFlatGrid(xtest, thtest, xtest_grid_view, thtest_grid_view); - auto xtest_grid = - Rank1View(xtest_grid_view.data(), 40000); - auto thtest_grid = - Rank1View(thtest_grid_view.data(), 40000); + auto xtest_grid = MakeRank1View(xtest_grid_view); + auto thtest_grid = MakeRank1View(thtest_grid_view); Kokkos::View splinv_gt_view("splinv_view", 40000); - auto splinv_gt = - Rank1View(splinv_gt_view.data(), 40000); + auto splinv_gt = MakeRank1View(splinv_gt_view); generate_gt_2d(splinv_gt, fxtest, fthtest, ntest); ExplicitBiCubicSplineInterpolator @@ -310,8 +315,8 @@ void dotest2(Rank1View x, BoundaryCondition::FIRST_DERIVATIVE_MATCH, bcth2); explicit_interpolator.evaluate(xtest_grid, thtest_grid, splinv); - auto res1 = Rank1View(res_2d.data(), 2); - compare_2d(splinv, splinv_gt, res1, "bcspline"); + compare_2d(splinv, splinv_gt, + Kokkos::subview(res_2d, Kokkos::make_pair(0, 2)), "bcspline"); reset_2dspan(splinv); CompactBiCubicSplineInterpolator @@ -322,8 +327,8 @@ void dotest2(Rank1View x, BoundaryCondition::FIRST_DERIVATIVE_MATCH, bcth2); compact_interpolator.evaluate(xtest_grid, thtest_grid, splinv); - auto res2 = Rank1View(res_2d.data() + 2, 2); - compare_2d(splinv, splinv_gt, res2, "mkbicub"); + compare_2d(splinv, splinv_gt, + Kokkos::subview(res_2d, Kokkos::make_pair(2, 4)), "mkbicub"); // CompactBiCubicSplineInterpolator // test_interpolator(x, th, values); @@ -345,7 +350,7 @@ void pspltest2(Kokkos::View res_2d) Kokkos::View ttest_view("ttest_view", 200); Kokkos::View stest_view("stest_view", 200); Kokkos::View ctest_view("ctest_view", 200); - Kokkos::View f1_view("f1_view", 1600); + Kokkos::View f1("f1", 10, 10); Kokkos::View bcx1_view("bcx1_view", 40); Kokkos::View bcx2_view("bcx2_view", 40); Kokkos::View bcth1_view("bcth1_view", 40); @@ -357,34 +362,22 @@ void pspltest2(Kokkos::View res_2d) tset(10, t1_view, st1_view, ct1_view, zero, pi2); tset(200, ttest_view, stest_view, ctest_view, zero, pi2); - ffset(10, ex1_view, st1_view, f1_view); - - auto x1 = Rank1View(x1_view.data(), x1_view.size()); - auto ex1 = - Rank1View(ex1_view.data(), ex1_view.size()); - auto t1 = Rank1View(t1_view.data(), t1_view.size()); - auto st1 = - Rank1View(st1_view.data(), st1_view.size()); - auto ct1 = - Rank1View(ct1_view.data(), ct1_view.size()); - auto f1 = Rank4View(f1_view.data(), 4, 4, 10, 10); - auto bcx1 = - Rank1View(bcx1_view.data(), bcx1_view.size()); - auto bcx2 = - Rank1View(bcx2_view.data(), bcx2_view.size()); - auto bcth1 = - Rank1View(bcth1_view.data(), bcth1_view.size()); - auto bcth2 = - Rank1View(bcth2_view.data(), bcth2_view.size()); - - auto xtest = - Rank1View(xtest_view.data(), xtest_view.size()); - auto extest = - Rank1View(extest_view.data(), extest_view.size()); - auto ttest = - Rank1View(ttest_view.data(), ttest_view.size()); - auto stest = - Rank1View(stest_view.data(), stest_view.size()); + ffset(10, ex1_view, st1_view, f1); + + auto x1 = MakeRank1View(x1_view); + auto ex1 = MakeRank1View(ex1_view); + auto t1 = MakeRank1View(t1_view); + auto st1 = MakeRank1View(st1_view); + auto ct1 = MakeRank1View(ct1_view); + auto bcx1 = MakeRank1View(bcx1_view); + auto bcx2 = MakeRank1View(bcx2_view); + auto bcth1 = MakeRank1View(bcth1_view); + auto bcth2 = MakeRank1View(bcth2_view); + + auto xtest = MakeRank1View(xtest_view); + auto extest = MakeRank1View(extest_view); + auto ttest = MakeRank1View(ttest_view); + auto stest = MakeRank1View(stest_view); dotest2(x1, ex1, 10, t1, st1, ct1, 10, f1, bcx1, bcx2, bcth1, bcth2, xtest, extest, ttest, stest, 200, res_2d); @@ -424,3 +417,1079 @@ TEST_CASE("test_cubic_spline_interpolator") std::cout << "Total time for spline tests: " << time << " seconds" << std::endl; } + +namespace +{ + +/// Copy an n x ncol evaluate output to the host as point-major rows +std::vector to_row_major(Kokkos::View out) +{ + auto h = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, out); + std::vector rows; + for (size_t i = 0; i < h.extent(0); ++i) + for (size_t j = 0; j < h.extent(1); ++j) + rows.push_back(h(i, j)); + return rows; +} + +/// Evaluate a 1D interpolator with a selector and return the n x ncol output +template +std::vector eval_1d(Interpolator& interp, const std::vector& sel, + const std::vector& xq, int ncol, + double fill = 0.0) +{ + auto xd = CreateDeviceRank1View(xq); + Kokkos::View out("out", xq.size(), ncol); + Kokkos::deep_copy(out, fill); + interp.evaluate(CreateDeviceRank1View(sel), MakeRank1View(xd), + MakeRank2View(out)); + return to_row_major(out); +} + +/// Evaluate a 2D interpolator with a selector and return the n x ncol output +template +std::vector eval_2d(Interpolator& interp, const std::vector& sel, + const std::vector& xq, + const std::vector& yq, int ncol, + double fill = 0.0) +{ + auto xd = CreateDeviceRank1View(xq); + auto yd = CreateDeviceRank1View(yq); + Kokkos::View out("out", xq.size(), ncol); + Kokkos::deep_copy(out, fill); + interp.evaluate(CreateDeviceRank1View(sel), MakeRank1View(xd), + MakeRank1View(yd), MakeRank2View(out)); + return to_row_major(out); +} + +/// Uniform grid of n points on [a, b] +std::vector linspace(double a, double b, int n) +{ + std::vector x(n); + for (int i = 0; i < n; ++i) + x[i] = a + (b - a) * i / (n - 1); + return x; +} + +/// Sample f on the tensor grid with x as the slow index +template +std::vector sample_2d(const std::vector& x, + const std::vector& y, F f) +{ + std::vector v; + for (double xi : x) + for (double yj : y) + v.push_back(f(xi, yj)); + return v; +} + +} // namespace + +TEMPLATE_TEST_CASE("two-point splines keep both endpoint derivatives", + "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + // f = x^3 on [1, 3]: any pair of first/second derivative end conditions + // determines the cubic exactly + auto x = CreateDeviceRank1View({1.0, 3.0}); + auto f = CreateDeviceRank1View({1.0, 27.0}); + const auto d1 = BoundaryCondition::FIRST_DERIVATIVE_MATCH; + const auto d2 = BoundaryCondition::SECOND_DERIVATIVE_MATCH; + auto [lo, lo_data, hi, hi_data] = GENERATE_COPY( + std::make_tuple(d1, 3.0, d1, 27.0), std::make_tuple(d1, 3.0, d2, 18.0), + std::make_tuple(d2, 6.0, d1, 27.0), std::make_tuple(d2, 6.0, d2, 18.0)); + TestType interp(MakeRank1View(x), MakeRank1View(f), lo, lo_data, hi, hi_data); + auto out = eval_1d(interp, {1, 1, 1}, {1.5, 2.0, 2.5}, 3); + for (int i = 0; i < 3; ++i) { + const double xi = 1.5 + 0.5 * i; + CHECK(out[3 * i] == Catch::Approx(xi * xi * xi)); + CHECK(out[3 * i + 1] == Catch::Approx(3 * xi * xi)); + CHECK(out[3 * i + 2] == Catch::Approx(6 * xi)); + } +} + +TEMPLATE_TEST_CASE("non-zero boundary data on the lower side only is applied", + "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + // f = g(x) (y - y^2/2) has df/dy = g(x) at y = 0 and 0 at y = 1 + auto g = [](double x) { return 1 + x + x * x + x * x * x; }; + auto f = [&](double x, double y) { return g(x) * (y - 0.5 * y * y); }; + auto xh = linspace(0, 1, 5); + auto yh = linspace(0, 1, 5); + std::vector lo_h; + for (double xi : xh) + lo_h.push_back(g(xi)); + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + auto v = CreateDeviceRank1View(sample_2d(xh, yh, f)); + auto lo = CreateDeviceRank1View(lo_h); + auto hi = CreateDeviceRank1View(std::vector(xh.size(), 0.0)); + auto none = CreateDeviceRank1View({}); + const auto nak = BoundaryCondition::NOT_A_KNOT; + const auto d1 = BoundaryCondition::FIRST_DERIVATIVE_MATCH; + TestType interp(MakeRank1View(x), MakeRank1View(y), MakeRank1View(v), nak, + MakeRank1View(none), nak, MakeRank1View(none), d1, + MakeRank1View(lo), d1, MakeRank1View(hi)); + std::vector xq = {0.1, 0.45, 0.8}; + std::vector yq = {0.05, 0.6, 0.95}; + auto out = eval_2d(interp, {1, 0, 0, 0, 0, 0}, xq, yq, 1); + for (size_t i = 0; i < xq.size(); ++i) + CHECK(out[i] == Catch::Approx(f(xq[i], yq[i]))); +} + +TEST_CASE("spline grids must be finite, ascending and non-degenerate", + "[spline]") +{ + const double nan = std::numeric_limits::quiet_NaN(); + const double inf = std::numeric_limits::infinity(); + auto grid = GENERATE_COPY(std::vector{1.0}, + std::vector{1.0, 1.0, 1.0, 1.0}, + std::vector{3.0, 2.0, 1.0, 0.0}, + std::vector{0.0, nan, 2.0, 3.0}, + std::vector{0.0, 1.0, 2.0, inf}); + auto x = CreateDeviceRank1View(grid); + auto f = CreateDeviceRank1View(std::vector(grid.size(), 1.0)); + using Interp = CompactCubicSplineInterpolator; + const auto nak = BoundaryCondition::NOT_A_KNOT; + REQUIRE_THROWS_AS(Interp(MakeRank1View(x), MakeRank1View(f), nak, 0, nak, 0), + pcms_error); +} + +TEST_CASE("nearly uniform spline grids are rejected", "[spline]") +{ + // the solver assumes one spacing, so a 5e-4 relative deviation would be + // silently interpolated as if uniform + auto x = CreateDeviceRank1View({0.0, 1.0, 2.0005, 3.0}); + auto f = CreateDeviceRank1View({0.0, 1.0, 8.0, 27.0}); + using Interp = ExplicitCubicSplineInterpolator; + const auto nak = BoundaryCondition::NOT_A_KNOT; + REQUIRE_THROWS_AS(Interp(MakeRank1View(x), MakeRank1View(f), nak, 0, nak, 0), + pcms_error); +} + +TEMPLATE_TEST_CASE("1D queries outside the grid leave the output untouched", + "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + auto xh = linspace(0, 1, 6); + auto x = CreateDeviceRank1View(xh); + auto f = CreateDeviceRank1View(xh); + TestType interp(MakeRank1View(x), MakeRank1View(f), + BoundaryCondition::NOT_A_KNOT, 0, + BoundaryCondition::NOT_A_KNOT, 0); + const double nan = std::numeric_limits::quiet_NaN(); + auto out = eval_1d(interp, {1, 1, 0}, {nan, -1.0e6, 1.0e6}, 2); + for (double v : out) + CHECK(v == 0.0); +} + +TEMPLATE_TEST_CASE("2D queries outside the grid leave the output untouched", + "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + auto xh = linspace(0, 1, 6); + auto x = CreateDeviceRank1View(xh); + auto v = CreateDeviceRank1View( + sample_2d(xh, xh, [](double a, double b) { return a + b; })); + auto none = CreateDeviceRank1View({}); + const auto nak = BoundaryCondition::NOT_A_KNOT; + TestType interp(MakeRank1View(x), MakeRank1View(x), MakeRank1View(v), nak, + MakeRank1View(none), nak, MakeRank1View(none), nak, + MakeRank1View(none), nak, MakeRank1View(none)); + const double nan = std::numeric_limits::quiet_NaN(); + auto out = eval_2d(interp, {1, 1, 0, 0, 0, 0}, {nan, 0.5, -1.0e6, 0.5}, + {0.5, nan, 0.5, 1.0e6}, 2); + for (double val : out) + CHECK(val == 0.0); +} + +namespace +{ + +/// d^k/dx^k of the cubic with coefficients c +double cubic(const double (&c)[4], double x, int k) +{ + const double fall[4][4] = { + {1, 1, 1, 1}, {0, 1, 2, 3}, {0, 0, 2, 6}, {0, 0, 0, 6}}; + double sum = 0; + for (int j = k; j < 4; ++j) + sum += c[j] * fall[k][j] * std::pow(x, j - k); + return sum; +} + +double px(double x, int k) +{ + const double c[4] = {1, 2, -1, 0.5}; + return cubic(c, x, k); +} + +double py(double y, int k) +{ + const double c[4] = {2, -1, 0.3, 0.2}; + return cubic(c, y, k); +} + +struct SelectorCase +{ + std::vector selector; + std::vector> orders; +}; + +} // namespace + +TEMPLATE_TEST_CASE("1D selectors pick derivatives in PSPLINE order", "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + auto xh = linspace(-1, 2, 6); + std::vector fh; + for (double x : xh) + fh.push_back(px(x, 0)); + auto x = CreateDeviceRank1View(xh); + auto f = CreateDeviceRank1View(fh); + TestType interp(MakeRank1View(x), MakeRank1View(f), + BoundaryCondition::NOT_A_KNOT, 0, + BoundaryCondition::NOT_A_KNOT, 0); + const std::vector cases = {{{1, 1, 1}, {{0}, {1}, {2}}}, + {{0, 1, 0}, {{1}}}, + {{0, 0, 1}, {{2}}}, + {{3, 0, 0}, {{3}}}}; + const std::vector xq = {-1.0, 0.3, 1.7, 2.0}; + const int ncol = 4; + for (const auto& c : cases) { + auto out = eval_1d(interp, c.selector, xq, ncol, -999.0); + for (size_t i = 0; i < xq.size(); ++i) { + for (int j = 0; j < ncol; ++j) { + INFO("selector " << c.selector[0] << c.selector[1] << c.selector[2] + << " point " << i << " column " << j); + const double v = out[i * ncol + j]; + if (j < static_cast(c.orders.size())) + CHECK(v == Catch::Approx(px(xq[i], c.orders[j][0]))); + else + CHECK(v == -999.0); + } + } + } +} + +TEMPLATE_TEST_CASE("2D selectors pick derivatives in PSPLINE order", "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + constexpr bool is_explicit = + std::is_same_v>; + auto xh = linspace(0, 2, 6); + auto yh = linspace(-1, 1, 5); + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + auto v = CreateDeviceRank1View( + sample_2d(xh, yh, [](double a, double b) { return px(a, 0) * py(b, 0); })); + auto none = CreateDeviceRank1View({}); + const auto nak = BoundaryCondition::NOT_A_KNOT; + TestType interp(MakeRank1View(x), MakeRank1View(y), MakeRank1View(v), nak, + MakeRank1View(none), nak, MakeRank1View(none), nak, + MakeRank1View(none), nak, MakeRank1View(none)); + + std::vector cases = { + {{1, 1, 1, 1, 1, 1}, {{0, 0}, {1, 0}, {0, 1}, {2, 0}, {0, 2}, {1, 1}}}, + {{0, 1, 0, 0, 1, 0}, {{1, 0}, {0, 2}}}, + {{3, 1, 1, 1, 1}, {{3, 0}, {2, 1}, {1, 2}, {0, 3}}}, + {{3, 0, 1, 0, 1}, {{2, 1}, {0, 3}}}, + {{4, 1, 1, 1}, {{3, 1}, {2, 2}, {1, 3}}}, + {{5, 1, 1}, {{3, 2}, {2, 3}}}, + {{6}, {{3, 3}}}}; + if (is_explicit) + cases.push_back({{-1, 0, 0, 0, 0, 0}, {{0, 0}, {2, 0}, {0, 2}, {2, 2}}}); + const std::vector xq = {0.0, 0.7, 1.3, 2.0}; + const std::vector yq = {-1.0, 0.1, -0.45, 1.0}; + const int ncol = 7; + for (const auto& c : cases) { + auto out = eval_2d(interp, c.selector, xq, yq, ncol, -999.0); + for (size_t i = 0; i < xq.size(); ++i) { + for (int j = 0; j < ncol; ++j) { + INFO("selector " << c.selector[0] << " point " << i << " column " << j); + const double val = out[i * ncol + j]; + if (j < static_cast(c.orders.size())) { + const double expected = + px(xq[i], c.orders[j][0]) * py(yq[i], c.orders[j][1]); + CHECK(val == Catch::Approx(expected).margin(1e-9)); + } else { + CHECK(val == -999.0); + } + } + } + } +} + +TEMPLATE_TEST_CASE("1D evaluate without a selector writes every value", + "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + // the overload builds its own value-only selector on the device; NaN marks + // outputs it never wrote + auto xh = linspace(-1, 2, 6); + std::vector fh; + for (double x : xh) + fh.push_back(px(x, 0)); + auto x = CreateDeviceRank1View(xh); + auto f = CreateDeviceRank1View(fh); + TestType interp(MakeRank1View(x), MakeRank1View(f), + BoundaryCondition::NOT_A_KNOT, 0, + BoundaryCondition::NOT_A_KNOT, 0); + const std::vector xq = {-1.0, 0.3, 1.7, 2.0}; + auto xd = CreateDeviceRank1View(xq); + Kokkos::View out("out", xq.size(), 1); + Kokkos::deep_copy(out, std::numeric_limits::quiet_NaN()); + interp.evaluate(MakeRank1View(xd), MakeRank2View(out)); + auto h = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, out); + for (size_t i = 0; i < xq.size(); ++i) { + INFO("point " << i); + CHECK(h(i, 0) == Catch::Approx(px(xq[i], 0))); + } +} + +TEMPLATE_TEST_CASE("2D evaluate without a selector writes every value", + "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + auto xh = linspace(0, 2, 6); + auto yh = linspace(-1, 1, 5); + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + auto v = CreateDeviceRank1View( + sample_2d(xh, yh, [](double a, double b) { return px(a, 0) * py(b, 0); })); + auto none = CreateDeviceRank1View({}); + const auto nak = BoundaryCondition::NOT_A_KNOT; + TestType interp(MakeRank1View(x), MakeRank1View(y), MakeRank1View(v), nak, + MakeRank1View(none), nak, MakeRank1View(none), nak, + MakeRank1View(none), nak, MakeRank1View(none)); + const std::vector xq = {0.0, 0.7, 1.3, 2.0}; + const std::vector yq = {-1.0, 0.1, -0.45, 1.0}; + auto xd = CreateDeviceRank1View(xq); + auto yd = CreateDeviceRank1View(yq); + Kokkos::View out("out", xq.size(), 1); + Kokkos::deep_copy(out, std::numeric_limits::quiet_NaN()); + interp.evaluate(MakeRank1View(xd), MakeRank1View(yd), MakeRank2View(out)); + auto h = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace{}, out); + for (size_t i = 0; i < xq.size(); ++i) { + INFO("point " << i); + CHECK(h(i, 0) == Catch::Approx(px(xq[i], 0) * py(yq[i], 0)).margin(1e-9)); + } +} + +TEMPLATE_TEST_CASE("1D splines build and evaluate in host memory", "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + // every kernel must run in the interpolator's own space, even when the + // default execution space is a device + auto xh = linspace(-1, 2, 6); + std::vector fh; + for (double x : xh) + fh.push_back(px(x, 0)); + std::vector xq = {-1.0, 0.3, 1.7, 2.0}; + using H = Kokkos::HostSpace; + Kokkos::View out("out", xq.size(), 1); + Kokkos::deep_copy(out, std::numeric_limits::quiet_NaN()); + TestType interp(make_array_view(xh), make_array_view(fh), + BoundaryCondition::NOT_A_KNOT, 0, + BoundaryCondition::NOT_A_KNOT, 0); + interp.evaluate(make_array_view(xq), MakeRank2View(out)); + Kokkos::fence(); + for (size_t i = 0; i < xq.size(); ++i) { + INFO("point " << i); + CHECK(out(i, 0) == Catch::Approx(px(xq[i], 0))); + } +} + +TEMPLATE_TEST_CASE( + "2D splines build and evaluate in host memory", "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + auto xh = linspace(0, 2, 6); + auto yh = linspace(-1, 1, 5); + auto v = + sample_2d(xh, yh, [](double a, double b) { return px(a, 0) * py(b, 0); }); + std::vector xq = {0.0, 0.7, 1.3, 2.0}; + std::vector yq = {-1.0, 0.1, -0.45, 1.0}; + using H = Kokkos::HostSpace; + Kokkos::View out("out", xq.size(), 1); + Kokkos::deep_copy(out, std::numeric_limits::quiet_NaN()); + Rank1View none(nullptr, 0); + const auto nak = BoundaryCondition::NOT_A_KNOT; + TestType interp(make_array_view(xh), make_array_view(yh), make_array_view(v), + nak, none, nak, none, nak, none, nak, none); + interp.evaluate(make_array_view(xq), make_array_view(yq), MakeRank2View(out)); + Kokkos::fence(); + for (size_t i = 0; i < xq.size(); ++i) { + INFO("point " << i); + CHECK(out(i, 0) == Catch::Approx(px(xq[i], 0) * py(yq[i], 0)).margin(1e-9)); + } +} + +TEST_CASE("explicit and compact splines agree for every boundary condition", + "[spline]") +{ + using BC = BoundaryCondition; + const double pi2 = 2 * M_PI; + auto [lo, hi] = GENERATE( + std::make_pair(BC::NOT_A_KNOT, BC::NOT_A_KNOT), + std::make_pair(BC::PERIODIC, BC::PERIODIC), + std::make_pair(BC::FIRST_DERIVATIVE_MATCH, BC::SECOND_DERIVATIVE_MATCH), + std::make_pair(BC::FIRST_DERIVATIVE_ZERO, BC::SECOND_DERIVATIVE_ZERO), + std::make_pair(BC::FIRST_DIV_DIFF_MATCH, BC::SECOND_DIV_DIFF_MATCH), + std::make_pair(BC::THIRD_DIV_DIFF_MATCH, BC::NOT_A_KNOT)); + INFO("boundary conditions " << static_cast(lo) << " " + << static_cast(hi)); + auto xh = linspace(0, pi2, 9); + auto yh = linspace(0, pi2, 7); + auto f = [](double a, double b) { return std::sin(a) * std::cos(b) + 1; }; + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + std::vector fx; + for (double a : xh) + fx.push_back(f(a, 0.4)); + auto f1 = CreateDeviceRank1View(fx); + auto f2 = CreateDeviceRank1View(sample_2d(xh, yh, f)); + std::vector bcx(yh.size()), bcy(xh.size()); + for (size_t j = 0; j < yh.size(); ++j) + bcx[j] = 0.3 + j; + for (size_t i = 0; i < xh.size(); ++i) + bcy[i] = -0.2 * i; + auto bx = CreateDeviceRank1View(bcx); + auto by = CreateDeviceRank1View(bcy); + + std::vector xq, yq; + for (int i = 0; i < 25; ++i) { + xq.push_back(pi2 * std::fmod(0.137 * i + 0.01, 1.0)); + yq.push_back(pi2 * std::fmod(0.291 * i + 0.02, 1.0)); + } + + ExplicitCubicSplineInterpolator e1( + MakeRank1View(x), MakeRank1View(f1), lo, 0.7, hi, -0.4); + CompactCubicSplineInterpolator c1( + MakeRank1View(x), MakeRank1View(f1), lo, 0.7, hi, -0.4); + auto oe1 = eval_1d(e1, {1, 1, 1}, xq, 3); + auto oc1 = eval_1d(c1, {1, 1, 1}, xq, 3); + for (size_t i = 0; i < oe1.size(); ++i) + CHECK(oc1[i] == Catch::Approx(oe1[i]).margin(1e-10)); + + ExplicitBiCubicSplineInterpolator e2( + MakeRank1View(x), MakeRank1View(y), MakeRank1View(f2), lo, + MakeRank1View(bx), hi, MakeRank1View(bx), lo, MakeRank1View(by), hi, + MakeRank1View(by)); + CompactBiCubicSplineInterpolator c2( + MakeRank1View(x), MakeRank1View(y), MakeRank1View(f2), lo, + MakeRank1View(bx), hi, MakeRank1View(bx), lo, MakeRank1View(by), hi, + MakeRank1View(by)); + auto oe2 = eval_2d(e2, {1, 1, 1, 1, 1, 1}, xq, yq, 6); + auto oc2 = eval_2d(c2, {1, 1, 1, 1, 1, 1}, xq, yq, 6); + for (size_t i = 0; i < oe2.size(); ++i) + CHECK(oc2[i] == Catch::Approx(oe2[i]).margin(1e-10)); +} + +namespace +{ + +/// Divided difference f[x_i, .., x_{i+k}] +double divided_difference(const std::vector& x, + const std::vector& f, int i, int k) +{ + if (k == 0) + return f[i]; + return (divided_difference(x, f, i + 1, k - 1) - + divided_difference(x, f, i, k - 1)) / + (x[i + k] - x[i]); +} + +/// The conditions the solver applies on n points: two points take the first +/// divided difference, three points cannot use the third one and take the +/// parabola for not-a-knot at both ends +std::pair effective_conditions(int n, int lo, int hi) +{ + if (n == 2) { + lo = (lo <= 0 || lo > 5) ? 5 : lo; + hi = (hi <= 0 || hi > 5) ? 5 : hi; + } + if (n == 3) { + lo = lo == 7 ? 6 : lo; + hi = hi == 7 ? 6 : hi; + if (lo == 0 && hi == 0) + hi = 7; + } + return {lo, hi}; +} + +/// End data that a product of the test cubics satisfies for condition bc +double end_data(BoundaryCondition bc, double (*p)(double, int), double x) +{ + switch (bc) { + case BoundaryCondition::FIRST_DERIVATIVE_MATCH: return p(x, 1); + case BoundaryCondition::SECOND_DERIVATIVE_MATCH: return p(x, 2); + default: return 0; + } +} + +} // namespace + +TEMPLATE_TEST_CASE("every end condition holds on grids of 2 to 8 points", + "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + const double lo_data = 0.7, hi_data = -0.4; + for (int n = 2; n <= 8; ++n) { + for (int lo = -1; lo <= 7; ++lo) { + for (int hi = lo == -1 ? -1 : 0; hi <= (lo == -1 ? -1 : 7); ++hi) { + INFO("n " << n << " lo " << lo << " hi " << hi); + auto xh = linspace(0.5, 2.0, n); + const double h = xh[1] - xh[0]; + std::vector fh; + for (double x : xh) + fh.push_back(std::sin(1.3 * x) + 0.2 * x * x); + if (lo == -1) + fh.back() = fh.front(); + auto x = CreateDeviceRank1View(xh); + auto f = CreateDeviceRank1View(fh); + TestType s(MakeRank1View(x), MakeRank1View(f), + static_cast(lo), lo_data, + static_cast(hi), hi_data); + + // s', s'', s''' at both ends and in the two cells next to each end + auto mid = [&](int cell) { return xh[cell] + h / 2; }; + const std::vector pts = {xh[0], + mid(0), + mid(std::min(1, n - 2)), + mid(std::max(n - 3, 0)), + mid(n - 2), + xh[n - 1]}; + auto d12 = eval_1d(s, {0, 1, 1}, pts, 2); + auto d3 = eval_1d(s, {3, 0, 0}, pts, 1); + auto at = [&](int point, int deriv) { + return deriv == 3 ? d3[point] : d12[2 * point + deriv - 1]; + }; + + auto check_end = [&](int side, int bc) { + const int end = side == 0 ? 0 : 5; + const double d1 = at(end, 1), d2 = at(end, 2); + const double d3 = at(side == 0 ? 1 : 4, 3); + const double d3_next = at(side == 0 ? 2 : 3, 3); + const double data = side == 0 ? lo_data : hi_data; + auto dd = [&](int k) { + return divided_difference(xh, fh, side == 0 ? 0 : n - 1 - k, k); + }; + auto approx = [](double v) { + return Catch::Approx(v).margin(1e-9).epsilon(1e-9); + }; + switch (bc) { + case 0: CHECK(d3 == approx(d3_next)); break; + case 1: CHECK(d1 == approx(data)); break; + case 2: CHECK(d2 == approx(data)); break; + case 3: CHECK(d1 == approx(0)); break; + case 4: CHECK(d2 == approx(0)); break; + case 5: CHECK(d1 == approx(dd(1))); break; + case 6: CHECK(d2 == approx(2 * dd(2))); break; + case 7: CHECK(d3 == approx(n < 4 ? 0 : 6 * dd(3))); break; + default: FAIL("unexpected end condition " << bc); + } + }; + if (lo == -1) { + CHECK(at(0, 1) == Catch::Approx(at(5, 1)).margin(1e-9)); + CHECK(at(0, 2) == Catch::Approx(at(5, 2)).margin(1e-9)); + } else { + const auto [elo, ehi] = effective_conditions(n, lo, hi); + check_end(0, elo); + check_end(1, ehi); + } + } + } + } +} + +TEMPLATE_TEST_CASE("1D splines reproduce cubics for every exact end condition", + "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + using BC = BoundaryCondition; + const std::vector conditions = { + BC::NOT_A_KNOT, BC::FIRST_DERIVATIVE_MATCH, BC::SECOND_DERIVATIVE_MATCH, + BC::THIRD_DIV_DIFF_MATCH}; + for (int n = 4; n <= 8; ++n) { + for (BC lo : conditions) { + for (BC hi : conditions) { + INFO("n " << n << " lo " << static_cast(lo) << " hi " + << static_cast(hi)); + auto xh = linspace(-1, 2, n); + std::vector fh; + for (double x : xh) + fh.push_back(px(x, 0)); + auto x = CreateDeviceRank1View(xh); + auto f = CreateDeviceRank1View(fh); + TestType s(MakeRank1View(x), MakeRank1View(f), lo, + end_data(lo, px, xh.front()), hi, + end_data(hi, px, xh.back())); + const std::vector xq = {-1.0, -0.2, 0.9, 1.6, 2.0}; + auto out = eval_1d(s, {1, 1, 1}, xq, 3); + auto d3 = eval_1d(s, {3, 0, 0}, xq, 1); + for (size_t q = 0; q < xq.size(); ++q) { + for (int j = 0; j < 3; ++j) + CHECK(out[3 * q + j] == Catch::Approx(px(xq[q], j)).margin(1e-10)); + CHECK(d3[q] == Catch::Approx(px(xq[q], 3)).margin(1e-9)); + } + } + } + } +} + +TEMPLATE_TEST_CASE( + "2D splines reproduce bicubics for every exact end condition", "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + using BC = BoundaryCondition; + const std::vector conditions = { + BC::NOT_A_KNOT, BC::FIRST_DERIVATIVE_MATCH, BC::SECOND_DERIVATIVE_MATCH, + BC::THIRD_DIV_DIFF_MATCH}; + auto xh = linspace(0, 2, 5); + auto yh = linspace(-1, 1, 6); + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + auto v = CreateDeviceRank1View( + sample_2d(xh, yh, [](double a, double b) { return px(a, 0) * py(b, 0); })); + const std::vector xq = {0.0, 0.7, 1.3, 2.0, 0.0, 2.0}; + const std::vector yq = {-1.0, 0.1, -0.45, 1.0, 1.0, -1.0}; + const std::vector> orders = {{0, 0}, {1, 0}, {0, 1}, + {2, 0}, {0, 2}, {1, 1}}; + for (BC lo : conditions) { + for (BC hi : conditions) { + INFO("lo " << static_cast(lo) << " hi " << static_cast(hi)); + // x faces carry d^k f / dx^k as a function of y, y faces as one of x + std::vector bx_lo, bx_hi, by_lo, by_hi; + for (double b : yh) { + bx_lo.push_back(end_data(lo, px, xh.front()) * py(b, 0)); + bx_hi.push_back(end_data(hi, px, xh.back()) * py(b, 0)); + } + for (double a : xh) { + by_lo.push_back(px(a, 0) * end_data(lo, py, yh.front())); + by_hi.push_back(px(a, 0) * end_data(hi, py, yh.back())); + } + auto bxl = CreateDeviceRank1View(bx_lo); + auto bxh = CreateDeviceRank1View(bx_hi); + auto byl = CreateDeviceRank1View(by_lo); + auto byh = CreateDeviceRank1View(by_hi); + TestType s(MakeRank1View(x), MakeRank1View(y), MakeRank1View(v), lo, + MakeRank1View(bxl), hi, MakeRank1View(bxh), lo, + MakeRank1View(byl), hi, MakeRank1View(byh)); + auto out = eval_2d(s, {1, 1, 1, 1, 1, 1}, xq, yq, 6); + for (size_t q = 0; q < xq.size(); ++q) { + for (size_t j = 0; j < orders.size(); ++j) { + INFO("point " << q << " column " << j); + const double expected = + px(xq[q], orders[j][0]) * py(yq[q], orders[j][1]); + CHECK(out[6 * q + j] == Catch::Approx(expected).margin(1e-9)); + } + } + } + } +} + +TEMPLATE_TEST_CASE("periodic 1D splines wrap queries outside the period", + "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + const double pi2 = 2 * M_PI; + auto xh = linspace(0, pi2, 16); + std::vector fh; + for (double a : xh) + fh.push_back(std::sin(a) + 0.5 * std::cos(2 * a)); + fh.back() = fh.front(); + auto x = CreateDeviceRank1View(xh); + auto f = CreateDeviceRank1View(fh); + const auto per = BoundaryCondition::PERIODIC; + TestType s(MakeRank1View(x), MakeRank1View(f), per, 0, per, 0); + const std::vector outside = {-0.1, pi2 + 0.1, -pi2 + 0.3, + 2 * pi2 + 1.0}; + const std::vector inside = {pi2 - 0.1, 0.1, 0.3, 1.0}; + auto o = eval_1d(s, {1, 1, 1}, outside, 3, -999.0); + auto i = eval_1d(s, {1, 1, 1}, inside, 3); + for (size_t k = 0; k < o.size(); ++k) + CHECK(o[k] == Catch::Approx(i[k]).margin(1e-10)); +} + +TEMPLATE_TEST_CASE("periodic 2D splines wrap queries outside the period", + "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + const double pi2 = 2 * M_PI; + auto xh = linspace(0, pi2, 12); + auto yh = linspace(0, pi2, 10); + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + // sin and cos of the grid ends agree to round-off, so the samples are + // periodic in both directions + auto v = CreateDeviceRank1View(sample_2d( + xh, yh, [](double a, double b) { return std::sin(a) * std::cos(b) + 1; })); + auto none = CreateDeviceRank1View({}); + const auto per = BoundaryCondition::PERIODIC; + TestType s(MakeRank1View(x), MakeRank1View(y), MakeRank1View(v), per, + MakeRank1View(none), per, MakeRank1View(none), per, + MakeRank1View(none), per, MakeRank1View(none)); + const std::vector xo = {-0.1, pi2 + 0.2, 1.0, -0.4}; + const std::vector yo = {1.0, -0.3, pi2 + 0.5, pi2 + 0.1}; + const std::vector xi = {pi2 - 0.1, 0.2, 1.0, pi2 - 0.4}; + const std::vector yi = {1.0, pi2 - 0.3, 0.5, 0.1}; + auto o = eval_2d(s, {1, 1, 1, 1, 1, 1}, xo, yo, 6, -999.0); + auto i = eval_2d(s, {1, 1, 1, 1, 1, 1}, xi, yi, 6); + for (size_t k = 0; k < o.size(); ++k) + CHECK(o[k] == Catch::Approx(i[k]).margin(1e-10)); +} + +TEMPLATE_TEST_CASE("short-grid splines agree with the two-point spline", + "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + // a two-point spline with derivative ends is a cubic; splines on finer + // grids sampled from it, with the same ends, must be that cubic (PSPLINE + // smalltest) + using BC = BoundaryCondition; + const auto d1 = BC::FIRST_DERIVATIVE_MATCH; + const auto d2 = BC::SECOND_DERIVATIVE_MATCH; + auto [lo, lo_data, hi, hi_data] = GENERATE_COPY( + std::make_tuple(d1, 1.0, d1, 2.0), std::make_tuple(d1, 1.0, d2, -3.0), + std::make_tuple(d2, 0.5, d1, 2.0), std::make_tuple(d2, 0.5, d2, -3.0)); + INFO("lo " << static_cast(lo) << " hi " << static_cast(hi)); + auto x2 = CreateDeviceRank1View({0.0, 1.0}); + auto f2 = CreateDeviceRank1View({0.25, 0.75}); + TestType s2(MakeRank1View(x2), MakeRank1View(f2), lo, lo_data, hi, hi_data); + const std::vector xq = {0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0}; + auto expected = eval_1d(s2, {1, 1, 1}, xq, 3); + for (int n : {3, 4, 5}) { + INFO("n " << n); + auto xh = linspace(0, 1, n); + auto samples = eval_1d(s2, {1, 0, 0}, xh, 1); + auto x = CreateDeviceRank1View(xh); + auto f = CreateDeviceRank1View(samples); + TestType s(MakeRank1View(x), MakeRank1View(f), lo, lo_data, hi, hi_data); + auto out = eval_1d(s, {1, 1, 1}, xq, 3); + for (size_t k = 0; k < out.size(); ++k) + CHECK(out[k] == Catch::Approx(expected[k]).margin(1e-10)); + } +} + +namespace +{ + +/// 2 + sin(y) sampled on y, with the last sample set to the first so the +/// data is exactly periodic +std::vector periodic_samples(const std::vector& y) +{ + std::vector g; + for (double b : y) + g.push_back(2 + std::sin(b)); + g.back() = g.front(); + return g; +} + +} // namespace + +TEMPLATE_TEST_CASE("2D splines take periodic ends on one axis only", "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + // f = px(x) (2 + sin y): non-periodic in x, periodic in y (PSPLINE + // bpspline and bpsplinb) + using BC = BoundaryCondition; + const double pi2 = 2 * M_PI; + auto xh = linspace(0, 2, 6); + auto yh = linspace(0, pi2, 12); + const auto g = periodic_samples(yh); + std::vector vh; + for (double a : xh) + for (double gj : g) + vh.push_back(px(a, 0) * gj); + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + auto v = CreateDeviceRank1View(vh); + auto none = CreateDeviceRank1View({}); + const auto per = BC::PERIODIC; + const BC xbc = GENERATE(BC::NOT_A_KNOT, BC::FIRST_DERIVATIVE_MATCH); + INFO("x condition " << static_cast(xbc)); + std::vector bx_lo, bx_hi; + for (double gj : g) { + bx_lo.push_back(px(xh.front(), 1) * gj); + bx_hi.push_back(px(xh.back(), 1) * gj); + } + auto bxl = CreateDeviceRank1View(bx_lo); + auto bxh = CreateDeviceRank1View(bx_hi); + TestType s(MakeRank1View(x), MakeRank1View(y), MakeRank1View(v), xbc, + MakeRank1View(bxl), xbc, MakeRank1View(bxh), per, + MakeRank1View(none), per, MakeRank1View(none)); + + // along a y node the spline is the x spline of a cubic, so it is exact + std::vector xq, yq; + for (double a : {0.0, 0.3, 1.1, 2.0}) + for (int j : {0, 3, 7, 11}) { + xq.push_back(a); + yq.push_back(yh[j]); + } + auto out = eval_2d(s, {1, 1, 0, 0, 0, 0}, xq, yq, 2); + for (size_t q = 0; q < xq.size(); ++q) { + const double gj = g[std::lround(yq[q] / (yh[1] - yh[0])) % 11]; + CHECK(out[2 * q] == Catch::Approx(px(xq[q], 0) * gj).margin(1e-9)); + CHECK(out[2 * q + 1] == Catch::Approx(px(xq[q], 1) * gj).margin(1e-9)); + } + + // y wraps; x does not + const std::vector xi = {0.4, 1.7, 0.9}; + const std::vector yi = {0.2, 5.9, 3.0}; + const std::vector yo = {0.2 + pi2, 5.9 - pi2, 3.0 - 2 * pi2}; + auto inside = eval_2d(s, {1, 1, 1, 1, 1, 1}, xi, yi, 6); + auto wrapped = eval_2d(s, {1, 1, 1, 1, 1, 1}, xi, yo, 6, -999.0); + for (size_t k = 0; k < inside.size(); ++k) + CHECK(wrapped[k] == Catch::Approx(inside[k]).margin(1e-10)); + auto outside = + eval_2d(s, {1, 0, 0, 0, 0, 0}, {-0.1, 2.1}, {1.0, 1.0}, 1, -999.0); + for (double val : outside) + CHECK(val == -999.0); +} + +namespace +{ + +/// log2 of the ratio of successive errors +std::vector observed_orders(const std::vector& err) +{ + std::vector order; + for (size_t i = 1; i < err.size(); ++i) + order.push_back(std::log2(err[i - 1] / err[i])); + return order; +} + +} // namespace + +TEMPLATE_TEST_CASE("1D spline errors converge at fourth order", "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + // f = exp(x) on [0, 1]; the value, f' and f'' errors fall as h^4, h^3 and + // h^2 for exact first derivative and not-a-knot ends + using BC = BoundaryCondition; + const BC bc = GENERATE(BC::FIRST_DERIVATIVE_MATCH, BC::NOT_A_KNOT); + INFO("ends " << static_cast(bc)); + const auto xq = linspace(0, 1, 101); + std::vector err[3]; + for (int n : {10, 20, 40}) { + auto xh = linspace(0, 1, n); + std::vector fh; + for (double a : xh) + fh.push_back(std::exp(a)); + auto x = CreateDeviceRank1View(xh); + auto f = CreateDeviceRank1View(fh); + TestType s(MakeRank1View(x), MakeRank1View(f), bc, 1.0, bc, std::exp(1.0)); + auto out = eval_1d(s, {1, 1, 1}, xq, 3); + double e[3] = {0, 0, 0}; + for (size_t q = 0; q < xq.size(); ++q) + for (int k = 0; k < 3; ++k) + e[k] = std::max(e[k], std::abs(out[3 * q + k] - std::exp(xq[q]))); + for (int k = 0; k < 3; ++k) + err[k].push_back(e[k]); + } + const double expected[3] = {4, 3, 2}; + for (int k = 0; k < 3; ++k) + for (double order : observed_orders(err[k])) { + INFO("derivative " << k); + CHECK(order > expected[k] - 0.5); + } +} + +TEMPLATE_TEST_CASE("2D spline errors converge at fourth order", "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + // f = exp(2x - 1) (2 + sin y) with exact x derivative faces and periodic + // y, at 10, 20 and 40 points per axis (PSPLINE pspltest2) + const double pi2 = 2 * M_PI; + auto f = [](double a, double b) { + return std::exp(2 * a - 1) * (2 + std::sin(b)); + }; + std::vector xq, yq; + for (double a : linspace(0, 1, 31)) + for (double b : linspace(0, pi2, 31)) { + xq.push_back(a); + yq.push_back(b); + } + std::vector err; + for (int n : {10, 20, 40}) { + auto xh = linspace(0, 1, n); + auto yh = linspace(0, pi2, n); + const auto g = periodic_samples(yh); + std::vector vh, bx_lo, bx_hi; + for (double a : xh) + for (double gj : g) + vh.push_back(std::exp(2 * a - 1) * gj); + for (double gj : g) { + bx_lo.push_back(2 * std::exp(-1.0) * gj); + bx_hi.push_back(2 * std::exp(1.0) * gj); + } + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + auto v = CreateDeviceRank1View(vh); + auto bxl = CreateDeviceRank1View(bx_lo); + auto bxh = CreateDeviceRank1View(bx_hi); + auto none = CreateDeviceRank1View({}); + const auto d1 = BoundaryCondition::FIRST_DERIVATIVE_MATCH; + const auto per = BoundaryCondition::PERIODIC; + TestType s(MakeRank1View(x), MakeRank1View(y), MakeRank1View(v), d1, + MakeRank1View(bxl), d1, MakeRank1View(bxh), per, + MakeRank1View(none), per, MakeRank1View(none)); + auto out = eval_2d(s, {1, 0, 0, 0, 0, 0}, xq, yq, 1); + double e = 0; + for (size_t q = 0; q < xq.size(); ++q) + e = std::max(e, std::abs(out[q] - f(xq[q], yq[q]))); + err.push_back(e); + } + for (double order : observed_orders(err)) + CHECK(order > 3.5); +} + +TEMPLATE_TEST_CASE("1D splines of zero data follow the end conditions", + "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + // zero samples with non-zero end data, and periodic splines of constant + // data (PSPLINE smalltest) + using BC = BoundaryCondition; + for (int n : {2, 3, 5}) { + auto xh = linspace(0, 1, n); + auto x = CreateDeviceRank1View(xh); + for (auto [bc, deriv] : {std::pair{BC::FIRST_DERIVATIVE_MATCH, 1}, + std::pair{BC::SECOND_DERIVATIVE_MATCH, 2}}) { + INFO("n " << n << " ends " << static_cast(bc)); + auto f = CreateDeviceRank1View(std::vector(n, 0.0)); + TestType s(MakeRank1View(x), MakeRank1View(f), bc, 1.0, bc, 2.0); + auto nodes = eval_1d(s, {1, 0, 0}, xh, 1); + for (double val : nodes) + CHECK(val == Catch::Approx(0.0).margin(1e-12)); + std::vector sel = {0, 0, 0}; + sel[deriv] = 1; + auto ends = eval_1d(s, sel, {0.0, 1.0}, 1); + CHECK(ends[0] == Catch::Approx(1.0)); + CHECK(ends[1] == Catch::Approx(2.0)); + } + INFO("n " << n << " periodic"); + auto c = CreateDeviceRank1View(std::vector(n, 0.7)); + const auto per = BC::PERIODIC; + TestType s(MakeRank1View(x), MakeRank1View(c), per, 0, per, 0); + auto out = eval_1d(s, {1, 1, 1}, {0.0, 0.3, 0.8, 1.0}, 3); + for (size_t q = 0; q < 4; ++q) { + CHECK(out[3 * q] == Catch::Approx(0.7)); + CHECK(out[3 * q + 1] == Catch::Approx(0.0).margin(1e-12)); + CHECK(out[3 * q + 2] == Catch::Approx(0.0).margin(1e-12)); + } + } +} + +TEMPLATE_TEST_CASE("2D splines of zero data follow the face data", "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + // all-zero values: only the x face derivative data drives the spline + auto xh = linspace(0, 1, 5); + auto yh = linspace(-1, 1, 6); + std::vector lo_h, hi_h; + for (double b : yh) { + lo_h.push_back(1 + b); + hi_h.push_back(b * b); + } + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + auto v = + CreateDeviceRank1View(std::vector(xh.size() * yh.size(), 0.0)); + auto lo = CreateDeviceRank1View(lo_h); + auto hi = CreateDeviceRank1View(hi_h); + auto none = CreateDeviceRank1View({}); + const auto nak = BoundaryCondition::NOT_A_KNOT; + const auto d1 = BoundaryCondition::FIRST_DERIVATIVE_MATCH; + TestType s(MakeRank1View(x), MakeRank1View(y), MakeRank1View(v), d1, + MakeRank1View(lo), d1, MakeRank1View(hi), nak, MakeRank1View(none), + nak, MakeRank1View(none)); + std::vector xn, yn, xf, yf, expected; + for (double a : xh) + for (double b : yh) { + xn.push_back(a); + yn.push_back(b); + } + for (size_t j = 0; j < yh.size(); ++j) { + xf.insert(xf.end(), {0.0, 1.0}); + yf.insert(yf.end(), {yh[j], yh[j]}); + expected.insert(expected.end(), {lo_h[j], hi_h[j]}); + } + auto nodes = eval_2d(s, {1, 0, 0, 0, 0, 0}, xn, yn, 1); + for (double val : nodes) + CHECK(val == Catch::Approx(0.0).margin(1e-12)); + auto faces = eval_2d(s, {0, 1, 0, 0, 0, 0}, xf, yf, 1); + for (size_t k = 0; k < faces.size(); ++k) + CHECK(faces[k] == Catch::Approx(expected[k]).margin(1e-10)); +} + +TEMPLATE_TEST_CASE("1D queries just outside the grid are clamped", "[spline]", + (ExplicitCubicSplineInterpolator), + (CompactCubicSplineInterpolator)) +{ + // the default lookup tolerance is 4e-7 relative to max(|x0|, |xn|) = 2 + auto xh = linspace(0, 2, 6); + std::vector fh; + for (double a : xh) + fh.push_back(px(a, 0)); + auto x = CreateDeviceRank1View(xh); + auto f = CreateDeviceRank1View(fh); + const auto nak = BoundaryCondition::NOT_A_KNOT; + TestType s(MakeRank1View(x), MakeRank1View(f), nak, 0, nak, 0); + auto clamped = eval_1d(s, {1, 1, 1}, {-4e-7, 2 + 4e-7}, 3, -999.0); + auto ends = eval_1d(s, {1, 1, 1}, {0.0, 2.0}, 3); + for (size_t k = 0; k < ends.size(); ++k) + CHECK(clamped[k] == ends[k]); + auto outside = eval_1d(s, {1, 1, 1}, {-2e-6, 2 + 2e-6}, 3, -999.0); + for (double val : outside) + CHECK(val == -999.0); +} + +TEMPLATE_TEST_CASE("2D queries just outside the grid are clamped", "[spline]", + (ExplicitBiCubicSplineInterpolator), + (CompactBiCubicSplineInterpolator)) +{ + auto xh = linspace(0, 2, 6); + auto yh = linspace(-1, 1, 5); + auto x = CreateDeviceRank1View(xh); + auto y = CreateDeviceRank1View(yh); + auto v = CreateDeviceRank1View( + sample_2d(xh, yh, [](double a, double b) { return px(a, 0) * py(b, 0); })); + auto none = CreateDeviceRank1View({}); + const auto nak = BoundaryCondition::NOT_A_KNOT; + TestType s(MakeRank1View(x), MakeRank1View(y), MakeRank1View(v), nak, + MakeRank1View(none), nak, MakeRank1View(none), nak, + MakeRank1View(none), nak, MakeRank1View(none)); + const std::vector all = {1, 1, 1, 1, 1, 1}; + auto clamped = eval_2d(s, all, {2 + 4e-7, 0.5, -4e-7}, + {0.3, 1 + 4e-7, -1 - 4e-7}, 6, -999.0); + auto ends = eval_2d(s, all, {2.0, 0.5, 0.0}, {0.3, 1.0, -1.0}, 6); + for (size_t k = 0; k < ends.size(); ++k) + CHECK(clamped[k] == ends[k]); + auto outside = eval_2d(s, all, {2 + 2e-6, 0.5}, {0.3, -1 - 2e-6}, 6, -999.0); + for (double val : outside) + CHECK(val == -999.0); +} diff --git a/test/test_spr_meshfields.cpp b/test/test_spr_meshfields.cpp index 42a6acf02..7ad4470a9 100644 --- a/test/test_spr_meshfields.cpp +++ b/test/test_spr_meshfields.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -127,22 +128,7 @@ TEST_CASE("meshfields_spr_test") const auto& ntargets = mesh.nverts(); - Write source_coordinates( - dim * nfaces, 0, "stores coordinates of cell centroid of each tri element"); - - const auto& faces2nodes = mesh.ask_down(FACE, VERT).ab2b; - - Kokkos::parallel_for( - "calculate the centroid in each tri element", nfaces, - OMEGA_H_LAMBDA(const LO id) { - const auto current_el_verts = gather_verts<3>(faces2nodes, id); - const Omega_h::Few, 3> current_el_vert_coords = - gather_vectors<3, 2>(target_coordinates, current_el_verts); - auto centroid = 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, FACE); pcms::Points source_points; diff --git a/test/test_svd_serial.cpp b/test/test_svd_serial.cpp index bdc23290b..1774e29bb 100644 --- a/test/test_svd_serial.cpp +++ b/test/test_svd_serial.cpp @@ -38,7 +38,7 @@ TEST_CASE("test_serial_svd") Kokkos::deep_copy(A_data, host_A_data); - Kokkos::View rhs_data("Device rhs data", row, column); + Kokkos::View rhs_data("Device rhs data", row); auto host_rhs_data = Kokkos::create_mirror_view(rhs_data); host_rhs_data(0) = 1.28571; @@ -54,7 +54,7 @@ TEST_CASE("test_serial_svd") { Kokkos::View result("result", row, row); - Kokkos::View transpose_expected("result", row, row); + Kokkos::View transpose_expected("transpose_expected", row, row); Kokkos::deep_copy(result, 0.0); Kokkos::deep_copy(transpose_expected, 0.0); team_policy tp(1, Kokkos::AUTO); diff --git a/test/test_tagged_outputs.cpp b/test/test_tagged_outputs.cpp new file mode 100644 index 000000000..69dac2a7d --- /dev/null +++ b/test/test_tagged_outputs.cpp @@ -0,0 +1,142 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "field_test_utils.h" +#include +#include +#include "pcms/field/coordinate_systems/cartesian.hpp" +#include "pcms/field/coordinate_systems/cylindrical.hpp" + +using Catch::Matchers::ContainsSubstring; +using pcms::DeviceMemorySpace; +using pcms::Field; +using pcms::LagrangeFunctionSpace; +using pcms::Real; +using pcms::ValueView; +namespace values = pcms::values; +using Catch::Matchers::WithinAbs; +using pcms::ComponentScaling; +using pcms::HostMemorySpace; +using pcms::ValueBasis; +using pcms::Variance; +using pcms::VarianceSignature; + +namespace +{ + +// A 3D simplex mesh whose coordinates are interpreted as (r, theta, z). +Omega_h::Mesh BuildCylindricalMesh(Omega_h::Library& lib, int divisions) +{ + return Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 2.0, 1.6, 1.1, + divisions, divisions, divisions, false); +} + +std::shared_ptr BuildCylindricalSpace( + Omega_h::Mesh& mesh, int num_components) +{ + return LagrangeFunctionSpace::FromMesh( + mesh, 1, num_components, pcms::csys::CylindricalRThetaZ::Create(), "global", + LagrangeFunctionSpace::Backend::OmegaH); +} + +int NumDOFHolders(const LagrangeFunctionSpace& space) +{ + return static_cast( + space.GetLayout()->GetDOFHolderCoordinates().GetValues().extent(0)); +} + +} // namespace + +TEST_CASE("PointEvaluator::Evaluate gates on the source field's stored basis") +{ + auto lib = Omega_h::Library{}; + auto mesh = BuildCylindricalMesh(lib, 6); + auto space = BuildCylindricalSpace(mesh, 3); + // Borrowed-basis storage: Cartesian components on a cylindrical-system space. + auto cartesian = space->CreateFunction( + "b", values::Vector, pcms::csys::Cartesian::Create(3)); + auto cylindrical = space->CreateFunction("b_native", values::Vector, + ComponentScaling::Physical); + pcms::test::SetFieldComponents(cartesian, [](Real, Real, Real, Real* out) { + out[0] = 1.0; + out[1] = 0.0; + out[2] = 0.0; + }); + + const std::vector pts = {0.7, 0.3, 0.4, 1.0, 0.9, 0.9}; + auto query = pcms::test::CreateDeviceCoordinateView( + pts, pcms::csys::CylindricalRThetaZ::Create(), 3); + auto evaluator = space->CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(query.coordinate_view)); + const int n = static_cast(pts.size()) / 3; + Kokkos::View out("out", n, 3); + + SECTION("a matching tag is accepted") + { + REQUIRE_NOTHROW(evaluator->Evaluate( + cartesian, + ValueView(cartesian.GetData().GetValueBasis(), + pcms::MakeRank2View(out)))); + } + + SECTION("a mismatched basis claim is rejected") + { + REQUIRE_THROWS_WITH( + evaluator->Evaluate(cartesian, ValueView( + cylindrical.GetData().GetValueBasis(), + pcms::MakeRank2View(out))), + ContainsSubstring("does not match the basis this call writes")); + } +} + +TEST_CASE("tagged writes gate on the field's declaration") +{ + auto lib = Omega_h::Library{}; + auto mesh = BuildCylindricalMesh(lib, 6); + auto space = BuildCylindricalSpace(mesh, 3); + auto b = space->CreateFunction("b", values::Vector, + ComponentScaling::Physical); + const int n = static_cast( + space->GetLayout()->GetDOFHolderCoordinates().GetValues().extent(0)); + std::vector data(static_cast(n) * 3, 1.0); + const pcms::Rank2View raw(data.data(), n, 3); + + SECTION("a matching tag is accepted") + { + b.SetDOFHolderDataHost(ValueView( + ValueBasis{pcms::csys::CylindricalRThetaZ::Create(), + ComponentScaling::Physical, + VarianceSignature{Variance::Contravariant}}, + raw)); + REQUIRE_THAT(b.GetDOFHolderDataHost().GetValues()(0, 0), + WithinAbs(1.0, 1e-14)); // tagged getter forwards indexing + } + + SECTION("a mismatched basis claim is rejected") + { + REQUIRE_THROWS_WITH( + b.SetDOFHolderDataHost(ValueView( + ValueBasis{pcms::csys::Cartesian::Create(3), ComponentScaling::Physical, + VarianceSignature{Variance::Contravariant}}, + raw)), + ContainsSubstring("does not match this field's declaration")); + } + + SECTION("a mismatched value type is rejected") + { + REQUIRE_THROWS_WITH( + b.SetDOFHolderDataHost( + ValueView(ValueBasis{}, raw)), + ContainsSubstring("does not match this field's declaration")); + } + + SECTION("the unchecked path inherits the declaration unverified") + { + REQUIRE_NOTHROW(b.SetDOFHolderDataUncheckedHost(raw)); + } +} diff --git a/test/test_transformed_source.cpp b/test/test_transformed_source.cpp new file mode 100644 index 000000000..21433427d --- /dev/null +++ b/test/test_transformed_source.cpp @@ -0,0 +1,486 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "field_test_utils.h" +#include +#include +#include + +using Catch::Matchers::ContainsSubstring; +using Catch::Matchers::WithinAbs; +using pcms::ComponentScaling; +using pcms::DeviceMemorySpace; +using pcms::EvaluationRequest; +using pcms::GO; +using pcms::HostMemorySpace; +using pcms::LagrangeFunctionSpace; +using pcms::OutOfBoundsMode; +using pcms::OutOfBoundsPolicy; +using pcms::Real; +using pcms::SameValueBasis; +using pcms::TransformedSource; +using pcms::ValueBasis; +using pcms::ValueView; +namespace values = pcms::values; + +namespace +{ + +constexpr double kTol = 1e-10; + +std::shared_ptr BuildCylindricalSource( + Omega_h::Library& lib, Omega_h::Mesh& mesh, int num_components) +{ + mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, 2.0, 1.6, 1.1, 8, 8, + 6, false); + return LagrangeFunctionSpace::FromMesh( + mesh, 1, num_components, pcms::csys::CylindricalRThetaZ::Create(), "global", + LagrangeFunctionSpace::Backend::OmegaH); +} + +std::shared_ptr BuildCartesianTarget( + Omega_h::Library& lib, Omega_h::Mesh& mesh, int num_components, + double x_extent = 1.0) +{ + mesh = Omega_h::build_box(lib.world(), OMEGA_H_SIMPLEX, x_extent, 1.0, 1.0, 4, + 4, 4, false); + return LagrangeFunctionSpace::FromMesh( + mesh, 1, num_components, pcms::csys::Cartesian::Create(3), "global", + LagrangeFunctionSpace::Backend::OmegaH); +} + +ValueBasis CylindricalVector() +{ + return ValueBasis{pcms::csys::CylindricalRThetaZ::Create(), + ComponentScaling::Physical, values::Vector}; +} + +ValueBasis CartesianVector() +{ + return ValueBasis{pcms::csys::Cartesian::Create(3), + ComponentScaling::Physical, values::Vector}; +} + +Kokkos::View EvaluateToHost( + const pcms::PointEvaluator& evaluator, const pcms::Field& field, + const ValueBasis& tag, int n, int width) +{ + Kokkos::View out("out", n, width); + evaluator.Evaluate( + field, ValueView(tag, pcms::MakeRank2View(out))); + Kokkos::View host("host", n, width); + pcms::DeepCopyMismatchLayouts(host, out); + return host; +} + +} // namespace + +TEST_CASE("TransformedSource: scalar evaluation maps the query points") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib), tgt_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 1); + auto tgt = BuildCartesianTarget(lib, tgt_mesh, 1); + auto f = src->CreateFunction("f"); + pcms::test::SetField( + f, OMEGA_H_LAMBDA(Real r, Real theta, Real z) { + return 2.0 * r + 3.0 * theta - z; + }); + + TransformedSource src_as_cartesian( + src, std::make_shared()); + REQUIRE(pcms::SameCoordinateSystem(src_as_cartesian.GetCoordinateSystem(), + tgt->GetCoordinateSystem())); + + const auto pts = tgt->GetLayout()->GetDOFHolderCoordinates(); + const int n = static_cast(pts.GetValues().extent(0)); + auto evaluator = src_as_cartesian.CreatePointEvaluator( + EvaluationRequest::FromCoordinates(pts)); + REQUIRE(SameValueBasis(evaluator->OutputBasis(ValueBasis{}), ValueBasis{})); + + const auto coords = pcms::test::CopyCoordinatesToHost(pts.GetValues()); + const auto result = EvaluateToHost(*evaluator, f, ValueBasis{}, n, 1); + for (int i = 0; i < n; ++i) { + const Real x = coords(i, 0), y = coords(i, 1), z = coords(i, 2); + CAPTURE(i); + REQUIRE_THAT(result(i, 0), WithinAbs(2.0 * std::sqrt(x * x + y * y) + + 3.0 * std::atan2(y, x) - z, + kTol)); + } +} + +TEST_CASE("TransformedSource: vector evaluation rotates into the target basis") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib), tgt_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 3); + auto tgt = BuildCartesianTarget(lib, tgt_mesh, 3); + auto b = + src->CreateFunction("b", values::Vector, ComponentScaling::Physical); + pcms::test::SetFieldComponents(b, [](Real r, Real, Real z, Real* out) { + out[0] = r; + out[1] = 0.0; + out[2] = z; + }); + + TransformedSource src_as_cartesian( + src, std::make_shared()); + const auto pts = tgt->GetLayout()->GetDOFHolderCoordinates(); + const int n = static_cast(pts.GetValues().extent(0)); + auto evaluator = src_as_cartesian.CreatePointEvaluator( + EvaluationRequest::FromCoordinates(pts)); + REQUIRE(SameValueBasis(evaluator->OutputBasis(CylindricalVector()), + CartesianVector())); + REQUIRE(SameValueBasis(evaluator->OutputBasis(CartesianVector()), + CartesianVector())); + const ValueBasis natural_cylindrical{pcms::csys::CylindricalRThetaZ::Create(), + ComponentScaling::Natural, + values::Vector}; + REQUIRE_THROWS_WITH(evaluator->OutputBasis(natural_cylindrical), + ContainsSubstring("cannot re-express")); + + const auto coords = pcms::test::CopyCoordinatesToHost(pts.GetValues()); + const auto result = EvaluateToHost(*evaluator, b, CartesianVector(), n, 3); + for (int i = 0; i < n; ++i) { + CAPTURE(i); + REQUIRE_THAT(result(i, 0), WithinAbs(coords(i, 0), kTol)); + REQUIRE_THAT(result(i, 1), WithinAbs(coords(i, 1), kTol)); + REQUIRE_THAT(result(i, 2), WithinAbs(coords(i, 2), kTol)); + } + + SECTION("evaluate gates the field's stored basis and the output tag") + { + auto natural = + src->CreateFunction("c", values::Vector, ComponentScaling::Natural); + Kokkos::View out("out", n, 3); + REQUIRE_THROWS_WITH( + evaluator->Evaluate( + natural, ValueView(CartesianVector(), + pcms::MakeRank2View(out))), + ContainsSubstring("cannot re-express")); + REQUIRE_THROWS_WITH( + evaluator->Evaluate(b, ValueView( + CylindricalVector(), pcms::MakeRank2View(out))), + ContainsSubstring("does not match")); + } + + SECTION("evaluate rejects an output with the wrong number of points") + { + REQUIRE(evaluator->NumPoints() == n); + Kokkos::View short_out("short_out", n - 1, 3); + REQUIRE_THROWS_AS(evaluator->Evaluate(b, ValueView( + CartesianVector(), + pcms::MakeRank2View(short_out))), + std::invalid_argument); + } +} + +TEST_CASE("TransformedSource: FILL rows survive the rotation untouched") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib), tgt_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 3); + auto tgt = BuildCartesianTarget(lib, tgt_mesh, 3, 3.0); + auto b = + src->CreateFunction("b", values::Vector, ComponentScaling::Physical); + pcms::test::SetFieldComponents(b, [](Real r, Real, Real z, Real* out) { + out[0] = r; + out[1] = 0.0; + out[2] = z; + }); + + const Real fill = -7.0; + TransformedSource src_as_cartesian( + src, std::make_shared()); + const auto pts = tgt->GetLayout()->GetDOFHolderCoordinates(); + const int n = static_cast(pts.GetValues().extent(0)); + auto evaluator = src_as_cartesian.CreatePointEvaluator( + EvaluationRequest::FromCoordinates( + pts, OutOfBoundsPolicy{OutOfBoundsMode::FILL, fill})); + + const auto coords = pcms::test::CopyCoordinatesToHost(pts.GetValues()); + const auto result = EvaluateToHost(*evaluator, b, CartesianVector(), n, 3); + int num_filled = 0; + for (int i = 0; i < n; ++i) { + const Real x = coords(i, 0), y = coords(i, 1); + CAPTURE(i); + if (std::sqrt(x * x + y * y) > 2.0) { + ++num_filled; + REQUIRE(result(i, 0) == fill); + REQUIRE(result(i, 1) == fill); + REQUIRE(result(i, 2) == fill); + } else { + REQUIRE_THAT(result(i, 0), WithinAbs(x, kTol)); + REQUIRE_THAT(result(i, 1), WithinAbs(y, kTol)); + REQUIRE_THAT(result(i, 2), WithinAbs(coords(i, 2), kTol)); + } + } + REQUIRE(num_filled > 0); + REQUIRE(static_cast(evaluator->FilledPoints().extent(0)) == num_filled); +} + +TEST_CASE("TransformedSource: in-bounds values equal to the fill value are " + "still rotated") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib), tgt_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 3); + auto tgt = BuildCartesianTarget(lib, tgt_mesh, 3, 3.0); + const Real fill = 1.0; + auto b = + src->CreateFunction("b", values::Vector, ComponentScaling::Physical); + pcms::test::SetFieldComponents(b, [fill](Real, Real, Real, Real* out) { + out[0] = fill; + out[1] = fill; + out[2] = fill; + }); + + TransformedSource src_as_cartesian( + src, std::make_shared()); + const auto pts = tgt->GetLayout()->GetDOFHolderCoordinates(); + const int n = static_cast(pts.GetValues().extent(0)); + auto evaluator = src_as_cartesian.CreatePointEvaluator( + EvaluationRequest::FromCoordinates( + pts, OutOfBoundsPolicy{OutOfBoundsMode::FILL, fill})); + + const auto coords = pcms::test::CopyCoordinatesToHost(pts.GetValues()); + const auto result = EvaluateToHost(*evaluator, b, CartesianVector(), n, 3); + int num_rotated = 0; + for (int i = 0; i < n; ++i) { + const Real x = coords(i, 0), y = coords(i, 1); + const Real r = std::sqrt(x * x + y * y); + CAPTURE(i); + if (r > 2.0) { + REQUIRE(result(i, 0) == fill); + REQUIRE(result(i, 1) == fill); + } else if (r > 0.0) { + const Real c = x / r, s = y / r; + REQUIRE_THAT(result(i, 0), WithinAbs(fill * (c - s), kTol)); + REQUIRE_THAT(result(i, 1), WithinAbs(fill * (s + c), kTol)); + ++num_rotated; + } + REQUIRE_THAT(result(i, 2), WithinAbs(fill, kTol)); + } + REQUIRE(num_rotated > 0); +} + +TEST_CASE("TransformedSource: construction errors") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 3); + + REQUIRE_THROWS_WITH(TransformedSource(src, nullptr), + ContainsSubstring("must not be null")); + REQUIRE_THROWS_WITH( + TransformedSource(src, std::make_shared()), + ContainsSubstring("source's coordinate system")); +} + +TEST_CASE("TransformedSource: evaluator creation and evaluation errors") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 3); + auto cart_to_cyl = std::make_shared(); + const std::vector cart_pts = {0.5, 0.5, 0.5, 0.2, 0.9, 0.1}; + auto query = pcms::test::CreateDeviceCoordinateView( + cart_pts, pcms::csys::Cartesian::Create(3), 3); + TransformedSource src_as_cartesian(src, cart_to_cyl); + + SECTION("query points must be in the transformed system") + { + auto cyl_query = pcms::test::CreateDeviceCoordinateView( + cart_pts, pcms::csys::CylindricalRThetaZ::Create(), 3); + REQUIRE_THROWS_WITH( + src_as_cartesian.CreatePointEvaluator( + EvaluationRequest::FromCoordinates(cyl_query.coordinate_view)), + ContainsSubstring("not the factory's coordinate system")); + } + + SECTION("non-Real evaluation surfaces the source backend's own limit") + { + // No in-tree backend evaluates integer fields at points, so a + // TransformedSource simply forwards the inner factory's refusal; the + // T != Real guard in TransformedPointEvaluator::Evaluate is defensive. + REQUIRE_THROWS_WITH( + src_as_cartesian.CreatePointEvaluator( + EvaluationRequest::FromCoordinates(query.coordinate_view)), + ContainsSubstring("only supports double")); + } +} + +TEST_CASE("TransformedSource: one source view serves many fields and targets") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib), tgt_a_mesh(&lib), tgt_b_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 3); + auto tgt_a = BuildCartesianTarget(lib, tgt_a_mesh, 3); + auto tgt_b = BuildCartesianTarget(lib, tgt_b_mesh, 3, 0.8); + auto b = + src->CreateFunction("b", values::Vector, ComponentScaling::Physical); + auto e = + src->CreateFunction("e", values::Vector, ComponentScaling::Physical); + pcms::test::SetFieldComponents(b, [](Real r, Real, Real z, Real* out) { + out[0] = r; + out[1] = 0.0; + out[2] = z; + }); + pcms::test::SetFieldComponents(e, [](Real r, Real, Real z, Real* out) { + out[0] = 2.0 * r; + out[1] = 0.0; + out[2] = -z; + }); + + // Built from the source and the map alone, so it is reused for + // every field on that space and every target space. + TransformedSource src_as_cartesian( + src, std::make_shared()); + pcms::Interpolator to_a(src_as_cartesian, *tgt_a); + pcms::Interpolator to_b(src_as_cartesian, *tgt_b); + + auto b_a = tgt_a->CreateFunction("b", values::Vector); + auto e_a = tgt_a->CreateFunction("e", values::Vector); + auto b_b = tgt_b->CreateFunction("b", values::Vector); + to_a.Apply(b, b_a); + to_a.Apply(e, e_a); + to_b.Apply(b, b_b); + + const auto coords = pcms::test::CopyCoordinatesToHost( + tgt_a->GetLayout()->GetDOFHolderCoordinates().GetValues()); + const auto rb = b_a.GetDOFHolderDataHost().GetValues(); + const auto re = e_a.GetDOFHolderDataHost().GetValues(); + for (int i = 0; i < static_cast(coords.extent(0)); ++i) { + CAPTURE(i); + REQUIRE_THAT(rb(i, 0), WithinAbs(coords(i, 0), kTol)); + REQUIRE_THAT(re(i, 0), WithinAbs(2.0 * coords(i, 0), kTol)); + REQUIRE_THAT(re(i, 2), WithinAbs(-coords(i, 2), kTol)); + } +} + +TEST_CASE("TransformedSource: nesting composes maps and rotations") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 3); + auto b = + src->CreateFunction("b", values::Vector, ComponentScaling::Physical); + pcms::test::SetFieldComponents(b, [](Real r, Real, Real z, Real* out) { + out[0] = r; + out[1] = 0.0; + out[2] = z; + }); + + auto src_as_cartesian = std::make_shared( + src, std::make_shared()); + TransformedSource src_as_cylindrical( + src_as_cartesian, std::make_shared()); + + const std::vector cyl_pts = {0.7, 0.3, 0.4, 1.0, 0.9, + 0.9, 1.5, 1.2, 0.2}; + auto query = pcms::test::CreateDeviceCoordinateView( + cyl_pts, pcms::csys::CylindricalRThetaZ::Create(), 3); + const int n = 3; + auto evaluator = src_as_cylindrical.CreatePointEvaluator( + EvaluationRequest::FromCoordinates(query.coordinate_view)); + const auto result = EvaluateToHost(*evaluator, b, CylindricalVector(), n, 3); + for (int i = 0; i < n; ++i) { + CAPTURE(i); + const auto k = 3 * static_cast(i); + REQUIRE_THAT(result(i, 0), WithinAbs(cyl_pts[k], kTol)); + REQUIRE_THAT(result(i, 1), WithinAbs(0.0, kTol)); + REQUIRE_THAT(result(i, 2), WithinAbs(cyl_pts[k + 2], kTol)); + } +} + +TEST_CASE("Interpolator over a TransformedSource transfers across systems") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib), tgt_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 3); + auto tgt = BuildCartesianTarget(lib, tgt_mesh, 3); + auto b_src = + src->CreateFunction("b", values::Vector, ComponentScaling::Physical); + auto b_tgt = tgt->CreateFunction("b", values::Vector); + pcms::test::SetFieldComponents(b_src, [](Real r, Real, Real z, Real* out) { + out[0] = r; + out[1] = 0.0; + out[2] = z; + }); + + TransformedSource src_as_cartesian( + src, std::make_shared()); + pcms::Interpolator op(src_as_cartesian, *tgt); + op.Apply(b_src, b_tgt); + + const auto coords = pcms::test::CopyCoordinatesToHost( + tgt->GetLayout()->GetDOFHolderCoordinates().GetValues()); + const auto result = b_tgt.GetDOFHolderDataHost().GetValues(); + const int n = static_cast(coords.extent(0)); + for (int i = 0; i < n; ++i) { + CAPTURE(i); + REQUIRE_THAT(result(i, 0), WithinAbs(coords(i, 0), kTol)); + REQUIRE_THAT(result(i, 1), WithinAbs(coords(i, 1), kTol)); + REQUIRE_THAT(result(i, 2), WithinAbs(coords(i, 2), kTol)); + } + + SECTION("the target must declare the basis the evaluator writes") + { + auto wrong = tgt->CreateFunction( + "w", values::Vector, pcms::csys::CylindricalRThetaZ::Create(), + ComponentScaling::Physical); + REQUIRE_THROWS_WITH(op.Apply(b_src, wrong), + ContainsSubstring("target's declared basis")); + } + + SECTION("a plain Interpolator refuses to bridge coordinate systems") + { + REQUIRE_THROWS_WITH(pcms::Interpolator(*src, *tgt), + ContainsSubstring("coordinate system")); + } +} + +TEST_CASE("Interpolator over a TransformedSource passes through values already " + "in the target basis") +{ + auto lib = Omega_h::Library{}; + Omega_h::Mesh src_mesh(&lib), tgt_mesh(&lib); + auto src = BuildCylindricalSource(lib, src_mesh, 3); + auto tgt = BuildCartesianTarget(lib, tgt_mesh, 3); + // Cartesian components on a cylindrical space; linear in (r, theta, z) so + // the interpolation is exact and any stray rotation would show. + auto b_src = src->CreateFunction("b", values::Vector, + pcms::csys::Cartesian::Create(3)); + auto b_tgt = tgt->CreateFunction("b", values::Vector); + pcms::test::SetFieldComponents(b_src, + [](Real r, Real theta, Real z, Real* out) { + out[0] = r; + out[1] = theta; + out[2] = z; + }); + + TransformedSource src_as_cartesian( + src, std::make_shared()); + pcms::Interpolator op(src_as_cartesian, *tgt); + op.Apply(b_src, b_tgt); + + const auto coords = pcms::test::CopyCoordinatesToHost( + tgt->GetLayout()->GetDOFHolderCoordinates().GetValues()); + const auto result = b_tgt.GetDOFHolderDataHost().GetValues(); + for (int i = 0; i < static_cast(coords.extent(0)); ++i) { + const Real x = coords(i, 0), y = coords(i, 1); + CAPTURE(i); + REQUIRE_THAT(result(i, 0), WithinAbs(std::sqrt(x * x + y * y), kTol)); + REQUIRE_THAT(result(i, 1), WithinAbs(std::atan2(y, x), kTol)); + REQUIRE_THAT(result(i, 2), WithinAbs(coords(i, 2), kTol)); + } +} diff --git a/test/test_transient_timestepper.cpp b/test/test_transient_timestepper.cpp new file mode 100644 index 000000000..c39309912 --- /dev/null +++ b/test/test_transient_timestepper.cpp @@ -0,0 +1,21 @@ +#include +#include + +#include "pcms/transient/timestepper.hpp" + +using namespace pcms; + +TEST_CASE("FixedTimestepper always accepts its configured step", "[transient]") +{ + // 0.25 represents a positive fixed window size. + FixedTimestepper timestepper(0.25); + + // The completed step (0.1) and large normalized error (100). + // A fixed controller ignores both. + const auto [accepted, next_step] = timestepper.UpdateStep(0.1, 100.0); + + // The first window must use the user-configured size. + REQUIRE(timestepper.InitialStep() == Catch::Approx(0.25)); + REQUIRE(accepted); + REQUIRE(next_step == Catch::Approx(0.25)); +} diff --git a/test/test_tree_localization.cpp b/test/test_tree_localization.cpp new file mode 100644 index 000000000..0ae6e7faf --- /dev/null +++ b/test/test_tree_localization.cpp @@ -0,0 +1,266 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include + +TEST_CASE("Test 10x10 2D tree point classification") +{ + // Setup for 10x10 tree test cases + auto lib = Omega_h::Library{}; + auto world = lib.world(); + Omega_h::Mesh mesh_1x1 = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 10, 10, 0, false); + REQUIRE(mesh_1x1.dim() == 2); + Omega_h::ExecSpace execs; + + pcms::TreePointSearch tree_search(mesh_1x1); + // End setup for 10x10 tree test cases + + auto check_res = [](auto const& results, int dim) { + auto dims = Kokkos::create_mirror_view(results.dimensionalities); + auto ids = Kokkos::create_mirror_view(results.element_ids); + Kokkos::deep_copy(dims, results.dimensionalities); + Kokkos::deep_copy(ids, results.element_ids); + for (int i = 0; i < dims.size(); i++) { + CHECK(dims(i) == (pcms::TreePointSearch::Dimensionality)dim); + CHECK(ids(i) == i); + // CHECK_THAT(results(i).parametric_coords, + // Catch::Matchers::Contains(Catch::Matchers::WithinAbs(1.0, + // 10e-7))); + } + }; + + SECTION("Vertex intersection") + { + Kokkos::View coords( + "vertex coordinates", mesh_1x1.nverts(), 2); + auto coords_h = Kokkos::create_mirror_view(coords); + auto meshcoords = Omega_h::HostRead(mesh_1x1.coords()); + for (int i = 0; i < mesh_1x1.nverts(); i++) { + coords_h(i, 0) = meshcoords[i * 2]; + coords_h(i, 1) = meshcoords[i * 2 + 1]; + } + Kokkos::deep_copy(execs, coords, coords_h); + pcms::CoordinateView> + vert_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::TreePointSearch::Results results = tree_search.Apply(vert_cv); + check_res(results, 0); + } + SECTION("Edge intersection") + { + Kokkos::View coords( + "edge coordinates", mesh_1x1.nedges(), 2); + auto coords_h = Kokkos::create_mirror_view(coords); + auto edge2vert = + Omega_h::HostRead(mesh_1x1.get_adj(Omega_h::EDGE, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh_1x1.coords()); + for (int i = 0; i < mesh_1x1.nedges(); i++) { + Omega_h::Vector<2> v0{meshcoords[edge2vert[2 * i] * 2], + meshcoords[edge2vert[2 * i] * 2 + 1]}; + Omega_h::Vector<2> v1{meshcoords[edge2vert[2 * i + 1] * 2], + meshcoords[edge2vert[2 * i + 1] * 2 + 1]}; + Omega_h::Vector<2> midpoint = (v0 + v1) / 2.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + } + Kokkos::deep_copy(execs, coords, coords_h); + + pcms::CoordinateView> + edge_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::TreePointSearch::Results results = tree_search.Apply(edge_cv); + check_res(results, 1); + } + SECTION("Face intersection") + { + Kokkos::View coords( + "face coordinates", mesh_1x1.nfaces(), 2); + auto face2vert = + Omega_h::HostRead(mesh_1x1.get_adj(Omega_h::FACE, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh_1x1.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + for (int i = 0; i < mesh_1x1.nfaces(); i++) { + Omega_h::Vector<2> v0{meshcoords[face2vert[3 * i] * 2], + meshcoords[face2vert[3 * i] * 2 + 1]}; + Omega_h::Vector<2> v1{meshcoords[face2vert[3 * i + 1] * 2], + meshcoords[face2vert[3 * i + 1] * 2 + 1]}; + Omega_h::Vector<2> v2{meshcoords[face2vert[3 * i + 2] * 2], + meshcoords[face2vert[3 * i + 2] * 2 + 1]}; + Omega_h::Vector<2> midpoint = (v0 + v1 + v2) / 3.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + } + Kokkos::deep_copy(execs, coords, coords_h); + + pcms::CoordinateView> + face_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::TreePointSearch::Results results = tree_search.Apply(face_cv); + check_res(results, 2); + } +} + +TEST_CASE("Test 5x5x5 3D tree point classification") +{ + // Setup for 5x5x5 tree test cases + auto lib = Omega_h::Library{}; + auto world = lib.world(); + Omega_h::Mesh mesh_1x1 = + Omega_h::build_box(world, OMEGA_H_SIMPLEX, 1, 1, 1, 5, 5, 5, false); + REQUIRE(mesh_1x1.dim() == 3); + Omega_h::ExecSpace execs; + + pcms::TreePointSearch tree_search(mesh_1x1); + + auto check_res = [](auto const& results, int dim) { + auto dims = Kokkos::create_mirror_view(results.dimensionalities); + auto ids = Kokkos::create_mirror_view(results.element_ids); + Kokkos::deep_copy(dims, results.dimensionalities); + Kokkos::deep_copy(ids, results.element_ids); + for (int i = 0; i < dims.size(); i++) { + CHECK(dims(i) == (pcms::TreePointSearch::Dimensionality)dim); + CHECK(ids(i) == i); + // CHECK_THAT(results(i).parametric_coords, + // Catch::Matchers::Contains(Catch::Matchers::WithinAbs(1.0, + // 10e-7))); + } + }; + + SECTION("Vertex intersection") + { + Kokkos::View coords( + "vertex coordinates", mesh_1x1.nverts(), 3); + auto coords_h = Kokkos::create_mirror_view(coords); + auto meshcoords = Omega_h::HostRead(mesh_1x1.coords()); + for (int i = 0; i < mesh_1x1.nverts(); i++) { + coords_h(i, 0) = meshcoords[i * 3]; + coords_h(i, 1) = meshcoords[i * 3 + 1]; + coords_h(i, 2) = meshcoords[i * 3 + 2]; + } + Kokkos::deep_copy(execs, coords, coords_h); + pcms::CoordinateView> + vert_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::TreePointSearch::Results results = tree_search.Apply(vert_cv); + + check_res(results, 0); + } + SECTION("Edge intersection") + { + Kokkos::View coords( + "intersection coords", mesh_1x1.nedges(), 3); + auto edge2vert = + Omega_h::HostRead(mesh_1x1.get_adj(Omega_h::EDGE, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh_1x1.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + + for (int i = 0; i < mesh_1x1.nedges(); i++) { + Omega_h::Vector<3> v0{meshcoords[edge2vert[2 * i] * 3], + meshcoords[edge2vert[2 * i] * 3 + 1], + meshcoords[edge2vert[2 * i] * 3 + 2]}; + Omega_h::Vector<3> v1{meshcoords[edge2vert[2 * i + 1] * 3], + meshcoords[edge2vert[2 * i + 1] * 3 + 1], + meshcoords[edge2vert[2 * i + 1] * 3 + 2]}; + Omega_h::Vector<3> midpoint = (v0 + v1) / 2.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + coords_h(i, 2) = midpoint[2]; + } + + Kokkos::deep_copy(execs, coords, coords_h); + + pcms::CoordinateView> + edge_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::TreePointSearch::Results results = tree_search.Apply(edge_cv); + check_res(results, 1); + } + SECTION("Face intersection") + { + Kokkos::View coords( + "intersection coords", mesh_1x1.nfaces(), 3); + auto face2vert = + Omega_h::HostRead(mesh_1x1.get_adj(Omega_h::FACE, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh_1x1.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + + for (int i = 0; i < mesh_1x1.nfaces(); i++) { + Omega_h::Vector<3> v0{meshcoords[face2vert[3 * i] * 3], + meshcoords[face2vert[3 * i] * 3 + 1], + meshcoords[face2vert[3 * i] * 3 + 2]}; + Omega_h::Vector<3> v1{meshcoords[face2vert[3 * i + 1] * 3], + meshcoords[face2vert[3 * i + 1] * 3 + 1], + meshcoords[face2vert[3 * i + 1] * 3 + 2]}; + Omega_h::Vector<3> v2{meshcoords[face2vert[3 * i + 2] * 3], + meshcoords[face2vert[3 * i + 2] * 3 + 1], + meshcoords[face2vert[3 * i + 2] * 3 + 2]}; + Omega_h::Vector<3> midpoint = (v0 + v1 + v2) / 3.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + coords_h(i, 2) = midpoint[2]; + } + Kokkos::deep_copy(execs, coords, coords_h); + + pcms::CoordinateView> + face_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::TreePointSearch::Results results = tree_search.Apply(face_cv); + check_res(results, 2); + } + SECTION("Region intersection") + { + Kokkos::View coords( + "intersection coords", mesh_1x1.nelems(), 3); + auto region2vert = + Omega_h::HostRead(mesh_1x1.get_adj(Omega_h::REGION, Omega_h::VERT).ab2b); + auto meshcoords = Omega_h::HostRead(mesh_1x1.coords()); + auto coords_h = Kokkos::create_mirror_view(coords); + + for (int i = 0; i < mesh_1x1.nelems(); i++) { + Omega_h::Vector<3> v0{meshcoords[region2vert[4 * i] * 3], + meshcoords[region2vert[4 * i] * 3 + 1], + meshcoords[region2vert[4 * i] * 3 + 2]}; + Omega_h::Vector<3> v1{meshcoords[region2vert[4 * i + 1] * 3], + meshcoords[region2vert[4 * i + 1] * 3 + 1], + meshcoords[region2vert[4 * i + 1] * 3 + 2]}; + Omega_h::Vector<3> v2{meshcoords[region2vert[4 * i + 2] * 3], + meshcoords[region2vert[4 * i + 2] * 3 + 1], + meshcoords[region2vert[4 * i + 2] * 3 + 2]}; + Omega_h::Vector<3> v3{meshcoords[region2vert[4 * i + 3] * 3], + meshcoords[region2vert[4 * i + 3] * 3 + 1], + meshcoords[region2vert[4 * i + 3] * 3 + 2]}; + Omega_h::Vector<3> midpoint = (v0 + v1 + v2 + v3) / 4.; + coords_h(i, 0) = midpoint[0]; + coords_h(i, 1) = midpoint[1]; + coords_h(i, 2) = midpoint[2]; + } + Kokkos::deep_copy(execs, coords, coords_h); + + pcms::CoordinateView> + region_cv(pcms::CoordinateSystem::Cartesian, + pcms::MakeConstRank2View(coords)); + pcms::TreePointSearch::Results results = tree_search.Apply(region_cv); + check_res(results, 3); + } +} diff --git a/test/test_twoClientOverlap.cpp b/test/test_twoClientOverlap.cpp index 1532b7279..61a03da69 100644 --- a/test/test_twoClientOverlap.cpp +++ b/test/test_twoClientOverlap.cpp @@ -73,7 +73,9 @@ redev::ClassPtn setupServerPartition(Omega_h::Mesh& mesh, auto setupComms(redev::Redev& rdv, std::string clientName, const int clientId) { const bool isSST = false; - adios2::Params params{{"Streaming", "On"}, {"OpenTimeoutSecs", "60"}}; + adios2::Params params{{"Streaming", "On"}, + {"OpenTimeoutSecs", "60"}, + {"BeginStepPollingFrequencySecs", "0.05"}}; REDEV_ALWAYS_ASSERT(clientId == 0 || clientId == 1); return rdv.CreateAdiosChannel(clientName, params, static_cast(isSST)); diff --git a/test/test_uniform_grid_field.cpp b/test/test_uniform_grid_field.cpp index b179167d3..e4e44ec69 100644 --- a/test/test_uniform_grid_field.cpp +++ b/test/test_uniform_grid_field.cpp @@ -5,7 +5,7 @@ #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/field/value_view.hpp" #include "pcms/utility/uniform_grid.h" #include "Omega_h_library.hpp" #include "Omega_h_build.hpp" @@ -15,6 +15,7 @@ #include "pcms/utility/arrays.h" #include "field_test_utils.h" #include +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::CreateUniformGridFromMesh; @@ -26,9 +27,9 @@ TEST_CASE("UniformGridDiscretization SameEntities: identical grids") grid.divisions = {4, 4}; pcms::UniformGridFieldLayout<2> layout_a(grid, 1, - pcms::CoordinateSystem::Cartesian); + pcms::csys::Cartesian::Deferred()); pcms::UniformGridFieldLayout<2> layout_b(grid, 1, - pcms::CoordinateSystem::Cartesian); + pcms::csys::Cartesian::Deferred()); auto disc_a = layout_a.GetDiscretization(); auto disc_b = layout_b.GetDiscretization(); @@ -51,9 +52,9 @@ TEST_CASE("UniformGridDiscretization SameEntities: different grids") grid_b.divisions = {8, 8}; pcms::UniformGridFieldLayout<2> layout_a(grid_a, 1, - pcms::CoordinateSystem::Cartesian); + pcms::csys::Cartesian::Deferred()); pcms::UniformGridFieldLayout<2> layout_b(grid_b, 1, - pcms::CoordinateSystem::Cartesian); + pcms::csys::Cartesian::Deferred()); auto disc_a = layout_a.GetDiscretization(); auto disc_b = layout_b.GetDiscretization(); @@ -72,7 +73,7 @@ void VerifyUniformGridFieldValues( int vertex_id = j * (grid.divisions[0] + 1) + i; pcms::Real x = ug_coords.GetValues()(vertex_id, 0); pcms::Real y = ug_coords.GetValues()(vertex_id, 1); - pcms::Real expected = x + 2.0 * y; + pcms::Real expected = pcms::test::linear_f(x, y); pcms::Real actual = ug_field_data[vertex_id]; REQUIRE(std::abs(expected - actual) <= 1e-10); } @@ -83,7 +84,8 @@ void VerifyUniformGridFieldValues( void VerifyMaskFieldValues(const pcms::UniformGrid<2>& grid, const pcms::Field& mask_field) { - auto mask_data = pcms::FlattenToRank1View(mask_field.GetDOFHolderDataHost()); + auto mask_data = + pcms::FlattenToRank1View(mask_field.GetDOFHolderDataHost().GetValues()); 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; @@ -100,7 +102,7 @@ TEST_CASE("UniformGrid field creation") grid.divisions = {5, 5}; auto layout = std::make_shared>( - grid, 1, pcms::CoordinateSystem::Cartesian); + grid, 1, pcms::csys::Cartesian::Deferred()); REQUIRE(layout->GetNumComponents() == 1); REQUIRE(layout->GetNumOwnedDofHolder() == 36); // (5+1)x(5+1) = 36 vertices @@ -108,8 +110,8 @@ TEST_CASE("UniformGrid field creation") REQUIRE_FALSE(layout->IsDistributed()); auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( - grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = field_space.CreateField(pcms::FieldMetadata{}); + grid, 1, pcms::csys::Cartesian::Deferred()); + auto field = field_space->CreateFunction(); REQUIRE(field.GetDOFHolderDataHost().size() == static_cast(layout->OwnedSize())); } @@ -122,17 +124,17 @@ TEST_CASE("UniformGrid order-0 field creation and evaluation") grid.divisions = {2, 2}; auto layout = std::make_shared>( - grid, 1, pcms::CoordinateSystem::Cartesian, 0); + grid, 1, pcms::csys::Cartesian::Deferred(), 0); auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( - grid, 1, pcms::CoordinateSystem::Cartesian, 0); - auto field = field_space.CreateField(pcms::FieldMetadata{}); + grid, 1, pcms::csys::Cartesian::Deferred(), 0); + auto field = field_space->CreateFunction(); pcms::UniformGridEvaluatorFactory<2> eval_factory(layout); REQUIRE(layout->GetOrder() == 0); REQUIRE(layout->GetNumOwnedDofHolder() == 4); auto coords_device = layout->GetDOFHolderCoordinates().GetValues(); - auto coords = pcms::test::CopyCoordinatesToHost(coords_device, 4, 2); + auto coords = pcms::test::CopyCoordinatesToHost(coords_device); REQUIRE(coords(0, 0) == Catch::Approx(2.5)); REQUIRE(coords(0, 1) == Catch::Approx(2.5)); @@ -140,32 +142,27 @@ TEST_CASE("UniformGrid order-0 field creation and evaluation") REQUIRE(coords(3, 1) == Catch::Approx(7.5)); std::vector data = {1.0, 2.0, 3.0, 4.0}; - field.SetDOFHolderDataHost( + field.SetDOFHolderDataUncheckedHost( pcms::Rank2View( data.data(), static_cast(data.size()), 1)); 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); + eval_coords, pcms::csys::Cartesian::Deferred()); 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)); + Kokkos::View results_device( + "results_device", 4, 1); + evaluator->Evaluate(field, pcms::test::TagLike(field, results_device)); + auto results_host = Kokkos::create_mirror_view_and_copy( + pcms::HostMemorySpace(), results_device); + + REQUIRE(results_host(0, 0) == Catch::Approx(1.0)); + REQUIRE(results_host(1, 0) == Catch::Approx(2.0)); + REQUIRE(results_host(2, 0) == Catch::Approx(3.0)); + REQUIRE(results_host(3, 0) == Catch::Approx(4.0)); } TEST_CASE("UniformGrid field data operations", "[uniform_grid_field]") @@ -176,20 +173,21 @@ TEST_CASE("UniformGrid field data operations", "[uniform_grid_field]") grid.divisions = {4, 4}; auto layout = std::make_shared>( - grid, 1, pcms::CoordinateSystem::Cartesian); + grid, 1, pcms::csys::Cartesian::Deferred()); auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( - grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = field_space.CreateField(pcms::FieldMetadata{}); + grid, 1, pcms::csys::Cartesian::Deferred()); + auto field = field_space->CreateFunction(); std::vector data(25); for (size_t i = 0; i < 25; ++i) data[i] = static_cast(i); - field.SetDOFHolderDataHost( + field.SetDOFHolderDataUncheckedHost( pcms::Rank2View( data.data(), static_cast(data.size()), 1)); - auto retrieved = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto retrieved = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(retrieved.size() == 25); for (size_t i = 0; i < 25; ++i) REQUIRE(retrieved[i] == static_cast(i)); @@ -203,10 +201,10 @@ TEST_CASE("UniformGrid field evaluation - piecewise constant") grid.divisions = {2, 2}; auto layout = std::make_shared>( - grid, 1, pcms::CoordinateSystem::Cartesian); + grid, 1, pcms::csys::Cartesian::Deferred()); auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( - grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = field_space.CreateField(pcms::FieldMetadata{}); + grid, 1, pcms::csys::Cartesian::Deferred()); + auto field = field_space->CreateFunction(); pcms::UniformGridEvaluatorFactory<2> eval_factory(layout); // Set vertex values for a 2x2 cell grid (3x3 = 9 vertices) @@ -221,7 +219,7 @@ 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) }; - field.SetDOFHolderDataHost( + field.SetDOFHolderDataUncheckedHost( pcms::Rank2View( data.data(), static_cast(data.size()), 1)); @@ -232,54 +230,25 @@ TEST_CASE("UniformGrid field evaluation - piecewise constant") 7.5, 7.5 // Cell 3 center }; auto device_coords = pcms::test::CreateDeviceCoordinateView( - eval_coords, pcms::CoordinateSystem::Cartesian); + eval_coords, pcms::csys::Cartesian::Deferred()); 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); + Kokkos::View results_device( + "results_device", 4, 1); + evaluator->Evaluate(field, pcms::test::TagLike(field, results_device)); + auto results_host = Kokkos::create_mirror_view_and_copy( + pcms::HostMemorySpace(), 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_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") -{ - pcms::UniformGrid<2> grid; - grid.bot_left = {0.0, 0.0}; - grid.edge_length = {10.0, 10.0}; - grid.divisions = {3, 3}; - - 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{}); - - std::vector data(16); - for (size_t i = 0; i < 16; ++i) - data[i] = static_cast(i * 10); - - field.SetDOFHolderDataHost( - pcms::Rank2View( - data.data(), static_cast(data.size()), 1)); - - pcms::test::CheckSerializeDeserialize(field); + REQUIRE(std::abs(results_host(0, 0) - 1.75) < 1e-10); + REQUIRE(std::abs(results_host(1, 0) - 2.25) < 1e-10); + REQUIRE(std::abs(results_host(2, 0) - 2.75) < 1e-10); + REQUIRE(std::abs(results_host(3, 0) - 3.25) < 1e-10); } TEST_CASE("UniformGrid field copy") @@ -290,7 +259,7 @@ TEST_CASE("UniformGrid field copy") grid.divisions = {2, 2}; auto layout = std::make_shared>( - grid, 1, pcms::CoordinateSystem::Cartesian); + grid, 1, pcms::csys::Cartesian::Deferred()); // 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), @@ -302,17 +271,18 @@ TEST_CASE("UniformGrid field copy") }; auto factory = pcms::LagrangeFunctionSpace::FromUniformGrid( - grid, 1, pcms::CoordinateSystem::Cartesian); - auto field = factory.CreateField(pcms::FieldMetadata{}); - field.SetDOFHolderDataHost( + grid, 1, pcms::csys::Cartesian::Deferred()); + auto field = factory->CreateFunction(); + field.SetDOFHolderDataUncheckedHost( pcms::Rank2View( data.data(), static_cast(data.size()), 1)); - auto field2 = factory.CreateField(pcms::FieldMetadata{}); - pcms::Copy copy(factory, factory); + auto field2 = factory->CreateFunction(); + pcms::Copy copy(*factory, *factory); copy.Apply(field, field2); - auto copied_data = pcms::FlattenToRank1View(field2.GetDOFHolderDataHost()); + auto copied_data = + pcms::FlattenToRank1View(field2.GetDOFHolderDataHost().GetValues()); REQUIRE(copied_data.size() == data.size()); for (size_t i = 0; i < data.size(); ++i) REQUIRE(copied_data[i] == data[i]); @@ -325,37 +295,37 @@ TEST_CASE("Transfer from OmegaH field to UniformGrid field") 2, 0, false); auto omega_h_factory = pcms::LagrangeFunctionSpace::FromMesh( - mesh, 1, 1, pcms::CoordinateSystem::Cartesian); - auto omega_h_field = - omega_h_factory.CreateField(pcms::FieldMetadata{}); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto omega_h_field = omega_h_factory->CreateFunction(); pcms::test::SetField( - omega_h_field, - OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); + omega_h_field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { + return pcms::test::linear_f(x, y); + }); pcms::UniformGrid<2> grid; grid.edge_length = {1.0, 1.0}; 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{}); + grid, 1, pcms::csys::Cartesian::Deferred()); + auto ug_field = ug_factory->CreateFunction(); - pcms::Interpolator interp(omega_h_factory, ug_factory); + pcms::Interpolator interp(*omega_h_factory, *ug_factory); interp.Apply(omega_h_field, ug_field); auto transferred_data = - pcms::FlattenToRank1View(ug_field.GetDOFHolderDataHost()); - auto ug_coords = ug_factory.GetLayout()->GetDOFHolderCoordinates(); - int num_ug_nodes = ug_factory.GetLayout()->GetNumOwnedDofHolder(); + pcms::FlattenToRank1View(ug_field.GetDOFHolderDataHost().GetValues()); + 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.GetValues(), num_ug_nodes, 2); + pcms::test::CopyCoordinatesToHost(ug_coords.GetValues()); for (int i = 0; i < num_ug_nodes; ++i) { 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 expected = pcms::test::linear_f(x, y); REQUIRE(std::abs(transferred_data[i] - expected) < 1e-6); } } @@ -372,7 +342,8 @@ TEST_CASE("Create binary field from uniform grid") auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{5, 5}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(field_data.size() == 36); // (5+1) * (5+1) = 36 vertices @@ -391,7 +362,8 @@ TEST_CASE("Create binary field from uniform grid") auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{10, 8}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(field_data.size() == 99); // (10+1) * (8+1) = 99 vertices @@ -409,7 +381,8 @@ TEST_CASE("Create binary field from uniform grid") auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{10, 10}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); for (size_t i = 0; i < field_data.size(); ++i) { REQUIRE((field_data[i] == 0.0 || field_data[i] == 1.0)); @@ -427,7 +400,8 @@ TEST_CASE("Create binary field from uniform grid") grid.divisions = {10, 10}; auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, grid); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(field_data.size() == 121); // (10+1) * (10+1) = 121 vertices @@ -461,7 +435,8 @@ TEST_CASE("Create binary field from uniform grid") auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{20, 20}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(field_data.size() == 441); // (20+1) * (20+1) = 441 vertices @@ -482,7 +457,8 @@ TEST_CASE("Create binary field from uniform grid") auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{30, 10}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(field_data.size() == 341); // (30+1) * (10+1) = 341 vertices @@ -509,7 +485,8 @@ TEST_CASE("Binary field integration with grid methods") auto grid = CreateUniformGridFromMesh<2>(mesh, std::array{8, 8}); auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{8, 8}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); // Get field value for a specific vertex (middle vertex at i=4, j=4) pcms::LO vertex_id = 4 * 9 + 4; @@ -529,7 +506,8 @@ TEST_CASE("Binary field integration with grid methods") auto grid = CreateUniformGridFromMesh<2>(mesh, std::array{10, 10}); auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{10, 10}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); int q1 = 0, q2 = 0, q3 = 0, q4 = 0; @@ -584,7 +562,8 @@ TEST_CASE("Performance and edge cases") auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{50, 50}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(field_data.size() == 2601); // (50+1) * (50+1) = 2601 vertices @@ -601,7 +580,8 @@ TEST_CASE("Performance and edge cases") auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{2, 2}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(field_data.size() == 9); // (2+1) * (2+1) = 9 vertices @@ -618,7 +598,8 @@ TEST_CASE("Performance and edge cases") auto [layout, field] = pcms::CreateUniformGridBinaryField<2>(mesh, std::array{25, 10}); - auto field_data = pcms::FlattenToRank1View(field.GetDOFHolderDataHost()); + auto field_data = + pcms::FlattenToRank1View(field.GetDOFHolderDataHost().GetValues()); REQUIRE(field_data.size() == 286); // (25+1) * (10+1) = 286 vertices @@ -639,45 +620,176 @@ TEST_CASE("UniformGrid workflow") 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{}); + mesh, 1, 1, pcms::csys::Cartesian::Deferred()); + auto omega_h_field = omega_h_factory->CreateFunction(); pcms::test::SetField( - omega_h_field, - OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { return x + 2.0 * y; }); + omega_h_field, OMEGA_H_LAMBDA(pcms::Real x, pcms::Real y) { + return pcms::test::linear_f(x, y); + }); auto ug_factory = pcms::LagrangeFunctionSpace::FromUniformGrid( - grid, 1, pcms::CoordinateSystem::Cartesian); - auto ug_field = ug_factory.CreateField(pcms::FieldMetadata{}); + grid, 1, pcms::csys::Cartesian::Deferred()); + auto ug_field = ug_factory->CreateFunction(); auto [mask_layout, mask_field] = pcms::CreateUniformGridBinaryField<2>(mesh, grid); - pcms::Interpolator interp(omega_h_factory, ug_factory); + pcms::Interpolator interp(*omega_h_factory, *ug_factory); interp.Apply(omega_h_field, ug_field); auto ug_coords_device_view = - ug_factory.GetLayout()->GetDOFHolderCoordinates().GetValues(); + ug_factory->GetLayout()->GetDOFHolderCoordinates().GetValues(); 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, 0); - }); - auto ug_field_data_host_view = - Kokkos::View("", 25); - Kokkos::deep_copy(ug_field_data_host_view, ug_field_data_device_view); + pcms::test::CopyCoordinatesToHost(ug_coords_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); + pcms::csys::Cartesian::Deferred(), + pcms::MakeConstRank2View(ug_coords_host_view)); + const auto ug_field_data = + pcms::FlattenToRank1View(ug_field.GetDOFHolderDataHost().GetValues()); VerifyUniformGridFieldValues(grid, ug_coords_view, ug_field_data); VerifyMaskFieldValues(grid, mask_field); } + +TEST_CASE("UniformGrid Order-1 multi-component field evaluation") +{ + // Create a 2x2 cell uniform grid + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {10.0, 10.0}; + grid.divisions = {2, 2}; + + // Components: x, y, x+y (separable) and x*y (non-separable). Off-center + // query points plus the non-separable component catch axis-mixing bugs that + // symmetric cell-center samples would hide. + const int num_components = 4; + + auto layout = std::make_shared>( + grid, num_components, pcms::csys::Cartesian::Deferred()); + auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, num_components, pcms::csys::Cartesian::Deferred()); + auto field = field_space->CreateFunction(); + pcms::UniformGridEvaluatorFactory<2> eval_factory(layout); + + // Vertex layout: + // v6---v7---v8 + // | 2 | 3 | + // v3---v4---v5 + // | 0 | 1 | + // v0---v1---v2 + std::vector data; + for (int j = 0; j <= grid.divisions[1]; ++j) { + for (int i = 0; i <= grid.divisions[0]; ++i) { + pcms::Real x = + grid.bot_left[0] + i * (grid.edge_length[0] / grid.divisions[0]); + pcms::Real y = + grid.bot_left[1] + j * (grid.edge_length[1] / grid.divisions[1]); + data.push_back(x); // Component 0: x + data.push_back(y); // Component 1: y + data.push_back(x + y); // Component 2: x + y + data.push_back(x * y); // Component 3: x * y (non-separable) + } + } + + // Reshape data to [num_vertices][num_components] + pcms::LO num_vertices = (grid.divisions[0] + 1) * (grid.divisions[1] + 1); + field.SetDOFHolderDataHost( + pcms::ValueView( + pcms::ValueBasis{}, + pcms::Rank2View( + data.data(), num_vertices, num_components))); + + // Mix of cell centers and off-center points. + const int num_points = 4; + std::vector eval_coords = { + 2.5, 2.5, // cell 0 center + 1.0, 3.0, // cell 0 off-center + 6.0, 1.5, // cell 1 off-center + 7.5, 7.5 // cell 3 center + }; + auto device_coords = pcms::test::CreateDeviceCoordinateView( + eval_coords, pcms::csys::Cartesian::Deferred()); + auto evaluator = eval_factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View results_device( + "results_device", num_points, num_components); + evaluator->Evaluate(field, pcms::test::TagLike(field, results_device)); + auto results_host = Kokkos::create_mirror_view_and_copy( + pcms::HostMemorySpace(), results_device); + + // Expected [x, y, x+y, x*y] at each query point. + const pcms::Real expected[num_points][num_components] = { + {2.5, 2.5, 5.0, 6.25}, // (2.5, 2.5) + {1.0, 3.0, 4.0, 3.0}, // (1.0, 3.0) + {6.0, 1.5, 7.5, 9.0}, // (6.0, 1.5) + {7.5, 7.5, 15.0, 56.25}, // (7.5, 7.5) + }; + + for (int p = 0; p < num_points; ++p) { + for (int c = 0; c < num_components; ++c) { + INFO("point " << p << " component " << c); + REQUIRE(std::abs(results_host(p, c) - expected[p][c]) < 1e-10); + } + } +} + +TEST_CASE("UniformGrid Order-0 multi-component field (regression)") +{ + // Verify Order-0 (piecewise constant) still works with multiple components + pcms::UniformGrid<2> grid; + grid.bot_left = {0.0, 0.0}; + grid.edge_length = {10.0, 10.0}; + grid.divisions = {2, 2}; + + const int num_components = 2; + + auto layout = std::make_shared>( + grid, num_components, pcms::csys::Cartesian::Deferred(), 0); // Order 0 + auto field_space = pcms::LagrangeFunctionSpace::FromUniformGrid( + grid, num_components, pcms::csys::Cartesian::Deferred(), 0); + auto field = field_space->CreateFunction(); + pcms::UniformGridEvaluatorFactory<2> eval_factory(layout); + + // Set cell values (4 cells, 2 components each) + std::vector data = { + 1.0, 10.0, // Cell 0 + 2.0, 20.0, // Cell 1 + 3.0, 30.0, // Cell 2 + 4.0, 40.0 // Cell 3 + }; + + field.SetDOFHolderDataHost( + pcms::ValueView( + pcms::ValueBasis{}, + pcms::Rank2View( + data.data(), 4, num_components))); + + // 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 device_coords = pcms::test::CreateDeviceCoordinateView( + eval_coords, pcms::csys::Cartesian::Deferred()); + auto evaluator = eval_factory.CreatePointEvaluator( + pcms::EvaluationRequest::FromCoordinates(device_coords.coordinate_view)); + + Kokkos::View results_device( + "results_device", 4, num_components); + evaluator->Evaluate(field, pcms::test::TagLike(field, results_device)); + auto results_host = Kokkos::create_mirror_view_and_copy( + pcms::HostMemorySpace(), results_device); + + // For Order-0, values should match cell values exactly + REQUIRE(results_host(0, 0) == 1.0); + REQUIRE(results_host(0, 1) == 10.0); + REQUIRE(results_host(1, 0) == 2.0); + REQUIRE(results_host(1, 1) == 20.0); + REQUIRE(results_host(2, 0) == 3.0); + REQUIRE(results_host(2, 1) == 30.0); + REQUIRE(results_host(3, 0) == 4.0); + REQUIRE(results_host(3, 1) == 40.0); +} diff --git a/test/test_xgc_field_data.cpp b/test/test_xgc_field_data.cpp index 7aa95d329..55a89d593 100644 --- a/test/test_xgc_field_data.cpp +++ b/test/test_xgc_field_data.cpp @@ -1,9 +1,7 @@ #include -#include #include "pcms/field/data/xgc.h" +#include "pcms/coupler/overlap_mask.h" #include "pcms/field/function_space/xgc.h" -#include "pcms/coupler/serializer/xgc.h" -#include #include namespace @@ -29,111 +27,40 @@ bool in_overlap(int, int id) } // namespace -TEST_CASE("XGC FieldLayout marks overlap entries and gids") +TEST_CASE("XGC FieldLayout owns and classifies every entry") { static constexpr int data_size = 16; auto rc = create_dummy_rc(data_size); - pcms::XGCFieldLayout layout(rc, in_overlap, data_size); + pcms::XGCFieldLayout layout(rc, data_size); auto owned = layout.GetOwnedHost(); auto gids = layout.GetGidsHost(); auto class_dims = layout.GetDOFHolderClassificationDimensionsHost(); auto class_ids = layout.GetDOFHolderClassificationIdsHost(); + const pcms::OverlapMask overlap_mask(in_overlap); + auto overlap = overlap_mask.GetMask(layout); 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); - - auto owned = layout->GetOwnedHost(); - - // Non-owned / non-participating holders must have negative permutation - // entries (matching the real FieldExchangePlanner output). Only the 4 - // owned holders get compacted non-negative indices. - std::vector permutation(data_size, -1); - int entry = 0; - for (int i = 0; i < data_size; ++i) { - if (owned[i]) { - permutation[i] = entry++; - } - } - const int num_owned = entry; - REQUIRE(num_owned == 4); - - std::vector buffer(num_owned, -1.0); - - REQUIRE(serializer.Serialize(field, *layout, pcms::make_array_view(buffer), - pcms::make_const_array_view(permutation)) == - num_owned); - - for (int i = 0; i < data_size; ++i) { - if (owned[i]) { - REQUIRE(buffer[permutation[i]] == data[i]); - buffer[permutation[i]] += 100.0; - } else { - REQUIRE(permutation[i] == -1); - } - } - - serializer.Deserialize(field, *layout, pcms::make_const_array_view(buffer), - pcms::make_const_array_view(permutation)); - - auto after = pcms::FlattenToRank1View(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])); - } + REQUIRE(owned[i]); + REQUIRE(class_dims[i] == 0); + REQUIRE(class_ids[i] == (i % 4 == 0 ? 0 : 1)); + REQUIRE(overlap[i] == (i % 4 == 0)); } } -TEST_CASE("XGCFunctionSpace creates fields and rejects evaluator access") +TEST_CASE("XGCFieldFactory creates fields and rejects evaluator access") { static constexpr int data_size = 16; auto rc = create_dummy_rc(data_size); - pcms::XGCFunctionSpace function_space(rc, in_overlap, data_size); + pcms::XGCFieldFactory function_space(rc, 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))); + "", std::make_unique>( + function_space.GetXGCLayout(), pcms::ValueBasis{}, + pcms::make_array_view(data))); REQUIRE(&field.GetLayout() == function_space.GetLayout().get()); - REQUIRE(function_space.GetCoordinateSystem() == pcms::CoordinateSystem::XGC); - // XGC does not support point evaluation; CreatePointEvaluator throws. - using LayoutPolicy = - pcms::detail::default_layout_for_memory_space_t; - pcms::Rank2View - empty_coords{nullptr, 0, 2}; - REQUIRE_THROWS_AS(function_space.CreatePointEvaluator( - pcms::EvaluationRequest::FromCoordinates( - pcms::CoordinateView{ - pcms::CoordinateSystem::XGC, empty_coords})), - pcms::pcms_error); } diff --git a/test/test_xgc_reverse_classification.cpp b/test/test_xgc_reverse_classification.cpp index 4b87fcb9c..5603b52b1 100644 --- a/test/test_xgc_reverse_classification.cpp +++ b/test/test_xgc_reverse_classification.cpp @@ -39,7 +39,7 @@ TEST_CASE("reverse classification") } auto vec = rc.Serialize(); pcms::ReverseClassificationVertex rc_deserialized; - pcms::Rank1View av{vec.data(), vec.size()}; + auto av = pcms::make_array_view(vec); rc_deserialized.Deserialize(av); REQUIRE(rc_deserialized == rc); } diff --git a/test/testing.cmake b/test/testing.cmake index 12590a575..d26a4a4eb 100644 --- a/test/testing.cmake +++ b/test/testing.cmake @@ -7,7 +7,9 @@ endif() set(PCMS_TEST_DATA_DIR "" CACHE PATH "Path to a local copy of the pcms_coupling_data repo.") -if(NOT EXISTS ${PCMS_TEST_DATA_DIR}) +# The data is only needed to run tests. Configuring with BUILD_TESTING=OFF +# (e.g. a package build without tests) shouldn't require it. +if(BUILD_TESTING AND NOT EXISTS "${PCMS_TEST_DATA_DIR}") message( FATAL_ERROR "PCMS_TEST_DATA_DIR \"${PCMS_TEST_DATA_DIR}\" is not accessible" ) @@ -26,6 +28,27 @@ if(MPIEXEC_PREFLAGS STREQUAL "") set(MPIEXEC_PREFLAGS "none") endif() +set(PCMS_TEST_OMP_NUM_THREADS + "1" + CACHE STRING "OMP_NUM_THREADS for each rank of the MPI tests") + +function(set_mpi_test_environment TESTNAME) + set_property( + TEST ${TESTNAME} + APPEND + PROPERTY ENVIRONMENT "OMP_NUM_THREADS=${PCMS_TEST_OMP_NUM_THREADS}") +endfunction() + +# Run an MPI test in a directory of its own so that the ADIOS2 files (*.bp, +# *.sst) and logs of concurrently running tests don't collide, and tell ctest +# how many ranks the test uses so that `ctest -j N` treats N as a core budget. +function(isolate_mpi_test TESTNAME NPROCS) + set(dir ${CMAKE_CURRENT_BINARY_DIR}/mpi_test_runs/${TESTNAME}) + file(MAKE_DIRECTORY ${dir}) + set_tests_properties(${TESTNAME} PROPERTIES WORKING_DIRECTORY ${dir} + PROCESSORS ${NPROCS}) +endfunction() + message(STATUS "MPIEXEC_EXECUTABLE: ${MPIEXEC_EXECUTABLE}") message(STATUS "MPIEXEC_PREFLAGS: ${MPIEXEC_PREFLAGS}") message(STATUS "MPIEXEC_NUMPROC_FLAG: ${MPIEXEC_NUMPROC_FLAG}") @@ -46,12 +69,6 @@ function(add_exe NAME) endif() endfunction(add_exe) -function(removeBpFiles TESTNAME) - add_test(NAME ${TESTNAME} - COMMAND ${CMAKE_COMMAND} -P - ${CMAKE_SOURCE_DIR}/ctest/removeBpFiles.cmake) -endfunction() - function(increaseTimeoutForValgrind TIME TIME_VAR_OUT) set(factor 10) if(NOT ${VALGRIND_EXECUTABLE} MATCHES "none") @@ -67,8 +84,19 @@ function(increaseTimeoutForValgrind TIME TIME_VAR_OUT) endif() endfunction() +# MPI tests don't run in the build directory, so an executable given as a +# target name is replaced with the full path to the target's file. Anything +# else (a path or generator expression) is used as is. +function(resolve_test_exe EXE_VAR) + if(TARGET ${${EXE_VAR}}) + set(${EXE_VAR} + $ + PARENT_SCOPE) + endif() +endfunction() + function(mpi_test TESTNAME PROCS EXE) - removebpfiles(${TESTNAME}_cleanup) + resolve_test_exe(EXE) set(TEST_COMMAND "") list(APPEND TEST_COMMAND ${MPIEXEC_EXECUTABLE}) if(NOT ${MPIEXEC_PREFLAGS} MATCHES "none") @@ -80,6 +108,8 @@ function(mpi_test TESTNAME PROCS EXE) endif() list(APPEND TEST_COMMAND ${EXE} ${ARGN}) add_test(NAME ${TESTNAME} COMMAND ${TEST_COMMAND }) + set_mpi_test_environment(${TESTNAME}) + isolate_mpi_test(${TESTNAME} ${PROCS}) endfunction(mpi_test) function(dual_mpi_test) @@ -95,18 +125,22 @@ function(dual_mpi_test) set(multiValueArgs ARGS1 ARGS2) cmake_parse_arguments(DUALTEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - removebpfiles(${DUALTEST_TESTNAME}_cleanup) + resolve_test_exe(DUALTEST_EXE1) + resolve_test_exe(DUALTEST_EXE2) increasetimeoutforvalgrind(${DUALTEST_TIMEOUT} DUALTEST_TIMEOUT) add_test( NAME ${DUALTEST_TESTNAME} COMMAND - ${CMAKE_SOURCE_DIR}/ctest/runMultipleMpiJobs.sh ${MPIEXEC_EXECUTABLE} - ${MPIEXEC_PREFLAGS} ${MPIEXEC_NUMPROC_FLAG} ${VALGRIND_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/ctest/runMultipleMpiJobs.sh --clean-adios2 + ${MPIEXEC_EXECUTABLE} ${MPIEXEC_PREFLAGS} ${MPIEXEC_NUMPROC_FLAG} ${VALGRIND_EXECUTABLE} ${VALGRIND_ARGS} ${DUALTEST_NAME1} ${DUALTEST_PROCS1} ${DUALTEST_EXE1} "${DUALTEST_ARGS1}" ${DUALTEST_NAME2} ${DUALTEST_PROCS2} ${DUALTEST_EXE2} "${DUALTEST_ARGS2}") set_tests_properties(${DUALTEST_TESTNAME} PROPERTIES TIMEOUT ${DUALTEST_TIMEOUT}) + set_mpi_test_environment(${DUALTEST_TESTNAME}) + math(EXPR nprocs "${DUALTEST_PROCS1} + ${DUALTEST_PROCS2}") + isolate_mpi_test(${DUALTEST_TESTNAME} ${nprocs}) endfunction(dual_mpi_test) function(tri_mpi_test) @@ -125,17 +159,23 @@ function(tri_mpi_test) set(multiValueArgs ARGS1 ARGS2 ARGS3) cmake_parse_arguments(TRITEST "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - removebpfiles(${TRITEST_TESTNAME}_cleanup) + resolve_test_exe(TRITEST_EXE1) + resolve_test_exe(TRITEST_EXE2) + resolve_test_exe(TRITEST_EXE3) increasetimeoutforvalgrind(${TRITEST_TIMEOUT} TRITEST_TIMEOUT) add_test( NAME ${TRITEST_TESTNAME} COMMAND - ${CMAKE_SOURCE_DIR}/ctest/runMultipleMpiJobs.sh ${MPIEXEC_EXECUTABLE} - ${MPIEXEC_PREFLAGS} ${MPIEXEC_NUMPROC_FLAG} ${VALGRIND_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/ctest/runMultipleMpiJobs.sh --clean-adios2 + ${MPIEXEC_EXECUTABLE} ${MPIEXEC_PREFLAGS} ${MPIEXEC_NUMPROC_FLAG} ${VALGRIND_EXECUTABLE} ${VALGRIND_ARGS} ${TRITEST_NAME1} ${TRITEST_PROCS1} ${TRITEST_EXE1} "${TRITEST_ARGS1}" ${TRITEST_NAME2} ${TRITEST_PROCS2} ${TRITEST_EXE2} "${TRITEST_ARGS2}" ${TRITEST_NAME3} ${TRITEST_PROCS3} ${TRITEST_EXE3} "${TRITEST_ARGS3}") set_tests_properties(${TRITEST_TESTNAME} PROPERTIES TIMEOUT ${TRITEST_TIMEOUT}) + set_mpi_test_environment(${TRITEST_TESTNAME}) + math(EXPR nprocs + "${TRITEST_PROCS1} + ${TRITEST_PROCS2} + ${TRITEST_PROCS3}") + isolate_mpi_test(${TRITEST_TESTNAME} ${nprocs}) endfunction(tri_mpi_test) diff --git a/test/xgc_n0_coupling_server.cpp b/test/xgc_n0_coupling_server.cpp index ddbac140a..417e965f5 100644 --- a/test/xgc_n0_coupling_server.cpp +++ b/test/xgc_n0_coupling_server.cpp @@ -8,11 +8,12 @@ #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/value_view.hpp" #include "pcms/field/data/simple.h" #include "pcms/transfer/copy.h" #include #include +#include "pcms/field/coordinate_systems/cartesian.hpp" using pcms::GO; using pcms::LO; @@ -24,8 +25,8 @@ namespace ts = test_support; // maximum flexibility moving forward // -[[nodiscard]] -static std::string MakeFieldName(const std::string& name, int plane) +[[nodiscard]] static std::string MakeFieldName(const std::string& name, + int plane) { std::stringstream field_name; field_name << name; @@ -40,19 +41,15 @@ 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) +[[nodiscard]] static RegisteredField AddField( + pcms::ApplicationComm* application, + const std::shared_ptr& 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)); + auto field = function_space->CreateFunction(path + field_name); + auto handle = application->AddField(std::move(field)); return {std::move(handle)}; } @@ -90,7 +87,7 @@ static void CopyFields(const std::vector& from_fields, } } -void SendRecvDensity(pcms::Application* core, pcms::Application* edge, +void SendRecvDensity(pcms::ApplicationComm* core, pcms::ApplicationComm* edge, XGCAnalysis& core_analysis, XGCAnalysis& edge_analysis, int rank) { @@ -143,7 +140,7 @@ void SendRecvDensity(pcms::Application* core, pcms::Application* edge, if (!rank) ts::printTime("Send Density", min, max, avg); } -void SendRecvPotential(pcms::Application* core, pcms::Application* edge, +void SendRecvPotential(pcms::ApplicationComm* core, pcms::ApplicationComm* edge, XGCAnalysis& core_analysis, XGCAnalysis& edge_analysis, int rank) { @@ -200,8 +197,9 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, MPI_Comm_rank(comm, &rank); auto time1 = std::chrono::steady_clock::now(); - pcms::Coupler cpl("xgc_n0_coupling", comm, true, - redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); + pcms::CouplerComm cpl( + "xgc_n0_coupling", comm, true, + redev::Partition{ts::setupServerPartition(mesh, cpn_file)}); const auto partition = std::get(cpl.GetPartition()); std::string numbering = "simNumbering"; PCMS_ALWAYS_ASSERT(mesh.has_tag(0, numbering)); @@ -219,11 +217,8 @@ void omegah_coupler(MPI_Comm comm, Omega_h::Mesh& mesh, 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); + mesh, 1, 1, pcms::csys::Cartesian::Deferred(), is_overlap, numbering, + pcms::LagrangeFunctionSpace::Backend::OmegaH, "n0_layout"); auto time2 = std::chrono::steady_clock::now(); elapsed_seconds = time2 - time1; ts::timeMinMaxAvg(elapsed_seconds.count(), min, max, avg);