From 32b511902547ed73275d8d2f0cce4b3b6cd487ce Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 10:55:35 +0200 Subject: [PATCH 01/18] add rule of five --- .../t8_cmesh_boundary_condition_handler.hxx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx index 167bd3dba4..35ae67e55e 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx @@ -73,6 +73,8 @@ struct t8_cmesh_boundary_condition_handler using boundary_condition_hash = T8Type; public: + /**************************************** CONSTRUCTORS & ASSIGNMENT OPERATORS ****************************************/ + /** * Standard constructor. Associates the handler with a cmesh * \param [in] cmesh @@ -81,6 +83,42 @@ struct t8_cmesh_boundary_condition_handler { } + /** + * Copy constructor. + * \param [in] other The other. + */ + t8_cmesh_boundary_condition_handler (const t8_cmesh_boundary_condition_handler &other) = default; + + /** + * Move constructor. + * \param [in] other The other. + */ + t8_cmesh_boundary_condition_handler (t8_cmesh_boundary_condition_handler &&other) noexcept = default; + + /** + * Copy assignment operator. + * \param [in] other The other. + * \return A copy of this. + */ + t8_cmesh_boundary_condition_handler & + operator= (const t8_cmesh_boundary_condition_handler &other) + = default; + + /** + * Move assignment operator. + * \param [in] other The other. + * \return A reference to a moved version of this. + */ + t8_cmesh_boundary_condition_handler & + operator= (t8_cmesh_boundary_condition_handler &&other) noexcept + = default; + + /** + * The destructor. + */ + ~t8_cmesh_boundary_condition_handler () = default; + + /**************************************** BOUNDARY CONDITION SETUP ****************************************/ /** * Applies boundary conditions to the faces of a cmesh cell. * @@ -105,6 +143,8 @@ struct t8_cmesh_boundary_condition_handler hashes.data (), sizeof (boundary_condition_hash) * hashes.size (), 0); } + /**************************************** BOUNDARY CONDITION RETRIEVAL ****************************************/ + /** * Retrieves the boundary conditions of a cmesh cell. * @@ -195,6 +235,8 @@ struct t8_cmesh_boundary_condition_handler return std::nullopt; } + /**************************************** HELPER FUNCTIONS ****************************************/ + #if T8_ENABLE_DEBUG /** Verifies the proper attribution of boundary conditions. Can only be called on a cmesh * during commit. @@ -267,6 +309,8 @@ struct t8_cmesh_boundary_condition_handler return m_boundary_conditions.at (hash); } + /**************************************** MEMBERS ****************************************/ + /** The associated cmesh of this struct */ t8_cmesh_t m_cmesh; From 262d698975626c90d94aad1a7517a0405edab27d Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:01:34 +0200 Subject: [PATCH 02/18] make bc tests parallel --- test/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fdc8409356..8eb1989447 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -139,7 +139,7 @@ add_t8_cpp_test( NAME t8_gtest_compute_first_element_serial SOUR add_t8_cpp_test( NAME t8_gtest_multiple_attributes_parallel SOURCES t8_cmesh/t8_gtest_multiple_attributes.cxx ) add_t8_cpp_test( NAME t8_gtest_attribute_gloidx_array_serial SOURCES t8_cmesh/t8_gtest_attribute_gloidx_array.cxx ) add_t8_cpp_test( NAME t8_gtest_cmesh_bounding_box_serial SOURCES t8_cmesh/t8_gtest_cmesh_bounding_box.cxx ) -add_t8_cpp_test( NAME t8_gtest_cmesh_boundary_conditions_serial SOURCES t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx ) +add_t8_cpp_test( NAME t8_gtest_cmesh_boundary_conditions_parallel SOURCES t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx ) add_t8_cpp_test( NAME t8_gtest_shmem_parallel SOURCES t8_data/t8_gtest_shmem.cxx ) add_t8_cpp_test( NAME t8_gtest_data_pack_parallel SOURCES t8_data/t8_gtest_data_handler.cxx t8_data/t8_data_handler_specs.cxx) From 6e531d4b5d4a9889a13316552e2c79b8b4227ad1 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:04:22 +0200 Subject: [PATCH 03/18] add a debug message --- .../internal/t8_cmesh_boundary_condition_handler.hxx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx index 35ae67e55e..40339d8ade 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx @@ -136,7 +136,14 @@ struct t8_cmesh_boundary_condition_handler hashes.reserve (std::size (boundary_conditions)); for (const auto &boundary_condition : boundary_conditions) { const boundary_condition_hash hash = hash_boundary_condition_name (boundary_condition); - m_boundary_conditions.try_emplace (hash, boundary_condition); + const auto inserted = m_boundary_conditions.try_emplace (hash, boundary_condition); +#if T8_ENABLE_DEBUG + if (inserted.second) { + const std::string_view boundary_condition_view = boundary_condition; + t8_debugf ("Registered boundary condition %.*s\n", static_cast (boundary_condition_view.size ()), + boundary_condition_view.data ()); + } +#endif hashes.emplace_back (std::move (hash)); } t8_cmesh_set_attribute (m_cmesh, gtreeid, t8_get_package_id (), get_boundary_condition_attribute_key (), From 44796de5d7d574aee077763303c5f2b35a5413d1 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:05:37 +0200 Subject: [PATCH 04/18] add cmesh setter function --- .../t8_cmesh_boundary_condition_handler.hxx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx index 40339d8ade..9bacbffb6c 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx @@ -150,6 +150,19 @@ struct t8_cmesh_boundary_condition_handler hashes.data (), sizeof (boundary_condition_hash) * hashes.size (), 0); } + /** + * Updates the internal cmesh. The boundary condition handler can only be given to uncommitted cmeshes. + * \param [in] new_cmesh The new cmesh. + */ + inline void + set_cmesh (t8_cmesh_t new_cmesh) + { + T8_ASSERT (t8_cmesh_is_initialized (new_cmesh)); + T8_ASSERTF (t8_cmesh_is_committed (new_cmesh, 0), + "The boundary condition handler can only be set for uncommitted cmeshes.\n"); + m_cmesh = new_cmesh; + } + /**************************************** BOUNDARY CONDITION RETRIEVAL ****************************************/ /** From a258ce8f7e3d886c9d0bff30e0ef19240c22a22c Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:06:19 +0200 Subject: [PATCH 05/18] copy bc handler in meshes with set_from --- src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx b/src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx index 289b9a0b3d..6e752480aa 100644 --- a/src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx +++ b/src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx @@ -36,6 +36,7 @@ #include #include #include +#include /** * A struct to hold the information about a ghost facejoin. @@ -583,6 +584,15 @@ t8_cmesh_commit (t8_cmesh_t cmesh, sc_MPI_Comm comm) cmesh->geometry_handler->ref (); } + /* Copy the boundary condition handler if available. */ + T8_ASSERT (cmesh->boundary_condition_handler == nullptr); + if (cmesh->set_from->boundary_condition_handler != nullptr) { + cmesh->boundary_condition_handler + = new detail::t8_cmesh_boundary_condition_handler (*cmesh->set_from->boundary_condition_handler); + /* Assign handler to new cmesh. */ + cmesh->boundary_condition_handler->set_cmesh (cmesh); + } + #if T8_ENABLE_DEBUG /* Copy negative volume check from set_from */ cmesh->negative_volume_check = cmesh->set_from->negative_volume_check; From 9471765fc9c8e95c1ae1fc51fae6f9668eec3805 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:07:17 +0200 Subject: [PATCH 06/18] getter and setter for boundary condition names --- .../t8_cmesh_boundary_condition_handler.hxx | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx index 9bacbffb6c..337ebb4c76 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx @@ -150,6 +150,32 @@ struct t8_cmesh_boundary_condition_handler hashes.data (), sizeof (boundary_condition_hash) * hashes.size (), 0); } + /** + * Adds a boundary condition name to this handler without registering it to a tree. + * Mostly needed for debugging and testing reasons. + * Boundary conditions added via \ref add_boundary_conditions do not need to be registered explicitly. + * \tparam TString A string-like object. + * \param [in] boundary_condition The name of the boundary condition. + */ + template + requires std::convertible_to + inline void + register_boundary_condition (TString &&boundary_condition) + { + const std::string boundary_condition_string { boundary_condition }; + const boundary_condition_hash hash = hash_boundary_condition_name (boundary_condition_string); + + const auto inserted = m_boundary_conditions.try_emplace (hash, std::move (boundary_condition_string)); + +#if T8_ENABLE_DEBUG + if (inserted.second) { + const std::string_view boundary_condition_view = boundary_condition; + t8_debugf ("Registered boundary condition %.*s\n", static_cast (boundary_condition_view.size ()), + boundary_condition_view.data ()); + } +#endif + } + /** * Updates the internal cmesh. The boundary condition handler can only be given to uncommitted cmeshes. * \param [in] new_cmesh The new cmesh. @@ -255,6 +281,21 @@ struct t8_cmesh_boundary_condition_handler return std::nullopt; } + /** + * Get all registered boundary condition names. + * \return A vector containing all registered boundary condition names. + */ + inline std::vector + get_registered_boundary_conditions () const + { + std::vector boundary_conditions; + boundary_conditions.reserve (m_boundary_conditions.size ()); + for (const auto &boundary_condition : m_boundary_conditions) { + boundary_conditions.push_back (boundary_condition.second); + } + return boundary_conditions; + } + /**************************************** HELPER FUNCTIONS ****************************************/ #if T8_ENABLE_DEBUG From 012bf6268a0c480c826ae7f4980a412c6ce3c380 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:09:03 +0200 Subject: [PATCH 07/18] implement bcast for bc handler --- .../t8_cmesh_boundary_condition_handler.cxx | 63 +++++++++++++++++++ .../t8_cmesh_boundary_condition_handler.hxx | 31 +++++++++ 2 files changed, 94 insertions(+) diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx index 20800df38e..d001149903 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx @@ -30,6 +30,8 @@ #include #include +#include + using namespace detail; #if T8_ENABLE_DEBUG @@ -91,3 +93,64 @@ t8_cmesh_boundary_condition_handler::get_boundary_condition_attribute_key () con { return T8_CMESH_BOUNDARY_CONDITION_ATTRIBUTE_KEY; } + +std::vector +t8_cmesh_boundary_condition_handler::serialize_map () const +{ + std::vector serial_data; + /* Fill serial_data with strings only. The hashes can be re-generated locally. */ + for (const auto &[key, string] : t8_cmesh_boundary_condition_handler::m_boundary_conditions) { + serial_data.insert (serial_data.end (), string.begin (), string.end ()); + /* Null terminate strings to be able to split them again later. */ + serial_data.push_back ('\0'); + } + serial_data.shrink_to_fit (); + return serial_data; +} + +void +t8_cmesh_boundary_condition_handler::unpack_map (std::vector &serial_data, bool overwrite) +{ + if (overwrite) { + m_boundary_conditions.clear (); + } + + /* Iterate over stings. */ + for (const char *string = serial_data.data (); string < serial_data.data () + serial_data.size (); + string += std::strlen (string) + 1) { + /* Interpret c string as std::string, rehash it and insert it. */ + std::string value (string); + t8_cmesh_boundary_condition_handler::m_boundary_conditions.try_emplace ( + t8_cmesh_boundary_condition_handler::hash_boundary_condition_name (value), std::move (value)); + } +} + +void +t8_cmesh_boundary_condition_handler::bcast (int main_rank, sc_MPI_Comm comm) +{ + int rank; + sc_MPI_Comm_rank (comm, &rank); + + /* Buffer for sending and receiving */ + std::vector buffer; + + /* Serialize data. */ + if (rank == main_rank) { + buffer = t8_cmesh_boundary_condition_handler::serialize_map (); + } + + /* Prepare buffer length. */ + int size = static_cast (buffer.size ()); + sc_MPI_Bcast (&size, 1, sc_MPI_INT, main_rank, comm); + if (rank != main_rank) { + buffer.resize (size); + } + + /* Broadcast data */ + sc_MPI_Bcast (buffer.data (), size, sc_MPI_BYTE, main_rank, comm); + + /* Add data to local map. */ + if (rank != main_rank) { + t8_cmesh_boundary_condition_handler::unpack_map (buffer, true); + } +} diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx index 337ebb4c76..1a0737661d 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx @@ -370,8 +370,39 @@ struct t8_cmesh_boundary_condition_handler return m_boundary_conditions.at (hash); } + /**************************************** MPI HELPER FUNCTIONS ****************************************/ + + public: + + /** + * Broadcasts the boundary conditions from \a main_rank to all other ranks. + * \param [in] main_rank The main rank from which to broadcast. + * \param [in] comm The communicator to use. + */ + void + bcast (int main_rank, sc_MPI_Comm comm); + + /** + * Converts the contents of \ref m_boundary_conditions into a serial vector of chars. + * The keys are omitted and only the strings are serialized. + * In the serialized vector, the individual strings are null-terminated. + * \return The serialized map. + */ + std::vector + serialize_map () const; + + /** + * Unpacks and integrates the \a serial_data into \ref m_boundary_conditions. + * It either merges the already existing data or overwrites the complete map if \a overwrite is set to true. + * \param [in] serial_data The data to unpack and integrate. + * \param [in] overwrite Overwrites the data in this handler if true. Merges the data with the existing data on false. + */ + void + unpack_map (std::vector &serial_data, bool overwrite); + /**************************************** MEMBERS ****************************************/ + private: /** The associated cmesh of this struct */ t8_cmesh_t m_cmesh; From 91d836f9190c56149623cd6fea07859880299b9b Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:10:43 +0200 Subject: [PATCH 08/18] add bc handler bcast test --- .../t8_gtest_cmesh_boundary_conditions.cxx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx index aac4d4836d..9d79bca26f 100644 --- a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx +++ b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx @@ -324,3 +324,57 @@ TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_c_interface) t8_forest_unref (&forest); } + +/** + * Checks if the registered boundary conditions of the \a handler match with some \a testing_conditions. + * \param [in] handler The handler to check. + * \param [in] testing_conditions The conditions which should be registered in \a handler. + */ +static void +check_boundary_conditions (detail::t8_cmesh_boundary_condition_handler &handler, + std::vector testing_conditions) +{ + auto retrieved_conditions = handler.get_registered_boundary_conditions (); + ASSERT_EQ (retrieved_conditions.size (), testing_conditions.size ()); + + std::ranges::sort (retrieved_conditions); + std::ranges::sort (testing_conditions); + for (size_t i_bc = 0; i_bc < retrieved_conditions.size (); ++i_bc) { + EXPECT_EQ (retrieved_conditions[i_bc], testing_conditions[i_bc]); + } +} + +/** + * This test registers boundary conditions on rank 0 and broadcasts them to the other ranks. + * In the end we check, if every rank hast the boundary conditions. + */ +TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_broadcast) +{ + /* Get MPI info. */ + sc_MPI_Comm comm = sc_MPI_COMM_WORLD; + int rank; + sc_MPI_Comm_rank (comm, &rank); + + /* We create the same vector with 20 boundary conditions on each rank. */ + std::vector testing_boundary_conditions; + size_t num_boundary_conditions = 20; + testing_boundary_conditions.reserve (num_boundary_conditions); + for (size_t i_bc = 0; i_bc < num_boundary_conditions; ++i_bc) { + testing_boundary_conditions.emplace_back ("testing_boundary_condition_" + std::to_string (i_bc)); + } + + /* We create a handler and register the boundary conditions only on rank 0. */ + detail::t8_cmesh_boundary_condition_handler handler (nullptr); + if (rank == 0) { + for (const auto &boundary_condition : testing_boundary_conditions) { + handler.register_boundary_condition (boundary_condition); + } + } + + /* Broadcast the conditions. */ + handler.bcast (0, comm); + + /* Check if every rank has all conditions. */ + check_boundary_conditions (handler, testing_boundary_conditions); +} + From 8159832bb0447aa7a5653983a54d8322a2c6eb95 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:11:26 +0200 Subject: [PATCH 09/18] add synchronize function for bc handler --- src/t8.h | 1 + .../t8_cmesh_boundary_condition_handler.cxx | 58 +++++++++++++++++++ .../t8_cmesh_boundary_condition_handler.hxx | 6 ++ 3 files changed, 65 insertions(+) diff --git a/src/t8.h b/src/t8.h index 6528ce6e13..8a0bfd5b0a 100644 --- a/src/t8.h +++ b/src/t8.h @@ -161,6 +161,7 @@ typedef enum { T8_MPI_CMESH_UNIFORM_BOUNDS_END, /**< Used for cmesh uniform bounds computation. */ T8_MPI_TEST_ELEMENT_PACK_TAG, /**< Used for testing mpi pack and unpack functionality */ T8_MPI_PFC_TAG, /**< Used for data exchange during partition for coarsening. */ + T8_MPI_BOUNDARY_CONDITION_SYNC_TAG, /**< Used for the global synchronization of boundary conditions. */ T8_MPI_TAG_LAST /**< Dummy last MPI tag. */ } t8_MPI_tag_t; diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx index d001149903..dac349623d 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx @@ -25,6 +25,7 @@ * Implementation context of \ref t8_cmesh_boundary_condition_handler.hxx */ +#include #include #include #include @@ -125,6 +126,63 @@ t8_cmesh_boundary_condition_handler::unpack_map (std::vector &serial_data, } } +void +t8_cmesh_boundary_condition_handler::synchronize (sc_MPI_Comm comm) +{ + /* Use a bottom-up binomial tree merge approach instead of an MPI_Allgatherv to secure O(log(p)) scaling. + * In every level of the merge tree each rank with rank = rank & ~mask merges all information of rank = rank | mask. + * The rank = rank && ~mask then drops out of the communication pattern. + * The receiving rank also checks if there is a sender in the first place (src >= mpisize) for non-power of 2 mpisizes. + * + * After all data is collected, rank 0 broadcasts the collected data. + */ + + T8_ASSERT (comm != sc_MPI_COMM_NULL); + + int rank, mpisize; + sc_MPI_Comm_rank (comm, &rank); + sc_MPI_Comm_size (comm, &mpisize); + + if (mpisize == 1) { + /* Nothing to do. */ + return; + } + + /* Iterate over all levels of the merge tree. */ + for (int mask = 1; mask < mpisize; mask <<= 1) { + /* This rank receives a message. */ + if ((rank & mask) == 0) { + const int src = rank | mask; + if (src >= mpisize) { + /* There is no sender, so we have nothing to do on this level. */ + continue; + } + /* Probe the size of the incoming message. */ + sc_MPI_Status status; + sc_MPI_Probe (src, T8_MPI_BOUNDARY_CONDITION_SYNC_TAG, comm, &status); + int incoming_bytes; + sc_MPI_Get_count (&status, sc_MPI_BYTE, &incoming_bytes); + + /* Prepare buffer and receive data. */ + std::vector incoming (incoming_bytes); + sc_MPI_Recv (incoming.data (), incoming_bytes, sc_MPI_BYTE, src, T8_MPI_BOUNDARY_CONDITION_SYNC_TAG, comm, + sc_MPI_STATUS_IGNORE); + t8_cmesh_boundary_condition_handler::unpack_map (incoming, false); + } + /* This process sends a message and then drops out. */ + else { + const int dst = rank & ~mask; + const std::vector outgoing = serialize_map (); + sc_MPI_Send (const_cast (outgoing.data ()), static_cast (outgoing.size ()), sc_MPI_BYTE, dst, + T8_MPI_BOUNDARY_CONDITION_SYNC_TAG, comm); + /* Drop out. */ + break; + } + } + + t8_cmesh_boundary_condition_handler::bcast (0, comm); +} + void t8_cmesh_boundary_condition_handler::bcast (int main_rank, sc_MPI_Comm comm) { diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx index 1a0737661d..1ff72db53e 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx @@ -373,6 +373,12 @@ struct t8_cmesh_boundary_condition_handler /**************************************** MPI HELPER FUNCTIONS ****************************************/ public: + /** + * Synchronizes the contents of the boundary condition handler across all processes. + * \param [in] comm The communicator to use. + */ + void + synchronize (sc_MPI_Comm comm); /** * Broadcasts the boundary conditions from \a main_rank to all other ranks. From 31cd31c819cf4a5a85cda505b1bf01b34388bbdd Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:11:46 +0200 Subject: [PATCH 10/18] add synchronize test for bc handler --- .../t8_gtest_cmesh_boundary_conditions.cxx | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx index 9d79bca26f..f7181d3fb1 100644 --- a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx +++ b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx @@ -378,3 +378,50 @@ TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_broadcast) check_boundary_conditions (handler, testing_boundary_conditions); } +/** + * This test creates boundary conditions for each rank. + * Then we synchronize all bcs and check that all boundary conditions are available + * on all ranks. + */ +TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_synchronize) +{ + /* Get MPI info */ + sc_MPI_Comm comm = sc_MPI_COMM_WORLD; + int rank, mpisize; + sc_MPI_Comm_rank (comm, &rank); + sc_MPI_Comm_size (comm, &mpisize); + + /* Create the boundary conditions. + * For each rank, we create 6 bcs. + * The last rank gets no boundary conditions as a special testing case. + * Each rank gets his own 6 boundary conditions as well as 2 boundary conditions + * of the next rank. This way each rank (except the first, last and second to last) + * has 2 shared bcs with rank - 1, 2 unique bcs and 2 shared bcs with rank + 1. + * Rank 0 and mpisize - 2 have 4 unique bcs and 2 shared ones (if there are more than 2 ranks). + * Rank mpisize - 1 has no boundary conditions. */ + std::vector testing_boundary_conditions; + const size_t num_boundary_conditions_per_rank = 6; + /* Since the last rank gets no bcs we use mpisize - 1 and since every rank gets 2 additional bcs we also add 2. */ + size_t num_boundary_conditions = (mpisize - 1) * num_boundary_conditions_per_rank + 2; + testing_boundary_conditions.reserve (num_boundary_conditions); + for (size_t i_bc = 0; i_bc < num_boundary_conditions; ++i_bc) { + testing_boundary_conditions.emplace_back ("testing_boundary_condition_" + std::to_string (i_bc)); + } + + /* Assign all bcs. */ + detail::t8_cmesh_boundary_condition_handler handler (nullptr); + size_t bc_min = rank * num_boundary_conditions_per_rank; + size_t bc_max = (rank + 1) * num_boundary_conditions_per_rank + 2; + /* All ranks except the last one assign bcs. */ + if (rank != mpisize - 1) { + for (size_t i_bc = bc_min; i_bc < bc_max; ++i_bc) { + handler.register_boundary_condition (testing_boundary_conditions[i_bc]); + } + } + + /* Synchronize the conditions. */ + handler.synchronize (comm); + + /* Check if every rank has all conditions. */ + check_boundary_conditions (handler, testing_boundary_conditions); +} From 844babb4966c13b2e262082d3ec423da5e80969d Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Fri, 21 Aug 2026 16:22:26 +0200 Subject: [PATCH 11/18] fix documentation [run ci] --- .../internal/t8_cmesh_boundary_condition_handler.hxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx index 1ff72db53e..acbc5ba591 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx @@ -389,7 +389,7 @@ struct t8_cmesh_boundary_condition_handler bcast (int main_rank, sc_MPI_Comm comm); /** - * Converts the contents of \ref m_boundary_conditions into a serial vector of chars. + * Converts the contents of m_boundary_conditions into a serial vector of chars. * The keys are omitted and only the strings are serialized. * In the serialized vector, the individual strings are null-terminated. * \return The serialized map. @@ -398,7 +398,7 @@ struct t8_cmesh_boundary_condition_handler serialize_map () const; /** - * Unpacks and integrates the \a serial_data into \ref m_boundary_conditions. + * Unpacks and integrates the \a serial_data into m_boundary_conditions. * It either merges the already existing data or overwrites the complete map if \a overwrite is set to true. * \param [in] serial_data The data to unpack and integrate. * \param [in] overwrite Overwrites the data in this handler if true. Merges the data with the existing data on false. From a225c33e34da89d595d6d6c0fdfbce31ea882550 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Mon, 24 Aug 2026 10:30:07 +0200 Subject: [PATCH 12/18] maybe unused variable --- .../internal/t8_cmesh_boundary_condition_handler.hxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx index acbc5ba591..d0dd9389f5 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx @@ -136,7 +136,7 @@ struct t8_cmesh_boundary_condition_handler hashes.reserve (std::size (boundary_conditions)); for (const auto &boundary_condition : boundary_conditions) { const boundary_condition_hash hash = hash_boundary_condition_name (boundary_condition); - const auto inserted = m_boundary_conditions.try_emplace (hash, boundary_condition); + [[maybe_unused]] const auto inserted = m_boundary_conditions.try_emplace (hash, boundary_condition); #if T8_ENABLE_DEBUG if (inserted.second) { const std::string_view boundary_condition_view = boundary_condition; From ee1a9efb077fbd1040a147ed8f9f0072b52c2309 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Mon, 24 Aug 2026 15:03:57 +0200 Subject: [PATCH 13/18] make boundary condition sync test also work in serial --- .../t8_cmesh_boundary_condition_handler.hxx | 12 ++--- .../t8_cmesh_boundary_conditions.hxx | 2 +- src/t8_data/t8_static_vector.hxx | 44 +++++++++++++++++ .../t8_gtest_cmesh_boundary_conditions.cxx | 48 +++++++++++++++---- 4 files changed, 89 insertions(+), 17 deletions(-) diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx index d0dd9389f5..6c1c91ec09 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.hxx @@ -128,18 +128,18 @@ struct t8_cmesh_boundary_condition_handler * as the eclass of the cell has faces. */ template - requires std::convertible_to, std::string_view> + requires std::convertible_to, std::string_view> inline void add_boundary_conditions (t8_gloidx_t gtreeid, TStringRange boundary_conditions) { std::vector hashes; hashes.reserve (std::size (boundary_conditions)); for (const auto &boundary_condition : boundary_conditions) { - const boundary_condition_hash hash = hash_boundary_condition_name (boundary_condition); - [[maybe_unused]] const auto inserted = m_boundary_conditions.try_emplace (hash, boundary_condition); + const std::string_view boundary_condition_view = boundary_condition; + const boundary_condition_hash hash = hash_boundary_condition_name (boundary_condition_view); + [[maybe_unused]] const auto inserted = m_boundary_conditions.try_emplace (hash, boundary_condition_view); #if T8_ENABLE_DEBUG if (inserted.second) { - const std::string_view boundary_condition_view = boundary_condition; t8_debugf ("Registered boundary condition %.*s\n", static_cast (boundary_condition_view.size ()), boundary_condition_view.data ()); } @@ -337,9 +337,9 @@ struct t8_cmesh_boundary_condition_handler * \return The hash of the name. */ inline boundary_condition_hash - hash_boundary_condition_name (const std::string &boundary_condition_name) const + hash_boundary_condition_name (const std::string_view &boundary_condition_name) const { - return boundary_condition_hash (std::hash {}(boundary_condition_name)); + return boundary_condition_hash (std::hash {}(boundary_condition_name)); } /** diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/t8_cmesh_boundary_conditions.hxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/t8_cmesh_boundary_conditions.hxx index 8561fdc6d9..a61d72dce4 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/t8_cmesh_boundary_conditions.hxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/t8_cmesh_boundary_conditions.hxx @@ -45,7 +45,7 @@ * as the eclass of the cell has faces. */ template - requires std::convertible_to, std::string_view> + requires std::convertible_to, std::string_view> void t8_cmesh_set_boundary_conditions (t8_cmesh_t cmesh, t8_gloidx_t gtreeid, TStringRange boundary_conditions) { diff --git a/src/t8_data/t8_static_vector.hxx b/src/t8_data/t8_static_vector.hxx index 56f1895865..18a575ee0b 100644 --- a/src/t8_data/t8_static_vector.hxx +++ b/src/t8_data/t8_static_vector.hxx @@ -90,6 +90,25 @@ class t8_static_vector { } } + /** + * Creates a static vector from a range. + * + * \tparam TRange The type of the input range. + * \param [in] range The elements to store in the vector. + * + * \note The number of elements in the range must not exceed the vector capacity. + */ + template + requires std::convertible_to, TType> + constexpr t8_static_vector (TRange&& range) + { + T8_ASSERT (std::ranges::size (range) <= TCapacity); + + for (const auto& value : range) { + m_data[m_size++] = value; + } + } + /** * Returns the current number of elements stored in the vector. * @@ -292,6 +311,31 @@ class t8_static_vector { return *this; } + /** + * Assigns the contents of a range to the vector. + * + * \tparam TRange The type of the input range. + * \param [in] range The elements to copy into the vector. + * \return A reference to this vector. + * + * \note The number of elements in the range must not exceed the vector capacity. + */ + template + requires std::convertible_to, TType> + constexpr t8_static_vector& + operator= (TRange&& range) + { + T8_ASSERT (std::ranges::size (range) <= TCapacity); + + m_size = 0; + + for (const auto& value : range) { + m_data[m_size++] = value; + } + + return *this; + } + /** * Returns a reference to the element at the given index. * diff --git a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx index f7181d3fb1..c6ca4d8e6a 100644 --- a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx +++ b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx @@ -64,6 +64,13 @@ * Test fixture for the cmesh boundary condition module. It applies boundary conditions * to single tree cmeshes in accordance to their face number. */ + +/** Variable for setting hex boundary conditions. */ +constexpr std::array single_hex_bcs = { "bc_0", "bc_1", "bc_2", "bc_3", "bc_4", "bc_5" }; + +/** C Version of \ref single_hex_bcs. */ +const char *single_hex_bcs_c[6] = { "bc_0", "bc_1", "bc_2", "bc_3", "bc_4", "bc_5" }; + struct t8_cmesh_single_tree_bc: public testing::TestWithParam { protected: @@ -71,7 +78,7 @@ struct t8_cmesh_single_tree_bc: public testing::TestWithParam SetUp () override { eclass = GetParam (); - boundary_conditions = { "bc_0", "bc_1", "bc_2", "bc_3", "bc_4", "bc_5" }; + boundary_conditions = single_hex_bcs; boundary_conditions.resize (static_cast (t8_eclass_num_faces[eclass])); t8_cmesh_init (&cmesh); t8_cmesh_set_tree_class (cmesh, 0, eclass); @@ -85,7 +92,7 @@ struct t8_cmesh_single_tree_bc: public testing::TestWithParam t8_cmesh_unref (&cmesh); } - t8_boundary_conditions boundary_conditions; + t8_boundary_conditions boundary_conditions; t8_cmesh_t cmesh; t8_eclass eclass; }; @@ -243,14 +250,30 @@ TEST (t8_gtest_cmesh_boundary_conditions, test_hybrid_hypercube_boundary_conditi TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_c_interface) { /* Create a cmesh with one hex and apply boundary conditions */ - const char *boundary_conditions[6] = { "bc_0", "bc_1", "bc_2", "bc_3", "bc_4", "bc_5" }; t8_cmesh_t cmesh; t8_cmesh_init (&cmesh); t8_cmesh_set_tree_class (cmesh, 0, T8_ECLASS_HEX); - t8_cmesh_set_boundary_conditions (cmesh, 0, boundary_conditions, 6); + t8_cmesh_set_boundary_conditions (cmesh, 0, single_hex_bcs_c, 6); t8_cmesh_commit (cmesh, sc_MPI_COMM_WORLD); - /* Only check if the cmesh tree is local to our process. */ + /* Check if the cmesh tree is local to our process. */ + ASSERT_EQ (t8_cmesh_get_num_local_trees (cmesh), 1); + /* Some variables for retrieving and checking the boundary conditions. */ + const char *retrieved_boundary_conditions[6]; + const char *retrieved_single_boundary_condition; + size_t length = 0; + + /* Retrieve boundary conditions via t8_cmesh_get_boundary_conditions and t8_cmesh_get_boundary_condition() and check them. */ + t8_cmesh_get_boundary_conditions (cmesh, 0, retrieved_boundary_conditions, &length); + for (size_t i_boundary_condition = 0; i_boundary_condition < length; ++i_boundary_condition) { + /* Check t8_cmesh_get_boundary_conditions */ + EXPECT_STREQ (single_hex_bcs_c[i_boundary_condition], retrieved_boundary_conditions[i_boundary_condition]); + + /* Check t8_cmesh_get_boundary_condition */ + t8_cmesh_get_boundary_condition (cmesh, 0, i_boundary_condition, &retrieved_single_boundary_condition); + EXPECT_STREQ (single_hex_bcs_c[i_boundary_condition], retrieved_single_boundary_condition); + } + if (t8_cmesh_get_num_local_trees (cmesh)) { /* Some variables for retrieving and checking the boundary conditions. */ const char *retrieved_boundary_conditions[6]; @@ -261,11 +284,11 @@ TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_c_interface) t8_cmesh_get_boundary_conditions (cmesh, 0, retrieved_boundary_conditions, &length); for (size_t i_boundary_condition = 0; i_boundary_condition < length; ++i_boundary_condition) { /* Check t8_cmesh_get_boundary_conditions */ - EXPECT_STREQ (boundary_conditions[i_boundary_condition], retrieved_boundary_conditions[i_boundary_condition]); + EXPECT_STREQ (single_hex_bcs_c[i_boundary_condition], retrieved_boundary_conditions[i_boundary_condition]); /* Check t8_cmesh_get_boundary_condition */ t8_cmesh_get_boundary_condition (cmesh, 0, i_boundary_condition, &retrieved_single_boundary_condition); - EXPECT_STREQ (boundary_conditions[i_boundary_condition], retrieved_single_boundary_condition); + EXPECT_STREQ (single_hex_bcs_c[i_boundary_condition], retrieved_single_boundary_condition); } } @@ -315,8 +338,8 @@ TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_c_interface) ASSERT_TRUE (retrieved_boundary_conditions[iface] != nullptr); ASSERT_TRUE (retrieved_single_boundary_condition != nullptr); - EXPECT_STREQ (retrieved_boundary_conditions[iface], boundary_conditions[iface]); - EXPECT_STREQ (retrieved_single_boundary_condition, boundary_conditions[iface]); + EXPECT_STREQ (retrieved_boundary_conditions[iface], single_hex_bcs_c[iface]); + EXPECT_STREQ (retrieved_single_boundary_condition, single_hex_bcs_c[iface]); } } } @@ -403,6 +426,9 @@ TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_synchronize) const size_t num_boundary_conditions_per_rank = 6; /* Since the last rank gets no bcs we use mpisize - 1 and since every rank gets 2 additional bcs we also add 2. */ size_t num_boundary_conditions = (mpisize - 1) * num_boundary_conditions_per_rank + 2; + /* If we are serial we just apply 6 boundary conditions. */ + if (mpisize == 1) + num_boundary_conditions = num_boundary_conditions_per_rank; testing_boundary_conditions.reserve (num_boundary_conditions); for (size_t i_bc = 0; i_bc < num_boundary_conditions; ++i_bc) { testing_boundary_conditions.emplace_back ("testing_boundary_condition_" + std::to_string (i_bc)); @@ -412,8 +438,10 @@ TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_synchronize) detail::t8_cmesh_boundary_condition_handler handler (nullptr); size_t bc_min = rank * num_boundary_conditions_per_rank; size_t bc_max = (rank + 1) * num_boundary_conditions_per_rank + 2; + if (mpisize == 1) + bc_max = num_boundary_conditions_per_rank; /* All ranks except the last one assign bcs. */ - if (rank != mpisize - 1) { + if (rank != mpisize - 1 || mpisize == 1) { for (size_t i_bc = bc_min; i_bc < bc_max; ++i_bc) { handler.register_boundary_condition (testing_boundary_conditions[i_bc]); } From c20ebb13b79e2c55a7ec83cb5f0d6b768b5f5e20 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Mon, 24 Aug 2026 15:06:13 +0200 Subject: [PATCH 14/18] add boundary condition bcast to cmesh bcast --- src/t8_cmesh/t8_cmesh.cxx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/t8_cmesh/t8_cmesh.cxx b/src/t8_cmesh/t8_cmesh.cxx index 27c7e7cd79..73a6459993 100644 --- a/src/t8_cmesh/t8_cmesh.cxx +++ b/src/t8_cmesh/t8_cmesh.cxx @@ -686,7 +686,7 @@ t8_cmesh_bcast (const t8_cmesh_t cmesh_in, const int root, sc_MPI_Comm comm) t8_cmesh_struct_t cmesh; t8_gloidx_t num_trees_per_eclass[T8_ECLASS_COUNT]; size_t stash_elem_counts[3]; - int pre_commit; /* True, if cmesh on root is not committed yet. */ + int pre_commit; /** True if cmesh on root is not committed yet. */ #if T8_ENABLE_DEBUG sc_MPI_Comm comm; #endif @@ -768,6 +768,9 @@ t8_cmesh_bcast (const t8_cmesh_t cmesh_in, const int root, sc_MPI_Comm comm) if (meta_info.cmesh.profile != nullptr) { t8_cmesh_set_profiling (cmesh_in, 1); } + if (meta_info.cmesh.boundary_condition_handler != nullptr) { + t8_cmesh_add_boundary_condition_handler (cmesh_out); + } for (iclass = 0; iclass < T8_ECLASS_COUNT; iclass++) { cmesh_out->num_trees_per_eclass[iclass] = meta_info.num_trees_per_eclass[iclass]; cmesh_out->num_local_trees_per_eclass[iclass] = meta_info.num_trees_per_eclass[iclass]; @@ -793,6 +796,8 @@ t8_cmesh_bcast (const t8_cmesh_t cmesh_in, const int root, sc_MPI_Comm comm) cmesh_out->committed = 1; } } + /* Broadcast boundary conditions */ + cmesh_out->boundary_condition_handler->bcast (root, comm); cmesh_out->mpirank = mpirank; cmesh_out->mpisize = mpisize; From af73b82c841cf0af5a554692275d0708a35e07ec Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Mon, 24 Aug 2026 15:07:01 +0200 Subject: [PATCH 15/18] add cmesh bcast test for boundary conditions --- .../t8_gtest_cmesh_boundary_conditions.cxx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx index c6ca4d8e6a..87ad7ecdf9 100644 --- a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx +++ b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx @@ -453,3 +453,37 @@ TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_synchronize) /* Check if every rank has all conditions. */ check_boundary_conditions (handler, testing_boundary_conditions); } + +/** + * We create a cmesh on one rank and broadcast it. The bcs should be available on all ranks. + */ +TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_broadcasted_cmesh) +{ + /* Get MPI info */ + sc_MPI_Comm comm = sc_MPI_COMM_WORLD; + int rank, mpisize; + sc_MPI_Comm_rank (comm, &rank); + sc_MPI_Comm_size (comm, &mpisize); + + /* Create cmesh and apply boundary conditions just on rank 0. */ + const t8_boundary_conditions boundary_conditions = { "bc_0", "bc_1", "bc_2", "bc_3", "bc_4", "bc_5" }; + t8_cmesh_t cmesh = NULL; + if (rank == 0) { + t8_cmesh_init (&cmesh); + t8_cmesh_set_tree_class (cmesh, 0, T8_ECLASS_HEX); + t8_cmesh_set_boundary_conditions (cmesh, 0, boundary_conditions); + } + cmesh = t8_cmesh_bcast (cmesh, 0, comm); + t8_cmesh_commit (cmesh, comm); + + ASSERT_EQ (t8_cmesh_get_num_local_trees (cmesh), 1); + /* Retrieve and test boundary conditions on all ranks. */ + const auto retrieved_boundary_conditions = t8_cmesh_get_boundary_conditions (cmesh, 0); + ASSERT_EQ (retrieved_boundary_conditions.size (), single_hex_bcs.size ()); + for (size_t i_boundary_condition = 0; i_boundary_condition < single_hex_bcs.size (); ++i_boundary_condition) { + /* Check t8_cmesh_get_boundary_conditions */ + EXPECT_EQ (single_hex_bcs[i_boundary_condition], retrieved_boundary_conditions[i_boundary_condition]); + } + t8_cmesh_destroy (&cmesh); +} + From 6520df794a51571fd2bfd23264d44e9e604bf4fa Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Mon, 24 Aug 2026 15:08:41 +0200 Subject: [PATCH 16/18] add boundary condition synchronization to partitioned commit --- src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx b/src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx index 6e752480aa..68d7f9845d 100644 --- a/src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx +++ b/src/t8_cmesh/t8_cmesh_internal/t8_cmesh_commit.cxx @@ -504,6 +504,18 @@ t8_cmesh_commit_partitioned_new (t8_cmesh_t cmesh, sc_MPI_Comm comm) } sc_MPI_Allreduce (&id1, &cmesh->num_trees, 1, T8_MPI_GLOIDX, sc_MPI_SUM, comm); + /* Communicate the boundary conditions. + * Since some processes can have no trees, + * we have to communicate if boundary conditions were applied first. */ + int boundary_conditions_applied_locally = cmesh->boundary_condition_handler != nullptr; + int boundary_conditions_applied_globally = 0; + sc_MPI_Allreduce (&boundary_conditions_applied_locally, &boundary_conditions_applied_globally, 1, sc_MPI_INT, + sc_MPI_MAX, comm); + if (boundary_conditions_applied_globally && cmesh->boundary_condition_handler == nullptr) { + t8_cmesh_add_boundary_condition_handler (cmesh); + } + cmesh->boundary_condition_handler->synchronize (comm); + #if T8_ENABLE_DEBUG sc_flops_shot (&fi, &snapshot); sc_stats_set1 (&stats[2], snapshot.iwtime, "cmesh_commit_end"); From 95c6726b7282a847d359fea4b75fd87bfe5ab073 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Mon, 24 Aug 2026 15:09:07 +0200 Subject: [PATCH 17/18] add test for partitioned cmesh boundary conditions --- .../t8_gtest_cmesh_boundary_conditions.cxx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx index 87ad7ecdf9..4ee9d1cda1 100644 --- a/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx +++ b/test/t8_cmesh/t8_gtest_cmesh_boundary_conditions.cxx @@ -487,3 +487,77 @@ TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_broadcasted_cm t8_cmesh_destroy (&cmesh); } +/** + * We create a distributed cmesh on multiple ranks. The bcs are distributed as well. + * After committing, the boundary conditions should be available on every rank. + */ +TEST (t8_gtest_cmesh_boundary_conditions, test_boundary_condition_distributed_cmesh) +{ + /* Get MPI info */ + sc_MPI_Comm comm = sc_MPI_COMM_WORLD; + int rank, mpisize; + sc_MPI_Comm_rank (comm, &rank); + sc_MPI_Comm_size (comm, &mpisize); + + /* The three boundary conditions test three things: + * 1. test_boundary_condition is equal on every rank and should not be duplicated. + * 2. tree_id: gtreeid is unique to each process and tree and should be available everywhere afterwards. + * 3. rank: rank is unique to each rank an tests, that every rank contributed his part. */ + const t8_boundary_conditions boundary_conditions + = { "test_boundary_condition", "tree_id: ", "rank: " + std::to_string (rank) }; + + /* Every rank gets 5 trees except the last one, which gets 0. + * If there is only one rank, it still gets 5 trees. */ + constexpr size_t num_trees_per_rank = 5; + t8_gloidx_t min_tree_id; + t8_gloidx_t max_tree_id; + if (mpisize == 1 || rank != mpisize - 1) { + min_tree_id = rank * num_trees_per_rank; + max_tree_id = (rank + 1) * num_trees_per_rank - 1; + } + else { + min_tree_id = 1; /* min tree id has to be bigger than max tree id for t8_cmesh_set_partition_range */ + max_tree_id = 0; + } + + /* Create the distributed cmesh. */ + t8_cmesh_t cmesh; + t8_cmesh_init (&cmesh); + t8_cmesh_set_dimension (cmesh, 2); + for (t8_gloidx_t itree = min_tree_id; itree <= max_tree_id; ++itree) { + t8_cmesh_set_tree_class (cmesh, itree, T8_ECLASS_TRIANGLE); + auto current_bcs = boundary_conditions; + current_bcs[1] += std::to_string (itree); + t8_cmesh_set_boundary_conditions (cmesh, itree, current_bcs); + } + t8_cmesh_set_partition_range (cmesh, 3, min_tree_id, max_tree_id); + t8_cmesh_commit (cmesh, comm); + + /* Retrieve boundary conditions and compute how many there should be. */ + auto registered_boundary_conditions + = t8_cmesh_get_boundary_condition_handler (cmesh)->get_registered_boundary_conditions (); + std::ranges::sort (registered_boundary_conditions); + const t8_gloidx_t num_trees_in_boundary_conditions + = mpisize == 1 ? num_trees_per_rank : (mpisize - 1) * num_trees_per_rank; + const t8_gloidx_t num_ranks_in_boundary_conditions = mpisize == 1 ? 1 : mpisize - 1; + + /* Check the common boundary condition. */ + ASSERT_FALSE (std::find (registered_boundary_conditions.begin (), registered_boundary_conditions.end (), + "test_boundary_condition") + == registered_boundary_conditions.end ()); + + /* Check that all bcs from all ranks (except the last one) are there. */ + for (int irank = 0; irank < num_ranks_in_boundary_conditions; ++irank) { + ASSERT_FALSE (std::find (registered_boundary_conditions.begin (), registered_boundary_conditions.end (), + "rank: " + std::to_string (irank)) + == registered_boundary_conditions.end ()); + } + + /* Check that all tree id bcs are there. */ + for (int itree = 0; itree < num_trees_in_boundary_conditions; ++itree) { + ASSERT_FALSE (std::find (registered_boundary_conditions.begin (), registered_boundary_conditions.end (), + "tree_id: " + std::to_string (itree)) + == registered_boundary_conditions.end ()); + } + t8_cmesh_destroy (&cmesh); +} From 4e30c873c227479c20921a1b58268673e15b5717 Mon Sep 17 00:00:00 2001 From: Sandro Elsweijer Date: Mon, 24 Aug 2026 15:27:58 +0200 Subject: [PATCH 18/18] use MPI_Allgatherv instead of own implementation --- .../t8_cmesh_boundary_condition_handler.cxx | 60 +++++++------------ 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx index dac349623d..0313efc18b 100644 --- a/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx +++ b/src/t8_cmesh/t8_cmesh_boundary_conditions/internal/t8_cmesh_boundary_condition_handler.cxx @@ -129,14 +129,6 @@ t8_cmesh_boundary_condition_handler::unpack_map (std::vector &serial_data, void t8_cmesh_boundary_condition_handler::synchronize (sc_MPI_Comm comm) { - /* Use a bottom-up binomial tree merge approach instead of an MPI_Allgatherv to secure O(log(p)) scaling. - * In every level of the merge tree each rank with rank = rank & ~mask merges all information of rank = rank | mask. - * The rank = rank && ~mask then drops out of the communication pattern. - * The receiving rank also checks if there is a sender in the first place (src >= mpisize) for non-power of 2 mpisizes. - * - * After all data is collected, rank 0 broadcasts the collected data. - */ - T8_ASSERT (comm != sc_MPI_COMM_NULL); int rank, mpisize; @@ -148,39 +140,29 @@ t8_cmesh_boundary_condition_handler::synchronize (sc_MPI_Comm comm) return; } - /* Iterate over all levels of the merge tree. */ - for (int mask = 1; mask < mpisize; mask <<= 1) { - /* This rank receives a message. */ - if ((rank & mask) == 0) { - const int src = rank | mask; - if (src >= mpisize) { - /* There is no sender, so we have nothing to do on this level. */ - continue; - } - /* Probe the size of the incoming message. */ - sc_MPI_Status status; - sc_MPI_Probe (src, T8_MPI_BOUNDARY_CONDITION_SYNC_TAG, comm, &status); - int incoming_bytes; - sc_MPI_Get_count (&status, sc_MPI_BYTE, &incoming_bytes); - - /* Prepare buffer and receive data. */ - std::vector incoming (incoming_bytes); - sc_MPI_Recv (incoming.data (), incoming_bytes, sc_MPI_BYTE, src, T8_MPI_BOUNDARY_CONDITION_SYNC_TAG, comm, - sc_MPI_STATUS_IGNORE); - t8_cmesh_boundary_condition_handler::unpack_map (incoming, false); - } - /* This process sends a message and then drops out. */ - else { - const int dst = rank & ~mask; - const std::vector outgoing = serialize_map (); - sc_MPI_Send (const_cast (outgoing.data ()), static_cast (outgoing.size ()), sc_MPI_BYTE, dst, - T8_MPI_BOUNDARY_CONDITION_SYNC_TAG, comm); - /* Drop out. */ - break; - } + /* Prepare the sendbuffer. */ + const std::vector send_buffer = t8_cmesh_boundary_condition_handler::serialize_map (); + + /* Communicate the local sizes */ + const int local_size = static_cast (send_buffer.size ()); + std::vector sizes (mpisize); + MPI_Allgather (&local_size, 1, sc_MPI_INT, sizes.data (), 1, sc_MPI_INT, comm); + + /* Compute the offsets for the data and create receive buffer. */ + std::vector offsets (mpisize); + int total_size = 0; + for (int rank = 0; rank < mpisize; ++rank) { + offsets[rank] = total_size; + total_size += sizes[rank]; } + std::vector recv_buffer (total_size); + + /* Communicate. */ + MPI_Allgatherv (send_buffer.data (), local_size, MPI_BYTE, recv_buffer.data (), sizes.data (), offsets.data (), + MPI_BYTE, comm); - t8_cmesh_boundary_condition_handler::bcast (0, comm); + /* Unpack the data. */ + t8_cmesh_boundary_condition_handler::unpack_map (recv_buffer, false); } void