From cbe496bd92dc01a2ec78f5827a339e6f485c0eeb Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 18:16:07 +0000 Subject: [PATCH 01/19] Add regression tests for the mesh readers The new CTest suite runs pumgen with 1 to 4 ranks on small gmsh-generated fixtures and compares the written datasets with h5diff against reference PUML files. fixtures/generate.py computes the references from the gmsh model directly, i.e. independently of PUMGen. The element and vertex counts of the layered fixture do not divide evenly among 3 or 4 ranks. CI now runs the tests for the Release and the Debug (AddressSanitizer) build. AI-generated. Model: Claude Opus 5.5 --- .github/workflows/cmake.yml | 6 +- .pre-commit-config.yaml | 2 + CMakeLists.txt | 5 + REUSE.toml | 11 + tests/CMakeLists.txt | 69 + tests/RunPumgenTest.cmake | 59 + tests/fixtures/coarse-binary-v41.msh | Bin 0 -> 27978 bytes tests/fixtures/coarse-o2-v22.msh | 1186 +++++++ tests/fixtures/coarse-o2-v41.msh | 1921 ++++++++++++ tests/fixtures/coarse-o2.puml.h5 | Bin 0 -> 48120 bytes tests/fixtures/coarse-offset-v41.msh | 877 ++++++ tests/fixtures/coarse-partitioned-v41.msh | 1000 ++++++ tests/fixtures/coarse-v41.msh | 877 ++++++ tests/fixtures/coarse.puml.h5 | Bin 0 -> 20280 bytes tests/fixtures/generate.py | 246 ++ tests/fixtures/layered-v22.msh | 2893 +++++++++++++++++ tests/fixtures/layered-v41.msh | 3444 +++++++++++++++++++++ tests/fixtures/layered.neu | 3378 ++++++++++++++++++++ tests/fixtures/layered.puml.h5 | Bin 0 -> 78656 bytes tests/fixtures/tiny-badnode-v22.msh | 14 + tests/fixtures/tiny-badnode-v41.msh | 24 + tests/fixtures/tiny-badnumber-v41.msh | 24 + tests/fixtures/tiny-duplicate-v22.msh | 14 + tests/fixtures/tiny-duplicate-v41.msh | 24 + tests/fixtures/tiny-sparse-v41.msh | 24 + tests/fixtures/tiny-truncated-v41.msh | 23 + tests/fixtures/tiny-unknowntype-v41.msh | 24 + tests/fixtures/tiny-v22.msh | 14 + tests/fixtures/tiny-v40.msh | 24 + tests/fixtures/tiny-v41.msh | 24 + tests/fixtures/tiny.puml.h5 | Bin 0 -> 4368 bytes 31 files changed, 16206 insertions(+), 1 deletion(-) create mode 100644 REUSE.toml create mode 100644 tests/CMakeLists.txt create mode 100644 tests/RunPumgenTest.cmake create mode 100644 tests/fixtures/coarse-binary-v41.msh create mode 100644 tests/fixtures/coarse-o2-v22.msh create mode 100644 tests/fixtures/coarse-o2-v41.msh create mode 100644 tests/fixtures/coarse-o2.puml.h5 create mode 100644 tests/fixtures/coarse-offset-v41.msh create mode 100644 tests/fixtures/coarse-partitioned-v41.msh create mode 100644 tests/fixtures/coarse-v41.msh create mode 100644 tests/fixtures/coarse.puml.h5 create mode 100644 tests/fixtures/generate.py create mode 100644 tests/fixtures/layered-v22.msh create mode 100644 tests/fixtures/layered-v41.msh create mode 100644 tests/fixtures/layered.neu create mode 100644 tests/fixtures/layered.puml.h5 create mode 100644 tests/fixtures/tiny-badnode-v22.msh create mode 100644 tests/fixtures/tiny-badnode-v41.msh create mode 100644 tests/fixtures/tiny-badnumber-v41.msh create mode 100644 tests/fixtures/tiny-duplicate-v22.msh create mode 100644 tests/fixtures/tiny-duplicate-v41.msh create mode 100644 tests/fixtures/tiny-sparse-v41.msh create mode 100644 tests/fixtures/tiny-truncated-v41.msh create mode 100644 tests/fixtures/tiny-unknowntype-v41.msh create mode 100644 tests/fixtures/tiny-v22.msh create mode 100644 tests/fixtures/tiny-v40.msh create mode 100644 tests/fixtures/tiny-v41.msh create mode 100644 tests/fixtures/tiny.puml.h5 diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index be078f9..a6c4ebb 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -29,12 +29,16 @@ jobs: run: | git submodule update --init --recursive mkdir build_${{ matrix.build_type}} && cd build_${{ matrix.build_type}} - cmake .. -DCMAKE_PREFIX_PATH=${{ runner.workspace }}/opt -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + cmake .. -DCMAKE_PREFIX_PATH=${{ runner.workspace }}/opt -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DMPIEXEC_PREFLAGS=--oversubscribe - name: Build working-directory: ${{ github.workspace }}/build_${{ matrix.build_type }} run: cmake --build . + - name: Test + working-directory: ${{ github.workspace }}/build_${{ matrix.build_type }} + run: ctest --output-on-failure + pre-commit-check: runs-on: ubuntu-latest steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d38090d..4034174 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,5 +59,7 @@ repos: hooks: - id: end-of-file-fixer name: '[GENERIC] newline eof' + exclude: ^tests/fixtures/ - id: trailing-whitespace name: '[GENERIC] remove trailing whitespace' + exclude: ^tests/fixtures/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 58e4d5f..0d40349 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -139,3 +139,8 @@ if(SIMMETRIX) endif() set_target_properties(pumgen PROPERTIES INSTALL_RPATH "${INSTALL_RPATH}") install(TARGETS pumgen RUNTIME DESTINATION bin DESTINATION ${CMAKE_INSTALL_BINDIR}) + +include(CTest) +if (BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..4465d3c --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 SeisSol Group +# +# SPDX-License-Identifier: BSD-3-Clause + +version = 1 + +[[annotations]] +path = ["tests/fixtures/*.msh", "tests/fixtures/*.neu", "tests/fixtures/*.h5"] +precedence = "override" +SPDX-FileCopyrightText = "2026 SeisSol Group" +SPDX-License-Identifier = "BSD-3-Clause" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..d51db5c --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,69 @@ +# SPDX-FileCopyrightText: 2026 SeisSol Group +# +# SPDX-License-Identifier: BSD-3-Clause + +find_program(H5DIFF_EXECUTABLE NAMES h5diff DOC "h5diff, used to compare the test output") +if(NOT H5DIFF_EXECUTABLE) + message(WARNING "h5diff not found: the tests only check whether pumgen succeeds") +endif() + +set(PUMGEN_TEST_FIXTURES ${CMAKE_CURRENT_SOURCE_DIR}/fixtures) +set(PUMGEN_TEST_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/output) +file(MAKE_DIRECTORY ${PUMGEN_TEST_OUTPUT}) + +# pumgen_add_test( RANKS SOURCE INPUT +# [REFERENCE [TOLERANCE ]] [EXPECT_ERROR ] +# [ARGS ...]) +function(pumgen_add_test name) + cmake_parse_arguments(PARSE_ARGV 1 TEST "" "RANKS;SOURCE;INPUT;REFERENCE;TOLERANCE;EXPECT_ERROR" + "ARGS") + set(output ${PUMGEN_TEST_OUTPUT}/${name}.puml.h5) + set(command + ${MPIEXEC_EXECUTABLE} + ${MPIEXEC_NUMPROC_FLAG} + ${TEST_RANKS} + ${MPIEXEC_PREFLAGS} + $ + ${MPIEXEC_POSTFLAGS} + -s + ${TEST_SOURCE} + ${TEST_ARGS} + ${PUMGEN_TEST_FIXTURES}/${TEST_INPUT} + ${output}) + string(REPLACE ";" "|" command "${command}") + + set(script_args -DCOMMAND=${command} -DOUTPUT=${output}) + if(DEFINED TEST_EXPECT_ERROR) + list(APPEND script_args "-DEXPECT_ERROR=${TEST_EXPECT_ERROR}") + elseif(DEFINED TEST_REFERENCE AND H5DIFF_EXECUTABLE) + list(APPEND script_args -DREFERENCE=${PUMGEN_TEST_FIXTURES}/${TEST_REFERENCE} + -DH5DIFF=${H5DIFF_EXECUTABLE}) + if(DEFINED TEST_TOLERANCE) + list(APPEND script_args -DTOLERANCE=${TEST_TOLERANCE}) + endif() + endif() + + add_test(NAME pumgen.${name} COMMAND ${CMAKE_COMMAND} ${script_args} -P + ${CMAKE_CURRENT_SOURCE_DIR}/RunPumgenTest.cmake) + set_tests_properties( + pumgen.${name} PROPERTIES PROCESSORS ${TEST_RANKS} TIMEOUT 120 ENVIRONMENT + "ASAN_OPTIONS=detect_leaks=0") +endfunction() + +# The fixtures are created by fixtures/generate.py; the reference files are computed from the +# gmsh model directly. The layered mesh has element and vertex counts which do not divide evenly +# among 3 or 4 ranks. + +foreach(ranks 1 2 3 4) + pumgen_add_test(msh4.layered.np${ranks} RANKS ${ranks} SOURCE msh4 INPUT layered-v41.msh + REFERENCE layered.puml.h5) +endforeach() +foreach(ranks 1 2 3) + pumgen_add_test(msh2.layered.np${ranks} RANKS ${ranks} SOURCE msh2 INPUT layered-v22.msh + REFERENCE layered.puml.h5) +endforeach() +pumgen_add_test(msh4.coarse.np2 RANKS 2 SOURCE msh4 INPUT coarse-v41.msh REFERENCE coarse.puml.h5) +pumgen_add_test(msh2.coarse-o2.np1 RANKS 1 SOURCE msh2 INPUT coarse-o2-v22.msh + REFERENCE coarse-o2.puml.h5 ARGS --order=2) +pumgen_add_test(msh4.tiny.np1 RANKS 1 SOURCE msh4 INPUT tiny-v41.msh REFERENCE tiny.puml.h5) +pumgen_add_test(msh2.tiny.np1 RANKS 1 SOURCE msh2 INPUT tiny-v22.msh REFERENCE tiny.puml.h5) diff --git a/tests/RunPumgenTest.cmake b/tests/RunPumgenTest.cmake new file mode 100644 index 0000000..19ec71a --- /dev/null +++ b/tests/RunPumgenTest.cmake @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2026 SeisSol Group +# +# SPDX-License-Identifier: BSD-3-Clause + +# Runs pumgen for one test case and checks the result. +# +# Variables: +# COMMAND pumgen command line, with '|' as the argument separator +# OUTPUT file written by pumgen +# REFERENCE (optional) reference PUML file the output has to match +# TOLERANCE (optional) absolute tolerance for the vertex coordinates +# H5DIFF h5diff executable, required together with REFERENCE +# EXPECT_ERROR (optional) pumgen has to fail with a message matching this regular expression + +string(REPLACE "|" ";" command "${COMMAND}") +file(REMOVE "${OUTPUT}") + +execute_process( + COMMAND ${command} + RESULT_VARIABLE result + OUTPUT_VARIABLE log + ERROR_VARIABLE log + TIMEOUT 100) +message("${log}") + +if(DEFINED EXPECT_ERROR) + if(result EQUAL 0) + message(FATAL_ERROR "pumgen succeeded, but was expected to fail") + endif() + if(NOT log MATCHES "${EXPECT_ERROR}") + message(FATAL_ERROR "pumgen failed without a message matching \"${EXPECT_ERROR}\"") + endif() + return() +endif() + +if(NOT result EQUAL 0) + message(FATAL_ERROR "pumgen failed: ${result}") +endif() + +if(DEFINED REFERENCE) + set(failures "") + foreach(dataset connect geometry group boundary) + set(options "") + if(dataset STREQUAL "geometry" AND DEFINED TOLERANCE) + set(options -d ${TOLERANCE}) + endif() + execute_process( + COMMAND ${H5DIFF} -n 10 ${options} ${OUTPUT} ${REFERENCE} /${dataset} /${dataset} + RESULT_VARIABLE differs + OUTPUT_VARIABLE difflog + ERROR_VARIABLE difflog) + if(NOT differs EQUAL 0) + string(APPEND failures "\n/${dataset} differs from the reference:\n${difflog}") + endif() + endforeach() + if(NOT failures STREQUAL "") + message(FATAL_ERROR "${failures}") + endif() +endif() diff --git a/tests/fixtures/coarse-binary-v41.msh b/tests/fixtures/coarse-binary-v41.msh new file mode 100644 index 0000000000000000000000000000000000000000..26acd2b7230057571f46a02ff4bfa97efd016989 GIT binary patch literal 27978 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 V-1*yrL|Fz~VK~#z$V8Ma` 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 L{>wA5!;JnvI11fu literal 0 HcmV?d00001 diff --git a/tests/fixtures/coarse-offset-v41.msh b/tests/fixtures/coarse-offset-v41.msh new file mode 100644 index 0000000..29d0fb3 --- /dev/null +++ b/tests/fixtures/coarse-offset-v41.msh @@ -0,0 +1,877 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +12 20 11 2 +1 0 0 -0.25 0 +2 0 0 0 0 +3 0 2 0 0 +4 0 2 -0.25 0 +5 2 0 -0.25 0 +6 2 0 0 0 +7 2 2 0 0 +8 2 2 -0.25 0 +9 0 0 -1 0 +10 0 2 -1 0 +11 2 0 -1 0 +12 2 2 -1 0 +1 -1e-07 -1e-07 -0.2500001 1e-07 1e-07 1.000000000028756e-07 0 2 1 -2 +2 -1e-07 -9.999999994736442e-08 -1e-07 1e-07 2.0000001 1e-07 0 2 2 -3 +3 -1e-07 1.9999999 -0.2500001 1e-07 2.0000001 1.000000000028756e-07 0 2 4 -3 +4 -1e-07 -9.999999994736442e-08 -0.2500001 1e-07 2.0000001 -0.2499999 0 2 1 -4 +5 1.9999999 -1e-07 -0.2500001 2.0000001 1e-07 1.000000000028756e-07 0 2 5 -6 +6 1.9999999 -9.999999994736442e-08 -1e-07 2.0000001 2.0000001 1e-07 0 2 6 -7 +7 1.9999999 1.9999999 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 0 2 8 -7 +8 1.9999999 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 -0.2499999 0 2 5 -8 +9 -9.999999994736442e-08 -1e-07 -0.2500001 2.0000001 1e-07 -0.2499999 0 2 1 -5 +10 -9.999999994736442e-08 -1e-07 -1e-07 2.0000001 1e-07 1e-07 0 2 2 -6 +11 -9.999999994736442e-08 1.9999999 -0.2500001 2.0000001 2.0000001 -0.2499999 0 2 4 -8 +12 -9.999999994736442e-08 1.9999999 -1e-07 2.0000001 2.0000001 1e-07 0 2 3 -7 +13 -1e-07 -1e-07 -1.0000001 1e-07 1e-07 -0.2499999 0 2 9 -1 +14 -1e-07 1.9999999 -1.0000001 1e-07 2.0000001 -0.2499999 0 2 10 -4 +15 -1e-07 -9.999999994736442e-08 -1.0000001 1e-07 2.0000001 -0.9999999000000001 0 2 9 -10 +16 1.9999999 -1e-07 -1.0000001 2.0000001 1e-07 -0.2499999 0 2 11 -5 +17 1.9999999 1.9999999 -1.0000001 2.0000001 2.0000001 -0.2499999 0 2 12 -8 +18 1.9999999 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.9999999000000001 0 2 11 -12 +19 -9.999999994736442e-08 -1e-07 -1.0000001 2.0000001 1e-07 -0.9999999000000001 0 2 9 -11 +20 -9.999999994736442e-08 1.9999999 -1.0000001 2.0000001 2.0000001 -0.9999999000000001 0 2 10 -12 +1 -1e-07 -9.999999994736442e-08 -0.2500001 1e-07 2.0000001 1.000000000028756e-07 1 105 4 -1 4 3 -2 +2 1.9999999 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 1 105 4 -5 8 7 -6 +3 -9.999999994736442e-08 -1e-07 -0.2500001 2.0000001 1e-07 1.000000000028756e-07 1 105 4 -9 1 10 -5 +4 -9.999999994736442e-08 1.9999999 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 1 105 4 -11 3 12 -7 +5 -9.999999994736442e-08 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 -0.2499999 1 103 4 -4 9 8 -11 +6 -9.999999994736442e-08 -9.999999994736442e-08 -1e-07 2.0000001 2.0000001 1e-07 1 101 4 -2 10 6 -12 +7 -1e-07 -9.999999994736442e-08 -1.0000001 1e-07 2.0000001 -0.2499999 1 105 4 -13 15 14 -4 +8 1.9999999 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.2499999 1 105 4 -16 18 17 -8 +9 -9.999999994736442e-08 -1e-07 -1.0000001 2.0000001 1e-07 -0.2499999 1 105 4 -19 13 9 -16 +10 -9.999999994736442e-08 1.9999999 -1.0000001 2.0000001 2.0000001 -0.2499999 1 105 4 -20 14 11 -17 +11 -9.999999994736442e-08 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.9999999000000001 1 105 4 -15 19 18 -20 +1 -9.999999994736442e-08 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 1 1 6 -1 2 -3 4 -5 6 +2 -9.999999994736442e-08 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.2499999 1 2 6 -7 8 -9 10 -11 5 +$EndEntities +$Nodes +41 109 1001 1109 +0 1 0 1 +1001 +0 0 -0.25 +0 2 0 1 +1002 +0 0 0 +0 3 0 1 +1003 +0 2 0 +0 4 0 1 +1004 +0 2 -0.25 +0 5 0 1 +1005 +2 0 -0.25 +0 6 0 1 +1006 +2 0 0 +0 7 0 1 +1007 +2 2 0 +0 8 0 1 +1008 +2 2 -0.25 +0 9 0 1 +1009 +0 0 -1 +0 10 0 1 +1010 +0 2 -1 +0 11 0 1 +1011 +2 0 -1 +0 12 0 1 +1012 +2 2 -1 +1 2 0 3 +1013 +1014 +1015 +0 0.5 0 +0 1 0 +0 1.5 0 +1 4 0 3 +1016 +1017 +1018 +0 0.5 -0.25 +0 1 -0.25 +0 1.5 -0.25 +1 6 0 3 +1019 +1020 +1021 +2 0.5 0 +2 1 0 +2 1.5 0 +1 8 0 3 +1022 +1023 +1024 +2 0.5 -0.25 +2 1 -0.25 +2 1.5 -0.25 +1 9 0 3 +1025 +1026 +1027 +0.5 0 -0.25 +1 0 -0.25 +1.5 0 -0.25 +1 10 0 3 +1028 +1029 +1030 +0.5 0 0 +1 0 0 +1.5 0 0 +1 11 0 3 +1031 +1032 +1033 +0.5 2 -0.25 +1 2 -0.25 +1.5 2 -0.25 +1 12 0 3 +1034 +1035 +1036 +0.5 2 0 +1 2 0 +1.5 2 0 +1 13 0 1 +1037 +0 0 -0.5863498029562912 +1 14 0 1 +1038 +0 2 -0.5863498029562912 +1 15 0 1 +1039 +0 1 -1 +1 16 0 1 +1040 +2 0 -0.5863498029562912 +1 17 0 1 +1041 +2 2 -0.5863498029562912 +1 18 0 1 +1042 +2 1 -1 +1 19 0 1 +1043 +1 0 -1 +1 20 0 1 +1044 +1 2 -1 +2 1 0 4 +1045 +1046 +1047 +1048 +0 1.75 -0.125 +0 1.25 -0.125 +0 0.25 -0.125 +0 0.75 -0.125 +2 2 0 4 +1049 +1050 +1051 +1052 +2 1.75 -0.125 +2 0.25 -0.125 +2 0.75 -0.125 +2 1.25 -0.125 +2 3 0 4 +1053 +1054 +1055 +1056 +1.75 0 -0.125 +0.25 0 -0.125 +0.75 0 -0.125 +1.25 0 -0.125 +2 4 0 4 +1057 +1058 +1059 +1060 +1.25 2 -0.125 +0.25 2 -0.125 +1.75 2 -0.125 +0.75 2 -0.125 +2 5 0 15 +1061 +1062 +1063 +1064 +1065 +1066 +1067 +1068 +1069 +1070 +1071 +1072 +1073 +1074 +1075 +1.578151847792411 1.230662432702593 -0.25 +0.7183803468335941 0.3953531183916653 -0.25 +0.4514095486548143 1.234183475777031 -0.25 +1.577292096379071 0.7509441482399639 -0.25 +1.19048230709083 0.9938634724449638 -0.25 +1.211526285167907 1.539080572974888 -0.25 +0.389289068198761 0.7180466052574374 -0.25 +1.213941721673849 0.4470568705367829 -0.25 +0.7375873251999121 1.636990986207155 -0.25 +0.8638678665980398 1.245262305856504 -0.25 +1.633974596215561 0.3660254037844386 -0.25 +1.633974596215561 1.633974596215561 -0.25 +0.3660254037844386 0.3660254037844387 -0.25 +0.3630158654516971 1.636984134548303 -0.25 +0.799919109919719 0.8127176335914889 -0.25 +2 6 0 14 +1076 +1077 +1078 +1079 +1080 +1081 +1082 +1083 +1084 +1085 +1086 +1087 +1088 +1089 +1.271813574233536 0.4264624335545749 0 +1.57791535636081 1.249332516611218 0 +0.7707992533266995 1.491026004178691 0 +0.3838980770800826 0.7512620920870399 0 +1.604157990548827 0.756865646419863 0 +1.138381937170266 0.9886994546006772 0 +1.25740778765016 1.566401986104808 0 +0.7720151968557594 0.504236995066732 0 +0.3774516866522022 1.228912045653614 0 +0.6885092302170019 0.9928273183173509 0 +1.633974596215561 1.633974596215561 0 +0.3660254037844387 0.3660254037844386 0 +1.633974596215561 0.3660254037844387 0 +0.3296501879957803 1.643987609966461 0 +2 7 0 4 +1090 +1091 +1092 +1093 +0 0.8250160980116606 -0.5458226013255596 +0 1.189987121590671 -0.5299871215906714 +0 0.3875026830019433 -0.6053620673803085 +0 1.614997853598445 -0.6027228207578272 +2 8 0 4 +1094 +1095 +1096 +1097 +2 0.8250160980116606 -0.5458226013255596 +2 1.189987121590671 -0.5299871215906714 +2 0.3875026830019433 -0.6053620673803085 +2 1.614997853598445 -0.6027228207578272 +2 9 0 4 +1098 +1099 +1100 +1101 +0.8250160980116606 0 -0.5458226013255596 +1.189987121590671 0 -0.5299871215906714 +0.3875026830019435 0 -0.6053620673803086 +1.614997853598445 0 -0.6027228207578272 +2 10 0 4 +1102 +1103 +1104 +1105 +0.8250160980116606 2 -0.5458226013255596 +1.189987121590671 2 -0.5299871215906714 +0.3875026830019435 2 -0.6053620673803086 +1.614997853598445 2 -0.6027228207578272 +2 11 0 4 +1106 +1107 +1108 +1109 +1.5 1.5 -1 +1.25 0.7499999999999999 -1 +0.5625 0.5625000000000002 -1 +0.6914062499999998 1.30859375 -1 +3 1 0 0 +3 2 0 0 +$EndNodes +$Elements +13 548 1 548 +2 1 2 16 +1 1001 1002 1047 +2 1016 1001 1047 +3 1002 1013 1047 +4 1003 1004 1045 +5 1015 1003 1045 +6 1004 1018 1045 +7 1013 1014 1048 +8 1013 1016 1047 +9 1016 1013 1048 +10 1014 1015 1046 +11 1017 1014 1046 +12 1014 1017 1048 +13 1018 1015 1045 +14 1015 1018 1046 +15 1017 1016 1048 +16 1018 1017 1046 +2 2 2 16 +17 1005 1050 1006 +18 1022 1050 1005 +19 1006 1050 1019 +20 1007 1049 1008 +21 1021 1049 1007 +22 1008 1049 1024 +23 1019 1051 1020 +24 1019 1050 1022 +25 1022 1051 1019 +26 1020 1052 1021 +27 1020 1051 1023 +28 1023 1052 1020 +29 1024 1049 1021 +30 1021 1052 1024 +31 1023 1051 1022 +32 1024 1052 1023 +2 3 2 16 +33 1002 1001 1054 +34 1001 1025 1054 +35 1028 1002 1054 +36 1005 1006 1053 +37 1027 1005 1053 +38 1006 1030 1053 +39 1025 1026 1055 +40 1025 1028 1054 +41 1028 1025 1055 +42 1026 1027 1056 +43 1026 1029 1055 +44 1029 1026 1056 +45 1030 1027 1053 +46 1027 1030 1056 +47 1029 1028 1055 +48 1030 1029 1056 +2 4 2 16 +49 1003 1058 1004 +50 1034 1058 1003 +51 1004 1058 1031 +52 1008 1059 1007 +53 1007 1059 1036 +54 1033 1059 1008 +55 1031 1060 1032 +56 1031 1058 1034 +57 1034 1060 1031 +58 1032 1057 1033 +59 1035 1057 1032 +60 1032 1060 1035 +61 1033 1057 1036 +62 1036 1059 1033 +63 1035 1060 1034 +64 1036 1057 1035 +2 5 2 44 +65 1001 1016 1073 +66 1025 1001 1073 +67 1018 1004 1074 +68 1004 1031 1074 +69 1022 1005 1071 +70 1005 1027 1071 +71 1008 1024 1072 +72 1033 1008 1072 +73 1016 1017 1067 +74 1016 1067 1073 +75 1017 1018 1063 +76 1017 1063 1067 +77 1063 1018 1074 +78 1023 1022 1064 +79 1064 1022 1071 +80 1024 1023 1061 +81 1061 1023 1064 +82 1024 1061 1072 +83 1026 1025 1062 +84 1062 1025 1073 +85 1027 1026 1068 +86 1026 1062 1068 +87 1027 1068 1071 +88 1031 1032 1069 +89 1031 1069 1074 +90 1032 1033 1066 +91 1032 1066 1069 +92 1066 1033 1072 +93 1061 1064 1065 +94 1061 1065 1066 +95 1061 1066 1072 +96 1067 1062 1073 +97 1062 1067 1075 +98 1068 1062 1075 +99 1067 1063 1075 +100 1063 1069 1070 +101 1069 1063 1074 +102 1063 1070 1075 +103 1065 1064 1068 +104 1068 1064 1071 +105 1066 1065 1070 +106 1065 1068 1075 +107 1070 1065 1075 +108 1069 1066 1070 +2 6 2 42 +109 1002 1087 1013 +110 1028 1087 1002 +111 1015 1089 1003 +112 1003 1089 1034 +113 1019 1088 1006 +114 1006 1088 1030 +115 1007 1086 1021 +116 1036 1086 1007 +117 1013 1079 1014 +118 1013 1087 1079 +119 1014 1084 1015 +120 1079 1084 1014 +121 1084 1089 1015 +122 1020 1080 1019 +123 1080 1088 1019 +124 1021 1077 1020 +125 1077 1080 1020 +126 1021 1086 1077 +127 1029 1083 1028 +128 1083 1087 1028 +129 1030 1076 1029 +130 1076 1083 1029 +131 1030 1088 1076 +132 1034 1078 1035 +133 1034 1089 1078 +134 1035 1082 1036 +135 1078 1082 1035 +136 1082 1086 1036 +137 1080 1081 1076 +138 1076 1088 1080 +139 1081 1083 1076 +140 1077 1081 1080 +141 1077 1082 1081 +142 1077 1086 1082 +143 1081 1082 1078 +144 1078 1085 1081 +145 1084 1085 1078 +146 1078 1089 1084 +147 1083 1085 1079 +148 1079 1087 1083 +149 1079 1085 1084 +150 1081 1085 1083 +2 7 2 16 +151 1001 1016 1092 +152 1037 1001 1092 +153 1018 1004 1093 +154 1004 1038 1093 +155 1009 1037 1092 +156 1039 1009 1092 +157 1038 1010 1093 +158 1010 1039 1093 +159 1016 1017 1090 +160 1016 1090 1092 +161 1017 1018 1091 +162 1090 1017 1091 +163 1091 1018 1093 +164 1039 1090 1091 +165 1090 1039 1092 +166 1039 1091 1093 +2 8 2 16 +167 1005 1096 1022 +168 1040 1096 1005 +169 1024 1097 1008 +170 1008 1097 1041 +171 1011 1096 1040 +172 1042 1096 1011 +173 1041 1097 1012 +174 1012 1097 1042 +175 1022 1094 1023 +176 1022 1096 1094 +177 1023 1095 1024 +178 1094 1095 1023 +179 1095 1097 1024 +180 1042 1095 1094 +181 1094 1096 1042 +182 1042 1097 1095 +2 9 2 16 +183 1025 1001 1100 +184 1001 1037 1100 +185 1005 1027 1101 +186 1040 1005 1101 +187 1037 1009 1100 +188 1009 1043 1100 +189 1011 1040 1101 +190 1043 1011 1101 +191 1026 1025 1098 +192 1098 1025 1100 +193 1027 1026 1099 +194 1026 1098 1099 +195 1027 1099 1101 +196 1098 1043 1099 +197 1043 1098 1100 +198 1099 1043 1101 +2 10 2 16 +199 1031 1104 1004 +200 1004 1104 1038 +201 1008 1105 1033 +202 1041 1105 1008 +203 1038 1104 1010 +204 1010 1104 1044 +205 1012 1105 1041 +206 1044 1105 1012 +207 1032 1102 1031 +208 1102 1104 1031 +209 1033 1103 1032 +210 1032 1103 1102 +211 1033 1105 1103 +212 1102 1103 1044 +213 1044 1104 1102 +214 1103 1105 1044 +2 11 2 14 +215 1009 1039 1108 +216 1043 1009 1108 +217 1039 1010 1109 +218 1010 1044 1109 +219 1042 1011 1107 +220 1011 1043 1107 +221 1012 1042 1106 +222 1044 1012 1106 +223 1108 1039 1109 +224 1106 1042 1107 +225 1107 1043 1108 +226 1044 1106 1109 +227 1106 1107 1109 +228 1107 1108 1109 +3 1 4 180 +229 1022 1023 1064 1051 +230 1023 1024 1061 1052 +231 1020 1021 1052 1077 +232 1048 1014 1013 1079 +233 1026 1025 1055 1062 +234 1033 1032 1066 1057 +235 1018 1017 1063 1046 +236 1015 1046 1084 1014 +237 1026 1027 1068 1056 +238 1051 1080 1019 1020 +239 1017 1016 1067 1048 +240 1003 1045 1089 1015 +241 1002 1013 1087 1047 +242 1030 1006 1053 1088 +243 1006 1050 1088 1019 +244 1086 1007 1021 1049 +245 1086 1036 1007 1059 +246 1032 1031 1069 1060 +247 1024 1008 1072 1049 +248 1005 1022 1071 1050 +249 1025 1001 1054 1073 +250 1001 1016 1047 1073 +251 1031 1004 1074 1058 +252 1083 1028 1055 1029 +253 1078 1060 1034 1035 +254 1004 1018 1074 1045 +255 1005 1027 1053 1071 +256 1076 1030 1029 1056 +257 1008 1033 1072 1059 +258 1057 1035 1082 1036 +259 1089 1058 1003 1034 +260 1054 1087 1002 1028 +261 1049 1086 1007 1059 +262 1054 1002 1087 1047 +263 1053 1006 1050 1088 +264 1059 1008 1049 1072 +265 1047 1054 1001 1073 +266 1050 1071 1005 1053 +267 1058 1004 1074 1045 +268 1045 1058 1003 1089 +269 1021 1052 1077 1024 +270 1077 1052 1061 1024 +271 1061 1052 1020 1023 +272 1079 1017 1084 1067 +273 1029 1055 1062 1026 +274 1077 1052 1020 1061 +275 1046 1084 1018 1015 +276 1080 1022 1064 1051 +277 1080 1019 1022 1051 +278 1032 1057 1035 1066 +279 1017 1084 1014 1079 +280 1084 1018 1063 1046 +281 1048 1017 1014 1079 +282 1051 1064 1080 1023 +283 1061 1020 1080 1023 +284 1080 1064 1061 1023 +285 1079 1048 1017 1067 +286 1068 1029 1062 1026 +287 1020 1051 1080 1023 +288 1068 1056 1029 1026 +289 1017 1063 1084 1067 +290 1077 1020 1080 1061 +291 1076 1068 1065 1064 +292 1083 1055 1062 1029 +293 1028 1055 1062 1083 +294 1060 1034 1069 1078 +295 1082 1057 1036 1033 +296 1070 1078 1085 1081 +297 1080 1064 1065 1061 +298 1070 1078 1081 1082 +299 1082 1057 1033 1066 +300 1084 1063 1017 1046 +301 1077 1080 1065 1061 +302 1032 1035 1069 1066 +303 1076 1030 1056 1027 +304 1087 1054 1047 1073 +305 1053 1050 1071 1088 +306 1059 1049 1086 1072 +307 1013 1048 1079 1067 +308 1074 1058 1045 1089 +309 1069 1060 1078 1035 +310 1017 1084 1046 1014 +311 1082 1035 1057 1066 +312 1068 1076 1056 1027 +313 1048 1013 1016 1067 +314 1029 1062 1083 1068 +315 1056 1029 1076 1068 +316 1069 1060 1035 1032 +317 1035 1082 1069 1066 +318 1062 1025 1055 1028 +319 1072 1021 1077 1024 +320 1061 1072 1077 1024 +321 1084 1089 1018 1015 +322 1089 1018 1074 1084 +323 1082 1069 1078 1035 +324 1080 1065 1081 1077 +325 1021 1072 1077 1086 +326 1063 1067 1075 1085 +327 1083 1076 1029 1068 +328 1034 1069 1031 1060 +329 1075 1085 1083 1081 +330 1070 1063 1075 1085 +331 1071 1022 1064 1080 +332 1022 1088 1019 1080 +333 1018 1063 1074 1084 +334 1022 1071 1088 1080 +335 1082 1078 1069 1066 +336 1034 1089 1069 1078 +337 1086 1082 1036 1072 +338 1083 1062 1075 1068 +339 1072 1082 1036 1033 +340 1082 1070 1078 1066 +341 1028 1062 1087 1083 +342 1030 1071 1076 1088 +343 1062 1087 1073 1025 +344 1087 1013 1079 1067 +345 1088 1050 1022 1019 +346 1073 1047 1013 1016 +347 1071 1030 1076 1027 +348 1072 1049 1021 1024 +349 1013 1073 1016 1067 +350 1033 1072 1082 1066 +351 1069 1074 1089 1034 +352 1077 1082 1072 1061 +353 1087 1073 1067 1062 +354 1053 1071 1030 1088 +355 1059 1086 1036 1072 +356 1047 1013 1087 1073 +357 1087 1054 1073 1025 +358 1049 1021 1086 1072 +359 1022 1071 1050 1088 +360 1013 1087 1073 1067 +361 1027 1068 1076 1071 +362 1063 1070 1078 1085 +363 1030 1027 1071 1053 +364 1025 1062 1087 1028 +365 1084 1063 1078 1085 +366 1066 1072 1082 1061 +367 1072 1036 1059 1033 +368 1074 1069 1031 1034 +369 1070 1069 1078 1066 +370 1077 1082 1086 1072 +371 1074 1058 1089 1034 +372 1079 1087 1067 1083 +373 1087 1054 1025 1028 +374 1062 1067 1087 1083 +375 1085 1079 1075 1083 +376 1058 1074 1031 1034 +377 1063 1069 1074 1078 +378 1064 1076 1068 1071 +379 1045 1018 1074 1089 +380 1089 1045 1018 1015 +381 1063 1070 1069 1078 +382 1075 1079 1067 1083 +383 1085 1075 1070 1081 +384 1062 1075 1067 1083 +385 1075 1067 1079 1085 +386 1089 1074 1069 1078 +387 1081 1075 1070 1065 +388 1058 1003 1004 1045 +389 1007 1008 1049 1059 +390 1050 1005 1006 1053 +391 1054 1001 1002 1047 +392 1084 1067 1085 1079 +393 1085 1067 1084 1063 +394 1076 1065 1075 1081 +395 1075 1065 1076 1068 +396 1075 1083 1076 1081 +397 1076 1083 1075 1068 +398 1065 1080 1076 1064 +399 1076 1080 1065 1081 +400 1074 1084 1078 1089 +401 1078 1084 1074 1063 +402 1061 1082 1065 1077 +403 1061 1065 1082 1066 +404 1081 1065 1082 1077 +405 1071 1080 1076 1088 +406 1076 1080 1071 1064 +407 1065 1082 1070 1081 +408 1065 1070 1082 1066 +3 2 4 140 +409 1066 1065 1070 1106 +410 1090 1109 1063 1067 +411 1066 1109 1102 1103 +412 1061 1065 1066 1106 +413 1109 1070 1075 1065 +414 1094 1061 1095 1107 +415 1090 1109 1091 1063 +416 1064 1061 1094 1107 +417 1098 1062 1068 1107 +418 1109 1102 1069 1066 +419 1067 1109 1063 1075 +420 1069 1070 1109 1066 +421 1068 1098 1107 1099 +422 1065 1109 1070 1106 +423 1066 1070 1109 1106 +424 1068 1062 1075 1107 +425 1109 1090 1108 1067 +426 1061 1064 1065 1107 +427 1070 1063 1109 1075 +428 1103 1066 1109 1106 +429 1062 1098 1108 1107 +430 1075 1067 1108 1062 +431 1108 1109 1067 1075 +432 1071 1068 1064 1107 +433 1067 1073 1108 1062 +434 1106 1095 1061 1107 +435 1061 1066 1072 1106 +436 1109 1063 1069 1074 +437 1023 1064 1061 1094 +438 1107 1064 1071 1096 +439 1107 1094 1064 1096 +440 1070 1109 1063 1069 +441 1108 1075 1062 1107 +442 1017 1090 1091 1063 +443 1106 1061 1065 1107 +444 1095 1023 1061 1094 +445 1063 1017 1090 1067 +446 1032 1069 1102 1066 +447 1107 1071 1068 1101 +448 1109 1044 1102 1103 +449 1069 1104 1109 1074 +450 1102 1104 1109 1069 +451 1102 1032 1066 1103 +452 1062 1098 1068 1026 +453 1099 1107 1068 1101 +454 1026 1098 1068 1099 +455 1100 1098 1108 1062 +456 1065 1068 1075 1107 +457 1092 1108 1090 1067 +458 1109 1039 1090 1091 +459 1095 1042 1094 1107 +460 1073 1100 1108 1062 +461 1068 1065 1064 1107 +462 1108 1092 1073 1067 +463 1097 1095 1061 1106 +464 1066 1103 1105 1106 +465 1097 1061 1072 1106 +466 1101 1107 1071 1096 +467 1043 1107 1098 1099 +468 1105 1072 1066 1106 +469 1065 1075 1109 1107 +470 1091 1018 1063 1074 +471 1104 1093 1109 1074 +472 1106 1065 1109 1107 +473 1109 1044 1103 1106 +474 1039 1090 1108 1109 +475 1022 1064 1094 1096 +476 1064 1022 1071 1096 +477 1095 1106 1042 1107 +478 1108 1109 1075 1107 +479 1101 1011 1107 1096 +480 1098 1108 1107 1043 +481 1108 1092 1100 1073 +482 1107 1011 1042 1096 +483 1043 1011 1107 1101 +484 1018 1091 1093 1074 +485 1044 1010 1109 1104 +486 1109 1010 1039 1093 +487 1023 1022 1064 1094 +488 1042 1094 1107 1096 +489 1108 1009 1092 1039 +490 1100 1009 1108 1043 +491 1099 1043 1107 1101 +492 1100 1098 1062 1025 +493 1032 1033 1066 1103 +494 1024 1023 1061 1095 +495 1106 1012 1097 1042 +496 1105 1044 1012 1106 +497 1093 1109 1010 1104 +498 1017 1018 1063 1091 +499 1109 1044 1104 1102 +500 1026 1025 1062 1098 +501 1031 1104 1102 1069 +502 1016 1092 1090 1067 +503 1100 1037 1001 1073 +504 1037 1092 1001 1073 +505 1040 1071 1005 1096 +506 1027 1026 1068 1099 +507 1008 1072 1041 1097 +508 1105 1008 1072 1041 +509 1101 1071 1005 1040 +510 1039 1109 1093 1091 +511 1104 1004 1038 1074 +512 1062 1073 1100 1025 +513 1004 1093 1038 1074 +514 1069 1031 1104 1074 +515 1072 1105 1097 1106 +516 1016 1017 1067 1090 +517 1016 1073 1092 1067 +518 1090 1108 1092 1039 +519 1108 1098 1100 1043 +520 1032 1031 1102 1069 +521 1025 1001 1073 1100 +522 1005 1022 1096 1071 +523 1001 1016 1073 1092 +524 1004 1031 1074 1104 +525 1106 1097 1095 1042 +526 1105 1103 1044 1106 +527 1008 1024 1072 1097 +528 1092 1037 1100 1073 +529 1040 1101 1071 1096 +530 1104 1038 1093 1074 +531 1041 1072 1105 1097 +532 1018 1004 1074 1093 +533 1005 1027 1071 1101 +534 1033 1008 1072 1105 +535 1100 1009 1092 1108 +536 1037 1009 1092 1100 +537 1097 1105 1012 1106 +538 1038 1093 1010 1104 +539 1041 1105 1012 1097 +540 1040 1011 1101 1096 +541 1066 1105 1033 1072 +542 1033 1105 1066 1103 +543 1068 1101 1027 1099 +544 1027 1101 1068 1071 +545 1074 1091 1109 1063 +546 1074 1109 1091 1093 +547 1061 1097 1024 1095 +548 1024 1097 1061 1072 +$EndElements diff --git a/tests/fixtures/coarse-partitioned-v41.msh b/tests/fixtures/coarse-partitioned-v41.msh new file mode 100644 index 0000000..72e68b4 --- /dev/null +++ b/tests/fixtures/coarse-partitioned-v41.msh @@ -0,0 +1,1000 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +12 20 11 2 +1 0 0 -0.25 0 +2 0 0 0 0 +3 0 2 0 0 +4 0 2 -0.25 0 +5 2 0 -0.25 0 +6 2 0 0 0 +7 2 2 0 0 +8 2 2 -0.25 0 +9 0 0 -1 0 +10 0 2 -1 0 +11 2 0 -1 0 +12 2 2 -1 0 +1 -1e-07 -1e-07 -0.2500001 1e-07 1e-07 1.000000000028756e-07 0 2 1 -2 +2 -1e-07 -9.999999994736442e-08 -1e-07 1e-07 2.0000001 1e-07 0 2 2 -3 +3 -1e-07 1.9999999 -0.2500001 1e-07 2.0000001 1.000000000028756e-07 0 2 4 -3 +4 -1e-07 -9.999999994736442e-08 -0.2500001 1e-07 2.0000001 -0.2499999 0 2 1 -4 +5 1.9999999 -1e-07 -0.2500001 2.0000001 1e-07 1.000000000028756e-07 0 2 5 -6 +6 1.9999999 -9.999999994736442e-08 -1e-07 2.0000001 2.0000001 1e-07 0 2 6 -7 +7 1.9999999 1.9999999 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 0 2 8 -7 +8 1.9999999 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 -0.2499999 0 2 5 -8 +9 -9.999999994736442e-08 -1e-07 -0.2500001 2.0000001 1e-07 -0.2499999 0 2 1 -5 +10 -9.999999994736442e-08 -1e-07 -1e-07 2.0000001 1e-07 1e-07 0 2 2 -6 +11 -9.999999994736442e-08 1.9999999 -0.2500001 2.0000001 2.0000001 -0.2499999 0 2 4 -8 +12 -9.999999994736442e-08 1.9999999 -1e-07 2.0000001 2.0000001 1e-07 0 2 3 -7 +13 -1e-07 -1e-07 -1.0000001 1e-07 1e-07 -0.2499999 0 2 9 -1 +14 -1e-07 1.9999999 -1.0000001 1e-07 2.0000001 -0.2499999 0 2 10 -4 +15 -1e-07 -9.999999994736442e-08 -1.0000001 1e-07 2.0000001 -0.9999999000000001 0 2 9 -10 +16 1.9999999 -1e-07 -1.0000001 2.0000001 1e-07 -0.2499999 0 2 11 -5 +17 1.9999999 1.9999999 -1.0000001 2.0000001 2.0000001 -0.2499999 0 2 12 -8 +18 1.9999999 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.9999999000000001 0 2 11 -12 +19 -9.999999994736442e-08 -1e-07 -1.0000001 2.0000001 1e-07 -0.9999999000000001 0 2 9 -11 +20 -9.999999994736442e-08 1.9999999 -1.0000001 2.0000001 2.0000001 -0.9999999000000001 0 2 10 -12 +1 -1e-07 -9.999999994736442e-08 -0.2500001 1e-07 2.0000001 1.000000000028756e-07 1 105 4 -1 4 3 -2 +2 1.9999999 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 1 105 4 -5 8 7 -6 +3 -9.999999994736442e-08 -1e-07 -0.2500001 2.0000001 1e-07 1.000000000028756e-07 1 105 4 -9 1 10 -5 +4 -9.999999994736442e-08 1.9999999 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 1 105 4 -11 3 12 -7 +5 -9.999999994736442e-08 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 -0.2499999 1 103 4 -4 9 8 -11 +6 -9.999999994736442e-08 -9.999999994736442e-08 -1e-07 2.0000001 2.0000001 1e-07 1 101 4 -2 10 6 -12 +7 -1e-07 -9.999999994736442e-08 -1.0000001 1e-07 2.0000001 -0.2499999 1 105 4 -13 15 14 -4 +8 1.9999999 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.2499999 1 105 4 -16 18 17 -8 +9 -9.999999994736442e-08 -1e-07 -1.0000001 2.0000001 1e-07 -0.2499999 1 105 4 -19 13 9 -16 +10 -9.999999994736442e-08 1.9999999 -1.0000001 2.0000001 2.0000001 -0.2499999 1 105 4 -20 14 11 -17 +11 -9.999999994736442e-08 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.9999999000000001 1 105 4 -15 19 18 -20 +1 -9.999999994736442e-08 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 1 1 6 -1 2 -3 4 -5 6 +2 -9.999999994736442e-08 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.2499999 1 2 6 -7 8 -9 10 -11 5 +$EndEntities +$PartitionedEntities +2 +0 +18 33 20 4 +13 0 1 1 2 0 0 -0.25 0 +14 0 2 1 2 0 0 0 0 +15 0 3 1 2 0 2 0 0 +16 0 4 1 2 0 2 -0.25 0 +17 0 5 1 1 2 0 -0.25 0 +18 0 6 1 1 2 0 0 0 +19 0 7 1 1 2 2 0 0 +20 0 8 1 1 2 2 -0.25 0 +21 0 9 1 2 0 0 -1 0 +22 0 10 1 2 0 2 -1 0 +23 0 11 1 1 2 0 -1 0 +24 0 12 1 1 2 2 -1 0 +25 1 20 2 1 2 1 2 -1 0 +26 1 19 2 1 2 1 0 -1 0 +27 1 12 2 1 2 1.5 2 0 0 +28 1 11 2 1 2 1.5 2 -0.25 0 +29 1 9 2 1 2 0.5 0 -0.25 0 +30 1 10 2 1 2 0.5 0 0 0 +21 1 1 1 2 0 0 -0.25 0 0 0 0 2 13 -14 +22 1 2 1 2 0 0 0 0 2 0 0 2 14 -15 +23 1 3 1 2 0 2 -0.25 0 2 0 0 2 16 -15 +24 1 4 1 2 0 0 -0.25 0 2 -0.25 0 2 13 -16 +25 1 5 1 1 2 0 -0.25 2 0 0 0 2 17 -18 +26 1 6 1 1 2 0 0 2 2 0 0 2 18 -19 +27 1 7 1 1 2 2 -0.25 2 2 0 0 2 20 -19 +28 1 8 1 1 2 0 -0.25 2 2 -0.25 0 2 17 -20 +29 1 9 1 2 0 0 -0.25 0.5 0 -0.25 0 2 13 -29 +30 1 9 1 1 0.5 0 -0.25 2 0 -0.25 0 2 29 -17 +31 1 10 1 2 0 0 0 0.5 0 0 0 2 14 -30 +32 1 10 1 1 0.5 0 0 2 0 0 0 2 30 -18 +33 1 11 1 2 0 2 -0.25 1.5 2 -0.25 0 2 16 -28 +34 1 11 1 1 1.5 2 -0.25 2 2 -0.25 0 2 28 -20 +35 1 12 1 2 0 2 0 1.5 2 0 0 2 15 -27 +36 1 12 1 1 1.5 2 0 2 2 0 0 2 27 -19 +37 1 13 1 2 0 0 -1 0 0 -0.25 0 2 21 -13 +38 1 14 1 2 0 2 -1 0 2 -0.25 0 2 22 -16 +39 1 15 1 2 0 0 -1 0 2 -1 0 2 21 -22 +40 1 16 1 1 2 0 -1 2 0 -0.25 0 2 23 -17 +41 1 17 1 1 2 2 -1 2 2 -0.25 0 2 24 -20 +42 1 18 1 1 2 0 -1 2 2 -1 0 2 23 -24 +43 1 19 1 2 0 0 -1 1 0 -1 0 2 21 -26 +44 1 19 1 1 1 0 -1 2 0 -1 0 2 26 -23 +45 1 20 1 2 0 2 -1 1 2 -1 0 2 22 -25 +46 1 20 1 1 1 2 -1 2 2 -1 0 2 25 -24 +47 2 11 2 1 2 0.5625 0 -1 1.5 2 -1 0 2 26 -25 +48 2 5 2 1 2 0.5 0 -0.25 1.5 2 -0.25 0 2 29 -28 +49 2 9 2 1 2 0.3875026830019435 0 -1 1 0 -0.25 0 2 29 -26 +50 2 4 2 1 2 1.5 2 -0.25 1.5 2 0 0 2 28 -27 +51 2 3 2 1 2 0.5 0 -0.25 0.5 0 0 0 2 30 -29 +52 2 6 2 1 2 0.5 0 0 1.5 2 0 0 2 27 -30 +53 2 10 2 1 2 1 2 -1 1.5 2 -0.25 0 2 25 -28 +12 2 1 1 2 0 0 -0.25 0 2 0 1 105 4 -23 -24 21 22 +13 2 2 1 1 2 0 -0.25 2 2 0 1 105 4 -25 -26 27 28 +14 2 3 1 2 0 0 -0.25 0.5 0 0 1 105 4 -21 -31 29 -51 +15 2 3 1 1 0.5 0 -0.25 2 0 0 1 105 4 -32 25 30 51 +16 2 4 1 2 0 2 -0.25 1.5 2 0 1 105 4 -33 23 35 -50 +17 2 4 1 1 1.5 2 -0.25 2 2 0 1 105 4 -27 -34 36 50 +18 2 5 1 2 0 0 -0.25 1.5 2 -0.25 1 103 4 -29 24 33 -48 +19 2 5 1 1 0.5 0 -0.25 2 2 -0.25 1 103 4 -28 -30 34 48 +20 2 6 1 2 0 0 0 1.5 2 0 1 101 4 -22 -35 31 -52 +21 2 6 1 1 0.5 0 0 2 2 0 1 101 4 -36 26 32 52 +22 2 7 1 2 0 0 -1 0 2 -0.25 1 105 4 -38 -39 24 37 +23 2 8 1 1 2 0 -1 2 2 -0.25 1 105 4 -28 -40 41 42 +24 2 9 1 2 0 0 -1 1 0 -0.25 1 105 4 -29 -37 43 -49 +25 2 9 1 1 0.3875026830019435 0 -1 2 0 -0.25 1 105 4 -30 40 44 49 +26 2 10 1 2 0 2 -1 1.5 2 -0.25 1 105 4 -45 33 38 -53 +27 2 10 1 1 1 2 -1 2 2 -0.25 1 105 4 -41 -46 34 53 +28 2 11 1 2 0 0 -1 1 2 -1 1 105 3 -43 39 45 +29 2 11 1 1 0.5625 0 -1 2 2 -1 1 105 4 -42 -44 46 47 +30 3 2 2 1 2 0.3875026830019435 0 -1 1.5 2 -0.25 0 4 -47 48 -49 -53 +31 3 1 2 1 2 0.5 0 -0.25 1.5 2 0 0 4 -48 -50 -51 -52 +3 3 1 1 1 0.5 0 -0.25 2 2 0 1 1 6 -13 -15 -17 -19 -21 31 +4 3 1 1 2 0 0 -0.25 1.5 2 0 1 1 6 -12 -14 -16 -18 -20 -31 +5 3 2 1 2 0 0 -1 1.5 2 -0.25 1 2 6 -18 -22 -24 -26 -28 -30 +6 3 2 1 1 0.3875026830019435 0 -1 2 2 -0.25 1 2 6 -19 -23 -27 25 29 30 +$EndPartitionedEntities +$Nodes +75 109 1 109 +0 13 0 1 +1 +0 0 -0.25 +0 14 0 1 +2 +0 0 0 +0 15 0 1 +3 +0 2 0 +0 16 0 1 +4 +0 2 -0.25 +0 17 0 1 +5 +2 0 -0.25 +0 18 0 1 +6 +2 0 0 +0 19 0 1 +7 +2 2 0 +0 20 0 1 +8 +2 2 -0.25 +0 21 0 1 +9 +0 0 -1 +0 22 0 1 +10 +0 2 -1 +0 23 0 1 +11 +2 0 -1 +0 24 0 1 +12 +2 2 -1 +0 25 0 1 +44 +1 2 -1 +0 26 0 1 +43 +1 0 -1 +0 27 0 1 +36 +1.5 2 0 +0 28 0 1 +33 +1.5 2 -0.25 +0 29 0 1 +25 +0.5 0 -0.25 +0 30 0 1 +28 +0.5 0 0 +1 21 0 0 +1 22 0 3 +13 +14 +15 +0 0.5 0 +0 1 0 +0 1.5 0 +1 23 0 0 +1 24 0 3 +16 +17 +18 +0 0.5 -0.25 +0 1 -0.25 +0 1.5 -0.25 +1 25 0 0 +1 26 0 3 +19 +20 +21 +2 0.5 0 +2 1 0 +2 1.5 0 +1 27 0 0 +1 28 0 3 +22 +23 +24 +2 0.5 -0.25 +2 1 -0.25 +2 1.5 -0.25 +1 29 0 0 +1 30 0 2 +26 +27 +1 0 -0.25 +1.5 0 -0.25 +1 31 0 0 +1 32 0 2 +29 +30 +1 0 0 +1.5 0 0 +1 33 0 2 +31 +32 +0.5 2 -0.25 +1 2 -0.25 +1 34 0 0 +1 35 0 2 +34 +35 +0.5 2 0 +1 2 0 +1 36 0 0 +1 37 0 1 +37 +0 0 -0.5863498029562912 +1 38 0 1 +38 +0 2 -0.5863498029562912 +1 39 0 1 +39 +0 1 -1 +1 40 0 1 +40 +2 0 -0.5863498029562912 +1 41 0 1 +41 +2 2 -0.5863498029562912 +1 42 0 1 +42 +2 1 -1 +1 43 0 0 +1 44 0 0 +1 45 0 0 +1 46 0 0 +1 47 0 3 +106 +109 +108 +1.5 1.5 -1 +0.6914062499999998 1.30859375 -1 +0.5625 0.5625000000000002 -1 +1 48 0 4 +75 +65 +66 +62 +0.799919109919719 0.8127176335914889 -0.25 +1.19048230709083 0.9938634724449638 -0.25 +1.211526285167907 1.539080572974888 -0.25 +0.7183803468335941 0.3953531183916653 -0.25 +1 49 0 1 +100 +0.3875026830019435 0 -0.6053620673803086 +1 50 0 0 +1 51 0 0 +1 52 0 3 +83 +82 +81 +0.7720151968557594 0.504236995066732 0 +1.25740778765016 1.566401986104808 0 +1.138381937170266 0.9886994546006772 0 +1 53 0 1 +103 +1.189987121590671 2 -0.5299871215906714 +2 12 0 4 +47 +45 +48 +46 +0 0.25 -0.125 +0 1.75 -0.125 +0 0.75 -0.125 +0 1.25 -0.125 +2 13 0 4 +50 +49 +51 +52 +2 0.25 -0.125 +2 1.75 -0.125 +2 0.75 -0.125 +2 1.25 -0.125 +2 14 0 1 +54 +0.25 0 -0.125 +2 15 0 3 +53 +55 +56 +1.75 0 -0.125 +0.75 0 -0.125 +1.25 0 -0.125 +2 16 0 3 +58 +60 +57 +0.25 2 -0.125 +0.75 2 -0.125 +1.25 2 -0.125 +2 17 0 1 +59 +1.75 2 -0.125 +2 18 0 6 +73 +74 +67 +63 +69 +70 +0.3660254037844386 0.3660254037844387 -0.25 +0.3630158654516971 1.636984134548303 -0.25 +0.389289068198761 0.7180466052574374 -0.25 +0.4514095486548143 1.234183475777031 -0.25 +0.7375873251999121 1.636990986207155 -0.25 +0.8638678665980398 1.245262305856504 -0.25 +2 19 0 5 +71 +72 +64 +61 +68 +1.633974596215561 0.3660254037844386 -0.25 +1.633974596215561 1.633974596215561 -0.25 +1.577292096379071 0.7509441482399639 -0.25 +1.578151847792411 1.230662432702593 -0.25 +1.213941721673849 0.4470568705367829 -0.25 +2 20 0 6 +87 +89 +79 +84 +78 +85 +0.3660254037844387 0.3660254037844386 0 +0.3296501879957803 1.643987609966461 0 +0.3838980770800826 0.7512620920870399 0 +0.3774516866522022 1.228912045653614 0 +0.7707992533266995 1.491026004178691 0 +0.6885092302170019 0.9928273183173509 0 +2 21 0 5 +88 +86 +80 +77 +76 +1.633974596215561 0.3660254037844387 0 +1.633974596215561 1.633974596215561 0 +1.604157990548827 0.756865646419863 0 +1.57791535636081 1.249332516611218 0 +1.271813574233536 0.4264624335545749 0 +2 22 0 4 +92 +93 +90 +91 +0 0.3875026830019433 -0.6053620673803085 +0 1.614997853598445 -0.6027228207578272 +0 0.8250160980116606 -0.5458226013255596 +0 1.189987121590671 -0.5299871215906714 +2 23 0 4 +96 +97 +94 +95 +2 0.3875026830019433 -0.6053620673803085 +2 1.614997853598445 -0.6027228207578272 +2 0.8250160980116606 -0.5458226013255596 +2 1.189987121590671 -0.5299871215906714 +2 24 0 0 +2 25 0 3 +101 +98 +99 +1.614997853598445 0 -0.6027228207578272 +0.8250160980116606 0 -0.5458226013255596 +1.189987121590671 0 -0.5299871215906714 +2 26 0 2 +104 +102 +0.3875026830019435 2 -0.6053620673803086 +0.8250160980116606 2 -0.5458226013255596 +2 27 0 1 +105 +1.614997853598445 2 -0.6027228207578272 +2 28 0 0 +2 29 0 1 +107 +1.25 0.7499999999999999 -1 +2 30 0 0 +2 31 0 0 +3 3 0 0 +3 4 0 0 +3 5 0 0 +3 6 0 0 +$EndNodes +$Elements +22 548 1 548 +2 12 2 16 +1 1 2 47 +2 16 1 47 +3 2 13 47 +4 3 4 45 +5 15 3 45 +6 4 18 45 +7 13 14 48 +8 13 16 47 +9 16 13 48 +10 14 15 46 +11 17 14 46 +12 14 17 48 +13 18 15 45 +14 15 18 46 +15 17 16 48 +16 18 17 46 +2 13 2 16 +17 5 50 6 +18 22 50 5 +19 6 50 19 +20 7 49 8 +21 21 49 7 +22 8 49 24 +23 19 51 20 +24 19 50 22 +25 22 51 19 +26 20 52 21 +27 20 51 23 +28 23 52 20 +29 24 49 21 +30 21 52 24 +31 23 51 22 +32 24 52 23 +2 14 2 4 +33 2 1 54 +34 1 25 54 +35 28 2 54 +40 25 28 54 +2 15 2 12 +36 5 6 53 +37 27 5 53 +38 6 30 53 +39 25 26 55 +41 28 25 55 +42 26 27 56 +43 26 29 55 +44 29 26 56 +45 30 27 53 +46 27 30 56 +47 29 28 55 +48 30 29 56 +2 16 2 12 +49 3 58 4 +50 34 58 3 +51 4 58 31 +55 31 60 32 +56 31 58 34 +57 34 60 31 +58 32 57 33 +59 35 57 32 +60 32 60 35 +61 33 57 36 +63 35 60 34 +64 36 57 35 +2 17 2 4 +52 8 59 7 +53 7 59 36 +54 33 59 8 +62 36 59 33 +2 18 2 23 +65 1 16 73 +66 25 1 73 +67 18 4 74 +68 4 31 74 +73 16 17 67 +74 16 67 73 +75 17 18 63 +76 17 63 67 +77 63 18 74 +84 62 25 73 +88 31 32 69 +89 31 69 74 +90 32 33 66 +91 32 66 69 +96 67 62 73 +97 62 67 75 +99 67 63 75 +100 63 69 70 +101 69 63 74 +102 63 70 75 +105 66 65 70 +107 70 65 75 +108 69 66 70 +2 19 2 21 +69 22 5 71 +70 5 27 71 +71 8 24 72 +72 33 8 72 +78 23 22 64 +79 64 22 71 +80 24 23 61 +81 61 23 64 +82 24 61 72 +83 26 25 62 +85 27 26 68 +86 26 62 68 +87 27 68 71 +92 66 33 72 +93 61 64 65 +94 61 65 66 +95 61 66 72 +98 68 62 75 +103 65 64 68 +104 68 64 71 +106 65 68 75 +2 20 2 22 +109 2 87 13 +110 28 87 2 +111 15 89 3 +112 3 89 34 +117 13 79 14 +118 13 87 79 +119 14 84 15 +120 79 84 14 +121 84 89 15 +128 83 87 28 +132 34 78 35 +133 34 89 78 +134 35 82 36 +135 78 82 35 +143 81 82 78 +144 78 85 81 +145 84 85 78 +146 78 89 84 +147 83 85 79 +148 79 87 83 +149 79 85 84 +150 81 85 83 +2 21 2 20 +113 19 88 6 +114 6 88 30 +115 7 86 21 +116 36 86 7 +122 20 80 19 +123 80 88 19 +124 21 77 20 +125 77 80 20 +126 21 86 77 +127 29 83 28 +129 30 76 29 +130 76 83 29 +131 30 88 76 +136 82 86 36 +137 80 81 76 +138 76 88 80 +139 81 83 76 +140 77 81 80 +141 77 82 81 +142 77 86 82 +2 22 2 16 +151 1 16 92 +152 37 1 92 +153 18 4 93 +154 4 38 93 +155 9 37 92 +156 39 9 92 +157 38 10 93 +158 10 39 93 +159 16 17 90 +160 16 90 92 +161 17 18 91 +162 90 17 91 +163 91 18 93 +164 39 90 91 +165 90 39 92 +166 39 91 93 +2 23 2 16 +167 5 96 22 +168 40 96 5 +169 24 97 8 +170 8 97 41 +171 11 96 40 +172 42 96 11 +173 41 97 12 +174 12 97 42 +175 22 94 23 +176 22 96 94 +177 23 95 24 +178 94 95 23 +179 95 97 24 +180 42 95 94 +181 94 96 42 +182 42 97 95 +2 24 2 4 +183 25 1 100 +184 1 37 100 +187 37 9 100 +188 9 43 100 +2 25 2 12 +185 5 27 101 +186 40 5 101 +189 11 40 101 +190 43 11 101 +191 26 25 98 +192 98 25 100 +193 27 26 99 +194 26 98 99 +195 27 99 101 +196 98 43 99 +197 43 98 100 +198 99 43 101 +2 26 2 10 +199 31 104 4 +200 4 104 38 +203 38 104 10 +204 10 104 44 +207 32 102 31 +208 102 104 31 +209 33 103 32 +210 32 103 102 +212 102 103 44 +213 44 104 102 +2 27 2 6 +201 8 105 33 +202 41 105 8 +205 12 105 41 +206 44 105 12 +211 33 105 103 +214 103 105 44 +2 28 2 5 +215 9 39 108 +216 43 9 108 +217 39 10 109 +218 10 44 109 +223 108 39 109 +2 29 2 9 +219 42 11 107 +220 11 43 107 +221 12 42 106 +222 44 12 106 +224 106 42 107 +225 107 43 108 +226 44 106 109 +227 106 107 109 +228 107 108 109 +3 3 4 86 +229 22 23 64 51 +230 23 24 61 52 +231 20 21 52 77 +233 26 25 55 62 +237 26 27 68 56 +238 51 80 19 20 +242 30 6 53 88 +243 6 50 88 19 +244 86 7 21 49 +245 86 36 7 59 +247 24 8 72 49 +248 5 22 71 50 +252 83 28 55 29 +255 5 27 53 71 +256 76 30 29 56 +257 8 33 72 59 +261 49 86 7 59 +263 53 6 50 88 +264 59 8 49 72 +266 50 71 5 53 +269 21 52 77 24 +270 77 52 61 24 +271 61 52 20 23 +273 29 55 62 26 +274 77 52 20 61 +276 80 22 64 51 +277 80 19 22 51 +282 51 64 80 23 +283 61 20 80 23 +284 80 64 61 23 +286 68 29 62 26 +287 20 51 80 23 +288 68 56 29 26 +290 77 20 80 61 +291 76 68 65 64 +292 83 55 62 29 +293 28 55 62 83 +297 80 64 65 61 +301 77 80 65 61 +303 76 30 56 27 +305 53 50 71 88 +306 59 49 86 72 +312 68 76 56 27 +314 29 62 83 68 +315 56 29 76 68 +318 62 25 55 28 +319 72 21 77 24 +320 61 72 77 24 +324 80 65 81 77 +325 21 72 77 86 +327 83 76 29 68 +331 71 22 64 80 +332 22 88 19 80 +334 22 71 88 80 +337 86 82 36 72 +338 83 62 75 68 +339 72 82 36 33 +342 30 71 76 88 +345 88 50 22 19 +347 71 30 76 27 +348 72 49 21 24 +350 33 72 82 66 +352 77 82 72 61 +354 53 71 30 88 +355 59 86 36 72 +358 49 21 86 72 +359 22 71 50 88 +361 27 68 76 71 +363 30 27 71 53 +366 66 72 82 61 +367 72 36 59 33 +370 77 82 86 72 +378 64 76 68 71 +389 7 8 49 59 +390 50 5 6 53 +394 76 65 75 81 +395 75 65 76 68 +396 75 83 76 81 +397 76 83 75 68 +398 65 80 76 64 +399 76 80 65 81 +402 61 82 65 77 +403 61 65 82 66 +404 81 65 82 77 +405 71 80 76 88 +406 76 80 71 64 +3 4 4 94 +232 48 14 13 79 +234 33 32 66 57 +235 18 17 63 46 +236 15 46 84 14 +239 17 16 67 48 +240 3 45 89 15 +241 2 13 87 47 +246 32 31 69 60 +249 25 1 54 73 +250 1 16 47 73 +251 31 4 74 58 +253 78 60 34 35 +254 4 18 74 45 +258 57 35 82 36 +259 89 58 3 34 +260 54 87 2 28 +262 54 2 87 47 +265 47 54 1 73 +267 58 4 74 45 +268 45 58 3 89 +272 79 17 84 67 +275 46 84 18 15 +278 32 57 35 66 +279 17 84 14 79 +280 84 18 63 46 +281 48 17 14 79 +285 79 48 17 67 +289 17 63 84 67 +294 60 34 69 78 +295 82 57 36 33 +296 70 78 85 81 +298 70 78 81 82 +299 82 57 33 66 +300 84 63 17 46 +302 32 35 69 66 +304 87 54 47 73 +307 13 48 79 67 +308 74 58 45 89 +309 69 60 78 35 +310 17 84 46 14 +311 82 35 57 66 +313 48 13 16 67 +316 69 60 35 32 +317 35 82 69 66 +321 84 89 18 15 +322 89 18 74 84 +323 82 69 78 35 +326 63 67 75 85 +328 34 69 31 60 +329 75 85 83 81 +330 70 63 75 85 +333 18 63 74 84 +335 82 78 69 66 +336 34 89 69 78 +340 82 70 78 66 +341 28 62 87 83 +343 62 87 73 25 +344 87 13 79 67 +346 73 47 13 16 +349 13 73 16 67 +351 69 74 89 34 +353 87 73 67 62 +356 47 13 87 73 +357 87 54 73 25 +360 13 87 73 67 +362 63 70 78 85 +364 25 62 87 28 +365 84 63 78 85 +368 74 69 31 34 +369 70 69 78 66 +371 74 58 89 34 +372 79 87 67 83 +373 87 54 25 28 +374 62 67 87 83 +375 85 79 75 83 +376 58 74 31 34 +377 63 69 74 78 +379 45 18 74 89 +380 89 45 18 15 +381 63 70 69 78 +382 75 79 67 83 +383 85 75 70 81 +384 62 75 67 83 +385 75 67 79 85 +386 89 74 69 78 +387 81 75 70 65 +388 58 3 4 45 +391 54 1 2 47 +392 84 67 85 79 +393 85 67 84 63 +400 74 84 78 89 +401 78 84 74 63 +407 65 82 70 81 +408 65 70 82 66 +3 5 4 66 +409 66 65 70 106 +410 90 109 63 67 +411 66 109 102 103 +413 109 70 75 65 +415 90 109 91 63 +418 109 102 69 66 +419 67 109 63 75 +420 69 70 109 66 +422 65 109 70 106 +423 66 70 109 106 +425 109 90 108 67 +427 70 63 109 75 +428 103 66 109 106 +430 75 67 108 62 +431 108 109 67 75 +433 67 73 108 62 +436 109 63 69 74 +440 70 109 63 69 +442 17 90 91 63 +445 63 17 90 67 +446 32 69 102 66 +448 109 44 102 103 +449 69 104 109 74 +450 102 104 109 69 +451 102 32 66 103 +457 92 108 90 67 +458 109 39 90 91 +460 73 100 108 62 +462 108 92 73 67 +470 91 18 63 74 +471 104 93 109 74 +474 39 90 108 109 +481 108 92 100 73 +484 18 91 93 74 +485 44 10 109 104 +486 109 10 39 93 +489 108 9 92 39 +490 100 9 108 43 +493 32 33 66 103 +497 93 109 10 104 +498 17 18 63 91 +499 109 44 104 102 +501 31 104 102 69 +502 16 92 90 67 +503 100 37 1 73 +504 37 92 1 73 +510 39 109 93 91 +511 104 4 38 74 +512 62 73 100 25 +513 4 93 38 74 +514 69 31 104 74 +516 16 17 67 90 +517 16 73 92 67 +518 90 108 92 39 +520 32 31 102 69 +521 25 1 73 100 +523 1 16 73 92 +524 4 31 74 104 +528 92 37 100 73 +530 104 38 93 74 +532 18 4 74 93 +535 100 9 92 108 +536 37 9 92 100 +538 38 93 10 104 +545 74 91 109 63 +546 74 109 91 93 +3 6 4 74 +412 61 65 66 106 +414 94 61 95 107 +416 64 61 94 107 +417 98 62 68 107 +421 68 98 107 99 +424 68 62 75 107 +426 61 64 65 107 +429 62 98 108 107 +432 71 68 64 107 +434 106 95 61 107 +435 61 66 72 106 +437 23 64 61 94 +438 107 64 71 96 +439 107 94 64 96 +441 108 75 62 107 +443 106 61 65 107 +444 95 23 61 94 +447 107 71 68 101 +452 62 98 68 26 +453 99 107 68 101 +454 26 98 68 99 +455 100 98 108 62 +456 65 68 75 107 +459 95 42 94 107 +461 68 65 64 107 +463 97 95 61 106 +464 66 103 105 106 +465 97 61 72 106 +466 101 107 71 96 +467 43 107 98 99 +468 105 72 66 106 +469 65 75 109 107 +472 106 65 109 107 +473 109 44 103 106 +475 22 64 94 96 +476 64 22 71 96 +477 95 106 42 107 +478 108 109 75 107 +479 101 11 107 96 +480 98 108 107 43 +482 107 11 42 96 +483 43 11 107 101 +487 23 22 64 94 +488 42 94 107 96 +491 99 43 107 101 +492 100 98 62 25 +494 24 23 61 95 +495 106 12 97 42 +496 105 44 12 106 +500 26 25 62 98 +505 40 71 5 96 +506 27 26 68 99 +507 8 72 41 97 +508 105 8 72 41 +509 101 71 5 40 +515 72 105 97 106 +519 108 98 100 43 +522 5 22 96 71 +525 106 97 95 42 +526 105 103 44 106 +527 8 24 72 97 +529 40 101 71 96 +531 41 72 105 97 +533 5 27 71 101 +534 33 8 72 105 +537 97 105 12 106 +539 41 105 12 97 +540 40 11 101 96 +541 66 105 33 72 +542 33 105 66 103 +543 68 101 27 99 +544 27 101 68 71 +547 61 97 24 95 +548 24 97 61 72 +$EndElements diff --git a/tests/fixtures/coarse-v41.msh b/tests/fixtures/coarse-v41.msh new file mode 100644 index 0000000..01930be --- /dev/null +++ b/tests/fixtures/coarse-v41.msh @@ -0,0 +1,877 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +12 20 11 2 +1 0 0 -0.25 0 +2 0 0 0 0 +3 0 2 0 0 +4 0 2 -0.25 0 +5 2 0 -0.25 0 +6 2 0 0 0 +7 2 2 0 0 +8 2 2 -0.25 0 +9 0 0 -1 0 +10 0 2 -1 0 +11 2 0 -1 0 +12 2 2 -1 0 +1 -1e-07 -1e-07 -0.2500001 1e-07 1e-07 1.000000000028756e-07 0 2 1 -2 +2 -1e-07 -9.999999994736442e-08 -1e-07 1e-07 2.0000001 1e-07 0 2 2 -3 +3 -1e-07 1.9999999 -0.2500001 1e-07 2.0000001 1.000000000028756e-07 0 2 4 -3 +4 -1e-07 -9.999999994736442e-08 -0.2500001 1e-07 2.0000001 -0.2499999 0 2 1 -4 +5 1.9999999 -1e-07 -0.2500001 2.0000001 1e-07 1.000000000028756e-07 0 2 5 -6 +6 1.9999999 -9.999999994736442e-08 -1e-07 2.0000001 2.0000001 1e-07 0 2 6 -7 +7 1.9999999 1.9999999 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 0 2 8 -7 +8 1.9999999 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 -0.2499999 0 2 5 -8 +9 -9.999999994736442e-08 -1e-07 -0.2500001 2.0000001 1e-07 -0.2499999 0 2 1 -5 +10 -9.999999994736442e-08 -1e-07 -1e-07 2.0000001 1e-07 1e-07 0 2 2 -6 +11 -9.999999994736442e-08 1.9999999 -0.2500001 2.0000001 2.0000001 -0.2499999 0 2 4 -8 +12 -9.999999994736442e-08 1.9999999 -1e-07 2.0000001 2.0000001 1e-07 0 2 3 -7 +13 -1e-07 -1e-07 -1.0000001 1e-07 1e-07 -0.2499999 0 2 9 -1 +14 -1e-07 1.9999999 -1.0000001 1e-07 2.0000001 -0.2499999 0 2 10 -4 +15 -1e-07 -9.999999994736442e-08 -1.0000001 1e-07 2.0000001 -0.9999999000000001 0 2 9 -10 +16 1.9999999 -1e-07 -1.0000001 2.0000001 1e-07 -0.2499999 0 2 11 -5 +17 1.9999999 1.9999999 -1.0000001 2.0000001 2.0000001 -0.2499999 0 2 12 -8 +18 1.9999999 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.9999999000000001 0 2 11 -12 +19 -9.999999994736442e-08 -1e-07 -1.0000001 2.0000001 1e-07 -0.9999999000000001 0 2 9 -11 +20 -9.999999994736442e-08 1.9999999 -1.0000001 2.0000001 2.0000001 -0.9999999000000001 0 2 10 -12 +1 -1e-07 -9.999999994736442e-08 -0.2500001 1e-07 2.0000001 1.000000000028756e-07 1 105 4 -1 4 3 -2 +2 1.9999999 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 1 105 4 -5 8 7 -6 +3 -9.999999994736442e-08 -1e-07 -0.2500001 2.0000001 1e-07 1.000000000028756e-07 1 105 4 -9 1 10 -5 +4 -9.999999994736442e-08 1.9999999 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 1 105 4 -11 3 12 -7 +5 -9.999999994736442e-08 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 -0.2499999 1 103 4 -4 9 8 -11 +6 -9.999999994736442e-08 -9.999999994736442e-08 -1e-07 2.0000001 2.0000001 1e-07 1 101 4 -2 10 6 -12 +7 -1e-07 -9.999999994736442e-08 -1.0000001 1e-07 2.0000001 -0.2499999 1 105 4 -13 15 14 -4 +8 1.9999999 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.2499999 1 105 4 -16 18 17 -8 +9 -9.999999994736442e-08 -1e-07 -1.0000001 2.0000001 1e-07 -0.2499999 1 105 4 -19 13 9 -16 +10 -9.999999994736442e-08 1.9999999 -1.0000001 2.0000001 2.0000001 -0.2499999 1 105 4 -20 14 11 -17 +11 -9.999999994736442e-08 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.9999999000000001 1 105 4 -15 19 18 -20 +1 -9.999999994736442e-08 -9.999999994736442e-08 -0.2500001 2.0000001 2.0000001 1.000000000028756e-07 1 1 6 -1 2 -3 4 -5 6 +2 -9.999999994736442e-08 -9.999999994736442e-08 -1.0000001 2.0000001 2.0000001 -0.2499999 1 2 6 -7 8 -9 10 -11 5 +$EndEntities +$Nodes +41 109 1 109 +0 1 0 1 +1 +0 0 -0.25 +0 2 0 1 +2 +0 0 0 +0 3 0 1 +3 +0 2 0 +0 4 0 1 +4 +0 2 -0.25 +0 5 0 1 +5 +2 0 -0.25 +0 6 0 1 +6 +2 0 0 +0 7 0 1 +7 +2 2 0 +0 8 0 1 +8 +2 2 -0.25 +0 9 0 1 +9 +0 0 -1 +0 10 0 1 +10 +0 2 -1 +0 11 0 1 +11 +2 0 -1 +0 12 0 1 +12 +2 2 -1 +1 2 0 3 +13 +14 +15 +0 0.5 0 +0 1 0 +0 1.5 0 +1 4 0 3 +16 +17 +18 +0 0.5 -0.25 +0 1 -0.25 +0 1.5 -0.25 +1 6 0 3 +19 +20 +21 +2 0.5 0 +2 1 0 +2 1.5 0 +1 8 0 3 +22 +23 +24 +2 0.5 -0.25 +2 1 -0.25 +2 1.5 -0.25 +1 9 0 3 +25 +26 +27 +0.5 0 -0.25 +1 0 -0.25 +1.5 0 -0.25 +1 10 0 3 +28 +29 +30 +0.5 0 0 +1 0 0 +1.5 0 0 +1 11 0 3 +31 +32 +33 +0.5 2 -0.25 +1 2 -0.25 +1.5 2 -0.25 +1 12 0 3 +34 +35 +36 +0.5 2 0 +1 2 0 +1.5 2 0 +1 13 0 1 +37 +0 0 -0.5863498029562912 +1 14 0 1 +38 +0 2 -0.5863498029562912 +1 15 0 1 +39 +0 1 -1 +1 16 0 1 +40 +2 0 -0.5863498029562912 +1 17 0 1 +41 +2 2 -0.5863498029562912 +1 18 0 1 +42 +2 1 -1 +1 19 0 1 +43 +1 0 -1 +1 20 0 1 +44 +1 2 -1 +2 1 0 4 +45 +46 +47 +48 +0 1.75 -0.125 +0 1.25 -0.125 +0 0.25 -0.125 +0 0.75 -0.125 +2 2 0 4 +49 +50 +51 +52 +2 1.75 -0.125 +2 0.25 -0.125 +2 0.75 -0.125 +2 1.25 -0.125 +2 3 0 4 +53 +54 +55 +56 +1.75 0 -0.125 +0.25 0 -0.125 +0.75 0 -0.125 +1.25 0 -0.125 +2 4 0 4 +57 +58 +59 +60 +1.25 2 -0.125 +0.25 2 -0.125 +1.75 2 -0.125 +0.75 2 -0.125 +2 5 0 15 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +1.578151847792411 1.230662432702593 -0.25 +0.7183803468335941 0.3953531183916653 -0.25 +0.4514095486548143 1.234183475777031 -0.25 +1.577292096379071 0.7509441482399639 -0.25 +1.19048230709083 0.9938634724449638 -0.25 +1.211526285167907 1.539080572974888 -0.25 +0.389289068198761 0.7180466052574374 -0.25 +1.213941721673849 0.4470568705367829 -0.25 +0.7375873251999121 1.636990986207155 -0.25 +0.8638678665980398 1.245262305856504 -0.25 +1.633974596215561 0.3660254037844386 -0.25 +1.633974596215561 1.633974596215561 -0.25 +0.3660254037844386 0.3660254037844387 -0.25 +0.3630158654516971 1.636984134548303 -0.25 +0.799919109919719 0.8127176335914889 -0.25 +2 6 0 14 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +1.271813574233536 0.4264624335545749 0 +1.57791535636081 1.249332516611218 0 +0.7707992533266995 1.491026004178691 0 +0.3838980770800826 0.7512620920870399 0 +1.604157990548827 0.756865646419863 0 +1.138381937170266 0.9886994546006772 0 +1.25740778765016 1.566401986104808 0 +0.7720151968557594 0.504236995066732 0 +0.3774516866522022 1.228912045653614 0 +0.6885092302170019 0.9928273183173509 0 +1.633974596215561 1.633974596215561 0 +0.3660254037844387 0.3660254037844386 0 +1.633974596215561 0.3660254037844387 0 +0.3296501879957803 1.643987609966461 0 +2 7 0 4 +90 +91 +92 +93 +0 0.8250160980116606 -0.5458226013255596 +0 1.189987121590671 -0.5299871215906714 +0 0.3875026830019433 -0.6053620673803085 +0 1.614997853598445 -0.6027228207578272 +2 8 0 4 +94 +95 +96 +97 +2 0.8250160980116606 -0.5458226013255596 +2 1.189987121590671 -0.5299871215906714 +2 0.3875026830019433 -0.6053620673803085 +2 1.614997853598445 -0.6027228207578272 +2 9 0 4 +98 +99 +100 +101 +0.8250160980116606 0 -0.5458226013255596 +1.189987121590671 0 -0.5299871215906714 +0.3875026830019435 0 -0.6053620673803086 +1.614997853598445 0 -0.6027228207578272 +2 10 0 4 +102 +103 +104 +105 +0.8250160980116606 2 -0.5458226013255596 +1.189987121590671 2 -0.5299871215906714 +0.3875026830019435 2 -0.6053620673803086 +1.614997853598445 2 -0.6027228207578272 +2 11 0 4 +106 +107 +108 +109 +1.5 1.5 -1 +1.25 0.7499999999999999 -1 +0.5625 0.5625000000000002 -1 +0.6914062499999998 1.30859375 -1 +3 1 0 0 +3 2 0 0 +$EndNodes +$Elements +13 548 1 548 +2 1 2 16 +1 1 2 47 +2 16 1 47 +3 2 13 47 +4 3 4 45 +5 15 3 45 +6 4 18 45 +7 13 14 48 +8 13 16 47 +9 16 13 48 +10 14 15 46 +11 17 14 46 +12 14 17 48 +13 18 15 45 +14 15 18 46 +15 17 16 48 +16 18 17 46 +2 2 2 16 +17 5 50 6 +18 22 50 5 +19 6 50 19 +20 7 49 8 +21 21 49 7 +22 8 49 24 +23 19 51 20 +24 19 50 22 +25 22 51 19 +26 20 52 21 +27 20 51 23 +28 23 52 20 +29 24 49 21 +30 21 52 24 +31 23 51 22 +32 24 52 23 +2 3 2 16 +33 2 1 54 +34 1 25 54 +35 28 2 54 +36 5 6 53 +37 27 5 53 +38 6 30 53 +39 25 26 55 +40 25 28 54 +41 28 25 55 +42 26 27 56 +43 26 29 55 +44 29 26 56 +45 30 27 53 +46 27 30 56 +47 29 28 55 +48 30 29 56 +2 4 2 16 +49 3 58 4 +50 34 58 3 +51 4 58 31 +52 8 59 7 +53 7 59 36 +54 33 59 8 +55 31 60 32 +56 31 58 34 +57 34 60 31 +58 32 57 33 +59 35 57 32 +60 32 60 35 +61 33 57 36 +62 36 59 33 +63 35 60 34 +64 36 57 35 +2 5 2 44 +65 1 16 73 +66 25 1 73 +67 18 4 74 +68 4 31 74 +69 22 5 71 +70 5 27 71 +71 8 24 72 +72 33 8 72 +73 16 17 67 +74 16 67 73 +75 17 18 63 +76 17 63 67 +77 63 18 74 +78 23 22 64 +79 64 22 71 +80 24 23 61 +81 61 23 64 +82 24 61 72 +83 26 25 62 +84 62 25 73 +85 27 26 68 +86 26 62 68 +87 27 68 71 +88 31 32 69 +89 31 69 74 +90 32 33 66 +91 32 66 69 +92 66 33 72 +93 61 64 65 +94 61 65 66 +95 61 66 72 +96 67 62 73 +97 62 67 75 +98 68 62 75 +99 67 63 75 +100 63 69 70 +101 69 63 74 +102 63 70 75 +103 65 64 68 +104 68 64 71 +105 66 65 70 +106 65 68 75 +107 70 65 75 +108 69 66 70 +2 6 2 42 +109 2 87 13 +110 28 87 2 +111 15 89 3 +112 3 89 34 +113 19 88 6 +114 6 88 30 +115 7 86 21 +116 36 86 7 +117 13 79 14 +118 13 87 79 +119 14 84 15 +120 79 84 14 +121 84 89 15 +122 20 80 19 +123 80 88 19 +124 21 77 20 +125 77 80 20 +126 21 86 77 +127 29 83 28 +128 83 87 28 +129 30 76 29 +130 76 83 29 +131 30 88 76 +132 34 78 35 +133 34 89 78 +134 35 82 36 +135 78 82 35 +136 82 86 36 +137 80 81 76 +138 76 88 80 +139 81 83 76 +140 77 81 80 +141 77 82 81 +142 77 86 82 +143 81 82 78 +144 78 85 81 +145 84 85 78 +146 78 89 84 +147 83 85 79 +148 79 87 83 +149 79 85 84 +150 81 85 83 +2 7 2 16 +151 1 16 92 +152 37 1 92 +153 18 4 93 +154 4 38 93 +155 9 37 92 +156 39 9 92 +157 38 10 93 +158 10 39 93 +159 16 17 90 +160 16 90 92 +161 17 18 91 +162 90 17 91 +163 91 18 93 +164 39 90 91 +165 90 39 92 +166 39 91 93 +2 8 2 16 +167 5 96 22 +168 40 96 5 +169 24 97 8 +170 8 97 41 +171 11 96 40 +172 42 96 11 +173 41 97 12 +174 12 97 42 +175 22 94 23 +176 22 96 94 +177 23 95 24 +178 94 95 23 +179 95 97 24 +180 42 95 94 +181 94 96 42 +182 42 97 95 +2 9 2 16 +183 25 1 100 +184 1 37 100 +185 5 27 101 +186 40 5 101 +187 37 9 100 +188 9 43 100 +189 11 40 101 +190 43 11 101 +191 26 25 98 +192 98 25 100 +193 27 26 99 +194 26 98 99 +195 27 99 101 +196 98 43 99 +197 43 98 100 +198 99 43 101 +2 10 2 16 +199 31 104 4 +200 4 104 38 +201 8 105 33 +202 41 105 8 +203 38 104 10 +204 10 104 44 +205 12 105 41 +206 44 105 12 +207 32 102 31 +208 102 104 31 +209 33 103 32 +210 32 103 102 +211 33 105 103 +212 102 103 44 +213 44 104 102 +214 103 105 44 +2 11 2 14 +215 9 39 108 +216 43 9 108 +217 39 10 109 +218 10 44 109 +219 42 11 107 +220 11 43 107 +221 12 42 106 +222 44 12 106 +223 108 39 109 +224 106 42 107 +225 107 43 108 +226 44 106 109 +227 106 107 109 +228 107 108 109 +3 1 4 180 +229 22 23 64 51 +230 23 24 61 52 +231 20 21 52 77 +232 48 14 13 79 +233 26 25 55 62 +234 33 32 66 57 +235 18 17 63 46 +236 15 46 84 14 +237 26 27 68 56 +238 51 80 19 20 +239 17 16 67 48 +240 3 45 89 15 +241 2 13 87 47 +242 30 6 53 88 +243 6 50 88 19 +244 86 7 21 49 +245 86 36 7 59 +246 32 31 69 60 +247 24 8 72 49 +248 5 22 71 50 +249 25 1 54 73 +250 1 16 47 73 +251 31 4 74 58 +252 83 28 55 29 +253 78 60 34 35 +254 4 18 74 45 +255 5 27 53 71 +256 76 30 29 56 +257 8 33 72 59 +258 57 35 82 36 +259 89 58 3 34 +260 54 87 2 28 +261 49 86 7 59 +262 54 2 87 47 +263 53 6 50 88 +264 59 8 49 72 +265 47 54 1 73 +266 50 71 5 53 +267 58 4 74 45 +268 45 58 3 89 +269 21 52 77 24 +270 77 52 61 24 +271 61 52 20 23 +272 79 17 84 67 +273 29 55 62 26 +274 77 52 20 61 +275 46 84 18 15 +276 80 22 64 51 +277 80 19 22 51 +278 32 57 35 66 +279 17 84 14 79 +280 84 18 63 46 +281 48 17 14 79 +282 51 64 80 23 +283 61 20 80 23 +284 80 64 61 23 +285 79 48 17 67 +286 68 29 62 26 +287 20 51 80 23 +288 68 56 29 26 +289 17 63 84 67 +290 77 20 80 61 +291 76 68 65 64 +292 83 55 62 29 +293 28 55 62 83 +294 60 34 69 78 +295 82 57 36 33 +296 70 78 85 81 +297 80 64 65 61 +298 70 78 81 82 +299 82 57 33 66 +300 84 63 17 46 +301 77 80 65 61 +302 32 35 69 66 +303 76 30 56 27 +304 87 54 47 73 +305 53 50 71 88 +306 59 49 86 72 +307 13 48 79 67 +308 74 58 45 89 +309 69 60 78 35 +310 17 84 46 14 +311 82 35 57 66 +312 68 76 56 27 +313 48 13 16 67 +314 29 62 83 68 +315 56 29 76 68 +316 69 60 35 32 +317 35 82 69 66 +318 62 25 55 28 +319 72 21 77 24 +320 61 72 77 24 +321 84 89 18 15 +322 89 18 74 84 +323 82 69 78 35 +324 80 65 81 77 +325 21 72 77 86 +326 63 67 75 85 +327 83 76 29 68 +328 34 69 31 60 +329 75 85 83 81 +330 70 63 75 85 +331 71 22 64 80 +332 22 88 19 80 +333 18 63 74 84 +334 22 71 88 80 +335 82 78 69 66 +336 34 89 69 78 +337 86 82 36 72 +338 83 62 75 68 +339 72 82 36 33 +340 82 70 78 66 +341 28 62 87 83 +342 30 71 76 88 +343 62 87 73 25 +344 87 13 79 67 +345 88 50 22 19 +346 73 47 13 16 +347 71 30 76 27 +348 72 49 21 24 +349 13 73 16 67 +350 33 72 82 66 +351 69 74 89 34 +352 77 82 72 61 +353 87 73 67 62 +354 53 71 30 88 +355 59 86 36 72 +356 47 13 87 73 +357 87 54 73 25 +358 49 21 86 72 +359 22 71 50 88 +360 13 87 73 67 +361 27 68 76 71 +362 63 70 78 85 +363 30 27 71 53 +364 25 62 87 28 +365 84 63 78 85 +366 66 72 82 61 +367 72 36 59 33 +368 74 69 31 34 +369 70 69 78 66 +370 77 82 86 72 +371 74 58 89 34 +372 79 87 67 83 +373 87 54 25 28 +374 62 67 87 83 +375 85 79 75 83 +376 58 74 31 34 +377 63 69 74 78 +378 64 76 68 71 +379 45 18 74 89 +380 89 45 18 15 +381 63 70 69 78 +382 75 79 67 83 +383 85 75 70 81 +384 62 75 67 83 +385 75 67 79 85 +386 89 74 69 78 +387 81 75 70 65 +388 58 3 4 45 +389 7 8 49 59 +390 50 5 6 53 +391 54 1 2 47 +392 84 67 85 79 +393 85 67 84 63 +394 76 65 75 81 +395 75 65 76 68 +396 75 83 76 81 +397 76 83 75 68 +398 65 80 76 64 +399 76 80 65 81 +400 74 84 78 89 +401 78 84 74 63 +402 61 82 65 77 +403 61 65 82 66 +404 81 65 82 77 +405 71 80 76 88 +406 76 80 71 64 +407 65 82 70 81 +408 65 70 82 66 +3 2 4 140 +409 66 65 70 106 +410 90 109 63 67 +411 66 109 102 103 +412 61 65 66 106 +413 109 70 75 65 +414 94 61 95 107 +415 90 109 91 63 +416 64 61 94 107 +417 98 62 68 107 +418 109 102 69 66 +419 67 109 63 75 +420 69 70 109 66 +421 68 98 107 99 +422 65 109 70 106 +423 66 70 109 106 +424 68 62 75 107 +425 109 90 108 67 +426 61 64 65 107 +427 70 63 109 75 +428 103 66 109 106 +429 62 98 108 107 +430 75 67 108 62 +431 108 109 67 75 +432 71 68 64 107 +433 67 73 108 62 +434 106 95 61 107 +435 61 66 72 106 +436 109 63 69 74 +437 23 64 61 94 +438 107 64 71 96 +439 107 94 64 96 +440 70 109 63 69 +441 108 75 62 107 +442 17 90 91 63 +443 106 61 65 107 +444 95 23 61 94 +445 63 17 90 67 +446 32 69 102 66 +447 107 71 68 101 +448 109 44 102 103 +449 69 104 109 74 +450 102 104 109 69 +451 102 32 66 103 +452 62 98 68 26 +453 99 107 68 101 +454 26 98 68 99 +455 100 98 108 62 +456 65 68 75 107 +457 92 108 90 67 +458 109 39 90 91 +459 95 42 94 107 +460 73 100 108 62 +461 68 65 64 107 +462 108 92 73 67 +463 97 95 61 106 +464 66 103 105 106 +465 97 61 72 106 +466 101 107 71 96 +467 43 107 98 99 +468 105 72 66 106 +469 65 75 109 107 +470 91 18 63 74 +471 104 93 109 74 +472 106 65 109 107 +473 109 44 103 106 +474 39 90 108 109 +475 22 64 94 96 +476 64 22 71 96 +477 95 106 42 107 +478 108 109 75 107 +479 101 11 107 96 +480 98 108 107 43 +481 108 92 100 73 +482 107 11 42 96 +483 43 11 107 101 +484 18 91 93 74 +485 44 10 109 104 +486 109 10 39 93 +487 23 22 64 94 +488 42 94 107 96 +489 108 9 92 39 +490 100 9 108 43 +491 99 43 107 101 +492 100 98 62 25 +493 32 33 66 103 +494 24 23 61 95 +495 106 12 97 42 +496 105 44 12 106 +497 93 109 10 104 +498 17 18 63 91 +499 109 44 104 102 +500 26 25 62 98 +501 31 104 102 69 +502 16 92 90 67 +503 100 37 1 73 +504 37 92 1 73 +505 40 71 5 96 +506 27 26 68 99 +507 8 72 41 97 +508 105 8 72 41 +509 101 71 5 40 +510 39 109 93 91 +511 104 4 38 74 +512 62 73 100 25 +513 4 93 38 74 +514 69 31 104 74 +515 72 105 97 106 +516 16 17 67 90 +517 16 73 92 67 +518 90 108 92 39 +519 108 98 100 43 +520 32 31 102 69 +521 25 1 73 100 +522 5 22 96 71 +523 1 16 73 92 +524 4 31 74 104 +525 106 97 95 42 +526 105 103 44 106 +527 8 24 72 97 +528 92 37 100 73 +529 40 101 71 96 +530 104 38 93 74 +531 41 72 105 97 +532 18 4 74 93 +533 5 27 71 101 +534 33 8 72 105 +535 100 9 92 108 +536 37 9 92 100 +537 97 105 12 106 +538 38 93 10 104 +539 41 105 12 97 +540 40 11 101 96 +541 66 105 33 72 +542 33 105 66 103 +543 68 101 27 99 +544 27 101 68 71 +545 74 91 109 63 +546 74 109 91 93 +547 61 97 24 95 +548 24 97 61 72 +$EndElements diff --git a/tests/fixtures/coarse.puml.h5 b/tests/fixtures/coarse.puml.h5 new file mode 100644 index 0000000000000000000000000000000000000000..fb3cf154dd52746dbfa2438631df9a9e68d180aa GIT binary patch literal 20280 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 NcfaS}c*LuB{vV=n&V>K~ literal 0 HcmV?d00001 diff --git a/tests/fixtures/generate.py b/tests/fixtures/generate.py new file mode 100644 index 0000000..be00377 --- /dev/null +++ b/tests/fixtures/generate.py @@ -0,0 +1,246 @@ +# SPDX-FileCopyrightText: 2026 SeisSol Group +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Generate the mesh fixtures and the reference PUML files for the regression tests. + +Run from this directory: python3 generate.py +Requires the gmsh Python API, numpy and h5py. The reference files are computed +from the gmsh model directly, i.e. independently of PUMGen. +""" + +import itertools + +import gmsh +import h5py +import numpy as np + +# local face numbering of a tetrahedron as used by SeisSol +FACE_NODES = [(1, 0, 2), (0, 1, 3), (1, 2, 3), (2, 0, 3)] + +WIDTH, DEPTH, LAYER = 2.0, 1.0, 0.25 +# OpenCASCADE enlarges bounding boxes slightly, so classify with a generous tolerance +TOL = 1e-3 +FREE_SURFACE, INTERFACE, ABSORBING = 101, 103, 105 + + +def boundary_offset(tag): + return tag - 100 if tag >= 100 else tag + + +def build_model(size_top, size_bottom): + gmsh.model.add("layered") + top = gmsh.model.occ.addBox(0, 0, -LAYER, WIDTH, WIDTH, LAYER) + bottom = gmsh.model.occ.addBox(0, 0, -DEPTH, WIDTH, WIDTH, DEPTH - LAYER) + gmsh.model.occ.fragment([(3, top)], [(3, bottom)]) + gmsh.model.occ.synchronize() + + for dim, tag in gmsh.model.getEntities(3): + zmin = gmsh.model.getBoundingBox(dim, tag)[2] + gmsh.model.addPhysicalGroup(3, [tag], 1 if zmin > -LAYER - TOL else 2) + + groups = {FREE_SURFACE: [], INTERFACE: [], ABSORBING: []} + for dim, tag in gmsh.model.getEntities(2): + _, _, zmin, _, _, zmax = gmsh.model.getBoundingBox(dim, tag) + if zmin > -TOL: + groups[FREE_SURFACE].append(tag) + elif abs(zmin + LAYER) < TOL and abs(zmax + LAYER) < TOL: + groups[INTERFACE].append(tag) + else: + groups[ABSORBING].append(tag) + for physical, tags in groups.items(): + gmsh.model.addPhysicalGroup(2, tags, physical) + + for dim, tag in gmsh.model.getEntities(0): + z = gmsh.model.getValue(dim, tag, [])[2] + gmsh.model.mesh.setSize([(dim, tag)], size_bottom if z < -LAYER - TOL else size_top) + + gmsh.model.mesh.generate(3) + + +def physical_of(dim, tag): + physicals = gmsh.model.getPhysicalGroupsForEntity(dim, tag) + return int(physicals[0]) if len(physicals) > 0 else 0 + + +def reference_data(): + """Mesh data in the layout PUMGen writes: vertices by node tag, cells in file order.""" + node_tags, coords, _ = gmsh.model.mesh.getNodes() + order = np.argsort(node_tags) + node_tags = node_tags[order] + assert np.array_equal(node_tags, np.arange(1, len(node_tags) + 1)), "expected dense node tags" + vertices = coords.reshape(-1, 3)[order] + + face_bc = {} + for dim, tag in gmsh.model.getEntities(2): + physical = physical_of(dim, tag) + if physical == 0: + continue + types, _, nodes = gmsh.model.mesh.getElements(dim, tag) + for element_type, element_nodes in zip(types, nodes): + _, _, _, num_nodes, _, _ = gmsh.model.mesh.getElementProperties(element_type) + for tri in np.asarray(element_nodes).reshape(-1, num_nodes): + face_bc[frozenset(int(v) for v in tri[:3])] = boundary_offset(physical) + + cells, groups, boundary = [], [], [] + for dim, tag in gmsh.model.getEntities(3): + physical = physical_of(dim, tag) + types, _, nodes = gmsh.model.mesh.getElements(dim, tag) + for element_type, element_nodes in zip(types, nodes): + _, _, _, num_nodes, _, _ = gmsh.model.mesh.getElementProperties(element_type) + for tet in np.asarray(element_nodes).reshape(-1, num_nodes): + code = 0 + for face, local in enumerate(FACE_NODES): + bc = face_bc.get(frozenset(int(tet[i]) for i in local), 0) + code |= bc << (8 * face) + cells.append(tet - 1) + groups.append(physical) + boundary.append(code) + return vertices, np.asarray(cells), np.asarray(groups), np.asarray(boundary) + + +def write_reference(path, mesh_file): + """Reference PUML data from a written mesh file, so coordinates match their text form.""" + gmsh.clear() + gmsh.open(mesh_file) + vertices, cells, groups, boundary = reference_data() + with h5py.File(path, "w") as out: + out.create_dataset("connect", data=cells.astype("bK$ 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/tiny-badnode-v22.msh b/tests/fixtures/tiny-badnode-v22.msh new file mode 100644 index 0000000..9e459ce --- /dev/null +++ b/tests/fixtures/tiny-badnode-v22.msh @@ -0,0 +1,14 @@ +$MeshFormat +2.2 0 8 +$EndMeshFormat +$Nodes +4 +1 0 0 0 +2 1 0 0 +3 0 1 0 +4 0 0 1 +$EndNodes +$Elements +1 +1 4 2 7 1 1 2 3 9 +$EndElements diff --git a/tests/fixtures/tiny-badnode-v41.msh b/tests/fixtures/tiny-badnode-v41.msh new file mode 100644 index 0000000..8754070 --- /dev/null +++ b/tests/fixtures/tiny-badnode-v41.msh @@ -0,0 +1,24 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 0 0 0 1 1 1 1 7 0 +$EndEntities +$Nodes +1 4 1 4 +3 1 0 4 +1 +2 +3 +4 +0 0 0 +1 0 0 +0 1 0 +0 0 1 +$EndNodes +$Elements +1 1 1 1 +3 1 4 1 +1 1 2 3 9 +$EndElements diff --git a/tests/fixtures/tiny-badnumber-v41.msh b/tests/fixtures/tiny-badnumber-v41.msh new file mode 100644 index 0000000..c1d58c9 --- /dev/null +++ b/tests/fixtures/tiny-badnumber-v41.msh @@ -0,0 +1,24 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 0 0 0 1 1 1 1 7 0 +$EndEntities +$Nodes +1 4 1 4 +3 1 0 4 +1 +2 +3 +4 +0 0 0 +1 abc 0 +0 1 0 +0 0 1 +$EndNodes +$Elements +1 1 1 1 +3 1 4 1 +1 1 2 3 4 +$EndElements diff --git a/tests/fixtures/tiny-duplicate-v22.msh b/tests/fixtures/tiny-duplicate-v22.msh new file mode 100644 index 0000000..cab31c6 --- /dev/null +++ b/tests/fixtures/tiny-duplicate-v22.msh @@ -0,0 +1,14 @@ +$MeshFormat +2.2 0 8 +$EndMeshFormat +$Nodes +4 +1 0 0 0 +2 1 0 0 +2 0 1 0 +4 0 0 1 +$EndNodes +$Elements +1 +1 4 2 7 1 1 2 3 4 +$EndElements diff --git a/tests/fixtures/tiny-duplicate-v41.msh b/tests/fixtures/tiny-duplicate-v41.msh new file mode 100644 index 0000000..ce03d45 --- /dev/null +++ b/tests/fixtures/tiny-duplicate-v41.msh @@ -0,0 +1,24 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 0 0 0 1 1 1 1 7 0 +$EndEntities +$Nodes +1 4 1 4 +3 1 0 4 +1 +2 +2 +4 +0 0 0 +1 0 0 +0 1 0 +0 0 1 +$EndNodes +$Elements +1 1 1 1 +3 1 4 1 +1 1 2 3 4 +$EndElements diff --git a/tests/fixtures/tiny-sparse-v41.msh b/tests/fixtures/tiny-sparse-v41.msh new file mode 100644 index 0000000..16ffb6f --- /dev/null +++ b/tests/fixtures/tiny-sparse-v41.msh @@ -0,0 +1,24 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 0 0 0 1 1 1 1 7 0 +$EndEntities +$Nodes +1 4 1 5 +3 1 0 4 +1 +2 +3 +5 +0 0 0 +1 0 0 +0 1 0 +0 0 1 +$EndNodes +$Elements +1 1 1 1 +3 1 4 1 +1 1 2 3 5 +$EndElements diff --git a/tests/fixtures/tiny-truncated-v41.msh b/tests/fixtures/tiny-truncated-v41.msh new file mode 100644 index 0000000..906031c --- /dev/null +++ b/tests/fixtures/tiny-truncated-v41.msh @@ -0,0 +1,23 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 0 0 0 1 1 1 1 7 0 +$EndEntities +$Nodes +1 4 1 4 +3 1 0 4 +1 +2 +3 +4 +0 0 0 +1 0 0 +0 1 0 +0 0 1 +$EndNodes +$Elements +1 1 1 1 +3 1 4 1 +1 1 2 diff --git a/tests/fixtures/tiny-unknowntype-v41.msh b/tests/fixtures/tiny-unknowntype-v41.msh new file mode 100644 index 0000000..7c7ebdf --- /dev/null +++ b/tests/fixtures/tiny-unknowntype-v41.msh @@ -0,0 +1,24 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 0 0 0 1 1 1 1 7 0 +$EndEntities +$Nodes +1 4 1 4 +3 1 0 4 +1 +2 +3 +4 +0 0 0 +1 0 0 +0 1 0 +0 0 1 +$EndNodes +$Elements +1 1 1 1 +3 1 999 1 +1 1 2 3 4 +$EndElements diff --git a/tests/fixtures/tiny-v22.msh b/tests/fixtures/tiny-v22.msh new file mode 100644 index 0000000..449d58a --- /dev/null +++ b/tests/fixtures/tiny-v22.msh @@ -0,0 +1,14 @@ +$MeshFormat +2.2 0 8 +$EndMeshFormat +$Nodes +4 +1 0 0 0 +2 1 0 0 +3 0 1 0 +4 0 0 1 +$EndNodes +$Elements +1 +1 4 2 7 1 1 2 3 4 +$EndElements diff --git a/tests/fixtures/tiny-v40.msh b/tests/fixtures/tiny-v40.msh new file mode 100644 index 0000000..517e8b7 --- /dev/null +++ b/tests/fixtures/tiny-v40.msh @@ -0,0 +1,24 @@ +$MeshFormat +4 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 0 0 0 1 1 1 1 7 0 +$EndEntities +$Nodes +1 4 1 4 +3 1 0 4 +1 +2 +3 +4 +0 0 0 +1 0 0 +0 1 0 +0 0 1 +$EndNodes +$Elements +1 1 1 1 +3 1 4 1 +1 1 2 3 4 +$EndElements diff --git a/tests/fixtures/tiny-v41.msh b/tests/fixtures/tiny-v41.msh new file mode 100644 index 0000000..c0db8d4 --- /dev/null +++ b/tests/fixtures/tiny-v41.msh @@ -0,0 +1,24 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 0 0 0 1 1 1 1 7 0 +$EndEntities +$Nodes +1 4 1 4 +3 1 0 4 +1 +2 +3 +4 +0 0 0 +1 0 0 +0 1 0 +0 0 1 +$EndNodes +$Elements +1 1 1 1 +3 1 4 1 +1 1 2 3 4 +$EndElements diff --git a/tests/fixtures/tiny.puml.h5 b/tests/fixtures/tiny.puml.h5 new file mode 100644 index 0000000000000000000000000000000000000000..7b0993b1026c5bcb2b16941f738169c7c7b30185 GIT binary patch literal 4368 zcmeHJu};G<5IrXiL`6u=hL}7uGIU@oNDb-)2pAX(CF(?is#Ypn2Zplo8GH(#!ygpe zch@Mk+DrviwJZ7Vo-e1|>$@Z$z3$~<^SB9m#W6IXWnA7Cv|0%w9P@df96@$Y_A5X~ zfc^m-5uQ+eJMaEBO+jUHeYSKNoHo#t40yX!4D`~A8{O~&b$v$mn{-AY42nF@24fgz z#Y;9Gy)ix<6_ZyLJQb7t`LV=Vw~`YrDp#AJv7ujK1Vo}8xU2v?}|u5#k7UAq(n8s*elj&ah_y89GN5fXhsy z3hV@ukolf-v{gWzu1fu2TXq~Oy=eU;va{4x`vBSt;(4{cB>g3Qi?68_|83~3~c_p>wT-fyM6)Tp+p`4 literal 0 HcmV?d00001 From d5f86bfe9249c6454a23dafbd20b2be20b559c47 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 18:18:36 +0000 Subject: [PATCH 02/19] Fix the parallel Gambit reader Vertices and elements were distributed in ceil-sized chunks, while SerialMeshFile allocates the chunks given by getChunksize. Whenever the counts do not divide evenly (e.g. 815206 elements on 3 ranks), ranks received more data than their buffers hold; pumgen then aborted or hung. Group numbers and boundary conditions were routed to their elements with the same mismatch, and readBoundaries only read the first nBoundaries/P faces in many cases, so with 2 ranks half of the boundary conditions were silently dropped. All four readers now use getChunksize/getChunksum, a new getChunkOwner maps elements to ranks, and groups and boundaries are read completely and ended by an explicit terminator message. The serial reader no longer reads past a group section that ends exactly at a chunk boundary or past the last section. AI-generated. Model: Claude Opus 5.5 --- src/helper/Distributor.h | 15 ++ src/meshreader/GambitReader.h | 8 +- src/meshreader/ParallelGambitReader.h | 250 +++++++++++--------------- src/meshreader/ParallelMeshReader.h | 158 +++++++--------- tests/CMakeLists.txt | 4 + 5 files changed, 185 insertions(+), 250 deletions(-) diff --git a/src/helper/Distributor.h b/src/helper/Distributor.h index 02897f2..d583427 100644 --- a/src/helper/Distributor.h +++ b/src/helper/Distributor.h @@ -4,7 +4,9 @@ #ifndef PUMGEN_SRC_HELPER_DISTRIBUTOR_H_ #define PUMGEN_SRC_HELPER_DISTRIBUTOR_H_ +#include #include +#include #include // no namespace for now @@ -20,4 +22,17 @@ constexpr std::size_t getChunksum(std::size_t total, int until, int size) { return base + addon; } +/** + * The rank whose chunk (as given by getChunksize/getChunksum) contains the item index. + */ +constexpr int getChunkOwner(std::size_t total, std::size_t index, int size) { + const std::size_t base = total / size; + const std::size_t rest = total % size; + const std::size_t largeChunks = rest * (base + 1); + if (index < largeChunks) { + return static_cast(index / (base + 1)); + } + return static_cast(rest + (index - largeChunks) / base); +} + #endif // PUMGEN_SRC_HELPER_DISTRIBUTOR_H_ diff --git a/src/meshreader/GambitReader.h b/src/meshreader/GambitReader.h index a00d653..260f649 100644 --- a/src/meshreader/GambitReader.h +++ b/src/meshreader/GambitReader.h @@ -393,7 +393,7 @@ class GambitReader : public MeshReader { // Get the group, were we should start reading std::vector::const_iterator section; - for (section = m_groups.begin(); section != m_groups.end() && section->nLines < start; + for (section = m_groups.begin(); section != m_groups.end() && section->nLines <= start; section++) { start -= section->nLines; } @@ -417,7 +417,9 @@ class GambitReader : public MeshReader { start = 0; section++; - m_mesh.seekg(section->seekPosition); + if (section != m_groups.end()) { + m_mesh.seekg(section->seekPosition); + } } else if (start % ELEMENTS_PER_LINE_GROUP == 0) // Skip newline char at end of line m_mesh.seekg(section->lineSize - section->elementSize * ELEMENTS_PER_LINE_GROUP, @@ -456,7 +458,7 @@ class GambitReader : public MeshReader { // Get the boundary, were we should start reading std::vector::const_iterator section; - for (section = m_boundaries.begin(); section != m_boundaries.end() && section->nLines < start; + for (section = m_boundaries.begin(); section != m_boundaries.end() && section->nLines <= start; section++) { start -= section->nLines; } diff --git a/src/meshreader/ParallelGambitReader.h b/src/meshreader/ParallelGambitReader.h index bf63ab6..fac8859 100644 --- a/src/meshreader/ParallelGambitReader.h +++ b/src/meshreader/ParallelGambitReader.h @@ -9,11 +9,18 @@ #include +#include +#include +#include +#include +#include + #include "GambitReader.h" #include "ParallelMeshReader.h" #include "helper/Distributor.h" #include "third_party/MPITraits.h" +#include "utils/logger.h" namespace puml { @@ -25,180 +32,125 @@ class ParallelGambitReader : public ParallelMeshReader { : ParallelMeshReader(meshFile, comm) {} /** - * Reads all group numbers. - * Each process gets nElements() + processes - 1) / processes - * group numbers, except for the last, which gets the remaining + * Reads all group numbers. Each process gets the group numbers of its elements (see + * ParallelMeshReader::readElements). * * This is a collective operation. */ void readGroups(int* groups) { - std::size_t chunkSize = getChunksize(nElements(), m_rank, m_nProcs); - - if (m_rank == 0) { - std::size_t maxChunkSize = chunkSize; - std::vector map(maxChunkSize); - - std::vector> aggregator(m_nProcs - 1); - std::vector sizes(m_nProcs - 1); - std::vector requests((m_nProcs - 1) * 2, MPI_REQUEST_NULL); - - std::size_t position = 0; - - for (int i = 0; i < m_nProcs; i++) { - logInfo() << "Reading group information part" << (i + 1) << "of" << m_nProcs; - - chunkSize = getChunksize(nElements(), i, m_nProcs); - - m_serialReader.readGroups(position, chunkSize, map.data()); - position += chunkSize; - - // Wait for all sending from last iteration - MPI_Waitall((m_nProcs - 1) * 2, requests.data(), MPI_STATUSES_IGNORE); - for (int j = 0; j < m_nProcs - 1; j++) { - aggregator[j].clear(); - } - - // Sort group numbers into the corresponding aggregator - for (std::size_t j = 0; j < chunkSize; j++) { - if (map[j].element < maxChunkSize) - // Local element - groups[map[j].element] = map[j].group; - else { - // Element for another processor - // Serials the struct to make sending easier - unsigned int proc = map[j].element / maxChunkSize; - aggregator[proc - 1].push_back(map[j].element % maxChunkSize); - aggregator[proc - 1].push_back(map[j].group); - } - } - - // Send send aggregated mapping - for (int j = 0; j < m_nProcs - 1; j++) { - if (aggregator[j].empty()) { - continue; - } - - sizes[j] = aggregator[j].size() / 2; // element id + group number - MPI_Isend(&sizes[j], 1, tndm::mpi_type_t(), j + 1, 0, m_comm, - &requests[j * 2]); - MPI_Isend(&aggregator[j][0], aggregator[j].size(), MPI_INT, j + 1, 0, m_comm, - &requests[j * 2 + 1]); - } - } - - MPI_Waitall((m_nProcs - 1) * 2, requests.data(), MPI_STATUSES_IGNORE); - } else { - // Allocate enough memory - std::vector buf(chunkSize * 2); - - unsigned int recieved = 0; - - while (recieved < chunkSize) { - std::size_t size; - MPI_Recv(&size, 1, tndm::mpi_type_t(), 0, 0, m_comm, MPI_STATUS_IGNORE); - MPI_Recv(buf.data(), size * 2, MPI_INT, 0, 0, m_comm, MPI_STATUS_IGNORE); - - for (std::size_t i = 0; i < size * 2; i += 2) { - groups[buf[i]] = buf[i + 1]; - } - - recieved += size; - } - } + scatterToElementOwners( + nElements(), 1, groups, "group information", + [&](std::size_t start, std::size_t count, ElementGroup* entries) { + m_serialReader.readGroups(start, count, entries); + }, + [](const ElementGroup& entry) { + return std::make_tuple(entry.element, std::size_t{0}, entry.group); + }); } /** - * Reads all boundaries. - * Each process gets the boundaries for nElements() + processes - 1) / - * processes elements. Boundaries not specified in the mesh are not - * modified. + * Reads all boundaries. Each process gets the boundaries of the faces of its elements (see + * ParallelMeshReader::readElements). Boundaries not specified in the mesh are not modified. * - * This is a collective operation + * This is a collective operation. * * @todo Only tetrahedral meshes are supported */ void readBoundaries(int* boundaries) { - std::size_t chunkSize = getChunksize(nElements(), m_rank, m_nProcs); - - if (m_rank == 0) { - std::size_t maxChunkSize = chunkSize; - std::vector faces(maxChunkSize); - - std::vector> aggregator(m_nProcs - 1); - std::vector sizes(m_nProcs - 1); - std::vector requests((m_nProcs - 1) * 2, MPI_REQUEST_NULL); - - std::size_t nChunks = (nBoundaries() + chunkSize - 1) / chunkSize; - std::size_t position = 0; - for (std::size_t i = 0; i < nChunks; i++) { - logInfo() << "Reading boundary conditions part" << (i + 1) << "of" << nChunks; - - chunkSize = getChunksize(nBoundaries(), i, m_nProcs); + scatterToElementOwners( + nBoundaries(), 4, boundaries, "boundary conditions", + [&](std::size_t start, std::size_t count, GambitBoundaryFace* entries) { + m_serialReader.readBoundaries(start, count, entries); + }, + [](const GambitBoundaryFace& entry) { + return std::make_tuple(entry.element, static_cast(entry.face), entry.type); + }); + } - m_serialReader.readBoundaries(position, chunkSize, faces.data()); - position += chunkSize; + private: + /** + * Reads per-element entries in chunks on rank 0 and stores the value of each entry at + * local[localElement * slotsPerElement + slot] on the process owning the element. + * + * @param read Callable (start, count, entries) reading count entries starting at + * start + * @param decode Callable returning the tuple (element, slot, value) for an entry + */ + template + void scatterToElementOwners(std::size_t numEntries, std::size_t slotsPerElement, int* local, + const char* name, ReadF&& read, DecodeF&& decode) { + const auto messageType = tndm::mpi_type_t(); - // Wait for all sending from last iteration - MPI_Waitall((m_nProcs - 1) * 2, requests.data(), MPI_STATUSES_IGNORE); - for (int j = 0; j < m_nProcs - 1; j++) { - aggregator[j].clear(); + if (m_rank != 0) { + std::vector buffer; + while (true) { + std::size_t size = 0; + MPI_Recv(&size, 1, tndm::mpi_type_t(), 0, 0, m_comm, MPI_STATUS_IGNORE); + if (size == 0) { + break; } - - // Sort boundary conditions into the corresponding aggregator - for (std::size_t j = 0; j < chunkSize; j++) { - if (faces[j].element < maxChunkSize) - // Local element - boundaries[faces[j].element * 4 + faces[j].face] = faces[j].type; - else { - // Face for another processor - // Serials the struct to make sending easier - unsigned int proc = faces[j].element / maxChunkSize; - aggregator[proc - 1].push_back((faces[j].element % maxChunkSize) * 4 + faces[j].face); - aggregator[proc - 1].push_back(faces[j].type); - } - } - - // Send send aggregated values - for (int j = 0; j < m_nProcs - 1; j++) { - if (aggregator[j].empty()) { - continue; - } - - sizes[j] = aggregator[j].size() / 2; // element id + face type - MPI_Isend(&sizes[j], 1, tndm::mpi_type_t(), j + 1, 0, m_comm, - &requests[j * 2]); - MPI_Isend(&aggregator[j][0], aggregator[j].size(), MPI_INT, j + 1, 0, m_comm, - &requests[j * 2 + 1]); + buffer.resize(2 * size); + MPI_Recv(buffer.data(), messageSize(buffer.size()), messageType, 0, 0, m_comm, + MPI_STATUS_IGNORE); + for (std::size_t i = 0; i < size; ++i) { + local[buffer[2 * i]] = static_cast(buffer[2 * i + 1]); } } + return; + } - MPI_Waitall((m_nProcs - 1) * 2, requests.data(), MPI_STATUSES_IGNORE); - - // Send finish signal to all other processes - std::fill(sizes.begin(), sizes.end(), 0); - for (int i = 0; i < m_nProcs - 1; i++) { - MPI_Isend(&sizes[i], 1, tndm::mpi_type_t(), i + 1, 0, m_comm, &requests[i]); + const std::size_t chunkSize = std::max(getChunksize(nElements(), 0, m_nProcs), std::size_t{1}); + std::vector entries(std::min(chunkSize, numEntries)); + std::vector> outgoing(m_nProcs); + std::vector sizes(m_nProcs, 0); + std::vector requests(2 * m_nProcs, MPI_REQUEST_NULL); + + for (std::size_t start = 0; start < numEntries; start += chunkSize) { + const std::size_t count = std::min(chunkSize, numEntries - start); + logInfo() << "Reading" << name << start << "to" << start + count << "of" << numEntries; + read(start, count, entries.data()); + + // the send buffers of the previous chunk are reused + MPI_Waitall(requests.size(), requests.data(), MPI_STATUSES_IGNORE); + for (auto& buffer : outgoing) { + buffer.clear(); } - MPI_Waitall(m_nProcs - 1, requests.data(), MPI_STATUSES_IGNORE); - } else { - // Allocate enough memory - std::vector buf(chunkSize * 4); - while (true) { - std::size_t size; - MPI_Recv(&size, 1, tndm::mpi_type_t(), 0, 0, m_comm, MPI_STATUS_IGNORE); - if (size == 0) { - // Finished - break; + for (std::size_t i = 0; i < count; ++i) { + const auto [element, slot, value] = decode(entries[i]); + if (element >= nElements() || slot >= slotsPerElement) { + logError() << "Invalid" << name << "entry: element" << element << "slot" << slot; } + const int owner = getChunkOwner(nElements(), element, m_nProcs); + const std::size_t index = + (element - getChunksum(nElements(), owner, m_nProcs)) * slotsPerElement + slot; + if (owner == 0) { + local[index] = value; + } else { + outgoing[owner].push_back(static_cast(index)); + outgoing[owner].push_back(value); + } + } - MPI_Recv(buf.data(), size * 2, MPI_INT, 0, 0, m_comm, MPI_STATUS_IGNORE); - - for (std::size_t i = 0; i < size * 2; i += 2) { - boundaries[buf[i]] = buf[i + 1]; + for (int rank = 1; rank < m_nProcs; ++rank) { + if (outgoing[rank].empty()) { + continue; } + sizes[rank] = outgoing[rank].size() / 2; + MPI_Isend(&sizes[rank], 1, tndm::mpi_type_t(), rank, 0, m_comm, + &requests[2 * rank]); + MPI_Isend(outgoing[rank].data(), messageSize(outgoing[rank].size()), messageType, rank, 0, + m_comm, &requests[2 * rank + 1]); } } + MPI_Waitall(requests.size(), requests.data(), MPI_STATUSES_IGNORE); + + // an empty message ends the transfer + std::fill(sizes.begin(), sizes.end(), 0); + for (int rank = 1; rank < m_nProcs; ++rank) { + MPI_Isend(&sizes[rank], 1, tndm::mpi_type_t(), rank, 0, m_comm, &requests[rank]); + } + MPI_Waitall(requests.size(), requests.data(), MPI_STATUSES_IGNORE); } }; diff --git a/src/meshreader/ParallelMeshReader.h b/src/meshreader/ParallelMeshReader.h index 205bce8..114f5b5 100644 --- a/src/meshreader/ParallelMeshReader.h +++ b/src/meshreader/ParallelMeshReader.h @@ -10,9 +10,13 @@ #include #include +#include +#include #include +#include "helper/Distributor.h" #include "third_party/MPITraits.h" +#include "utils/logger.h" template class ParallelMeshReader { private: @@ -77,129 +81,87 @@ template class ParallelMeshReader { std::size_t nBoundaries() const { return m_nBoundaries; } /** - * Reads all vertices - * Each process gets nVertices() + processes - 1) / processes - * elements, except for the last, which gets the remaining elements + * Reads all vertices. Each process gets the chunk given by getChunksize/getChunksum. * - * This is a collective operation + * This is a collective operation. * * @todo Only 3 dimensional meshes are supported */ void readVertices(double* vertices) { - std::size_t chunkSize = (m_nVertices + m_nProcs - 1) / m_nProcs; - - if (m_rank == 0) { - // Allocate second buffer so we can read and send in parallel - std::vector tempVertices(chunkSize * 3); - double* vertices2 = tempVertices.data(); - if (m_nProcs % 2 == 0) { - // swap once so we have the correct buffer at the end - swap(vertices, vertices2); - } - - MPI_Request request = MPI_REQUEST_NULL; - for (int i = 1; i < m_nProcs - 1; i++) { - logInfo() << "Reading vertices part" << i << "of" << m_nProcs; - m_serialReader.readVertices(i * chunkSize, chunkSize, vertices); - MPI_Wait(&request, MPI_STATUS_IGNORE); - MPI_Isend(vertices, chunkSize * 3, MPI_DOUBLE, i, 0, m_comm, &request); - swap(vertices, vertices2); - } - - if (m_nProcs > 1) { - // Read last one - const std::size_t lastChunkSize = m_nVertices - (m_nProcs - 1) * chunkSize; - logInfo() << "Reading vertices part" << (m_nProcs - 1) << "of" << m_nProcs; - m_serialReader.readVertices((m_nProcs - 1) * chunkSize, lastChunkSize, vertices); - MPI_Wait(&request, MPI_STATUS_IGNORE); - MPI_Isend(vertices, lastChunkSize * 3, MPI_DOUBLE, m_nProcs - 1, 0, m_comm, &request); - swap(vertices, vertices2); - } - - // Finally read the first part - logInfo() << "Reading vertices part" << m_nProcs << "of" << m_nProcs; - m_serialReader.readVertices(0, chunkSize, vertices); - MPI_Wait(&request, MPI_STATUS_IGNORE); - } else { - if (m_rank == m_nProcs - 1) - chunkSize = m_nVertices - (m_nProcs - 1) * chunkSize; - - MPI_Recv(vertices, chunkSize * 3, MPI_DOUBLE, 0, 0, m_comm, MPI_STATUS_IGNORE); - } + distributeChunks(m_nVertices, 3, vertices, "vertices", + [&](std::size_t start, std::size_t count, double* buffer) { + m_serialReader.readVertices(start, count, buffer); + }); } /** - * Reads all elements. - * Each process gets nElements() + processes - 1) / processes - * elements, except for the last, which gets the remaining elements + * Reads all elements. Each process gets the chunk given by getChunksize/getChunksum. * * This is a collective operation. * * @todo Only tetrahedral meshes are supported */ virtual void readElements(std::size_t* elements) { - std::size_t chunkSize = (m_nElements + m_nProcs - 1) / m_nProcs; + distributeChunks(m_nElements, 4, elements, "elements", + [&](std::size_t start, std::size_t count, std::size_t* buffer) { + m_serialReader.readElements(start, count, buffer); + }); + } - if (m_rank == 0) { - // Allocate second buffer so we can read and send in parallel - std::vector tempElements(chunkSize * 4); - std::size_t* elements2 = tempElements.data(); - if (m_nProcs % 2 == 0) { - // swap once so we have the correct buffer at the end - swap(elements, elements2); - } - - MPI_Request request = MPI_REQUEST_NULL; - - for (int i = 1; i < m_nProcs - 1; i++) { - logInfo() << "Reading elements part" << i << "of" << m_nProcs; - m_serialReader.readElements(i * chunkSize, chunkSize, elements); - MPI_Wait(&request, MPI_STATUS_IGNORE); - MPI_Isend(elements, chunkSize * 4, tndm::mpi_type_t(), i, 0, m_comm, &request); - swap(elements, elements2); - } - - if (m_nProcs > 1) { - // Read last one - const std::size_t lastChunkSize = m_nElements - (m_nProcs - 1) * chunkSize; - logInfo() << "Reading elements part" << (m_nProcs - 1) << "of" << m_nProcs; - m_serialReader.readElements((m_nProcs - 1) * chunkSize, lastChunkSize, elements); - MPI_Wait(&request, MPI_STATUS_IGNORE); - MPI_Isend(elements, lastChunkSize * 4, tndm::mpi_type_t(), m_nProcs - 1, 0, - m_comm, &request); - swap(elements, elements2); - } - - // Finally read the first part - logInfo() << "Reading elements part" << m_nProcs << "of" << m_nProcs; - m_serialReader.readElements(0, chunkSize, elements); - MPI_Wait(&request, MPI_STATUS_IGNORE); - } else { - if (m_rank == m_nProcs - 1) - chunkSize = m_nElements - (m_nProcs - 1) * chunkSize; - - MPI_Recv(elements, chunkSize * 4, tndm::mpi_type_t(), 0, 0, m_comm, - MPI_STATUS_IGNORE); + protected: + static int messageSize(std::size_t count) { + if (count > static_cast(std::numeric_limits::max())) { + logError() << "MPI message too large:" << count; } + return static_cast(count); } private: /** - * Initialize some parameters + * Reads the items on rank 0 and sends each process its chunk. + * + * @param read Callable (start, count, buffer) reading count items starting at + * start */ - void init() { - MPI_Comm_rank(m_comm, &m_rank); - MPI_Comm_size(m_comm, &m_nProcs); + template + void distributeChunks(std::size_t total, std::size_t valuesPerItem, T* local, const char* name, + ReadF&& read) { + const std::size_t localCount = getChunksize(total, m_rank, m_nProcs); + + if (m_rank != 0) { + MPI_Recv(local, messageSize(localCount * valuesPerItem), tndm::mpi_type_t(), 0, 0, m_comm, + MPI_STATUS_IGNORE); + return; + } + + // two send buffers, so that reading the next chunk overlaps with sending the previous one + const std::size_t maxCount = getChunksize(total, 0, m_nProcs); + std::array, 2> buffers; + std::array requests = {MPI_REQUEST_NULL, MPI_REQUEST_NULL}; + + for (int rank = 1; rank < m_nProcs; ++rank) { + const auto slot = rank % 2; + MPI_Wait(&requests[slot], MPI_STATUS_IGNORE); + buffers[slot].resize(maxCount * valuesPerItem); + + const std::size_t count = getChunksize(total, rank, m_nProcs); + logInfo() << "Reading" << name << "for rank" << rank << "of" << m_nProcs; + read(getChunksum(total, rank, m_nProcs), count, buffers[slot].data()); + MPI_Isend(buffers[slot].data(), messageSize(count * valuesPerItem), tndm::mpi_type_t(), + rank, 0, m_comm, &requests[slot]); + } + + logInfo() << "Reading" << name << "for rank 0 of" << m_nProcs; + read(0, localCount, local); + MPI_Waitall(2, requests.data(), MPI_STATUSES_IGNORE); } - protected: /** - * Swaps two pointers + * Initialize some parameters */ - template static void swap(T*& p1, T*& p2) { - T* tmp = p1; - p1 = p2; - p2 = tmp; + void init() { + MPI_Comm_rank(m_comm, &m_rank); + MPI_Comm_size(m_comm, &m_nProcs); } }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d51db5c..7be4b01 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,6 +62,10 @@ foreach(ranks 1 2 3) pumgen_add_test(msh2.layered.np${ranks} RANKS ${ranks} SOURCE msh2 INPUT layered-v22.msh REFERENCE layered.puml.h5) endforeach() +foreach(ranks 1 2 3 4) + pumgen_add_test(gambit.layered.np${ranks} RANKS ${ranks} SOURCE gambit INPUT layered.neu + REFERENCE layered.puml.h5 TOLERANCE 1e-9) +endforeach() pumgen_add_test(msh4.coarse.np2 RANKS 2 SOURCE msh4 INPUT coarse-v41.msh REFERENCE coarse.puml.h5) pumgen_add_test(msh2.coarse-o2.np1 RANKS 1 SOURCE msh2 INPUT coarse-o2-v22.msh REFERENCE coarse-o2.puml.h5 ARGS --order=2) From d6b3c948b39f55b7724e5bb596f47a38b4e24ef3 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 18:21:12 +0000 Subject: [PATCH 03/19] Fix writing with HDF5 filters and with ranks without cells --filter-enable failed for every mesh: the chunk shape of the one-dimensional datasets (group, boundary) was set with rank 2 and a zero second dimension, which HDF5 rejects. The chunk rows were also derived from the local size, although the collective dataset creation needs the same shape on all ranks. The chunk shape now uses the dataset rank and the global size. With more ranks than cells, the empty ranks dereferenced the end iterator of their empty insphere list. They now contribute +inf to the minimum and take part in the collective writes with an empty selection. AI-generated. Model: Claude Opus 5.5 --- src/pumgen.cpp | 22 ++++++++++++++++------ tests/CMakeLists.txt | 12 ++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/pumgen.cpp b/src/pumgen.cpp index 39d1f14..77c8d7d 100644 --- a/src/pumgen.cpp +++ b/src/pumgen.cpp @@ -120,8 +120,11 @@ static void writeH5Data(const F& handler, hid_t h5file, const std::string& name, hid_t h5filter = H5P_DEFAULT; if (filterEnable > 0) { h5filter = checkH5Err(H5Pcreate(H5P_DATASET_CREATE)); - hsize_t chunk[2] = {std::min(filterChunksize, bufferSize), secondDim}; - checkH5Err(H5Pset_chunk(h5filter, 2, chunk)); + // the dataset creation is collective, so the chunk shape must not depend on the local size + const hsize_t chunkRows = + std::max(static_cast(1), std::min(filterChunksize, globalSize)); + hsize_t chunk[2] = {chunkRows, secondDim}; + checkH5Err(H5Pset_chunk(h5filter, dimensions, chunk)); if (filterEnable == 1 && std::is_integral_v) { checkH5Err(H5Pset_scaleoffset(h5filter, H5Z_SO_INT, H5Z_SO_INT_MINBITS_DEFAULT)); } else if (filterEnable < 11) { @@ -143,9 +146,15 @@ static void writeH5Data(const F& handler, hid_t h5file, const std::string& name, std::copy_n(handler.begin() + written * secondSize, count[0] * secondSize, data.begin()); - checkH5Err(H5Sselect_hyperslab(h5memspace, H5S_SELECT_SET, nullstart, nullptr, count, nullptr)); - - checkH5Err(H5Sselect_hyperslab(h5space, H5S_SELECT_SET, start, nullptr, count, nullptr)); + if (count[0] > 0) { + checkH5Err( + H5Sselect_hyperslab(h5memspace, H5S_SELECT_SET, nullstart, nullptr, count, nullptr)); + checkH5Err(H5Sselect_hyperslab(h5space, H5S_SELECT_SET, start, nullptr, count, nullptr)); + } else { + // ranks without (further) data still take part in the collective write + checkH5Err(H5Sselect_none(h5memspace)); + checkH5Err(H5Sselect_none(h5space)); + } checkH5Err(H5Dwrite(h5data, h5memtype, h5memspace, h5space, h5dxlist, data.data())); @@ -415,7 +424,8 @@ int main(int argc, char* argv[]) { auto inspheres = calculateInsphere(meshInput->connectivity(), meshInput->geometry(), MPI_COMM_WORLD); - double min = *std::min_element(inspheres.begin(), inspheres.end()); + double min = inspheres.empty() ? std::numeric_limits::infinity() + : *std::min_element(inspheres.begin(), inspheres.end()); MPI_Reduce((rank == 0 ? MPI_IN_PLACE : &min), &min, 1, MPI_DOUBLE, MPI_MIN, 0, MPI_COMM_WORLD); logInfo() << "Minimum insphere found:" << min; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7be4b01..2b6c488 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -71,3 +71,15 @@ pumgen_add_test(msh2.coarse-o2.np1 RANKS 1 SOURCE msh2 INPUT coarse-o2-v22.msh REFERENCE coarse-o2.puml.h5 ARGS --order=2) pumgen_add_test(msh4.tiny.np1 RANKS 1 SOURCE msh4 INPUT tiny-v41.msh REFERENCE tiny.puml.h5) pumgen_add_test(msh2.tiny.np1 RANKS 1 SOURCE msh2 INPUT tiny-v22.msh REFERENCE tiny.puml.h5) + +# more ranks than cells +foreach(ranks 2 3) + pumgen_add_test(msh4.tiny.np${ranks} RANKS ${ranks} SOURCE msh4 INPUT tiny-v41.msh + REFERENCE tiny.puml.h5) +endforeach() + +# HDF5 filters +pumgen_add_test(msh4.layered.deflate.np3 RANKS 3 SOURCE msh4 INPUT layered-v41.msh + REFERENCE layered.puml.h5 ARGS --filter-enable=deflate1 --filter-chunksize=64) +pumgen_add_test(msh4.tiny.scaleoffset.np3 RANKS 3 SOURCE msh4 INPUT tiny-v41.msh + REFERENCE tiny.puml.h5 ARGS --filter-enable=scaleoffset) From 8ae310462a3cd97cfe9decbe9cfd7460e10a75d2 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 18:27:52 +0000 Subject: [PATCH 04/19] Fix the --chunksize, --order and --compactify-datatypes options The write chunk size was read from --filter-chunksize, so --chunksize had no effect and e.g. --filter-chunksize 4096 shrank the write buffer to 4 KiB. --order took an optional argument, so "-o 2" left the order empty (read as 0, reported as "Order too high: 0") and consumed the input file name as a positional argument; the order is now a required argument and has to be at least 1. --compactify-datatypes is a flag, but required an argument and thus swallowed the next command line argument. AI-generated. Model: Claude Opus 5.5 --- src/pumgen.cpp | 9 ++++++--- tests/CMakeLists.txt | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pumgen.cpp b/src/pumgen.cpp index 77c8d7d..ec4ff43 100644 --- a/src/pumgen.cpp +++ b/src/pumgen.cpp @@ -218,7 +218,7 @@ int main(int argc, char* argv[]) { "deflate7", "deflate8", "deflate9"}; args.addOption("compactify-datatypes", 0, "Compress index and group data types to minimum byte size (no HDF5 filters)", - utils::Args::Required, false); + utils::Args::No, false); args.addEnumOption("filter-enable", filters, 0, "Apply HDF5 filters (i.e. compression). Disabled by default.", false); args.addOption("filter-chunksize", 0, "Chunksize for filters (default=4096).", @@ -251,7 +251,7 @@ int main(int argc, char* argv[]) { args.addOption("sim_log", 0, "Create SimModSuite log", utils::Args::Required, false); args.addAdditionalOption("input", "Input file (mesh or model)"); args.addAdditionalOption("output", "Output parallel unstructured mesh file", false); - args.addOption("order", 'o', "Mesh order", utils::Args::Optional, false); + args.addOption("order", 'o', "Mesh order (default: 1)", utils::Args::Required, false); if (args.parse(argc, argv, rank == 0) != utils::Args::Success) return 1; @@ -271,7 +271,7 @@ int main(int argc, char* argv[]) { outputFile.append(".puml.h5"); } - hsize_t chunksize = args.getArgument("filter-chunksize", static_cast(1) << 30); + hsize_t chunksize = args.getArgument("chunksize", static_cast(1) << 30); bool reduceInts = args.isSet("compactify-datatypes"); int filterEnable = args.getArgument("filter-enable", 0); @@ -334,6 +334,9 @@ int main(int argc, char* argv[]) { } const auto meshOrder = args.getArgument("order", 1); + if (meshOrder < 1) { + logError() << "The mesh order has to be at least 1, got" << meshOrder; + } // Create/read the mesh MeshData* meshInput = nullptr; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2b6c488..b56ae85 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -83,3 +83,11 @@ pumgen_add_test(msh4.layered.deflate.np3 RANKS 3 SOURCE msh4 INPUT layered-v41.m REFERENCE layered.puml.h5 ARGS --filter-enable=deflate1 --filter-chunksize=64) pumgen_add_test(msh4.tiny.scaleoffset.np3 RANKS 3 SOURCE msh4 INPUT tiny-v41.msh REFERENCE tiny.puml.h5 ARGS --filter-enable=scaleoffset) + +# command line options +pumgen_add_test(msh2.coarse-o2.shortopt.np1 RANKS 1 SOURCE msh2 INPUT coarse-o2-v22.msh + REFERENCE coarse-o2.puml.h5 ARGS -o 2) +pumgen_add_test(msh4.layered.chunksize.np3 RANKS 3 SOURCE msh4 INPUT layered-v41.msh + REFERENCE layered.puml.h5 ARGS --chunksize=4096) +pumgen_add_test(msh4.tiny.order0 RANKS 1 SOURCE msh4 INPUT tiny-v41.msh ARGS --order=0 + EXPECT_ERROR "mesh order has to be at least 1") From 9c3445a8e9f2140ad924501e6b495587a9b22d81 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 18:30:13 +0000 Subject: [PATCH 05/19] Fix --compactify-datatypes Writing with compact types aborted at the geometry: the datatype was closed whenever the option was set, including the immutable predefined float type. Besides, the reduced integer size was derived from the number of cells rather than from the stored values. The bit-packed boundary codes then did not fit and HDF5 clipped them (306 of 1597 cells in the layered test mesh), and vertex indices could exceed the reduced range as well. The size is now the smallest one that holds the global value range of the dataset, including a sign bit for signed types; data with negative values is not reduced. Only copied types are closed. AI-generated. Model: Claude Opus 5.5 --- src/pumgen.cpp | 48 +++++++++++++++++++++++++++++++++++--------- tests/CMakeLists.txt | 6 ++++++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/pumgen.cpp b/src/pumgen.cpp index ec4ff43..68ed458 100644 --- a/src/pumgen.cpp +++ b/src/pumgen.cpp @@ -71,6 +71,33 @@ static int ilog(std::size_t value, int expbase = 1) { constexpr std::size_t NoSecondDim = 0; +/** + * The smallest size in bytes of an integer type with the given signedness which holds all values + * of the (distributed) data. Data with negative values is not reduced. + */ +template +static std::size_t requiredIntegerBytes(const F& data, std::size_t count, bool isSigned) { + using ValueT = typename F::value_type; + unsigned long long largest = 0; + int negative = 0; + for (std::size_t i = 0; i < count; ++i) { + if constexpr (std::is_signed_v) { + if (data[i] < 0) { + negative = 1; + continue; + } + } + largest = std::max(largest, static_cast(data[i])); + } + MPI_Allreduce(MPI_IN_PLACE, &largest, 1, MPI_UNSIGNED_LONG_LONG, MPI_MAX, MPI_COMM_WORLD); + MPI_Allreduce(MPI_IN_PLACE, &negative, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD); + if (negative != 0) { + return sizeof(ValueT); + } + const std::size_t bits = ilog(largest) + (isSigned ? 1 : 0); + return std::max(static_cast(1), (bits + 7) / 8); +} + template static void writeH5Data(const F& handler, hid_t h5file, const std::string& name, void* mesh, int meshdim, hid_t h5memtype, hid_t h5outtype, std::size_t chunk, @@ -107,14 +134,17 @@ static void writeH5Data(const F& handler, hid_t h5file, const std::string& name, checkH5Err(H5Pset_dxpl_mpio(h5dxlist, H5FD_MPIO_COLLECTIVE)); hid_t h5type = h5outtype; - if (reduceInts && std::is_integral_v) { - h5type = H5Tcopy(h5outtype); - std::size_t bits = ilog(globalSize); - std::size_t unsignedSize = (bits + 7) / 8; - checkH5Err(h5type); - checkH5Err(H5Tset_size(h5type, unsignedSize)); - checkH5Err(H5Tcommit(h5file, (std::string("/") + name + std::string("Type")).c_str(), h5type, - H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)); + if constexpr (std::is_integral_v) { + if (reduceInts) { + const std::size_t bytes = requiredIntegerBytes(handler, localSize * secondSize, + H5Tget_sign(h5outtype) == H5T_SGN_2); + if (bytes < H5Tget_size(h5outtype)) { + h5type = checkH5Err(H5Tcopy(h5outtype)); + checkH5Err(H5Tset_size(h5type, bytes)); + checkH5Err(H5Tcommit(h5file, (std::string("/") + name + std::string("Type")).c_str(), + h5type, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)); + } + } } hid_t h5filter = H5P_DEFAULT; @@ -164,7 +194,7 @@ static void writeH5Data(const F& handler, hid_t h5file, const std::string& name, if (filterEnable > 0) { checkH5Err(H5Pclose(h5filter)); } - if (reduceInts) { + if (h5type != h5outtype) { checkH5Err(H5Tclose(h5type)); } checkH5Err(H5Sclose(h5space)); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b56ae85..9eb80e8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -91,3 +91,9 @@ pumgen_add_test(msh4.layered.chunksize.np3 RANKS 3 SOURCE msh4 INPUT layered-v41 REFERENCE layered.puml.h5 ARGS --chunksize=4096) pumgen_add_test(msh4.tiny.order0 RANKS 1 SOURCE msh4 INPUT tiny-v41.msh ARGS --order=0 EXPECT_ERROR "mesh order has to be at least 1") + +# integer types reduced to the value range +pumgen_add_test(msh4.layered.compact.np3 RANKS 3 SOURCE msh4 INPUT layered-v41.msh + REFERENCE layered.puml.h5 ARGS --compactify-datatypes) +pumgen_add_test(msh2.coarse-o2.compact.np2 RANKS 2 SOURCE msh2 INPUT coarse-o2-v22.msh + REFERENCE coarse-o2.puml.h5 ARGS --compactify-datatypes --order=2) From 283ead12e10a60de05db2bd598e584de93cdcfbf Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 18:34:09 +0000 Subject: [PATCH 06/19] Stop the GMSH parsers at the first error Parse errors were only appended to a message and parsing continued with the value 0, e.g. a coordinate "abc" was silently read as 0 and pumgen succeeded; an erroneous node tag became 0 and wrapped around when shifted to a 0-based index. Errors now throw, and parseFile returns false with the first error message. Further messages and checks: - binary MSH files are rejected with a hint instead of "Expected 0" - MSH 4.0 is rejected, since the parser implements the 4.1 layout - unknown element types are reported instead of indexing past the node count table (which aborted with std::out_of_range) - missing $Entities/$Nodes/$Elements sections are reported The node buffer is sized by the largest element (up to 1000 nodes) instead of the number of element types (136), which overflowed the stack in the msh2 parser, and msh2 elements without tags get tag 0 instead of the tag of the previous element. AI-generated. Model: Claude Opus 5.5 --- src/meshreader/GMSH4Parser.cpp | 44 ++++++++++++++++------------ src/meshreader/GMSH4Parser.h | 2 +- src/third_party/GMSH2Parser.cpp | 32 +++++++++++++-------- src/third_party/GMSHParser.cpp | 6 +++- src/third_party/GMSHParser.h | 51 +++++++++++++++++++++++++++------ tests/CMakeLists.txt | 14 +++++++++ 6 files changed, 109 insertions(+), 40 deletions(-) diff --git a/src/meshreader/GMSH4Parser.cpp b/src/meshreader/GMSH4Parser.cpp index ab36671..a2da382 100644 --- a/src/meshreader/GMSH4Parser.cpp +++ b/src/meshreader/GMSH4Parser.cpp @@ -10,15 +10,18 @@ namespace puml { bool GMSH4Parser::parse_() { - errorMsg.clear(); getNextToken(); const double version = parseMeshFormat(); if (version < 4.0 || version >= 5.0) { char buf[128]; - sprintf(buf, "Unsupported MSH version %.1lf", version); + snprintf(buf, sizeof(buf), "Unsupported MSH version %.1lf", version); return logError(buf); } + if (version < 4.1) { + return logError( + "MSH 4.0 is not supported; write the mesh as MSH 4.1 (Mesh.MshFileVersion = 4.1)"); + } bool hasEntities = false; bool hasNodes = false; @@ -37,7 +40,7 @@ bool GMSH4Parser::parse_() { hasElements = parseElements(); break; case tndm::GMSHToken::periodic: - hasPeriodic = parsePeriodic(version >= 4.1); + hasPeriodic = parsePeriodic(); break; default: getNextToken(); @@ -45,7 +48,16 @@ bool GMSH4Parser::parse_() { } } - return hasEntities && hasNodes && hasElements; + if (!hasEntities) { + return logError("Missing $Entities section"); + } + if (!hasNodes) { + return logError("Missing $Nodes section"); + } + if (!hasElements) { + return logError("Missing $Elements section"); + } + return true; } bool GMSH4Parser::parseEntities() { @@ -238,9 +250,7 @@ bool GMSH4Parser::parseElements() { [[maybe_unused]] const std::size_t maxElementTag = expectNonNegativeInt(); builder->setNumElements(numElements); - constexpr std::size_t MaxNodes = sizeof(NumNodes) / sizeof(std::size_t); - std::array nodes{}; - builder->setNumElements(numElements); + std::array nodes{}; for (std::size_t blockIdx = 0; blockIdx < numBlocks; blockIdx++) { getNextToken(); @@ -249,6 +259,11 @@ bool GMSH4Parser::parseElements() { const std::size_t entityTag = expectNonNegativeInt(); getNextToken(); const std::size_t type = expectNonNegativeInt(); + if (type < 1 || type > NumTypes || NumNodes[type - 1] == 0) { + char buf[128]; + snprintf(buf, sizeof(buf), "Unknown element type %zu", type); + return logErrorAnnotated(buf); + } getNextToken(); const std::size_t numElementsInBlock = expectNonNegativeInt(); for (std::size_t elementIdx = 0; elementIdx < numElementsInBlock; ++elementIdx) { @@ -256,7 +271,7 @@ bool GMSH4Parser::parseElements() { [[maybe_unused]] const std::size_t id = expectNonNegativeInt(); for (std::size_t nodeIdx = 0; nodeIdx < NumNodes[type - 1]; nodeIdx++) { getNextToken(); - nodes.at(nodeIdx) = expectNonNegativeInt() - 1; + nodes[nodeIdx] = expectNonNegativeInt() - 1; } std::size_t tag = (type == 2) ? physicalSurfaceIds[entityTag] : physicalVolumeIds[entityTag]; builder->addElement(type, tag, nodes.data(), NumNodes[type - 1]); @@ -271,7 +286,7 @@ bool GMSH4Parser::parseElements() { return true; } -bool GMSH4Parser::parsePeriodic(bool variableAffine) { +bool GMSH4Parser::parsePeriodic() { getNextToken(); const auto numPeriodic = expectNonNegativeInt(); @@ -285,15 +300,8 @@ bool GMSH4Parser::parsePeriodic(bool variableAffine) { getNextToken(); [[maybe_unused]] const std::size_t entityIdentifyId = expectNonNegativeInt(); - // for MSH4.1 vs MSH4.0 - const auto affineSize = [&]() -> std::size_t { - if (variableAffine) { - getNextToken(); - return expectNonNegativeInt(); - } else { - return 16; - } - }(); + getNextToken(); + const std::size_t affineSize = expectNonNegativeInt(); for (std::size_t i = 0; i < affineSize; ++i) { getNextToken(); [[maybe_unused]] const double affineValue = expectNumber(); diff --git a/src/meshreader/GMSH4Parser.h b/src/meshreader/GMSH4Parser.h index 92f7ff4..73c8d17 100644 --- a/src/meshreader/GMSH4Parser.h +++ b/src/meshreader/GMSH4Parser.h @@ -40,7 +40,7 @@ class GMSH4Parser : public tndm::GMSHParser { bool parseEntities(); bool parseNodes(); bool parseElements(); - bool parsePeriodic(bool variableAffine); + bool parsePeriodic(); virtual bool parse_() override; }; diff --git a/src/third_party/GMSH2Parser.cpp b/src/third_party/GMSH2Parser.cpp index 0da1527..9d57f34 100644 --- a/src/third_party/GMSH2Parser.cpp +++ b/src/third_party/GMSH2Parser.cpp @@ -10,13 +10,12 @@ namespace tndm { bool GMSH2Parser::parse_() { - errorMsg.clear(); getNextToken(); double version = parseMeshFormat(); if (version < 2.0 || version >= 3.0) { char buf[128]; - sprintf(buf, "Unsupported MSH version %.1lf", version); + snprintf(buf, sizeof(buf), "Unsupported MSH version %.1lf", version); return logError(buf); } @@ -40,7 +39,13 @@ bool GMSH2Parser::parse_() { } } - return hasNodes && hasElements; + if (!hasNodes) { + return logError("Missing $Nodes section"); + } + if (!hasElements) { + return logError("Missing $Elements section"); + } + return true; } bool GMSH2Parser::parseNodes() { @@ -56,7 +61,7 @@ bool GMSH2Parser::parseNodes() { if (curTok != GMSHToken::integer || lexer.getInteger() < 1 || lexer.getInteger() > numVertices) { char buf[128]; - sprintf(buf, "Expected node-tag with 1 <= node-tag <= %zu", numVertices); + snprintf(buf, sizeof(buf), "Expected node-tag with 1 <= node-tag <= %zu", numVertices); return logErrorAnnotated(buf); } std::size_t id = lexer.getInteger() - 1; @@ -87,9 +92,7 @@ bool GMSH2Parser::parseElements() { } const auto numElements = lexer.getInteger(); - constexpr std::size_t MaxNodes = sizeof(NumNodes) / sizeof(std::size_t); - long tag{}; - std::array nodes; + std::array nodes{}; builder->setNumElements(numElements); @@ -100,18 +103,23 @@ bool GMSH2Parser::parseElements() { } getNextToken(); - if (curTok != GMSHToken::integer || lexer.getInteger() < 1 || lexer.getInteger() > MaxNodes) { + if (curTok != GMSHToken::integer) { + return logErrorAnnotated("Expected element-type"); + } + const long type = lexer.getInteger(); + if (type < 1 || type > static_cast(NumTypes) || NumNodes[type - 1] == 0) { char buf[128]; - sprintf(buf, "Expected element-type with 1 <= element-type <= %zu", MaxNodes); + snprintf(buf, sizeof(buf), "Unknown element type %li", type); return logErrorAnnotated(buf); } - long type = lexer.getInteger(); getNextToken(); if (curTok != GMSHToken::integer || lexer.getInteger() < 0) { return logErrorAnnotated("Expected number of tags"); } long numTags = lexer.getInteger(); + // the physical tag; elements without tags get 0 + long tag = 0; for (long tagIdx = 0; tagIdx < numTags; ++tagIdx) { getNextToken(); if (curTok != GMSHToken::integer) { @@ -126,8 +134,8 @@ bool GMSH2Parser::parseElements() { getNextToken(); if (curTok != GMSHToken::integer || lexer.getInteger() < 1) { char buf[128]; - sprintf(buf, "Expected node number > 0 (%zu/%zu for type %li)", nodeIdx + 1, - NumNodes[type - 1], type); + snprintf(buf, sizeof(buf), "Expected node number > 0 (%zu/%zu for type %li)", nodeIdx + 1, + NumNodes[type - 1], type); return logErrorAnnotated(buf); } nodes[nodeIdx] = lexer.getInteger() - 1; diff --git a/src/third_party/GMSHParser.cpp b/src/third_party/GMSHParser.cpp index 92881ee..753176d 100644 --- a/src/third_party/GMSHParser.cpp +++ b/src/third_party/GMSHParser.cpp @@ -16,8 +16,12 @@ double GMSHParser::parseMeshFormat() { return logErrorAnnotated("Expected version number"); } getNextToken(); + if (curTok == GMSHToken::integer && lexer.getInteger() == 1) { + return logErrorAnnotated( + "Binary MSH files are not supported; write the mesh as ASCII (Mesh.Binary = 0)"); + } if (curTok != GMSHToken::integer || lexer.getInteger() != 0) { - return logErrorAnnotated("Expected 0"); + return logErrorAnnotated("Expected file type 0 (ASCII)"); } getNextToken(); // skip data-size getNextToken(); diff --git a/src/third_party/GMSHParser.h b/src/third_party/GMSHParser.h index dbb7974..cbef6b6 100644 --- a/src/third_party/GMSHParser.h +++ b/src/third_party/GMSHParser.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,16 @@ namespace tndm { +namespace detail { +template constexpr std::size_t maxEntry(const std::size_t (&values)[N]) { + std::size_t result = 0; + for (std::size_t i = 0; i < N; ++i) { + result = values[i] > result ? values[i] : result; + } + return result; +} +} // namespace detail + class GMSHParser { public: // Look-up table from gmsh type to number of nodes @@ -149,33 +160,49 @@ class GMSHParser { 61, // MSH_PYR_61 69, // MSH_PYR_69 }; + static constexpr std::size_t NumTypes = sizeof(NumNodes) / sizeof(NumNodes[0]); + static constexpr std::size_t MaxNodesPerElement = detail::maxEntry(NumNodes); explicit GMSHParser(GMSHMeshBuilder* builder) : builder(builder) {}; [[nodiscard]] std::string_view getErrorMessage() const { return errorMsg; } + /** + * Parses the file; stops at the first error, whose description getErrorMessage() returns. + */ bool parseFile(std::string const& fileName) { + errorMsg.clear(); std::ifstream in(fileName); if (!in.is_open()) { - return logError("Unable to open MSH file"); + errorMsg = "GMSH parser error:\n\tUnable to open MSH file\n"; + return false; } lexer.setIStream(&in); - return parse_(); + try { + return parse_(); + } catch (const ParseError&) { + return false; + } catch (const std::runtime_error& error) { + appendAnnotated(error.what()); + return false; + } } protected: + /** + * Thrown after an error has been recorded in errorMsg. + */ + struct ParseError {}; + template T logErrorAnnotated(std::string_view msg) { - std::stringstream ss; - ss << "GMSH parser error in line " << curLoc.line << " in column " << curLoc.col << ":\n"; - ss << '\t' << msg << '\n'; - errorMsg += ss.str(); - return {}; + appendAnnotated(msg); + throw ParseError{}; } template T logError(std::string_view msg) { errorMsg += "GMSH parser error:\n\t"; errorMsg += msg; errorMsg += '\n'; - return {}; + throw ParseError{}; } GMSHMeshBuilder* builder; @@ -201,6 +228,14 @@ class GMSHParser { virtual bool parse_() = 0; double parseMeshFormat(); + + private: + void appendAnnotated(std::string_view msg) { + std::stringstream ss; + ss << "GMSH parser error in line " << curLoc.line << " in column " << curLoc.col << ":\n"; + ss << '\t' << msg << '\n'; + errorMsg += ss.str(); + } }; } // namespace tndm diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9eb80e8..cb75cc0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -97,3 +97,17 @@ pumgen_add_test(msh4.layered.compact.np3 RANKS 3 SOURCE msh4 INPUT layered-v41.m REFERENCE layered.puml.h5 ARGS --compactify-datatypes) pumgen_add_test(msh2.coarse-o2.compact.np2 RANKS 2 SOURCE msh2 INPUT coarse-o2-v22.msh REFERENCE coarse-o2.puml.h5 ARGS --compactify-datatypes --order=2) + +# malformed or unsupported input +pumgen_add_test(msh4.tiny-truncated RANKS 1 SOURCE msh4 INPUT tiny-truncated-v41.msh + EXPECT_ERROR "GMSH parser error") +pumgen_add_test(msh4.tiny-badnumber RANKS 1 SOURCE msh4 INPUT tiny-badnumber-v41.msh + EXPECT_ERROR "Expected number") +pumgen_add_test(msh4.tiny-unknowntype RANKS 1 SOURCE msh4 INPUT tiny-unknowntype-v41.msh + EXPECT_ERROR "Unknown element type 999") +pumgen_add_test(msh4.tiny-v40 RANKS 1 SOURCE msh4 INPUT tiny-v40.msh + EXPECT_ERROR "MSH 4.0 is not supported") +pumgen_add_test(msh4.coarse-binary RANKS 1 SOURCE msh4 INPUT coarse-binary-v41.msh + EXPECT_ERROR "Binary MSH files are not supported") +pumgen_add_test(msh2.tiny-as-msh4 RANKS 1 SOURCE msh2 INPUT tiny-v41.msh + EXPECT_ERROR "Unsupported MSH version 4.1") From c155f08ce3d08b4be8e9c62c0c96e37a2080c59e Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 18:40:50 +0000 Subject: [PATCH 07/19] Fix periodic meshes with more than one rank Only rank 0 parses the GMSH file, but every rank asked its own (empty) builder whether the mesh has a vertex identification. Rank 0 therefore scattered and wrote the identification alone, and pumgen hung in the collective HDF5 write. The flag is now broadcast. Adds a periodic fixture (a box, periodic in x) whose reference identification is computed from gmsh's periodic node pairs; the tests compare the identify dataset as well. AI-generated. Model: Claude Opus 5.5 --- src/meshreader/ParallelGMSHReader.h | 6 +- tests/CMakeLists.txt | 15 +- tests/RunPumgenTest.cmake | 7 +- tests/fixtures/generate.py | 47 +- tests/fixtures/periodic-parametric-v41.msh | 2371 ++++++++++++++++++++ tests/fixtures/periodic-v41.msh | 2371 ++++++++++++++++++++ tests/fixtures/periodic.puml.h5 | Bin 0 -> 60528 bytes 7 files changed, 4811 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/periodic-parametric-v41.msh create mode 100644 tests/fixtures/periodic-v41.msh create mode 100644 tests/fixtures/periodic.puml.h5 diff --git a/src/meshreader/ParallelGMSHReader.h b/src/meshreader/ParallelGMSHReader.h index 19dcb99..0a9ac26 100644 --- a/src/meshreader/ParallelGMSHReader.h +++ b/src/meshreader/ParallelGMSHReader.h @@ -47,10 +47,12 @@ template class ParallelGMSHReader { nVertices_ = builder_.vertices.size(); nElements_ = builder_.elements.size(); + hasIdentify_ = builder_.identify.empty() ? 0 : 1; } MPI_Bcast(&nVertices_, 1, tndm::mpi_type_t(), 0, comm_); MPI_Bcast(&nElements_, 1, tndm::mpi_type_t(), 0, comm_); + MPI_Bcast(&hasIdentify_, 1, tndm::mpi_type_t(), 0, comm_); } [[nodiscard]] std::size_t nVertices() const { return nVertices_; } @@ -71,7 +73,7 @@ template class ParallelGMSHReader { void readGroups(int* groups) const { scatter(builder_.groups.data(), groups, nElements(), 1); } constexpr static bool SupportsIdentify = true; - bool hasIdentify() const { return !builder_.identify.empty(); } + bool hasIdentify() const { return hasIdentify_ != 0; } void readIdentify(std::size_t* vertices) const { scatter(builder_.identify.data(), vertices, nVertices(), 1); } @@ -175,6 +177,8 @@ template class ParallelGMSHReader { std::vector bcs_; std::size_t nVertices_ = 0; std::size_t nElements_ = 0; + // only rank 0 parses the file, so all ranks need this flag for the collective reads and writes + int hasIdentify_ = 0; }; } // namespace puml diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cb75cc0..45fe62e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,11 +12,11 @@ set(PUMGEN_TEST_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/output) file(MAKE_DIRECTORY ${PUMGEN_TEST_OUTPUT}) # pumgen_add_test( RANKS SOURCE INPUT -# [REFERENCE [TOLERANCE ]] [EXPECT_ERROR ] +# [REFERENCE [TOLERANCE ] [IDENTIFY]] [EXPECT_ERROR ] # [ARGS ...]) function(pumgen_add_test name) - cmake_parse_arguments(PARSE_ARGV 1 TEST "" "RANKS;SOURCE;INPUT;REFERENCE;TOLERANCE;EXPECT_ERROR" - "ARGS") + cmake_parse_arguments(PARSE_ARGV 1 TEST "IDENTIFY" + "RANKS;SOURCE;INPUT;REFERENCE;TOLERANCE;EXPECT_ERROR" "ARGS") set(output ${PUMGEN_TEST_OUTPUT}/${name}.puml.h5) set(command ${MPIEXEC_EXECUTABLE} @@ -41,6 +41,9 @@ function(pumgen_add_test name) if(DEFINED TEST_TOLERANCE) list(APPEND script_args -DTOLERANCE=${TEST_TOLERANCE}) endif() + if(TEST_IDENTIFY) + list(APPEND script_args -DIDENTIFY=ON) + endif() endif() add_test(NAME pumgen.${name} COMMAND ${CMAKE_COMMAND} ${script_args} -P @@ -111,3 +114,9 @@ pumgen_add_test(msh4.coarse-binary RANKS 1 SOURCE msh4 INPUT coarse-binary-v41.m EXPECT_ERROR "Binary MSH files are not supported") pumgen_add_test(msh2.tiny-as-msh4 RANKS 1 SOURCE msh2 INPUT tiny-v41.msh EXPECT_ERROR "Unsupported MSH version 4.1") + +# periodic meshes +foreach(ranks 1 2 3) + pumgen_add_test(msh4.periodic.np${ranks} RANKS ${ranks} SOURCE msh4 INPUT periodic-v41.msh + REFERENCE periodic.puml.h5 IDENTIFY) +endforeach() diff --git a/tests/RunPumgenTest.cmake b/tests/RunPumgenTest.cmake index 19ec71a..3c88ea2 100644 --- a/tests/RunPumgenTest.cmake +++ b/tests/RunPumgenTest.cmake @@ -9,6 +9,7 @@ # OUTPUT file written by pumgen # REFERENCE (optional) reference PUML file the output has to match # TOLERANCE (optional) absolute tolerance for the vertex coordinates +# IDENTIFY (optional) also compare the vertex identification of periodic meshes # H5DIFF h5diff executable, required together with REFERENCE # EXPECT_ERROR (optional) pumgen has to fail with a message matching this regular expression @@ -39,7 +40,11 @@ endif() if(DEFINED REFERENCE) set(failures "") - foreach(dataset connect geometry group boundary) + set(datasets connect geometry group boundary) + if(IDENTIFY) + list(APPEND datasets identify) + endif() + foreach(dataset ${datasets}) set(options "") if(dataset STREQUAL "geometry" AND DEFINED TOLERANCE) set(options -d ${TOLERANCE}) diff --git a/tests/fixtures/generate.py b/tests/fixtures/generate.py index be00377..f7340fa 100644 --- a/tests/fixtures/generate.py +++ b/tests/fixtures/generate.py @@ -99,7 +99,28 @@ def reference_data(): return vertices, np.asarray(cells), np.asarray(groups), np.asarray(boundary) -def write_reference(path, mesh_file): +def periodic_identification(num_vertices): + """Each vertex is identified with the smallest vertex index of its periodic class.""" + parent = list(range(num_vertices)) + + def find(v): + while parent[v] != v: + parent[v] = parent[parent[v]] + v = parent[v] + return v + + for dim in range(3): + for _, tag in gmsh.model.getEntities(dim): + master, nodes, master_nodes, _ = gmsh.model.mesh.getPeriodicNodes(dim, tag) + if master == tag: + continue + for node, master_node in zip(nodes, master_nodes): + a, b = find(int(node) - 1), find(int(master_node) - 1) + parent[max(a, b)] = min(a, b) + return np.array([find(v) for v in range(num_vertices)]) + + +def write_reference(path, mesh_file, periodic=False): """Reference PUML data from a written mesh file, so coordinates match their text form.""" gmsh.clear() gmsh.open(mesh_file) @@ -109,6 +130,8 @@ def write_reference(path, mesh_file): out.create_dataset("geometry", data=vertices.astype("YwTuDfVYbJA%D9vdi zb(-ijsZ(K#lHu|{!~1>q+3W6u|NY(H{r�f9GC(_A{(!t@W&DSl@m0b!YP?XI3cp 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 bdt}?7=lhU-zqId(?tt9)-FAZZJ-+`3sbF$; literal 0 HcmV?d00001 From 20c86340fca2703389718e9295830810338a62f1 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 18:44:40 +0000 Subject: [PATCH 08/19] Validate the node tags in the GMSH parsers The msh4 parser used tag - 1 as the vertex index and only checked it with an assert, so any file whose tags do not form 1..N wrote out of bounds, e.g. dense tags starting at 1001. Element and periodic node references were not checked in either parser, and duplicate tags left vertices undefined. Now the node tags have to be contiguous; in msh4 files they may start at any positive tag. Duplicate tags and references to unknown nodes are reported, and $Nodes has to precede $Elements and $Periodic. Gmsh writes contiguous tags also for models with volumes outside of any physical group, so non-contiguous tags are rejected instead of remapped. Parametric node coordinates (Mesh.SaveParametric = 1) are skipped instead of being read as the next node, and facet node indices use std::size_t like the cell node indices. AI-generated. Model: Claude Opus 5.5 --- src/meshreader/GMSH4Parser.cpp | 57 +++++++++++++++++++++++++-------- src/meshreader/GMSH4Parser.h | 19 +++++++++++ src/meshreader/GMSHBuilder.h | 2 +- src/third_party/GMSH2Parser.cpp | 30 +++++++++++------ src/third_party/GMSH2Parser.h | 15 +++++++++ tests/CMakeLists.txt | 16 +++++++++ 6 files changed, 115 insertions(+), 24 deletions(-) diff --git a/src/meshreader/GMSH4Parser.cpp b/src/meshreader/GMSH4Parser.cpp index a2da382..18a2c2b 100644 --- a/src/meshreader/GMSH4Parser.cpp +++ b/src/meshreader/GMSH4Parser.cpp @@ -3,7 +3,6 @@ // SPDX-License-Identifier: BSD-3-Clause #include "GMSH4Parser.h" -#include #include #include "utils/logger.h" @@ -24,7 +23,6 @@ bool GMSH4Parser::parse_() { } bool hasEntities = false; - bool hasNodes = false; bool hasElements = false; bool hasPeriodic = false; @@ -34,7 +32,7 @@ bool GMSH4Parser::parse_() { hasEntities = parseEntities(); break; case tndm::GMSHToken::nodes: - hasNodes = parseNodes(); + parseNodes(); break; case tndm::GMSHToken::elements: hasElements = parseElements(); @@ -199,18 +197,32 @@ bool GMSH4Parser::parseNodes() { getNextToken(); const std::size_t numVertices = expectNonNegativeInt(); getNextToken(); - [[maybe_unused]] const std::size_t minNodeTag = expectNonNegativeInt(); + const std::size_t minNodeTag = expectNonNegativeInt(); getNextToken(); - [[maybe_unused]] const std::size_t maxNodeTag = expectNonNegativeInt(); + const std::size_t maxNodeTag = expectNonNegativeInt(); + if (numVertices > 0 && + (minNodeTag == 0 || maxNodeTag < minNodeTag || maxNodeTag - minNodeTag + 1 != numVertices)) { + char buf[192]; + snprintf(buf, sizeof(buf), + "Non-contiguous node tags are not supported (%zu nodes with tags from %zu to %zu)", + numVertices, minNodeTag, maxNodeTag); + return logErrorAnnotated(buf); + } + hasNodes = true; + firstNodeTag = minNodeTag; + numNodes = numVertices; builder->setNumVertices(numVertices); + // with as many distinct tags as the tag range is long, every node is defined exactly once + std::vector defined(numVertices, false); + for (std::size_t blockIdx = 0; blockIdx < numBlocks; blockIdx++) { getNextToken(); - [[maybe_unused]] const std::size_t dim = expectNonNegativeInt(); + const std::size_t dim = expectNonNegativeInt(); getNextToken(); [[maybe_unused]] const std::size_t entityTag = expectNonNegativeInt(); getNextToken(); - [[maybe_unused]] const std::size_t parametric = expectNonNegativeInt(); + const std::size_t parametric = expectNonNegativeInt(); getNextToken(); const std::size_t numVerticesInBlock = expectNonNegativeInt(); std::vector vertexIds; @@ -218,17 +230,28 @@ bool GMSH4Parser::parseNodes() { // first read vertex ids for (std::size_t vertexIdx = 0; vertexIdx < numVerticesInBlock; ++vertexIdx) { getNextToken(); - vertexIds.push_back(expectNonNegativeInt() - 1); + const std::size_t index = expectNodeIndex(); + if (defined[index]) { + char buf[128]; + snprintf(buf, sizeof(buf), "Duplicate node tag %zu", firstNodeTag + index); + return logErrorAnnotated(buf); + } + defined[index] = true; + vertexIds.push_back(index); } - // then read vertex data + // then read vertex data; parametric nodes carry dim additional parametric coordinates + const std::size_t numParametric = parametric != 0 ? dim : 0; for (std::size_t vertexIdx = 0; vertexIdx < numVerticesInBlock; ++vertexIdx) { std::array x{}; for (std::size_t i = 0; i < 3; i++) { getNextToken(); x[i] = expectNumber(); } - assert(vertexIds.at(vertexIdx) < numVertices); - builder->setVertex(vertexIds.at(vertexIdx), x); + for (std::size_t i = 0; i < numParametric; ++i) { + getNextToken(); + expectNumber(); + } + builder->setVertex(vertexIds[vertexIdx], x); } } getNextToken(); @@ -240,6 +263,9 @@ bool GMSH4Parser::parseNodes() { } bool GMSH4Parser::parseElements() { + if (!hasNodes) { + return logErrorAnnotated("Expected $Nodes before $Elements"); + } getNextToken(); const std::size_t numBlocks = expectNonNegativeInt(); getNextToken(); @@ -271,7 +297,7 @@ bool GMSH4Parser::parseElements() { [[maybe_unused]] const std::size_t id = expectNonNegativeInt(); for (std::size_t nodeIdx = 0; nodeIdx < NumNodes[type - 1]; nodeIdx++) { getNextToken(); - nodes[nodeIdx] = expectNonNegativeInt() - 1; + nodes[nodeIdx] = static_cast(expectNodeIndex()); } std::size_t tag = (type == 2) ? physicalSurfaceIds[entityTag] : physicalVolumeIds[entityTag]; builder->addElement(type, tag, nodes.data(), NumNodes[type - 1]); @@ -287,6 +313,9 @@ bool GMSH4Parser::parseElements() { } bool GMSH4Parser::parsePeriodic() { + if (!hasNodes) { + return logErrorAnnotated("Expected $Nodes before $Periodic"); + } getNextToken(); const auto numPeriodic = expectNonNegativeInt(); @@ -312,9 +341,9 @@ bool GMSH4Parser::parsePeriodic() { for (std::size_t i = 0; i < identifySize; ++i) { getNextToken(); - const std::size_t nodeId = expectNonNegativeInt() - 1; + const std::size_t nodeId = expectNodeIndex(); getNextToken(); - const std::size_t nodeIdentifyId = expectNonNegativeInt() - 1; + const std::size_t nodeIdentifyId = expectNodeIndex(); builder->addVertexLink(nodeId, nodeIdentifyId); } } diff --git a/src/meshreader/GMSH4Parser.h b/src/meshreader/GMSH4Parser.h index 73c8d17..2da26f2 100644 --- a/src/meshreader/GMSH4Parser.h +++ b/src/meshreader/GMSH4Parser.h @@ -4,6 +4,7 @@ #ifndef PUMGEN_SRC_MESHREADER_GMSH4PARSER_H_ #define PUMGEN_SRC_MESHREADER_GMSH4PARSER_H_ +#include #include #include #include @@ -22,6 +23,24 @@ class GMSH4Parser : public tndm::GMSHParser { std::map physicalSurfaceIds; std::map physicalVolumeIds; + // the node tags form the contiguous range [firstNodeTag, firstNodeTag + numNodes) + bool hasNodes = false; + std::size_t firstNodeTag = 0; + std::size_t numNodes = 0; + + /** + * Reads a node tag and returns the index of the node. + */ + std::size_t expectNodeIndex() { + const std::size_t tag = expectNonNegativeInt(); + if (tag < firstNodeTag || tag - firstNodeTag >= numNodes) { + char buf[128]; + snprintf(buf, sizeof(buf), "Unknown node tag %zu", tag); + return logErrorAnnotated(buf); + } + return tag - firstNodeTag; + } + unsigned long expectNonNegativeInt() { if (curTok != tndm::GMSHToken::integer || lexer.getInteger() < 0) { return logErrorAnnotated("Expected non-negative integer"); diff --git a/src/meshreader/GMSHBuilder.h b/src/meshreader/GMSHBuilder.h index d47cebc..d5935a2 100644 --- a/src/meshreader/GMSHBuilder.h +++ b/src/meshreader/GMSHBuilder.h @@ -147,7 +147,7 @@ template class GMSHBuilder : public tndm::GMS public: using vertex_t = std::array; using element_t = std::array; - using facet_t = std::array; + using facet_t = std::array; std::vector vertices; std::vector identify; diff --git a/src/third_party/GMSH2Parser.cpp b/src/third_party/GMSH2Parser.cpp index 9d57f34..0f0965a 100644 --- a/src/third_party/GMSH2Parser.cpp +++ b/src/third_party/GMSH2Parser.cpp @@ -6,6 +6,7 @@ #include "GMSHLexer.h" #include +#include namespace tndm { @@ -55,6 +56,11 @@ bool GMSH2Parser::parseNodes() { } std::size_t numVertices = lexer.getInteger(); builder->setNumVertices(numVertices); + hasNodes = true; + numNodes = numVertices; + + // with numVertices distinct tags in [1, numVertices], every node is defined exactly once + std::vector defined(numVertices, false); for (std::size_t i = 0; i < numVertices; ++i) { getNextToken(); @@ -65,6 +71,12 @@ bool GMSH2Parser::parseNodes() { return logErrorAnnotated(buf); } std::size_t id = lexer.getInteger() - 1; + if (defined[id]) { + char buf[128]; + snprintf(buf, sizeof(buf), "Duplicate node tag %zu", id + 1); + return logErrorAnnotated(buf); + } + defined[id] = true; std::array x; for (std::size_t i = 0; i < 3; ++i) { @@ -86,6 +98,9 @@ bool GMSH2Parser::parseNodes() { } bool GMSH2Parser::parseElements() { + if (!hasNodes) { + return logErrorAnnotated("Expected $Nodes before $Elements"); + } getNextToken(); if (curTok != GMSHToken::integer || lexer.getInteger() < 0) { return logErrorAnnotated("Expected non-negative integer"); @@ -132,13 +147,7 @@ bool GMSH2Parser::parseElements() { for (std::size_t nodeIdx = 0; nodeIdx < NumNodes[type - 1]; ++nodeIdx) { getNextToken(); - if (curTok != GMSHToken::integer || lexer.getInteger() < 1) { - char buf[128]; - snprintf(buf, sizeof(buf), "Expected node number > 0 (%zu/%zu for type %li)", nodeIdx + 1, - NumNodes[type - 1], type); - return logErrorAnnotated(buf); - } - nodes[nodeIdx] = lexer.getInteger() - 1; + nodes[nodeIdx] = static_cast(expectNodeIndex()); } builder->addElement(type, tag, nodes.data(), NumNodes[type - 1]); @@ -153,6 +162,9 @@ bool GMSH2Parser::parseElements() { } bool GMSH2Parser::parsePeriodic() { + if (!hasNodes) { + return logErrorAnnotated("Expected $Nodes before $Periodic"); + } getNextToken(); const auto numPeriodic = expectNonNegativeInt(); @@ -180,9 +192,9 @@ bool GMSH2Parser::parsePeriodic() { for (std::size_t i = 0; i < identifySize; ++i) { getNextToken(); - const std::size_t nodeId = expectNonNegativeInt() - 1; + const std::size_t nodeId = expectNodeIndex(); getNextToken(); - const std::size_t nodeIdentifyId = expectNonNegativeInt() - 1; + const std::size_t nodeIdentifyId = expectNodeIndex(); builder->addVertexLink(nodeId, nodeIdentifyId); } } diff --git a/src/third_party/GMSH2Parser.h b/src/third_party/GMSH2Parser.h index 83e33bb..38f9e33 100644 --- a/src/third_party/GMSH2Parser.h +++ b/src/third_party/GMSH2Parser.h @@ -37,6 +37,21 @@ class GMSH2Parser : public GMSHParser { return num.value(); } + // the node tags are 1, ..., numNodes + bool hasNodes = false; + std::size_t numNodes = 0; + + /** + * Reads a node tag and returns the index of the node. + */ + std::size_t expectNodeIndex() { + if (curTok != GMSHToken::integer || lexer.getInteger() < 1 || + static_cast(lexer.getInteger()) > numNodes) { + return logErrorAnnotated("Unknown node tag"); + } + return static_cast(lexer.getInteger()) - 1; + } + bool parseNodes(); bool parseElements(); bool parsePeriodic(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 45fe62e..a436afb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -120,3 +120,19 @@ foreach(ranks 1 2 3) pumgen_add_test(msh4.periodic.np${ranks} RANKS ${ranks} SOURCE msh4 INPUT periodic-v41.msh REFERENCE periodic.puml.h5 IDENTIFY) endforeach() + +# node tags +pumgen_add_test(msh4.coarse-offset.np2 RANKS 2 SOURCE msh4 INPUT coarse-offset-v41.msh + REFERENCE coarse.puml.h5) +pumgen_add_test(msh4.periodic-parametric.np2 RANKS 2 SOURCE msh4 INPUT periodic-parametric-v41.msh + REFERENCE periodic.puml.h5 IDENTIFY) +pumgen_add_test(msh4.tiny-sparse RANKS 1 SOURCE msh4 INPUT tiny-sparse-v41.msh + EXPECT_ERROR "Non-contiguous node tags are not supported") +pumgen_add_test(msh4.tiny-duplicate RANKS 1 SOURCE msh4 INPUT tiny-duplicate-v41.msh + EXPECT_ERROR "Duplicate node tag 2") +pumgen_add_test(msh4.tiny-badnode RANKS 1 SOURCE msh4 INPUT tiny-badnode-v41.msh + EXPECT_ERROR "Unknown node tag 9") +pumgen_add_test(msh2.tiny-duplicate RANKS 1 SOURCE msh2 INPUT tiny-duplicate-v22.msh + EXPECT_ERROR "Duplicate node tag 2") +pumgen_add_test(msh2.tiny-badnode RANKS 1 SOURCE msh2 INPUT tiny-badnode-v22.msh + EXPECT_ERROR "Unknown node tag") From 7cea1f7dc037e9fb94ef92b0b11ae13b4d315645 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 18:50:45 +0000 Subject: [PATCH 09/19] Fix higher-order msh4 meshes and reject partitioned files The msh4 parser picked the physical tag of an element from the surface map only for element type 2 (linear triangles) and from the volume map for all other types. The boundary conditions of higher-order meshes were thus looked up with surface entity tags in the volume map (208 of 320 cells wrong in the order-2 test mesh). The map is now chosen by the entity dimension of the element block, and lookups no longer insert entries. Elements of surfaces or volumes without a physical group still get 0, but pumgen now warns about them. Partitioned MSH files, whose physical groups live in $PartitionedEntities, are rejected instead of being read with all groups and boundary conditions 0. The minimum insphere assumed 4 nodes per cell and reported 0 for order 2; it now uses the four vertices of each cell. AI-generated. Model: Claude Opus 5.5 --- src/helper/InsphereCalculator.cpp | 24 +++++++++++++++-------- src/helper/InsphereCalculator.h | 7 ++++++- src/meshreader/GMSH4Parser.cpp | 27 +++++++++++++++++++++++--- src/meshreader/GMSH4Parser.h | 9 +++++++++ src/pumgen.cpp | 4 ++-- src/third_party/GMSHLexer.cpp | 3 +++ src/third_party/GMSHLexer.h | 1 + tests/CMakeLists.txt | 25 +++++++++++++++++++++--- tests/RunPumgenTest.cmake | 5 +++++ tests/fixtures/generate.py | 1 + tests/fixtures/tiny-nophysical-v41.msh | 24 +++++++++++++++++++++++ 11 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 tests/fixtures/tiny-nophysical-v41.msh diff --git a/src/helper/InsphereCalculator.cpp b/src/helper/InsphereCalculator.cpp index 309e35e..ddc2a0c 100644 --- a/src/helper/InsphereCalculator.cpp +++ b/src/helper/InsphereCalculator.cpp @@ -13,7 +13,11 @@ #include "MPIConvenience.h" std::vector calculateInsphere(const std::vector& connectivity, - const std::vector& geometry, MPI_Comm comm) { + const std::vector& geometry, std::size_t cellSize, + MPI_Comm comm) { + constexpr std::size_t VerticesPerCell = 4; + const std::size_t numCells = connectivity.size() / cellSize; + int commsize; int commrank; @@ -41,7 +45,11 @@ std::vector calculateInsphere(const std::vector& connectivi std::vector> outidxmap(commsize); - for (const auto& vertex : connectivity) { + for (std::size_t node = 0; node < connectivity.size(); ++node) { + if (node % cellSize >= VerticesPerCell) { + continue; + } + const auto vertex = connectivity[node]; auto itPosition = std::upper_bound(vertexDist.begin(), vertexDist.end(), vertex); auto position = std::distance(vertexDist.begin(), itPosition) - 1; auto localVertex = vertex - vertexDist[position]; @@ -84,7 +92,7 @@ std::vector calculateInsphere(const std::vector& connectivi tndm::mpi_type_t(), comm); } - std::vector outvertices(3 * connectivity.size()); + std::vector outvertices(3 * outdisp[commsize]); { std::vector invertices(3 * indisp[commsize]); @@ -102,12 +110,12 @@ std::vector calculateInsphere(const std::vector& connectivi } std::vector counter(commsize); - std::vector inspheres(connectivity.size() / 4); + std::vector inspheres(numCells); - for (std::size_t i = 0; i < connectivity.size() / 4; ++i) { - std::array, 4> vertices; - for (int j = 0; j < 4; ++j) { - auto vertex = connectivity[i * 4 + j]; + for (std::size_t i = 0; i < numCells; ++i) { + std::array, VerticesPerCell> vertices; + for (std::size_t j = 0; j < VerticesPerCell; ++j) { + auto vertex = connectivity[i * cellSize + j]; auto itPosition = std::upper_bound(vertexDist.begin(), vertexDist.end(), vertex); auto position = std::distance(vertexDist.begin(), itPosition) - 1; auto localVertex = vertex - vertexDist[position]; diff --git a/src/helper/InsphereCalculator.h b/src/helper/InsphereCalculator.h index 9a8e548..806e726 100644 --- a/src/helper/InsphereCalculator.h +++ b/src/helper/InsphereCalculator.h @@ -10,7 +10,12 @@ // assumes a contiguous distribution of all vertices over all processes +/** + * The insphere radius of each tetrahedron. Each cell has cellSize nodes, the first four of which + * are its vertices. + */ std::vector calculateInsphere(const std::vector& connectivity, - const std::vector& geometry, MPI_Comm comm); + const std::vector& geometry, std::size_t cellSize, + MPI_Comm comm); #endif // PUMGEN_SRC_HELPER_INSPHERECALCULATOR_H_ diff --git a/src/meshreader/GMSH4Parser.cpp b/src/meshreader/GMSH4Parser.cpp index 18a2c2b..e74a118 100644 --- a/src/meshreader/GMSH4Parser.cpp +++ b/src/meshreader/GMSH4Parser.cpp @@ -40,6 +40,9 @@ bool GMSH4Parser::parse_() { case tndm::GMSHToken::periodic: hasPeriodic = parsePeriodic(); break; + case tndm::GMSHToken::partitioned_entities: + return logErrorAnnotated( + "Partitioned MSH files are not supported; save the mesh without partitions"); default: getNextToken(); break; @@ -55,9 +58,27 @@ bool GMSH4Parser::parse_() { if (!hasElements) { return logError("Missing $Elements section"); } + if (!unassignedVolumes.empty() || !unassignedSurfaces.empty()) { + logWarning() << "The elements of" << unassignedVolumes.size() << "volume(s) and" + << unassignedSurfaces.size() + << "surface(s) without a physical group get group or boundary condition 0"; + } return true; } +long GMSH4Parser::physicalTag(std::size_t dim, unsigned long entityTag) { + if (dim != 2 && dim != 3) { + return 0; + } + const auto& physicalIds = dim == 3 ? physicalVolumeIds : physicalSurfaceIds; + const auto it = physicalIds.find(entityTag); + if (it == physicalIds.end()) { + (dim == 3 ? unassignedVolumes : unassignedSurfaces).insert(entityTag); + return 0; + } + return it->second; +} + bool GMSH4Parser::parseEntities() { getNextToken(); const std::size_t numPoints = expectNonNegativeInt(); @@ -280,7 +301,7 @@ bool GMSH4Parser::parseElements() { for (std::size_t blockIdx = 0; blockIdx < numBlocks; blockIdx++) { getNextToken(); - [[maybe_unused]] const std::size_t dim = expectNonNegativeInt(); + const std::size_t dim = expectNonNegativeInt(); getNextToken(); const std::size_t entityTag = expectNonNegativeInt(); getNextToken(); @@ -292,6 +313,7 @@ bool GMSH4Parser::parseElements() { } getNextToken(); const std::size_t numElementsInBlock = expectNonNegativeInt(); + const long tag = numElementsInBlock > 0 ? physicalTag(dim, entityTag) : 0; for (std::size_t elementIdx = 0; elementIdx < numElementsInBlock; ++elementIdx) { getNextToken(); [[maybe_unused]] const std::size_t id = expectNonNegativeInt(); @@ -299,8 +321,7 @@ bool GMSH4Parser::parseElements() { getNextToken(); nodes[nodeIdx] = static_cast(expectNodeIndex()); } - std::size_t tag = (type == 2) ? physicalSurfaceIds[entityTag] : physicalVolumeIds[entityTag]; - builder->addElement(type, tag, nodes.data(), NumNodes[type - 1]); + builder->addElement(static_cast(type), tag, nodes.data(), NumNodes[type - 1]); } } getNextToken(); diff --git a/src/meshreader/GMSH4Parser.h b/src/meshreader/GMSH4Parser.h index 2da26f2..97be825 100644 --- a/src/meshreader/GMSH4Parser.h +++ b/src/meshreader/GMSH4Parser.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -22,6 +23,14 @@ class GMSH4Parser : public tndm::GMSHParser { private: std::map physicalSurfaceIds; std::map physicalVolumeIds; + // surface and volume entities with elements, but without a physical group + std::set unassignedSurfaces; + std::set unassignedVolumes; + + /** + * The physical tag of the elements of an entity; 0 if the entity has no physical group. + */ + long physicalTag(std::size_t dim, unsigned long entityTag); // the node tags form the contiguous range [firstNodeTag, firstNodeTag + numNodes) bool hasNodes = false; diff --git a/src/pumgen.cpp b/src/pumgen.cpp index 68ed458..c7651a4 100644 --- a/src/pumgen.cpp +++ b/src/pumgen.cpp @@ -455,8 +455,8 @@ int main(int argc, char* argv[]) { logInfo() << "Total vertex count:" << globalSize[1]; logInfo() << "Vertex identification:" << meshInput->hasIdentify(); - auto inspheres = - calculateInsphere(meshInput->connectivity(), meshInput->geometry(), MPI_COMM_WORLD); + auto inspheres = calculateInsphere(meshInput->connectivity(), meshInput->geometry(), + meshInput->cellSize(), MPI_COMM_WORLD); double min = inspheres.empty() ? std::numeric_limits::infinity() : *std::min_element(inspheres.begin(), inspheres.end()); MPI_Reduce((rank == 0 ? MPI_IN_PLACE : &min), &min, 1, MPI_DOUBLE, MPI_MIN, 0, MPI_COMM_WORLD); diff --git a/src/third_party/GMSHLexer.cpp b/src/third_party/GMSHLexer.cpp index 65cbef6..059407a 100644 --- a/src/third_party/GMSHLexer.cpp +++ b/src/third_party/GMSHLexer.cpp @@ -73,6 +73,9 @@ GMSHToken GMSHLexer::getToken() { case "EndPeriodic"_fnv1a: token = GMSHToken::end_periodic; break; + case "PartitionedEntities"_fnv1a: + token = GMSHToken::partitioned_entities; + break; default: break; } diff --git a/src/third_party/GMSHLexer.h b/src/third_party/GMSHLexer.h index 2b9eb67..740f907 100644 --- a/src/third_party/GMSHLexer.h +++ b/src/third_party/GMSHLexer.h @@ -25,6 +25,7 @@ enum class GMSHToken { end_elements, periodic, end_periodic, + partitioned_entities, unknown_section, unknown_token }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a436afb..26ab1de 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,11 +12,11 @@ set(PUMGEN_TEST_OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/output) file(MAKE_DIRECTORY ${PUMGEN_TEST_OUTPUT}) # pumgen_add_test( RANKS SOURCE INPUT -# [REFERENCE [TOLERANCE ] [IDENTIFY]] [EXPECT_ERROR ] -# [ARGS ...]) +# [REFERENCE [TOLERANCE ] [IDENTIFY]] +# [EXPECT_OUTPUT ] [EXPECT_ERROR ] [ARGS ...]) function(pumgen_add_test name) cmake_parse_arguments(PARSE_ARGV 1 TEST "IDENTIFY" - "RANKS;SOURCE;INPUT;REFERENCE;TOLERANCE;EXPECT_ERROR" "ARGS") + "RANKS;SOURCE;INPUT;REFERENCE;TOLERANCE;EXPECT_OUTPUT;EXPECT_ERROR" "ARGS") set(output ${PUMGEN_TEST_OUTPUT}/${name}.puml.h5) set(command ${MPIEXEC_EXECUTABLE} @@ -33,6 +33,9 @@ function(pumgen_add_test name) string(REPLACE ";" "|" command "${command}") set(script_args -DCOMMAND=${command} -DOUTPUT=${output}) + if(DEFINED TEST_EXPECT_OUTPUT) + list(APPEND script_args "-DEXPECT_OUTPUT=${TEST_EXPECT_OUTPUT}") + endif() if(DEFINED TEST_EXPECT_ERROR) list(APPEND script_args "-DEXPECT_ERROR=${TEST_EXPECT_ERROR}") elseif(DEFINED TEST_REFERENCE AND H5DIFF_EXECUTABLE) @@ -136,3 +139,19 @@ pumgen_add_test(msh2.tiny-duplicate RANKS 1 SOURCE msh2 INPUT tiny-duplicate-v22 EXPECT_ERROR "Duplicate node tag 2") pumgen_add_test(msh2.tiny-badnode RANKS 1 SOURCE msh2 INPUT tiny-badnode-v22.msh EXPECT_ERROR "Unknown node tag") + +# higher-order meshes (with the inspheres of the linear mesh) and partitioned files +foreach(ranks 1 2) + pumgen_add_test(msh4.coarse-o2.np${ranks} RANKS ${ranks} SOURCE msh4 INPUT coarse-o2-v41.msh + REFERENCE coarse-o2.puml.h5 ARGS --order=2 + EXPECT_OUTPUT "Minimum insphere found: 0[.]03184[67]") +endforeach() +pumgen_add_test(msh2.coarse-o2.np2 RANKS 2 SOURCE msh2 INPUT coarse-o2-v22.msh + REFERENCE coarse-o2.puml.h5 ARGS --order=2 + EXPECT_OUTPUT "Minimum insphere found: 0[.]03184[67]") +pumgen_add_test(msh4.coarse.insphere RANKS 1 SOURCE msh4 INPUT coarse-v41.msh + EXPECT_OUTPUT "Minimum insphere found: 0[.]03184[67]") +pumgen_add_test(msh4.coarse-partitioned RANKS 1 SOURCE msh4 INPUT coarse-partitioned-v41.msh + EXPECT_ERROR "Partitioned MSH files are not supported") +pumgen_add_test(msh4.tiny-nophysical RANKS 1 SOURCE msh4 INPUT tiny-nophysical-v41.msh + EXPECT_OUTPUT "1 volume[(]s[)] and 0 surface[(]s[)] without a physical group") diff --git a/tests/RunPumgenTest.cmake b/tests/RunPumgenTest.cmake index 3c88ea2..df04c6a 100644 --- a/tests/RunPumgenTest.cmake +++ b/tests/RunPumgenTest.cmake @@ -12,6 +12,7 @@ # IDENTIFY (optional) also compare the vertex identification of periodic meshes # H5DIFF h5diff executable, required together with REFERENCE # EXPECT_ERROR (optional) pumgen has to fail with a message matching this regular expression +# EXPECT_OUTPUT (optional) the output of a successful run has to match this regular expression string(REPLACE "|" ";" command "${COMMAND}") file(REMOVE "${OUTPUT}") @@ -38,6 +39,10 @@ if(NOT result EQUAL 0) message(FATAL_ERROR "pumgen failed: ${result}") endif() +if(DEFINED EXPECT_OUTPUT AND NOT log MATCHES "${EXPECT_OUTPUT}") + message(FATAL_ERROR "The output does not match \"${EXPECT_OUTPUT}\"") +endif() + if(DEFINED REFERENCE) set(failures "") set(datasets connect geometry group boundary) diff --git a/tests/fixtures/generate.py b/tests/fixtures/generate.py index f7340fa..7315829 100644 --- a/tests/fixtures/generate.py +++ b/tests/fixtures/generate.py @@ -256,6 +256,7 @@ def generate_periodic(): def generate_tiny(): variants = { "tiny-v41.msh": TINY_V41, + "tiny-nophysical-v41.msh": TINY_V41.replace("1 0 0 0 1 1 1 1 7 0", "1 0 0 0 1 1 1 0 0"), "tiny-v40.msh": TINY_V41.replace("4.1 0 8", "4 0 8"), "tiny-truncated-v41.msh": TINY_V41.split("$Elements")[0] + "$Elements\n1 1 1 1\n3 1 4 1\n1 1 2\n", "tiny-badnumber-v41.msh": TINY_V41.replace("1 0 0\n0 1 0", "1 abc 0\n0 1 0"), diff --git a/tests/fixtures/tiny-nophysical-v41.msh b/tests/fixtures/tiny-nophysical-v41.msh new file mode 100644 index 0000000..e7ffbbc --- /dev/null +++ b/tests/fixtures/tiny-nophysical-v41.msh @@ -0,0 +1,24 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$Entities +0 0 0 1 +1 0 0 0 1 1 1 0 0 +$EndEntities +$Nodes +1 4 1 4 +3 1 0 4 +1 +2 +3 +4 +0 0 0 +1 0 0 +0 1 0 +0 0 1 +$EndNodes +$Elements +1 1 1 1 +3 1 4 1 +1 1 2 3 4 +$EndElements From 0e55cff56ffa5f8f9cbe1f3067a807d0ef205bdc Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 19:07:50 +0000 Subject: [PATCH 10/19] Replace the GMSH lexer by a buffered reader The lexer read the file character by character through std::istream::get and converted numbers with strtol/strtod; on a mesh with 3.6M cells (170 MB MSH 4.1, 196 MB MSH 2.2) parsing took 2.18 s and 2.82 s. The new MshInput reads the file in 16 MiB blocks and parses the numbers in place with std::from_chars (with a strtod fallback for standard libraries without floating-point from_chars), which brings parsing down to 0.41 s and 0.54 s (5.3x); the results are identical. The parsers become part of src/meshreader and read the values they expect instead of generic tokens. Unneeded sections are skipped as a whole. Errors are still reported with line and column; binary MSH 2 files get a message of their own. A new unit test parses the fixtures with a buffer of 256 bytes, so that tokens and section markers straddle the buffer boundaries. AI-generated. Model: Claude Opus 5.5 --- CMakeLists.txt | 6 +- src/meshreader/GMSH2Parser.cpp | 156 ++++++ src/meshreader/GMSH2Parser.h | 39 ++ src/meshreader/GMSH4Parser.cpp | 443 +++++++----------- src/meshreader/GMSH4Parser.h | 77 ++- src/meshreader/GMSHBuilder.h | 4 +- .../GMSHMeshBuilder.h | 11 +- src/meshreader/GMSHParser.cpp | 87 ++++ src/{third_party => meshreader}/GMSHParser.h | 136 +++--- src/meshreader/MshInput.cpp | 150 ++++++ src/meshreader/MshInput.h | 177 +++++++ src/pumgen.cpp | 4 +- src/third_party/GMSH2Parser.cpp | 211 --------- src/third_party/GMSH2Parser.h | 63 --- src/third_party/GMSHLexer.cpp | 130 ----- src/third_party/GMSHLexer.h | 66 --- src/third_party/GMSHParser.cpp | 34 -- src/third_party/Hash.h | 24 - src/third_party/README.md | 7 +- tests/CMakeLists.txt | 13 + tests/ReaderTest.cpp | 97 ++++ 21 files changed, 1000 insertions(+), 935 deletions(-) create mode 100644 src/meshreader/GMSH2Parser.cpp create mode 100644 src/meshreader/GMSH2Parser.h rename src/{third_party => meshreader}/GMSHMeshBuilder.h (73%) create mode 100644 src/meshreader/GMSHParser.cpp rename src/{third_party => meshreader}/GMSHParser.h (69%) create mode 100644 src/meshreader/MshInput.cpp create mode 100644 src/meshreader/MshInput.h delete mode 100644 src/third_party/GMSH2Parser.cpp delete mode 100644 src/third_party/GMSH2Parser.h delete mode 100644 src/third_party/GMSHLexer.cpp delete mode 100644 src/third_party/GMSHLexer.h delete mode 100644 src/third_party/GMSHParser.cpp delete mode 100644 src/third_party/Hash.h create mode 100644 tests/ReaderTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d40349..be1cf15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,11 +37,11 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O2") add_executable(pumgen ${CMAKE_CURRENT_SOURCE_DIR}/src/pumgen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/GambitReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/GMSHParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/GMSH2Parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/GMSH4Parser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/MshInput.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/input/ParallelVertexFilter.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/third_party/GMSHLexer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/third_party/GMSHParser.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/third_party/GMSH2Parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/helper/InsphereCalculator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/helper/MPIConvenience.cpp ) diff --git a/src/meshreader/GMSH2Parser.cpp b/src/meshreader/GMSH2Parser.cpp new file mode 100644 index 0000000..4365f3b --- /dev/null +++ b/src/meshreader/GMSH2Parser.cpp @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2022 SeisSol Group +// SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München +// +// SPDX-License-Identifier: BSD-3-Clause +#include "GMSH2Parser.h" + +#include +#include +#include +#include + +namespace puml { + +void GMSH2Parser::parse_() { + const auto format = parseMeshFormatHeader(); + if (format.version < 2.0 || format.version >= 3.0) { + char buf[128]; + snprintf(buf, sizeof(buf), "Unsupported MSH version %.1lf", format.version); + failFile(buf); + } + if (format.fileType != 0) { + failFile("Binary MSH 2 files are not supported; write the mesh as ASCII (Mesh.Binary = 0)"); + } + expectToken("$EndMeshFormat"); + + bool hasElements = false; + while (true) { + const auto section = nextSection(); + if (section.empty()) { + break; + } + if (section == "$Nodes") { + parseNodes(); + } else if (section == "$Elements") { + parseElements(); + hasElements = true; + } else if (section == "$Periodic") { + parsePeriodic(); + } else { + skipSection(section); + } + } + + if (!hasNodes) { + failFile("Missing $Nodes section"); + } + if (!hasElements) { + failFile("Missing $Elements section"); + } +} + +std::size_t GMSH2Parser::readNodeIndex() { + input->skipWhitespace(); + const auto offset = input->offset(); + const auto tag = input->readInteger(); + if (!tag || *tag < 1 || *tag > numNodes) { + failAt(offset, "Unknown node tag"); + } + return *tag - 1; +} + +void GMSH2Parser::parseNodes() { + const std::size_t numVertices = expectSize(); + builder->setNumVertices(numVertices); + hasNodes = true; + numNodes = numVertices; + + // with numVertices distinct tags in [1, numVertices], every node is defined exactly once + std::vector defined(numVertices, false); + + for (std::size_t i = 0; i < numVertices; ++i) { + input->skipWhitespace(); + const auto offset = input->offset(); + const auto tag = input->readInteger(); + if (!tag || *tag < 1 || *tag > numVertices) { + failAt(offset, "Expected node-tag with 1 <= node-tag <= " + std::to_string(numVertices)); + } + const std::size_t id = *tag - 1; + if (defined[id]) { + failAt(offset, "Duplicate node tag " + std::to_string(*tag)); + } + defined[id] = true; + + std::array x{}; + for (auto& coordinate : x) { + coordinate = expectNumber(); + } + builder->setVertex(static_cast(id), x); + } + expectToken("$EndNodes"); +} + +void GMSH2Parser::parseElements() { + if (!hasNodes) { + fail("Expected $Nodes before $Elements"); + } + const std::size_t numElements = expectSize(); + builder->setNumElements(numElements); + + std::array nodes{}; + + for (std::size_t elementIdx = 0; elementIdx < numElements; ++elementIdx) { + expectInteger(); // element tag + input->skipWhitespace(); + const auto typeOffset = input->offset(); + const long type = expectInteger(); + if (type < 1 || type > static_cast(NumTypes) || NumNodes[type - 1] == 0) { + failAt(typeOffset, "Unknown element type " + std::to_string(type)); + } + const std::size_t numTags = expectSize(); + // the physical tag; elements without tags get 0 + long tag = 0; + for (std::size_t tagIdx = 0; tagIdx < numTags; ++tagIdx) { + const long value = expectInteger(); + if (tagIdx == 0) { + tag = value; + } + } + const std::size_t numNodesPerElement = NumNodes[type - 1]; + for (std::size_t nodeIdx = 0; nodeIdx < numNodesPerElement; ++nodeIdx) { + nodes[nodeIdx] = static_cast(readNodeIndex()); + } + builder->addElement(type, tag, nodes.data(), numNodesPerElement); + } + expectToken("$EndElements"); +} + +void GMSH2Parser::parsePeriodic() { + if (!hasNodes) { + fail("Expected $Nodes before $Periodic"); + } + const std::size_t numPeriodic = expectSize(); + + // only the node identification is needed + for (std::size_t blockIdx = 0; blockIdx < numPeriodic; ++blockIdx) { + expectSize(); // entity dimension + expectInteger(); // entity tag + expectInteger(); // master entity tag + if (input->peek() == 'A') { + // "Affine" and 16 values + expectToken("Affine"); + for (int i = 0; i < 16; ++i) { + expectNumber(); + } + } + const std::size_t numIdentified = expectSize(); + for (std::size_t i = 0; i < numIdentified; ++i) { + const std::size_t node = readNodeIndex(); + const std::size_t master = readNodeIndex(); + builder->addVertexLink(node, master); + } + } + expectToken("$EndPeriodic"); +} + +} // namespace puml diff --git a/src/meshreader/GMSH2Parser.h b/src/meshreader/GMSH2Parser.h new file mode 100644 index 0000000..9b561e6 --- /dev/null +++ b/src/meshreader/GMSH2Parser.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2022 SeisSol Group +// SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München +// +// SPDX-License-Identifier: BSD-3-Clause +#ifndef PUMGEN_SRC_MESHREADER_GMSH2PARSER_H_ +#define PUMGEN_SRC_MESHREADER_GMSH2PARSER_H_ + +#include + +#include "GMSHParser.h" + +namespace puml { + +/** + * Parser for MSH 2 files (ASCII). + */ +class GMSH2Parser : public GMSHParser { + public: + using GMSHParser::GMSHParser; + + private: + void parse_() override; + void parseNodes(); + void parseElements(); + void parsePeriodic(); + + /** + * Reads a node tag and returns the index of the node. + */ + std::size_t readNodeIndex(); + + // the node tags are 1, ..., numNodes + bool hasNodes = false; + std::size_t numNodes = 0; +}; + +} // namespace puml + +#endif // PUMGEN_SRC_MESHREADER_GMSH2PARSER_H_ diff --git a/src/meshreader/GMSH4Parser.cpp b/src/meshreader/GMSH4Parser.cpp index e74a118..8264b7e 100644 --- a/src/meshreader/GMSH4Parser.cpp +++ b/src/meshreader/GMSH4Parser.cpp @@ -3,231 +3,139 @@ // SPDX-License-Identifier: BSD-3-Clause #include "GMSH4Parser.h" +#include #include +#include +#include +#include #include "utils/logger.h" namespace puml { -bool GMSH4Parser::parse_() { - getNextToken(); - const double version = parseMeshFormat(); - if (version < 4.0 || version >= 5.0) { +void GMSH4Parser::parse_() { + const auto format = parseMeshFormatHeader(); + if (format.version < 4.0 || format.version >= 5.0) { char buf[128]; - snprintf(buf, sizeof(buf), "Unsupported MSH version %.1lf", version); - return logError(buf); + snprintf(buf, sizeof(buf), "Unsupported MSH version %.1lf", format.version); + failFile(buf); } - if (version < 4.1) { - return logError( - "MSH 4.0 is not supported; write the mesh as MSH 4.1 (Mesh.MshFileVersion = 4.1)"); + if (format.version < 4.1) { + failFile("MSH 4.0 is not supported; write the mesh as MSH 4.1 (Mesh.MshFileVersion = 4.1)"); } + if (format.fileType == 1) { + failFile("Binary MSH files are not supported; write the mesh as ASCII (Mesh.Binary = 0)"); + } + if (format.fileType != 0) { + failFile("Expected file type 0 (ASCII)"); + } + expectToken("$EndMeshFormat"); bool hasEntities = false; bool hasElements = false; - bool hasPeriodic = false; - - while (curTok != tndm::GMSHToken::eof) { - switch (curTok) { - case tndm::GMSHToken::entities: - hasEntities = parseEntities(); + while (true) { + const auto section = nextSection(); + if (section.empty()) { break; - case tndm::GMSHToken::nodes: + } + if (section == "$Entities") { + parseEntities(); + hasEntities = true; + } else if (section == "$Nodes") { parseNodes(); - break; - case tndm::GMSHToken::elements: - hasElements = parseElements(); - break; - case tndm::GMSHToken::periodic: - hasPeriodic = parsePeriodic(); - break; - case tndm::GMSHToken::partitioned_entities: - return logErrorAnnotated( - "Partitioned MSH files are not supported; save the mesh without partitions"); - default: - getNextToken(); - break; + } else if (section == "$Elements") { + parseElements(); + hasElements = true; + } else if (section == "$Periodic") { + parsePeriodic(); + } else if (section == "$PartitionedEntities") { + failFile("Partitioned MSH files are not supported; save the mesh without partitions"); + } else { + skipSection(section); } } if (!hasEntities) { - return logError("Missing $Entities section"); + failFile("Missing $Entities section"); } if (!hasNodes) { - return logError("Missing $Nodes section"); + failFile("Missing $Nodes section"); } if (!hasElements) { - return logError("Missing $Elements section"); + failFile("Missing $Elements section"); } if (!unassignedVolumes.empty() || !unassignedSurfaces.empty()) { logWarning() << "The elements of" << unassignedVolumes.size() << "volume(s) and" << unassignedSurfaces.size() << "surface(s) without a physical group get group or boundary condition 0"; } - return true; -} - -long GMSH4Parser::physicalTag(std::size_t dim, unsigned long entityTag) { - if (dim != 2 && dim != 3) { - return 0; - } - const auto& physicalIds = dim == 3 ? physicalVolumeIds : physicalSurfaceIds; - const auto it = physicalIds.find(entityTag); - if (it == physicalIds.end()) { - (dim == 3 ? unassignedVolumes : unassignedSurfaces).insert(entityTag); - return 0; - } - return it->second; } -bool GMSH4Parser::parseEntities() { - getNextToken(); - const std::size_t numPoints = expectNonNegativeInt(); - getNextToken(); - const std::size_t numCurves = expectNonNegativeInt(); - getNextToken(); - const std::size_t numSurfaces = expectNonNegativeInt(); - getNextToken(); - const std::size_t numVolumes = expectNonNegativeInt(); +void GMSH4Parser::parseEntities() { + const std::size_t numPoints = readSize(); + const std::size_t numCurves = readSize(); + const std::size_t numSurfaces = readSize(); + const std::size_t numVolumes = readSize(); - // ignore points for (std::size_t i = 0; i < numPoints; ++i) { - getNextToken(); - [[maybe_unused]] const std::size_t id = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const double x_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double y_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double z_1 = expectNumber(); - getNextToken(); - const std::size_t numTags = expectNonNegativeInt(); - // ignore tags - for (std::size_t tagIdx = 0; tagIdx < numTags; tagIdx++) { - getNextToken(); - } - } - - // ignore curves - for (std::size_t i = 0; i < numCurves; ++i) { - getNextToken(); - [[maybe_unused]] const std::size_t id = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const double x_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double y_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double z_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double x_2 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double y_2 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double z_2 = expectNumber(); - getNextToken(); - const std::size_t numPhysicalTags = expectNonNegativeInt(); - getNextToken(); - // ignore tags - for (std::size_t tagIdx = 0; tagIdx < numPhysicalTags; tagIdx++) { - getNextToken(); + readInt(); + for (int k = 0; k < 3; ++k) { + readDouble(); } - std::size_t numBoundaryTags = expectNonNegativeInt(); - // ignore boundary - for (std::size_t tagIdx = 0; tagIdx < numBoundaryTags; tagIdx++) { - getNextToken(); + const std::size_t numPhysicalTags = readSize(); + for (std::size_t j = 0; j < numPhysicalTags; ++j) { + readInt(); } } - // read surfaces - for (std::size_t i = 0; i < numSurfaces; ++i) { - getNextToken(); - const std::size_t id = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const double x_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double y_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double z_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double x_2 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double y_2 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double z_2 = expectNumber(); - getNextToken(); - const std::size_t numPhysicalTags = expectNonNegativeInt(); - std::vector tags; - for (std::size_t tagIdx = 0; tagIdx < numPhysicalTags; ++tagIdx) { - getNextToken(); - tags.push_back(expectNonNegativeInt()); - } - if (!tags.empty()) { - physicalSurfaceIds.insert_or_assign(id, tags.at(0)); - } - getNextToken(); - std::size_t numBoundaryTags = expectNonNegativeInt(); - // ignore boundary - for (std::size_t curveIdx = 0; curveIdx < numBoundaryTags; ++curveIdx) { - getNextToken(); + const auto readEntities = [&](std::size_t count, std::map* physicalIds) { + for (std::size_t i = 0; i < count; ++i) { + const long tag = readInt(); + for (int k = 0; k < 6; ++k) { + readDouble(); + } + const std::size_t numPhysicalTags = readSize(); + for (std::size_t j = 0; j < numPhysicalTags; ++j) { + const long physical = readInt(); + if (j == 0 && physicalIds != nullptr) { + physicalIds->insert_or_assign(tag, physical); + } + } + const std::size_t numBoundingEntities = readSize(); + for (std::size_t j = 0; j < numBoundingEntities; ++j) { + readInt(); + } } - } + }; + readEntities(numCurves, nullptr); + readEntities(numSurfaces, &physicalSurfaceIds); + readEntities(numVolumes, &physicalVolumeIds); - // read volumes - for (std::size_t i = 0; i < numVolumes; ++i) { - getNextToken(); - const std::size_t id = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const double x_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double y_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double z_1 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double x_2 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double y_2 = expectNumber(); - getNextToken(); - [[maybe_unused]] const double z_2 = expectNumber(); - getNextToken(); - const std::size_t numPhysicalTags = expectNonNegativeInt(); - std::vector tags; - for (std::size_t tagIdx = 0; tagIdx < numPhysicalTags; ++tagIdx) { - getNextToken(); - tags.push_back(expectNonNegativeInt()); - } - if (!tags.empty()) { - physicalVolumeIds.insert_or_assign(id, tags.at(0)); - } - getNextToken(); - const std::size_t numBoundaryTags = expectNonNegativeInt(); - // ignore boundary - for (std::size_t curveIdx = 0; curveIdx < numBoundaryTags; ++curveIdx) { - getNextToken(); - } - } - getNextToken(); - if (curTok != tndm::GMSHToken::end_entities) { - return logErrorAnnotated("Expected $EndEntities"); + expectToken("$EndEntities"); +} + +std::size_t GMSH4Parser::readNodeIndex() { + input->skipWhitespace(); + const auto offset = input->offset(); + const std::size_t tag = readSize(); + if (tag < firstNodeTag || tag - firstNodeTag >= numNodes) { + failAt(offset, "Unknown node tag " + std::to_string(tag)); } - getNextToken(); - return true; + return tag - firstNodeTag; } -bool GMSH4Parser::parseNodes() { - getNextToken(); - const std::size_t numBlocks = expectNonNegativeInt(); - getNextToken(); - const std::size_t numVertices = expectNonNegativeInt(); - getNextToken(); - const std::size_t minNodeTag = expectNonNegativeInt(); - getNextToken(); - const std::size_t maxNodeTag = expectNonNegativeInt(); +void GMSH4Parser::parseNodes() { + const std::size_t numBlocks = readSize(); + const std::size_t numVertices = readSize(); + const std::size_t minNodeTag = readSize(); + const std::size_t maxNodeTag = readSize(); if (numVertices > 0 && (minNodeTag == 0 || maxNodeTag < minNodeTag || maxNodeTag - minNodeTag + 1 != numVertices)) { char buf[192]; snprintf(buf, sizeof(buf), "Non-contiguous node tags are not supported (%zu nodes with tags from %zu to %zu)", numVertices, minNodeTag, maxNodeTag); - return logErrorAnnotated(buf); + fail(buf); } hasNodes = true; firstNodeTag = minNodeTag; @@ -236,146 +144,113 @@ bool GMSH4Parser::parseNodes() { // with as many distinct tags as the tag range is long, every node is defined exactly once std::vector defined(numVertices, false); + std::vector indices; - for (std::size_t blockIdx = 0; blockIdx < numBlocks; blockIdx++) { - getNextToken(); - const std::size_t dim = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const std::size_t entityTag = expectNonNegativeInt(); - getNextToken(); - const std::size_t parametric = expectNonNegativeInt(); - getNextToken(); - const std::size_t numVerticesInBlock = expectNonNegativeInt(); - std::vector vertexIds; - vertexIds.reserve(numVerticesInBlock); - // first read vertex ids - for (std::size_t vertexIdx = 0; vertexIdx < numVerticesInBlock; ++vertexIdx) { - getNextToken(); - const std::size_t index = expectNodeIndex(); + for (std::size_t blockIdx = 0; blockIdx < numBlocks; ++blockIdx) { + const long dim = readInt(); + readInt(); // entity tag + const long parametric = readInt(); + const std::size_t numVerticesInBlock = readSize(); + + indices.resize(numVerticesInBlock); + for (auto& index : indices) { + input->skipWhitespace(); + const auto offset = input->offset(); + index = readNodeIndex(); if (defined[index]) { - char buf[128]; - snprintf(buf, sizeof(buf), "Duplicate node tag %zu", firstNodeTag + index); - return logErrorAnnotated(buf); + failAt(offset, "Duplicate node tag " + std::to_string(firstNodeTag + index)); } defined[index] = true; - vertexIds.push_back(index); } - // then read vertex data; parametric nodes carry dim additional parametric coordinates - const std::size_t numParametric = parametric != 0 ? dim : 0; - for (std::size_t vertexIdx = 0; vertexIdx < numVerticesInBlock; ++vertexIdx) { + + // parametric nodes carry dim additional parametric coordinates + const long numParametric = parametric != 0 ? dim : 0; + for (const auto index : indices) { std::array x{}; - for (std::size_t i = 0; i < 3; i++) { - getNextToken(); - x[i] = expectNumber(); + for (auto& coordinate : x) { + coordinate = readDouble(); } - for (std::size_t i = 0; i < numParametric; ++i) { - getNextToken(); - expectNumber(); + for (long i = 0; i < numParametric; ++i) { + readDouble(); } - builder->setVertex(vertexIds[vertexIdx], x); + builder->setVertex(static_cast(index), x); } } - getNextToken(); - if (curTok != tndm::GMSHToken::end_nodes) { - return logErrorAnnotated("Expected $EndNodes"); - } - getNextToken(); - return true; + expectToken("$EndNodes"); } -bool GMSH4Parser::parseElements() { +void GMSH4Parser::parseElements() { if (!hasNodes) { - return logErrorAnnotated("Expected $Nodes before $Elements"); + fail("Expected $Nodes before $Elements"); } - getNextToken(); - const std::size_t numBlocks = expectNonNegativeInt(); - getNextToken(); - const std::size_t numElements = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const std::size_t minElementTag = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const std::size_t maxElementTag = expectNonNegativeInt(); + const std::size_t numBlocks = readSize(); + const std::size_t numElements = readSize(); + readSize(); // minimal element tag + readSize(); // maximal element tag builder->setNumElements(numElements); std::array nodes{}; - for (std::size_t blockIdx = 0; blockIdx < numBlocks; blockIdx++) { - getNextToken(); - const std::size_t dim = expectNonNegativeInt(); - getNextToken(); - const std::size_t entityTag = expectNonNegativeInt(); - getNextToken(); - const std::size_t type = expectNonNegativeInt(); - if (type < 1 || type > NumTypes || NumNodes[type - 1] == 0) { - char buf[128]; - snprintf(buf, sizeof(buf), "Unknown element type %zu", type); - return logErrorAnnotated(buf); + for (std::size_t blockIdx = 0; blockIdx < numBlocks; ++blockIdx) { + const long dim = readInt(); + const long entityTag = readInt(); + input->skipWhitespace(); + const auto typeOffset = input->offset(); + const long type = readInt(); + if (type < 1 || type > static_cast(NumTypes) || NumNodes[type - 1] == 0) { + failAt(typeOffset, "Unknown element type " + std::to_string(type)); } - getNextToken(); - const std::size_t numElementsInBlock = expectNonNegativeInt(); + const std::size_t numNodesPerElement = NumNodes[type - 1]; + const std::size_t numElementsInBlock = readSize(); const long tag = numElementsInBlock > 0 ? physicalTag(dim, entityTag) : 0; + for (std::size_t elementIdx = 0; elementIdx < numElementsInBlock; ++elementIdx) { - getNextToken(); - [[maybe_unused]] const std::size_t id = expectNonNegativeInt(); - for (std::size_t nodeIdx = 0; nodeIdx < NumNodes[type - 1]; nodeIdx++) { - getNextToken(); - nodes[nodeIdx] = static_cast(expectNodeIndex()); + readSize(); // element tag + for (std::size_t nodeIdx = 0; nodeIdx < numNodesPerElement; ++nodeIdx) { + nodes[nodeIdx] = static_cast(readNodeIndex()); } - builder->addElement(static_cast(type), tag, nodes.data(), NumNodes[type - 1]); + builder->addElement(type, tag, nodes.data(), numNodesPerElement); } } - getNextToken(); - if (curTok != tndm::GMSHToken::end_elements) { - return logErrorAnnotated("Expected $EndElements"); - } - getNextToken(); - - return true; + expectToken("$EndElements"); } -bool GMSH4Parser::parsePeriodic() { +void GMSH4Parser::parsePeriodic() { if (!hasNodes) { - return logErrorAnnotated("Expected $Nodes before $Periodic"); + fail("Expected $Nodes before $Periodic"); } - getNextToken(); - const auto numPeriodic = expectNonNegativeInt(); - - // ignore everything but the node identification + const std::size_t numPeriodic = readSize(); - for (std::size_t blockIdx = 0; blockIdx < numPeriodic; blockIdx++) { - getNextToken(); - [[maybe_unused]] const std::size_t entityDim = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const std::size_t entityId = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const std::size_t entityIdentifyId = expectNonNegativeInt(); - - getNextToken(); - const std::size_t affineSize = expectNonNegativeInt(); - for (std::size_t i = 0; i < affineSize; ++i) { - getNextToken(); - [[maybe_unused]] const double affineValue = expectNumber(); + // only the node identification is needed + for (std::size_t blockIdx = 0; blockIdx < numPeriodic; ++blockIdx) { + readInt(); // entity dimension + readInt(); // entity tag + readInt(); // master entity tag + const std::size_t numAffine = readSize(); + for (std::size_t i = 0; i < numAffine; ++i) { + readDouble(); } - - getNextToken(); - const std::size_t identifySize = expectNonNegativeInt(); - - for (std::size_t i = 0; i < identifySize; ++i) { - getNextToken(); - const std::size_t nodeId = expectNodeIndex(); - getNextToken(); - const std::size_t nodeIdentifyId = expectNodeIndex(); - builder->addVertexLink(nodeId, nodeIdentifyId); + const std::size_t numIdentified = readSize(); + for (std::size_t i = 0; i < numIdentified; ++i) { + const std::size_t node = readNodeIndex(); + const std::size_t master = readNodeIndex(); + builder->addVertexLink(node, master); } } + expectToken("$EndPeriodic"); +} - getNextToken(); - if (curTok != tndm::GMSHToken::end_periodic) { - return logErrorAnnotated("Expected $EndPeriodic"); +long GMSH4Parser::physicalTag(std::size_t dim, long entityTag) { + if (dim != 2 && dim != 3) { + return 0; } - getNextToken(); - - return true; + const auto& physicalIds = dim == 3 ? physicalVolumeIds : physicalSurfaceIds; + const auto it = physicalIds.find(entityTag); + if (it == physicalIds.end()) { + (dim == 3 ? unassignedVolumes : unassignedSurfaces).insert(entityTag); + return 0; + } + return it->second; } } // namespace puml diff --git a/src/meshreader/GMSH4Parser.h b/src/meshreader/GMSH4Parser.h index 97be825..7ca1ef7 100644 --- a/src/meshreader/GMSH4Parser.h +++ b/src/meshreader/GMSH4Parser.h @@ -4,72 +4,53 @@ #ifndef PUMGEN_SRC_MESHREADER_GMSH4PARSER_H_ #define PUMGEN_SRC_MESHREADER_GMSH4PARSER_H_ -#include +#include #include #include -#include -#include -#include "meshreader/GMSHBuilder.h" -#include "third_party/GMSHLexer.h" -#include "third_party/GMSHParser.h" +#include "GMSHParser.h" namespace puml { -class GMSH4Parser : public tndm::GMSHParser { +/** + * Parser for MSH 4.1 files. + */ +class GMSH4Parser : public GMSHParser { public: - using tndm::GMSHParser::GMSHParser; + using GMSHParser::GMSHParser; private: - std::map physicalSurfaceIds; - std::map physicalVolumeIds; - // surface and volume entities with elements, but without a physical group - std::set unassignedSurfaces; - std::set unassignedVolumes; + void parse_() override; + void parseEntities(); + void parseNodes(); + void parseElements(); + void parsePeriodic(); + + // the values of the sections + std::size_t readSize() { return expectSize(); } + long readInt() { return expectInteger(); } + double readDouble() { return expectNumber(); } + + /** + * Reads a node tag and returns the index of the node. + */ + std::size_t readNodeIndex(); /** * The physical tag of the elements of an entity; 0 if the entity has no physical group. */ - long physicalTag(std::size_t dim, unsigned long entityTag); + long physicalTag(std::size_t dim, long entityTag); + + std::map physicalSurfaceIds; + std::map physicalVolumeIds; + // surface and volume entities with elements, but without a physical group + std::set unassignedSurfaces; + std::set unassignedVolumes; // the node tags form the contiguous range [firstNodeTag, firstNodeTag + numNodes) bool hasNodes = false; std::size_t firstNodeTag = 0; std::size_t numNodes = 0; - - /** - * Reads a node tag and returns the index of the node. - */ - std::size_t expectNodeIndex() { - const std::size_t tag = expectNonNegativeInt(); - if (tag < firstNodeTag || tag - firstNodeTag >= numNodes) { - char buf[128]; - snprintf(buf, sizeof(buf), "Unknown node tag %zu", tag); - return logErrorAnnotated(buf); - } - return tag - firstNodeTag; - } - - unsigned long expectNonNegativeInt() { - if (curTok != tndm::GMSHToken::integer || lexer.getInteger() < 0) { - return logErrorAnnotated("Expected non-negative integer"); - } - return static_cast(lexer.getInteger()); - } - - double expectNumber() { - auto num = getNumber(); - if (!num) { - return logErrorAnnotated("Expected number"); - } - return num.value(); - } - - bool parseEntities(); - bool parseNodes(); - bool parseElements(); - bool parsePeriodic(); - virtual bool parse_() override; }; } // namespace puml diff --git a/src/meshreader/GMSHBuilder.h b/src/meshreader/GMSHBuilder.h index d5935a2..287eda1 100644 --- a/src/meshreader/GMSHBuilder.h +++ b/src/meshreader/GMSHBuilder.h @@ -14,7 +14,7 @@ #include "utils/logger.h" -#include "third_party/GMSHMeshBuilder.h" +#include "GMSHMeshBuilder.h" namespace puml { @@ -143,7 +143,7 @@ template <> struct GMSHSimplexType<3U, 10U> { static constexpr long type = 75; }; -template class GMSHBuilder : public tndm::GMSHMeshBuilder { +template class GMSHBuilder : public GMSHMeshBuilder { public: using vertex_t = std::array; using element_t = std::array; diff --git a/src/third_party/GMSHMeshBuilder.h b/src/meshreader/GMSHMeshBuilder.h similarity index 73% rename from src/third_party/GMSHMeshBuilder.h rename to src/meshreader/GMSHMeshBuilder.h index a66ac2a..d72d1d8 100644 --- a/src/third_party/GMSHMeshBuilder.h +++ b/src/meshreader/GMSHMeshBuilder.h @@ -1,12 +1,13 @@ // SPDX-FileCopyrightText: 2022 SeisSol Group // // SPDX-License-Identifier: BSD-3-Clause -#ifndef PUMGEN_SRC_THIRD_PARTY_GMSHMESHBUILDER_H_ -#define PUMGEN_SRC_THIRD_PARTY_GMSHMESHBUILDER_H_ +#ifndef PUMGEN_SRC_MESHREADER_GMSHMESHBUILDER_H_ +#define PUMGEN_SRC_MESHREADER_GMSHMESHBUILDER_H_ #include +#include -namespace tndm { +namespace puml { class GMSHMeshBuilder { public: @@ -19,6 +20,6 @@ class GMSHMeshBuilder { virtual void postprocess() = 0; }; -} // namespace tndm +} // namespace puml -#endif // PUMGEN_SRC_THIRD_PARTY_GMSHMESHBUILDER_H_ +#endif // PUMGEN_SRC_MESHREADER_GMSHMESHBUILDER_H_ diff --git a/src/meshreader/GMSHParser.cpp b/src/meshreader/GMSHParser.cpp new file mode 100644 index 0000000..a44488d --- /dev/null +++ b/src/meshreader/GMSHParser.cpp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2022 SeisSol Group +// SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München +// +// SPDX-License-Identifier: BSD-3-Clause +#include "GMSHParser.h" + +#include +#include +#include + +namespace puml { + +bool GMSHParser::parseFile(const std::string& fileName) { + errorMsg.clear(); + try { + input = std::make_unique(fileName, bufferSize); + parse_(); + input.reset(); + return true; + } catch (const ParseError&) { + } catch (const std::exception& error) { + errorMsg += "GMSH parser error:\n\t"; + errorMsg += error.what(); + errorMsg += '\n'; + } + input.reset(); + return false; +} + +void GMSHParser::failAt(std::size_t offset, std::string_view message) { + const auto [line, column] = input->location(offset); + std::stringstream stream; + stream << "GMSH parser error in line " << line << " in column " << column << ":\n"; + stream << '\t' << message << '\n'; + errorMsg += stream.str(); + throw ParseError{}; +} + +void GMSHParser::fail(std::string_view message) { + input->skipWhitespace(); + failAt(input->offset(), message); +} + +void GMSHParser::failFile(std::string_view message) { + errorMsg += "GMSH parser error:\n\t"; + errorMsg += message; + errorMsg += '\n'; + throw ParseError{}; +} + +void GMSHParser::expectToken(std::string_view token) { + input->skipWhitespace(); + const auto offset = input->offset(); + if (input->readToken() != token) { + failAt(offset, "Expected " + std::string(token)); + } +} + +GMSHParser::MeshFormat GMSHParser::parseMeshFormatHeader() { + expectToken("$MeshFormat"); + MeshFormat format{}; + format.version = expectNumber(); + format.fileType = expectInteger(); + format.dataSize = expectSize(); + return format; +} + +std::string_view GMSHParser::nextSection() { + if (!input->skipWhitespace()) { + return {}; + } + const auto offset = input->offset(); + const auto section = input->readToken(); + if (section.empty() || section.front() != '$') { + failAt(offset, "Expected a section"); + } + return section; +} + +void GMSHParser::skipSection(std::string_view section) { + const std::string endMarker = "$End" + std::string(section.substr(1)); + if (!input->skipTo(endMarker)) { + failFile("Missing " + endMarker); + } +} + +} // namespace puml diff --git a/src/third_party/GMSHParser.h b/src/meshreader/GMSHParser.h similarity index 69% rename from src/third_party/GMSHParser.h rename to src/meshreader/GMSHParser.h index cbef6b6..9462d06 100644 --- a/src/third_party/GMSHParser.h +++ b/src/meshreader/GMSHParser.h @@ -2,20 +2,18 @@ // SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München // // SPDX-License-Identifier: BSD-3-Clause -#ifndef PUMGEN_SRC_THIRD_PARTY_GMSHPARSER_H_ -#define PUMGEN_SRC_THIRD_PARTY_GMSHPARSER_H_ +#ifndef PUMGEN_SRC_MESHREADER_GMSHPARSER_H_ +#define PUMGEN_SRC_MESHREADER_GMSHPARSER_H_ -#include -#include -#include -#include +#include +#include #include #include -#include "GMSHLexer.h" #include "GMSHMeshBuilder.h" +#include "MshInput.h" -namespace tndm { +namespace puml { namespace detail { template constexpr std::size_t maxEntry(const std::size_t (&values)[N]) { @@ -27,6 +25,9 @@ template constexpr std::size_t maxEntry(const std::size_t (&valu } } // namespace detail +/** + * Common parts of the MSH parsers: element types, error handling and reading of text values. + */ class GMSHParser { public: // Look-up table from gmsh type to number of nodes @@ -163,80 +164,93 @@ class GMSHParser { static constexpr std::size_t NumTypes = sizeof(NumNodes) / sizeof(NumNodes[0]); static constexpr std::size_t MaxNodesPerElement = detail::maxEntry(NumNodes); - explicit GMSHParser(GMSHMeshBuilder* builder) : builder(builder) {}; - [[nodiscard]] std::string_view getErrorMessage() const { return errorMsg; } + explicit GMSHParser(GMSHMeshBuilder* builder, + std::size_t bufferSize = MshInput::DefaultBufferSize) + : builder(builder), bufferSize(bufferSize) {} + virtual ~GMSHParser() = default; /** * Parses the file; stops at the first error, whose description getErrorMessage() returns. */ - bool parseFile(std::string const& fileName) { - errorMsg.clear(); - std::ifstream in(fileName); - if (!in.is_open()) { - errorMsg = "GMSH parser error:\n\tUnable to open MSH file\n"; - return false; - } - lexer.setIStream(&in); - try { - return parse_(); - } catch (const ParseError&) { - return false; - } catch (const std::runtime_error& error) { - appendAnnotated(error.what()); - return false; - } - } + bool parseFile(const std::string& fileName); + + [[nodiscard]] std::string_view getErrorMessage() const { return errorMsg; } protected: + struct MeshFormat { + double version; + long fileType; + std::size_t dataSize; + }; + /** * Thrown after an error has been recorded in errorMsg. */ struct ParseError {}; - template T logErrorAnnotated(std::string_view msg) { - appendAnnotated(msg); - throw ParseError{}; - } + /** + * Records an error at the given file offset and aborts parsing. + */ + [[noreturn]] void failAt(std::size_t offset, std::string_view message); - template T logError(std::string_view msg) { - errorMsg += "GMSH parser error:\n\t"; - errorMsg += msg; - errorMsg += '\n'; - throw ParseError{}; - } + /** + * Records an error at the next token and aborts parsing. + */ + [[noreturn]] void fail(std::string_view message); - GMSHMeshBuilder* builder; - GMSHSourceLocation curLoc = {0, 0}; - GMSHToken curTok; - GMSHLexer lexer; - std::string errorMsg; + /** + * Records an error concerning the whole file and aborts parsing. + */ + [[noreturn]] void failFile(std::string_view message); - GMSHToken getNextToken() { - curLoc = lexer.getSourceLoc(); - return curTok = lexer.getToken(); + std::size_t expectSize() { + if (const auto value = input->readInteger()) { + return *value; + } + fail("Expected non-negative integer"); } - std::optional getNumber() { - if (curTok == GMSHToken::integer) { - return {lexer.getInteger()}; - } else if (curTok == GMSHToken::real) { - return {lexer.getReal()}; + long expectInteger() { + if (const auto value = input->readInteger()) { + return *value; } - return std::nullopt; + fail("Expected integer"); } - virtual bool parse_() = 0; + double expectNumber() { + if (const auto value = input->readReal()) { + return *value; + } + fail("Expected number"); + } + + void expectToken(std::string_view token); + + /** + * Reads the $MeshFormat section up to (and including) the data size. + */ + MeshFormat parseMeshFormatHeader(); + + /** + * Returns the name of the next section (e.g. "$Nodes"), or an empty view at the end of the file. + */ + std::string_view nextSection(); + + /** + * Skips the rest of a section which is not needed. + */ + void skipSection(std::string_view section); - double parseMeshFormat(); + virtual void parse_() = 0; + + GMSHMeshBuilder* builder; + std::unique_ptr input; private: - void appendAnnotated(std::string_view msg) { - std::stringstream ss; - ss << "GMSH parser error in line " << curLoc.line << " in column " << curLoc.col << ":\n"; - ss << '\t' << msg << '\n'; - errorMsg += ss.str(); - } + std::size_t bufferSize; + std::string errorMsg; }; -} // namespace tndm -#endif // PUMGEN_SRC_THIRD_PARTY_GMSHPARSER_H_ +} // namespace puml + +#endif // PUMGEN_SRC_MESHREADER_GMSHPARSER_H_ diff --git a/src/meshreader/MshInput.cpp b/src/meshreader/MshInput.cpp new file mode 100644 index 0000000..aeee650 --- /dev/null +++ b/src/meshreader/MshInput.cpp @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#include "MshInput.h" + +#include +#include +#include + +namespace puml { + +MshInput::MshInput(const std::string& fileName, std::size_t bufferSize) + : fileName(fileName), buffer(std::max(bufferSize, 2 * MaxTokenLength) + 1) { + file = std::fopen(fileName.c_str(), "rb"); + if (file == nullptr) { + throw std::runtime_error("Unable to open MSH file " + fileName); + } + buffer[0] = '\0'; +} + +MshInput::~MshInput() { + if (file != nullptr) { + std::fclose(file); + } +} + +std::string_view MshInput::readToken() { + if (!skipWhitespace()) { + return {}; + } + ensure(MaxTokenLength); + std::size_t stop = begin; + while (stop < end && !isSpace(buffer[stop]) && stop - begin < MaxTokenLength) { + ++stop; + } + const std::string_view token(buffer.data() + begin, stop - begin); + begin = stop; + return token; +} + +bool MshInput::readLineBreak() { + ensure(2); + if (begin < end && buffer[begin] == '\r') { + ++begin; + } + if (begin < end && buffer[begin] == '\n') { + ++begin; + return true; + } + return false; +} + +bool MshInput::readRaw(void* data, std::size_t bytes) { + auto* out = static_cast(data); + while (bytes > 0) { + if (begin == end && bytes >= (buffer.size() - 1) / 2) { + // large reads bypass the (empty) buffer + bufferOffset += end; + begin = end = 0; + const std::size_t read = std::fread(out, 1, bytes, file); + bufferOffset += read; + if (read < bytes) { + eof = true; + return false; + } + return true; + } + if (begin == end && !refill()) { + return false; + } + const std::size_t count = std::min(bytes, end - begin); + std::memcpy(out, buffer.data() + begin, count); + begin += count; + out += count; + bytes -= count; + } + return true; +} + +bool MshInput::skipTo(std::string_view marker) { + while (true) { + const std::string_view data(buffer.data() + begin, end - begin); + const auto found = data.find(marker); + if (found != std::string_view::npos) { + begin += found + marker.size(); + return true; + } + // the tail may hold the beginning of the marker + begin = end - std::min(data.size(), marker.size() - 1); + if (!refill()) { + begin = end; + return false; + } + } +} + +std::pair MshInput::location(std::size_t offset) const { + std::size_t line = 1; + std::size_t column = 1; + std::FILE* scan = std::fopen(fileName.c_str(), "rb"); + if (scan == nullptr) { + return {line, column}; + } + std::vector chunk(std::size_t{1} << 20); + std::size_t position = 0; + while (position < offset) { + const std::size_t read = + std::fread(chunk.data(), 1, std::min(chunk.size(), offset - position), scan); + if (read == 0) { + break; + } + for (std::size_t i = 0; i < read; ++i) { + if (chunk[i] == '\n') { + ++line; + column = 1; + } else { + ++column; + } + } + position += read; + } + std::fclose(scan); + return {line, column}; +} + +bool MshInput::refill() { + if (eof) { + return false; + } + const std::size_t remaining = end - begin; + if (begin > 0) { + std::memmove(buffer.data(), buffer.data() + begin, remaining); + bufferOffset += begin; + begin = 0; + end = remaining; + } + const std::size_t capacity = buffer.size() - 1; + if (end == capacity) { + return false; + } + const std::size_t read = std::fread(buffer.data() + end, 1, capacity - end, file); + end += read; + buffer[end] = '\0'; + if (read == 0) { + eof = true; + } + return read > 0; +} + +} // namespace puml diff --git a/src/meshreader/MshInput.h b/src/meshreader/MshInput.h new file mode 100644 index 0000000..f00e6eb --- /dev/null +++ b/src/meshreader/MshInput.h @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#ifndef PUMGEN_SRC_MESHREADER_MSHINPUT_H_ +#define PUMGEN_SRC_MESHREADER_MSHINPUT_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace puml { + +/** + * Buffered sequential reading of an MSH file: whitespace-separated text tokens for the ASCII + * parts and raw bytes for the binary parts. + */ +class MshInput { + public: + static constexpr std::size_t DefaultBufferSize = std::size_t{16} << 20; + // numbers and section names are shorter; the buffer always holds this many bytes ahead + static constexpr std::size_t MaxTokenLength = 128; + + /** + * @throws std::runtime_error if the file cannot be opened + */ + explicit MshInput(const std::string& fileName, std::size_t bufferSize = DefaultBufferSize); + ~MshInput(); + MshInput(const MshInput&) = delete; + MshInput& operator=(const MshInput&) = delete; + + /** + * Skips whitespace; returns false at the end of the file. + */ + bool skipWhitespace() { + while (true) { + while (begin < end && isSpace(buffer[begin])) { + ++begin; + } + if (begin < end) { + return true; + } + if (!refill()) { + return false; + } + } + } + + /** + * The first character of the next token, or '\0' at the end of the file. + */ + char peek() { return skipWhitespace() ? buffer[begin] : '\0'; } + + /** + * The next whitespace-separated token (at most MaxTokenLength characters); empty at the end of + * the file. The view is valid until the next read. + */ + std::string_view readToken(); + + /** + * Reads a whitespace-separated integer; std::nullopt (without consuming anything but + * whitespace) if the next token is not an integer of type T. + */ + template std::optional readInteger() { + if (!skipWhitespace()) { + return std::nullopt; + } + ensure(MaxTokenLength); + const char* first = buffer.data() + begin; + const char* last = buffer.data() + end; + if (*first == '+') { + ++first; + } + T value{}; + const auto [ptr, ec] = std::from_chars(first, last, value); + if (ec != std::errc() || (ptr != last && !isSpace(*ptr))) { + return std::nullopt; + } + begin = static_cast(ptr - buffer.data()); + return value; + } + + /** + * Reads a whitespace-separated floating-point number; std::nullopt (without consuming anything + * but whitespace) if the next token is not a number. + */ + std::optional readReal() { + if (!skipWhitespace()) { + return std::nullopt; + } + ensure(MaxTokenLength); + const char* first = buffer.data() + begin; + const char* last = buffer.data() + end; + if (*first == '+') { + ++first; + } + double value = 0; +#if defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 201611L + const auto [ptr, ec] = std::from_chars(first, last, value); + const bool parsed = ec == std::errc(); +#else + // the buffer is null-terminated behind the valid data + char* ptr = nullptr; + value = std::strtod(first, &ptr); + const bool parsed = ptr != first; +#endif + if (!parsed || (ptr != last && !isSpace(*ptr))) { + return std::nullopt; + } + begin = static_cast(ptr - buffer.data()); + return value; + } + + /** + * Consumes a single line break ("\n" or "\r\n"), as it separates the header line of a binary + * section from its data. + */ + bool readLineBreak(); + + /** + * Reads the next bytes as they are. + */ + bool readRaw(void* data, std::size_t bytes); + + /** + * Skips everything up to and including the next occurrence of the marker. + */ + bool skipTo(std::string_view marker); + + /** + * The offset of the next unread byte in the file. + */ + [[nodiscard]] std::size_t offset() const { return bufferOffset + begin; } + + /** + * Line and column (both starting at 1) of a byte offset; reads the file up to that offset. + */ + [[nodiscard]] std::pair location(std::size_t offset) const; + + private: + static bool isSpace(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); } + + /** + * Moves the unread data to the front of the buffer and appends more data from the file; + * returns false if nothing could be appended. + */ + bool refill(); + + /** + * Makes at least count bytes available unless the end of the file comes first. + */ + void ensure(std::size_t count) { + while (end - begin < count && !eof) { + refill(); + } + } + + std::string fileName; + std::FILE* file = nullptr; + // one byte more than the capacity for the terminating null character + std::vector buffer; + // the unread data is buffer[begin, end) + std::size_t begin = 0; + std::size_t end = 0; + // the file offset of buffer[0] + std::size_t bufferOffset = 0; + bool eof = false; +}; + +} // namespace puml + +#endif // PUMGEN_SRC_MESHREADER_MSHINPUT_H_ diff --git a/src/pumgen.cpp b/src/pumgen.cpp index c7651a4..7227859 100644 --- a/src/pumgen.cpp +++ b/src/pumgen.cpp @@ -44,10 +44,10 @@ #ifdef USE_SIMMOD #include "input/SimModSuite.h" #endif // USE_SIMMOD +#include "meshreader/GMSH2Parser.h" #include "meshreader/GMSH4Parser.h" #include "meshreader/ParallelGMSHReader.h" #include "meshreader/ParallelGambitReader.h" -#include "third_party/GMSH2Parser.h" #include "helper/InsphereCalculator.h" @@ -217,7 +217,7 @@ void addAttribute(hid_t h5file, const std::string& name, const std::string& valu } template -using SMF2 = SerialMeshFile>; +using SMF2 = SerialMeshFile>; template using SMF4 = SerialMeshFile>; diff --git a/src/third_party/GMSH2Parser.cpp b/src/third_party/GMSH2Parser.cpp deleted file mode 100644 index 0f0965a..0000000 --- a/src/third_party/GMSH2Parser.cpp +++ /dev/null @@ -1,211 +0,0 @@ -// SPDX-FileCopyrightText: 2022 SeisSol Group -// SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München -// -// SPDX-License-Identifier: BSD-3-Clause -#include "GMSH2Parser.h" -#include "GMSHLexer.h" - -#include -#include - -namespace tndm { - -bool GMSH2Parser::parse_() { - getNextToken(); - - double version = parseMeshFormat(); - if (version < 2.0 || version >= 3.0) { - char buf[128]; - snprintf(buf, sizeof(buf), "Unsupported MSH version %.1lf", version); - return logError(buf); - } - - bool hasNodes = false; - bool hasElements = false; - - while (curTok != GMSHToken::eof) { - switch (curTok) { - case GMSHToken::nodes: - hasNodes = parseNodes(); - break; - case GMSHToken::elements: - hasElements = parseElements(); - break; - case GMSHToken::periodic: - parsePeriodic(); - break; - default: - getNextToken(); - break; - } - } - - if (!hasNodes) { - return logError("Missing $Nodes section"); - } - if (!hasElements) { - return logError("Missing $Elements section"); - } - return true; -} - -bool GMSH2Parser::parseNodes() { - getNextToken(); - if (curTok != GMSHToken::integer || lexer.getInteger() < 0) { - return logErrorAnnotated("Expected non-zero integer"); - } - std::size_t numVertices = lexer.getInteger(); - builder->setNumVertices(numVertices); - hasNodes = true; - numNodes = numVertices; - - // with numVertices distinct tags in [1, numVertices], every node is defined exactly once - std::vector defined(numVertices, false); - - for (std::size_t i = 0; i < numVertices; ++i) { - getNextToken(); - if (curTok != GMSHToken::integer || lexer.getInteger() < 1 || - lexer.getInteger() > numVertices) { - char buf[128]; - snprintf(buf, sizeof(buf), "Expected node-tag with 1 <= node-tag <= %zu", numVertices); - return logErrorAnnotated(buf); - } - std::size_t id = lexer.getInteger() - 1; - if (defined[id]) { - char buf[128]; - snprintf(buf, sizeof(buf), "Duplicate node tag %zu", id + 1); - return logErrorAnnotated(buf); - } - defined[id] = true; - - std::array x; - for (std::size_t i = 0; i < 3; ++i) { - getNextToken(); - auto coord = getNumber(); - if (!coord) { - return logErrorAnnotated("Expected coordinate"); - } - x[i] = *coord; - } - builder->setVertex(id, x); - } - getNextToken(); - if (curTok != GMSHToken::end_nodes) { - return logErrorAnnotated("Expected $EndNodes"); - } - getNextToken(); - return true; -} - -bool GMSH2Parser::parseElements() { - if (!hasNodes) { - return logErrorAnnotated("Expected $Nodes before $Elements"); - } - getNextToken(); - if (curTok != GMSHToken::integer || lexer.getInteger() < 0) { - return logErrorAnnotated("Expected non-negative integer"); - } - const auto numElements = lexer.getInteger(); - - std::array nodes{}; - - builder->setNumElements(numElements); - - for (std::size_t elementIdx = 0; elementIdx < numElements; ++elementIdx) { - getNextToken(); - if (curTok != GMSHToken::integer) { - return logErrorAnnotated("Expected element-tag"); - } - - getNextToken(); - if (curTok != GMSHToken::integer) { - return logErrorAnnotated("Expected element-type"); - } - const long type = lexer.getInteger(); - if (type < 1 || type > static_cast(NumTypes) || NumNodes[type - 1] == 0) { - char buf[128]; - snprintf(buf, sizeof(buf), "Unknown element type %li", type); - return logErrorAnnotated(buf); - } - - getNextToken(); - if (curTok != GMSHToken::integer || lexer.getInteger() < 0) { - return logErrorAnnotated("Expected number of tags"); - } - long numTags = lexer.getInteger(); - // the physical tag; elements without tags get 0 - long tag = 0; - for (long tagIdx = 0; tagIdx < numTags; ++tagIdx) { - getNextToken(); - if (curTok != GMSHToken::integer) { - return logErrorAnnotated("Expected tag (integer)"); - } - if (tagIdx == 0) { - tag = lexer.getInteger(); - } - } - - for (std::size_t nodeIdx = 0; nodeIdx < NumNodes[type - 1]; ++nodeIdx) { - getNextToken(); - nodes[nodeIdx] = static_cast(expectNodeIndex()); - } - - builder->addElement(type, tag, nodes.data(), NumNodes[type - 1]); - } - getNextToken(); - if (curTok != GMSHToken::end_elements) { - return logErrorAnnotated("Expected $EndElements"); - } - getNextToken(); - - return true; -} - -bool GMSH2Parser::parsePeriodic() { - if (!hasNodes) { - return logErrorAnnotated("Expected $Nodes before $Periodic"); - } - getNextToken(); - const auto numPeriodic = expectNonNegativeInt(); - - // ignore everything but the node identification - - for (std::size_t blockIdx = 0; blockIdx < numPeriodic; blockIdx++) { - getNextToken(); - [[maybe_unused]] const std::size_t entityDim = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const std::size_t entityId = expectNonNegativeInt(); - getNextToken(); - [[maybe_unused]] const std::size_t entityIdentifyId = expectNonNegativeInt(); - - getNextToken(); - if (curTok == GMSHToken::string) { - // Gmsh msh2 may write: "Affine a11 a12 ... a44" (16 values) - constexpr std::size_t affineSize = 16; - for (std::size_t i = 0; i < affineSize; ++i) { - getNextToken(); - [[maybe_unused]] const double affineValue = expectNumber(); - } - getNextToken(); - } - const std::size_t identifySize = expectNonNegativeInt(); - - for (std::size_t i = 0; i < identifySize; ++i) { - getNextToken(); - const std::size_t nodeId = expectNodeIndex(); - getNextToken(); - const std::size_t nodeIdentifyId = expectNodeIndex(); - builder->addVertexLink(nodeId, nodeIdentifyId); - } - } - - getNextToken(); - if (curTok != tndm::GMSHToken::end_periodic) { - return logErrorAnnotated("Expected $EndPeriodic"); - } - getNextToken(); - - return true; -} - -} // namespace tndm diff --git a/src/third_party/GMSH2Parser.h b/src/third_party/GMSH2Parser.h deleted file mode 100644 index 38f9e33..0000000 --- a/src/third_party/GMSH2Parser.h +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2022 SeisSol Group -// SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München -// -// SPDX-License-Identifier: BSD-3-Clause -#ifndef PUMGEN_SRC_THIRD_PARTY_GMSH2PARSER_H_ -#define PUMGEN_SRC_THIRD_PARTY_GMSH2PARSER_H_ - -#include "GMSHLexer.h" -#include "GMSHMeshBuilder.h" -#include "GMSHParser.h" - -#include -#include -#include -#include -#include - -namespace tndm { - -class GMSH2Parser : public GMSHParser { - public: - using GMSHParser::GMSHParser; - - private: - unsigned long expectNonNegativeInt() { - if (curTok != tndm::GMSHToken::integer || lexer.getInteger() < 0) { - return logErrorAnnotated("Expected non-negative integer"); - } - return static_cast(lexer.getInteger()); - } - - double expectNumber() { - auto num = getNumber(); - if (!num) { - return logErrorAnnotated("Expected number"); - } - return num.value(); - } - - // the node tags are 1, ..., numNodes - bool hasNodes = false; - std::size_t numNodes = 0; - - /** - * Reads a node tag and returns the index of the node. - */ - std::size_t expectNodeIndex() { - if (curTok != GMSHToken::integer || lexer.getInteger() < 1 || - static_cast(lexer.getInteger()) > numNodes) { - return logErrorAnnotated("Unknown node tag"); - } - return static_cast(lexer.getInteger()) - 1; - } - - bool parseNodes(); - bool parseElements(); - bool parsePeriodic(); - virtual bool parse_() override; -}; - -}; // namespace tndm - -#endif // PUMGEN_SRC_THIRD_PARTY_GMSH2PARSER_H_ diff --git a/src/third_party/GMSHLexer.cpp b/src/third_party/GMSHLexer.cpp deleted file mode 100644 index 059407a..0000000 --- a/src/third_party/GMSHLexer.cpp +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-FileCopyrightText: 2022 SeisSol Group -// SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München -// -// SPDX-License-Identifier: BSD-3-Clause -#include "GMSHLexer.h" -#include "Hash.h" - -namespace tndm { - -void GMSHLexer::advance() { - in->get(lastChar); - - if (lastChar == '\n' || lastChar == '\r') { - ++loc.line; - loc.col = 1; - } else { - ++loc.col; - } -} - -GMSHToken GMSHLexer::getToken() { - if (in == nullptr) { - return GMSHToken::eof; - } - in->peek(); - if (!in->good()) { - return GMSHToken::eof; - } - - while (isspace(lastChar) && in->good()) { - advance(); - } - if (!in->good()) { - return GMSHToken::eof; - } - - if (lastChar == '$') { - advance(); - auto hash = fnv1a0(); - while (isalpha(lastChar)) { - hash = fnv1a_step(hash, lastChar); - advance(); - } - GMSHToken token = GMSHToken::unknown_section; - switch (hash) { - case "MeshFormat"_fnv1a: - token = GMSHToken::mesh_format; - break; - case "EndMeshFormat"_fnv1a: - token = GMSHToken::end_mesh_format; - break; - case "Entities"_fnv1a: - token = GMSHToken::entities; - break; - case "EndEntities"_fnv1a: - token = GMSHToken::end_entities; - break; - case "Nodes"_fnv1a: - token = GMSHToken::nodes; - break; - case "EndNodes"_fnv1a: - token = GMSHToken::end_nodes; - break; - case "Elements"_fnv1a: - token = GMSHToken::elements; - break; - case "EndElements"_fnv1a: - token = GMSHToken::end_elements; - break; - case "Periodic"_fnv1a: - token = GMSHToken::periodic; - break; - case "EndPeriodic"_fnv1a: - token = GMSHToken::end_periodic; - break; - case "PartitionedEntities"_fnv1a: - token = GMSHToken::partitioned_entities; - break; - default: - break; - } - return token; - } - - auto mustbereal = [](char c) { return c == '.' || c == 'e' || c == 'E'; }; - auto isnumber = [&mustbereal](char c) { - return isdigit(c) || c == '+' || c == '-' || mustbereal(c); - }; - - if (isnumber(lastChar)) { - char buf[MaxNumberLength + 1]; - int pos = 0; - bool isreal = false; - do { - buf[pos++] = lastChar; - isreal = isreal || mustbereal(lastChar); - advance(); - } while (isnumber(lastChar) && pos < MaxNumberLength); - buf[pos] = 0; - if (pos == MaxNumberLength) { - throw std::runtime_error("Too large number encountered in GMSHLexer: " + std::string(buf)); - } - if (isreal) { - real = std::strtod(buf, 0); - return GMSHToken::real; - } - integer = std::strtol(buf, 0, 10); - return GMSHToken::integer; - } - - if (lastChar == '"') { - do { - advance(); - } while (lastChar != '"'); - advance(); - return GMSHToken::string; - } - if (isalpha(lastChar)) { - do { - advance(); - } while (isalpha(lastChar)); - return GMSHToken::string; - } - // Always consume at least one character for unknown tokens to avoid - // getting stuck in an infinite tokenization loop. - advance(); - return GMSHToken::unknown_token; -} - -} // namespace tndm diff --git a/src/third_party/GMSHLexer.h b/src/third_party/GMSHLexer.h deleted file mode 100644 index 740f907..0000000 --- a/src/third_party/GMSHLexer.h +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2022 SeisSol Group -// SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München -// -// SPDX-License-Identifier: BSD-3-Clause -#ifndef PUMGEN_SRC_THIRD_PARTY_GMSHLEXER_H_ -#define PUMGEN_SRC_THIRD_PARTY_GMSHLEXER_H_ - -#include -#include - -namespace tndm { - -enum class GMSHToken { - eof, - integer, - real, - string, - mesh_format, - end_mesh_format, - entities, - end_entities, - nodes, - end_nodes, - elements, - end_elements, - periodic, - end_periodic, - partitioned_entities, - unknown_section, - unknown_token -}; - -struct GMSHSourceLocation { - std::size_t line; - std::size_t col; -}; - -class GMSHLexer { - private: - uint64_t identifier; - long integer; - double real; - char lastChar = ' '; - std::istream* in = nullptr; - GMSHSourceLocation loc = {1, 1}; - - protected: - void advance(); - - public: - static constexpr std::size_t MaxNumberLength = 128; - - void setIStream(std::istream* istream) { - in = istream; - loc = {1, 1}; - } - - GMSHToken getToken(); - uint64_t getIdentifier() const { return identifier; } - long getInteger() const { return integer; } - double getReal() const { return real; } - GMSHSourceLocation getSourceLoc() const { return loc; } -}; -} // namespace tndm - -#endif // PUMGEN_SRC_THIRD_PARTY_GMSHLEXER_H_ diff --git a/src/third_party/GMSHParser.cpp b/src/third_party/GMSHParser.cpp deleted file mode 100644 index 753176d..0000000 --- a/src/third_party/GMSHParser.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2022 SeisSol Group -// SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München -// -// SPDX-License-Identifier: BSD-3-Clause -#include "GMSHParser.h" - -namespace tndm { - -double GMSHParser::parseMeshFormat() { - if (curTok != GMSHToken::mesh_format) { - return logErrorAnnotated("Expected $MeshFormat"); - } - getNextToken(); - auto version = getNumber(); - if (!version) { - return logErrorAnnotated("Expected version number"); - } - getNextToken(); - if (curTok == GMSHToken::integer && lexer.getInteger() == 1) { - return logErrorAnnotated( - "Binary MSH files are not supported; write the mesh as ASCII (Mesh.Binary = 0)"); - } - if (curTok != GMSHToken::integer || lexer.getInteger() != 0) { - return logErrorAnnotated("Expected file type 0 (ASCII)"); - } - getNextToken(); // skip data-size - getNextToken(); - if (curTok != GMSHToken::end_mesh_format) { - return logErrorAnnotated("Expected $EndMeshFormat"); - } - getNextToken(); - return *version; -} -} // namespace tndm diff --git a/src/third_party/Hash.h b/src/third_party/Hash.h deleted file mode 100644 index 11c328a..0000000 --- a/src/third_party/Hash.h +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2025 SeisSol Group -// SPDX-FileCopyrightText: 2020 Ludwig-Maximilians-Universität München -// -// SPDX-License-Identifier: BSD-3-Clause -#ifndef PUMGEN_SRC_THIRD_PARTY_HASH_H_ -#define PUMGEN_SRC_THIRD_PARTY_HASH_H_ - -#include -#include - -namespace tndm { - -// https://de.wikipedia.org/wiki/FNV_(Informatik) -constexpr uint64_t fnv1a0() { return 0xcbf29ce484222325; } -constexpr uint64_t fnv1a_step(uint64_t hash, char c) { return (hash ^ c) * 0x00000100000001b3; } -constexpr uint64_t fnv1a(char const* s, std::size_t len) { - return len > 0 ? fnv1a_step(fnv1a(s, len - 1), s[len - 1]) : fnv1a0(); -} -uint64_t fnv1a(std::string const& s) { return fnv1a(s.data(), s.size()); } -constexpr uint64_t operator""_fnv1a(char const* s, std::size_t len) { return tndm::fnv1a(s, len); } - -} // namespace tndm - -#endif // PUMGEN_SRC_THIRD_PARTY_HASH_H_ diff --git a/src/third_party/README.md b/src/third_party/README.md index 55b51fd..3721c9a 100644 --- a/src/third_party/README.md +++ b/src/third_party/README.md @@ -6,5 +6,8 @@ # Third party sources -Taken from ; -one further addition from . +`MPITraits.h` is taken from tandem, commit +. +The GMSH parsers in `src/meshreader` are derived from the tandem GMSH +parser of that commit and of commit +. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 26ab1de..e901b9b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -56,6 +56,19 @@ function(pumgen_add_test name) "ASAN_OPTIONS=detect_leaks=0") endfunction() +# the GMSH parsers with a tiny read buffer +add_executable( + pumgen_reader_test + ReaderTest.cpp ${PROJECT_SOURCE_DIR}/src/meshreader/MshInput.cpp + ${PROJECT_SOURCE_DIR}/src/meshreader/GMSHParser.cpp + ${PROJECT_SOURCE_DIR}/src/meshreader/GMSH2Parser.cpp + ${PROJECT_SOURCE_DIR}/src/meshreader/GMSH4Parser.cpp) +target_include_directories(pumgen_reader_test PRIVATE ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/submodules) +target_compile_definitions(pumgen_reader_test PRIVATE LOG_LEVEL=${LOG_LEVEL}) +add_test(NAME reader.buffer-boundaries COMMAND pumgen_reader_test ${PUMGEN_TEST_FIXTURES}) +set_tests_properties(reader.buffer-boundaries PROPERTIES ENVIRONMENT "ASAN_OPTIONS=detect_leaks=0") + # The fixtures are created by fixtures/generate.py; the reference files are computed from the # gmsh model directly. The layered mesh has element and vertex counts which do not divide evenly # among 3 or 4 ranks. diff --git a/tests/ReaderTest.cpp b/tests/ReaderTest.cpp new file mode 100644 index 0000000..75efd93 --- /dev/null +++ b/tests/ReaderTest.cpp @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause + +// Parses the fixtures with a tiny read buffer, so that tokens and section markers straddle buffer +// boundaries, and compares the result with the one of the default buffer. + +#include +#include +#include +#include + +#include "meshreader/GMSH2Parser.h" +#include "meshreader/GMSH4Parser.h" +#include "meshreader/GMSHBuilder.h" +#include "meshreader/MshInput.h" + +namespace { + +constexpr std::size_t TinyBuffer = 2 * puml::MshInput::MaxTokenLength; + +template +bool parse(const std::string& file, std::size_t bufferSize, Builder& builder) { + Parser parser(&builder, bufferSize); + if (!parser.parseFile(file)) { + std::fprintf(stderr, "%s (buffer %zu): %s", file.c_str(), bufferSize, + std::string(parser.getErrorMessage()).c_str()); + return false; + } + builder.postprocess(); + return true; +} + +template bool check(const std::string& file) { + puml::GMSHBuilder<3, Order> reference; + puml::GMSHBuilder<3, Order> tiny; + if (!parse(file, puml::MshInput::DefaultBufferSize, reference) || + !parse(file, TinyBuffer, tiny)) { + return false; + } + const bool same = reference.vertices == tiny.vertices && reference.elements == tiny.elements && + reference.groups == tiny.groups && reference.facets == tiny.facets && + reference.bcs == tiny.bcs && reference.identify == tiny.identify && + !reference.elements.empty(); + std::printf("%s: %s\n", file.c_str(), same ? "ok" : "DIFFERENT"); + return same; +} + +std::string readFile(const std::string& file) { + std::ifstream in(file, std::ios::binary); + std::stringstream content; + content << in.rdbuf(); + return content.str(); +} + +// a section to be skipped, longer than the tiny buffer and with partial end markers +bool checkSkippedSection(const std::string& fixtures) { + std::string content = readFile(fixtures + "/tiny-v41.msh"); + std::string junk = "$Comments\n"; + for (int i = 0; i < 200; ++i) { + junk += "$EndComment $EndCommentz "; + } + junk += "\n$EndComments\n"; + content.insert(content.find("$Nodes"), junk); + const std::string file = "reader-test-skipped-section.msh"; + std::ofstream(file, std::ios::binary) << content; + + puml::GMSHBuilder<3, 1> reference; + puml::GMSHBuilder<3, 1> skipped; + const bool ok = parse(fixtures + "/tiny-v41.msh", + puml::MshInput::DefaultBufferSize, reference) && + parse(file, TinyBuffer, skipped) && + reference.vertices == skipped.vertices && + reference.elements == skipped.elements && reference.groups == skipped.groups; + std::remove(file.c_str()); + std::printf("skipped section: %s\n", ok ? "ok" : "DIFFERENT"); + return ok; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "Usage: %s \n", argv[0]); + return 2; + } + const std::string fixtures = argv[1]; + bool ok = true; + ok &= check(fixtures + "/layered-v41.msh"); + ok &= check(fixtures + "/layered-v22.msh"); + ok &= check(fixtures + "/periodic-v41.msh"); + ok &= check(fixtures + "/periodic-parametric-v41.msh"); + ok &= check(fixtures + "/coarse-o2-v41.msh"); + ok &= check(fixtures + "/coarse-o2-v22.msh"); + ok &= checkSkippedSection(fixtures); + return ok ? 0 : 1; +} From 7a5f13c9534473d3f98718a13da2a98a53fc138c Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 19:24:32 +0000 Subject: [PATCH 11/19] Read binary MSH 4.1 files Binary files are parsed as the ASCII ones, but with the values read as raw 4-byte ints, doubles and data-size integers; node tags, coordinates and element nodes are read in bulk. Other byte orders (detected by the marker in $MeshFormat) and a data size of 4 are supported as well. On the mesh with 3.6M cells, parsing the binary file takes 0.18 s instead of 0.43 s for the ASCII file (and 2.18 s for the ASCII file before the buffered reader). Errors in binary data are reported with their byte offset. The binary fixtures are written by gmsh; fixtures/generate.py derives big-endian and 4-byte-size variants and first checks that its converter reproduces gmsh's file byte for byte. The references of the binary tests are compared with a tolerance, since they are built from the ASCII files (%.16g). AI-generated. Model: Claude Opus 5.5 --- src/meshreader/GMSH4Parser.cpp | 214 ++++++++++++++---- src/meshreader/GMSH4Parser.h | 35 ++- src/meshreader/GMSHParser.cpp | 13 +- src/meshreader/GMSHParser.h | 2 + tests/CMakeLists.txt | 18 +- tests/ReaderTest.cpp | 5 + .../fixtures/coarse-binary-bigendian-v41.msh | Bin 0 -> 27978 bytes tests/fixtures/coarse-binary-size4-v41.msh | Bin 0 -> 16918 bytes .../fixtures/coarse-binary-truncated-v41.msh | Bin 0 -> 7966 bytes tests/fixtures/coarse-o2-binary-v41.msh | Bin 0 -> 65530 bytes tests/fixtures/generate.py | 97 ++++++++ tests/fixtures/layered-binary-v41.msh | Bin 0 -> 109538 bytes tests/fixtures/periodic-binary-v41.msh | Bin 0 -> 73486 bytes 13 files changed, 334 insertions(+), 50 deletions(-) create mode 100644 tests/fixtures/coarse-binary-bigendian-v41.msh create mode 100644 tests/fixtures/coarse-binary-size4-v41.msh create mode 100644 tests/fixtures/coarse-binary-truncated-v41.msh create mode 100644 tests/fixtures/coarse-o2-binary-v41.msh create mode 100644 tests/fixtures/layered-binary-v41.msh create mode 100644 tests/fixtures/periodic-binary-v41.msh diff --git a/src/meshreader/GMSH4Parser.cpp b/src/meshreader/GMSH4Parser.cpp index 8264b7e..6e1b53c 100644 --- a/src/meshreader/GMSH4Parser.cpp +++ b/src/meshreader/GMSH4Parser.cpp @@ -3,8 +3,11 @@ // SPDX-License-Identifier: BSD-3-Clause #include "GMSH4Parser.h" +#include #include +#include #include +#include #include #include #include @@ -13,6 +16,19 @@ namespace puml { +namespace { +template T swapBytesOf(T value) { + std::array bytes{}; + std::memcpy(bytes.data(), &value, sizeof(T)); + std::reverse(bytes.begin(), bytes.end()); + std::memcpy(&value, bytes.data(), sizeof(T)); + return value; +} + +// blocks of binary element data are read in pieces of this many elements +constexpr std::size_t ElementChunk = 1 << 16; +} // namespace + void GMSH4Parser::parse_() { const auto format = parseMeshFormatHeader(); if (format.version < 4.0 || format.version >= 5.0) { @@ -24,10 +40,21 @@ void GMSH4Parser::parse_() { failFile("MSH 4.0 is not supported; write the mesh as MSH 4.1 (Mesh.MshFileVersion = 4.1)"); } if (format.fileType == 1) { - failFile("Binary MSH files are not supported; write the mesh as ASCII (Mesh.Binary = 0)"); - } - if (format.fileType != 0) { - failFile("Expected file type 0 (ASCII)"); + binary = true; + dataSize = format.dataSize; + if (dataSize != 4 && dataSize != 8) { + failFile("Unsupported data size " + std::to_string(dataSize) + " (expected 4 or 8)"); + } + beginSectionData(); + std::int32_t one = 0; + readBinary(&one, sizeof(one)); + if (swapBytesOf(one) == 1) { + swapBytes = true; + } else if (one != 1) { + failFile("Invalid byte order mark in the binary MSH file"); + } + } else if (format.fileType != 0) { + failFile("Expected file type 0 (ASCII) or 1 (binary)"); } expectToken("$EndMeshFormat"); @@ -71,7 +98,99 @@ void GMSH4Parser::parse_() { } } +void GMSH4Parser::beginSectionData() { + if (binary) { + if (!input->readLineBreak()) { + fail("Expected a line break before the binary data"); + } + binaryData = true; + } +} + +void GMSH4Parser::readBinary(void* data, std::size_t bytes) { + if (!input->readRaw(data, bytes)) { + failFile("Unexpected end of file in binary data"); + } +} + +std::size_t GMSH4Parser::readSize() { + if (!binary) { + return expectSize(); + } + std::size_t value = 0; + readSizes(&value, 1); + return value; +} + +long GMSH4Parser::readInt() { + if (!binary) { + return expectInteger(); + } + std::int32_t value = 0; + readBinary(&value, sizeof(value)); + return swapBytes ? swapBytesOf(value) : value; +} + +double GMSH4Parser::readDouble() { + if (!binary) { + return expectNumber(); + } + double value = 0; + readDoubles(&value, 1); + return value; +} + +void GMSH4Parser::readSizes(std::size_t* values, std::size_t count) { + if (!binary) { + for (std::size_t i = 0; i < count; ++i) { + values[i] = expectSize(); + } + return; + } + if (dataSize == sizeof(std::uint64_t)) { + static_assert(sizeof(std::size_t) == sizeof(std::uint64_t)); + readBinary(values, count * sizeof(std::uint64_t)); + if (swapBytes) { + std::transform(values, values + count, values, swapBytesOf); + } + } else { + narrowSizes.resize(count); + readBinary(narrowSizes.data(), count * sizeof(std::uint32_t)); + for (std::size_t i = 0; i < count; ++i) { + values[i] = swapBytes ? swapBytesOf(narrowSizes[i]) : narrowSizes[i]; + } + } +} + +void GMSH4Parser::readDoubles(double* values, std::size_t count) { + if (!binary) { + for (std::size_t i = 0; i < count; ++i) { + values[i] = expectNumber(); + } + return; + } + readBinary(values, count * sizeof(double)); + if (swapBytes) { + std::transform(values, values + count, values, swapBytesOf); + } +} + +std::size_t GMSH4Parser::position() { + if (!binary) { + input->skipWhitespace(); + } + return input->offset(); +} + +std::size_t GMSH4Parser::nodeIndex(std::size_t tag, std::size_t offset) { + if (tag < firstNodeTag || tag - firstNodeTag >= numNodes) { + failAt(offset, "Unknown node tag " + std::to_string(tag)); + } + return tag - firstNodeTag; +} + void GMSH4Parser::parseEntities() { + beginSectionData(); const std::size_t numPoints = readSize(); const std::size_t numCurves = readSize(); const std::size_t numSurfaces = readSize(); @@ -114,17 +233,8 @@ void GMSH4Parser::parseEntities() { expectToken("$EndEntities"); } -std::size_t GMSH4Parser::readNodeIndex() { - input->skipWhitespace(); - const auto offset = input->offset(); - const std::size_t tag = readSize(); - if (tag < firstNodeTag || tag - firstNodeTag >= numNodes) { - failAt(offset, "Unknown node tag " + std::to_string(tag)); - } - return tag - firstNodeTag; -} - void GMSH4Parser::parseNodes() { + beginSectionData(); const std::size_t numBlocks = readSize(); const std::size_t numVertices = readSize(); const std::size_t minNodeTag = readSize(); @@ -145,6 +255,7 @@ void GMSH4Parser::parseNodes() { // with as many distinct tags as the tag range is long, every node is defined exactly once std::vector defined(numVertices, false); std::vector indices; + std::vector coordinates; for (std::size_t blockIdx = 0; blockIdx < numBlocks; ++blockIdx) { const long dim = readInt(); @@ -153,27 +264,31 @@ void GMSH4Parser::parseNodes() { const std::size_t numVerticesInBlock = readSize(); indices.resize(numVerticesInBlock); - for (auto& index : indices) { - input->skipWhitespace(); - const auto offset = input->offset(); - index = readNodeIndex(); + const auto tagsOffset = input->offset(); + if (binary) { + readSizes(indices.data(), numVerticesInBlock); + } + for (std::size_t i = 0; i < numVerticesInBlock; ++i) { + auto offset = tagsOffset + i * dataSize; + if (!binary) { + offset = position(); + indices[i] = expectSize(); + } + const std::size_t index = nodeIndex(indices[i], offset); if (defined[index]) { - failAt(offset, "Duplicate node tag " + std::to_string(firstNodeTag + index)); + failAt(offset, "Duplicate node tag " + std::to_string(indices[i])); } defined[index] = true; + indices[i] = index; } // parametric nodes carry dim additional parametric coordinates - const long numParametric = parametric != 0 ? dim : 0; - for (const auto index : indices) { - std::array x{}; - for (auto& coordinate : x) { - coordinate = readDouble(); - } - for (long i = 0; i < numParametric; ++i) { - readDouble(); - } - builder->setVertex(static_cast(index), x); + const std::size_t valuesPerNode = 3 + (parametric != 0 ? static_cast(dim) : 0); + coordinates.resize(numVerticesInBlock * valuesPerNode); + readDoubles(coordinates.data(), coordinates.size()); + for (std::size_t i = 0; i < numVerticesInBlock; ++i) { + const double* x = &coordinates[i * valuesPerNode]; + builder->setVertex(static_cast(indices[i]), {x[0], x[1], x[2]}); } } expectToken("$EndNodes"); @@ -183,6 +298,7 @@ void GMSH4Parser::parseElements() { if (!hasNodes) { fail("Expected $Nodes before $Elements"); } + beginSectionData(); const std::size_t numBlocks = readSize(); const std::size_t numElements = readSize(); readSize(); // minimal element tag @@ -190,12 +306,12 @@ void GMSH4Parser::parseElements() { builder->setNumElements(numElements); std::array nodes{}; + std::vector values; for (std::size_t blockIdx = 0; blockIdx < numBlocks; ++blockIdx) { const long dim = readInt(); const long entityTag = readInt(); - input->skipWhitespace(); - const auto typeOffset = input->offset(); + const auto typeOffset = position(); const long type = readInt(); if (type < 1 || type > static_cast(NumTypes) || NumNodes[type - 1] == 0) { failAt(typeOffset, "Unknown element type " + std::to_string(type)); @@ -204,12 +320,31 @@ void GMSH4Parser::parseElements() { const std::size_t numElementsInBlock = readSize(); const long tag = numElementsInBlock > 0 ? physicalTag(dim, entityTag) : 0; - for (std::size_t elementIdx = 0; elementIdx < numElementsInBlock; ++elementIdx) { - readSize(); // element tag - for (std::size_t nodeIdx = 0; nodeIdx < numNodesPerElement; ++nodeIdx) { - nodes[nodeIdx] = static_cast(readNodeIndex()); + // each element consists of its tag and its node tags + const std::size_t valuesPerElement = 1 + numNodesPerElement; + for (std::size_t first = 0; first < numElementsInBlock; first += ElementChunk) { + const std::size_t count = std::min(ElementChunk, numElementsInBlock - first); + const auto chunkOffset = input->offset(); + if (binary) { + values.resize(count * valuesPerElement); + readSizes(values.data(), values.size()); + } + for (std::size_t element = 0; element < count; ++element) { + if (!binary) { + readSize(); // element tag + } + for (std::size_t node = 0; node < numNodesPerElement; ++node) { + if (binary) { + const std::size_t valueIdx = element * valuesPerElement + 1 + node; + nodes[node] = + static_cast(nodeIndex(values[valueIdx], chunkOffset + valueIdx * dataSize)); + } else { + const auto offset = position(); + nodes[node] = static_cast(nodeIndex(expectSize(), offset)); + } + } + builder->addElement(type, tag, nodes.data(), numNodesPerElement); } - builder->addElement(type, tag, nodes.data(), numNodesPerElement); } } expectToken("$EndElements"); @@ -219,6 +354,7 @@ void GMSH4Parser::parsePeriodic() { if (!hasNodes) { fail("Expected $Nodes before $Periodic"); } + beginSectionData(); const std::size_t numPeriodic = readSize(); // only the node identification is needed @@ -232,8 +368,10 @@ void GMSH4Parser::parsePeriodic() { } const std::size_t numIdentified = readSize(); for (std::size_t i = 0; i < numIdentified; ++i) { - const std::size_t node = readNodeIndex(); - const std::size_t master = readNodeIndex(); + const auto nodeOffset = position(); + const std::size_t node = nodeIndex(readSize(), nodeOffset); + const auto masterOffset = position(); + const std::size_t master = nodeIndex(readSize(), masterOffset); builder->addVertexLink(node, master); } } diff --git a/src/meshreader/GMSH4Parser.h b/src/meshreader/GMSH4Parser.h index 7ca1ef7..77f8c5b 100644 --- a/src/meshreader/GMSH4Parser.h +++ b/src/meshreader/GMSH4Parser.h @@ -5,15 +5,17 @@ #define PUMGEN_SRC_MESHREADER_GMSH4PARSER_H_ #include +#include #include #include +#include #include "GMSHParser.h" namespace puml { /** - * Parser for MSH 4.1 files. + * Parser for MSH 4.1 files, ASCII or binary. */ class GMSH4Parser : public GMSHParser { public: @@ -26,15 +28,34 @@ class GMSH4Parser : public GMSHParser { void parseElements(); void parsePeriodic(); - // the values of the sections - std::size_t readSize() { return expectSize(); } - long readInt() { return expectInteger(); } - double readDouble() { return expectNumber(); } + /** + * Starts the data of a section; in binary files, the data begins after the line break. + */ + void beginSectionData(); + + // the values of the sections: text, or binary with sizeof(size_t) == dataSize and 4-byte ints + std::size_t readSize(); + long readInt(); + double readDouble(); + // bulk reading of binary sizes and doubles + void readSizes(std::size_t* values, std::size_t count); + void readDoubles(double* values, std::size_t count); + void readBinary(void* data, std::size_t bytes); /** - * Reads a node tag and returns the index of the node. + * The offset of the next value, e.g. for error messages. */ - std::size_t readNodeIndex(); + std::size_t position(); + + /** + * The index of the node with the given tag; the tag started at the given file offset. + */ + std::size_t nodeIndex(std::size_t tag, std::size_t offset); + + bool binary = false; + bool swapBytes = false; + std::size_t dataSize = 8; + std::vector narrowSizes; /** * The physical tag of the elements of an entity; 0 if the entity has no physical group. diff --git a/src/meshreader/GMSHParser.cpp b/src/meshreader/GMSHParser.cpp index a44488d..5c0c3bd 100644 --- a/src/meshreader/GMSHParser.cpp +++ b/src/meshreader/GMSHParser.cpp @@ -28,16 +28,22 @@ bool GMSHParser::parseFile(const std::string& fileName) { } void GMSHParser::failAt(std::size_t offset, std::string_view message) { - const auto [line, column] = input->location(offset); std::stringstream stream; - stream << "GMSH parser error in line " << line << " in column " << column << ":\n"; + if (binaryData) { + stream << "GMSH parser error at byte " << offset << ":\n"; + } else { + const auto [line, column] = input->location(offset); + stream << "GMSH parser error in line " << line << " in column " << column << ":\n"; + } stream << '\t' << message << '\n'; errorMsg += stream.str(); throw ParseError{}; } void GMSHParser::fail(std::string_view message) { - input->skipWhitespace(); + if (!binaryData) { + input->skipWhitespace(); + } failAt(input->offset(), message); } @@ -49,6 +55,7 @@ void GMSHParser::failFile(std::string_view message) { } void GMSHParser::expectToken(std::string_view token) { + binaryData = false; input->skipWhitespace(); const auto offset = input->offset(); if (input->readToken() != token) { diff --git a/src/meshreader/GMSHParser.h b/src/meshreader/GMSHParser.h index 9462d06..f71f32c 100644 --- a/src/meshreader/GMSHParser.h +++ b/src/meshreader/GMSHParser.h @@ -245,6 +245,8 @@ class GMSHParser { GMSHMeshBuilder* builder; std::unique_ptr input; + // errors in binary data are located by byte offset instead of line and column + bool binaryData = false; private: std::size_t bufferSize; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e901b9b..066b0ff 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -126,8 +126,6 @@ pumgen_add_test(msh4.tiny-unknowntype RANKS 1 SOURCE msh4 INPUT tiny-unknowntype EXPECT_ERROR "Unknown element type 999") pumgen_add_test(msh4.tiny-v40 RANKS 1 SOURCE msh4 INPUT tiny-v40.msh EXPECT_ERROR "MSH 4.0 is not supported") -pumgen_add_test(msh4.coarse-binary RANKS 1 SOURCE msh4 INPUT coarse-binary-v41.msh - EXPECT_ERROR "Binary MSH files are not supported") pumgen_add_test(msh2.tiny-as-msh4 RANKS 1 SOURCE msh2 INPUT tiny-v41.msh EXPECT_ERROR "Unsupported MSH version 4.1") @@ -168,3 +166,19 @@ pumgen_add_test(msh4.coarse-partitioned RANKS 1 SOURCE msh4 INPUT coarse-partiti EXPECT_ERROR "Partitioned MSH files are not supported") pumgen_add_test(msh4.tiny-nophysical RANKS 1 SOURCE msh4 INPUT tiny-nophysical-v41.msh EXPECT_OUTPUT "1 volume[(]s[)] and 0 surface[(]s[)] without a physical group") + +# binary MSH 4.1; the coordinates of the references come from the ASCII files +foreach(ranks 1 2 3) + pumgen_add_test(msh4.layered-binary.np${ranks} RANKS ${ranks} SOURCE msh4 + INPUT layered-binary-v41.msh REFERENCE layered.puml.h5 TOLERANCE 1e-12) +endforeach() +foreach(variant binary binary-bigendian binary-size4) + pumgen_add_test(msh4.coarse-${variant}.np2 RANKS 2 SOURCE msh4 INPUT coarse-${variant}-v41.msh + REFERENCE coarse.puml.h5 TOLERANCE 1e-12) +endforeach() +pumgen_add_test(msh4.coarse-o2-binary.np2 RANKS 2 SOURCE msh4 INPUT coarse-o2-binary-v41.msh + REFERENCE coarse-o2.puml.h5 TOLERANCE 1e-12 ARGS --order=2) +pumgen_add_test(msh4.periodic-binary.np2 RANKS 2 SOURCE msh4 INPUT periodic-binary-v41.msh + REFERENCE periodic.puml.h5 TOLERANCE 1e-12 IDENTIFY) +pumgen_add_test(msh4.coarse-binary-truncated RANKS 1 SOURCE msh4 + INPUT coarse-binary-truncated-v41.msh EXPECT_ERROR "Unexpected end of file") diff --git a/tests/ReaderTest.cpp b/tests/ReaderTest.cpp index 75efd93..ae43e60 100644 --- a/tests/ReaderTest.cpp +++ b/tests/ReaderTest.cpp @@ -92,6 +92,11 @@ int main(int argc, char** argv) { ok &= check(fixtures + "/periodic-parametric-v41.msh"); ok &= check(fixtures + "/coarse-o2-v41.msh"); ok &= check(fixtures + "/coarse-o2-v22.msh"); + ok &= check(fixtures + "/layered-binary-v41.msh"); + ok &= check(fixtures + "/periodic-binary-v41.msh"); + ok &= check(fixtures + "/coarse-binary-bigendian-v41.msh"); + ok &= check(fixtures + "/coarse-binary-size4-v41.msh"); + ok &= check(fixtures + "/coarse-o2-binary-v41.msh"); ok &= checkSkippedSection(fixtures); return ok ? 0 : 1; } diff --git a/tests/fixtures/coarse-binary-bigendian-v41.msh b/tests/fixtures/coarse-binary-bigendian-v41.msh new file mode 100644 index 0000000000000000000000000000000000000000..d47cc2caa0f079ef13539bcf546a811fe7cf83b6 GIT binary patch literal 27978 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 k+T}YhyTUsNq+Xf*|dEJR@!2#np;_# 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/coarse-o2-binary-v41.msh b/tests/fixtures/coarse-o2-binary-v41.msh new file mode 100644 index 0000000000000000000000000000000000000000..95693b19011b75ee9ef7bdf444738f264a507a8d GIT binary patch literal 65530 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") and size of size_t (4 or 8).""" + with open(source, "rb") as f: + data = f.read() + pos = 0 + out = bytearray() + size_format = "Q" if size_bytes == 8 else "I" + + def line(): + nonlocal pos + end = data.index(b"\n", pos) + text = data[pos:end] + pos = end + 1 + return text + + def get(fmt, count=1): + nonlocal pos + fmt = f"<{count}{fmt}" + values = struct.unpack_from(fmt, data, pos) + pos += struct.calcsize(fmt) + out.extend(struct.pack(byte_order + fmt[1:].replace("Q", size_format), *values)) + return values + + def sizes(count=1): + return get("Q", count) + + assert line() == b"$MeshFormat" and line() == b"4.1 1 8" + out.extend(f"$MeshFormat\n4.1 1 {size_bytes}\n".encode()) + assert get("i")[0] == 1 + assert line() == b"" + out.extend(b"\n") + while pos < len(data): + text = line() + out.extend(text + b"\n") + if text == b"$MeshFormat": + continue + if text == b"$Entities": + counts = sizes(4) + for kind, count in enumerate(counts): + for _ in range(count): + get("i") + get("d", 3 if kind == 0 else 6) + get("i", sizes()[0]) + if kind > 0: + get("i", sizes()[0]) + elif text == b"$Nodes": + num_blocks = sizes(4)[0] + for _ in range(num_blocks): + dim, _, parametric = get("i", 3) + count = sizes()[0] + sizes(count) + get("d", count * (3 + (dim if parametric else 0))) + elif text == b"$Elements": + num_blocks = sizes(4)[0] + for _ in range(num_blocks): + _, _, element_type = get("i", 3) + count = sizes()[0] + sizes(count * (1 + ELEMENT_NODES[element_type])) + elif text == b"$Periodic": + for _ in range(sizes()[0]): + get("i", 3) + get("d", sizes()[0]) + sizes(2 * sizes()[0]) + elif text.startswith(b"$End"): + continue + else: + raise ValueError(f"unexpected section {text}") + assert line() == b"" + end = line() + assert end == b"$End" + text[1:] + out.extend(b"\n" + end + b"\n") + with open(target, "wb") as f: + f.write(out) + + def counts_exercise_chunking(count): # the element/vertex counts are chosen such that the default chunk distribution differs # from a ceil-based one for 3 and for 4 ranks @@ -157,6 +239,7 @@ def generate_layered(): print(f"layered: {len(cells)} cells, {len(vertices)} vertices, size {size_top:.4f}") write_mesh("layered-v41.msh", 4.1) + write_mesh("layered-binary-v41.msh", 4.1, binary=True) write_mesh("layered-v22.msh", 2.2) gmsh.write("layered.neu") write_reference("layered.puml.h5", "layered-v41.msh") @@ -179,12 +262,25 @@ def generate_coarse(): gmsh.model.mesh.setOrder(2) write_mesh("coarse-o2-v41.msh", 4.1) + write_mesh("coarse-o2-binary-v41.msh", 4.1, binary=True) write_mesh("coarse-o2-v22.msh", 2.2) gmsh.model.mesh.setOrder(1) gmsh.model.mesh.partition(2) write_mesh("coarse-partitioned-v41.msh", 4.1) + # the converter reproduces gmsh's layout exactly before it is used for other layouts + convert_binary("coarse-binary-v41.msh", "coarse-binary-copy.msh", "<", 8) + with open("coarse-binary-v41.msh", "rb") as a, open("coarse-binary-copy.msh", "rb") as b: + assert a.read() == b.read() + os.remove("coarse-binary-copy.msh") + convert_binary("coarse-binary-v41.msh", "coarse-binary-bigendian-v41.msh", ">", 8) + convert_binary("coarse-binary-v41.msh", "coarse-binary-size4-v41.msh", "<", 4) + with open("coarse-binary-v41.msh", "rb") as f: + data = f.read() + with open("coarse-binary-truncated-v41.msh", "wb") as f: + f.write(data[: data.index(b"$Elements") + 400]) + write_reference("coarse.puml.h5", "coarse-v41.msh") write_reference("coarse-o2.puml.h5", "coarse-o2-v41.msh") @@ -204,6 +300,7 @@ def generate_periodic(): gmsh.option.setNumber("Mesh.MeshSizeMax", 0.4) gmsh.model.mesh.generate(3) write_mesh("periodic-v41.msh", 4.1) + write_mesh("periodic-binary-v41.msh", 4.1, binary=True) gmsh.option.setNumber("Mesh.SaveParametric", 1) write_mesh("periodic-parametric-v41.msh", 4.1) gmsh.option.setNumber("Mesh.SaveParametric", 0) diff --git a/tests/fixtures/layered-binary-v41.msh b/tests/fixtures/layered-binary-v41.msh new file mode 100644 index 0000000000000000000000000000000000000000..b9bda8934e4ada7c09ec76abecf21ab4df105d18 GIT binary patch literal 109538 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/periodic-binary-v41.msh b/tests/fixtures/periodic-binary-v41.msh new file mode 100644 index 0000000000000000000000000000000000000000..d0713ef6f68e647c11ec28b53b389ff878a35eab GIT binary patch literal 73486 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%0DH>(qKTu8fw?y)5_3g;qutaxAz# z*XKu~%o}rL`zBw~dXNuhm{`2tl;u(9^&7`rQ|Dmxb@|g@L^VIZ978TkOJ&~j8tt0j?kD4y%cXOeKcSTP&9Wt!!yra>E z-Yu{A|%KXu1X;f_Lymvksc_3=LveoksT)!u}yu#~CH&oabOxh6@&i&>ik8Il(wcFD6)dGe0MOQX+j>AL&i z*cDO54<=sl>cq{_qM8Fv&;Mvs^w_*SFVsA+E&BAQ12vB9J{rZ|to7BB{QILp=#L=J z$G&KF`@|Fbw??n!-d()IxE;~jlRy2j^6=58#erW8>1JUtInocQ^ z;Xu^*pZ8mhs*)IWn)_^)jkg?#iq5XEJk#m@(YSl=oZ0Z;f#~Fte0%a|-Wru(JiB|w zC^5Qc&%lS4Hv2mIWZceUmC9_6GIkqRuGip$QHfEXcKM=OVzh4hgV+52!Pcm9uI*3X zT6u6zsLIS+W8bw)j7|-hU#i3v`=fcef3CZr%(CdUkH5Wm zT$cmUAN>~I(Yf6IXyJ#Syu9X;tx>NpieJ%n<#$DC>j<-i|-4@k2^V`f7cOH#uujqDS!kf#Zvp;xF{+V;}nms!A z?A)tg-|T*D*}=bXS0yONU!LO4P8?e&{S*HqN6eM1PVg^O;@tS3o0B1huRZQB{wG^9 zr2J*Ke-YzHW&K#=z5#hWcv)aqKMTEab=-~UCxgbraq_eq7sr)n+>>AtP`i4^yB^1RPNp!fDCjtO z+KXXVcD(yv9Q!Sxb>u1J5}>kn^RFcKTS5K2f_zn=*N^tAu~z`~GY+{FsI1-b*I=&* z>gQGD(t%z-+RFsv)tf(Mv73M9*|X@c4eaWlLtieit2h6z!#)x8{98ZFTlMl@gKxfB z7v#;rZeFQ(oObi&W%%Y%m@n$(y$RoZFkj_qHy@0<@iETsr+RsB;NSgJ@4jny-#vfM z<8{VY0;hrUwBHu!<;_BWJLq%GU*lI9yL$IQdxTxRJkL)R?8enPYkpM4u3nz|Q!TKo z_k35!ZhXyO<6i^2vUc;oCU)~yKhLAD1uAQIe`*Kg<-LHuPGDE>I_m~@^~R?jcE{=e zMfCMSW$hEtHvpBjJO75*^{=0o&^HR~>dpVg*qyh2v^T-7U+3XDZHisL@~i{Ruq$hK zd~@vjHIL2D7TDFxbNwx`>rcDm@4)W-oQLP@PVAmbdD`y^#>+E4t+4CYy6E`U*ws%% z)}Ap*J}>E?ILd7UClk0Dyclc?RsprkzZ-o%a1|JTPW(^1WJvkJhIVVwJ=jZwE5Y-@ zdjnZ}4fO57tl&4`RbU6u+M`{5N9-4Y?}M(VQ=oVL+B;*<4=x4u*Cmj(`<(l*7XaS@ z_187fJ6?M??C#fMWc}SA$l85Q_h7vHE3XH3-9-FO>UOT$aZy~F9{;ub3 z?E0S{_?sEnJsAbW3T?Ez;3Q&MSmT54D<{{$nvxs8`pgay!D`a zpMbr9lsi$d7+<8=+}Vi^`m`l zFkZd+V_a{5=Q%lmtUqP#=8t)CBYgerK{g)B+V!^)UU^VI&fEDaYuDdq#(A#vvk(22 zz`uU9Zw_3|A5 zJ$&QrdC>1+?CRy|*LiApJ&yA^`je;KIGX34H{;|vQ`T-?|G@l|wY%@;vGex4I{%*- zuU?*UiD7piXtOtqce*u-XyMMo8SJv*jPGa}`m`BFPxI3scdM6YeEre4cIWMQe+=&P zJ)h1mW77U-N%|*_t&{$V|M7?88Ir{WedI>=2jw>hcKInklEtpSOvwItR#|%$?Ea{> zD6-GXjO>qXm9a+!k^OOQjleG7AMLtcYt#ks{V}h0<0H=>`N}tkE@Zqv z{*|v?zCQ|C0@R;Bf>!T%?fy90eE5y=`twKA%G&jJHP3gv z`6bUES>J+RYmn!~A786Cf3*9f>{}Ucjn$t&)>f|{?PZx?1;%@a>+f3Z>h+_&9Cpu> zHCBJu1^%_`&mW09-u;*7kH@PpKkx9W$o{BYyZI;2ADhefj;h9Ze}pbyyL^9~F5e!g zI^+G(x^~CQ^T+J+y~C_0{>WXvcKQDJUA_6^cz+asiuuUXpFfsY_70KnkLaDZcKQCe z-u?29F%SIFy>|D_ySxhfuk3jFRk7Q9c*k0As$o~Qe#x&M*u6uoM>VkPSG)ddV)r~6 z7te1k?CMVwXYIAItG9k=uY=wBI)CfA|B->RJ%aptjQ8AXmtP;dvh_iJ1MJSreXu?@ z#ID}`)7}WXde5i!#@IbS#@Bpmg5CYlF5mx1LfP{x-~V{R{V~7Y5C5YI?Vb;L{>K*b ze`o*PFaIM9?dHF{JD9)nAHn!Lv8y+Z`o9aivUc~QRp9?m{OhlEV7G^t*Cwz#e|c@O z%l92Y|94|o@A=f;4!g2;*Ktqa-@Mh|z1W?P=g|4I55{ZPPY3ME=D+?sVprB~ed>f= z+4;-wjNN=SKb>zE?8?Sh{(XVn@vft5@O~*Tdc&yp!?rLtYPf&haW_=V2}FiT)7&u0g&MtP1u5oww`IpS7bm zJbU#L$i8=Y$31|4DbFp2d>3*l)+kjjhnou;O)YGWiU?u)99^hpTRdS*0pEg&A{$C3)lZF zJoWND_l|oGUQPU2SKMdw?0I+@K=(s`p7R&5n}6o>HuLjNzp zbG@$5d3w)Ggy*?*eao4j=kjHE?yvjiKD)n@;2A&t8NXWCCxf2f?8x#xzpuz=KGwZG z$mYwd@XXWcf!+B{fp-aL{)c%#75l}YcKM!{Y1rL2_sMvB{-(qGmU)@K#<2?a*Fg8p zbNeFWJb$ml8;IUKFu%;J8SrYs^Laks`tk-m<5vW^1LIuBo9JIhf3CX!7QFD@a^Kv? zx8Yq1`rM}Yci(1Wcb!AAoA=jap9NYcGazRJ&HvfRKF8-b!LRdp2cC7n{4`(9(|6%{ zj=YBw&^zuupjfL=@ z#ovv{`tx4+61{nKuKKYk81MWFqIZ6aktgw7pEn>_*H`fJfOSCgrV;WI(0sOznfK=N zQg~TF@8_p@uKBeLyZK_iyRWxnUk-X6t$)@t_iY8dmxJ{hpN81K2CWl*^O?gq^Lr({ zC(w5b{JM{;;6038z2~ec_HRJ*)co`unWwAaIbMGm9S^PnJs-YTJjguUzqRmukMW!t zPwU9H@cbs^^B)P;wGN(fF|We>UJuXxHy<*fHxD)-`|U6*vi-iijp&y$pZ7uQsd=~w zUTy5&uN{y*_nYCl4^6RKkE&wd0$O*>)2{HX&s*Vz`RDmF54XWPS3a39+u@z!IoBb( zFD;OF1bTV*F|IcDouK*RdWORD-q{7O7kd5thOGbZ;8{=1gJJMo?{0YJx%b8S=xcy` zz&jcLGiW|IJ`vt}?A{~x$>#T7c;4sM4fEc*u@9d0c0YdGfacYHc%BFMAw2g7;CcS) zAYTGnPY)vJ1IvRx&pzT1`ddKfT?Dy4_&vCed7Gc+t9g1D-ll*z7+L-icn8pr2g82o zC_L+ib;|fyzmCEC6+gzYC$hZb@XS-=_#iy<`3HEOTjS9jp6B;Rc;S6vJ~YJs6KKEe zy2JIx;Dz_S?+uNy{|tupzA^Kwh5dwx-t%VsJa50i>jBSu^8;k}?N@l7FVD@-@H~Gf z;h8Vyhkhzz|1I#RpX})McM5q9&#^C0K<~YJ8rizo9sbjxb>nwIZpoH@a+HX+YaKt3if|M|1QpbJq6GGJp->ddh(F`dO7a}f z^~K1ZkIeA$p!b~DM6L^Ffp;~s@%V`OHA85(|pMW&w6Zq zz7jv~Z+3X+q4#_=2>j-NHygd{xgXj2=7eWGF}~J+bKYZg}p$`)+?# zgLz*7uOPDLP`}p63*mXbKjnF)8CMO=11|wt``_@b8+qX^Lm&2Y5##g0v;GZ)ZymI5 zT?8*5Rz&wa}e&pIK`IwQXTJo^aG*L0rm{#*?2C-koOJ!JQ{AiS^97iFHl@3`Je z;4Oz|einmooG*pvz4bkO?=R9#>I7A3D5j9@3J#5^RFm8`&jQI?~rjUj8h4CwVA(l*E($7EeS6MZyb0P>(Jj-@LFLvE{lp zO2PBqvi}e7rEB2jWj;kf>y_uDG(78$d1#+v9V!FQI%%CZ&Yq*P@Yb=uuAqIb_261~ z-p`)%@SZLQ?;_^!yv&5SF&-NC&6wQhLc zr+32l-ZpV7we z-w56m{Otq1kKK>P@H}4);CcRBM-zCy|2_}T@80(JP2o+%?mmS1+zg)mmG#&CGVhzi zvoA2-;XT#@p7)C9(sg()Tf+0cb$;fJ&%Xm+W7g3Gv`;kNcf#{rdrmwbjo6R7;JHrE zWmvCT!D|;h&-=^%v^BgZ;5`dk=ggNj@I2@CrJi@sd0TiV@Z)=`_rJWm;q`*o8w~q{ zcJRChy+_E>%8wN_3>-Hx)+{#?|!-uo`d%A7BSv>ZvC)sbb#l1Hm>>DFZ=P1 z@H{t9;@5NKIqC#2hTU`PId(rf!#e}d{xBoYmDdHH`DflWW1Q>051w_@`4q%o1F$PR z&v`@qc-}qd-QelpxEgQw<9>MS@MHhA8QC~@4|wY3Rb+lW;Cb&>Mqe0oKYGI3&2th# z>#FPT1@B_U8&~VR_hoN*VST?BzvjgQ@Rp(Xef520^XNf%*?8_R*sVwAcOQ7h!~L^v z7?-~A4&ldn`rXI6&=1~S@ahHgavlBQ9l>tAcOq9|-VebukBqDHv_E|q-sg;WJ-(mY z9}Ix^GCbe2o<`RHBk+2n_dV)iWcP6(ys(ajdHX0l_su+Vzgn{1LGbb+n}5c~af9K7 z_m6d{8si^>r@jt)&r@^cA@J^Hea_4MbKi!-^Zk51eBaN#w}!#%7R1Z=nfH&wGmqNB z*N^@4aCr9wcKPx~z%#GRU*l(9jf7`C)^0s8e@4OcJ;i&*d&Bqc(eOMMZJ6i1pz$07 zZwdP4p!?{)JpnI_i}5x-W8swx_x_T9X-o{obz7@qI7k0a}EJiLYI z=YX!qI{p;A@V@XKF|VG6*BU?8r>V%!_ZfKheZHrJ{ok|jzGb}oatPUa^&Gqz=$`^# z0iDP5@WS^?--FD97vOncSa-}X&+m)y-sL&&pFHC{0iNq~y)Q7%{`4hy3G8cj@FYC< zV z{Lbllbbp%h+$r$X%X3@{cvIo+;5mM0^!rN#c+=q7hge51$6f=R4lj(0`C|Qh4W9XE zAMX8OU3(o~c#b?jem9!|&%8g(dVGJh4!r@-?|+`V@VvbV&-aw(`0D^#Ki-1Z7`^j% zzRvG$czKDF=UPAJ^GtX?PrLIlF0!|*c%d*-{!z8#CpU2z;pTmJkOi!^Sm`={D<)D z!^7_e?%PN3+)wk)drRKO@J_Qn^Suer^SsT4XMM4*cyC)@K7sca{;tB0@z(D=c%DP` zKHulghj$TvJm=RSn^&L0GhWtR@14f*KMQ!qS-x@p9G>wrZtYo@@%sXvb;$a26rOcx zLGWCk<8z(gLU^w-{#h`5AN&&D0PMrS?x1zPw+AkuT#j@;jiHN zJ>VnkUk1D-@WMXq(SWxU-e&aP7v`7!;4*k6nV0c!LyH8gFn~nIr;{k_0#8AXWX~d@TTC$?+m^# znFnj&S?|odn#|Mqt%dgx{(Ns6hV1*>xA44=ycasav!1Sl=l6r+Jg*Tv&(V5#;rzU> zo#zI4iH!IA%06V*yAhu8HNW=4GtV}`GmoqvVcpma&w95NKjx=(d<#6+;k?88w-uiI zt{?9S{cVG1-K$UhtcTW@?eNTZ`5|uyJm2#Mv5tp9-;;O3%gpoqK9vz!-Y$6F%hT~Q z7BnxugEt(#{boO8`_SF++M%~jh4pI>Jmcqn+LznMC&F_-+=uX7?uBO_Sa+>Ao{xR- z$`J?W{ebgDzaQRs^wx{X$e!l|@Cq`{{B5B`8>f95>BM||#|@XQnIo%v|K`~^=xuETYEUjBxc zpSbzHW&FGc{|R{ZA+A&28F&X-pZjI}%*V6;kI#}g)=A$#%olkXG9^3me0VNgzvnF@ zys13bIv@4{nc!JBtW)+S#^pSC&d+@^|DA7Uc-ANLW*hVH{>%c;bLsaR*Xy}FAD(&Z zdd+Y5H!Hk6?3+CM68AeBJl~^EF|TrrYXN45Xa8Ip{R;S=^BnN*MDO2gwM4eR$_dZD zJqP~u>vMC#bKb7Q`^0tRhG+k5KUo65?%M_MT(9f2KQMk5!n2?9-guF9`20NZ+$Z)JK&tXI}a>y~+68s1Bcw;uU?pIZiA ze|X+ABaoZ2?y~TU`8lm_7_D*E)_3Pl-Z}^>P4?Nd# zJv{T?`!vj#8{o}jy#38wWbe%z;km!=Yxvyq@NQ+id1F6r{@eub4|soqzk}A9o8fs6 zoW_0zG*54VXZ(zp@$-D#3eR;o|1b|Kz;hkW{}rC=Ix505ZehRSy;BL^$Beg+4)ggo zc*b9Ujx+CXhZlYy%0NCjzsm5;JM+zaG%gW5>$CTP>-Ju#0x#?nty`6NepPtk{TtTF zYViEMf!~9y^S9xzI=rgHZw{D%{UmY?c-Bwn70$ONJoneQnXksL7Cg^~=fVA{#`9~# z(~o|`e5nI3%yaXA^xoQ)-U6D2Ry&84S;X|%gD!)vx~Az_adHpIxtYs4cv(pS+!Q+vnX4&wJVR z87KFn9lULf_Z)eytXKEIv+uP2^ktm;eJ?!Y=RO%H^S(Vi`%~w?AXrBSc#gMT8gK89 zj_{1j=lJ;=EXw|Lf_FE1`^Of@RpE7pXI!>m_x-~6nJ(}WvDRy54T^hBLk&==*@@{C;?zFVCBP&A;yOth4qP_8-2t_kd@f7>}^-_Jn8M z3-`@D=mjs4`Cm``jkodZ4R1EQf#Ch1>w5s+7Xi<4@*ad&4c>d8eX{4Y4?N=)-fPCM zFTDNuDFoI6+kpMxUBo(l?=TO|zy9#dd(V;aG=2}kv)=dSIo5si;9+>-y;_}lSjPvz zv;Th?f4+bCJ>n5~c^U6}ly%E|83^wd{CM9#g`9=)kHYgk#`uT#^dNYDGhSYfU>$?u z`F`)cY~3<0kHK4pzxkl&tR;Sjz%ySK!ZUB2$543oUGl@c8V0W_e&&Gsb^PP-JQwzR zoFT=iKEIjLz>$bjFzn+9=9@k;t^MIbWaqvnouU??%?>FS} z@M6e0@sknsyTDWM!a6pbapvjM@XmE_xQ=JwIiE-H6TS~V3-4TY-uXQT&vWBB^1GLP z#Pjfq@w}ekub}gI0iJc>4eS%ZtBB8w@Vpo8=ewN z;n~LyU|wM#^)fvBe&Zd+c@jL&na_0|K6f%a>tR?gGZ2?o;GOF{`o8`uyk^Y5DHxuk zDe(L*T$ksR1g-Z|1DWPFwg3vp9Rl)>i)ODuYJdCczw`&-d%4~{Jj%A*XQ~C%JANWXWWc~eB<|C z;7`5JH^1J8XW#C6!gb7n=Xm`YC-0pP;8`#17u^T*;KN{k;rxyBNAPMBzYbta(D%2G z1Aop>zs7GaJnySY#B~S$tQ()e^ZZnU=lhrEV;(%~z4u$#|ILTjiRaV@bK}?V>Yu{9 z6MI(d7X;%zgLfFcb$lnXb^LR9OVQ^C?*gq0U%+$Ut!vHU**`CUXTRZoy1(YZLU_j* zzX==+dY-?8_ZNEmTi=hoFBidcecspKG0ycahG%{>!frgxm#^R%|18AW{@MLl0?*%_ zm{*=Z^JOW#u{`e%Fb8PCeHT3YQP=S>JoElLcx};l0gr>% zkl(xEeW)IM6mOOqwvgA=XVUd^E(F5zTG~=zRr95IJ^an{~DYRx*tElvo6;q z-+XT{Pk)4G|Kq)7zvw)Eg6DTL`&RoR@BJ9O^LSnZ=G76r7X8ogyl2eoFrQDrbNpib ztPcGB0uB6w@S z%HUq`Z+PR7Gc(T$$ghI`z%%~t$4Kn%#~FC`SLU&K-4Oq0|DW_EjkSMWil28toUPx!gIVYBK-Saa2`CL=Q;A68~4oc7U6F%7}kv} z@akfp23r5E_vgd&e0VN$!mkZxg=b!Q&gNh@ud=}l`}s>)uiq21!z+N^{I$+lUvj`Z z&U40t2SC@66Q1+;T+e`a6VJ&7&p6D&u7CaIhPM{`K5zotw4tAew9$XC1JUWSA^USyugy(*l2jRY508f!==JKEi#!9G-Q+?{t?kPxI{xcs=lEA7uY( zpHu{%{nc&o!+z~bc=>rwxPQi_D7>&gy&u1`!D8@?vw2{=^;aC8`|deejUV%%1U%zr z-xl_3CE*oeyzzYk**@+nc==ebaW?M8`D%F08Gi?O59qy63Z8MXUtbB&d+Qo_#nC?k zo&oOzOT+sT`909SqB?RJc%Cog?EV?&vhb_}+P#ll#xK10Z-D1}Sv}@;5x5k*5#F`PT|xV%-N@zPDTn+a@NR;)26+)! z8GIMK8QxgroZwW@d-WE0_Ce#Y+YiN%Z-wXI?|2VbFB-ES72tV4O=0|6@JX;Dyxfed zkKXqd@6Sr`tQ+>XhvEBPaT`4I_6+vo;0f?{c-GU)c)shgzE+0kIem-qzGvOYxCma@ zM|gkPPgFU_bA6t5yed57T$AVc_g_trtHJBTy!^g30=X%?>hRu1@42v_@O;#O=lSql z_}<|8s0pt<{_21Y!CE}G7QE8P)&u*n7U*lkb3fb%_rf#-eZKDaOLM_qX8&Fc$! zp1gYS{QCf(YoB5K>cjh#^;xgxBHKqafcHCk`x3tsy55HHypOE!{{6D`uMs@=-8?Wa zJa3Hyf38Em`PBrT!Eqj3|@EEI|ek*%&X?`tV8yT6XDw@wt(mN z5B-MzTEeprF>c1u_}u}|b((kPq5E+synC3>-CzsQd-X1Op&#?xJZJ^abMX#-3V@5j z*6`j(_WoFfT#9^c15dmB@SbT4?>OVvgKa_IEAEEpJ#D`k_CxL9?O^;ia69Px;63p2 zF<At6d*RJz{3l@eUeF$1m>=fNtvsg#JkPi1-S>CTc}IAz*L9i)=1V7d=9~4= z`^$Xp4DU1M@BS`DwvXu&_#1=W`m4YD0)OXvPwxuvFZ}ra^9Qo;J>B4KM!yN%3;JE} zet3t_UkaN4wUE2RvmS@_#X8gjp8H_EwO+d)J>j_@=HKn?tNYUnp7*NvsP$T2Z+PlG zpZ@)bya(VF)&}i2+#Yh6>(?)Udwp@|1BFg%kw`b25ML^WpHU-#g$P z1^rGq0-pVV=iGB|A2AZ1b=Lm)S^RmJt zM^C}?zTJwSouK#X)9^B~j$g5RFS;Mkza%scbYe%Ab(4A1fU3;U~A;62GYCWD@*5&EYOco|r~^)bdc->+uCGq09mx8JrOe*<0?{QUwi8?yKLoAAu@^Rb)n_G@p! z^ZvA+c#oO4Z^OHf=RE*E0+z%7OnA=Ub79|YKF@-8n(>~?Q^>v_&4y?F`v|*r!E^Ku zJoChQW*xC^ybJFY{EP#==iHC?;7!Fo0~`xlr{9P7J$m!-0P;1&We&U(=+A(z-}rq1 z&+ie|ZTl+w&=28#fuGr+adJODf~OzH>)-nEF}&i;*E(QbFrVkbvtAwMx!w!z?Nv0;k7|u54;!D-)Hcaqn`sV2CpR!pTj$b zelNHQ^toTaJBZ#oxgFWMy8zy3^sXmd$3l4Sm;IyPZ9JD>!W)Xehrqs|`@RU?3iO|X z;W=6i&-3^CbNLlK?@8-q7x?D=5_sNQuE%+qe@o%bWW0G|-k2}T;JpWLA?UjN z9z*a`VGyg|su z_ex~r{yV%~=)VVdfc9m7z^j7ZI=C8n4)`ZL*X250|7duB!TSN(d=2yTZ+NcHJPrGv zf8bdkt&6VTygCEV?=99@>#4ki%*hkm$NYW-H-PR(26&$98rZD|=2=E~%I4un_~u_G zc>B>CpUaTjfak$e4tbs7Wrkvt;hM`*+;&{ z_!mL@$&26}L~s4rj%?k?4{tg8CE%x^-x&(P^E-;~Z`R*N8IlKF4DWBodklj)L6#P0DYgk z0bW7$KF|8w7X6Lz-bc;{hWSz+-Y3|<20sV=E_@Tbd(gK4n}bz}%gykdhx2m2>*3!L zJlE%ajNRwn3a=Pv(EC37CbI9fmEqlsz8TmK^zU^ec%{*Q2wnp|3RZ#VJ>|VM54-nPRd|1* z_dfa?+54y(yf4s~0tbRu%ZE1!`3mrIZ~#~X-j&Ejz-PcgU`=@517*>BKUkM)!8?fE zdweId_d;!Wf1vlhJSTbF1grzkI?)%tb;P<+7v2i&bHR_mX85ZI?|1Zmr#yx1ch>sw z-a~KQ{t3A{*Z|&0Wc&5=kynBZ;dMs-0Xz@1E;NG2bQ64EnF-H6q%pkF=)VE8g2%un z@H!%|1~Y;7p-tfd38S#v-`U4DgRlv^{rq6$a$s|K_V;#Y&w|f`joHxF z@cN)10X_`+eW(q*+~_X=e*x=*ZQ+IQ3+87Hcz4753;7?=dSidu4&Dyzp5I-__T%@! zn~&al_8GGM>Amn4p?AL`~GGt-!_Leek9t=LBhO+@BKY?*mVO55W5X`D(BScop~{Joha>`gg!OU>|t5AU_4(4EnvZFTC5( zZv?*rD}nvs{fvAemw%BL`vKW_ork;%90ad4@<{M}a3wexUIt|E zRrhTs`p4jTkJmsS`Wq7Pw3meMJchzE&%VK)6&wu?g9jwMh2Fa7{rfnCOxV3oJ0g3Z z4~MrJy?pDD{1Nbegy;SHHnR0}B)ntj!*y8CM+Llao!2pbG(7j$eYIY9hBpS@mB@(y z>QA1);6CPp?|yee9t+Pnh4HBZ?@4&0kbQ1ZWcOnnhA@6*;Ee&t!!xd)BjYXaDR}lH zvl#DrI*$A_ybF-68(CTKo#>x|XT9EyzAQKdd={SdHXHi7;56_#cwLa?h4XzLUT1jj zujkzJ`vN@oDK~u2r~CFIJfGveOr(J$=<~I{wxF4V3 z$L~3_0-kpF!+o0#uPfs>f#tw;;5z|NyZpiM-i3D>`FF4h^Zx_+y@01(zVUw_-sOxB zdHS0Z@bn{Je;>eWz&y;SJjgY{4+EZd`M&Rd1g{?BmV%|2@B7Ff!&5KMad*L+3(xc2 z8ht(d)kgj#;Axj{{h9~QIK&w5-&^`UW`4laF5mk3DLmg_ec$js_X_6uS-{gS{{iOl zIlKpvL!SP=2zdIDufGNG8ZwV3@$28ezKpyu;Axj{eg6{PTE_XEQGO!&Mex+ibDaJb z!yCsqzZV$qXVHHJPrW?HxgSg5nNM{XUySv?gS-@;dU=jB|CYhC{@1~e^~ZC#Jm6`U zpPz9n;Q3uMefcbSiM*XOPcc-rMJVBELxywCkE<9%I-`K=3h+U0xiuZL%RUAOD;?@u-aJniyb z??!muN0pgRb1)ycDd1_BUl`tIc=cI-R`k|0`-Uw6PrLkI7{3*s{grXFJ{Xs6@YKt5 z+-1yTJ3P;K`1fD_{n(Cxr$71bTlV5Ldv&VWqjS&Bz54adUc5=?KHYkE>eey4{dRmA zaaZxjUl&f6s~c?*DCbjn6Co$M=f( zzn`DV*!7IUBU_2K8ZRg=!u~2W$6=e;%!D8SAU~$lVEP-sF zToO4i7>`B#8UIb^%{7azJ7f|ugCF!;xRFok}2o9k3xMYCy)O3|NhT;6&d{h zS1*j&x$@?JyPo`SpUMC2y#C+Ljr-V#nqXJ37T67}4c-sd0lR~B!5*OR+&z))HG3g9 z0DFTC!3V%bpzo}Wk$o3!g4`Eu3ibn=f&Ibe;6q>w@L{keH~_o@d<3*79*FF_+@r{? zz(Jtj2nHj!0Ura~f_^i;8+j<$4jcyF13nJk3l0a{gCoEW;7G6|I120pjs`n}W56!p K+1@#&WBxz$B~F0= literal 0 HcmV?d00001 From e720b2269d759c666b04e1bb2a89f0eb222f405e Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 19:36:42 +0000 Subject: [PATCH 12/19] Run the single-rank error tests without mpiexec In one of about 90 runs, a test expecting an error did not see pumgen's error message: mpiexec may drop the output of a process which ends with MPI_Abort. The tests expecting an error on one rank now start pumgen as MPI singleton, which also shortens them from about 2.3 s to 0.35 s each. AI-generated. Model: Claude Opus 5.5 --- tests/CMakeLists.txt | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 066b0ff..eb510a5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,18 +18,21 @@ function(pumgen_add_test name) cmake_parse_arguments(PARSE_ARGV 1 TEST "IDENTIFY" "RANKS;SOURCE;INPUT;REFERENCE;TOLERANCE;EXPECT_OUTPUT;EXPECT_ERROR" "ARGS") set(output ${PUMGEN_TEST_OUTPUT}/${name}.puml.h5) - set(command - ${MPIEXEC_EXECUTABLE} - ${MPIEXEC_NUMPROC_FLAG} - ${TEST_RANKS} - ${MPIEXEC_PREFLAGS} - $ - ${MPIEXEC_POSTFLAGS} - -s - ${TEST_SOURCE} - ${TEST_ARGS} - ${PUMGEN_TEST_FIXTURES}/${TEST_INPUT} - ${output}) + if(DEFINED TEST_EXPECT_ERROR AND TEST_RANKS EQUAL 1) + # started as MPI singleton, since mpiexec may drop the output of a process ending with + # MPI_Abort + set(command $) + else() + set(command + ${MPIEXEC_EXECUTABLE} + ${MPIEXEC_NUMPROC_FLAG} + ${TEST_RANKS} + ${MPIEXEC_PREFLAGS} + $ + ${MPIEXEC_POSTFLAGS}) + endif() + list(APPEND command -s ${TEST_SOURCE} ${TEST_ARGS} ${PUMGEN_TEST_FIXTURES}/${TEST_INPUT} + ${output}) string(REPLACE ";" "|" command "${command}") set(script_args -DCOMMAND=${command} -DOUTPUT=${output}) From a7cf1b990abb9e783a0463e71b5c06536889b7fb Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 19:42:02 +0000 Subject: [PATCH 13/19] Speed up the boundary condition matching of GMSH meshes The matching kept a list of adjacent surface facets for every vertex and intersected these lists for each face of each cell. It now marks the vertices of the surface mesh and looks up only the faces of cells with at least three such vertices, in the surface faces sorted by their vertices. On the mesh with 3.6M cells (151016 boundary faces), 161886 faces remain to be looked up; the matching takes 0.105 s instead of 0.21 s, and the peak memory of reading the mesh drops from 230 MB to 215 MB. The results are identical. Adds a test for a surface face given twice, which remains an error. AI-generated. Model: Claude Opus 5.5 --- src/meshreader/ParallelGMSHReader.h | 96 +++++++++++++---------- tests/CMakeLists.txt | 4 + tests/fixtures/generate.py | 3 + tests/fixtures/tiny-duplicateface-v22.msh | 16 ++++ 4 files changed, 78 insertions(+), 41 deletions(-) create mode 100644 tests/fixtures/tiny-duplicateface-v22.msh diff --git a/src/meshreader/ParallelGMSHReader.h b/src/meshreader/ParallelGMSHReader.h index 0a9ac26..6e6d8b7 100644 --- a/src/meshreader/ParallelGMSHReader.h +++ b/src/meshreader/ParallelGMSHReader.h @@ -12,7 +12,9 @@ #include #include +#include #include +#include #include #include @@ -86,53 +88,65 @@ template class ParallelGMSHReader { * with the surface mesh. */ void convertBoundaryConditions() { - const auto nVerts = builder_.vertices.size(); - const auto nFacets = builder_.facets.size(); - const auto nElements = builder_.elements.size(); - - bcs_.clear(); - bcs_.resize(nElements); - - constexpr auto nFacetsPerTet = Dim + 1u; - constexpr auto nVertsPerTri = Dim; - - // vertex to triangle map - std::vector> vertex2facets(nVerts); - for (std::size_t fctNo = 0; fctNo < nFacets; ++fctNo) { - for (auto const& vtxNo : builder_.facets[fctNo]) { - vertex2facets[vtxNo].push_back(fctNo); + using Face = std::array; + struct BoundaryFace { + Face vertices; + int bc; + }; + const auto sortedFace = [](Face face) { + std::sort(face.begin(), face.end()); + return face; + }; + + // the surface mesh as sorted vertex triples; the vertices of a facet come first + std::vector onBoundary(builder_.vertices.size(), 0); + std::vector boundaryFaces(builder_.facets.size()); + for (std::size_t fctNo = 0; fctNo < builder_.facets.size(); ++fctNo) { + Face face{}; + for (std::size_t i = 0; i < Dim; ++i) { + face[i] = builder_.facets[fctNo][i]; + onBoundary[face[i]] = 1; } + boundaryFaces[fctNo] = {sortedFace(face), builder_.bcs[fctNo]}; } - for (auto& v2f : vertex2facets) { - std::sort(v2f.begin(), v2f.end()); - } + const auto byVertices = [](const BoundaryFace& a, const BoundaryFace& b) { + return a.vertices < b.vertices; + }; + std::sort(boundaryFaces.begin(), boundaryFaces.end(), byVertices); + const auto nElements = builder_.elements.size(); + bcs_.assign(nElements, bc_t{}); for (std::size_t elNo = 0; elNo < nElements; ++elNo) { - for (int localFctNo = 0; localFctNo < nFacetsPerTet; ++localFctNo) { - std::array nodes; - for (int localNo = 0; localNo < nVertsPerTri; ++localNo) { - const auto localNoElement = Facet2Nodes[localFctNo][localNo]; - nodes[localNo] = builder_.elements[elNo][localNoElement]; + const auto& element = builder_.elements[elNo]; + // only faces with all vertices on the surface mesh can be part of it + std::array vertexOnBoundary{}; + std::size_t verticesOnBoundary = 0; + for (std::size_t i = 0; i < Dim + 1; ++i) { + vertexOnBoundary[i] = onBoundary[element[i]] != 0; + verticesOnBoundary += vertexOnBoundary[i] ? 1 : 0; + } + if (verticesOnBoundary < Dim) { + continue; + } + for (std::size_t localFctNo = 0; localFctNo < Dim + 1; ++localFctNo) { + Face face{}; + bool candidate = true; + for (std::size_t i = 0; i < Dim; ++i) { + const auto localVertex = Facet2Nodes[localFctNo][i]; + candidate = candidate && vertexOnBoundary[localVertex]; + face[i] = element[localVertex]; + } + if (!candidate) { + continue; } - std::array, nVertsPerTri - 1> intersect; - std::set_intersection(vertex2facets[nodes[0]].begin(), vertex2facets[nodes[0]].end(), - vertex2facets[nodes[1]].begin(), vertex2facets[nodes[1]].end(), - std::back_inserter(intersect[0])); - for (int node = 2; node < nVertsPerTri; ++node) { - std::set_intersection(intersect[node - 2].begin(), intersect[node - 2].end(), - vertex2facets[nodes[node]].begin(), - vertex2facets[nodes[node]].end(), - std::back_inserter(intersect[node - 1])); + const BoundaryFace key{sortedFace(face), 0}; + const auto [first, last] = + std::equal_range(boundaryFaces.begin(), boundaryFaces.end(), key, byVertices); + if (last - first > 1) { + logError() << "A face of an element exists multiple times in the surface mesh."; } - if (!intersect[nVertsPerTri - 2].empty()) { - if (intersect[nVertsPerTri - 2].size() > 1) { - logError() << "A face of an element exists multiple times in the " - "surface mesh."; - } - const auto fctNo = intersect[nVertsPerTri - 2][0]; - bcs_[elNo][localFctNo] = adjustBoundaryCondition(builder_.bcs[fctNo]); - } else { - bcs_[elNo][localFctNo] = 0; + if (first != last) { + bcs_[elNo][localFctNo] = adjustBoundaryCondition(first->bc); } } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index eb510a5..117e837 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -185,3 +185,7 @@ pumgen_add_test(msh4.periodic-binary.np2 RANKS 2 SOURCE msh4 INPUT periodic-bina REFERENCE periodic.puml.h5 TOLERANCE 1e-12 IDENTIFY) pumgen_add_test(msh4.coarse-binary-truncated RANKS 1 SOURCE msh4 INPUT coarse-binary-truncated-v41.msh EXPECT_ERROR "Unexpected end of file") + +# a face of the surface mesh given twice +pumgen_add_test(msh2.tiny-duplicateface RANKS 1 SOURCE msh2 INPUT tiny-duplicateface-v22.msh + EXPECT_ERROR "exists multiple times in the surface mesh") diff --git a/tests/fixtures/generate.py b/tests/fixtures/generate.py index 3ae5a2f..c75b460 100644 --- a/tests/fixtures/generate.py +++ b/tests/fixtures/generate.py @@ -366,6 +366,9 @@ def generate_tiny(): "tiny-v22.msh": TINY_V22, "tiny-duplicate-v22.msh": TINY_V22.replace("3 0 1 0", "2 0 1 0"), "tiny-badnode-v22.msh": TINY_V22.replace("1 2 3 4\n$EndElements", "1 2 3 9\n$EndElements"), + "tiny-duplicateface-v22.msh": TINY_V22.replace( + "1\n1 4 2 7 1 1 2 3 4\n", "3\n1 4 2 7 1 1 2 3 4\n2 2 2 101 1 1 3 2\n3 2 2 105 1 2 1 3\n" + ), } for name, content in variants.items(): with open(name, "w") as out: diff --git a/tests/fixtures/tiny-duplicateface-v22.msh b/tests/fixtures/tiny-duplicateface-v22.msh new file mode 100644 index 0000000..7d9288e --- /dev/null +++ b/tests/fixtures/tiny-duplicateface-v22.msh @@ -0,0 +1,16 @@ +$MeshFormat +2.2 0 8 +$EndMeshFormat +$Nodes +4 +1 0 0 0 +2 1 0 0 +3 0 1 0 +4 0 0 1 +$EndNodes +$Elements +3 +1 4 2 7 1 1 2 3 4 +2 2 2 101 1 1 3 2 +3 2 2 105 1 2 1 3 +$EndElements From 6db9f7a7bf474988e0328dd4bbf0ed4d18e921c8 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 19:45:44 +0000 Subject: [PATCH 14/19] Release the GMSH mesh on rank 0 once it is distributed Rank 0 kept the complete parsed mesh until pumgen ended, i.e. also while computing the inspheres and writing. Each part (vertices, elements, groups, boundary conditions, identification) is now released right after it has been scattered, and the surface mesh right after the boundary conditions have been matched. On the mesh with 3.6M cells and one rank, the memory after reading drops from 392 MB to 192 MB and the peak of the whole run from 534 MB to 388 MB; the peak is now reached while the elements are scattered. The scatter no longer dereferences the data of empty vectors on the other ranks. AI-generated. Model: Claude Opus 5.5 --- src/meshreader/ParallelGMSHReader.h | 38 +++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/meshreader/ParallelGMSHReader.h b/src/meshreader/ParallelGMSHReader.h index 6e6d8b7..718cca8 100644 --- a/src/meshreader/ParallelGMSHReader.h +++ b/src/meshreader/ParallelGMSHReader.h @@ -46,6 +46,8 @@ template class ParallelGMSHReader { } builder_.postprocess(); convertBoundaryConditions(); + release(builder_.facets); + release(builder_.bcs); nVertices_ = builder_.vertices.size(); nElements_ = builder_.elements.size(); @@ -59,28 +61,48 @@ template class ParallelGMSHReader { [[nodiscard]] std::size_t nVertices() const { return nVertices_; } [[nodiscard]] std::size_t nElements() const { return nElements_; } - void readElements(std::size_t* elements) const { + + // Each read function distributes one part of the mesh and releases it on rank 0 afterwards, so + // it can be called only once. + + void readElements(std::size_t* elements) { static_assert(sizeof(typename GMSHBuilder::element_t) == nodeCount(Dim, Order) * sizeof(std::size_t)); - scatter(builder_.elements.data()->data(), elements, nElements(), nodeCount(Dim, Order)); + scatter(flatData(builder_.elements), elements, nElements(), nodeCount(Dim, Order)); + release(builder_.elements); } - void readVertices(double* vertices) const { + void readVertices(double* vertices) { static_assert(sizeof(typename GMSHBuilder::vertex_t) == Dim * sizeof(double)); - scatter(builder_.vertices.data()->data(), vertices, nVertices(), Dim); + scatter(flatData(builder_.vertices), vertices, nVertices(), Dim); + release(builder_.vertices); } - void readBoundaries(int* boundaries) const { + void readBoundaries(int* boundaries) { static_assert(sizeof(bc_t) == (Dim + 1) * sizeof(int)); - scatter(bcs_.data()->data(), boundaries, nElements(), Dim + 1); + scatter(flatData(bcs_), boundaries, nElements(), Dim + 1); + release(bcs_); + } + void readGroups(int* groups) { + scatter(builder_.groups.data(), groups, nElements(), 1); + release(builder_.groups); } - void readGroups(int* groups) const { scatter(builder_.groups.data(), groups, nElements(), 1); } constexpr static bool SupportsIdentify = true; bool hasIdentify() const { return hasIdentify_ != 0; } - void readIdentify(std::size_t* vertices) const { + void readIdentify(std::size_t* vertices) { scatter(builder_.identify.data(), vertices, nVertices(), 1); + release(builder_.identify); } private: + template + static const T* flatData(const std::vector>& values) { + return values.empty() ? nullptr : values.front().data(); + } + + template static void release(std::vector& values) { + std::vector().swap(values); + } + /** * GMSH stores boundary conditions on a surface mesh whereas SeisSol expects * boundary conditions to be stored per element. In the following we convert From 7f29207a83cb6b3f29a8009ea1072599a4a37242 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 19:56:33 +0000 Subject: [PATCH 15/19] Separate the velocity-aware mesh size from SimModSuite The settings of velocity-aware meshing (refinement cuboids, frequency, easi file, elements per wavelength) and their XML parsing move from MeshAttributes to src/sizing, as does the mesh size computation from EasiMeshSize, which now evaluates the easi model for many points at once. EasiMeshSize keeps only the SimModSuite part, i.e. finding the region of a point, and passes single points to the shared computation; the settings and the size formula stay the same. easi becomes a dependency of its own (option EASI, implied by SIMMETRIX), so that tools without SimModSuite can use the same settings and sizes. AI-generated. Model: Claude Opus 5.5 --- CMakeLists.txt | 24 ++++- src/input/EasiMeshSize.cpp | 90 +++---------------- src/input/EasiMeshSize.h | 25 ++---- src/input/MeshAttributes.cpp | 89 +----------------- src/input/MeshAttributes.h | 41 +-------- src/sizing/VelocityAwareMeshSize.cpp | 98 ++++++++++++++++++++ src/sizing/VelocityAwareMeshSize.h | 54 +++++++++++ src/sizing/VelocityAwareSettings.cpp | 129 +++++++++++++++++++++++++++ src/sizing/VelocityAwareSettings.h | 74 +++++++++++++++ 9 files changed, 398 insertions(+), 226 deletions(-) create mode 100644 src/sizing/VelocityAwareMeshSize.cpp create mode 100644 src/sizing/VelocityAwareMeshSize.h create mode 100644 src/sizing/VelocityAwareSettings.cpp create mode 100644 src/sizing/VelocityAwareSettings.h diff --git a/CMakeLists.txt b/CMakeLists.txt index be1cf15..4333aa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -fsa set(CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address") option(SIMMETRIX "Use simmetrix libraries" OFF) +option(EASI "Build the velocity-aware sizing with easi (always on with SIMMETRIX)" OFF) set(SIMMETRIX_ROOT "" CACHE STRING "Root directory of simmetrix dev files") set(SIM_MPI "" CACHE STRING "MPI version used by simmetrix") option(NETCDF "Use netcdf" OFF) @@ -93,10 +94,6 @@ target_link_libraries(pumgen PUBLIC tinyxml2) if (SIMMETRIX) find_package(SIMMETRIX REQUIRED) - # easi - find_package(easi 1.0.0 REQUIRED) - target_link_libraries(pumgen PUBLIC easi::easi) - target_sources(pumgen PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/input/AnalysisAttributes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/input/EasiMeshSize.cpp @@ -134,6 +131,25 @@ endfunction() cast_log_level_to_int(LOG_LEVEL LOG_LEVEL) target_compile_definitions(pumgen PUBLIC LOG_LEVEL=${LOG_LEVEL}) + +if (SIMMETRIX) + set(EASI ON) +endif() +if (EASI) + find_package(easi 1.0.0 REQUIRED) + add_library(pumgen_sizing STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/sizing/VelocityAwareSettings.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/sizing/VelocityAwareMeshSize.cpp + ) + target_include_directories(pumgen_sizing PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/submodules + ) + target_compile_definitions(pumgen_sizing PRIVATE LOG_LEVEL=${LOG_LEVEL}) + target_link_libraries(pumgen_sizing PUBLIC easi::easi tinyxml2) + target_link_libraries(pumgen PUBLIC pumgen_sizing) + target_compile_definitions(pumgen PUBLIC USE_EASI) +endif() if(SIMMETRIX) set(INSTALL_RPATH "${SIM_PS_KRNL_LIB_DIR}:${INSTALL_RPATH}") endif() diff --git a/src/input/EasiMeshSize.cpp b/src/input/EasiMeshSize.cpp index 6c82a96..d4a9e82 100644 --- a/src/input/EasiMeshSize.cpp +++ b/src/input/EasiMeshSize.cpp @@ -2,20 +2,15 @@ // // SPDX-License-Identifier: BSD-3-Clause #include "EasiMeshSize.h" -#include -#include -#include +#include +#include #include -EasiMeshSize::EasiMeshSize() : parser(nullptr), model(nullptr) {} - -EasiMeshSize::EasiMeshSize(VelocityAwareRefinementSettings refinementSettings, pGModel simModel, - std::unordered_map groupMap) - : refinementSettings(refinementSettings), simModel(simModel), parser(new easi::YAMLParser(3)), - groupMap(std::move(groupMap)) { - model = parser->parse(refinementSettings.getEasiFileName()); -} +EasiMeshSize::EasiMeshSize(const VelocityAwareRefinementSettings& refinementSettings, + pGModel simModel, std::unordered_map groupMap) + : meshSize(std::make_shared(refinementSettings)), simModel(simModel), + groupMap(std::move(groupMap)) {} int EasiMeshSize::findGroup(std::array point) { // GR_containsPoint can be expensive for large geometry, @@ -34,77 +29,20 @@ int EasiMeshSize::findGroup(std::array point) { return -1; } -std::tuple -EasiMeshSize::getTargetedFrequencyAndRegion(const std::array& point) { - const auto& refinementRegions = refinementSettings.getRefinementRegions(); - double targetedFrequency = 0.0; - int bypassFindRegionAndUseGroup = 0; - for (const auto& refinementCube : refinementRegions) { - auto& cuboid = refinementCube.cuboid; - bool isInCuboid = true; - - double u0 = (point[0] - cuboid.center[0]) * cuboid.cosSinRotationZ[0] + - (point[1] - cuboid.center[1]) * cuboid.cosSinRotationZ[1]; - isInCuboid &= std::abs(u0) <= cuboid.halfSize[0]; - double u1 = (point[0] - cuboid.center[0]) * -cuboid.cosSinRotationZ[1] + - (point[1] - cuboid.center[1]) * cuboid.cosSinRotationZ[0]; - isInCuboid &= std::abs(u1) <= cuboid.halfSize[1]; - double u2 = (point[2] - cuboid.center[2]); - isInCuboid &= std::abs(u2) <= cuboid.halfSize[2]; - - if (isInCuboid) { - if (refinementCube.targetedFrequency >= targetedFrequency) { - bypassFindRegionAndUseGroup = refinementCube.bypassFindRegionAndUseGroup; - } - targetedFrequency = std::max(targetedFrequency, refinementCube.targetedFrequency); - } - } - return std::make_tuple(targetedFrequency, bypassFindRegionAndUseGroup); -} - double EasiMeshSize::getMeshSize(const std::array& point) { - if (!model) { - logError() << "Model was not parsed correctly"; - } - - constexpr double defaultMeshSize = std::numeric_limits::max(); + constexpr double DefaultMeshSize = std::numeric_limits::max(); const auto [targetedFrequency, bypassFindRegionAndUseGroup] = - getTargetedFrequencyAndRegion(point); - + meshSize->targetedFrequencyAndGroup(point); if (targetedFrequency == 0.0) { - return defaultMeshSize; + return DefaultMeshSize; } - easi::Query query(1, 3); - query.x(0, 0) = point[0]; - query.x(0, 1) = point[1]; - query.x(0, 2) = point[2]; - query.group(0) = bypassFindRegionAndUseGroup ? bypassFindRegionAndUseGroup : findGroup(point); - + const int group = + bypassFindRegionAndUseGroup != 0 ? bypassFindRegionAndUseGroup : findGroup(point); // This means the point is slightly outside the geometry - if (query.group(0) <= 0) { - return defaultMeshSize; - } - - // Need array for easi interface - auto materials = std::array(); - auto& material = materials[0]; - auto adapter = easi::ArrayOfStructsAdapter(materials.data()); - adapter.addBindingPoint("lambda", &ElasticMaterial::lambda); - adapter.addBindingPoint("mu", &ElasticMaterial::mu); - adapter.addBindingPoint("rho", &ElasticMaterial::rho); - - model->evaluate(query, adapter); - - double waveSpeed = 0.0; - if (material.mu < 10e-14) { - // acoustic - waveSpeed = std::sqrt((material.lambda + 2 * material.mu) / material.rho); - } else { - // elastic - waveSpeed = std::sqrt(material.mu / material.rho); + if (group <= 0) { + return DefaultMeshSize; } - const auto waveLength = waveSpeed / targetedFrequency; - return waveLength / refinementSettings.getElementsPerWaveLength(); + return meshSize->meshSizes({point}, {group}, {targetedFrequency})[0]; } diff --git a/src/input/EasiMeshSize.h b/src/input/EasiMeshSize.h index a9fa62e..1df68ea 100644 --- a/src/input/EasiMeshSize.h +++ b/src/input/EasiMeshSize.h @@ -4,37 +4,26 @@ #ifndef PUMGEN_SRC_INPUT_EASIMESHSIZE_H_ #define PUMGEN_SRC_INPUT_EASIMESHSIZE_H_ -#include "MeshAttributes.h" +#include "sizing/VelocityAwareMeshSize.h" #include #include #include -#include -#include #include -#include - -struct ElasticMaterial { - double lambda, mu, rho; -}; +#include +/** + * Velocity-aware mesh size for SimModSuite, which asks for one point at a time. + */ class EasiMeshSize { private: - VelocityAwareRefinementSettings refinementSettings; - - easi::YAMLParser* parser; - easi::Component* model; // Unique ptr to model leads to segfault + std::shared_ptr meshSize; pGModel simModel; std::unordered_map groupMap; int findGroup(std::array point); - std::tuple - getTargetedFrequencyAndRegion(const std::array& point); - public: - EasiMeshSize(); - ; - EasiMeshSize(VelocityAwareRefinementSettings refinementSettings, pGModel simModel, + EasiMeshSize(const VelocityAwareRefinementSettings& refinementSettings, pGModel simModel, std::unordered_map groupMap); double getMeshSize(const std::array& point); diff --git a/src/input/MeshAttributes.cpp b/src/input/MeshAttributes.cpp index 4559e6f..500e40a 100644 --- a/src/input/MeshAttributes.cpp +++ b/src/input/MeshAttributes.cpp @@ -3,31 +3,6 @@ // SPDX-License-Identifier: BSD-3-Clause #include "MeshAttributes.h" -VelocityAwareRefinementSettings::VelocityAwareRefinementSettings(double elementsPerWaveLength, - std::string easiFileName) - : elementsPerWaveLength(elementsPerWaveLength), easiFileName(std::move(easiFileName)) {} - -void VelocityAwareRefinementSettings::addRefinementRegion(SimpleCuboid cuboid, - double targetedFrequency, - int bypassFindRegionAndUseGroup) { - refinementRegions.emplace_back(cuboid, targetedFrequency, bypassFindRegionAndUseGroup); -} - -bool VelocityAwareRefinementSettings::isVelocityAwareRefinementOn() const { - return !refinementRegions.empty(); -} - -const std::string& VelocityAwareRefinementSettings::getEasiFileName() const { return easiFileName; } - -double VelocityAwareRefinementSettings::getElementsPerWaveLength() const { - return elementsPerWaveLength; -} - -const std::vector& -VelocityAwareRefinementSettings::getRefinementRegions() const { - return refinementRegions; -} - MeshAttributes::MeshAttributes() : lpair_lVertexId_MSize(), lpair_lEdgeId_MSize(), lpair_lFaceId_MSize(), lpair_lRegionId_MSize() {} @@ -168,69 +143,7 @@ void MeshAttributes::set_regionMSize() { } void MeshAttributes::set_velocity_aware_meshing() { - int numChilds = 0; - const auto name = "VelocityAwareMeshing"; - for (auto velocityAwareMeshingElement = doc.FirstChildElement(name); velocityAwareMeshingElement; - velocityAwareMeshingElement = velocityAwareMeshingElement->NextSiblingElement(name)) { - const auto easiFileName = velocityAwareMeshingElement->Attribute("easiFile"); - const auto elementsPerWaveLength = - std::stof(velocityAwareMeshingElement->Attribute("elementsPerWaveLength")); - velocityAwareRefinementSettings = - VelocityAwareRefinementSettings(elementsPerWaveLength, easiFileName); - constexpr auto cuboidName = "VelocityRefinementCuboid"; - logInfo() << "Activating velocity aware meshing, using" << elementsPerWaveLength - << "elements per wavelength and easi file" << easiFileName; - for (auto child = velocityAwareMeshingElement->FirstChildElement(cuboidName); child; - child = child->NextSiblingElement(cuboidName)) { - - const char* attr = child->Attribute("rotationZAnticlockwiseFromX"); - double rotationZAnticlockwiseFromX = 0.0; - if (attr != nullptr) { - rotationZAnticlockwiseFromX = std::stof(attr); - } - attr = child->Attribute("bypassFindRegionAndUseGroup"); - int bypassFindRegionAndUseGroup = 0; - if (attr != nullptr) { - bypassFindRegionAndUseGroup = std::stoi(attr); - } - auto cuboid = SimpleCuboid{{ - std::stof(child->Attribute("centerX")), - std::stof(child->Attribute("centerY")), - std::stof(child->Attribute("centerZ")), - }, - { - std::stof(child->Attribute("halfSizeX")), - std::stof(child->Attribute("halfSizeY")), - std::stof(child->Attribute("halfSizeZ")), - }, - {std::cos(rotationZAnticlockwiseFromX * toRadians), - std::sin(rotationZAnticlockwiseFromX * toRadians)}, - rotationZAnticlockwiseFromX}; - const auto targetedFrequency = std::stof(child->Attribute("frequency")); - - velocityAwareRefinementSettings.addRefinementRegion(cuboid, targetedFrequency, - bypassFindRegionAndUseGroup); - logInfo() << "Adding velocity aware refinement region targeting" << targetedFrequency - << "Hz, centered at x =" << cuboid.center[0] << "y=" << cuboid.center[1] - << "z=" << cuboid.center[2] << "with half sizes" - << "x =" << cuboid.halfSize[0] << "y =" << cuboid.halfSize[1] - << "z =" << cuboid.halfSize[2]; - if (std::abs(cuboid.rotationZ) > 0.0) { - logInfo() << "rotated around z axis by " << cuboid.rotationZ - << "degree(s) counterclockwise from x axis."; - } - if (bypassFindRegionAndUseGroup) { - logInfo() << "bypass findRegion and use group =" << bypassFindRegionAndUseGroup; - } - } - if (!velocityAwareRefinementSettings.isVelocityAwareRefinementOn()) { - logWarning() << "Activated velocity aware meshing but did not specify any refinement region!"; - } - ++numChilds; - } - if (numChilds > 1) { - logError() << "Multiple definitions of velocityAwareMeshing"; - } + velocityAwareRefinementSettings = readVelocityAwareSettings(doc); } void MeshAttributes::set_global_mesh_attributes() { diff --git a/src/input/MeshAttributes.h b/src/input/MeshAttributes.h index cac7e40..337e743 100644 --- a/src/input/MeshAttributes.h +++ b/src/input/MeshAttributes.h @@ -10,6 +10,7 @@ #include #include +#include "sizing/VelocityAwareSettings.h" #include "split.h" #include "tinyxml2/tinyxml2.h" #include "utils/logger.h" @@ -25,46 +26,6 @@ struct Cube { double CubeMSize = 0, CubeCenter[3], CubeWidth[3], CubeHeight[3], CubeDepth[3]; }; -struct SimpleCuboid { - std::array center; - std::array halfSize; - std::array cosSinRotationZ; - double rotationZ; -}; - -struct VelocityRefinementCube { - VelocityRefinementCube(SimpleCuboid cuboid, double targetedFrequency, - int bypassFindRegionAndUseGroup) - : cuboid(cuboid), targetedFrequency(targetedFrequency), - bypassFindRegionAndUseGroup(bypassFindRegionAndUseGroup) {}; - - SimpleCuboid cuboid; - double targetedFrequency; - int bypassFindRegionAndUseGroup; -}; - -class VelocityAwareRefinementSettings { - public: - VelocityAwareRefinementSettings() = default; - VelocityAwareRefinementSettings(double elementsPerWaveLength, std::string easiFileName); - - void addRefinementRegion(SimpleCuboid cuboid, double targetedFrequency, - int bypassFindRegionAndUseGroup); - - [[nodiscard]] bool isVelocityAwareRefinementOn() const; - - [[nodiscard]] const std::string& getEasiFileName() const; - - [[nodiscard]] double getElementsPerWaveLength() const; - - [[nodiscard]] const std::vector& getRefinementRegions() const; - - private: - double elementsPerWaveLength{}; - std::string easiFileName; - std::vector refinementRegions{}; -}; - enum class ElementType { vertex = 0, edge = 1, face = 2, region = 3 }; using namespace tinyxml2; diff --git a/src/sizing/VelocityAwareMeshSize.cpp b/src/sizing/VelocityAwareMeshSize.cpp new file mode 100644 index 0000000..dfbfae9 --- /dev/null +++ b/src/sizing/VelocityAwareMeshSize.cpp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2021 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#include "VelocityAwareMeshSize.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "utils/logger.h" + +namespace { +struct ElasticMaterial { + double lambda; + double mu; + double rho; +}; + +// the material model is evaluated in batches of this many points +constexpr std::size_t QueryBatch = std::size_t{1} << 20; +} // namespace + +VelocityAwareMeshSize::VelocityAwareMeshSize(VelocityAwareRefinementSettings settings) + : settings(std::move(settings)), parser(std::make_unique(3)) { + model = parser->parse(this->settings.getEasiFileName()); + if (model == nullptr) { + logError() << "Could not parse the easi file" << this->settings.getEasiFileName(); + } +} + +VelocityAwareMeshSize::~VelocityAwareMeshSize() { delete model; } + +std::pair +VelocityAwareMeshSize::targetedFrequencyAndGroup(const std::array& point) const { + double targetedFrequency = 0.0; + int bypassFindRegionAndUseGroup = 0; + for (const auto& refinementCube : settings.getRefinementRegions()) { + if (refinementCube.cuboid.contains(point)) { + if (refinementCube.targetedFrequency >= targetedFrequency) { + bypassFindRegionAndUseGroup = refinementCube.bypassFindRegionAndUseGroup; + } + targetedFrequency = std::max(targetedFrequency, refinementCube.targetedFrequency); + } + } + return {targetedFrequency, bypassFindRegionAndUseGroup}; +} + +std::vector +VelocityAwareMeshSize::meshSizes(const std::vector>& points, + const std::vector& groups, + const std::vector& frequencies) const { + std::vector sizes(points.size(), std::numeric_limits::max()); + + std::vector evaluated; + for (std::size_t i = 0; i < points.size(); ++i) { + if (frequencies[i] > 0.0 && groups[i] > 0) { + evaluated.push_back(i); + } + } + + std::vector materials; + for (std::size_t first = 0; first < evaluated.size(); first += QueryBatch) { + const std::size_t count = std::min(QueryBatch, evaluated.size() - first); + easi::Query query(count, 3); + for (std::size_t j = 0; j < count; ++j) { + const auto i = evaluated[first + j]; + for (int k = 0; k < 3; ++k) { + query.x(j, k) = points[i][k]; + } + query.group(j) = groups[i]; + } + + materials.assign(count, ElasticMaterial{}); + easi::ArrayOfStructsAdapter adapter(materials.data()); + adapter.addBindingPoint("lambda", &ElasticMaterial::lambda); + adapter.addBindingPoint("mu", &ElasticMaterial::mu); + adapter.addBindingPoint("rho", &ElasticMaterial::rho); + model->evaluate(query, adapter); + + for (std::size_t j = 0; j < count; ++j) { + const auto& material = materials[j]; + // the shear wave speed, or the P wave speed in acoustic material + const double waveSpeed = material.mu < 10e-14 + ? std::sqrt((material.lambda + 2 * material.mu) / material.rho) + : std::sqrt(material.mu / material.rho); + const auto i = evaluated[first + j]; + const double waveLength = waveSpeed / frequencies[i]; + sizes[i] = waveLength / settings.getElementsPerWaveLength(); + } + } + return sizes; +} diff --git a/src/sizing/VelocityAwareMeshSize.h b/src/sizing/VelocityAwareMeshSize.h new file mode 100644 index 0000000..2036175 --- /dev/null +++ b/src/sizing/VelocityAwareMeshSize.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2021 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#ifndef PUMGEN_SRC_SIZING_VELOCITYAWAREMESHSIZE_H_ +#define PUMGEN_SRC_SIZING_VELOCITYAWAREMESHSIZE_H_ + +#include +#include +#include +#include + +#include "VelocityAwareSettings.h" + +namespace easi { +class Component; +class YAMLParser; +} // namespace easi + +/** + * Mesh sizes from the wave speeds of an easi material model: the wavelength of the targeted + * frequency divided by the number of elements per wavelength. + */ +class VelocityAwareMeshSize { + public: + explicit VelocityAwareMeshSize(VelocityAwareRefinementSettings settings); + ~VelocityAwareMeshSize(); + VelocityAwareMeshSize(const VelocityAwareMeshSize&) = delete; + VelocityAwareMeshSize& operator=(const VelocityAwareMeshSize&) = delete; + + /** + * The highest targeted frequency of the refinement regions containing the point (0 if there is + * none) and the group fixed by that region (0 if the region does not fix one). + */ + [[nodiscard]] std::pair + targetedFrequencyAndGroup(const std::array& point) const; + + /** + * The mesh sizes at the points for the given frequencies, with the material of the given + * groups; points with a frequency of 0 or a group <= 0 get std::numeric_limits::max(). + * The material model is evaluated for all points at once. + */ + [[nodiscard]] std::vector meshSizes(const std::vector>& points, + const std::vector& groups, + const std::vector& frequencies) const; + + [[nodiscard]] const VelocityAwareRefinementSettings& getSettings() const { return settings; } + + private: + VelocityAwareRefinementSettings settings; + std::unique_ptr parser; + easi::Component* model = nullptr; +}; + +#endif // PUMGEN_SRC_SIZING_VELOCITYAWAREMESHSIZE_H_ diff --git a/src/sizing/VelocityAwareSettings.cpp b/src/sizing/VelocityAwareSettings.cpp new file mode 100644 index 0000000..98c9406 --- /dev/null +++ b/src/sizing/VelocityAwareSettings.cpp @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2020 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#include "VelocityAwareSettings.h" + +#include +#include +#include +#include + +#include "tinyxml2/tinyxml2.h" +#include "utils/logger.h" + +namespace { +const double ToRadians = std::acos(-1) / 180.0; +} // namespace + +bool SimpleCuboid::contains(const std::array& point, double margin) const { + const double u0 = + (point[0] - center[0]) * cosSinRotationZ[0] + (point[1] - center[1]) * cosSinRotationZ[1]; + const double u1 = + (point[0] - center[0]) * -cosSinRotationZ[1] + (point[1] - center[1]) * cosSinRotationZ[0]; + const double u2 = point[2] - center[2]; + return std::abs(u0) <= halfSize[0] + margin && std::abs(u1) <= halfSize[1] + margin && + std::abs(u2) <= halfSize[2] + margin; +} + +std::array, 2> SimpleCuboid::boundingBox(double margin) const { + const double hx = halfSize[0] + margin; + const double hy = halfSize[1] + margin; + const double hz = halfSize[2] + margin; + const double c = std::abs(cosSinRotationZ[0]); + const double s = std::abs(cosSinRotationZ[1]); + const double ex = hx * c + hy * s; + const double ey = hx * s + hy * c; + return {{{center[0] - ex, center[1] - ey, center[2] - hz}, + {center[0] + ex, center[1] + ey, center[2] + hz}}}; +} + +VelocityAwareRefinementSettings::VelocityAwareRefinementSettings(double elementsPerWaveLength, + std::string easiFileName) + : elementsPerWaveLength(elementsPerWaveLength), easiFileName(std::move(easiFileName)) {} + +void VelocityAwareRefinementSettings::addRefinementRegion(SimpleCuboid cuboid, + double targetedFrequency, + int bypassFindRegionAndUseGroup) { + refinementRegions.emplace_back(cuboid, targetedFrequency, bypassFindRegionAndUseGroup); +} + +bool VelocityAwareRefinementSettings::isVelocityAwareRefinementOn() const { + return !refinementRegions.empty(); +} + +const std::string& VelocityAwareRefinementSettings::getEasiFileName() const { return easiFileName; } + +double VelocityAwareRefinementSettings::getElementsPerWaveLength() const { + return elementsPerWaveLength; +} + +const std::vector& +VelocityAwareRefinementSettings::getRefinementRegions() const { + return refinementRegions; +} + +VelocityAwareRefinementSettings readVelocityAwareSettings(const tinyxml2::XMLDocument& doc) { + VelocityAwareRefinementSettings settings; + int numChilds = 0; + const auto name = "VelocityAwareMeshing"; + for (auto velocityAwareMeshingElement = doc.FirstChildElement(name); velocityAwareMeshingElement; + velocityAwareMeshingElement = velocityAwareMeshingElement->NextSiblingElement(name)) { + const auto easiFileName = velocityAwareMeshingElement->Attribute("easiFile"); + const auto elementsPerWaveLength = + std::stof(velocityAwareMeshingElement->Attribute("elementsPerWaveLength")); + settings = VelocityAwareRefinementSettings(elementsPerWaveLength, easiFileName); + constexpr auto cuboidName = "VelocityRefinementCuboid"; + logInfo() << "Activating velocity aware meshing, using" << elementsPerWaveLength + << "elements per wavelength and easi file" << easiFileName; + for (auto child = velocityAwareMeshingElement->FirstChildElement(cuboidName); child; + child = child->NextSiblingElement(cuboidName)) { + + const char* attr = child->Attribute("rotationZAnticlockwiseFromX"); + double rotationZAnticlockwiseFromX = 0.0; + if (attr != nullptr) { + rotationZAnticlockwiseFromX = std::stof(attr); + } + attr = child->Attribute("bypassFindRegionAndUseGroup"); + int bypassFindRegionAndUseGroup = 0; + if (attr != nullptr) { + bypassFindRegionAndUseGroup = std::stoi(attr); + } + auto cuboid = SimpleCuboid{{ + std::stof(child->Attribute("centerX")), + std::stof(child->Attribute("centerY")), + std::stof(child->Attribute("centerZ")), + }, + { + std::stof(child->Attribute("halfSizeX")), + std::stof(child->Attribute("halfSizeY")), + std::stof(child->Attribute("halfSizeZ")), + }, + {std::cos(rotationZAnticlockwiseFromX * ToRadians), + std::sin(rotationZAnticlockwiseFromX * ToRadians)}, + rotationZAnticlockwiseFromX}; + const auto targetedFrequency = std::stof(child->Attribute("frequency")); + + settings.addRefinementRegion(cuboid, targetedFrequency, bypassFindRegionAndUseGroup); + logInfo() << "Adding velocity aware refinement region targeting" << targetedFrequency + << "Hz, centered at x =" << cuboid.center[0] << "y=" << cuboid.center[1] + << "z=" << cuboid.center[2] << "with half sizes" + << "x =" << cuboid.halfSize[0] << "y =" << cuboid.halfSize[1] + << "z =" << cuboid.halfSize[2]; + if (std::abs(cuboid.rotationZ) > 0.0) { + logInfo() << "rotated around z axis by " << cuboid.rotationZ + << "degree(s) counterclockwise from x axis."; + } + if (bypassFindRegionAndUseGroup) { + logInfo() << "bypass findRegion and use group =" << bypassFindRegionAndUseGroup; + } + } + if (!settings.isVelocityAwareRefinementOn()) { + logWarning() << "Activated velocity aware meshing but did not specify any refinement region!"; + } + ++numChilds; + } + if (numChilds > 1) { + logError() << "Multiple definitions of velocityAwareMeshing"; + } + return settings; +} diff --git a/src/sizing/VelocityAwareSettings.h b/src/sizing/VelocityAwareSettings.h new file mode 100644 index 0000000..7a9b798 --- /dev/null +++ b/src/sizing/VelocityAwareSettings.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2020 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#ifndef PUMGEN_SRC_SIZING_VELOCITYAWARESETTINGS_H_ +#define PUMGEN_SRC_SIZING_VELOCITYAWARESETTINGS_H_ + +#include +#include +#include + +namespace tinyxml2 { +class XMLDocument; +} // namespace tinyxml2 + +/** + * A cuboid, rotated around the z axis. + */ +struct SimpleCuboid { + std::array center; + std::array halfSize; + std::array cosSinRotationZ; + double rotationZ; + + /** + * Whether the point lies in the cuboid, enlarged by margin in each direction. + */ + [[nodiscard]] bool contains(const std::array& point, double margin = 0) const; + + /** + * The axis-aligned bounding box (minimum and maximum corner) of the enlarged cuboid. + */ + [[nodiscard]] std::array, 2> boundingBox(double margin = 0) const; +}; + +struct VelocityRefinementCube { + VelocityRefinementCube(SimpleCuboid cuboid, double targetedFrequency, + int bypassFindRegionAndUseGroup) + : cuboid(cuboid), targetedFrequency(targetedFrequency), + bypassFindRegionAndUseGroup(bypassFindRegionAndUseGroup) {} + + SimpleCuboid cuboid; + double targetedFrequency; + int bypassFindRegionAndUseGroup; +}; + +class VelocityAwareRefinementSettings { + public: + VelocityAwareRefinementSettings() = default; + VelocityAwareRefinementSettings(double elementsPerWaveLength, std::string easiFileName); + + void addRefinementRegion(SimpleCuboid cuboid, double targetedFrequency, + int bypassFindRegionAndUseGroup); + + [[nodiscard]] bool isVelocityAwareRefinementOn() const; + + [[nodiscard]] const std::string& getEasiFileName() const; + + [[nodiscard]] double getElementsPerWaveLength() const; + + [[nodiscard]] const std::vector& getRefinementRegions() const; + + private: + double elementsPerWaveLength{}; + std::string easiFileName; + std::vector refinementRegions{}; +}; + +/** + * Reads the VelocityAwareMeshing element of a mesh attributes document; the settings are empty + * (isVelocityAwareRefinementOn() is false) if the document has none. + */ +VelocityAwareRefinementSettings readVelocityAwareSettings(const tinyxml2::XMLDocument& doc); + +#endif // PUMGEN_SRC_SIZING_VELOCITYAWARESETTINGS_H_ From b18ea60baaadaed7cd90912b23be74ee7434b00d Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 20:06:09 +0000 Subject: [PATCH 16/19] Separate the gathering of cell vertices from the insphere computation CellVertices fetches the coordinates of the vertices of the local cells which are owned by other ranks once and provides the vertices per cell, so that further per-cell computations can share them with the insphere calculation. AI-generated. Model: Claude Opus 5.5 --- src/helper/InsphereCalculator.cpp | 70 ++++++++++++++++--------------- src/helper/InsphereCalculator.h | 37 +++++++++++++--- src/pumgen.cpp | 5 ++- 3 files changed, 70 insertions(+), 42 deletions(-) diff --git a/src/helper/InsphereCalculator.cpp b/src/helper/InsphereCalculator.cpp index ddc2a0c..cad62b7 100644 --- a/src/helper/InsphereCalculator.cpp +++ b/src/helper/InsphereCalculator.cpp @@ -12,15 +12,10 @@ #include "MPIConvenience.h" -std::vector calculateInsphere(const std::vector& connectivity, - const std::vector& geometry, std::size_t cellSize, - MPI_Comm comm) { - constexpr std::size_t VerticesPerCell = 4; - const std::size_t numCells = connectivity.size() / cellSize; - +CellVertices::CellVertices(const std::vector& connectivity, + const std::vector& geometry, std::size_t cellSize, MPI_Comm comm) + : connectivity(connectivity), geometry(geometry), cellSize(cellSize) { int commsize; - int commrank; - MPI_Comm_size(comm, &commsize); MPI_Comm_rank(comm, &commrank); @@ -34,7 +29,7 @@ std::vector calculateInsphere(const std::vector& connectivi std::size_t localVertices = geometry.size() / 3; - std::vector vertexDist(commsize + 1); + vertexDist.resize(commsize + 1); MPI_Allgather(&localVertices, 1, tndm::mpi_type_t(), vertexDist.data() + 1, 1, tndm::mpi_type_t(), comm); @@ -43,7 +38,7 @@ std::vector calculateInsphere(const std::vector& connectivi vertexDist[i + 1] += vertexDist[i]; } - std::vector> outidxmap(commsize); + outidxmap.resize(commsize); for (std::size_t node = 0; node < connectivity.size(); ++node) { if (node % cellSize >= VerticesPerCell) { @@ -65,7 +60,7 @@ std::vector calculateInsphere(const std::vector& connectivi MPI_Alltoall(outrequests.data(), 1, tndm::mpi_type_t(), inrequests.data(), 1, tndm::mpi_type_t(), comm); - std::vector outdisp(commsize + 1); + outdisp.resize(commsize + 1); std::vector indisp(commsize + 1); for (std::size_t i = 1; i < commsize + 1; ++i) { outdisp[i] = outdisp[i - 1] + outrequests[i - 1]; @@ -92,7 +87,7 @@ std::vector calculateInsphere(const std::vector& connectivi tndm::mpi_type_t(), comm); } - std::vector outvertices(3 * outdisp[commsize]); + outvertices.resize(3 * outdisp[commsize]); { std::vector invertices(3 * indisp[commsize]); @@ -109,27 +104,36 @@ std::vector calculateInsphere(const std::vector& connectivi outvertices.data(), outrequests.data(), outdisp.data(), vertexType, comm); } - std::vector counter(commsize); - std::vector inspheres(numCells); - - for (std::size_t i = 0; i < numCells; ++i) { - std::array, VerticesPerCell> vertices; - for (std::size_t j = 0; j < VerticesPerCell; ++j) { - auto vertex = connectivity[i * cellSize + j]; - auto itPosition = std::upper_bound(vertexDist.begin(), vertexDist.end(), vertex); - auto position = std::distance(vertexDist.begin(), itPosition) - 1; - auto localVertex = vertex - vertexDist[position]; - if (position == commrank) { - vertices[j][0] = geometry[localVertex * 3 + 0]; - vertices[j][1] = geometry[localVertex * 3 + 1]; - vertices[j][2] = geometry[localVertex * 3 + 2]; - } else { - auto transferidx = outidxmap[position][localVertex] + outdisp[position]; - vertices[j][0] = outvertices[transferidx * 3 + 0]; - vertices[j][1] = outvertices[transferidx * 3 + 1]; - vertices[j][2] = outvertices[transferidx * 3 + 2]; - } + MPI_Type_free(&vertexType); +} + +std::array, CellVertices::VerticesPerCell> +CellVertices::operator()(std::size_t cell) const { + std::array, VerticesPerCell> vertices; + for (std::size_t j = 0; j < VerticesPerCell; ++j) { + auto vertex = connectivity[cell * cellSize + j]; + auto itPosition = std::upper_bound(vertexDist.begin(), vertexDist.end(), vertex); + auto position = std::distance(vertexDist.begin(), itPosition) - 1; + auto localVertex = vertex - vertexDist[position]; + if (position == commrank) { + vertices[j][0] = geometry[localVertex * 3 + 0]; + vertices[j][1] = geometry[localVertex * 3 + 1]; + vertices[j][2] = geometry[localVertex * 3 + 2]; + } else { + auto transferidx = outidxmap[position].at(localVertex) + outdisp[position]; + vertices[j][0] = outvertices[transferidx * 3 + 0]; + vertices[j][1] = outvertices[transferidx * 3 + 1]; + vertices[j][2] = outvertices[transferidx * 3 + 2]; } + } + return vertices; +} + +std::vector calculateInsphere(const CellVertices& cells) { + std::vector inspheres(cells.numCells()); + + for (std::size_t i = 0; i < cells.numCells(); ++i) { + const auto vertices = cells(i); double a11 = vertices[1][0] - vertices[0][0]; double a12 = vertices[1][1] - vertices[0][1]; double a13 = vertices[1][2] - vertices[0][2]; @@ -170,7 +174,5 @@ std::vector calculateInsphere(const std::vector& connectivi inspheres[i] = gram / faces; } - MPI_Type_free(&vertexType); - return inspheres; } diff --git a/src/helper/InsphereCalculator.h b/src/helper/InsphereCalculator.h index 806e726..94feb2d 100644 --- a/src/helper/InsphereCalculator.h +++ b/src/helper/InsphereCalculator.h @@ -4,18 +4,43 @@ #ifndef PUMGEN_SRC_HELPER_INSPHERECALCULATOR_H_ #define PUMGEN_SRC_HELPER_INSPHERECALCULATOR_H_ +#include #include #include +#include #include -// assumes a contiguous distribution of all vertices over all processes +/** + * The vertex coordinates of the local cells; the coordinates of vertices owned by other ranks are + * fetched once. Assumes a contiguous distribution of all vertices over all processes. Each cell + * has cellSize nodes, the first four of which are its vertices. + */ +class CellVertices { + public: + static constexpr std::size_t VerticesPerCell = 4; + + CellVertices(const std::vector& connectivity, const std::vector& geometry, + std::size_t cellSize, MPI_Comm comm); + + [[nodiscard]] std::size_t numCells() const { return connectivity.size() / cellSize; } + + [[nodiscard]] std::array, VerticesPerCell> + operator()(std::size_t cell) const; + + private: + const std::vector& connectivity; + const std::vector& geometry; + std::size_t cellSize; + int commrank = 0; + std::vector vertexDist; + std::vector> outidxmap; + std::vector outdisp; + std::vector outvertices; +}; /** - * The insphere radius of each tetrahedron. Each cell has cellSize nodes, the first four of which - * are its vertices. + * The insphere radius of each tetrahedron. */ -std::vector calculateInsphere(const std::vector& connectivity, - const std::vector& geometry, std::size_t cellSize, - MPI_Comm comm); +std::vector calculateInsphere(const CellVertices& cells); #endif // PUMGEN_SRC_HELPER_INSPHERECALCULATOR_H_ diff --git a/src/pumgen.cpp b/src/pumgen.cpp index 7227859..cd67a80 100644 --- a/src/pumgen.cpp +++ b/src/pumgen.cpp @@ -455,8 +455,9 @@ int main(int argc, char* argv[]) { logInfo() << "Total vertex count:" << globalSize[1]; logInfo() << "Vertex identification:" << meshInput->hasIdentify(); - auto inspheres = calculateInsphere(meshInput->connectivity(), meshInput->geometry(), - meshInput->cellSize(), MPI_COMM_WORLD); + const CellVertices cellVertices(meshInput->connectivity(), meshInput->geometry(), + meshInput->cellSize(), MPI_COMM_WORLD); + auto inspheres = calculateInsphere(cellVertices); double min = inspheres.empty() ? std::numeric_limits::infinity() : *std::min_element(inspheres.begin(), inspheres.end()); MPI_Reduce((rank == 0 ? MPI_IN_PLACE : &min), &min, 1, MPI_DOUBLE, MPI_MIN, 0, MPI_COMM_WORLD); From 7e32225b6aa68f12f1d9bfb618eb57e50611edea Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 20:07:45 +0000 Subject: [PATCH 17/19] Add pumgen-sizefield for velocity-aware meshing with gmsh pumgen-sizefield reads the VelocityAwareMeshing element of a mesh attributes file and writes, for each refinement cuboid, a binary grid for gmsh's Structured field with the mesh size of the cuboid's group (given by bypassFindRegionAndUseGroup, which is required here). The easi model is evaluated for all grid nodes at once, distributed over the ranks in x slabs; each rank writes its slab with MPI-IO. Each node takes the minimum of its 3x3x3 neighbourhood, so that gmsh's trilinear interpolation never exceeds the smallest size sampled at the corners of a grid cell; the grid covers the cuboid with a margin of two spacings, so that all grid cells which intersect the cuboid have defined corners. A .geo file restricts each field to the physical volume of its group and uses their minimum as background field; it refers to the grids relative to its own directory. On the layered test model (20 x 20 x 10 km, 2 km sediment), gmsh's HXT meshes with these fields have the same edge length distribution as meshes from the exact per-point size callback (816945 vs. 815206 cells). AI-generated. Model: Claude Opus 5.5 --- CMakeLists.txt | 7 + REUSE.toml | 3 +- src/sizefield.cpp | 189 +++++++++++++++++++++++ src/sizing/SizeField.cpp | 73 +++++++++ src/sizing/SizeField.h | 53 +++++++ tests/CMakeLists.txt | 20 +++ tests/RunSizefieldTest.cmake | 36 +++++ tests/fixtures/velocity-material.yaml | 21 +++ tests/fixtures/velocity-sizefield-0.bin | Bin 0 -> 8172 bytes tests/fixtures/velocity-sizefield-1.bin | Bin 0 -> 14396 bytes tests/fixtures/velocity-sizefield.xml.in | 16 ++ 11 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 src/sizefield.cpp create mode 100644 src/sizing/SizeField.cpp create mode 100644 src/sizing/SizeField.h create mode 100644 tests/RunSizefieldTest.cmake create mode 100644 tests/fixtures/velocity-material.yaml create mode 100644 tests/fixtures/velocity-sizefield-0.bin create mode 100644 tests/fixtures/velocity-sizefield-1.bin create mode 100644 tests/fixtures/velocity-sizefield.xml.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 4333aa5..930b740 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,6 +140,7 @@ if (EASI) add_library(pumgen_sizing STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/sizing/VelocityAwareSettings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/sizing/VelocityAwareMeshSize.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/sizing/SizeField.cpp ) target_include_directories(pumgen_sizing PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src @@ -149,6 +150,12 @@ if (EASI) target_link_libraries(pumgen_sizing PUBLIC easi::easi tinyxml2) target_link_libraries(pumgen PUBLIC pumgen_sizing) target_compile_definitions(pumgen PUBLIC USE_EASI) + + add_executable(pumgen-sizefield ${CMAKE_CURRENT_SOURCE_DIR}/src/sizefield.cpp) + target_compile_definitions(pumgen-sizefield PRIVATE LOG_LEVEL=${LOG_LEVEL}) + target_link_libraries(pumgen-sizefield PRIVATE pumgen_sizing MPI::MPI_CXX) + set_target_properties(pumgen-sizefield PROPERTIES INSTALL_RPATH "${INSTALL_RPATH}") + install(TARGETS pumgen-sizefield RUNTIME DESTINATION bin) endif() if(SIMMETRIX) set(INSTALL_RPATH "${SIM_PS_KRNL_LIB_DIR}:${INSTALL_RPATH}") diff --git a/REUSE.toml b/REUSE.toml index 4465d3c..c2f58e0 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -5,7 +5,8 @@ version = 1 [[annotations]] -path = ["tests/fixtures/*.msh", "tests/fixtures/*.neu", "tests/fixtures/*.h5"] +path = ["tests/fixtures/*.msh", "tests/fixtures/*.neu", "tests/fixtures/*.h5", + "tests/fixtures/*.bin"] precedence = "override" SPDX-FileCopyrightText = "2026 SeisSol Group" SPDX-License-Identifier = "BSD-3-Clause" diff --git a/src/sizefield.cpp b/src/sizefield.cpp new file mode 100644 index 0000000..dcbea2d --- /dev/null +++ b/src/sizefield.cpp @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause + +// Writes gmsh Structured fields for velocity-aware meshing (one per refinement cuboid) and a .geo +// file which combines them into the background mesh size. + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "helper/Distributor.h" +#include "sizing/SizeField.h" +#include "sizing/VelocityAwareMeshSize.h" +#include "sizing/VelocityAwareSettings.h" +#include "tinyxml2/tinyxml2.h" +#include "utils/args.h" +#include "utils/logger.h" + +namespace { + +/** + * Writes the binary format of gmsh's Structured field: origin and spacing (3 doubles each), node + * counts (3 4-byte ints), then the values. Each rank writes the nodes with x index from first. + */ +void writeGrid(const std::string& fileName, const StructuredGrid& grid, + const std::vector& values, std::size_t first, MPI_Comm comm) { + MPI_File file; + if (MPI_File_open(comm, fileName.c_str(), MPI_MODE_CREATE | MPI_MODE_WRONLY, MPI_INFO_NULL, + &file) != MPI_SUCCESS) { + logError() << "Could not open" << fileName; + } + MPI_File_set_size(file, 0); + + constexpr std::size_t HeaderSize = 6 * sizeof(double) + 3 * sizeof(std::int32_t); + int rank = 0; + MPI_Comm_rank(comm, &rank); + if (rank == 0) { + char header[HeaderSize]; + const double spacing[3] = {grid.spacing, grid.spacing, grid.spacing}; + std::int32_t count[3]; + for (int d = 0; d < 3; ++d) { + count[d] = static_cast(grid.count[d]); + } + std::memcpy(header, grid.origin.data(), 3 * sizeof(double)); + std::memcpy(header + 3 * sizeof(double), spacing, 3 * sizeof(double)); + std::memcpy(header + 6 * sizeof(double), count, 3 * sizeof(std::int32_t)); + MPI_File_write_at(file, 0, header, HeaderSize, MPI_BYTE, MPI_STATUS_IGNORE); + } + + // in pieces, since MPI counts are ints + constexpr std::size_t Piece = std::size_t{1} << 26; + const MPI_Offset offset = HeaderSize + first * grid.count[1] * grid.count[2] * sizeof(double); + for (std::size_t done = 0; done < values.size(); done += Piece) { + const auto count = static_cast(std::min(Piece, values.size() - done)); + MPI_File_write_at(file, offset + static_cast(done * sizeof(double)), + values.data() + done, count, MPI_DOUBLE, MPI_STATUS_IGNORE); + } + MPI_File_close(&file); +} + +std::string baseName(const std::string& path) { + const auto slash = path.find_last_of('/'); + return slash == std::string::npos ? path : path.substr(slash + 1); +} + +void writeGeo(const std::string& fileName, const std::string& settingsFile, + const std::vector& gridFiles, const std::vector& groups, + int firstField) { + std::ofstream geo(fileName); + geo << "// Velocity-aware mesh size written by pumgen-sizefield from " << settingsFile << ":\n" + << "// one Structured field per refinement cuboid, restricted to the physical volume of\n" + << "// the cuboid's group, and their minimum as background field. The grid files are\n" + << "// expected next to this file. Recommended options:\n" + << "// Mesh.MeshSizeExtendFromBoundary = 0; Mesh.MeshSizeFromPoints = 0;\n" + << "// Mesh.MeshSizeFromCurvature = 0;\n"; + std::string restricted; + for (std::size_t c = 0; c < gridFiles.size(); ++c) { + const int grid = firstField + 2 * static_cast(c); + const int restrict = grid + 1; + geo << "Field[" << grid << "] = Structured;\n" + << "Field[" << grid << "].FileName = StrCat(CurrentDirectory, \"" << baseName(gridFiles[c]) + << "\");\n" + << "Field[" << grid << "].TextFormat = 0;\n" + << "Field[" << grid << "].SetOutsideValue = 1;\n" + << "Field[" << grid << "].OutsideValue = " << UnconstrainedSize << ";\n" + << "Field[" << restrict << "] = Restrict;\n" + << "Field[" << restrict << "].InField = " << grid << ";\n" + << "Field[" << restrict << "].VolumesList = {Physical Volume{" << groups[c] << "}};\n" + << "Field[" << restrict << "].IncludeBoundary = 1;\n"; + restricted += (c == 0 ? "" : ", ") + std::to_string(restrict); + } + const int minimum = firstField + 2 * static_cast(gridFiles.size()); + geo << "Field[" << minimum << "] = Min;\n" + << "Field[" << minimum << "].FieldsList = {" << restricted << "};\n" + << "Background Field = " << minimum << ";\n"; +} + +} // namespace + +int main(int argc, char* argv[]) { + MPI_Init(&argc, &argv); + int rank = 0; + int size = 1; + MPI_Comm_rank(MPI_COMM_WORLD, &rank); + MPI_Comm_size(MPI_COMM_WORLD, &size); + utils::Logger::setRank(rank); + + utils::Args args("Writes gmsh Structured fields for velocity-aware meshing, one per refinement " + "cuboid, and a .geo file which restricts each field to the physical volume of " + "the cuboid's group (bypassFindRegionAndUseGroup) and uses their minimum as " + "background field."); + args.addOption("spacing", 0, "Grid spacing; about the smallest mesh size", utils::Args::Required, + true); + args.addOption("first-field", 0, "Number of the first gmsh field (default: 1000)", + utils::Args::Required, false); + args.addAdditionalOption("settings", "Mesh attributes (XML) with a VelocityAwareMeshing element"); + args.addAdditionalOption("output", "Prefix of the output files"); + if (args.parse(argc, argv, rank == 0) != utils::Args::Success) { + MPI_Finalize(); + return 1; + } + const auto settingsFile = args.getAdditionalArgument("settings"); + const auto prefix = args.getAdditionalArgument("output"); + const auto spacing = args.getArgument("spacing"); + const auto firstField = args.getArgument("first-field", 1000); + if (!(spacing > 0)) { + logError() << "The grid spacing has to be positive"; + } + + tinyxml2::XMLDocument doc; + if (doc.LoadFile(settingsFile.c_str()) != tinyxml2::XML_SUCCESS) { + logError() << "Could not read" << settingsFile; + } + const auto settings = readVelocityAwareSettings(doc); + const auto& regions = settings.getRefinementRegions(); + if (regions.empty()) { + logError() << "No refinement cuboid in" << settingsFile; + } + for (std::size_t c = 0; c < regions.size(); ++c) { + if (regions[c].bypassFindRegionAndUseGroup <= 0) { + logError() << "Refinement cuboid" << c + << "has no group; pumgen-sizefield needs bypassFindRegionAndUseGroup"; + } + } + + const VelocityAwareMeshSize meshSize(settings); + std::vector gridFiles; + std::vector groups; + for (std::size_t c = 0; c < regions.size(); ++c) { + const auto grid = gridForCuboid(regions[c].cuboid, spacing); + for (const auto count : grid.count) { + if (count > static_cast(std::numeric_limits::max())) { + logError() << "Too many grid nodes for refinement cuboid" << c; + } + } + logInfo() << "Refinement cuboid" << c << "(group" << regions[c].bypassFindRegionAndUseGroup + << ", " << regions[c].targetedFrequency << "Hz): grid of" << grid.count[0] << "x" + << grid.count[1] << "x" << grid.count[2] << "nodes"; + + const std::size_t first = getChunksum(grid.count[0], rank, size); + const std::size_t last = first + getChunksize(grid.count[0], rank, size); + const auto values = cuboidMeshSizes(meshSize, regions[c], grid, first, last); + + double smallest = + values.empty() ? UnconstrainedSize : *std::min_element(values.begin(), values.end()); + MPI_Allreduce(MPI_IN_PLACE, &smallest, 1, MPI_DOUBLE, MPI_MIN, MPI_COMM_WORLD); + logInfo() << "Refinement cuboid" << c << ": smallest mesh size" << smallest; + + const std::string gridFile = prefix + "-" + std::to_string(c) + ".bin"; + writeGrid(gridFile, grid, values, first, MPI_COMM_WORLD); + gridFiles.push_back(gridFile); + groups.push_back(regions[c].bypassFindRegionAndUseGroup); + } + + if (rank == 0) { + writeGeo(prefix + ".geo", settingsFile, gridFiles, groups, firstField); + logInfo() << "Wrote" << (prefix + ".geo").c_str(); + } + + MPI_Finalize(); + return 0; +} diff --git a/src/sizing/SizeField.cpp b/src/sizing/SizeField.cpp new file mode 100644 index 0000000..93d3600 --- /dev/null +++ b/src/sizing/SizeField.cpp @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#include "SizeField.h" + +#include +#include + +StructuredGrid gridForCuboid(const SimpleCuboid& cuboid, double spacing) { + const auto box = cuboid.boundingBox(2 * spacing); + StructuredGrid grid{box[0], spacing, {}}; + for (int d = 0; d < 3; ++d) { + grid.count[d] = static_cast(std::ceil((box[1][d] - box[0][d]) / spacing)) + 1; + } + return grid; +} + +std::vector cuboidMeshSizes(const VelocityAwareMeshSize& meshSize, + const VelocityRefinementCube& region, + const StructuredGrid& grid, std::size_t first, + std::size_t last) { + const std::size_t ny = grid.count[1]; + const std::size_t nz = grid.count[2]; + const std::size_t layer = ny * nz; + // one layer more on each side for the neighbourhood minimum + const std::size_t haloFirst = first > 0 ? first - 1 : first; + const std::size_t haloLast = std::min(last + 1, grid.count[0]); + const std::size_t numLayers = haloLast - haloFirst; + + std::vector sizes(numLayers * layer, UnconstrainedSize); + std::vector> points; + std::vector indices; + for (std::size_t i = haloFirst; i < haloLast; ++i) { + for (std::size_t j = 0; j < ny; ++j) { + for (std::size_t k = 0; k < nz; ++k) { + const auto point = grid.node(i, j, k); + if (region.cuboid.contains(point, 2 * grid.spacing)) { + points.push_back(point); + indices.push_back(((i - haloFirst) * ny + j) * nz + k); + } + } + } + } + const std::vector groups(points.size(), region.bypassFindRegionAndUseGroup); + const std::vector frequencies(points.size(), region.targetedFrequency); + const auto values = meshSize.meshSizes(points, groups, frequencies); + for (std::size_t p = 0; p < points.size(); ++p) { + sizes[indices[p]] = std::min(values[p], UnconstrainedSize); + } + + // the minimum over the 3x3x3 neighbourhood, one direction after the other + std::vector previous; + const auto minimumAlong = [&](std::size_t count, std::size_t stride) { + previous = sizes; + for (std::size_t index = 0; index < sizes.size(); ++index) { + const std::size_t position = (index / stride) % count; + if (position > 0) { + sizes[index] = std::min(sizes[index], previous[index - stride]); + } + if (position + 1 < count) { + sizes[index] = std::min(sizes[index], previous[index + stride]); + } + } + }; + minimumAlong(nz, 1); + minimumAlong(ny, nz); + minimumAlong(numLayers, layer); + + // drop the halo layers + const std::size_t offset = (first - haloFirst) * layer; + return {sizes.begin() + static_cast(offset), + sizes.begin() + static_cast(offset + (last - first) * layer)}; +} diff --git a/src/sizing/SizeField.h b/src/sizing/SizeField.h new file mode 100644 index 0000000..c5b0758 --- /dev/null +++ b/src/sizing/SizeField.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#ifndef PUMGEN_SRC_SIZING_SIZEFIELD_H_ +#define PUMGEN_SRC_SIZING_SIZEFIELD_H_ + +#include +#include +#include + +#include "VelocityAwareMeshSize.h" + +/** + * A regular grid for gmsh's Structured field: the value of node (i, j, k) is stored at + * (i * count[1] + j) * count[2] + k. + */ +struct StructuredGrid { + std::array origin; + double spacing; + std::array count; + + [[nodiscard]] std::array node(std::size_t i, std::size_t j, std::size_t k) const { + return {origin[0] + static_cast(i) * spacing, + origin[1] + static_cast(j) * spacing, + origin[2] + static_cast(k) * spacing}; + } +}; + +/** + * The value of grid nodes which do not constrain the mesh size. + */ +constexpr double UnconstrainedSize = 1e22; + +/** + * The grid covering a cuboid with a margin of two grid spacings, so that all grid cells which + * intersect the cuboid have their corners within the margin. + */ +StructuredGrid gridForCuboid(const SimpleCuboid& cuboid, double spacing); + +/** + * The mesh sizes of the grid nodes with x index in [first, last) for one refinement cuboid. + * + * The nodes within the margin of the cuboid get the mesh size of the material of the given group; + * the others get UnconstrainedSize. Each node then takes the minimum of its 3x3x3 neighbourhood, + * so that the trilinear interpolation in a grid cell never exceeds the smallest size sampled at the + * cell's corners. + */ +std::vector cuboidMeshSizes(const VelocityAwareMeshSize& meshSize, + const VelocityRefinementCube& region, + const StructuredGrid& grid, std::size_t first, + std::size_t last); + +#endif // PUMGEN_SRC_SIZING_SIZEFIELD_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 117e837..ebf9e56 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -189,3 +189,23 @@ pumgen_add_test(msh4.coarse-binary-truncated RANKS 1 SOURCE msh4 # a face of the surface mesh given twice pumgen_add_test(msh2.tiny-duplicateface RANKS 1 SOURCE msh2 INPUT tiny-duplicateface-v22.msh EXPECT_ERROR "exists multiple times in the surface mesh") + +# velocity-aware sizing, with a material of shear wave speed 1 in group 1 and 2 in group 2 +if(EASI) + configure_file(fixtures/velocity-sizefield.xml.in + ${CMAKE_CURRENT_BINARY_DIR}/velocity-sizefield.xml @ONLY) + + # the expected grids are verified independently; they must not depend on the number of ranks + foreach(ranks 1 3) + set(prefix ${PUMGEN_TEST_OUTPUT}/sizefield-np${ranks}) + set(command ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} ${ranks} ${MPIEXEC_PREFLAGS} + $ ${MPIEXEC_POSTFLAGS} --spacing 0.25 + ${CMAKE_CURRENT_BINARY_DIR}/velocity-sizefield.xml ${prefix}) + string(REPLACE ";" "|" command "${command}") + add_test(NAME sizefield.np${ranks} + COMMAND ${CMAKE_COMMAND} -DCOMMAND=${command} -DPREFIX=${prefix} + -DEXPECTED=${PUMGEN_TEST_FIXTURES}/velocity-sizefield -DGRIDS=2 -P + ${CMAKE_CURRENT_SOURCE_DIR}/RunSizefieldTest.cmake) + set_tests_properties(sizefield.np${ranks} PROPERTIES PROCESSORS ${ranks} TIMEOUT 120) + endforeach() +endif() diff --git a/tests/RunSizefieldTest.cmake b/tests/RunSizefieldTest.cmake new file mode 100644 index 0000000..8be34df --- /dev/null +++ b/tests/RunSizefieldTest.cmake @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 SeisSol Group +# +# SPDX-License-Identifier: BSD-3-Clause + +# Runs pumgen-sizefield and compares the written grids with the expected ones. +# +# Variables: +# COMMAND pumgen-sizefield command line, with '|' as the argument separator +# PREFIX output prefix of the command +# EXPECTED prefix of the expected grids +# GRIDS number of grids + +string(REPLACE "|" ";" command "${COMMAND}") +file(REMOVE "${PREFIX}.geo") +execute_process( + COMMAND ${command} + RESULT_VARIABLE result + OUTPUT_VARIABLE log + ERROR_VARIABLE log + TIMEOUT 100) +message("${log}") +if(NOT result EQUAL 0) + message(FATAL_ERROR "pumgen-sizefield failed: ${result}") +endif() + +math(EXPR last "${GRIDS} - 1") +foreach(grid RANGE ${last}) + execute_process(COMMAND ${CMAKE_COMMAND} -E compare_files "${PREFIX}-${grid}.bin" + "${EXPECTED}-${grid}.bin" RESULT_VARIABLE differs) + if(NOT differs EQUAL 0) + message(FATAL_ERROR "${PREFIX}-${grid}.bin differs from ${EXPECTED}-${grid}.bin") + endif() +endforeach() +if(NOT EXISTS "${PREFIX}.geo") + message(FATAL_ERROR "${PREFIX}.geo was not written") +endif() diff --git a/tests/fixtures/velocity-material.yaml b/tests/fixtures/velocity-material.yaml new file mode 100644 index 0000000..d8214c1 --- /dev/null +++ b/tests/fixtures/velocity-material.yaml @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2026 SeisSol Group +# +# SPDX-License-Identifier: BSD-3-Clause + +# shear wave speed 1 in group 1 and 2 in group 2 +!Any +components: + - !GroupFilter + groups: [1] + components: !ConstantMap + map: + rho: 1.0 + mu: 1.0 + lambda: 2.0 + - !GroupFilter + groups: [2] + components: !ConstantMap + map: + rho: 1.0 + mu: 4.0 + lambda: 8.0 diff --git a/tests/fixtures/velocity-sizefield-0.bin b/tests/fixtures/velocity-sizefield-0.bin new file mode 100644 index 0000000000000000000000000000000000000000..8b7bbbf3d65ce232a1271a066815b411ed131ba2 GIT binary patch literal 8172 zcmeIyyA8k~3g;RG0Pl}Hxa1ixSWPWLt0Y#^D; z?)z`3qT1}&m&2<*EmqI%`B7KD=iTkMS^N3FPtzBZw8m*2@5|A(+0EJWIJ9h=j&XTA z-sE{R6mF&aAN~GA@z8aT==pR0F9F|s+vROv`r@D0Klp3>`!Ywl=OBMAfB1R%<$B+j zI{fka1Ai}n_Z*VHH-Gqh`Mc*&?T1i&{IvYk z`BCf1Ps4X{Y%fE`)j{nZhxcq$3M-#==`GlTl)E<`{zD? V>Hctk?f)Zgf7SlvujQ}u`v9eZP$&QZ literal 0 HcmV?d00001 diff --git a/tests/fixtures/velocity-sizefield.xml.in b/tests/fixtures/velocity-sizefield.xml.in new file mode 100644 index 0000000..4ce36ee --- /dev/null +++ b/tests/fixtures/velocity-sizefield.xml.in @@ -0,0 +1,16 @@ + + + + + + + From 8adbbc5699a01011427e371d44eb50d006781081 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 20:10:10 +0000 Subject: [PATCH 18/19] Add a velocity check of the cell sizes With --velocity-check , pumgen compares the edge lengths of the cells in the refinement regions of the VelocityAwareMeshing settings with the mesh size at their barycentre, for the material of the cell's group. It logs the distribution of the mean and of the longest edge length over the mesh size (quantiles from a histogram with 0.23 % wide bins, the maximum, and the number of cells above 1). This works for meshes from any source, so that meshes of different generators can be compared by the same measure. For the layered test model, meshed by gmsh with a size callback, the check reproduces the independently computed quantiles (mean edge: p50 1.37, p90 1.57, p99 1.71, max 2.14). AI-generated. Model: Claude Opus 5.5 --- CMakeLists.txt | 1 + src/pumgen.cpp | 30 +++++ src/sizing/VelocityCheck.cpp | 143 ++++++++++++++++++++++++ src/sizing/VelocityCheck.h | 21 ++++ tests/CMakeLists.txt | 14 +++ tests/fixtures/velocity-settings.xml.in | 16 +++ 6 files changed, 225 insertions(+) create mode 100644 src/sizing/VelocityCheck.cpp create mode 100644 src/sizing/VelocityCheck.h create mode 100644 tests/fixtures/velocity-settings.xml.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 930b740..269e851 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,7 @@ if (EASI) target_compile_definitions(pumgen_sizing PRIVATE LOG_LEVEL=${LOG_LEVEL}) target_link_libraries(pumgen_sizing PUBLIC easi::easi tinyxml2) target_link_libraries(pumgen PUBLIC pumgen_sizing) + target_sources(pumgen PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/sizing/VelocityCheck.cpp) target_compile_definitions(pumgen PUBLIC USE_EASI) add_executable(pumgen-sizefield ${CMAKE_CURRENT_SOURCE_DIR}/src/sizefield.cpp) diff --git a/src/pumgen.cpp b/src/pumgen.cpp index cd67a80..cb83b58 100644 --- a/src/pumgen.cpp +++ b/src/pumgen.cpp @@ -51,6 +51,13 @@ #include "helper/InsphereCalculator.h" +#ifdef USE_EASI +#include "sizing/VelocityAwareMeshSize.h" +#include "sizing/VelocityAwareSettings.h" +#include "sizing/VelocityCheck.h" +#include "tinyxml2/tinyxml2.h" +#endif + template static TT _checkH5Err(TT&& status, const char* file, int line) { if (status < 0) { logError() << utils::nospace << "An HDF5 error occurred (" << file << ": " << line << ")"; @@ -282,6 +289,10 @@ int main(int argc, char* argv[]) { args.addAdditionalOption("input", "Input file (mesh or model)"); args.addAdditionalOption("output", "Output parallel unstructured mesh file", false); args.addOption("order", 'o', "Mesh order (default: 1)", utils::Args::Required, false); + args.addOption("velocity-check", 0, + "Compare the cell sizes with the velocity-aware mesh size of the " + "VelocityAwareMeshing element of a mesh attributes file (XML)", + utils::Args::Required, false); if (args.parse(argc, argv, rank == 0) != utils::Args::Success) return 1; @@ -463,6 +474,25 @@ int main(int argc, char* argv[]) { MPI_Reduce((rank == 0 ? MPI_IN_PLACE : &min), &min, 1, MPI_DOUBLE, MPI_MIN, 0, MPI_COMM_WORLD); logInfo() << "Minimum insphere found:" << min; + if (args.isSet("velocity-check")) { +#ifdef USE_EASI + const auto* settingsFile = args.getArgument("velocity-check"); + tinyxml2::XMLDocument doc; + if (doc.LoadFile(settingsFile) != tinyxml2::XML_SUCCESS) { + logError() << "Could not read" << settingsFile; + } + const auto settings = readVelocityAwareSettings(doc); + if (!settings.isVelocityAwareRefinementOn()) { + logError() << "No refinement cuboid in" << settingsFile; + } + const VelocityAwareMeshSize meshSize(settings); + checkVelocityAwareMeshSize(meshSize, cellVertices, meshInput->group(), MPI_COMM_WORLD); +#else + logError() << "This version of PUMGen has been compiled without easi; hence, --velocity-check " + "is not available."; +#endif + } + // Get offsets std::size_t offsets[2] = {localSize[0], localSize[1]}; MPI_Scan(MPI_IN_PLACE, offsets, 2, tndm::mpi_type_t(), MPI_SUM, MPI_COMM_WORLD); diff --git a/src/sizing/VelocityCheck.cpp b/src/sizing/VelocityCheck.cpp new file mode 100644 index 0000000..04f6a59 --- /dev/null +++ b/src/sizing/VelocityCheck.cpp @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#include "VelocityCheck.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils/logger.h" + +namespace { + +// log-spaced histogram of the ratios between MinRatio and MaxRatio +constexpr double MinRatio = 1e-3; +constexpr double MaxRatio = 1e3; +constexpr std::size_t NumBins = 6000; +constexpr std::size_t Batch = std::size_t{1} << 20; + +std::size_t binOf(double ratio) { + const double position = + std::log(ratio / MinRatio) / std::log(MaxRatio / MinRatio) * static_cast(NumBins); + return static_cast(std::clamp(position, 0.0, static_cast(NumBins - 1))); +} + +double upperEdgeOf(std::size_t bin) { + return MinRatio * std::pow(MaxRatio / MinRatio, static_cast(bin + 1) / NumBins); +} + +struct RatioStatistics { + std::vector histogram = std::vector(NumBins, 0); + std::uint64_t above = 0; + double maximum = 0; + + void add(double ratio) { + ++histogram[binOf(ratio)]; + above += ratio > 1 ? 1 : 0; + maximum = std::max(maximum, ratio); + } + + void reduce(MPI_Comm comm) { + MPI_Allreduce(MPI_IN_PLACE, histogram.data(), NumBins, MPI_UINT64_T, MPI_SUM, comm); + MPI_Allreduce(MPI_IN_PLACE, &above, 1, MPI_UINT64_T, MPI_SUM, comm); + MPI_Allreduce(MPI_IN_PLACE, &maximum, 1, MPI_DOUBLE, MPI_MAX, comm); + } + + /** + * The quantile, rounded up to the next bin edge (0.23 % apart). + */ + [[nodiscard]] double quantile(double q, std::uint64_t total) const { + const auto target = static_cast(std::ceil(q * static_cast(total))); + std::uint64_t cumulative = 0; + for (std::size_t bin = 0; bin < NumBins; ++bin) { + cumulative += histogram[bin]; + if (cumulative >= target && cumulative > 0) { + return std::min(upperEdgeOf(bin), maximum); + } + } + return maximum; + } + + [[nodiscard]] std::string summary(std::uint64_t total) const { + std::stringstream stream; + stream << std::setprecision(3) << "p50 " << quantile(0.5, total) << ", p90 " + << quantile(0.9, total) << ", p99 " << quantile(0.99, total) << ", max " << maximum + << "; " << above << " cells above 1"; + return stream.str(); + } +}; + +double distance(const std::array& a, const std::array& b) { + return std::sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]) + + (a[2] - b[2]) * (a[2] - b[2])); +} + +} // namespace + +void checkVelocityAwareMeshSize(const VelocityAwareMeshSize& meshSize, const CellVertices& cells, + const std::vector& groups, MPI_Comm comm) { + RatioStatistics meanEdge; + RatioStatistics longestEdge; + std::uint64_t checked = 0; + + std::vector> barycenters; + std::vector cellGroups; + std::vector frequencies; + std::vector> edges; + for (std::size_t first = 0; first < cells.numCells(); first += Batch) { + const std::size_t count = std::min(Batch, cells.numCells() - first); + barycenters.resize(count); + cellGroups.resize(count); + frequencies.resize(count); + edges.resize(count); + for (std::size_t c = 0; c < count; ++c) { + const auto vertices = cells(first + c); + std::array barycenter{}; + double sum = 0; + double longest = 0; + for (std::size_t a = 0; a < vertices.size(); ++a) { + for (int d = 0; d < 3; ++d) { + barycenter[d] += vertices[a][d] / static_cast(vertices.size()); + } + for (std::size_t b = a + 1; b < vertices.size(); ++b) { + const double length = distance(vertices[a], vertices[b]); + sum += length; + longest = std::max(longest, length); + } + } + barycenters[c] = barycenter; + cellGroups[c] = groups[first + c]; + frequencies[c] = meshSize.targetedFrequencyAndGroup(barycenter).first; + edges[c] = {sum / 6, longest}; + } + + const auto sizes = meshSize.meshSizes(barycenters, cellGroups, frequencies); + for (std::size_t c = 0; c < count; ++c) { + if (sizes[c] == std::numeric_limits::max()) { + continue; + } + ++checked; + meanEdge.add(edges[c][0] / sizes[c]); + longestEdge.add(edges[c][1] / sizes[c]); + } + } + + std::uint64_t total = cells.numCells(); + MPI_Allreduce(MPI_IN_PLACE, &checked, 1, MPI_UINT64_T, MPI_SUM, comm); + MPI_Allreduce(MPI_IN_PLACE, &total, 1, MPI_UINT64_T, MPI_SUM, comm); + meanEdge.reduce(comm); + longestEdge.reduce(comm); + + logInfo() << "Velocity check:" << checked << "of" << total + << "cells lie in refinement regions (ratios of edge length and mesh size)"; + if (checked > 0) { + logInfo() << "Velocity check, mean edge:" << meanEdge.summary(checked).c_str(); + logInfo() << "Velocity check, longest edge:" << longestEdge.summary(checked).c_str(); + } +} diff --git a/src/sizing/VelocityCheck.h b/src/sizing/VelocityCheck.h new file mode 100644 index 0000000..202a3b3 --- /dev/null +++ b/src/sizing/VelocityCheck.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#ifndef PUMGEN_SRC_SIZING_VELOCITYCHECK_H_ +#define PUMGEN_SRC_SIZING_VELOCITYCHECK_H_ + +#include +#include + +#include "VelocityAwareMeshSize.h" +#include "helper/InsphereCalculator.h" + +/** + * Compares the edge lengths of the cells in the refinement regions with the velocity-aware mesh + * size at their barycentre (for the material of the cell's group) and logs the distribution of the + * ratios, as mean and as longest edge length over mesh size. + */ +void checkVelocityAwareMeshSize(const VelocityAwareMeshSize& meshSize, const CellVertices& cells, + const std::vector& groups, MPI_Comm comm); + +#endif // PUMGEN_SRC_SIZING_VELOCITYCHECK_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ebf9e56..5e342e3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -192,9 +192,19 @@ pumgen_add_test(msh2.tiny-duplicateface RANKS 1 SOURCE msh2 INPUT tiny-duplicate # velocity-aware sizing, with a material of shear wave speed 1 in group 1 and 2 in group 2 if(EASI) + configure_file(fixtures/velocity-settings.xml.in ${CMAKE_CURRENT_BINARY_DIR}/velocity-settings.xml + @ONLY) configure_file(fixtures/velocity-sizefield.xml.in ${CMAKE_CURRENT_BINARY_DIR}/velocity-sizefield.xml @ONLY) + # the counts are computed independently from the reference of the coarse mesh + pumgen_add_test( + msh4.coarse.velocity-check RANKS 2 SOURCE msh4 INPUT coarse-v41.msh + ARGS --velocity-check=${CMAKE_CURRENT_BINARY_DIR}/velocity-settings.xml + EXPECT_OUTPUT + "320 of 320 cells lie in refinement regions.*mean edge:[^\n]*; 4 cells above 1.*longest edge:[^\n]*; 143 cells above 1" + ) + # the expected grids are verified independently; they must not depend on the number of ranks foreach(ranks 1 3) set(prefix ${PUMGEN_TEST_OUTPUT}/sizefield-np${ranks}) @@ -209,3 +219,7 @@ if(EASI) set_tests_properties(sizefield.np${ranks} PROPERTIES PROCESSORS ${ranks} TIMEOUT 120) endforeach() endif() +if(NOT EASI) + pumgen_add_test(msh4.tiny.velocity-check-without-easi RANKS 1 SOURCE msh4 INPUT tiny-v41.msh + ARGS --velocity-check=unused.xml EXPECT_ERROR "compiled without easi") +endif() diff --git a/tests/fixtures/velocity-settings.xml.in b/tests/fixtures/velocity-settings.xml.in new file mode 100644 index 0000000..0c56141 --- /dev/null +++ b/tests/fixtures/velocity-settings.xml.in @@ -0,0 +1,16 @@ + + + + + + + From db7c86ac679f4c24d03a35a419b0df6b30352a95 Mon Sep 17 00:00:00 2001 From: David Schneller Date: Tue, 22 Sep 2026 20:33:55 +0000 Subject: [PATCH 19/19] Read binary MSH 4.1 files with all ranks For binary MSH 4.1 files, rank 0 now only reads the structure of the file (entities, block headers, periodic nodes) and skips the node and element data. With the resulting offsets, every rank reads a share of the nodes, cells and boundary faces. The nodes are sent to the ranks owning them in the chunk distribution; the cells are read directly by their owners, in file order as before. The boundary faces go to the ranks given by a hash of their vertices, their vertices are marked in a bit set shared by all ranks, and only cell faces whose vertices all lie on the surface mesh are looked up. Periodic nodes are joined into classes on rank 0 and sent to the owners. On the mesh with 3.6M cells, the output is identical to the one of the serial reader for 1, 2, 4 and 8 ranks. The highest peak memory of a rank drops from 255 MB to 161 MB with 4 ranks and from 235 MB to 129 MB with 8 ranks; with one rank, time and memory stay about the same (1.07 s vs. 1.11 s, 404 MB vs. 378 MB). --gmsh-reader=serial keeps reading on rank 0; ASCII files are always read there. AI-generated. Model: Claude Opus 5.5 --- CMakeLists.txt | 2 + src/meshreader/DistributedGMSHReader.cpp | 79 +++ src/meshreader/DistributedGMSHReader.h | 482 +++++++++++++++++++ src/meshreader/GMSH4Parser.h | 9 +- src/meshreader/Msh4Index.cpp | 139 ++++++ src/meshreader/Msh4Index.h | 82 ++++ src/meshreader/MshInput.cpp | 19 + src/meshreader/MshInput.h | 6 + src/pumgen.cpp | 25 +- tests/CMakeLists.txt | 20 + tests/fixtures/generate.py | 22 + tests/fixtures/tiny-binary-badnode-v41.msh | Bin 0 -> 482 bytes tests/fixtures/tiny-binary-duplicate-v41.msh | Bin 0 -> 482 bytes tests/fixtures/tiny-binary-sparse-v41.msh | Bin 0 -> 482 bytes tests/fixtures/tiny-binary-v41.msh | Bin 0 -> 482 bytes 15 files changed, 878 insertions(+), 7 deletions(-) create mode 100644 src/meshreader/DistributedGMSHReader.cpp create mode 100644 src/meshreader/DistributedGMSHReader.h create mode 100644 src/meshreader/Msh4Index.cpp create mode 100644 src/meshreader/Msh4Index.h create mode 100644 tests/fixtures/tiny-binary-badnode-v41.msh create mode 100644 tests/fixtures/tiny-binary-duplicate-v41.msh create mode 100644 tests/fixtures/tiny-binary-sparse-v41.msh create mode 100644 tests/fixtures/tiny-binary-v41.msh diff --git a/CMakeLists.txt b/CMakeLists.txt index 269e851..3ae9833 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,8 @@ add_executable(pumgen ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/GMSH2Parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/GMSH4Parser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/MshInput.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/Msh4Index.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/meshreader/DistributedGMSHReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/input/ParallelVertexFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/helper/InsphereCalculator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/helper/MPIConvenience.cpp diff --git a/src/meshreader/DistributedGMSHReader.cpp b/src/meshreader/DistributedGMSHReader.cpp new file mode 100644 index 0000000..238030b --- /dev/null +++ b/src/meshreader/DistributedGMSHReader.cpp @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#include "DistributedGMSHReader.h" + +#include +#include +#include + +namespace puml::distributed { + +namespace { +template T swapBytesOf(T value) { + std::array bytes{}; + std::memcpy(bytes.data(), &value, sizeof(T)); + std::reverse(bytes.begin(), bytes.end()); + std::memcpy(&value, bytes.data(), sizeof(T)); + return value; +} + +std::uint64_t mix(std::uint64_t value) { + // splitmix64 finalizer + value ^= value >> 30; + value *= 0xbf58476d1ce4e5b9ULL; + value ^= value >> 27; + value *= 0x94d049bb133111ebULL; + value ^= value >> 31; + return value; +} +} // namespace + +RawMshFile::RawMshFile(const std::string& fileName, const Msh4Index& index) + : dataSize(index.dataSize), swapBytes(index.swapBytes) { + file = std::fopen(fileName.c_str(), "rb"); + if (file == nullptr) { + logError() << "Unable to open MSH file" << fileName; + } +} + +RawMshFile::~RawMshFile() { + if (file != nullptr) { + std::fclose(file); + } +} + +void RawMshFile::readBytes(std::uint64_t offset, std::size_t bytes, void* data) { + if (std::fseek(file, static_cast(offset), SEEK_SET) != 0 || + std::fread(data, 1, bytes, file) != bytes) { + logError() << "Unexpected end of file in binary data"; + } +} + +void RawMshFile::readSizes(std::uint64_t offset, std::size_t count, std::uint64_t* values) { + if (dataSize == sizeof(std::uint64_t)) { + readBytes(offset, count * sizeof(std::uint64_t), values); + if (swapBytes) { + std::transform(values, values + count, values, swapBytesOf); + } + } else { + narrowSizes.resize(count); + readBytes(offset, count * sizeof(std::uint32_t), narrowSizes.data()); + for (std::size_t i = 0; i < count; ++i) { + values[i] = swapBytes ? swapBytesOf(narrowSizes[i]) : narrowSizes[i]; + } + } +} + +void RawMshFile::readDoubles(std::uint64_t offset, std::size_t count, double* values) { + readBytes(offset, count * sizeof(double), values); + if (swapBytes) { + std::transform(values, values + count, values, swapBytesOf); + } +} + +std::uint64_t hashFace(const std::array& face) { + return mix(mix(mix(face[0]) ^ face[1]) ^ face[2]); +} + +} // namespace puml::distributed diff --git a/src/meshreader/DistributedGMSHReader.h b/src/meshreader/DistributedGMSHReader.h new file mode 100644 index 0000000..c390cd3 --- /dev/null +++ b/src/meshreader/DistributedGMSHReader.h @@ -0,0 +1,482 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#ifndef PUMGEN_SRC_MESHREADER_DISTRIBUTEDGMSHREADER_H_ +#define PUMGEN_SRC_MESHREADER_DISTRIBUTEDGMSHREADER_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "GMSHBuilder.h" +#include "Msh4Index.h" +#include "helper/Distributor.h" +#include "helper/MPIConvenience.h" +#include "third_party/MPITraits.h" +#include "utils/logger.h" + +namespace puml { + +namespace distributed { + +/** + * Reads values of a binary MSH 4.1 file at given offsets. + */ +class RawMshFile { + public: + RawMshFile(const std::string& fileName, const Msh4Index& index); + ~RawMshFile(); + RawMshFile(const RawMshFile&) = delete; + RawMshFile& operator=(const RawMshFile&) = delete; + + void readSizes(std::uint64_t offset, std::size_t count, std::uint64_t* values); + void readDoubles(std::uint64_t offset, std::size_t count, double* values); + + private: + void readBytes(std::uint64_t offset, std::size_t bytes, void* data); + + std::FILE* file = nullptr; + std::uint64_t dataSize; + bool swapBytes; + std::vector narrowSizes; +}; + +/** + * Sends outgoing[r] to rank r; returns what each rank has sent to this one. + */ +template +std::vector> exchange(const std::vector>& outgoing, MPI_Comm comm) { + static_assert(std::is_trivially_copyable_v); + int size = 1; + MPI_Comm_size(comm, &size); + std::vector sendCounts(size); + std::vector recvCounts(size); + for (int r = 0; r < size; ++r) { + sendCounts[r] = outgoing[r].size(); + } + MPI_Alltoall(sendCounts.data(), 1, tndm::mpi_type_t(), recvCounts.data(), 1, + tndm::mpi_type_t(), comm); + std::vector sendDisp(size + 1, 0); + std::vector recvDisp(size + 1, 0); + for (int r = 0; r < size; ++r) { + sendDisp[r + 1] = sendDisp[r] + sendCounts[r]; + recvDisp[r + 1] = recvDisp[r] + recvCounts[r]; + } + std::vector sendBuffer(sendDisp[size]); + for (int r = 0; r < size; ++r) { + std::copy(outgoing[r].begin(), outgoing[r].end(), sendBuffer.begin() + sendDisp[r]); + } + std::vector recvBuffer(recvDisp[size]); + + MPI_Datatype type; + MPI_Type_contiguous(sizeof(T), MPI_BYTE, &type); + MPI_Type_commit(&type); + sparseAlltoallv(sendBuffer.data(), sendCounts.data(), sendDisp.data(), type, recvBuffer.data(), + recvCounts.data(), recvDisp.data(), type, comm); + MPI_Type_free(&type); + + std::vector> incoming(size); + for (int r = 0; r < size; ++r) { + incoming[r].assign(recvBuffer.begin() + recvDisp[r], recvBuffer.begin() + recvDisp[r + 1]); + } + return incoming; +} + +/** + * Calls f(block, begin, count) for the parts of the items [first, last) of the concatenated + * blocks, where begin is relative to the block. + */ +template +void forBlockRanges(const std::vector& blocks, std::uint64_t first, std::uint64_t last, + F&& f) { + std::uint64_t blockStart = 0; + for (const auto& block : blocks) { + const std::uint64_t blockEnd = blockStart + block.count; + const std::uint64_t begin = std::max(first, blockStart); + const std::uint64_t end = std::min(last, blockEnd); + if (begin < end) { + f(block, begin - blockStart, end - begin); + } + blockStart = blockEnd; + } +} + +std::uint64_t hashFace(const std::array& face); + +// items are read in pieces of at most this many +constexpr std::uint64_t Piece = std::uint64_t{1} << 20; + +} // namespace distributed + +/** + * Reads a binary MSH 4.1 file with all ranks: rank 0 only locates the data, every rank reads a + * share of the nodes, cells and boundary faces, and the parts are sent to the ranks which own + * them in the chunk distribution of getChunksize. The boundary conditions are matched in a + * distributed table of the boundary faces; only faces whose vertices all lie on the surface mesh + * are looked up. + */ +template class DistributedGMSHReader { + public: + constexpr static std::size_t Order = OrderP; + constexpr static std::size_t Dim = 3; + constexpr static bool SupportsIdentify = true; + constexpr static std::size_t NodesPerCell = nodeCount(Dim, Order); + constexpr static std::size_t NodesPerFacet = nodeCount(Dim - 1, Order); + constexpr static std::size_t Facet2Nodes[Dim + 1][Dim] = { + {1, 0, 2}, {0, 1, 3}, {1, 2, 3}, {2, 0, 3}}; + constexpr static long BoundaryConditionOffset = 100; + + explicit DistributedGMSHReader(MPI_Comm comm = MPI_COMM_WORLD) : comm(comm) { + MPI_Comm_rank(comm, &rank); + MPI_Comm_size(comm, &size); + } + + void open(const char* meshFile) { + readIndex(meshFile); + distributed::RawMshFile file(meshFile, index); + readNodes(file); + readCells(file); + matchBoundaryConditions(file); + identifyPeriodicNodes(); + } + + [[nodiscard]] std::size_t nVertices() const { return index.numNodes; } + [[nodiscard]] std::size_t nElements() const { return numCells; } + + // Each read function hands over one part of the local mesh and releases it, so it can be called + // only once. + + void readVertices(double* vertices) { handOver(geometry, vertices); } + void readElements(std::size_t* elements) { handOver(connectivity, elements); } + void readGroups(int* groupsOut) { handOver(groups, groupsOut); } + void readBoundaries(int* boundariesOut) { handOver(boundaries, boundariesOut); } + [[nodiscard]] bool hasIdentify() const { return hasIdentify_; } + void readIdentify(std::size_t* vertices) { handOver(identify, vertices); } + + private: + struct NodeRecord { + std::uint64_t index; + double x[3]; + }; + struct FaceRecord { + std::array vertices; + std::int64_t bc; + }; + struct FaceQuery { + std::array vertices; + std::uint64_t localFace; + }; + struct FaceReply { + std::uint64_t localFace; + std::int64_t bc; + }; + struct Identification { + std::uint64_t vertex; + std::uint64_t representative; + }; + constexpr static std::int64_t NoMatch = -1; + + template static void handOver(std::vector& values, U* out) { + std::copy(values.begin(), values.end(), out); + std::vector().swap(values); + } + + [[nodiscard]] std::uint64_t localFirstVertex() const { + return getChunksum(index.numNodes, rank, size); + } + [[nodiscard]] std::uint64_t localVertexCount() const { + return getChunksize(index.numNodes, rank, size); + } + [[nodiscard]] int vertexOwner(std::uint64_t vertex) const { + return getChunkOwner(index.numNodes, vertex, size); + } + + [[nodiscard]] std::uint64_t nodeIndex(std::uint64_t tag) const { + if (tag < index.firstNodeTag || tag - index.firstNodeTag >= index.numNodes) { + logError() << "Unknown node tag" << tag; + } + return tag - index.firstNodeTag; + } + + template void broadcastVector(std::vector& values) { + std::uint64_t count = values.size(); + MPI_Bcast(&count, 1, MPI_UINT64_T, 0, comm); + values.resize(count); + MPI_Bcast(values.data(), static_cast(count * sizeof(T)), MPI_BYTE, 0, comm); + } + + void readIndex(const char* meshFile) { + if (rank == 0) { + Msh4Indexer indexer(GMSHSimplexType::type, GMSHSimplexType::type); + if (!indexer.parseFile(meshFile)) { + logError() << meshFile << std::endl << indexer.getErrorMessage(); + } + index = indexer.getIndex(); + } + std::array scalars{index.dataSize, index.swapBytes ? 1U : 0U, + index.firstNodeTag, index.numNodes, + index.periodic.empty() ? 0U : 1U}; + MPI_Bcast(scalars.data(), scalars.size(), MPI_UINT64_T, 0, comm); + index.dataSize = scalars[0]; + index.swapBytes = scalars[1] != 0; + index.firstNodeTag = scalars[2]; + index.numNodes = scalars[3]; + hasIdentify_ = scalars[4] != 0; + broadcastVector(index.nodeBlocks); + broadcastVector(index.cellBlocks); + broadcastVector(index.facetBlocks); + numCells = 0; + for (const auto& block : index.cellBlocks) { + numCells += block.count; + } + } + + void readNodes(distributed::RawMshFile& file) { + const std::uint64_t first = getChunksum(index.numNodes, rank, size); + const std::uint64_t last = first + getChunksize(index.numNodes, rank, size); + + std::vector> outgoing(size); + std::vector tags; + std::vector coordinates; + distributed::forBlockRanges( + index.nodeBlocks, first, last, + [&](const Msh4NodeBlock& block, std::uint64_t begin, std::uint64_t count) { + for (std::uint64_t done = 0; done < count; done += distributed::Piece) { + const auto n = std::min(distributed::Piece, count - done); + tags.resize(n); + coordinates.resize(n * block.valuesPerNode); + file.readSizes(block.tagsOffset + (begin + done) * index.dataSize, n, tags.data()); + file.readDoubles(block.coordinatesOffset + + (begin + done) * block.valuesPerNode * sizeof(double), + coordinates.size(), coordinates.data()); + for (std::uint64_t i = 0; i < n; ++i) { + const auto vertex = nodeIndex(tags[i]); + const double* x = &coordinates[i * block.valuesPerNode]; + outgoing[vertexOwner(vertex)].push_back({vertex, {x[0], x[1], x[2]}}); + } + } + }); + + const auto incoming = distributed::exchange(outgoing, comm); + std::vector>().swap(outgoing); + + const auto localFirst = localFirstVertex(); + geometry.assign(localVertexCount() * Dim, 0.0); + std::vector defined(localVertexCount(), false); + std::uint64_t numDefined = 0; + for (const auto& records : incoming) { + for (const auto& record : records) { + const auto local = record.index - localFirst; + if (defined[local]) { + logError() << "Duplicate node tag" << record.index + index.firstNodeTag; + } + defined[local] = true; + ++numDefined; + std::copy_n(record.x, Dim, &geometry[local * Dim]); + } + } + if (numDefined != localVertexCount()) { + logError() << "Missing nodes: the node tags are not unique"; + } + } + + void readCells(distributed::RawMshFile& file) { + const std::uint64_t first = getChunksum(numCells, rank, size); + const std::uint64_t count = getChunksize(numCells, rank, size); + connectivity.resize(count * NodesPerCell); + groups.resize(count); + + constexpr std::uint64_t ValuesPerCell = 1 + NodesPerCell; + std::vector values; + std::uint64_t cell = 0; + distributed::forBlockRanges( + index.cellBlocks, first, first + count, + [&](const Msh4ElementBlock& block, std::uint64_t begin, std::uint64_t blockCount) { + for (std::uint64_t done = 0; done < blockCount; done += distributed::Piece) { + const auto n = std::min(distributed::Piece, blockCount - done); + values.resize(n * ValuesPerCell); + file.readSizes(block.offset + (begin + done) * ValuesPerCell * index.dataSize, + values.size(), values.data()); + for (std::uint64_t e = 0; e < n; ++e, ++cell) { + for (std::size_t k = 0; k < NodesPerCell; ++k) { + connectivity[cell * NodesPerCell + k] = + nodeIndex(values[e * ValuesPerCell + 1 + k]); + } + groups[cell] = static_cast(block.physical); + } + } + }); + } + + void matchBoundaryConditions(distributed::RawMshFile& file) { + std::uint64_t numFacets = 0; + for (const auto& block : index.facetBlocks) { + numFacets += block.count; + } + const std::uint64_t first = getChunksum(numFacets, rank, size); + const std::uint64_t last = first + getChunksize(numFacets, rank, size); + + // the boundary faces go to the ranks given by their hash; their vertices are marked in a bit + // set of all vertices, which all ranks share + std::vector> outgoingFaces(size); + std::vector onBoundary((index.numNodes + 63) / 64, 0); + constexpr std::uint64_t ValuesPerFacet = 1 + NodesPerFacet; + std::vector values; + distributed::forBlockRanges( + index.facetBlocks, first, last, + [&](const Msh4ElementBlock& block, std::uint64_t begin, std::uint64_t count) { + for (std::uint64_t done = 0; done < count; done += distributed::Piece) { + const auto n = std::min(distributed::Piece, count - done); + values.resize(n * ValuesPerFacet); + file.readSizes(block.offset + (begin + done) * ValuesPerFacet * index.dataSize, + values.size(), values.data()); + for (std::uint64_t e = 0; e < n; ++e) { + std::array face{}; + for (std::size_t k = 0; k < Dim; ++k) { + face[k] = nodeIndex(values[e * ValuesPerFacet + 1 + k]); + onBoundary[face[k] / 64] |= std::uint64_t{1} << (face[k] % 64); + } + std::sort(face.begin(), face.end()); + outgoingFaces[distributed::hashFace(face) % size].push_back({face, block.physical}); + } + } + }); + + std::vector faceTable; + for (const auto& faces : distributed::exchange(outgoingFaces, comm)) { + faceTable.insert(faceTable.end(), faces.begin(), faces.end()); + } + std::vector>().swap(outgoingFaces); + const auto byVertices = [](const FaceRecord& a, const FaceRecord& b) { + return a.vertices < b.vertices; + }; + std::sort(faceTable.begin(), faceTable.end(), byVertices); + + MPI_Allreduce(MPI_IN_PLACE, onBoundary.data(), static_cast(onBoundary.size()), + MPI_UINT64_T, MPI_BOR, comm); + const auto vertexOnBoundary = [&](std::uint64_t vertex) { + return ((onBoundary[vertex / 64] >> (vertex % 64)) & 1) != 0; + }; + + const std::uint64_t numLocalCells = groups.size(); + // only faces with all vertices on the surface mesh can be part of it + std::vector> queries(size); + for (std::uint64_t cell = 0; cell < numLocalCells; ++cell) { + std::array flagged{}; + std::size_t numFlagged = 0; + for (std::size_t k = 0; k < Dim + 1; ++k) { + flagged[k] = vertexOnBoundary(connectivity[cell * NodesPerCell + k]); + numFlagged += flagged[k] ? 1 : 0; + } + if (numFlagged < Dim) { + continue; + } + for (std::size_t localFace = 0; localFace < Dim + 1; ++localFace) { + std::array face{}; + bool candidate = true; + for (std::size_t k = 0; k < Dim; ++k) { + candidate = candidate && flagged[Facet2Nodes[localFace][k]]; + face[k] = connectivity[cell * NodesPerCell + Facet2Nodes[localFace][k]]; + } + if (candidate) { + std::sort(face.begin(), face.end()); + queries[distributed::hashFace(face) % size].push_back( + {face, cell * (Dim + 1) + localFace}); + } + } + } + + const auto received = distributed::exchange(queries, comm); + std::vector>().swap(queries); + std::vector> replies(size); + for (int r = 0; r < size; ++r) { + for (const auto& query : received[r]) { + const FaceRecord key{query.vertices, 0}; + const auto [lower, upper] = + std::equal_range(faceTable.begin(), faceTable.end(), key, byVertices); + if (upper - lower > 1) { + logError() << "A face of an element exists multiple times in the surface mesh."; + } + replies[r].push_back({query.localFace, lower != upper ? lower->bc : NoMatch}); + } + } + + boundaries.assign(numLocalCells * (Dim + 1), 0); + for (const auto& answersFrom : distributed::exchange(replies, comm)) { + for (const auto& reply : answersFrom) { + if (reply.bc != NoMatch) { + const auto bc = + reply.bc >= BoundaryConditionOffset ? reply.bc - BoundaryConditionOffset : reply.bc; + boundaries[reply.localFace] = static_cast(bc); + } + } + } + } + + void identifyPeriodicNodes() { + if (!hasIdentify_) { + return; + } + // rank 0 joins the periodic nodes into classes, represented by their smallest vertex + std::vector> outgoing(size); + if (rank == 0) { + std::unordered_map parent; + const auto find = [&](std::uint64_t vertex) { + auto it = parent.try_emplace(vertex, vertex).first; + while (it->second != it->first) { + const auto next = parent.find(it->second); + it->second = next->second; + it = next; + } + return it->first; + }; + for (const auto& [nodeTag, masterTag] : index.periodic) { + const auto a = find(nodeIndex(nodeTag)); + const auto b = find(nodeIndex(masterTag)); + parent[std::max(a, b)] = std::min(a, b); + } + std::vector vertices; + vertices.reserve(parent.size()); + for (const auto& entry : parent) { + vertices.push_back(entry.first); + } + for (const auto vertex : vertices) { + outgoing[vertexOwner(vertex)].push_back({vertex, find(vertex)}); + } + } + + const auto localFirst = localFirstVertex(); + identify.resize(localVertexCount()); + std::iota(identify.begin(), identify.end(), localFirst); + for (const auto& identifications : distributed::exchange(outgoing, comm)) { + for (const auto& identification : identifications) { + identify[identification.vertex - localFirst] = identification.representative; + } + } + } + + MPI_Comm comm; + int rank = 0; + int size = 1; + Msh4Index index; + std::uint64_t numCells = 0; + bool hasIdentify_ = false; + std::vector geometry; + std::vector connectivity; + std::vector groups; + std::vector boundaries; + std::vector identify; +}; + +} // namespace puml + +#endif // PUMGEN_SRC_MESHREADER_DISTRIBUTEDGMSHREADER_H_ diff --git a/src/meshreader/GMSH4Parser.h b/src/meshreader/GMSH4Parser.h index 77f8c5b..bc7794c 100644 --- a/src/meshreader/GMSH4Parser.h +++ b/src/meshreader/GMSH4Parser.h @@ -21,12 +21,13 @@ class GMSH4Parser : public GMSHParser { public: using GMSHParser::GMSHParser; - private: + protected: void parse_() override; void parseEntities(); - void parseNodes(); - void parseElements(); - void parsePeriodic(); + // overridden by readers which only locate the data of these sections + virtual void parseNodes(); + virtual void parseElements(); + virtual void parsePeriodic(); /** * Starts the data of a section; in binary files, the data begins after the line break. diff --git a/src/meshreader/Msh4Index.cpp b/src/meshreader/Msh4Index.cpp new file mode 100644 index 0000000..a73b71c --- /dev/null +++ b/src/meshreader/Msh4Index.cpp @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#include "Msh4Index.h" + +#include +#include + +namespace puml { + +void Msh4Indexer::skipData(std::size_t bytes) { + if (!input->seek(input->offset() + bytes)) { + failFile("Unexpected end of file in binary data"); + } +} + +void Msh4Indexer::parseNodes() { + if (!binary) { + failFile("Only binary MSH 4.1 files can be read in parallel"); + } + beginSectionData(); + const std::size_t numBlocks = readSize(); + const std::size_t numVertices = readSize(); + const std::size_t minNodeTag = readSize(); + const std::size_t maxNodeTag = readSize(); + if (numVertices > 0 && + (minNodeTag == 0 || maxNodeTag < minNodeTag || maxNodeTag - minNodeTag + 1 != numVertices)) { + char buf[192]; + snprintf(buf, sizeof(buf), + "Non-contiguous node tags are not supported (%zu nodes with tags from %zu to %zu)", + numVertices, minNodeTag, maxNodeTag); + fail(buf); + } + hasNodes = true; + firstNodeTag = minNodeTag; + numNodes = numVertices; + index.dataSize = dataSize; + index.swapBytes = swapBytes; + index.firstNodeTag = minNodeTag; + index.numNodes = numVertices; + + std::size_t numInBlocks = 0; + for (std::size_t blockIdx = 0; blockIdx < numBlocks; ++blockIdx) { + const long dim = readInt(); + readInt(); // entity tag + const long parametric = readInt(); + const std::size_t count = readSize(); + Msh4NodeBlock block{}; + block.count = count; + block.valuesPerNode = 3 + (parametric != 0 ? static_cast(dim) : 0); + block.tagsOffset = input->offset(); + block.coordinatesOffset = block.tagsOffset + count * dataSize; + skipData(count * dataSize + count * block.valuesPerNode * sizeof(double)); + if (count > 0) { + index.nodeBlocks.push_back(block); + } + numInBlocks += count; + } + if (numInBlocks != numVertices) { + failFile("The node blocks contain " + std::to_string(numInBlocks) + " nodes instead of " + + std::to_string(numVertices)); + } + expectToken("$EndNodes"); +} + +void Msh4Indexer::parseElements() { + if (!hasNodes) { + fail("Expected $Nodes before $Elements"); + } + beginSectionData(); + const std::size_t numBlocks = readSize(); + readSize(); // number of elements + readSize(); // minimal element tag + readSize(); // maximal element tag + + for (std::size_t blockIdx = 0; blockIdx < numBlocks; ++blockIdx) { + const long dim = readInt(); + const long entityTag = readInt(); + const auto typeOffset = position(); + const long type = readInt(); + if (type < 1 || type > static_cast(NumTypes) || NumNodes[type - 1] == 0) { + failAt(typeOffset, "Unknown element type " + std::to_string(type)); + } + const std::size_t count = readSize(); + const Msh4ElementBlock block{count, input->offset(), + count > 0 ? physicalTag(dim, entityTag) : 0}; + skipData(count * (1 + NumNodes[type - 1]) * dataSize); + if (count > 0 && type == cellType) { + index.cellBlocks.push_back(block); + } else if (count > 0 && type == facetType) { + index.facetBlocks.push_back(block); + } + } + expectToken("$EndElements"); +} + +void Msh4Indexer::parsePeriodic() { + if (!hasNodes) { + fail("Expected $Nodes before $Periodic"); + } + beginSectionData(); + const std::size_t numPeriodic = readSize(); + for (std::size_t blockIdx = 0; blockIdx < numPeriodic; ++blockIdx) { + readInt(); // entity dimension + readInt(); // entity tag + readInt(); // master entity tag + const std::size_t numAffine = readSize(); + for (std::size_t i = 0; i < numAffine; ++i) { + readDouble(); + } + const std::size_t numIdentified = readSize(); + for (std::size_t i = 0; i < numIdentified; ++i) { + const auto nodeOffset = position(); + const std::size_t node = readSize(); + nodeIndex(node, nodeOffset); + const auto masterOffset = position(); + const std::size_t master = readSize(); + nodeIndex(master, masterOffset); + index.periodic.emplace_back(node, master); + } + } + expectToken("$EndPeriodic"); +} + +bool isBinaryMsh4(const std::string& fileName) { + try { + MshInput input(fileName); + if (input.readToken() != "$MeshFormat") { + return false; + } + const auto version = input.readReal(); + const auto fileType = input.readInteger(); + return version && fileType && *version >= 4.1 && *version < 5.0 && *fileType == 1; + } catch (const std::exception&) { + return false; + } +} + +} // namespace puml diff --git a/src/meshreader/Msh4Index.h b/src/meshreader/Msh4Index.h new file mode 100644 index 0000000..3d24d18 --- /dev/null +++ b/src/meshreader/Msh4Index.h @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2026 SeisSol Group +// +// SPDX-License-Identifier: BSD-3-Clause +#ifndef PUMGEN_SRC_MESHREADER_MSH4INDEX_H_ +#define PUMGEN_SRC_MESHREADER_MSH4INDEX_H_ + +#include +#include +#include +#include + +#include "GMSH4Parser.h" + +namespace puml { + +/** + * A block of nodes in a binary MSH 4.1 file: first the tags, then the coordinates. + */ +struct Msh4NodeBlock { + std::uint64_t count; + std::uint64_t tagsOffset; + std::uint64_t coordinatesOffset; + // three coordinates and the parametric coordinates, if any + std::uint64_t valuesPerNode; +}; + +/** + * A block of elements of one type in a binary MSH 4.1 file: per element its tag and its nodes. + */ +struct Msh4ElementBlock { + std::uint64_t count; + std::uint64_t offset; + std::int64_t physical; +}; + +/** + * Where the data of a binary MSH 4.1 file lies. + */ +struct Msh4Index { + std::uint64_t dataSize = 8; + bool swapBytes = false; + // the node tags form the contiguous range [firstNodeTag, firstNodeTag + numNodes) + std::uint64_t firstNodeTag = 0; + std::uint64_t numNodes = 0; + std::vector nodeBlocks; + std::vector cellBlocks; + std::vector facetBlocks; + // pairs of node tags (node, master) from $Periodic + std::vector> periodic; +}; + +/** + * Reads the structure of a binary MSH 4.1 file (entities, block headers, periodic nodes) and + * skips the node and element data, recording where they lie. + */ +class Msh4Indexer : public GMSH4Parser { + public: + Msh4Indexer(long cellType, long facetType) + : GMSH4Parser(nullptr), cellType(cellType), facetType(facetType) {} + + [[nodiscard]] const Msh4Index& getIndex() const { return index; } + + private: + void parseNodes() override; + void parseElements() override; + void parsePeriodic() override; + + void skipData(std::size_t bytes); + + long cellType; + long facetType; + Msh4Index index; +}; + +/** + * Whether the file is a binary MSH 4.1 file; only reads its header. + */ +bool isBinaryMsh4(const std::string& fileName); + +} // namespace puml + +#endif // PUMGEN_SRC_MESHREADER_MSH4INDEX_H_ diff --git a/src/meshreader/MshInput.cpp b/src/meshreader/MshInput.cpp index aeee650..10683f2 100644 --- a/src/meshreader/MshInput.cpp +++ b/src/meshreader/MshInput.cpp @@ -77,6 +77,25 @@ bool MshInput::readRaw(void* data, std::size_t bytes) { return true; } +bool MshInput::seek(std::size_t offset) { + if (offset >= bufferOffset && offset <= bufferOffset + end) { + begin = offset - bufferOffset; + return true; + } + if (std::fseek(file, 0, SEEK_END) != 0) { + return false; + } + const auto size = static_cast(std::ftell(file)); + if (offset > size || std::fseek(file, static_cast(offset), SEEK_SET) != 0) { + return false; + } + bufferOffset = offset; + begin = end = 0; + buffer[0] = '\0'; + eof = false; + return true; +} + bool MshInput::skipTo(std::string_view marker) { while (true) { const std::string_view data(buffer.data() + begin, end - begin); diff --git a/src/meshreader/MshInput.h b/src/meshreader/MshInput.h index f00e6eb..0cc77f5 100644 --- a/src/meshreader/MshInput.h +++ b/src/meshreader/MshInput.h @@ -127,6 +127,12 @@ class MshInput { */ bool readRaw(void* data, std::size_t bytes); + /** + * Continues reading at the given file offset; returns false if the offset lies behind the end + * of the file. + */ + bool seek(std::size_t offset); + /** * Skips everything up to and including the next occurrence of the marker. */ diff --git a/src/pumgen.cpp b/src/pumgen.cpp index cb83b58..89b0eec 100644 --- a/src/pumgen.cpp +++ b/src/pumgen.cpp @@ -44,6 +44,7 @@ #ifdef USE_SIMMOD #include "input/SimModSuite.h" #endif // USE_SIMMOD +#include "meshreader/DistributedGMSHReader.h" #include "meshreader/GMSH2Parser.h" #include "meshreader/GMSH4Parser.h" #include "meshreader/ParallelGMSHReader.h" @@ -227,6 +228,8 @@ template using SMF2 = SerialMeshFile>; template using SMF4 = SerialMeshFile>; +template +using SMF4Distributed = SerialMeshFile>; int main(int argc, char* argv[]) { int rank = 0; @@ -289,6 +292,11 @@ int main(int argc, char* argv[]) { args.addAdditionalOption("input", "Input file (mesh or model)"); args.addAdditionalOption("output", "Output parallel unstructured mesh file", false); args.addOption("order", 'o', "Mesh order (default: 1)", utils::Args::Required, false); + const auto gmshReaders = std::vector{"auto", "serial"}; + args.addEnumOption("gmsh-reader", gmshReaders, 0, + "Reader for msh4 meshes (default: auto, which reads binary MSH 4.1 files " + "with all ranks; serial: rank 0 reads the file)", + false); args.addOption("velocity-check", 0, "Compare the cell sizes with the velocity-aware mesh size of the " "VelocityAwareMeshing element of a mesh attributes file (XML)", @@ -390,10 +398,21 @@ int main(int argc, char* argv[]) { logInfo() << "Using GMSH mesh format 2 (msh2) mesh"; meshInput = puml::makePointer(meshOrder, inputFile, faceOffset); break; - case 2: - logInfo() << "Using GMSH mesh format 4 (msh4) mesh"; - meshInput = puml::makePointer(meshOrder, inputFile, faceOffset); + case 2: { + int distributed = 0; + if (rank == 0) { + distributed = args.getArgument("gmsh-reader", 0) == 0 && puml::isBinaryMsh4(inputFile); + } + MPI_Bcast(&distributed, 1, MPI_INT, 0, MPI_COMM_WORLD); + if (distributed != 0) { + logInfo() << "Using GMSH mesh format 4 (msh4) mesh, binary, read by all ranks"; + meshInput = puml::makePointer(meshOrder, inputFile, faceOffset); + } else { + logInfo() << "Using GMSH mesh format 4 (msh4) mesh"; + meshInput = puml::makePointer(meshOrder, inputFile, faceOffset); + } break; + } case 3: #ifdef USE_NETCDF logInfo() << "Using netCDF mesh"; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5e342e3..d353628 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -186,6 +186,26 @@ pumgen_add_test(msh4.periodic-binary.np2 RANKS 2 SOURCE msh4 INPUT periodic-bina pumgen_add_test(msh4.coarse-binary-truncated RANKS 1 SOURCE msh4 INPUT coarse-binary-truncated-v41.msh EXPECT_ERROR "Unexpected end of file") +# binary MSH 4.1 files are read by all ranks unless --gmsh-reader=serial +foreach(ranks 4 7) + pumgen_add_test(msh4.layered-binary.np${ranks} RANKS ${ranks} SOURCE msh4 + INPUT layered-binary-v41.msh REFERENCE layered.puml.h5 TOLERANCE 1e-12) +endforeach() +pumgen_add_test(msh4.layered-binary-serial.np3 RANKS 3 SOURCE msh4 INPUT layered-binary-v41.msh + REFERENCE layered.puml.h5 TOLERANCE 1e-12 ARGS --gmsh-reader=serial) +pumgen_add_test(msh4.periodic-binary-serial.np2 RANKS 2 SOURCE msh4 INPUT periodic-binary-v41.msh + REFERENCE periodic.puml.h5 TOLERANCE 1e-12 IDENTIFY ARGS --gmsh-reader=serial) +pumgen_add_test(msh4.periodic-binary.np3 RANKS 3 SOURCE msh4 INPUT periodic-binary-v41.msh + REFERENCE periodic.puml.h5 TOLERANCE 1e-12 IDENTIFY) +pumgen_add_test(msh4.tiny-binary.np3 RANKS 3 SOURCE msh4 INPUT tiny-binary-v41.msh + REFERENCE tiny.puml.h5) +pumgen_add_test(msh4.tiny-binary-duplicate RANKS 1 SOURCE msh4 INPUT tiny-binary-duplicate-v41.msh + EXPECT_ERROR "Duplicate node tag 2") +pumgen_add_test(msh4.tiny-binary-badnode RANKS 1 SOURCE msh4 INPUT tiny-binary-badnode-v41.msh + EXPECT_ERROR "Unknown node tag 9") +pumgen_add_test(msh4.tiny-binary-sparse RANKS 1 SOURCE msh4 INPUT tiny-binary-sparse-v41.msh + EXPECT_ERROR "Non-contiguous node tags are not supported") + # a face of the surface mesh given twice pumgen_add_test(msh2.tiny-duplicateface RANKS 1 SOURCE msh2 INPUT tiny-duplicateface-v22.msh EXPECT_ERROR "exists multiple times in the surface mesh") diff --git a/tests/fixtures/generate.py b/tests/fixtures/generate.py index c75b460..227fe8c 100644 --- a/tests/fixtures/generate.py +++ b/tests/fixtures/generate.py @@ -350,6 +350,19 @@ def generate_periodic(): """ +def tiny_binary(node_tags, element_nodes, node_header=(1, 4, 1, 4)): + """The single tetrahedron of TINY_V41 as binary MSH 4.1 file.""" + data = b"$MeshFormat\n4.1 1 8\n" + struct.pack("9YmCL}c=CQJ`h7zYJ& sBo1i?!t!8`K%B4Qnvn+a literal 0 HcmV?d00001 diff --git a/tests/fixtures/tiny-binary-v41.msh b/tests/fixtures/tiny-binary-v41.msh new file mode 100644 index 0000000000000000000000000000000000000000..233943537770095f5d6124b481fa5dacf110699a GIT binary patch literal 482 zcmY%KO)buF%P-1JEa5WIGgL5Cu;5~3U|`@